Предостерегающий урок.
Сделаем классификатор тональности!
Анализ тональности (сентимент-анализ) — очень распространённая задача в обработке естественного языка (NLP), и это неудивительно. Для бизнеса важно понимать, какие мнения высказывают люди: положительные или отрицательные. Такой анализ используется для мониторинга социальных сетей, обратной связи с клиентами и даже в алгоритмической биржевой торговле (в результате боты покупают акции Berkshire Hathaway после публикации положительных отзывов о роли Энн Хэтэуэй в последнем фильме).
Метод анализа иногда слишком упрощён, но это один из самых простых способов получить измеримые результаты. Просто подаёте текст — и на выходе положительные и отрицательные оценки. Не нужно разбираться с деревом синтаксического анализа, строить граф или какое-то другое сложное представление.
Этим и займёмся. Пойдём по пути наименьшего сопротивления и сделаем самый простой классификатор, который наверняка выглядит очень знакомо для всех, кто занимается актуальными разработками в области NLP. Например, такую модель можно найти в статье Deep Averaging Networks (Iyyer et al., 2015). Мы вовсе не пытаемся оспорить их результаты или критиковать модель; просто приводим известный способ векторного представления слов.
План работ:
И тогда мы увидим, «как без особенных усилий создать ИИ-расиста». Конечно, нельзя оставлять систему в таком чудовищном виде, поэтому потом мы собираемся:
Данное руководство написано на Python и полагается на типичный стек машинного обучения Python:
В принципе,
Векторные представления часто используются при наличии текстовых данных на входе. Слова становятся векторами в многомерном пространстве, где соседние векторы представляют схожие значения. С помощью векторных представлений можно сравнивать слова по (грубо) их смыслу, а не только по точным совпадениям.
Для успешного обучения требуются сотни гигабайт текста. К счастью, различные научные коллективы уже провели эту работу и предоставили предварительно обученные модели векторных представлений, доступные для загрузки.
Два самых известных набора данных для английского языка — word2vec (обучена на текстах Google News) и GloVe (на веб-страницых Common Crawl). Любой из них даст аналогичный результат, но мы возьмём модель GloVe, потому что у неё более прозрачный источник данных.
GloVe поставляется в трёх размерах: 6 млрд, 42 млрд и 840 млрд. Последняя модель самая мощная, но требует значительных ресурсов для обработки. Версия на 42 млрд довольно хороша, а словарь аккуратно обрезан до 1 миллиона слов. Мы идём по пути наименьшего сопротивления, так что возьмём версию на 42 млрд.
Скачиваем glove.42B.300d.zip с сайта GloVe и извлекаем файл
Теперь нужна информация, какие слова считаются положительными, а какие — отрицательными. Есть много таких словарей, но мы возьмём очень простой словарь (Ху и Лю, 2004), который используется в статье Deep Averaging Networks.
Загружаем словарь с сайта Бинга Лю и извлекаем данные в
Далее определяем, как читать эти файлы, и назначаем их в качестве переменных
На основе векторов положительных и отрицательных слов применяем команду Pandas
Некоторые слова отсутствуют в словере GloVe. Чаще всего это опечатки вроде “fancinating”. Здесь мы видим кучу
Теперь создаём массивы данных на входе (векторные представления) и выходе (1 для положительных слов и -1 для отрицательных). Также проверяем, что векторы привязаны к словам, чтобы мы смогли интерпретировать результаты.
С помощью функции
Теперь создаём классификатор и пропускаем через него векторы в 100 итераций. Используем логистическую функцию потерь, чтобы итоговый классификатор мог выводить вероятность того, что слово является положительным или отрицательным.
Оцениваем классификатор на тестовых векторах. Он демонстрирует точность 95%. Неплохо.
Определим функцию прогноза тональности для определённых слов, а затем используем её на некоторых примерах из тестовых данных.
Видно, что классификатор работает. Он научился обобщать тональность на словах за пределами обучающих данных.
Есть много способов сложить вектора в общую оценку. Опять же, мы следуем по пути наименьшего сопротивления, поэтому просто берём среднее значение.
Здесь многое напрашивается на оптимизацию:
Но всё требует дополнительного кода и принципиально не изменит результаты. По крайней мере, теперь можно примерно сравнить разные предложения:
Не в каждом предложении чётко выражена тональность. Посмотрим, что происходит с нейтральными предложениями:
Я уже встречал такой феномен при анализе отзывов о ресторанах с учётом векторных представлений слов. Без видимых причин у всех мексиканских ресторанов итоговая оценка оказалась ниже.
Векторные представления улавливают тонкие смысловые различия по контексту. Поэтому они отражают предубеждения нашего общества.
Вот некоторые другие нейтральные предложения:
Ну блин…
Система связала с именами людей совершенно разные чувства. Вы можете посмотреть на эти и многие другие примеры и увидеть, что тональность обычно выше для стереотипно-белых имён и ниже для стереотипно-чёрных имен.
Это тест использовали Калискан, Брайсон и Нараянан в своей научной работе, опубликованной в журнале Science в апреле 2017 года. Она доказывает, что семантика из языковых корпусов содержит предубеждения общества. Будем использовать данный метод.
Мы хотим понять, как избежать подобных ошибок. Пропустим больше данных через классификатор и статистически измерим его «предвзятость».
Здесь у нас четыре списка имён, которые отражают различное этническое происхождение, главным образом, в США. Первые два — списки преимущественно «белых» и «чёрных» имён, адаптированные на основе статьи Калискана и др. Я также добавил испанские и мусульманские имена из арабского и урду.
Эти данные используются для проверки предвзятости алгоритма в процессе сборки ConceptNet: их можно найти в модуле
Вот списки:
С помощью Pandas составим таблицу имён, их преобладающего этнического происхождения и оценки тональности:
Пример данных:
Составим график распределения тональности по каждому имени.
Или в виде гистограммы с доверительными интервалами для средних в 95%.
Наконец, запустим серьёзный статистический пакет statsmodels. Он покажет, насколько велика предвзятость алгоритма (вместе с кучей другой статистики).
Результаты регрессии OLS
F-statistic — это отношение вариативности между группами к вариативности внутри групп, что можно принять в качестве общей оценки предвзятости.
Сразу под ним указана вероятность, что мы увидим максимальный показатель F-statistic при нулевой гипотезе: то есть при отсутствии разницы между сравниваемыми вариантами. Вероятность очень, очень низкая. В научной статье мы бы назвали результат «очень статистически значимым».
Нам нужно улучшить F-значение. Чем ниже, тем лучше.
Теперь у нас есть возможность численно измерять вредную предвзятость модели. Попробуем её скорректировать. Для этого нужно повторить кучу вещей, которые раньше были просто отдельными шагами в блокноте Python.
Если бы я писал хороший, поддерживаемый код, то не использовал бы глобальные переменные, такие как
Можно предположить, что проблема только у GloVe. Наверное, в базе Common Crawl много сомнительных сайтов и как минимум 20 копий словаря уличного сленга Urban Dictionary. Возможно, на другой базе будет лучше: как насчёт старого доброго word2vec, обученного на Google News?
Кажется, наиболее авторитетным источником для данных word2vec является этот файл на Google Drive. Загружаем его и сохраняем как
Итак, word2vec оказался ещё хуже с F-значением более 15.
В принципе, было глупо ожидать, что новости лучше защищены от предвзятости.
Наконец-то я могу рассказать о собственном проекте по векторному представлению слов.
ConceptNet с функцией векторных представлений — граф знаний, над которым я работаю. Он нормализует векторные представления на этапе обучения, выявляя и удаляя некоторые источники алгоритмического расизма и сексизма. Этот метод исправления предвзятости основан на научной статье Булукбаси и др. “Debiasing Word Embeddings” и обобщён для устранения одновременно нескольких видов предвзятости. Насколько я знаю, это единственная семантическая система, в которой есть что-то подобное.
Время от времени мы экспортируем предварительно вычисленные векторы из ConceptNet — эти выпуски называются ConceptNet Numberbatch. В апреле 2017 года вышел первый релиз с коррекцией предвзятости, поэтому загрузим англоязычные векторы и переобучим нашу модель.
Загружаем
Что же, ConceptNet Numberbatch полностью устранил проблему? Больше никакого алгоритмического расизма? Нет.
Расизма стало намного меньше? Определённо.
Диапазоны тональности для этнических групп перекрываются намного больше, чем в векторах GloVe или word2vec. По сравнению с GloVe значение F уменьшилось более чем в три раза, а по сравнению с word2vec — более чем в четыре раза. И в целом мы видим гораздо меньшие различия в тональности при сравнении различных имён: так и должно быть, потому что имена действительно не должны влиять на результат анализа.
Но небольшая корреляция по-прежнему осталась. Возможно, я могу подобрать такие данные и параметры обучения, что проблема покажется решённой. Но это будет плохой вариант, ведь на самом деле проблема остаётся, потому что в ConceptNet мы выявили и компенсировали далеко не все причины алгоритмического расизма. Но это хорошее начало.
Обратите внимание, что с переходом на ConceptNet Numberbatch повысилась точность прогнозирования тональности.
Кто-то мог предположить, что коррекция алгоритмического расизма ухудшит результаты в каком-то другом отношении. Но нет. У вас могут быть данные, которые лучше и менее расистские. Данные реально улучшаются с этой коррекцией. Приобретённый от людей расизм word2vec и GloVe не имеет никакого отношения к точности работы алгоритма.
Конечно, это только один способ анализа тональности. Какие-то детали можно реализовать иначе.
Вместо или в дополнение к смене векторной базы можно попытаться устранить эту проблему непосредственно в выдаче. Например, вообще устранить оценку тональности для имён и групп людей.
Есть вариант вообще отказаться от расчёта тональности всех слов, а рассчитывать её только для слов из списка. Пожалуй, это самая распространённая форма анализа тональности — вообще без машинного обучения. В результатах будет не больше предвзятости, чем у автора списка. Но отказ от машинного обучения означает уменьшение полноты (recall), а единственный способ адаптировать модель к набору данных — вручную отредактировать список.
В качестве гибридного подхода вы можете создать большое количество предполагаемых оценок тональности для слов и поручить человеку терпеливо их отредактировать, составить список слов-исключений с нулевой тональностью. Но это дополнительная работа. С другой стороны, вы действительно увидите, как работает модель. Думаю, в любом случае к этому следует стремиться.
Сделаем классификатор тональности!
Анализ тональности (сентимент-анализ) — очень распространённая задача в обработке естественного языка (NLP), и это неудивительно. Для бизнеса важно понимать, какие мнения высказывают люди: положительные или отрицательные. Такой анализ используется для мониторинга социальных сетей, обратной связи с клиентами и даже в алгоритмической биржевой торговле (в результате боты покупают акции Berkshire Hathaway после публикации положительных отзывов о роли Энн Хэтэуэй в последнем фильме).
Метод анализа иногда слишком упрощён, но это один из самых простых способов получить измеримые результаты. Просто подаёте текст — и на выходе положительные и отрицательные оценки. Не нужно разбираться с деревом синтаксического анализа, строить граф или какое-то другое сложное представление.
Этим и займёмся. Пойдём по пути наименьшего сопротивления и сделаем самый простой классификатор, который наверняка выглядит очень знакомо для всех, кто занимается актуальными разработками в области NLP. Например, такую модель можно найти в статье Deep Averaging Networks (Iyyer et al., 2015). Мы вовсе не пытаемся оспорить их результаты или критиковать модель; просто приводим известный способ векторного представления слов.
План работ:
- Внедрить типичный способ векторного представления слов для работы со смыслами (значениями).
- Внедрить обучающие и тестовые наборы данных со стандартными списками положительных и отрицательных слов.
- Обучить классификатор на градиентном спуске распознавать другие положительные и отрицательные слова на основе их векторного представления.
- Вычислить с помощью этого классификатора оценки тональности для предложений текста.
- Узреть чудовище, которое мы сотворили.
И тогда мы увидим, «как без особенных усилий создать ИИ-расиста». Конечно, нельзя оставлять систему в таком чудовищном виде, поэтому потом мы собираемся:
- Оценить проблему статистически, чтобы появилась возможность измерять прогресс по мере её решения.
- Улучшить данные, чтобы получить более точную и менее расистскую семантическую модель.
Зависимости программного обеспечения
Данное руководство написано на Python и полагается на типичный стек машинного обучения Python:
numpy
и scipy
для числовых вычислений, pandas
для управления данными и scikit-learn
для машинного обучения. В конце применим ещё matplotlib
и seaborn
для построения диаграмм.В принципе,
scikit-learn
можно заменить на TensorFlow или Keras, или что-то в этом роде: они тоже способны обучить классификатор на градиентном спуске. Но нам не нужны их абстракции, поскольку здесь обучение происходит в один этап.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)
Шаг 1. Векторное представление слов
Векторные представления часто используются при наличии текстовых данных на входе. Слова становятся векторами в многомерном пространстве, где соседние векторы представляют схожие значения. С помощью векторных представлений можно сравнивать слова по (грубо) их смыслу, а не только по точным совпадениям.
Для успешного обучения требуются сотни гигабайт текста. К счастью, различные научные коллективы уже провели эту работу и предоставили предварительно обученные модели векторных представлений, доступные для загрузки.
Два самых известных набора данных для английского языка — word2vec (обучена на текстах Google News) и GloVe (на веб-страницых Common Crawl). Любой из них даст аналогичный результат, но мы возьмём модель GloVe, потому что у неё более прозрачный источник данных.
GloVe поставляется в трёх размерах: 6 млрд, 42 млрд и 840 млрд. Последняя модель самая мощная, но требует значительных ресурсов для обработки. Версия на 42 млрд довольно хороша, а словарь аккуратно обрезан до 1 миллиона слов. Мы идём по пути наименьшего сопротивления, так что возьмём версию на 42 млрд.
— Почему так важно использовать «хорошо известную» модель?
— Я рад, что ты спросил об этом, гипотетический собеседник! На каждом шаге мы пытаемся сделать что-то чрезвычайно типичное, а лучшая модель для векторного представления слов по какой-то причине ещё не определена. Надеюсь, эта статья вызовет желание использовать современные высококачественные модели, особенно которые учитывают алгоритмическую ошибку и пытаются её скорректировать. Впрочем, об этом позже.
Скачиваем glove.42B.300d.zip с сайта GloVe и извлекаем файл
data/glove.42B.300d.txt
. Далее определяем функцию для чтения векторов в простом формате.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:
# 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. Золотой стандарт словаря тональности
Теперь нужна информация, какие слова считаются положительными, а какие — отрицательными. Есть много таких словарей, но мы возьмём очень простой словарь (Ху и Лю, 2004), который используется в статье Deep Averaging Networks.
Загружаем словарь с сайта Бинга Лю и извлекаем данные в
data/positive-words.txt
и data/negative-words.txt
.Далее определяем, как читать эти файлы, и назначаем их в качестве переменных
pos_words
и 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')
Шаг 3. Обучаем модель предсказывать тональность
На основе векторов положительных и отрицательных слов применяем команду Pandas
.loc[]
для поиска векторных представлений всех слов.Некоторые слова отсутствуют в словере GloVe. Чаще всего это опечатки вроде “fancinating”. Здесь мы видим кучу
NaN
, что указывает на отсутствие вектора, и удаляем их командой .dropna()
.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 примерами словами и увеличил вес для предсказания нейтральных слов. Но конечные результаты не сильно отличались от того, что вы сейчас увидите.
— Как этот список различает положительные и отрицательные слова? Разве это не зависит от контекста?
— Хороший вопрос. Анализ общих тональностей не так прост, как кажется. Граница местами довольно произвольна. В этом списке слово «дерзкий» отмечено как «плохое», а «амбициозный» — как «хорошее». «Комичный» — плохо, а «забавный» — хорошо. «Возврат средств» (refund) хорош, хотя обычно упоминается в плохом контексте, когда вы должны кому-то деньги или вам кто-то задолжал.
Все понимают, что тональность определяется контекстом, но в простой модели приходится игнорировать контекст и надеяться, что средняя тональность будет угадана верно.
С помощью функции
train_test_split
одновременно разделяем входные векторы, выходные значения и метки на обучающие и тестовые данные, при этом 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)
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)
Оцениваем классификатор на тестовых векторах. Он демонстрирует точность 95%. Неплохо.
accuracy_score(model.predict(test_vectors), test_targets)
0.95022624434389136
Определим функцию прогноза тональности для определённых слов, а затем используем её на некоторых примерах из тестовых данных.
def vecs_to_sentiment(vecs):
# predict_log_proba показывает log-вероятность для каждого класса
predictions = model.predict_log_proba(vecs)
# Для сведения воедино положительной и отрицательной классификации
# вычитаем log-вероятность отрицательной тональности из положительной.
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)
# Показываем 20 примеров из тестового набора данных
words_to_sentiment(test_labels).ix[:20]
тональность | |
---|---|
непоседа | -9.931679 |
прерывать | -9.634706 |
стойко | 1.466919 |
воображаемый | -2.989215 |
налогообложение | 0.468522 |
всемирно известный | 6.908561 |
недорогой | 9.237223 |
разочарование | -8.737182 |
тоталитарный | -10.851580 |
воинственный | -8.328674 |
замерзает | -8.456981 |
грех | -7.839670 |
хрупкий | -4.018289 |
одураченный | -4.309344 |
нерешённый | -2.816172 |
ловко | 2.339609 |
демонизирует | -2.102152 |
беззаботный | 8.747150 |
непопулярный | -7.887475 |
сочувствовать | 1.790899 |
Видно, что классификатор работает. Он научился обобщать тональность на словах за пределами обучающих данных.
Шаг 4. Получаем оценку тональности для текста
Есть много способов сложить вектора в общую оценку. Опять же, мы следуем по пути наименьшего сопротивления, поэтому просто берём среднее значение.
import re
TOKEN_RE = re.compile(r"\w.*?\b")
# regex находит объекты, которые начинаются с буквы (\w) и продолжает
# сравнивать символы (.+?) до окончания слова (\b). Это относительно
# простое выражение для извлечения слов из текста.
def text_to_sentiment(text):
tokens = [token.casefold() for token in TOKEN_RE.findall(text)]
sentiments = words_to_sentiment(tokens)
return sentiments['sentiment'].mean()
Здесь многое напрашивается на оптимизацию:
- Внедрение обратной зависимости веса слова и его частотности, чтобы те же предлоги не сильно влияли на тональность.
- Настройка, чтобы короткие предложения не завершались экстремальными значениями тональности.
- Учёт фраз.
- Более надёжный алгоритм сегментации слов, который не сбивают апострофы.
- Учёт отрицаний типа «не доволен».
Но всё требует дополнительного кода и принципиально не изменит результаты. По крайней мере, теперь можно примерно сравнить разные предложения:
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
Ну блин…
Система связала с именами людей совершенно разные чувства. Вы можете посмотреть на эти и многие другие примеры и увидеть, что тональность обычно выше для стереотипно-белых имён и ниже для стереотипно-чёрных имен.
Это тест использовали Калискан, Брайсон и Нараянан в своей научной работе, опубликованной в журнале Science в апреле 2017 года. Она доказывает, что семантика из языковых корпусов содержит предубеждения общества. Будем использовать данный метод.
Шаг 6. Оценка проблемы
Мы хотим понять, как избежать подобных ошибок. Пропустим больше данных через классификатор и статистически измерим его «предвзятость».
Здесь у нас четыре списка имён, которые отражают различное этническое происхождение, главным образом, в США. Первые два — списки преимущественно «белых» и «чёрных» имён, адаптированные на основе статьи Калискана и др. Я также добавил испанские и мусульманские имена из арабского и урду.
Эти данные используются для проверки предвзятости алгоритма в процессе сборки ConceptNet: их можно найти в модуле
conceptnet5.vectors.evaluation.bias
. Есть идея расширить словарь на другие этнические группы с учётом не только имён, но и фамилий.Вот списки:
NAMES_BY_ETHNICITY = {
# Первые два списка из приложения к научной статье Калискана и др.
'White': [
'Adam', 'Chip', 'Harry', 'Josh', 'Roger', 'Alan', 'Frank', 'Ian', 'Justin',
'Ryan', 'Andrew', 'Fred', 'Jack', 'Matthew', 'Stephen', 'Brad', 'Greg', 'Jed',
'Paul', 'Todd', 'Brandon', 'Hank', 'Jonathan', 'Peter', 'Wilbur', 'Amanda',
'Courtney', 'Heather', 'Melanie', 'Sara', 'Amber', 'Crystal', 'Katie',
'Meredith', 'Shannon', 'Betsy', 'Donna', 'Kristin', 'Nancy', 'Stephanie',
'Bobbie-Sue', 'Ellen', 'Lauren', 'Peggy', 'Sue-Ellen', 'Colleen', 'Emily',
'Megan', 'Rachel', 'Wendy'
],
'Black': [
'Alonzo', 'Jamel', 'Lerone', 'Percell', 'Theo', 'Alphonse', 'Jerome',
'Leroy', 'Rasaan', 'Torrance', 'Darnell', 'Lamar', 'Lionel', 'Rashaun',
'Tyree', 'Deion', 'Lamont', 'Malik', 'Terrence', 'Tyrone', 'Everol',
'Lavon', 'Marcellus', 'Terryl', 'Wardell', 'Aiesha', 'Lashelle', 'Nichelle',
'Shereen', 'Temeka', 'Ebony', 'Latisha', 'Shaniqua', 'Tameisha', 'Teretha',
'Jasmine', 'Latonya', 'Shanise', 'Tanisha', 'Tia', 'Lakisha', 'Latoya',
'Sharise', 'Tashika', 'Yolanda', 'Lashandra', 'Malika', 'Shavonn',
'Tawanda', 'Yvette'
],
# Список испанских имён составлен по данным переписи населения США.
'Hispanic': [
'Juan', 'Jose', 'Miguel', 'Luis', 'Jorge', 'Santiago', 'Matias', 'Sebastian',
'Mateo', 'Nicolas', 'Alejandro', 'Samuel', 'Diego', 'Daniel', 'Tomas',
'Juana', 'Ana', 'Luisa', 'Maria', 'Elena', 'Sofia', 'Isabella', 'Valentina',
'Camila', 'Valeria', 'Ximena', 'Luciana', 'Mariana', 'Victoria', 'Martina'
],
# Следующий список объединяет религию и этническую
# принадлежность, я в курсе. Также как и сами имена.
#
# Он составлен по данным сайтов с именами детей для
# родителей-мусульман в английском написании. Я не проводил
# грани между арабским, урду и другими языками.
#
# Буду рад обновить список более авторитетными данными.
'Arab/Muslim': [
'Mohammed', 'Omar', 'Ahmed', 'Ali', 'Youssef', 'Abdullah', 'Yasin', 'Hamza',
'Ayaan', 'Syed', 'Rishaan', 'Samar', 'Ahmad', 'Zikri', 'Rayyan', 'Mariam',
'Jana', 'Malak', 'Salma', 'Nour', 'Lian', 'Fatima', 'Ayesha', 'Zahra', 'Sana',
'Zara', 'Alya', 'Shaista', 'Zoya', 'Yasmin'
]
}
С помощью Pandas составим таблицу имён, их преобладающего этнического происхождения и оценки тональности:
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()
Пример данных:
name_sentiments.ix[::25]
тональность | группа | |
---|---|---|
mohammed | 0.834974 | Arab/Muslim |
alya | 3.916803 | Arab/Muslim |
terryl | -2.858010 | Black |
jose | 0.432956 | Hispanic |
luciana | 1.086073 | Hispanic |
hank | 0.391858 | White |
megan | 2.158679 | White |
Составим график распределения тональности по каждому имени.
plot = seaborn.swarmplot(x='group', y='sentiment', data=name_sentiments)
plot.set_ylim([-10, 10])
(-10, 10)
Или в виде гистограммы с доверительными интервалами для средних в 95%.
plot = seaborn.barplot(x='group', y='sentiment', data=name_sentiments, capsize=.1)
Наконец, запустим серьёзный статистический пакет statsmodels. Он покажет, насколько велика предвзятость алгоритма (вместе с кучей другой статистики).
Результаты регрессии OLS
Dep. Variable: | sentiment | R-squared: | 0.208 |
---|---|---|---|
Model: | OLS | Adj. R-squared: | 0.192 |
Method: | Least Squares | F-statistic: | 13.04 |
Date: | Thu, 13 Jul 2017 | Prob (F-statistic): | 1.31e-07 |
Time: | 11:31:17 | Log-Likelihood: | -356.78 |
No. Observations: | 153 | AIC: | 721.6 |
Df Residuals: | 149 | BIC: | 733.7 |
Df Model: | 3 | ||
Covariance Type: | nonrobust |
F-statistic — это отношение вариативности между группами к вариативности внутри групп, что можно принять в качестве общей оценки предвзятости.
Сразу под ним указана вероятность, что мы увидим максимальный показатель F-statistic при нулевой гипотезе: то есть при отсутствии разницы между сравниваемыми вариантами. Вероятность очень, очень низкая. В научной статье мы бы назвали результат «очень статистически значимым».
Нам нужно улучшить F-значение. Чем ниже, тем лучше.
ols_model.fvalue
13.041597745167659
Шаг 7. Пробуем другие данные
Теперь у нас есть возможность численно измерять вредную предвзятость модели. Попробуем её скорректировать. Для этого нужно повторить кучу вещей, которые раньше были просто отдельными шагами в блокноте Python.
Если бы я писал хороший, поддерживаемый код, то не использовал бы глобальные переменные, такие как
model
и embeddings
. Но нынешний спагетти-код позволяет лучше изучить каждый шаг и понять происходящее. Используем повторно часть кода и хотя бы определим функцию для повтора некоторых шагов: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))
# Выводим результаты на график с совместимой осью Y
plot = seaborn.swarmplot(x='group', y='sentiment', data=name_sentiments)
plot.set_ylim([-10, 10])
Пробуем word2vec
Можно предположить, что проблема только у GloVe. Наверное, в базе Common Crawl много сомнительных сайтов и как минимум 20 копий словаря уличного сленга Urban Dictionary. Возможно, на другой базе будет лучше: как насчёт старого доброго word2vec, обученного на Google News?
Кажется, наиболее авторитетным источником для данных word2vec является этот файл на Google Drive. Загружаем его и сохраняем как
data/word2vec-googlenews-300.bin.gz
.# Используем функцию ConceptNet для загрузки word2vec во фрейм Pandas из его бинарного формата
from conceptnet5.vectors.formats import load_word2vec_bin
w2v = load_word2vec_bin('data/word2vec-googlenews-300.bin.gz', nrows=2000000)
# Модель word2vec чувствительна к регистру
w2v.index = [label.casefold() for label in w2v.index]
# Удаляем дубликаты, которые реже встречаются
w2v = w2v.reset_index().drop_duplicates(subset='index', keep='first').set_index('index')
retrain_model(w2v)
Accuracy of sentiment: 94.30%
F-value of bias: 15.573
Probability given null hypothesis: 7.43e-09
Итак, word2vec оказался ещё хуже с F-значением более 15.
В принципе, было глупо ожидать, что новости лучше защищены от предвзятости.
Пробуем ConceptNet Numberbatch
Наконец-то я могу рассказать о собственном проекте по векторному представлению слов.
ConceptNet с функцией векторных представлений — граф знаний, над которым я работаю. Он нормализует векторные представления на этапе обучения, выявляя и удаляя некоторые источники алгоритмического расизма и сексизма. Этот метод исправления предвзятости основан на научной статье Булукбаси и др. “Debiasing Word Embeddings” и обобщён для устранения одновременно нескольких видов предвзятости. Насколько я знаю, это единственная семантическая система, в которой есть что-то подобное.
Время от времени мы экспортируем предварительно вычисленные векторы из ConceptNet — эти выпуски называются ConceptNet Numberbatch. В апреле 2017 года вышел первый релиз с коррекцией предвзятости, поэтому загрузим англоязычные векторы и переобучим нашу модель.
Загружаем
numberbatch-en-17.04b.txt.gz
, сохраняем в каталоге data/
и переобучаем модель: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
Что же, ConceptNet Numberbatch полностью устранил проблему? Больше никакого алгоритмического расизма? Нет.
Расизма стало намного меньше? Определённо.
Диапазоны тональности для этнических групп перекрываются намного больше, чем в векторах GloVe или word2vec. По сравнению с GloVe значение F уменьшилось более чем в три раза, а по сравнению с word2vec — более чем в четыре раза. И в целом мы видим гораздо меньшие различия в тональности при сравнении различных имён: так и должно быть, потому что имена действительно не должны влиять на результат анализа.
Но небольшая корреляция по-прежнему осталась. Возможно, я могу подобрать такие данные и параметры обучения, что проблема покажется решённой. Но это будет плохой вариант, ведь на самом деле проблема остаётся, потому что в ConceptNet мы выявили и компенсировали далеко не все причины алгоритмического расизма. Но это хорошее начало.
Никаких подводных камней
Обратите внимание, что с переходом на ConceptNet Numberbatch повысилась точность прогнозирования тональности.
Кто-то мог предположить, что коррекция алгоритмического расизма ухудшит результаты в каком-то другом отношении. Но нет. У вас могут быть данные, которые лучше и менее расистские. Данные реально улучшаются с этой коррекцией. Приобретённый от людей расизм word2vec и GloVe не имеет никакого отношения к точности работы алгоритма.
Другие подходы
Конечно, это только один способ анализа тональности. Какие-то детали можно реализовать иначе.
Вместо или в дополнение к смене векторной базы можно попытаться устранить эту проблему непосредственно в выдаче. Например, вообще устранить оценку тональности для имён и групп людей.
Есть вариант вообще отказаться от расчёта тональности всех слов, а рассчитывать её только для слов из списка. Пожалуй, это самая распространённая форма анализа тональности — вообще без машинного обучения. В результатах будет не больше предвзятости, чем у автора списка. Но отказ от машинного обучения означает уменьшение полноты (recall), а единственный способ адаптировать модель к набору данных — вручную отредактировать список.
В качестве гибридного подхода вы можете создать большое количество предполагаемых оценок тональности для слов и поручить человеку терпеливо их отредактировать, составить список слов-исключений с нулевой тональностью. Но это дополнительная работа. С другой стороны, вы действительно увидите, как работает модель. Думаю, в любом случае к этому следует стремиться.
Комментарии (2)
Laney1
17.01.2019 18:19бгг, это что, перевод без иронии?)) "Как создать модель, корректно отражающую реальность, и оскорбиться"
urticazoku
Было же в блоге Microsoft.