Как создать приложение для обработки естественного языка

Опубликовано: 2022-03-11

Обработка естественного языка — технология, которая позволяет программным приложениям обрабатывать человеческий язык — за последние несколько лет стала широко распространенной.

Поиск Google все чаще способен отвечать на естественно звучащие вопросы, Siri от Apple способна понимать широкий спектр вопросов, и все больше и больше компаний используют (в разумных пределах) интеллектуальных чатов и телефонных ботов для общения с клиентами. Но как на самом деле работает это, казалось бы, «умное» программное обеспечение?

В этой статье вы узнаете о технологии, благодаря которой работают эти приложения, и узнаете, как разработать собственное программное обеспечение для обработки естественного языка.

Эта статья проведет вас через пример процесса создания анализатора релевантности новостей. Представьте, что у вас есть портфель акций, и вы хотите, чтобы приложение автоматически сканировало популярные новостные веб-сайты и выявляло статьи, имеющие отношение к вашему портфолио. Например, если в ваш портфель акций входят такие компании, как Microsoft, BlackStone и Luxottica, вы хотели бы видеть статьи, в которых упоминаются эти три компании.

Начало работы со Стэнфордской библиотекой НЛП

Приложения для обработки естественного языка, как и любые другие приложения для машинного обучения, основаны на ряде относительно небольших, простых, интуитивно понятных алгоритмов, работающих в тандеме. Часто имеет смысл использовать внешнюю библиотеку, в которой все эти алгоритмы уже реализованы и интегрированы.

В нашем примере мы будем использовать библиотеку Stanford NLP, мощную библиотеку обработки естественного языка на основе Java, которая поставляется с поддержкой многих языков.

Один конкретный алгоритм из этой библиотеки, который нас интересует, — это тегировщик частей речи (POS). Теггер POS используется для автоматического присвоения частей речи каждому слову в фрагменте текста. Этот POS-теггер классифицирует слова в тексте на основе лексических признаков и анализирует их по отношению к другим словам вокруг них.

Точная механика алгоритма POS-тегера выходит за рамки этой статьи, но вы можете узнать о ней больше здесь.

Для начала мы создадим новый Java-проект (вы можете использовать вашу любимую IDE) и добавим библиотеку Stanford NLP в список зависимостей. Если вы используете Maven, просто добавьте его в свой файл 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>

Поскольку приложению потребуется автоматически извлекать содержимое статьи с веб-страницы, вам также потребуется указать следующие две зависимости:

 <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>

После добавления этих зависимостей вы готовы двигаться вперед.

Предметы для соскабливания и очистки

Первая часть нашего анализатора будет включать в себя поиск статей и извлечение их содержимого с веб-страниц.

При извлечении статей из новостных источников страницы обычно пронизаны посторонней информацией (встроенные видео, исходящие ссылки, видеоролики, реклама и т. д.), не относящейся к самой статье. Здесь в игру вступает Boilerpipe.

Boilerpipe — чрезвычайно надежный и эффективный алгоритм для удаления «беспорядка», который идентифицирует основной контент новостной статьи путем анализа различных блоков контента с использованием таких характеристик, как длина среднего предложения, типы тегов, используемых в блоках контента, и плотность ссылок. Алгоритм котельной трубы доказал свою конкурентоспособность по сравнению с другими гораздо более дорогостоящими в вычислительном отношении алгоритмами, например, основанными на машинном зрении. Вы можете узнать больше на сайте проекта.

Библиотека Boilerpipe поставляется со встроенной поддержкой парсинга веб-страниц. Он может извлекать HTML из Интернета, извлекать текст из HTML и очищать извлеченный текст. Вы можете определить функцию, extractFromURL , которая будет принимать URL-адрес и использовать Boilerpipe для возврата наиболее релевантного текста в виде строки, используя для этой задачи ArticleExtractor :

 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); } }

Библиотека Boilerpipe предоставляет различные экстракторы, основанные на алгоритме бойлерпайп, причем ArticleExtractor специально оптимизирован для новостных статей в формате HTML. ArticleExtractor уделяет особое внимание тегам HTML, используемым в каждом блоке контента, и плотности исходящих ссылок. Это лучше подходит для нашей задачи, чем более быстрый, но простой DefaultExtractor .

Встроенные функции позаботятся обо всем за нас:

  • HTMLFetcher.fetch получает HTML-документ
  • getTextDocument извлекает текстовый документ
  • CommonExtractors.ARTICLE_EXTRACTOR.getText извлекает соответствующий текст из статьи, используя алгоритм бойлерпайпа.

Теперь вы можете попробовать это на примере статьи о слиянии оптических гигантов Essilor и Luxottica, которую вы можете найти здесь. Вы можете передать этот URL-адрес функции и посмотреть, что получится.

Добавьте следующий код в вашу основную функцию:

 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); } }

Вы должны увидеть в своем выводе основную часть статьи без рекламы, тегов HTML и исходящих ссылок. Вот начальный фрагмент того, что я получил, когда запустил это:

 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...

И это действительно основная часть статьи. Трудно представить, что это намного проще реализовать.

Пометка частей речи

Теперь, когда вы успешно извлекли основной текст статьи, вы можете определить, упоминаются ли в статье компании, представляющие интерес для пользователя.

У вас может возникнуть соблазн просто выполнить поиск по строке или регулярному выражению, но у этого подхода есть несколько недостатков.

Прежде всего, строковый поиск может быть подвержен ложным срабатываниям. Например, статья, в которой упоминается Microsoft Excel, может быть помечена как упоминающая Microsoft.

