[Перевод] Как без особенных усилий создать ИИ-расиста

Предостерегающий урок.

Сделаем классификатор тональности!

Анализ тональности (сентимент-анализ) — очень распространённая задача в обработке естественного языка (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)

qvy7gel8rrvm5txo-nou6g0i0re.png

Или в виде гистограммы с доверительными интервалами для средних в 95%.

plot = seaborn.barplot(x='group', y='sentiment', data=name_sentiments, capsize=.1)


uvibn6olthaxt6cxbd96szehq94.png

Наконец, запустим серьёзный статистический пакет 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

5diuuqrst8bca5-m7fljox--pro.png

Что же, ConceptNet Numberbatch полностью устранил проблему? Больше никакого алгоритмического расизма? Нет.

Расизма стало намного меньше? Определённо.

Диапазоны тональности для этнических групп перекрываются намного больше, чем в векторах GloVe или word2vec. По сравнению с GloVe значение F уменьшилось более чем в три раза, а по сравнению с word2vec — более чем в четыре раза. И в целом мы видим гораздо меньшие различия в тональности при сравнении различных имён: так и должно быть, потому что имена действительно не должны влиять на результат анализа.

Но небольшая корреляция по-прежнему осталась. Возможно, я могу подобрать такие данные и параметры обучения, что проблема покажется решённой. Но это будет плохой вариант, ведь на самом деле проблема остаётся, потому что в ConceptNet мы выявили и компенсировали далеко не все причины алгоритмического расизма. Но это хорошее начало.

Никаких подводных камней


Обратите внимание, что с переходом на ConceptNet Numberbatch повысилась точность прогнозирования тональности.

Кто-то мог предположить, что коррекция алгоритмического расизма ухудшит результаты в каком-то другом отношении. Но нет. У вас могут быть данные, которые лучше и менее расистские. Данные реально улучшаются с этой коррекцией. Приобретённый от людей расизм word2vec и GloVe не имеет никакого отношения к точности работы алгоритма.


Конечно, это только один способ анализа тональности. Какие-то детали можно реализовать иначе.

Вместо или в дополнение к смене векторной базы можно попытаться устранить эту проблему непосредственно в выдаче. Например, вообще устранить оценку тональности для имён и групп людей.

Есть вариант вообще отказаться от расчёта тональности всех слов, а рассчитывать её только для слов из списка. Пожалуй, это самая распространённая форма анализа тональности — вообще без машинного обучения. В результатах будет не больше предвзятости, чем у автора списка. Но отказ от машинного обучения означает уменьшение полноты (recall), а единственный способ адаптировать модель к набору данных — вручную отредактировать список.

В качестве гибридного подхода вы можете создать большое количество предполагаемых оценок тональности для слов и поручить человеку терпеливо их отредактировать, составить список слов-исключений с нулевой тональностью. Но это дополнительная работа. С другой стороны, вы действительно увидите, как работает модель. Думаю, в любом случае к этому следует стремиться.

© Habrahabr.ru