Uma lição de advertência.
Vamos fazer um classificador de tonalidade!A análise de sentimentos (análise de sentimentos) é uma tarefa muito comum no processamento de linguagem natural (PNL), e isso não é surpreendente. É importante que uma empresa entenda o que as pessoas estão dizendo: positivas ou negativas. Essa análise é usada para monitorar redes sociais, feedback dos clientes e até mesmo na negociação algorítmica de ações (como resultado, os bots
compram ações da Berkshire Hathaway depois de postarem críticas positivas sobre o papel de Anne Hathaway no último filme ).
Às vezes, o método de análise é muito simplificado, mas é uma das maneiras mais fáceis de obter resultados mensuráveis. Basta enviar o texto - e o resultado é positivo e negativo. Não há necessidade de lidar com a árvore de análise, criar um gráfico ou outra representação complexa.
É isso que faremos. Seguiremos o caminho de menor resistência e tornaremos o classificador mais simples, que provavelmente parece muito familiar para todos os envolvidos em desenvolvimentos relevantes no campo da PNL. Por exemplo, esse modelo pode ser encontrado no artigo
Deep Averaging Networks (Iyyer et al., 2015). Não estamos tentando desafiar seus resultados ou criticar o modelo; simplesmente fornecemos um método bem conhecido de representação vetorial de palavras.
Plano de trabalho:
- Introduzir uma representação vetorial típica de palavras para trabalhar com significados (significados).
- Introduzir conjuntos de dados de treinamento e teste com listas padrão de palavras positivas e negativas.
- Treine o classificador de descida de gradiente para reconhecer outras palavras positivas e negativas com base em sua representação vetorial.
- Use este classificador para calcular classificações de tonalidade para frases de texto.
- Para ver o monstro que criamos.
E então veremos "como criar um racista de IA sem esforços especiais". Obviamente, você não pode deixar o sistema de uma forma tão monstruosa, então vamos:
- Avaliar o problema estatisticamente, para que seja possível medir o progresso conforme ele é resolvido.
- Melhore os dados para obter um modelo semântico mais preciso e menos racista.
Dependências de software
Este tutorial foi escrito em Python e baseia-se em uma pilha típica de aprendizado de máquina Python:
numpy
e
scipy
para computação numérica,
pandas
para gerenciamento de dados e
scikit-learn
para aprendizado de máquina. No final, também
seaborn
matplotlib
e
seaborn
para criar diagramas.
Em princípio, o
scikit-learn
pode ser substituído por TensorFlow ou Keras, ou algo assim: eles também são capazes de treinar o classificador em descidas gradientes. Mas não precisamos de suas abstrações, porque aqui o treinamento ocorre em um estágio.
import numpy as np import pandas as pd import matplotlib import seaborn import re import statsmodels.formula.api from sklearn.linear_model import SGDClassifier from sklearn.model_selection import train_test_split from sklearn.metrics import accuracy_score
Etapa 1. Representação vetorial de palavras
As representações vetoriais são frequentemente usadas quando há entrada de texto. As palavras se tornam vetores no espaço multidimensional, onde vetores adjacentes representam significados semelhantes. Usando representações vetoriais, você pode comparar as palavras pelo significado (aproximadamente) e não apenas pelas correspondências exatas.
O aprendizado bem-sucedido requer centenas de gigabytes de texto. Felizmente, várias equipes de pesquisa já fizeram esse trabalho e forneceram modelos pré-treinados de representações vetoriais disponíveis para download.
Os dois conjuntos de dados mais conhecidos para o idioma inglês são
word2vec (treinado nos textos do Google Notícias) e
GloVe (nas páginas da web de rastreamento comum). Qualquer um deles dará um resultado semelhante, mas usaremos o modelo GloVe porque ele tem uma fonte de dados mais transparente.
O GloVe vem em três tamanhos: 6 bilhões, 42 bilhões e 840 bilhões.O modelo mais recente é o mais poderoso, mas requer recursos de processamento significativos. A versão de 42 bilhões é muito boa, e o dicionário é bem aparado para 1 milhão de palavras. Estamos no caminho de menor resistência, então pegue a versão de 42 bilhões.
- Por que é tão importante usar um modelo "conhecido"?
"Estou feliz que você perguntou sobre isso, interlocutor hipotético!" Em cada etapa, tentamos fazer algo extremamente típico, e o melhor modelo para a representação vetorial de palavras por algum motivo ainda não foi determinado. Espero que este artigo desperte o desejo de usar modelos modernos de alta qualidade , especialmente aqueles que levam em conta um erro algorítmico e tentam corrigi-lo. No entanto, mais sobre isso mais tarde.
Faça
o download do arquivo
glove.42B.300d.zip no site da GloVe e extraia o arquivo
data/glove.42B.300d.txt
. Em seguida, definimos uma função para ler vetores em um formato simples.
def load_embeddings(filename): """ DataFrame , word2vec, GloVe, fastText ConceptNet Numberbatch. . """ labels = [] rows = [] with open(filename, encoding='utf-8') as infile: for i, line in enumerate(infile): items = line.rstrip().split(' ') if len(items) == 2:
(1917494, 300)
Etapa 2. Dicionário de tonalidade padrão-ouro
Agora precisamos de informações sobre quais palavras são consideradas positivas e quais são negativas. Existem muitos dicionários, mas usaremos um dicionário muito simples (Hu e Liu, 2004), que é usado no artigo da
Deep Averaging Networks .
Faça o download do dicionário
no site do
Bing Liu e extraia os dados em
data/positive-words.txt
e
data/negative-words.txt
.
Em seguida, determinamos como ler esses arquivos e os atribuímos como
neg_words
e
neg_words
:
def load_lexicon(filename): """ (https://www.cs.uic.edu/~liub/FBS/sentiment-analysis.html) Latin-1. , - . , ';' , . """ lexicon = [] with open(filename, encoding='latin-1') as infile: for line in infile: line = line.rstrip() if line and not line.startswith(';'): lexicon.append(line) return lexicon pos_words = load_lexicon('data/positive-words.txt') neg_words = load_lexicon('data/negative-words.txt')
Etapa 3. Treinamos o modelo para prever a tonalidade
Com base nos vetores de palavras positivas e negativas, usamos o comando Pandas
.loc[]
para procurar representações vetoriais de todas as palavras.
Algumas palavras estão faltando no dicionário GloVe. Na maioria das vezes, esses são erros de digitação, como "fantasiar". Aqui vemos um monte de
NaN
, que indica a ausência de um vetor, e os
.dropna()
com o comando
.dropna()
.
pos_vectors = embeddings.loc[pos_words].dropna()
neg_vectors = embeddings.loc[neg_words].dropna()
Agora, criamos matrizes de dados na entrada (representações vetoriais) e na saída (1 para palavras positivas e -1 para negativa). Também verificamos que os vetores estão anexados às palavras para que possamos interpretar os resultados.
vectors = pd.concat([pos_vectors, neg_vectors])
targets = np.array([1 for entry in pos_vectors.index] + [-1 for entry in neg_vectors.index])
labels = list(pos_vectors.index) + list(neg_vectors.index)
- Espere. Algumas palavras não são positivas nem negativas, são neutras. Uma terceira classe não deveria ser criada para palavras neutras?
"Eu acho que ele teria sido útil." Mais tarde, veremos quais problemas surgem devido à atribuição de tonalidade a palavras neutras. Se pudermos identificar com segurança palavras neutras, é bem possível aumentar a complexidade do classificador para três categorias. Mas você precisa encontrar um dicionário de palavras neutras, porque no dicionário de Liu existem apenas palavras positivas e negativas.
Então, tentei minha versão com 800 exemplos de palavras e aumentei o peso para prever palavras neutras. Mas os resultados finais não foram muito diferentes do que você verá agora.
- Como esta lista distingue palavras positivas e negativas? Isso não depende do contexto?
Boa pergunta. A análise das chaves gerais não é tão simples quanto parece. A fronteira é bastante arbitrária em alguns lugares. Nesta lista, a palavra "insolente" é marcada como "ruim" e "ambiciosa" como "boa". "Comic" é ruim e "engraçado" é bom. Um "reembolso" é bom, embora geralmente seja mencionado em um contexto ruim quando você deve dinheiro a alguém ou deve a alguém.
Todo mundo entende que a tonalidade é determinada pelo contexto, mas em um modelo simples, você precisa ignorar o contexto e esperar que a tonalidade média seja adivinhada corretamente.
Usando a função
train_test_split
, dividimos simultaneamente os vetores de entrada, valores de saída e rótulos em dados de treinamento e teste, deixando 10% para o teste.
train_vectors, test_vectors, train_targets, test_targets, train_labels, test_labels = \ train_test_split(vectors, targets, labels, test_size=0.1, random_state=0)
Agora crie um classificador e passe vetores através de iterações. Usamos a função de perda logística para que o classificador final possa deduzir a probabilidade de a palavra ser positiva ou negativa.
model = SGDClassifier(loss='log', random_state=0, n_iter=100) model.fit(train_vectors, train_targets) SGDClassifier(alpha=0.0001, average=False, class_weight=None, epsilon=0.1, eta0=0.0, fit_intercept=True, l1_ratio=0.15, learning_rate='optimal', loss='log', n_iter=100, n_jobs=1, penalty='l2', power_t=0.5, random_state=0, shuffle=True, verbose=0, warm_start=False)
Avaliamos o classificador em vetores de teste. Mostra uma precisão de 95%. Nada mal.
accuracy_score(model.predict(test_vectors), test_targets)
0.95022624434389136
Definimos a função de previsão de tonalidade para certas palavras e, em seguida, a usamos para alguns exemplos de dados de teste.
def vecs_to_sentiment(vecs):
| tonalidade |
---|
inquietação | -9.931679 |
---|
interromper | -9.634706 |
---|
firmemente | 1.466919 |
---|
imaginário | -2,989215 |
---|
tributação | 0.468522 |
---|
mundialmente famoso | 6.908561 |
---|
barato | 9.237223 |
---|
desapontamento | -8.737182 |
---|
totalitário | -10.851580 |
---|
beligerante | -8,328674 |
---|
congela | -8,456981 |
---|
pecado | -7.839670 |
---|
frágil | -4,018289 |
---|
enganado | -4,309344 |
---|
não resolvido | -2,816172 |
---|
habilmente | 2.339609 |
---|
demoniza | -2.102152 |
---|
despreocupado | 8.747150 |
---|
impopular | -7,887475 |
---|
simpatizar | 1.790899 |
---|
É visto que o classificador está funcionando. Ele aprendeu a generalizar a tonalidade em palavras fora dos dados de treinamento.
Etapa 4. Obtenha uma pontuação de tonalidade para o texto.
Existem várias maneiras de adicionar vetores a uma estimativa geral. Novamente, seguimos o caminho de menor resistência, portanto, basta pegar o valor médio.
import re TOKEN_RE = re.compile(r"\w.*?\b")
Há muito a ser solicitado para otimização:
- Introduzir uma relação inversa entre a palavra peso e sua frequência, para que as mesmas preposições não afetem muito a tonalidade.
- Configuração para que frases curtas não terminem com valores extremos de tonalidade.
- Frases de contabilidade.
- Um algoritmo de segmentação de palavras mais confiável que apóstrofos não são interrompidos.
- Contabilizando negativos como "não satisfeito".
Mas tudo requer código adicional e não altera fundamentalmente os resultados. Pelo menos agora você pode comparar aproximadamente ofertas diferentes:
text_to_sentiment("this example is pretty cool") 3.889968926086298
text_to_sentiment("this example is okay") 2.7997773492425186
text_to_sentiment("meh, this example sucks") -1.1774475917460698
Passo 5. Veja o monstro que criamos
Nem toda frase tem uma tonalidade pronunciada. Vamos ver o que acontece com frases neutras:
text_to_sentiment("Let's go get Italian food") 2.0429166109408983
text_to_sentiment("Let's go get Chinese food") 1.4094033658140972
text_to_sentiment("Let's go get Mexican food") 0.38801985560121732
Eu já encontrei esse fenômeno ao analisar resenhas de restaurantes, levando em consideração representações vetoriais de palavras. Por nenhuma razão aparente
, todos os restaurantes mexicanos têm uma pontuação geral mais baixa .
As representações vetoriais capturam diferenças semânticas sutis no contexto. Portanto, eles refletem os preconceitos da nossa sociedade.
Aqui estão algumas outras sugestões neutras:
text_to_sentiment("My name is Emily") 2.2286179364745311
text_to_sentiment("My name is Heather") 1.3976291151079159
text_to_sentiment("My name is Yvette") 0.98463802132985556
text_to_sentiment("My name is Shaniqua") -0.47048131775890656
Bem, droga ...
O sistema associado aos nomes das pessoas sentimentos completamente diferentes. Você pode ver esses e muitos outros exemplos e ver que a tonalidade é geralmente mais alta para nomes estereotipicamente brancos e menor para nomes estereotipados pretos.
Este teste foi usado por Caliscan, Bryson e Narayanan em seu trabalho de pesquisa publicado na revista
Science em abril de 2017. Prova que a
semântica do corpus da linguagem contém os preconceitos da sociedade . Vamos usar esse método.
Etapa 6. Avaliando o problema
Queremos entender como evitar esses erros. Vamos passar mais dados pelo classificador e medir estatisticamente seu “viés”.
Aqui temos quatro listas de nomes que refletem diferentes origens étnicas, principalmente nos EUA. Os dois primeiros são listas de nomes predominantemente “brancos” e “negros”, adaptados com base em um artigo de Kaliskan et al. Também adicionei nomes espanhóis e muçulmanos do árabe e do urdu.
Esses dados são usados para verificar o viés do algoritmo durante o processo de construção do ConceptNet: ele pode ser encontrado no módulo
conceptnet5.vectors.evaluation.bias
. Existe uma idéia de expandir o dicionário para outros grupos étnicos, levando em consideração não apenas nomes, mas também sobrenomes.
Aqui estão as listagens:
NAMES_BY_ETHNICITY = {
Usando Pandas, compilaremos uma tabela de nomes, sua origem étnica predominante e classificações de tonalidade:
def name_sentiment_table(): frames = [] for group, name_list in sorted(NAMES_BY_ETHNICITY.items()): lower_names = [name.lower() for name in name_list] sentiments = words_to_sentiment(lower_names) sentiments['group'] = group frames.append(sentiments) # return pd.concat(frames) name_sentiments = name_sentiment_table()
Dados de exemplo:
name_sentiments.ix[::25]
| tonalidade | o grupo |
---|
mohammed | 0.834974 | Árabe / muçulmano |
---|
alya | 3.916803 | Árabe / muçulmano |
---|
terryl | -2,858010 | Preto |
---|
josé | 0.432956 | Hispânico |
---|
luciana | 1.086073 | Hispânico |
---|
hank | 0,391858 | Branco |
---|
megan | 2.158679 | Branco |
---|
Faremos um gráfico da distribuição da tonalidade para cada nome.
plot = seaborn.swarmplot(x='group', y='sentiment', data=name_sentiments) plot.set_ylim([-10, 10])
(-10, 10)

Ou como um histograma com intervalos de confiança para a média de 95%.
plot = seaborn.barplot(x='group', y='sentiment', data=name_sentiments, capsize=.1)

Por fim, execute o pacote de estatísticas
statsmodels sério. Ele mostrará o quão grande é o viés do algoritmo (junto com várias outras estatísticas).
Resultados da regressão OLSDep. Variável: | sentimento | R-quadrado: | 0,208 |
---|
Modelo: | OLS | Adj. R-quadrado: | 0,192 |
---|
Método: | Mínimos quadrados | Estatística F: | 13/04 |
---|
Data: | Qui, 13 Jul 2017 | Prob (estatística F): | 1.31e-07 |
---|
Hora: | 11:31:17 | Probabilidade de log: | -356,78 |
---|
Não. Observações: | 153 | AIC: | 721,6 |
---|
Df Residuals: | 149 | BIC: | 733,7 |
---|
Modelo Df: | 3 | | |
---|
Tipo de covariância: | antiferrugem | | |
---|
Estatística F é a razão de variação entre os grupos para a variação dentro dos grupos, que pode ser tomada como uma avaliação geral do viés.
Imediatamente abaixo está indicada a probabilidade de vermos a estatística F máxima com a hipótese nula: ou seja, na ausência de uma diferença entre as opções comparadas. A probabilidade é muito, muito baixa. Em um artigo científico, chamaríamos o resultado de "muito estatisticamente significativo".
Precisamos melhorar o valor F. Quanto menor, melhor.
ols_model.fvalue
13.041597745167659
Etapa 7. Tentando outros dados.
Agora temos a oportunidade de medir numericamente o viés prejudicial do modelo. Vamos tentar ajustá-lo. Para fazer isso, você precisa repetir várias coisas que costumavam ser apenas etapas separadas em um bloco de notas Python.
Se eu escrevesse um código bom e suportado, não usaria variáveis globais, como
model
e
embeddings
. Mas o código atual do espaguete permite que você examine melhor cada etapa e entenda o que está acontecendo. Reutilizamos parte do código e pelo menos definimos uma função para repetir algumas etapas:
def retrain_model(new_embs): """ . """ global model, embeddings, name_sentiments embeddings = new_embs pos_vectors = embeddings.loc[pos_words].dropna() neg_vectors = embeddings.loc[neg_words].dropna() vectors = pd.concat([pos_vectors, neg_vectors]) targets = np.array([1 for entry in pos_vectors.index] + [-1 for entry in neg_vectors.index]) labels = list(pos_vectors.index) + list(neg_vectors.index) train_vectors, test_vectors, train_targets, test_targets, train_labels, test_labels = \ train_test_split(vectors, targets, labels, test_size=0.1, random_state=0) model = SGDClassifier(loss='log', random_state=0, n_iter=100) model.fit(train_vectors, train_targets) accuracy = accuracy_score(model.predict(test_vectors), test_targets) print("Accuracy of sentiment: {:.2%}".format(accuracy)) name_sentiments = name_sentiment_table() ols_model = statsmodels.formula.api.ols('sentiment ~ group', data=name_sentiments).fit() print("F-value of bias: {:.3f}".format(ols_model.fvalue)) print("Probability given null hypothesis: {:.3}".format(ols_model.f_pvalue))
Tentamos word2vec
Pode-se supor que apenas o GloVe tenha o problema. Provavelmente existem muitos sites duvidosos no banco de dados do Common Crawl e pelo menos 20 cópias do Dicionário Urbano de gírias de rua. Talvez em uma base diferente seja melhor: e a boa e velha word2vec treinada no Google Notícias?
Parece que a fonte mais autorizada para dados do word2vec é
esse arquivo no Google Drive . Faça o download e salve-o como
data/word2vec-googlenews-300.bin.gz
.
Accuracy of sentiment: 94.30%
F-value of bias: 15.573
Probability given null hypothesis: 7.43e-09
Portanto, o word2vec acabou sendo ainda pior com um valor F superior a 15.
Em princípio, era tolice esperar que as
notícias fossem melhor protegidas contra preconceitos.
Tentando ConceptNet Numberbatch
Finalmente, posso falar sobre meu próprio projeto sobre a representação vetorial de palavras.
O ConceptNet com o recurso de apresentação vetorial é o gráfico de conhecimento em que estou trabalhando. Normaliza as representações vetoriais na fase de treinamento, identificando e removendo algumas fontes de racismo algorítmico e sexismo. Esse método de correção de viés é baseado em um artigo científico de Bulukbashi et al.,
“Debiasing Word Embeddings” e é generalizado para eliminar vários tipos de viés ao mesmo tempo. Até onde eu sei, este é o único sistema semântico em que existe algo assim.
Periodicamente, exportamos vetores pré-computados do ConceptNet - esses lançamentos são chamados
ConceptNet Numberbatch . Em abril de 2017, o primeiro lançamento com correção de viés foi lançado, portanto, carregaremos os vetores no idioma inglês e treinaremos novamente nosso modelo.
numberbatch-en-17.04b.txt.gz
, salvamos no diretório
data/
e treinamos novamente o modelo:
retrain_model(load_embeddings('data/numberbatch-en-17.04b.txt'))
Accuracy of sentiment: 97.46%
F-value of bias: 3.805
Probability given null hypothesis: 0.0118

Então, o ConceptNet Numberbatch resolveu completamente o problema? Chega de racismo algorítmico?
Não.O racismo se tornou muito menos?
Definitivamente .
Os intervalos principais para grupos étnicos se sobrepõem muito mais do que nos vetores GloVe ou word2vec. Comparado ao GloVe, o valor de F diminuiu mais de três vezes e comparado ao word2vec - mais de quatro vezes. E, em geral, vemos diferenças muito menores na tonalidade ao comparar nomes diferentes: deve ser assim, porque os nomes realmente não devem afetar o resultado da análise.
Mas uma ligeira correlação permaneceu. Talvez eu possa pegar esses dados e parâmetros de treinamento que o problema parece estar resolvido. Mas essa será uma opção ruim, porque
, de fato, o problema permanece, porque no ConceptNet não identificamos e compensamos todas as causas do racismo algorítmico. Mas este é um bom começo.
Sem armadilhas
Observe que, com a mudança para o ConceptNet Numberbatch, a precisão da previsão de tonalidade melhorou.
Alguém pode ter sugerido que a correção do racismo algorítmico pioraria os resultados de alguma outra maneira. Mas não. Você pode ter dados melhores e menos racistas.
Os dados estão realmente melhorando com essa correção. O racismo word2vec e GloVe adquirido de pessoas não tem nada a ver com a precisão do algoritmo.Outras abordagens
Obviamente, essa é apenas uma maneira de analisar a tonalidade. Alguns detalhes podem ser implementados de maneira diferente.Em vez disso, ou além de alterar a base do vetor, você pode tentar corrigir esse problema diretamente na saída. Por exemplo, geralmente elimine a avaliação da tonalidade para nomes e grupos de pessoas.Em geral, existe uma opção para recusar calcular a tonalidade de todas as palavras e calculá-la apenas para palavras da lista. Esta é talvez a forma mais comum de análise de sentimentos - sem aprendizado de máquina. Os resultados não terão mais viés do que o autor da lista. Mas recusar o aprendizado de máquina significa reduzir o recall (recall), e a única maneira de adaptar o modelo a um conjunto de dados é editar manualmente a lista.Como uma abordagem híbrida, você pode criar um grande número de estimativas de tonalidade estimadas para palavras e instruir uma pessoa a editá-las pacientemente, fazer uma lista de palavras de exceção com tonalidade zero. Mas este é um trabalho extra. Por outro lado, você realmente verá como o modelo funciona. Eu acho que, em qualquer caso, isso deve ser buscado.