Современные методы анализа тональности текста

МЕНЮ


Искусственный интеллект
Поиск
Регистрация на сайте
Помощь проекту

ТЕМЫ


Новости ИИРазработка ИИВнедрение ИИРабота разума и сознаниеМодель мозгаРобототехника, БПЛАТрансгуманизмОбработка текстаТеория эволюцииДополненная реальностьЖелезоКиберугрозыНаучный мирИТ индустрияРазработка ПОТеория информацииМатематикаЦифровая экономика

Авторизация



RSS


RSS новости


Анализ тональности текста (sentiment analysis) – распространенное приложение методов обработки естественного языка (natural language processing, NLP), в частности, классификации, целью которой является извлечение из текста эмоционального содержания. Таким образом, анализ тональности можно рассматривать, как метод количественного описания качественных данных, реализуемый путем присваивания некоторых оценок настроения. Хотя тональность в общем случае субъективна, количественный анализ настроений находит много полезных применений. Например, компании получают возможность лучше понять реакцию потребителей на товар, а также могут выявлять негативные комментарии.

Простейшая форма анализа тональности реализуется с помощью словаря «хороших» и «плохих» слов. Каждое слово в предложении получает оценку: обычно +1 в случае позитивной тональности, и -1 – в случае негативной. Затем мы просто суммируем оценки всех слов, чтобы узнать общую тональность предложения. Очевидно, что этот подход имеет множество ограничений, самое существенное из которых – пренебрежение контекстом и близлежащими словами.

Например, в нашей простой модели фраза «не хороший» может быть классифицирована с оценкой тональности 0, потому что «не» имеет оценку -1, а «хороший» – оценку +1. Человек, скорее всего, классифицировал бы фразу «не хороший», как негативную, несмотря на присутствие слова «хороший».

В рамках другого распространенного метода текст рассматривается, как «мешок слов» (bag of words). Мы представляем каждый текст, как вектор размерности 1*N, где N – размер нашего словаря. Каждый столбец – это слово, а соответствующее значение – количество появлений этого слова в тексте.

Например, фраза «bag of bag of words» может быть представлена, как вектор [2, 2, 1]. Далее эту информацию можно передать алгоритму машинного обучения, такому как логистическая регрессия (logistic regression) или метод опорных векторов (support vector machine, SVM), чтобы произвести классификацию и прогнозировать тональность незнакомых данных. Обратите внимание, что для этого требуется набор данных с известными тональностями, чтобы произвести обучение с учителем. Несмотря на то, что этот подход является несколько лучше предыдущего, в данном случае также игнорируется контекст, кроме того, с увеличением объема словаря возрастает объем данных.

Word2Vec и Doc2Vec

Недавно компания Google разработала метод Word2Vec, который учитывает контекст, и в то же время уменьшает объем данных. На самом деле, Word2Vec представляет собой два разных метода: continuous bag of words (CBOW, непрерывный мешок слов) и skip-gram. Задачей метода CBOW является предсказание слова на основании близлежащих слов. У skip-gram обратная задача – предсказание набора близлежащих слов на основании одного слова (см. рис. 1).

Оба метода используют в качестве алгоритмов классификации искусственные нейронные сети. Первоначально каждое слово в словаре – случайный N-мерный вектор. Во время обучения алгоритм формирует оптимальный вектор для каждого слова с помощью метода CBOW или skip-gram.

Рисунок 1. Архитектура методов CBOW и skip-gram из публикации Efficient Estimation of Word Representations in Vector Space. w(t) – это данное слово, а w(t-2), w(t-1) и т.д. – близлежащие слова.

Рисунок 1. Архитектура методов CBOW и skip-gram из публикации Efficient Estimation of Word Representations in Vector Space. w(t) – это данное слово, а w(t-2), w(t-1) и т.д. – близлежащие слова.

Теперь векторы слов учитывают контекст близлежащих слов. Это можно увидеть, используя элементарную алгебру, чтобы определить взаимосвязи между словами (например, «king» — «man» + «woman» = «queen»). Эти векторы слов можно передать алгоритму классификации, чтобы прогнозировать тональность. Преимущество заключается в том, что теперь у нас есть контекст слова, а пространство признаков имеет значительно меньшую размерность – обычно ~300, в отличие от размерности словаря, которая составляет ~100 000.

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

