Jak zbudować aplikację do przetwarzania języka naturalnego
Opublikowany: 2022-03-11Przetwarzanie języka naturalnego — technologia, która pozwala aplikacjom przetwarzać język ludzki — stało się dość wszechobecne w ciągu ostatnich kilku lat.
Wyszukiwarka Google jest coraz bardziej zdolna do odpowiadania na naturalnie brzmiące pytania, Siri firmy Apple jest w stanie zrozumieć szeroką gamę pytań, a coraz więcej firm używa (rozsądnie) inteligentnych botów czatowych i telefonicznych do komunikowania się z klientami. Ale jak naprawdę działa to pozornie „inteligentne” oprogramowanie?
W tym artykule poznasz technologię, która sprawia, że te aplikacje działają, a także dowiesz się, jak tworzyć własne oprogramowanie do przetwarzania języka naturalnego.
Artykuł przeprowadzi Cię przez przykładowy proces tworzenia analizatora istotności wiadomości. Wyobraź sobie, że masz portfel akcji i chcesz, aby aplikacja automatycznie przeszukiwała popularne witryny z wiadomościami i identyfikowała artykuły, które pasują do Twojego portfela. Na przykład, jeśli Twój portfel akcji obejmuje firmy takie jak Microsoft, BlackStone i Luxottica, warto zapoznać się z artykułami wymieniającymi te trzy firmy.
Pierwsze kroki z Biblioteką Stanford NLP
Aplikacje do przetwarzania języka naturalnego, podobnie jak inne aplikacje do uczenia maszynowego, są zbudowane na wielu stosunkowo małych, prostych, intuicyjnych algorytmach działających w tandemie. Często warto skorzystać z zewnętrznej biblioteki, w której wszystkie te algorytmy są już zaimplementowane i zintegrowane.
W naszym przykładzie użyjemy biblioteki Stanford NLP, potężnej biblioteki przetwarzania języka naturalnego opartej na Javie, która obsługuje wiele języków.
Jednym z konkretnych algorytmów z tej biblioteki, który nas interesuje, jest tagger części mowy (POS). Tagger POS służy do automatycznego przypisywania części mowy do każdego słowa w fragmencie tekstu. Ten tager POS klasyfikuje słowa w tekście na podstawie cech leksykalnych i analizuje je w odniesieniu do innych słów wokół nich.
Dokładna mechanika algorytmu taggera POS wykracza poza zakres tego artykułu, ale możesz dowiedzieć się więcej na ten temat tutaj.
Na początek stworzymy nowy projekt Java (możesz użyć swojego ulubionego IDE) i dodamy bibliotekę Stanford NLP do listy zależności. Jeśli używasz Mavena, po prostu dodaj go do swojego pliku 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>
Ponieważ aplikacja będzie musiała automatycznie wyodrębnić zawartość artykułu ze strony internetowej, musisz również określić następujące dwie zależności:
<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>
Po dodaniu tych zależności możesz iść naprzód.
Artykuły do skrobania i czyszczenia
Pierwsza część naszego analizatora będzie polegać na pobieraniu artykułów i wyodrębnianiu ich treści ze stron internetowych.
Podczas pobierania artykułów ze źródeł wiadomości strony są zwykle pełne obcych informacji (osadzone filmy, linki wychodzące, filmy, reklamy itp.), które nie mają związku z samym artykułem. Tutaj do gry wkracza Boilerpipe.
Boilerpipe to niezwykle solidny i wydajny algorytm do usuwania „bałaganu”, który identyfikuje główną treść artykułu z wiadomościami, analizując różne bloki treści przy użyciu takich funkcji, jak długość przeciętnego zdania, typy tagów używanych w blokach treści i gęstość linków. Algorytm kotła rurowego okazał się konkurencyjny w stosunku do innych algorytmów znacznie bardziej kosztownych obliczeniowo, takich jak te oparte na wizji maszynowej. Więcej informacji można znaleźć na stronie projektu.
Biblioteka Boilerpipe ma wbudowaną obsługę skrobania stron internetowych. Może pobierać kod HTML z sieci, wyodrębniać tekst z kodu HTML i czyścić wyodrębniony tekst. Możesz zdefiniować funkcję extractFromURL
, która przyjmie adres URL i użyje Boilerpipe do zwrócenia najbardziej odpowiedniego tekstu jako ciągu za pomocą ArticleExtractor
do tego zadania:
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); } }
Biblioteka Boilerpipe zapewnia różne ekstraktory oparte na algorytmie boilerpipe, a ArticleExtractor
jest specjalnie zoptymalizowany pod kątem artykułów w formacie HTML. ArticleExtractor
koncentruje się w szczególności na znacznikach HTML używanych w każdym bloku treści i gęstości linków wychodzących. Jest to lepiej dopasowane do naszego zadania niż szybszy, ale prostszy DefaultExtractor
.
Wbudowane funkcje dbają o wszystko za nas:
-
HTMLFetcher.fetch
pobiera dokument HTML -
getTextDocument
wyodrębnia dokument tekstowy -
CommonExtractors.ARTICLE_EXTRACTOR.getText
wyodrębnia odpowiedni tekst z artykułu za pomocą algorytmu boilerpipe
Teraz możesz to wypróbować z przykładowym artykułem dotyczącym fuzji gigantów optycznych Essilor i Luxottica, który znajdziesz tutaj. Możesz podać ten adres URL do funkcji i zobaczyć, co wyjdzie.
Dodaj następujący kod do swojej funkcji głównej:
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); } }
Powinieneś zobaczyć w swoim wyniku w głównej części artykułu, bez reklam, tagów HTML i linków wychodzących. Oto fragment początkowy z tego, co otrzymałem, gdy uruchomiłem to:
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 to jest rzeczywiście główna treść artykułu. Trudno sobie wyobrazić, że byłoby to znacznie prostsze do wdrożenia.
Oznaczanie części mowy
Po pomyślnym wyodrębnieniu głównej treści artykułu możesz pracować nad ustaleniem, czy artykuł zawiera informacje o firmach, które są interesujące dla użytkownika.
Możesz ulec pokusie, aby po prostu wyszukać ciąg znaków lub wyrażenie regularne, ale takie podejście ma kilka wad.
Po pierwsze, wyszukiwanie ciągów może być podatne na fałszywe alarmy. Na przykład artykuł, który wspomina o programie Microsoft Excel, może zostać oznaczony jako wymieniający firmę Microsoft.
Po drugie, w zależności od konstrukcji wyrażenia regularnego, wyszukiwanie za pomocą wyrażenia regularnego może prowadzić do wyników fałszywie ujemnych. Na przykład artykuł zawierający wyrażenie „kwartalne zarobki Luxottica przekroczyły oczekiwania” może zostać pominięty w wyszukiwaniu wyrażenia regularnego, które wyszukuje hasło „Luxottica” otoczone białymi spacjami.
Wreszcie, jeśli interesuje Cię duża liczba firm i przetwarzasz dużą liczbę artykułów, przeszukanie całego tekstu dla każdej firmy z portfolio użytkownika może okazać się niezwykle czasochłonne, przynosząc nieakceptowalną wydajność.
Biblioteka CoreNLP Stanforda ma wiele zaawansowanych funkcji i umożliwia rozwiązanie wszystkich trzech problemów.
W naszym analizatorze użyjemy tagera Parts-of-Speech (POS). W szczególności możemy użyć tagera POS, aby znaleźć wszystkie właściwe rzeczowniki w artykule i porównać je z naszym portfelem interesujących akcji.
Włączając technologię NLP, nie tylko poprawiamy dokładność naszego tagera i minimalizujemy fałszywie dodatnie i ujemne wyniki, o których mowa powyżej, ale także radykalnie minimalizujemy ilość tekstu, który musimy porównać z naszym portfelem akcji, ponieważ nazwy własne stanowią tylko mały podzbiór pełnego tekstu artykułu.

