Cum să construiți o aplicație de procesare a limbajului natural
Publicat: 2022-03-11Procesarea limbajului natural – o tehnologie care permite aplicațiilor software să proceseze limbajul uman – a devenit destul de omniprezentă în ultimii câțiva ani.
Căutarea Google este din ce în ce mai capabilă să răspundă la întrebări care sună natural, Siri de la Apple este capabil să înțeleagă o mare varietate de întrebări și tot mai multe companii folosesc (în mod rezonabil) roboti de chat și telefon inteligenti pentru a comunica cu clienții. Dar cum funcționează cu adevărat acest software aparent „inteligent”?
În acest articol, veți afla despre tehnologia care face ca aceste aplicații să funcționeze și veți învăța cum să dezvoltați propriul software de procesare a limbajului natural.
Articolul vă va ghida prin procesul exemplu de construire a unui analizator de relevanță pentru știri. Imaginați-vă că aveți un portofoliu de acțiuni și doriți ca o aplicație să acceseze automat site-urile de știri populare și să identifice articolele care sunt relevante pentru portofoliul dvs. De exemplu, dacă portofoliul dvs. de acțiuni include companii precum Microsoft, BlackStone și Luxottica, ați dori să vedeți articole care menționează aceste trei companii.
Noțiuni introductive cu biblioteca Stanford NLP
Aplicațiile de procesare a limbajului natural, ca orice alte aplicații de învățare automată, sunt construite pe o serie de algoritmi relativ mici, simpli și intuitivi care lucrează în tandem. Este adesea logic să folosiți o bibliotecă externă în care toți acești algoritmi sunt deja implementați și integrați.
Pentru exemplul nostru, vom folosi biblioteca Stanford NLP, o bibliotecă puternică de procesare a limbajului natural, bazată pe Java, care vine cu suport pentru multe limbi.
Un algoritm special din această bibliotecă care ne interesează este etichetatorul POS (part-of-speech). Un etichetator POS este folosit pentru a atribui automat părți de vorbire fiecărui cuvânt dintr-o bucată de text. Acest etichetator POS clasifică cuvintele în text pe baza caracteristicilor lexicale și le analizează în relație cu alte cuvinte din jurul lor.
Mecanica exactă a algoritmului de etichetare POS depășește scopul acestui articol, dar puteți afla mai multe despre el aici.
Pentru a începe, vom crea un nou proiect Java (puteți folosi IDE-ul dvs. preferat) și vom adăuga biblioteca Stanford NLP la lista de dependențe. Dacă utilizați Maven, adăugați-l pur și simplu în fișierul pom.xml
:
<dependency> <groupId>edu.stanford.nlp</groupId> <artifactId>stanford-corenlp</artifactId> <version>3.6.0</version> </dependency> <dependency> <groupId>edu.stanford.nlp</groupId> <artifactId>stanford-corenlp</artifactId> <version>3.6.0</version> <classifier>models</classifier> </dependency>
Deoarece aplicația va trebui să extragă automat conținutul unui articol dintr-o pagină web, va trebui să specificați și următoarele două dependențe:
<dependency> <groupId>de.l3s.boilerpipe</groupId> <artifactId>boilerpipe</artifactId> <version>1.1.0</version> </dependency>
<dependency> <groupId>net.sourceforge.nekohtml</groupId> <artifactId>nekohtml</artifactId> <version>1.9.22</version> </dependency>
Cu aceste dependențe adăugate, sunteți gata să mergeți mai departe.
Articole de răzuit și curățare
Prima parte a analizorului nostru va implica preluarea articolelor și extragerea conținutului acestora din paginile web.
La preluarea articolelor din surse de știri, paginile sunt de obicei pline de informații străine (videoclipuri încorporate, link-uri de ieșire, videoclipuri, reclame etc.) care sunt irelevante pentru articolul în sine. Aici intervine Boilerpipe.
Boilerpipe este un algoritm extrem de robust și eficient pentru eliminarea „dezordinii” care identifică conținutul principal al unui articol de știri prin analiza diferitelor blocuri de conținut folosind caracteristici precum lungimea unei propoziții medii, tipurile de etichete utilizate în blocurile de conținut și densitatea link-urilor. Algoritmul boilerpipe s-a dovedit a fi competitiv cu alți algoritmi mult mai scumpi din punct de vedere computațional, cum ar fi cei bazați pe viziunea artificială. Puteți afla mai multe pe site-ul proiectului său.
Biblioteca Boilerpipe vine cu suport încorporat pentru răzuirea paginilor web. Poate prelua codul HTML de pe web, poate extrage text din HTML și poate curăța textul extras. Puteți defini o funcție, extractFromURL
, care va lua o adresă URL și va folosi Boilerpipe pentru a returna cel mai relevant text ca șir folosind ArticleExtractor
pentru această sarcină:
import java.net.URL; import de.l3s.boilerpipe.document.TextDocument; import de.l3s.boilerpipe.extractors.CommonExtractors; import de.l3s.boilerpipe.sax.BoilerpipeSAXInput; import de.l3s.boilerpipe.sax.HTMLDocument; import de.l3s.boilerpipe.sax.HTMLFetcher; public class BoilerPipeExtractor { public static String extractFromUrl(String userUrl) throws java.io.IOException, org.xml.sax.SAXException, de.l3s.boilerpipe.BoilerpipeProcessingException { final HTMLDocument htmlDoc = HTMLFetcher.fetch(new URL(userUrl)); final TextDocument doc = new BoilerpipeSAXInput(htmlDoc.toInputSource()).getTextDocument(); return CommonExtractors.ARTICLE_EXTRACTOR.getText(doc); } }
Biblioteca Boilerpipe oferă diferite extractoare bazate pe algoritmul boilerpipe, ArticleExtractor
fiind optimizat special pentru articolele de știri în format HTML. ArticleExtractor
se concentrează în mod special pe etichetele HTML utilizate în fiecare bloc de conținut și pe densitatea linkurilor de ieșire. Acesta este mai potrivit pentru sarcina noastră decât DefaultExtractor
, mai rapid, dar mai simplu.
Funcțiile încorporate se ocupă de totul pentru noi:
-
HTMLFetcher.fetch
primește documentul HTML -
getTextDocument
extrage documentul text -
CommonExtractors.ARTICLE_EXTRACTOR.getText
extrage textul relevant din articol folosind algoritmul boilerpipe
Acum o puteți încerca cu un articol exemplu despre fuziunile giganților optici Essilor și Luxottica, pe care îl găsiți aici. Puteți introduce această adresă URL în funcție și puteți vedea ce iese.
Adăugați următorul cod la funcția principală:
public class App { public static void main( String[] args ) throws java.io.IOException, org.xml.sax.SAXException, de.l3s.boilerpipe.BoilerpipeProcessingException { String urlString = "http://www.reuters.com/article/us-essilor-ma-luxottica-group-idUSKBN14Z110"; String text = BoilerPipeExtractor.extractFromUrl(urlString); System.out.println(text); } }
Ar trebui să vedeți în rezultatul dvs. în corpul principal al articolului, fără reclame, etichete HTML și link-uri de ieșire. Iată fragmentul de început din ceea ce am primit când am rulat asta:
MILAN/PARIS Italy's Luxottica (LUX.MI) and France's Essilor (ESSI.PA) have agreed a 46 billion euro ($49 billion) merger to create a global eyewear powerhouse with annual revenue of more than 15 billion euros. The all-share deal is one of Europe's largest cross-border tie-ups and brings together Luxottica, the world's top spectacles maker with brands such as Ray-Ban and Oakley, with leading lens manufacturer Essilor. "Finally ... two products which are naturally complementary -- namely frames and lenses -- will be designed, manufactured and distributed under the same roof," Luxottica's 81-year-old founder Leonardo Del Vecchio said in a statement on Monday. Shares in Luxottica were up by 8.6 percent at 53.80 euros by 1405 GMT (9:05 am ET), with Essilor up 12.2 percent at 114.60 euros. The merger between the top players in the 95 billion eyewear market is aimed at helping the businesses to take full advantage of expected strong demand for prescription spectacles and sunglasses due to an aging global population and increasing awareness about eye care. Jefferies analysts estimate that the market is growing at between...
Și acesta este într-adevăr corpul principal al articolului. Este greu de imaginat că acest lucru este mult mai simplu de implementat.
Etichetarea părților de vorbire
Acum că ați extras cu succes corpul principal al articolului, puteți lucra pentru a determina dacă articolul menționează companii care sunt de interes pentru utilizator.
Este posibil să fiți tentat să faceți pur și simplu o căutare de șir sau expresii regulate, dar există mai multe dezavantaje ale acestei abordări.
În primul rând, o căutare de șiruri poate fi predispusă la rezultate false pozitive. Un articol care menționează Microsoft Excel poate fi etichetat ca menționând Microsoft, de exemplu.
În al doilea rând, în funcție de construcția expresiei regulate, o căutare a expresiei regulate poate duce la fals negative. De exemplu, un articol care conține expresia „Câștigurile trimestriale ale Luxottica au depășit așteptările” poate fi ratat de o căutare cu expresii regulate care caută „Luxottica” înconjurat de spații albe.
În cele din urmă, dacă sunteți interesat de un număr mare de companii și procesați un număr mare de articole, căutarea în întregul corp al textului pentru fiecare companie din portofoliul utilizatorului se poate dovedi extrem de consumatoare de timp, oferind performanțe inacceptabile.
Biblioteca CoreNLP din Stanford are multe caracteristici puternice și oferă o modalitate de a rezolva toate aceste trei probleme.
Pentru analizorul nostru, vom folosi etichetatorul Parts-of-Speech (POS). În special, putem folosi etichetatorul POS pentru a găsi toate substantivele proprii din articol și pentru a le compara cu portofoliul nostru de acțiuni interesante.
Prin încorporarea tehnologiei NLP, nu numai că îmbunătățim acuratețea etichetării noastre și minimizăm falsele pozitive și negative menționate mai sus, dar și minimizăm dramatic cantitatea de text pe care trebuie să o comparăm cu portofoliul nostru de acțiuni, deoarece substantivele proprii cuprind doar un mic subset. a textului integral al articolului.