Однако, даже применяя описанное выше усреднение векторов, мы игнорируем порядок слов. Для работы с текстами переменной длины Куок Ле (Quoc Le) и Томас Миколов (Tomas Mikolov) разработали метод Doc2Vec. Этот метод почти идентичен методу Word2Vec, за исключением того, что он является более общим, благодаря добавленным векторам абзаца и документа.

Метод Doc2Vec представляет собой два метода: distributed memory (DM, распределенная память) и distributed bag of words (DBOW, распределенный мешок слов). Метод DM прогнозирует слово по известным предшествующим словам и вектору абзаца. Несмотря на то, что контекст перемещается по тексту, вектор абзаца не перемещается (отсюда название «распределенная память») и позволяет учесть порядок слов. DBOW прогнозирует случайные группы слов в абзаце только на основании вектора абзаца (см. рис. 2).

Рисунок 2. Архитектура Doc2Vec из публикации Distributed Representations of Sentences and Documents.

Рисунок 2. Архитектура Doc2Vec из публикации Distributed Representations of Sentences and Documents.

После обучения векторы абзацев можно передать классификатору без необходимости агрегировать слова. В настоящее время этот метод показывает превосходные результаты при классификации тональности рецензий из базы данных IMDB, обеспечивая уровень ошибок всего 7,42%. Безусловно, все эти методы бесполезны, если мы не можем их реализовать. К счастью, хорошо оптимизированные версии Word2Vec и Doc2Vec реализованы в Python-библиотеке gensim.

Пример работы с Word2Vec на Python

В этой части мы покажем, как можно использовать векторы слов при решении задачи классификации тональности. Библиотека gensim обычно входит в состав Python-дистрибутива Anaconda или может быть установлена с помощью менеджера пакетов pip. С помощью библиотеки вы можете обучать векторы слов на своем собственном корпусе (наборе текстовых документов) или импортировать предварительно обученные векторы в текстовом или двоичном формате:

from gensim.models.word2vec import Word2Vec    model = Word2Vec.load_word2vec_format('vectors.txt', binary=False) #C text format  model = Word2Vec.load_word2vec_format('vectors.bin', binary=True) #C binary format

Это особенно полезно в отношении векторов слов, опубликованных компанией Google, которые были предварительно обучены на ~100 миллиардах слов из набора данных Google News (их можно найти в разделе «Pre-trained word and phrase vectors» здесь). Обратите внимание на то, что в несжатом состоянии размер файла составляет ~3,5 ГБ. Используя векторы слов Google, мы можем увидеть некоторые интересные взаимосвязи между словами:

from gensim.models.word2vec import Word2Vec    model = Word2Vec.load_word2vec_format('GoogleNews-vectors-negative300.bin', binary=True)    model.most_similar(positive=['woman', 'king'], negative=['man'], topn=5)    [(u'queen', 0.711819589138031),   (u'monarch', 0.618967592716217),   (u'princess', 0.5902432799339294),   (u'crown_prince', 0.5499461889266968),   (u'prince', 0.5377323031425476)]

Метод способен находить грамматические взаимосвязи, например, идентифицируя превосходные степени или глагольные основы слов:

«biggest» — «big» + «small» = «smallest»

model.most_similar(positive=['biggest','small'], negative=['big'], topn=5)    [(u'smallest', 0.6086569428443909),   (u'largest', 0.6007465720176697),   (u'tiny', 0.5387299656867981),   (u'large', 0.456944078207016),   (u'minuscule', 0.43401968479156494)]

«ate» — «eat» + «speak» = «spoke»

model.most_similar(positive=['ate','speak'], negative=['eat'], topn=5)    [(u'spoke', 0.6965223550796509),   (u'speaking', 0.6261293292045593),   (u'conversed', 0.5754593014717102),   (u'spoken', 0.570488452911377),   (u'speaks', 0.5630602240562439)]

Из приведенных выше примеров видно, что Word2Vec получил возможность определять нетривиальные взаимосвязи между словами. Поэтому векторы Google эффективны при решении многих задач обработки естественного языка, в том числе в области анализа тональности текста.