Во-вторых, в зависимости от конструкции регулярного выражения поиск по регулярному выражению может привести к ложным отрицательным результатам. Например, статья, содержащая фразу «Квартальные доходы Luxottica превзошли ожидания», может быть пропущена при поиске по регулярному выражению, который ищет «Luxottica» в окружении пробелов.

Наконец, если вас интересует большое количество компаний и вы обрабатываете большое количество статей, поиск по всему тексту каждой компании в портфолио пользователя может занять очень много времени и привести к неприемлемой производительности.

Библиотека Стэнфорда CoreNLP обладает множеством мощных функций и позволяет решить все три проблемы.

Для нашего анализатора мы будем использовать теггер частей речи (POS). В частности, мы можем использовать POS-теггер, чтобы найти все имена собственные в статье и сравнить их с нашим портфелем интересных акций.

Внедряя технологию NLP, мы не только повышаем точность нашего тега и минимизируем ложные срабатывания и отрицательные значения, упомянутые выше, но также значительно уменьшаем объем текста, который нам нужно сравнивать с нашим портфелем акций, поскольку имена собственные составляют лишь небольшое подмножество. полного текста статьи.

Предварительно преобразовав наше портфолио в структуру данных с низкой стоимостью запроса членства, мы можем значительно сократить время, необходимое для анализа статьи.

Stanford CoreNLP предоставляет очень удобный тегировщик под названием MaxentTagger, который может обеспечить тегирование POS всего несколькими строками кода.

Вот простая реализация:

 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); }

Функция тега, tagPos , принимает строку в качестве входных данных и выводит строку, содержащую слова исходной строки вместе с соответствующей частью речи. В вашей основной функции создайте экземпляр PortfolioNewsAnalyzer и передайте выходные данные скребка в функцию tagger, и вы должны увидеть что-то вроде этого:

 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...

Преобразование помеченного вывода в набор

На данный момент мы создали функции для загрузки, очистки и пометки новостной статьи. Но нам еще нужно определить, упоминается ли в статье какая-либо из интересующих пользователя компаний.

Для этого нам нужно собрать все имена собственные и проверить, входят ли акции из нашего портфеля в эти имена собственные.

Чтобы найти все имена собственные, мы сначала хотим разделить вывод строки с тегами на токены (используя пробелы в качестве разделителей), затем разделить каждый из токенов на символ подчеркивания ( _ ) и проверить, является ли часть речи именем собственным. .

Как только у нас будут все имена собственные, мы захотим сохранить их в структуре данных, которая лучше оптимизирована для нашей цели. В нашем примере мы будем использовать HashSet . В обмен на запрет повторяющихся записей и отсутствие отслеживания порядка записей HashSet позволяет очень быстро выполнять запросы на членство. Поскольку нас интересует только запрос на членство, HashSet идеально подходит для наших целей.

Ниже представлена ​​функция, реализующая разбиение и запоминание имен собственных. Поместите эту функцию в свой класс 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; }

Однако есть проблема с этой реализацией. Если название компании состоит из нескольких слов (например, Carl Zeiss в примере с Luxottica), эта реализация не сможет его уловить. В примере с Carl Zeiss «Carl» и «Zeiss» будут вставлены в набор по отдельности и, следовательно, никогда не будут содержать одну строку «Carl Zeiss».

Чтобы решить эту проблему, мы можем собрать все идущие подряд имена собственные и соединить их пробелами. Вот обновленная реализация, которая выполняет это:

 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; }

Теперь функция должна возвращать набор с отдельными именами собственными и последовательными именами собственными (т. е. соединенными пробелами). Если вы напечатаете propNounSet , вы должны увидеть что-то вроде следующего:

 [... 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, ...]

Сравнение портфолио с набором PropNouns

Мы почти закончили!

В предыдущих разделах мы создали парсер, который может загружать и извлекать тело статьи, тегировщик, который может анализировать тело статьи и идентифицировать имена собственные, и процессор, который принимает помеченные выходные данные и собирает имена собственные в HashSet . Теперь все, что осталось сделать, это взять HashSet и сравнить его со списком интересующих нас компаний.

Реализация очень проста. Добавьте следующий код в свой класс 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); }

Собираем все вместе

Теперь мы можем запустить все приложение — парсинг, очистку, тегирование, сбор и сравнение. Вот функция, которая проходит через все приложение. Добавьте эту функцию в свой класс 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); }

Наконец, мы можем использовать приложение!

Вот пример использования той же статьи, что и выше, и Luxottica в качестве портфельной компании:

 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"); } }

Запустите это, и приложение должно напечатать «В статье упоминаются портфельные компании».

Измените портфельную компанию с Luxottica на компанию, не упомянутую в статье (например, «Microsoft»), и приложение должно напечатать «В статье не упоминаются портфельные компании».

Создание приложения НЛП не должно быть сложным

В этой статье мы рассмотрели процесс создания приложения, которое загружает статью с URL-адреса, очищает ее с помощью Boilerpipe, обрабатывает ее с помощью Stanford NLP и проверяет, содержит ли статья конкретные интересные ссылки (в нашем случае компании в нашем портфолио). Как показано, использование этого набора технологий превращает то, что в противном случае казалось бы сложной задачей, в относительно простую.

Я надеюсь, что эта статья познакомила вас с полезными концепциями и методами обработки естественного языка и вдохновила вас на написание собственных приложений на естественном языке.

[Примечание: вы можете найти копию кода, упомянутого в этой статье, здесь.]