Prin preprocesarea portofoliului nostru într-o structură de date care are un cost scăzut de interogare de membru, putem reduce dramatic timpul necesar analizării unui articol.
Stanford CoreNLP oferă un etichetator foarte convenabil numit MaxentTagger care poate oferi etichetare POS în doar câteva linii de cod.
Iată o implementare simplă:
public class PortfolioNewsAnalyzer { private HashSet<String> portfolio; private static final String modelPath = "edu\\stanford\\nlp\\models\\pos-tagger\\english-left3words\\english-left3words-distsim.tagger"; private MaxentTagger tagger; public PortfolioNewsAnalyzer() { tagger = new MaxentTagger(modelPath); } public String tagPos(String input) { return tagger.tagString(input); }
Funcția de etichetare, tagPos
, ia un șir ca intrare și scoate un șir care conține cuvintele din șirul original împreună cu partea corespunzătoare de vorbire. În funcția principală, instanțiați un PortfolioNewsAnalyzer
și introduceți rezultatul scraper-ului în funcția de etichetare și ar trebui să vedeți ceva de genul acesta:
MILAN/PARIS_NN Italy_NNP 's_POS Luxottica_NNP -LRB-_-LRB- LUX.MI_NNP -RRB-_-RRB- and_CC France_NNP 's_POS Essilor_NNP -LRB-_-LRB- ESSI.PA_NNP -RRB-_-RRB- have_VBP agreed_VBN a_DT 46_CD billion_CD euro_NN -LRB-_-LRB- $_$ 49_CD billion_CD -RRB-_-RRB- merger_NN to_TO create_VB a_DT global_JJ eyewear_NN powerhouse_NN with_IN annual_JJ revenue_NN of_IN more_JJR than_IN 15_CD billion_CD euros_NNS ._. The_DT all-share_JJ deal_NN is_VBZ one_CD of_IN Europe_NNP 's_POS largest_JJS cross-border_JJ tie-ups_NNS and_CC brings_VBZ together_RB Luxottica_NNP ,_, the_DT world_NN 's_POS top_JJ spectacles_NNS maker_NN with_IN brands_NNS such_JJ as_IN Ray-Ban_NNP and_CC Oakley_NNP ,_, with_IN leading_VBG lens_NN manufacturer_NN Essilor_NNP ._. ``_`` Finally_RB ..._: two_CD products_NNS which_WDT are_VBP naturally_RB complementary_JJ --_: namely_RB frames_NNS and_CC lenses_NNS --_: will_MD be_VB designed_VBN ,_, manufactured_VBN and_CC distributed_VBN under_IN the_DT same_JJ roof_NN ,_, ''_'' Luxottica_NNP 's_POS 81-year-old_JJ founder_NN Leonardo_NNP Del_NNP Vecchio_NNP said_VBD in_IN a_DT statement_NN on_IN Monday_NNP ._. Shares_NNS in_IN Luxottica_NNP were_VBD up_RB by_IN 8.6_CD percent_NN at_IN 53.80_CD euros_NNS by_IN 1405_CD GMT_NNP -LRB-_-LRB- 9:05_CD am_NN ET_NNP -RRB-_-RRB- ,_, with_IN Essilor_NNP up_IN 12.2_CD percent_NN at_IN 114.60_CD euros_NNS ._. The_DT merger_NN between_IN the_DT top_JJ players_NNS in_IN the_DT 95_CD billion_CD eyewear_NN market_NN is_VBZ aimed_VBN at_IN helping_VBG the_DT businesses_NNS to_TO take_VB full_JJ advantage_NN of_IN expected_VBN strong_JJ demand_NN for_IN prescription_NN spectacles_NNS and_CC sunglasses_NNS due_JJ to_TO an_DT aging_NN global_JJ population_NN and_CC increasing_VBG awareness_NN about_IN...
Procesarea ieșirii etichetate într-un set
Până acum, am creat funcții pentru a descărca, curăța și eticheta un articol de știri. Dar mai trebuie să stabilim dacă articolul menționează vreuna dintre companiile de interes pentru utilizator.
Pentru a face acest lucru, trebuie să colectăm toate substantivele proprii și să verificăm dacă stocurile din portofoliul nostru sunt incluse în acele substantive proprii.
Pentru a găsi toate substantivele proprii, vom dori mai întâi să împărțim ieșirea șirului etichetat în jetoane (folosind spații ca delimitatori), apoi împărțim fiecare dintre jetoane pe liniuța de subliniere ( _
) și să verificăm dacă partea de vorbire este un substantiv propriu. .
Odată ce avem toate substantivele proprii, vom dori să le stocăm într-o structură de date care este mai bine optimizată pentru scopul nostru. Pentru exemplul nostru, vom folosi un HashSet
. În schimbul interzicerii intrărilor duplicate și a nu ține evidența ordinii intrărilor, HashSet
permite interogări foarte rapide de abonare. Deoarece ne interesează doar să solicităm calitatea de membru, HashSet
este perfect pentru scopurile noastre.
Mai jos este funcția care implementează împărțirea și stocarea substantivelor proprii. Plasați această funcție în clasa dvs. PortfolioNewsAnalyzer
:
public static HashSet<String> extractProperNouns(String taggedOutput) { HashSet<String> propNounSet = new HashSet<String>(); String[] split = taggedOutput.split(" "); for (String token: split ){ String[] splitTokens = token.split("_"); if(splitTokesn[1].equals("NNP")){ propNounSet.add(splitTokens[0]); } } return propNounSet; }
Există totuși o problemă cu această implementare. Dacă numele unei companii constă din mai multe cuvinte (de exemplu, Carl Zeiss în exemplul Luxottica), această implementare nu va putea să-l surprindă. În exemplul lui Carl Zeiss, „Carl” și „Zeiss” vor fi introduse în set separat și, prin urmare, nu vor conține niciodată șirul unic „Carl Zeiss”.
Pentru a rezolva această problemă, putem colecta toate substantivele proprii consecutive și le unim cu spații. Iată implementarea actualizată care realizează acest lucru:
public static HashSet<String> extractProperNouns(String taggedOutput) { HashSet<String> propNounSet = new HashSet<String>(); String[] split = taggedOutput.split(" "); List<String> propNounList = new ArrayList<String>(); for (String token: split ){ String[] splitTokens = token.split("_"); if(splitTokens[1].equals("NNP")){ propNounList.add(splitTokens[0]); } else { if (!propNounList.isEmpty()) { propNounSet.add(StringUtils.join(propNounList, " ")); propNounList.clear(); } } } if (!propNounList.isEmpty()) { propNounSet.add(StringUtils.join(propNounList, " ")); propNounList.clear(); } return propNounSet; }
Acum funcția ar trebui să returneze un set cu substantivele proprii individuale și substantivele proprii consecutive (adică, unite prin spații). Dacă imprimați propNounSet
, ar trebui să vedeți ceva de genul următor:
[... Monday, Gianluca Semeraro, David Goodman, Delfin, North America, Luxottica, Latin America, Rossi/File Photo, Rome, Safilo Group, SFLG.MI, Friday, Valentina Za, Del Vecchio, CEO Hubert Sagnieres, Oakley, Sagnieres, Jefferies, Ray Ban, ...]
Compararea portofoliului cu setul de PropNouns
Aproape am terminat!
În secțiunile anterioare, am construit un scraper care poate descărca și extrage corpul unui articol, un tagger care poate analiza corpul articolului și poate identifica substantivele proprii și un procesor care preia rezultatul etichetat și colectează substantivele proprii într-un HashSet
. Acum tot ce mai rămâne de făcut este să luăm HashSet
-ul și să îl comparăm cu lista de companii care ne interesează.
Implementarea este foarte simplă. Adăugați următorul cod în clasa PortfolioNewsAnalyzer
:
private HashSet<String> portfolio; public PortfolioNewsAnalyzer() { portfolio = new HashSet<String>(); } public void addPortfolioCompany(String company) { portfolio.add(company); } public boolean arePortfolioCompaniesMentioned(HashSet<String> articleProperNouns){ return !Collections.disjoint(articleProperNouns, portfolio); }
Punând totul împreună
Acum putem rula întreaga aplicație - răzuirea, curățarea, etichetarea, colectarea și compararea. Iată funcția care rulează prin întreaga aplicație. Adăugați această funcție la clasa dvs. PortfolioNewsAnalyzer
:
public boolean analyzeArticle(String urlString) throws IOException, SAXException, BoilerpipeProcessingException { String articleText = extractFromUrl(urlString); String tagged = tagPos(articleText); HashSet<String> properNounsSet = extractProperNouns(tagged); return arePortfolioCompaniesMentioned(properNounsSet); }
În sfârșit, putem folosi aplicația!
Iată un exemplu folosind același articol ca mai sus și Luxottica ca companie de portofoliu:
public static void main( String[] args ) throws IOException, SAXException, BoilerpipeProcessingException { PortfolioNewsAnalyzer analyzer = new PortfolioNewsAnalyzer(); analyzer.addPortfolioCompany("Luxottica"); boolean mentioned = analyzer.analyzeArticle("http://www.reuters.com/article/us-essilor-ma-luxottica-group-idUSKBN14Z110"); if (mentioned) { System.out.println("Article mentions portfolio companies"); } else { System.out.println("Article does not mention portfolio companies"); } }
Rulați acest lucru și aplicația ar trebui să afișeze „Articolul menționează companiile din portofoliu”.
Schimbați compania din portofoliu de la Luxottica într-o companie care nu este menționată în articol (cum ar fi „Microsoft”), iar aplicația ar trebui să afișeze „Articolul nu menționează companiile din portofoliu”.
Construirea unei aplicații NLP nu trebuie să fie dificilă
În acest articol, am parcurs procesul de construire a unei aplicații care descarcă un articol dintr-o adresă URL, îl curăță folosind Boilerpipe, îl procesează folosind Stanford NLP și verifică dacă articolul face referințe specifice de interes (în cazul nostru, companiile noastre portofoliu). După cum sa demonstrat, valorificarea acestei game de tehnologii transformă ceea ce altfel ar fi o sarcină descurajantă într-una relativ simplă.
Sper că acest articol v-a introdus în concepte și tehnici utile în procesarea limbajului natural și că v-a inspirat să scrieți propriile aplicații în limbajul natural.
[Notă: puteți găsi o copie a codului la care se face referire în acest articol aici.]