Прежде чем мы начнем использовать их для анализа тональности, давайте сначала проверим способность Word2Vec различать и кластеризовать слова. Мы будем использовать три набора слов из таких областей, как питание, спорт и погода, полученных с сайта Enchanted Learning. Поскольку векторы 300-мерные, чтобы визуализировать их в двумерном пространстве, мы применим алгоритм понижения размерности под названием t-SNE (t-distributed stochastic neighbor embedding), реализованный в библиотеке scikit-learn.

Сначала мы должны получить векторы слов следующим образом:

import numpy as np    with open('food_words.txt', 'r') as infile:      food_words = infile.readlines()    with open('sports_words.txt', 'r') as infile:      sports_words = infile.readlines()    with open('weather_words.txt', 'r') as infile:      weather_words = infile.readlines()    def getWordVecs(words):      vecs = []      for word in words:          word = word.replace('', '')          try:              vecs.append(model[word].reshape((1,300)))          except KeyError:              continue      vecs = np.concatenate(vecs)      return np.array(vecs, dtype='float') #TSNE expects float type values    food_vecs = getWordVecs(food_words)  sports_vecs = getWordVecs(sports_words)  weather_vecs = getWordVecs(weather_words)

Затем мы можем использовать t-SNE и библиотеку matplotlib, чтобы визуализировать кластеры с помощью следующего кода:

from sklearn.manifold import TSNE  import matplotlib.pyplot as plt    ts = TSNE(2)  reduced_vecs = ts.fit_transform(np.concatenate((food_vecs, sports_vecs, weather_vecs)))    #color points by word group to see if Word2Vec can separate them  for i in range(len(reduced_vecs)):      if i < len(food_vecs):          #food words colored blue          color = 'b'      elif i >= len(food_vecs) and i < (len(food_vecs) + len(sports_vecs)):          #sports words colored red          color = 'r'      else:          #weather words colored green          color = 'g'      plt.plot(reduced_vecs[i,0], reduced_vecs[i,1], marker='o', color=color, markersize=8)  

Получим следующий результат:

Рисунок 3. Алгоритм t-SNE отобразил кластеры слов из областей питания (синий цвет), спорта (красный цвет) и погоды (зеленый цвет).

Рисунок 3. Алгоритм t-SNE отобразил кластеры слов из областей питания (синий цвет), спорта (красный цвет) и погоды (зеленый цвет).

Мы видим, что Word2Vec эффективно отделил несвязанные слова и объединил связанные.

Анализ тональности твитов

Теперь мы переходим к примеру анализа тональности твитов, собранных с использованием эмотиконов в качестве поисковых запросов. Мы используем эти эмотиконы в качестве меток для данных. Эмотикон «смайлик» ( ? ) обозначает позитивную тональность, а эмотикон «грустное лицо» ( ? ) – негативную.

Набор данных содержит равное количество позитивных и негативных твитов, общее количество которых составляет ~400 000. Мы случайным образом отбираем позитивные и негативные твиты, чтобы разделить исходный набор данных на два набора для обучения и тестирования в соотношении 80/20. Затем обучаем модель Word2Vec на обучающем наборе данных.

Чтобы предотвратить утечку данных из тестового набора, мы не обучаем Word2Vec на тестовом наборе, пока наш классификатор не обучится на обучающем наборе. Чтобы создать входные данные для классификатора, используем усредненный вектор, полученный из всех векторов слов в твите. Для реализации машинного обучения будем использовать библиотеку scikit-learn.

Вначале импортируем данные и обучаем модель Word2Vec.

from sklearn.cross_validation import train_test_split  from gensim.models.word2vec import Word2Vec    with open('twitter_data/pos_tweets.txt', 'r') as infile:      pos_tweets = infile.readlines()    with open('twitter_data/neg_tweets.txt', 'r') as infile:      neg_tweets = infile.readlines()    #use 1 for positive sentiment, 0 for negative  y = np.concatenate((np.ones(len(pos_tweets)), np.zeros(len(neg_tweets))))    x_train, x_test, y_train, y_test = train_test_split(np.concatenate((pos_tweets, neg_tweets)), y, test_size=0.2)    #Do some very minor text preprocessing  def cleanText(corpus):      corpus = [z.lower().replace('','').split() for z in corpus]      return corpus    x_train = cleanText(x_train)  x_test = cleanText(x_test)    n_dim = 300  #Initialize model and build vocab  imdb_w2v = Word2Vec(size=n_dim, min_count=10)  imdb_w2v.build_vocab(x_train)    #Train the model over train_reviews (this may take several minutes)  imdb_w2v.train(x_train)

