[Перевод] Как без особенных усилий создать ИИ-расиста
Предостерегающий урок.
Сделаем классификатор тональности!
Анализ тональности (сентимент-анализ) — очень распространённая задача в обработке естественного языка (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)
Векторные представления часто используются при наличии текстовых данных на входе. Слова становятся векторами в многомерном пространстве, где соседние векторы представляют схожие значения. С помощью векторных представлений можно сравнивать слова по (грубо) их смыслу, а не только по точным совпадениям.
Для успешного обучения требуются сотни гигабайт текста. К счастью, различные научные коллективы уже провели эту работу и предоставили предварительно обученные модели векторных представлений, доступные для загрузки.
Два самых известных набора данных для английского языка — 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)
Теперь нужна информация, какие слова считаются положительными, а какие — отрицательными. Есть много таких словарей, но мы возьмём очень простой словарь (Ху и Лю, 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')
На основе векторов положительных и отрицательных слов применяем команду 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 |
Видно, что классификатор работает. Он научился обобщать тональность на словах за пределами обучающих данных.
Есть много способов сложить вектора в общую оценку. Опять же, мы следуем по пути наименьшего сопротивления, поэтому просто берём среднее значение.
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
Не в каждом предложении чётко выражена тональность. Посмотрим, что происходит с нейтральными предложениями:
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 года. Она доказывает, что семантика из языковых корпусов содержит предубеждения общества. Будем использовать данный метод.
Мы хотим понять, как избежать подобных ошибок. Пропустим больше данных через классификатор и статистически измерим его «предвзятость».
Здесь у нас четыре списка имён, которые отражают различное этническое происхождение, главным образом, в США. Первые два — списки преимущественно «белых» и «чёрных» имён, адаптированные на основе статьи Калискана и др. Я также добавил испанские и мусульманские имена из арабского и урду.
Эти данные используются для проверки предвзятости алгоритма в процессе сборки 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', 'José', 'Miguel', 'Luís', 'Jorge', 'Santiago', 'Matías', 'Sebastián',
'Mateo', 'Nicolás', 'Alejandro', 'Samuel', 'Diego', 'Daniel', 'Tomás',
'Juana', 'Ana', 'Luisa', 'María', 'Elena', 'Sofía', '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 |
josé | 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
Теперь у нас есть возможность численно измерять вредную предвзятость модели. Попробуем её скорректировать. Для этого нужно повторить кучу вещей, которые раньше были просто отдельными шагами в блокноте 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), а единственный способ адаптировать модель к набору данных — вручную отредактировать список.
В качестве гибридного подхода вы можете создать большое количество предполагаемых оценок тональности для слов и поручить человеку терпеливо их отредактировать, составить список слов-исключений с нулевой тональностью. Но это дополнительная работа. С другой стороны, вы действительно увидите, как работает модель. Думаю, в любом случае к этому следует стремиться.