На днях, по мотивам очередной статьи, посвященной проблеме расизма в распознавании речи, я участвовала в большом споре о том, кто в этом виноват. Часть людей была уверена, что это заговор программистов. На самом деле, правда кроется в данных, которые ИИ использует для своего обучения. Я решила провести эксперимент, чтобы наглядно доказать это. Оказалось, что Роб Спир (Rob Speer) уже все сделал за меня.
Хочу поделиться с вами переводом его материала, который наглядно показывает, что даже самый дефолтный вариант ИИ будет насквозь пропитан расизмом. В первой статье мы проведем эксперимент, во второй попытаемся разобраться, как побороть то чудовище, которое мы породили.
Может быть, вы слышали об экспериментальном чат-боте Tay, которого специалисты Microsoft запустили в Twitter. За один только день его записи стали настолько провокационными, что Microsoft пришлось отключить бота и больше никогда не упоминать его имени. Вы, наверное, думаете, что вам такое не грозит, потому что не делаете никаких странных вещей (в частности, не даете всяким бездельникам возможности обучать ваш ИИ на базе Twitter).
В этом руководстве я хочу показать следующее: даже если вы используете самые стандартные алгоритмы обработки естественных языков, популярные наборы данных и методы, то в результате может получиться расистский классификатор, которого не должно существовать в природе.
Хорошая новость: этого можно избежать. Чтобы исключить появление у вашего классификатора расистских замашек, нужно будет приложить немного дополнительных усилий. При этом исправленная версия может оказаться даже точнее. Но чтобы устранить проблему, нужно знать, в чем она заключается, и не хвататься за первый работающий вариант.
Давайте сделаем классификатор тональностей текста!
Анализ тональности — очень распространенная задача NLP, что совсем неудивительно. У системы, которая способна понять, положительный или отрицательный комментарий оставил человек, есть множество применений в бизнесе. Такие решения используются для мониторинга публикаций в социальных сетях, отслеживания отзывов клиентов и даже в торговле ценными бумагами (пример: боты, которые покупали акции компании Berkshire Hathaway, после того как актриса Энн Хэтэуэй получила хорошие отзывы критиков).
Это упрощенный (иногда слишком упрощенный) подход, но это один из самых легких способов получить количественные оценки созданных людьми текстов. Всего за несколько шагов вы можете подготовить систему, которая обрабатывает тексты и выдает положительные и отрицательные оценки. При этом вам не придется иметь дело со сложными форматами представления данных, например с деревьями синтаксического разбора или с диаграммами сущностей.
Сейчас мы составим классификатор, хорошо знакомый любому специалисту NLP. При этом на каждом этапе мы будем выбирать самый простой в реализации вариант. Такая модель, к примеру, описана в статье Deep Averaging Networks. Она не является основным предметом статьи, поэтому не стоит считать это упоминание критикой полученных результатов. Там модель приводится просто в качестве примера широко известного способа использования векторных представлений слов.
Вот наш план действий:
- Взять где-нибудь широко используемые векторные представления слов.
- Взять данные для обучения и тестирования, содержащие наиболее стандартные слова позитивной и негативной тональности.
- Методом градиентного спуска обучить классификатор распознавать другие позитивные и негативные слова.
- Рассчитать оценки тональности предложений текста с помощью этого классификатора.
- Ужаснуться монстру, которого мы создали.
После этого вы будете знать, как ненамеренно сделать расистский ИИ.
Такого финала хотелось бы избежать, так что затем мы сделаем следующее:
- Выполним статистическую оценку проблемы, чтобы уметь распознать ее в будущем.
- Улучшим данные так, чтобы получить более точную и менее расистскую семантическую модель.
Необходимое программное обеспечение
Это руководство написано на Python, все библиотеки приведены ниже.
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
%matplotlib inline
seaborn.set_context('notebook', rc={'figure.figsize': (10, 6)}, font_scale=1.5)
Можно заменить scikit-learn на TensorFlow, Keras или любой другой компонент, который содержит алгоритм градиентного спуска.
Шаг 1. Векторные представления слов
Векторные представления слов часто используются для преобразования слов в формат, который удобно обрабатывать системами машинного обучения. Слова представляются в виде векторов в многомерном пространстве. Чем меньше расстояние между векторами, тем ближе значение соответствующих слов. Векторные представления слов позволяют сравнивать слова не побуквенно, а по (приблизительному) смыслу.
Чтобы получить хорошие векторные представления слов, нужно обработать сотни гигабайт текста. К счастью, многие группы специалистов по машинному обучению уже проделали эту работу и выложили готовые материалы в общий доступ.
Есть два широко известных набора векторных представлений слов: word2vec (в качестве учебного материала для их создания использовались данные из Google News) и GloVe (учебный материал: веб-страницы, переработанные роботом Common Crawl). Конечные результаты будут схожими для обоих наборов. GloVe основан на более прозрачном источнике данных, поэтому мы воспользуемся им.
Для загрузки доступно три архива GloVe: на 6, 42 и 840 млрд записей. 840 миллиардов — это много, но, чтобы извлечь из этого архива больше пользы, чем из 42-миллиардного набора, потребуется сложная постобработка. Версия на 42 миллиарда весьма функциональна и содержит круглое количество слов — 1 миллион. Мы идем по пути наименьшего сопротивления, поэтому воспользуемся версией на 42 миллиарда.
Итак, мы скачиваем архив glove.42B.300d.zip
с сайта GloVe и распаковываем файл data/glove.42B.300d.txt
. Далее нам нужно создать функцию, которая будет читать векторные представления слов в простом формате.
def load_embeddings(filename):
"""
Load a DataFrame from the generalized text format used by word2vec, GloVe,
fastText, and ConceptNet Numberbatch. The main point where they differ is
whether there is an initial line with the dimensions of the matrix.
"""
labels = []
rows = []
with open(filename, encoding='utf-8') as infile:
for i, line in enumerate(infile):
items = line.rstrip().split(' ')
if len(items) == 2:
# This is a header row giving the shape of the matrix
continue
labels.append(items[0])
values = np.array([float(x) for x in items[1:]], 'f')
rows.append(values)
arr = np.vstack(rows)
return pd.DataFrame(arr, index=labels, dtype='f')
embeddings = load_embeddings('data/glove.42B.300d.txt')
embeddings.shape
# (1917494, 300)
Шаг 2. Стандартный лексикон тональностей
Нам нужно откуда-то брать информацию о том, какие слова имеют позитивную тональность, а какие — негативную. Существует множество лексиконов тональности, но мы, как обычно, выберем один из самых простых. Скачаем архив с веб-сайта Биня Лю и извлечем файлы лексикона, data/positive-words.txt
и data/negative-words.txt
.
Далее нам нужно задать способ чтения этих файлов и считать их содержимое в переменные pos_words
и neg_words
.
def load_lexicon(filename):
"""
Load a file from Bing Liu's sentiment lexicon
(https://www.cs.uic.edu/~liub/FBS/sentiment-analysis.html), containing
English words in Latin-1 encoding.
One file contains a list of positive words, and the other contains
a list of negative words. The files contain comment lines starting
with ';' and blank lines, which should be skipped.
"""
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')
Шаг 3. Обучение модели для прогнозирования тональности слов
Некоторые слова отсутствуют в словаре GloVe. Если векторное значение отсутствует, то в результате считывания мы получаем вектор из величин NaN. Удалим такие векторы.
pos_vectors = embeddings.loc[pos_words].dropna()
neg_vectors = embeddings.loc[neg_words].dropna()
Далее составим массивы желаемых входных и выходных данных. Входные данные: векторные значения слов; выходные: значение 1 для положительно окрашенных слов и -1 для отрицательно окрашенных. Также нам нужно сохранять сами слова, чтобы иметь возможность интерпретировать результаты.
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)
Секунду! Но ведь некоторые слова нейтральны, они лишены какой-либо тональности. Разве нам не нужен третий класс для нейтральных слов?
Думаю, примеры нейтральных слов нам бы пригодились, особенно с учетом того, что проблемы, с которыми мы столкнемся, возникают из-за приписывания тональности нейтральным словам. Если бы мы могли надежно выявлять нейтральные слова, то усложнение классификатора (добавление третьего класса) было бы оправдано. Для этого нам потребуется источник примеров нейтральных слов, потому что в выбранном нами наборе представлены только позитивно и негативно окрашенные слова.
Поэтому я создал отдельную версию этого блокнота, добавил 800 нейтральных слов в качестве примеров и задал для нейтральности слов большой весовой коэффициент. Но результаты получились практически идентичными тем, что представлены ниже.
Как создатели списка разделили слова позитивной и негативной тональности? Разве тональность не зависит от контекста?
Хороший вопрос. Общий анализ тональности текста не такая простая задача, какой кажется. Граница, которую мы пытаемся найти, не всегда однозначна. В списке, который мы выбрали, слово «дерзкий» помечено как плохое, а «амбициозный» — как хорошее. «Комичный» — плохое, «забавный» — хорошее. «Возмещение» — хорошее, хотя ситуации, в которых вы или у вас требуют возмещения, редко бывают приятными.
Думаю, все понимают, что тональность слова зависит от контекста, но если мы реализуем простой подход к анализу тональности, то предполагаем, что усредненные значения тональности слов позволят получить в целом правильный ответ без учета контекста.
Мы разделим входные векторы, выходные значения и метки на наборы учебных и тестовых данных. Для тестирования будем использовать 10 % данных.
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)
Далее мы составляем классификатор и запускаем для него обучение — 100 итераций обработки учебных векторов. В качестве функции потерь используем логистическую функцию. Так наш классификатор сможет вычислять вероятность того, что заданное слово является позитивно или негативно окрашенным.
model = SGDClassifier(loss='log', random_state=0, n_iter=100)
model.fit(train_vectors, train_targets)
Теперь проверим классификатор на тестовых векторах. Оказывается, он правильно распознает тональность слов вне учебного набора в 95% случаев. Совсем неплохо.
accuracy_score(model.predict(test_vectors), test_targets)
# 0,95022624434389136
Определим также функцию, которая будет показывать прогнозируемую классификатором тональность отдельных слов. Наш классификатор способен неплохо оценивать тональность слов, не входящих в учебный набор.
def vecs_to_sentiment(vecs):
# predict_log_proba gives the log probability for each class
predictions = model.predict_log_proba(vecs)
# To see an overall positive vs. negative classification in one number,
# we take the log probability of positive sentiment minus the log
# probability of negative sentiment.
return predictions[:, 1] - predictions[:, 0]
def words_to_sentiment(words):
vecs = embeddings.loc[words].dropna()
log_odds = vecs_to_sentiment(vecs)
return pd.DataFrame({'sentiment': log_odds}, index=vecs.index)
Шаг 4. Получить оценку тональности текста
Существует множество способов оценить тональность текста на основе величин тональности для векторных представлений отдельных слов. Мы продолжим следовать путем наименьшего сопротивления и просто усредним их.
import re
TOKEN_RE = re.compile(r"w.*?b")
# The regex above finds tokens that start with a word-like character (w), and continues
# matching characters (.+?) until the next word break (b). It's a relatively simple
# expression that manages to extract something very much like words from text.
def text_to_sentiment(text):
tokens = [token.casefold() for token in TOKEN_RE.findall(text)]
sentiments = words_to_sentiment(tokens)
return sentiments['sentiment'].mean()
Что здесь можно улучшить?
- Рассчитать для слов весовые коэффициенты, обратно пропорциональные их частоте, чтобы наиболее распространенные слова (например, the или I) не сильно сказывались на оценке тональности.
- Изменить формулу усреднения так, чтобы не получать для коротких предложений самые большие по модулю оценки тональности.
- Учитывать контекст, то есть фразы целиком.
- Воспользоваться более функциональным алгоритмом разбиения предложения на слова, который корректно обрабатывает апострофы.
- Учитывать отрицания, т. е. правильно обрабатывать такие фразы, как not happy.
Но для всего этого нужно писать дополнительный код, а приведенные ниже результаты принципиально не изменятся. По крайней мере, мы можем приблизительно сравнивать относительный эмоциональный окрас различных предложений.
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
Шаг 5. Ужаснуться монстру, которого мы создали
Некоторые предложения не будут содержать слов с однозначной тональностью. Посмотрим, как наша система обработает несколько аналогов одного и того же нейтрального предложения.
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
Примерно то же самое встречалось мне и в других экспериментах, которые анализировали отзывы на рестораны с помощью векторных значений слов. Тогда оказалось, что все мексиканские рестораны получают более низкую оценку тональности без какой-либо объективной причины.
Если вы обрабатываете слова с учетом контекста, то векторные значения слов способны отразить тонкие оттенки смысла. А значит, они позволяют обнаружить и более выраженные явления, например общественные предрассудки.
Вот еще несколько нейтральных предложений.
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
Ну и ну.
Одно только изменение имени сильно меняет оценку тональности, которую выдает система. Этот и множество других примеров показывают, что при использовании имен, которые ассоциируются с белыми людьми, прогнозируемая тональность в среднем более положительная, чем при наличии стереотипных имен для людей с темным цветом кожи.
Итак, убедившись, что даже самая базовая реализация ИИ получается ужасно предвзятой, предлагаю взять небольшую паузу на осмысление. Во второй статье мы вернемся к теме и будем исправлять ошибки несмышленого ИИ.
Автор: Ольга Тележная