Затем мы должны создать векторы слов для входного текста, чтобы усреднить все векторы слов в твите:

#Build word vector for training set by using the average value of all word vectors in the tweet, then scale  def buildWordVector(text, size):      vec = np.zeros(size).reshape((1, size))      count = 0.      for word in text:          try:              vec += imdb_w2v[word].reshape((1, size))              count += 1.          except KeyError:              continue      if count != 0:          vec /= count      return vec

Масштабирование – это часть процесса стандартизации, в рамках которого мы приводим наш набор данных к гауссовому распределению со средним значением, равным нулю.

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

from sklearn.preprocessing import scale  train_vecs = np.concatenate([buildWordVector(z, n_dim) for z in x_train])  train_vecs = scale(train_vecs)    #Train word2vec on test tweets  imdb_w2v.train(x_test)

Наконец, мы должны создать векторы тестового набора и масштабировать их для оценки.

#Build test tweet vectors then scale  test_vecs = np.concatenate([buildWordVector(z, n_dim) for z in x_test])  test_vecs = scale(test_vecs)

Далее мы проверим наш классификатор, рассчитав точность прогнозирования на тестовых данных, а также проанализируем ROC-кривую. ROC-кривая характеризует соотношение истинноположительных и ложноположительных результатов классификации при варьировании параметра модели.

В нашем случае мы регулируем пороговую вероятность классификации твита, как позитивного или негативного. В общем случае, чем больше площадь под кривой (AUC), тем лучше наша модель максимизирует количество истинноположительных и минимизирует количество ложноположительных результатов. Подробную информацию о ROC-кривой можно найти здесь.

Для начала мы обучаем наш классификатор, используя в данном случае стохастический градиентный спуск (stochastic gradient descent) для логистической регрессии.

#Use classification algorithm (i.e. Stochastic Logistic Regression) on training set, then assess model performance on test set  from sklearn.linear_model import SGDClassifier    lr = SGDClassifier(loss='log', penalty='l1')  lr.fit(train_vecs, y_train)    print 'Test Accuracy: %.2f'%lr.score(test_vecs, y_test)

Затем для оценки создадим ROC-кривую с помощью библиотеки matplotlib и функции roc_curve из пакета metrics библиотеки scikit-learn.

#Create ROC curve  from sklearn.metrics import roc_curve, auc  import matplotlib.pyplot as plt    pred_probas = lr.predict_proba(test_vecs)[:,1]    fpr,tpr,_ = roc_curve(y_test, pred_probas)  roc_auc = auc(fpr,tpr)  plt.plot(fpr,tpr,label='area = %.2f' %roc_auc)  plt.plot([0, 1], [0, 1], 'k--')  plt.xlim([0.0, 1.0])  plt.ylim([0.0, 1.05])  plt.legend(loc='lower right')    plt.show()

Полученная кривая имеет вид:

Рисунок 4. ROC-кривая для результатов, полученных логистическим классификатором на обучающих данных из набора твитов.

Рисунок 4. ROC-кривая для результатов, полученных логистическим классификатором на обучающих данных из набора твитов.

Без процедуры создания признаков и с минимальной предобработкой текста мы можем получить точность 73%, используя простую линейную модель из библиотеки scikit-learn. Примечательно, что удаление знаков препинания снижает точность. Это означает, что Word2Vec способен находить полезные признаки, когда присутствуют такие символы, как «?» и «!».

Работа с отдельным словам, более длительное обучение, более серьезная предобработка и настройка параметров Word2Vec и классификатора могут помочь повысить точность.

Используем Doc2Vec для анализа рецензий на кинофильмы

