Como construir um aplicativo de processamento de linguagem natural
Publicados: 2022-03-11O processamento de linguagem natural – uma tecnologia que permite que aplicativos de software processem a linguagem humana – tornou-se bastante onipresente nos últimos anos.
A pesquisa do Google é cada vez mais capaz de responder a perguntas que soam naturais, a Siri da Apple é capaz de entender uma ampla variedade de perguntas e mais e mais empresas estão usando (razoavelmente) chats inteligentes e bots de telefone para se comunicar com os clientes. Mas como esse software aparentemente “inteligente” realmente funciona?
Neste artigo, você aprenderá sobre a tecnologia que faz esses aplicativos funcionarem e aprenderá a desenvolver seu próprio software de processamento de linguagem natural.
O artigo o guiará pelo processo de exemplo de construção de um analisador de relevância de notícias. Imagine que você tem um portfólio de ações e deseja que um aplicativo rastreie automaticamente sites de notícias populares e identifique artigos relevantes para seu portfólio. Por exemplo, se seu portfólio de ações inclui empresas como Microsoft, BlackStone e Luxottica, você gostaria de ver artigos que mencionam essas três empresas.
Introdução à biblioteca de PNL de Stanford
Os aplicativos de processamento de linguagem natural, como qualquer outro aplicativo de aprendizado de máquina, são criados com base em vários algoritmos relativamente pequenos, simples e intuitivos que trabalham em conjunto. Muitas vezes faz sentido usar uma biblioteca externa onde todos esses algoritmos já estejam implementados e integrados.
Para nosso exemplo, usaremos a biblioteca Stanford NLP, uma poderosa biblioteca de processamento de linguagem natural baseada em Java que vem com suporte para muitas linguagens.
Um algoritmo específico desta biblioteca que nos interessa é o tagger de parte de fala (POS). Um tagger POS é usado para atribuir automaticamente partes do discurso a cada palavra em um pedaço de texto. Este tagger POS classifica palavras em texto com base em recursos lexicais e as analisa em relação a outras palavras ao seu redor.
A mecânica exata do algoritmo do tagger POS está além do escopo deste artigo, mas você pode aprender mais sobre isso aqui.
Para começar, vamos criar um novo projeto Java (você pode usar seu IDE favorito) e adicionar a biblioteca Stanford NLP à lista de dependências. Se você estiver usando o Maven, basta adicioná-lo ao seu arquivo 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>Como o aplicativo precisará extrair automaticamente o conteúdo de um artigo de uma página da Web, você também precisará especificar as duas dependências a seguir:
<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>Com essas dependências adicionadas, você está pronto para seguir em frente.
Artigos de raspagem e limpeza
A primeira parte do nosso analisador envolverá a recuperação de artigos e a extração de seu conteúdo de páginas da web.
Ao recuperar artigos de fontes de notícias, as páginas geralmente estão repletas de informações estranhas (vídeos incorporados, links externos, vídeos, anúncios etc.) que são irrelevantes para o próprio artigo. Este é o lugar onde Boilerpipe entra em jogo.
Boilerpipe é um algoritmo extremamente robusto e eficiente para remover “desordem” que identifica o conteúdo principal de uma notícia analisando diferentes blocos de conteúdo usando recursos como tamanho médio de uma frase, tipos de tags usados em blocos de conteúdo e densidade de links. O algoritmo boilerpipe provou ser competitivo com outros algoritmos computacionalmente muito mais caros, como aqueles baseados em visão de máquina. Você pode saber mais no site do projeto.
A biblioteca Boilerpipe vem com suporte embutido para raspagem de páginas da web. Ele pode buscar o HTML da web, extrair texto do HTML e limpar o texto extraído. Você pode definir uma função, extractFromURL , que pegará uma URL e usará Boilerpipe para retornar o texto mais relevante como uma string usando ArticleExtractor para esta tarefa:
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); } } A biblioteca Boilerpipe fornece diferentes extratores baseados no algoritmo do boilerpipe, com ArticleExtractor sendo especificamente otimizado para artigos de notícias formatados em HTML. ArticleExtractor se concentra especificamente em tags HTML usadas em cada bloco de conteúdo e densidade de links de saída. Isso é mais adequado para nossa tarefa do que o DefaultExtractor mais rápido, mas mais simples.
As funções integradas cuidam de tudo para nós:
-
HTMLFetcher.fetchobtém o documento HTML -
getTextDocumentextrai o documento de texto -
CommonExtractors.ARTICLE_EXTRACTOR.getTextextrai o texto relevante do artigo usando o algoritmo boilerpipe
Agora você pode experimentá-lo com um artigo de exemplo sobre as fusões dos gigantes ópticos Essilor e Luxottica, que você pode encontrar aqui. Você pode alimentar este URL para a função e ver o que sai.
Adicione o seguinte código à sua função 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); } }Você deve ver sua saída no corpo principal do artigo, sem os anúncios, tags HTML e links externos. Aqui está o trecho inicial do que eu obtive quando executei isso:
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...E esse é de fato o corpo principal do artigo. Difícil imaginar isso sendo muito mais simples de implementar.
Marcando partes do discurso
Agora que você extraiu com sucesso o corpo do artigo principal, pode trabalhar para determinar se o artigo menciona empresas de interesse do usuário.
Você pode ficar tentado a simplesmente fazer uma pesquisa de string ou expressão regular, mas há várias desvantagens nessa abordagem.
Em primeiro lugar, uma pesquisa de string pode ser propensa a falsos positivos. Um artigo que menciona o Microsoft Excel pode ser marcado como mencionando a Microsoft, por exemplo.
Em segundo lugar, dependendo da construção da expressão regular, uma pesquisa de expressão regular pode levar a falsos negativos. Por exemplo, um artigo que contém a frase “Os ganhos trimestrais da Luxottica superaram as expectativas” pode ser perdido por uma pesquisa de expressão regular que pesquisa “Luxottica” cercada por espaços em branco.
Finalmente, se você estiver interessado em um grande número de empresas e estiver processando um grande número de artigos, pesquisar em todo o corpo do texto por todas as empresas do portfólio do usuário pode ser extremamente demorado, resultando em um desempenho inaceitável.
A biblioteca CoreNLP de Stanford tem muitos recursos poderosos e fornece uma maneira de resolver esses três problemas.
Para o nosso analisador, usaremos o tagger Parts-of-Speech (POS). Em particular, podemos usar o marcador POS para encontrar todos os nomes próprios no artigo e compará-los com nosso portfólio de ações interessantes.
Ao incorporar a tecnologia de PNL, não apenas melhoramos a precisão de nosso tagger e minimizamos falsos positivos e negativos mencionados acima, mas também minimizamos drasticamente a quantidade de texto que precisamos para comparar com nosso portfólio de ações, uma vez que os nomes próprios compreendem apenas um pequeno subconjunto do texto completo do artigo.