Dzięki wstępnemu przetwarzaniu naszego portfolio w strukturę danych, która ma niski koszt zapytań dotyczących członkostwa, możemy radykalnie skrócić czas potrzebny na analizę artykułu.
Stanford CoreNLP zapewnia bardzo wygodny tagger o nazwie MaxentTagger, który może zapewnić tagowanie POS w zaledwie kilku wierszach kodu.
Oto prosta implementacja:
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); }
Funkcja tagger, tagPos
, pobiera ciąg znaków jako dane wejściowe i wyprowadza ciąg, który zawiera słowa w oryginalnym ciągu wraz z odpowiednią częścią mowy. W swojej głównej funkcji stwórz instancję PortfolioNewsAnalyzer
i przekaż dane wyjściowe skrobaka do funkcji taggera, a powinieneś zobaczyć coś takiego:
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...
Przetwarzanie oznakowanego wyjścia w zestaw
Do tej pory opracowaliśmy funkcje pobierania, czyszczenia i oznaczania artykułów z wiadomościami. Ale nadal musimy ustalić, czy artykuł wspomina o którejkolwiek z firm interesujących użytkownika.
Aby to zrobić, musimy zebrać wszystkie nazwy własne i sprawdzić, czy akcje z naszego portfela są uwzględnione w tych rzeczownikach własnych.
Aby znaleźć wszystkie właściwe rzeczowniki, najpierw podzielimy znakowany łańcuch wyjściowy na tokeny (używając spacji jako ograniczników), a następnie podzielimy każdy z tokenów podkreślenia ( _
) i sprawdzimy, czy część mowy jest rzeczownikiem własnym .
Gdy mamy już wszystkie właściwe rzeczowniki, będziemy chcieli przechowywać je w strukturze danych, która jest lepiej zoptymalizowana do naszych celów. W naszym przykładzie użyjemy HashSet
. W zamian za uniemożliwienie zduplikowanych wpisów i brak śledzenia kolejności wpisów, HashSet
umożliwia bardzo szybkie zapytania o członkostwo. Ponieważ interesuje nas tylko wysyłanie zapytań o członkostwo, HashSet
jest idealny do naszych celów.
Poniżej znajduje się funkcja realizująca dzielenie i przechowywanie rzeczowników własnych. Umieść tę funkcję w swojej klasie 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; }
Jest jednak problem z tą implementacją. Jeśli nazwa firmy składa się z wielu słów (np. Carl Zeiss w przykładzie Luxottica), ta implementacja nie będzie w stanie jej przechwycić. W przykładzie Carl Zeiss „Carl” i „Zeiss” zostaną wstawione do zestawu osobno i dlatego nigdy nie będą zawierać pojedynczego ciągu „Carl Zeiss”.
Aby rozwiązać ten problem, możemy zebrać wszystkie kolejne rzeczowniki własne i połączyć je spacjami. Oto zaktualizowana implementacja, która to umożliwia:
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; }
Teraz funkcja powinna zwrócić zbiór z poszczególnymi rzeczownikami własnymi i kolejnymi rzeczownikami własnymi (tj. połączone spacjami). Jeśli wydrukujesz propNounSet
, powinieneś zobaczyć coś takiego:
[... 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, ...]
Porównanie portfela z zestawem PropNouns
Prawie skończyliśmy!
W poprzednich sekcjach zbudowaliśmy scraper, który może pobrać i wyodrębnić treść artykułu, tagger, który może przeanalizować treść artykułu i zidentyfikować właściwe rzeczowniki, oraz procesor, który pobiera otagowane dane wyjściowe i zbiera właściwe rzeczowniki w HashSet
. Teraz pozostaje tylko wziąć HashSet
i porównać go z listą interesujących nas firm.
Implementacja jest bardzo prosta. Dodaj następujący kod do swojej klasy 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); }
Kładąc wszystko razem
Teraz możemy uruchomić całą aplikację — skrobanie, czyszczenie, tagowanie, zbieranie i porównywanie. Oto funkcja, która działa w całej aplikacji. Dodaj tę funkcję do swojej klasy 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); }
Wreszcie możemy korzystać z aplikacji!
Oto przykład z wykorzystaniem tego samego artykułu co powyżej i Luxottica jako spółki portfelowej:
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"); } }
Uruchom to, a aplikacja powinna wydrukować „Artykuł wspomina o spółkach portfelowych”.
Zmień spółkę portfelową z Luxottica na spółkę niewymienioną w artykule (np. „Microsoft”), a aplikacja powinna wydrukować „Artykuł nie wspomina o spółkach portfelowych”.
Tworzenie aplikacji NLP nie musi być trudne
W tym artykule przeszliśmy przez proces tworzenia aplikacji, która pobiera artykuł z adresu URL, czyści go za pomocą Boilerpipe, przetwarza go za pomocą Stanford NLP i sprawdza, czy artykuł zawiera konkretne odniesienia (w naszym przypadku firmy w naszym teczka). Jak pokazano, wykorzystanie tej gamy technologii sprawia, że to, co w innym przypadku byłoby zniechęcającym zadaniem, staje się stosunkowo proste.
Mam nadzieję, że ten artykuł przybliżył Ci przydatne koncepcje i techniki przetwarzania języka naturalnego i zainspirował Cię do napisania własnych aplikacji w języku naturalnym.
[Uwaga: kopię kodu, o którym mowa w tym artykule, można znaleźć tutaj.]