В случае твитов использование усредненных векторов слов давало хорошие результаты. Это происходит потому, что твиты обычно содержат не более нескольких десятков слов, что позволяет нам сохранить соответствующие признаки даже при усреднении. Однако если мы будем игнорировать порядок слов и контекст в масштабе абзацев, мы рискуем потерять важные признаки.

В этом случае лучше использовать Doc2Vec, чтобы создать входные признаки. В качестве примера мы будем использовать набор данных IMDB, содержащий рецензии на кинофильмы, чтобы протестировать эффективность Doc2Vec в области анализа тональности текста. Набор данных содержит 25 000 позитивных рецензий, 25 000 негативных и 50 000 рецензий, тональность которых неизвестна.

Сначала мы обучаем Doc2Vec на рецензиях неизвестной тональности. Дальнейшая методика идентична примеру для Word2Vec, представленному выше, за исключением того, что мы будем использовать в качестве входных данных векторы обоих методов DM и DBOW, объединив их.

import gensim    LabeledSentence = gensim.models.doc2vec.LabeledSentence    from sklearn.cross_validation import train_test_split  import numpy as np    with open('IMDB_data/pos.txt','r') as infile:      pos_reviews = infile.readlines()    with open('IMDB_data/neg.txt','r') as infile:      neg_reviews = infile.readlines()    with open('IMDB_data/unsup.txt','r') as infile:      unsup_reviews = infile.readlines()    #use 1 for positive sentiment, 0 for negative  y = np.concatenate((np.ones(len(pos_reviews)), np.zeros(len(neg_reviews))))    x_train, x_test, y_train, y_test = train_test_split(np.concatenate((pos_reviews, neg_reviews)), y, test_size=0.2)    #Do some very minor text preprocessing  def cleanText(corpus):      punctuation = """.,?!:;(){}[]"""      corpus = [z.lower().replace('','') for z in corpus]      corpus = [z.replace('<br />', ' ') for z in corpus]        #treat punctuation as individual words      for c in punctuation:          corpus = [z.replace(c, ' %s '%c) for z in corpus]      corpus = [z.split() for z in corpus]      return corpus    x_train = cleanText(x_train)  x_test = cleanText(x_test)  unsup_reviews = cleanText(unsup_reviews)    #Gensim's Doc2Vec implementation requires each document/paragraph to have a label associated with it.  #We do this by using the LabeledSentence method. The format will be "TRAIN_i" or "TEST_i" where "i" is  #a dummy index of the review.  def labelizeReviews(reviews, label_type):      labelized = []      for i,v in enumerate(reviews):          label = '%s_%s'%(label_type,i)          labelized.append(LabeledSentence(v, [label]))      return labelized    x_train = labelizeReviews(x_train, 'TRAIN')  x_test = labelizeReviews(x_test, 'TEST')  unsup_reviews = labelizeReviews(unsup_reviews, 'UNSUP')

Создаем объекты типа LabeledSentence:

<gensim.models.doc2vec.LabeledSentence at 0xedd70b70>

Далее создаем экземпляры двух моделей Doc2Vec – DM и DBOW. Документация gensim рекомендует применять многократное обучение, а также либо варьировать скорость обучения, либо использовать случайный порядок входных данных при каждом проходе. Затем мы собираем векторы рецензий, на которых обучались модели.

import random    size = 400    #instantiate our DM and DBOW models  model_dm = gensim.models.Doc2Vec(min_count=1, window=10, size=size, sample=1e-3, negative=5, workers=3)  model_dbow = gensim.models.Doc2Vec(min_count=1, window=10, size=size, sample=1e-3, negative=5, dm=0, workers=3)    #build vocab over all reviews  model_dm.build_vocab(np.concatenate((x_train, x_test, unsup_reviews)))  model_dbow.build_vocab(np.concatenate((x_train, x_test, unsup_reviews)))    #We pass through the data set multiple times, shuffling the training reviews each time to improve accuracy.  all_train_reviews = np.concatenate((x_train, unsup_reviews))  for epoch in range(10):      perm = np.random.permutation(all_train_reviews.shape[0])      model_dm.train(all_train_reviews[perm])      model_dbow.train(all_train_reviews[perm])    #Get training set vectors from our models  def getVecs(model, corpus, size):      vecs = [np.array(model[z.labels[0]]).reshape((1, size)) for z in corpus]      return np.concatenate(vecs)    train_vecs_dm = getVecs(model_dm, x_train, size)  train_vecs_dbow = getVecs(model_dbow, x_train, size)    train_vecs = np.hstack((train_vecs_dm, train_vecs_dbow))    #train over test set  x_test = np.array(x_test)    for epoch in range(10):      perm = np.random.permutation(x_test.shape[0])      model_dm.train(x_test[perm])      model_dbow.train(x_test[perm])    #Construct vectors for test reviews  test_vecs_dm = getVecs(model_dm, x_test, size)  test_vecs_dbow = getVecs(model_dbow, x_test, size)    test_vecs = np.hstack((test_vecs_dm, test_vecs_dbow))