Ao pré-processar nosso portfólio em uma estrutura de dados com baixo custo de consulta de associação, podemos reduzir drasticamente o tempo necessário para analisar um artigo.
Stanford CoreNLP fornece um tagger muito conveniente chamado MaxentTagger que pode fornecer POS Tagging em apenas algumas linhas de código.
Aqui está uma implementação simples:
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); } A função tagger, tagPos , recebe uma string como entrada e gera uma string que contém as palavras na string original junto com a parte do discurso correspondente. Em sua função principal, instancie um PortfolioNewsAnalyzer e alimente a saída do scraper na função do tagger e você deverá ver algo assim:
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...Processando a saída marcada em um conjunto
Até agora, criamos funções para baixar, limpar e marcar um artigo de notícias. Mas ainda precisamos determinar se o artigo menciona alguma das empresas de interesse do usuário.
Para isso, precisamos coletar todos os nomes próprios e verificar se as ações do nosso portfólio estão incluídas nesses nomes próprios.
Para encontrar todos os nomes próprios, primeiro queremos dividir a saída da string marcada em tokens (usando espaços como delimitadores), depois dividir cada um dos tokens no sublinhado ( _ ) e verificar se a parte do discurso é um nome próprio .
Uma vez que tenhamos todos os nomes próprios, vamos querer armazená-los em uma estrutura de dados que seja melhor otimizada para nosso propósito. Para nosso exemplo, usaremos um HashSet . Em troca de não permitir entradas duplicadas e não acompanhar a ordem das entradas, HashSet permite consultas de associação muito rápidas. Como estamos interessados apenas em consultar membros, o HashSet é perfeito para nossos propósitos.
Abaixo está a função que implementa a divisão e armazenamento de nomes próprios. Coloque esta função em sua classe 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; }Há um problema com esta implementação embora. Se o nome de uma empresa consistir em várias palavras (por exemplo, Carl Zeiss no exemplo Luxottica), esta implementação não conseguirá pegá-lo. No exemplo de Carl Zeiss, “Carl” e “Zeiss” serão inseridos no conjunto separadamente e, portanto, nunca conterão a única string “Carl Zeiss”.
Para resolver esse problema, podemos coletar todos os nomes próprios consecutivos e juntá-los com espaços. Aqui está a implementação atualizada que faz isso:
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; } Agora a função deve retornar um conjunto com os nomes próprios individuais e os nomes próprios consecutivos (ou seja, unidos por espaços). Se você imprimir o propNounSet , deverá ver algo como o seguinte:
[... 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, ...]Comparando o Portfólio com o Conjunto PropNouns
Estamos quase terminando!
Nas seções anteriores, construímos um scraper que pode baixar e extrair o corpo de um artigo, um tagger que pode analisar o corpo do artigo e identificar nomes próprios e um processador que pega a saída marcada e coleta os nomes próprios em um HashSet . Agora tudo o que resta a fazer é pegar o HashSet e compará-lo com a lista de empresas nas quais estamos interessados.
A implementação é muito simples. Adicione o seguinte código em sua classe 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); }Juntando tudo
Agora podemos executar o aplicativo inteiro — raspagem, limpeza, marcação, coleta e comparação. Aqui está a função que é executada em todo o aplicativo. Adicione esta função à sua classe 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); }Finalmente, podemos usar o aplicativo!
Aqui está um exemplo usando o mesmo artigo acima e Luxottica como a empresa do portfólio:
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"); } }Execute isso e o aplicativo deve imprimir “O artigo menciona empresas do portfólio”.
Altere a empresa do portfólio de Luxottica para uma empresa não mencionada no artigo (como "Microsoft"), e o aplicativo deve imprimir "O artigo não menciona empresas do portfólio".
Construir um aplicativo de PNL não precisa ser difícil
Neste artigo, passamos pelo processo de construção de um aplicativo que baixa um artigo de uma URL, limpa-o usando Boilerpipe, processa-o usando Stanford NLP e verifica se o artigo faz referências específicas de interesse (no nosso caso, empresas em nosso carteira). Conforme demonstrado, alavancar esse conjunto de tecnologias torna o que de outra forma seria uma tarefa assustadora em uma tarefa relativamente simples.
Espero que este artigo tenha apresentado a você conceitos e técnicas úteis no processamento de linguagem natural e que tenha inspirado você a escrever seus próprios aplicativos de linguagem natural.
[Observação: você pode encontrar uma cópia do código mencionado neste artigo aqui.]
