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

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

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


  1. urticazoku
    17.01.2019 18:14
    +1

    Было же в блоге Microsoft.


  1. Laney1
    17.01.2019 18:19

    бгг, это что, перевод без иронии?)) "Как создать модель, корректно отражающую реальность, и оскорбиться"