Теперь мы готовы обучить классификатор на векторах рецензий. Мы снова будем использовать классификатор на основе стохастического градиентного спуска SGDClassifier из библиотеки scikit-learn.

from sklearn.linear_model import SGDClassifier    lr = SGDClassifier(loss='log', penalty='l1')  lr.fit(train_vecs, y_train)    print 'Test Accuracy: %.2f'%lr.score(test_vecs, y_test)

Эта модель обеспечивает точность 0,86. Мы также можем построить ROC-кривую для данного классификатора:

#Create ROC curve  from sklearn.metrics import roc_curve, auc  %matplotlib inline  import matplotlib.pyplot as plt    pred_probas = lr.predict_proba(test_vecs)[:,1]    fpr,tpr,_ = roc_curve(y_test, pred_probas)  roc_auc = auc(fpr,tpr)  plt.plot(fpr,tpr,label='area = %.2f' %roc_auc)  plt.plot([0, 1], [0, 1], 'k--')  plt.xlim([0.0, 1.0])  plt.ylim([0.0, 1.05])  plt.legend(loc='lower right')    plt.show()
Рисунок 5. ROC-кривая для результатов, полученных логистическим классификатором на обучающих данных из набора рецензий IMDB.

Рисунок 5. ROC-кривая для результатов, полученных логистическим классификатором на обучающих данных из набора рецензий IMDB.

В оригинальной публикации сказано, что наблюдалось улучшение результатов при использовании нейронной сети из 50 узлов по сравнению с простым классификатором на основе логистической регрессии:

from NNet import NeuralNet    nnet = NeuralNet(50, learn_rate=1e-2)  maxiter = 500  batch = 150  _ = nnet.fit(train_vecs, y_train, fine_tune=False, maxiter=maxiter, SGD=True, batch=batch, rho=0.9)    print 'Test Accuracy: %.2f'%nnet.score(test_vecs, y_test)

Интересно, что в данном случае мы не наблюдаем подобного улучшения. Точность составляет 0,85, и мы не достигли заявленного уровня ошибок 7,42%.

Причиной могли стать многие факторы: возможно, в нашем случае обучение на обучающих/тестовых данных происходило в течение недостаточного количества эпох, мы использовали другие реализации Doc2Vec/ИНС, другие гиперпараметры и т.д.

Трудно точно сказать, какая именно была причина, поскольку в публикации не приводятся все подробности. В любом случае, нам удалось получить точность 86% и при этом не пришлось делать существенную предобработку и создавать/отбирать признаки. Нам не потребовалась сложная свертка или корпус с синтаксической разметкой (treebank)!

Заключение

Надеемся, вы оценили не только полезность, но и легкость использования Word2Vec и Doc2Vec с помощью стандартных инструментов, таких как Python и библиотека gensim. С помощью очень простого алгоритма мы можем получить насыщенные векторы слов и абзацев, которые могут быть использованы при решении любых задач в области обработки естественного языка. Кроме того, очень полезны опубликованные компанией Google векторы слов, предварительно обученные на огромном объеме данных.

Если вы хотите обучить свои собственные векторы на больших наборах данных, для этого существует реализация Word2Vec в библиотеке Apache Spark MLlib. Желаем успехов в области обработки естественного языка!

По материалам: District Data Labs

Автор публикации

не в сети 8 часов

Лариса Шурига


Источник: datareview.info

Комментарии: