.

Сделать репост в соц сети!

четверг, 26 июля 2018 г.

Анализ тональности текста с использованием word2vec и реализацией в pipeline Python



Анализ тональности текста или сентимент анализ - это метод классификации текста. Самый популярный пример из курсов по машинному обучению - прогноз оценки, которую поставит посетитель ресторана заведению на основе его отзыва. Или, по другому, можем ли мы спрогнозировать на основе отзыва посетителя, будет ли он рекомендовать этот ресторан или нет.
В HR сама собой напрашивается аналогичная задача: можем ли мы спрогнозировать на основе отзыва увольняющегося работника в exit интервью спрогнозировать, будет ли он рекомендовать нашу компанию коллегам / будет ли он отзываться о компании позитивно или негативно. Хотя, безусловно, класс решаемых задач значительно шире. Я бы отослал здесь к статье Raja Sengupta Как NLP может в корне изменить HR. NLP - это Natural language processing или проще - анализ текстов. Моя задача проще - я хочу показать код для решения одной задачи в Python.

word2vec vs "bag of words"

Одним из самых популярных методов анализа текстов (а точнее, этот метод просто хронологически более ранний - и, может быть, более интуитивно понятный) является метод "мешок слов "bag of words". Мы просто получаем столько переменных, сколько у нас слов в тексте (исключая "мусорные" или редкие слова). Т.е. если в отзывах у нас используется 1 485 слов, то у нас будет 1485 новых переменных / колонок. И если в отзыве содержится слово - оно же название переменной - то переменная принимает значение "1", в противном случае "0". Т.е. если респондент написал отзыв "хорошая компания", то из 1485 ячеек напротив данного респондента будет только две "1" - в колонках "хорошая" и "компания".
Этот подход интуитивно понятен, но он имеет ряд недостатков (что делать с "не"? и т.п...), но главное: в этом подходе не отражается смысл слов, фраз.
word2vec
Преодолением такого подхода является метод word2vec (буквально 'word' to 'vector'), который превращает весь текст в N-мерное пространство, и каждое слово это вектор со своими координатами, т.е. буквально можно записать так:

'опрос': array([ 0.05069825, -0.01941545,  0.00567565, -0.0276236 ,  0.01180002,
      .......  0.00385726])
Не показываю весь вектор, потому что он имеет 100 значений. И эти 100 значений - это переменные в нашем уравнении.
"Плюс" этого метода в том, что близкие по значению слова имеют близкие координаты векторов. Например, когда я делал модель для функционала HR, то для слова "компенсации" самые близкие координаты вектора имело слово "c&b". И это замечательно, потому что в подходе "bag of words" слова "c&b" и "компенсации" это разные слова, а в подходе word2vec эти слова хоть и не идентичны, но очень близки.

Данные

У меня свой датасет, которым я с вами не поделюсь, но вы можете опробовать этот код на своих данных. Структура данных достаточно проста:
  1. переменная "текст";
  2. бинарная переменная 1/ 0, +1 / -1 и т.п..

Я свои данные взял из нашего исследования факторов текучести персонала (участвуем в исследовании) . Помимо всего прочего, в нашем опросе есть две переменные:
"Отзыв о компании";
"Готовы ли Вы рекомендовать эту компанию в качестве работодателя своим знакомым, коллегам?"


Реализация в Python


Итак, начинаем  с загрузки данных
import pandas as pd
import numpy as np
df = pd.read_csv('data.csv', sep=',', encoding = 'cp1251')
df.info()
Data columns (total 2 columns):
y 792 non-null int64
Отзыв о компании 792 non-null object
dtypes: int64(1), object(1)
df['y'].value_counts()
1    404
0    388
Name: y, dtype: int64


Да, у нас очень небольшой датасет, лучше иметь несколько тысяч, даже несколько десятков тысяч строк данных. Но моя задача скромнее - показать алгоритм. Выборка у нас достаточно сбалансированная - соотношение тех, кто готов рекомендовать компанию, и тех, кто не готов - почти 50/50.
Первая задача, которую нам надо решить - создать словарь слов. Т.е. присвоить каждому слову координаты вектора. Для этого нам необходимо взять весь текст и обучить его. Но прежде нам необходимо преобразовать наши отзывы из формата pandas в формат, годный для преобразований word2vec.
Преобразуем таким образом

import nltk
import nltk.data
tokenizer = nltk.data.load('tokenizers/punkt/english.pickle')
from nltk.tokenize import sent_tokenize, word_tokenize
import re 
 def preproc(sentence):
    sent_text = re.sub(r'[^\w\s]','', sentence)
    words = sent_text.lower().split()
    return(words)
 def senttxt(sent, tokenizer, remove_stopwords=False ):
        raw_sentences = tokenizer.tokenize(oped.strip())
        sentences = []
        for raw_sentence in raw_sentences:
            
            sentences.append(preproc(raw_sentence))
        
        len(sentences)
        return sentences
txt_snt = df['Отзыв о компании'].tolist()
sentences = []
for i in range(0,len(nyt_opeds)):
    sent = txt_snt[i].replace("/.", '')
    sentences += senttxt(sent, tokenizer)
В итоге мы получаем объект вот такого формата
sentences[0]
['классическая',
 'российская',
 'компания',
 'с',
 'назначениями',
 'не',
 'по',
 'знаниям',
 'а',
 'по',
 'личной',
 'приверженности',
 'не',
 'соблюдающая',
 'свои',
 'же',
 'правила',
 'делающая',
 'глупость',
 'за',
 'глупостью',
 'и',
 'оправдывающая',
 'их',
 'еще',
 'большими',
 'глупостями']
Этот формат уже можно использовать для создания словаря. Что мы и делаем.



from gensim.models.word2vec import Word2Vec
model = Word2Vec(size=100, min_count=1)
model.build_vocab(sentences)
Обращаю ваше внимание, что я здесь задаю минимальные параметры модели (size и min_count). Более подробно рекомендую ознакомиться models.word2vec – Word2vec embeddings. Далее мы тренируем модель и получаем объект типа dictionary (объясню позже, зачем).
model.train(sentences, total_examples=model.corpus_count, epochs=model.iter)
w2v = dict(zip(model.wv.index2word, model.wv.syn0))
w2v представляет собой вот такой объект. Это словарь dict слов, где каждое слово имеет вектор со 100 значениями (см. выше, в модели мы указали size = 100). И этот словарь нам необходим для тренировки уже модели классификации.
{'поощрялась': array([-3.5042786e-03,  5.2169146e-04,  2.1134880e-03,  3.4255565e-03,
         2.9535536e-03, -2.5336174e-04,  3.5182014e-03,  4.7578118e-03,
    .........................................................
        -2.3358944e-03,  3.9440244e-03,  7.6886819e-05, -3.4618229e-04],
       dtype=float32),
Далее я ввожу объект типа class. По сути дела это та же предобработка текста, что и выше, но поскольку мы будем модель тренировать в pipeline, то эту предобработку нам необходимо обернуть в объект типа class
class normword2vec():
        
    def transform(self, X, y=None, **fit_params):
        
        X['Отзыв о компании'] = X['Отзыв о компании'].str.strip()
        X['Отзыв о компании'] = X['Отзыв о компании'].str.lower()
        X['Отзыв о компании'] = X['Отзыв о компании'].astype(str)
        X['Отзыв о компании'] = [re.sub(r'[^\w\s]', 'hr директор', e) for e in X['Отзыв о компании']]
     
        return X['Отзыв о компании']

    def fit_transform(self, X, y=None, **fit_params):
        self.fit(X, y, **fit_params)
        return self.transform(X)

    def fit(self, X, y=None, **fit_params):
        return self
Теперь формула обработки слов. У нас каждое слово имеет размерность 100, но мы помним, что в данных у нас не одно слово соответствует "1" или "0", а целое предложение. И нам надо это предложение как то преобразовать. Для этого служит следующая формула.
class MeanVect(object):
    def __init__(self, word2vec):
        self.word2vec = word2vec
        # if a text is empty we should return a vector of zeros
        # with the same dimensionality as all the other vectors
        self.dim = len(word2vec.values())

    def fit(self, X, y):
        return self

    def transform(self, X):
        return np.array([
            np.mean([self.word2vec[w] for w in words if w in self.word2vec]
                    or [np.zeros(100)], axis=0)
            for words in X
        ])
Объект w2v нужен нам в этой формуле для извлечения векторов слов. В этой формуле self.word2vec = w2v. Вот это выражение
np.mean([self.word2vec[w] for w in words if w in self.word2vec]
                    or [np.zeros(100)], axis=0)
дает нам среднее значение вектора по всем словам предложения. Т.е. если у нас в предложение из пяти слов, то эта формула возвращает вектор размером 100, но каждое его значение является средним значением векторов всех слов в предложении. А если респондент не оставил отзыв о компании, то машина возвращает вектор из нулей np.zeros(100). Понятно, что в этом месте вы можете играться с параметром: медиана, сумма и т.п..

Теперь создаем pipeline
import sklearn
import gensim.sklearn_api
from sklearn.pipeline import Pipeline
from xgboost import XGBClassifier
from gensim.sklearn_api import W2VTransformer

xgb = XGBClassifier()

pipeline = Pipeline([
          ('selectword2vec',  normword2vec()),
      ("word2vec", MeanVect(w2v)),
   
     ('model_fitting',  xgb)]) 
from sklearn import model_selection
from sklearn.model_selection import train_test_split
y = df['y']
X = df
X_train,  X_test, y_train, y_test = model_selection.train_test_split(X, y, test_size = 0.3,random_state = 2)

pipeline.fit(X_train, y_train)

Результаты

pred = pipeline.predict(X_test)
pd.crosstab(y_test, pred)
   0  1
0 65 48
1 43 82
Confusion matrix
import seaborn as sns
import matplotlib.pylab as plt
%matplotlib inline
from matplotlib.pylab import rcParams
rcParams['figure.figsize'] = 3, 3
plt.figure(figsize=(2,2))
sns.set(font_scale=1.5)
ax = sns.heatmap((pd.crosstab(y_test, pred).apply(lambda r: r/r.sum()*100, axis=0)), cbar=None, annot=True, cmap="Blues")
ax.set_ylabel("")
ax.set_xlabel("")
plt.yticks(rotation=0, size = 15)
plt.xticks(rotation=0, size = 15)
Метрика Precision = 0, 63. Т.е. из всех, про кого мы говорим, что он будет рекомендовать компанию, прогноз оправдывается в 63 % случаев. Базовая наша точность 404/792 = 51 %, поэтому мы можем говорить, что точность модели выше случайной.
Показатели ROC кривой

from sklearn.metrics import roc_auc_score
pred_proba = pipeline.predict_proba(X_test)
print ('ROC AUC score = %0.4f' % roc_auc_score(y_test, pred_proba[:, 1]))
ROC AUC score = 0.6658
from sklearn import metrics
fpr, tpr, threshold = metrics.roc_curve(y_test, pred_proba[:, 1])
roc_auc = metrics.auc(fpr, tpr)
plt.title('Receiver Operating Characteristic')
plt.plot(fpr, tpr, color='red', lw = 2, label = 'AUC = %0.2f' % roc_auc)
plt.legend(loc = 'lower right')
plt.plot([0, 1], [0, 1])
plt.xlim([0, 1])
plt.ylim([0, 1])
plt.ylabel('True Positive Rate')
plt.xlabel('False Positive Rate')
plt.show()

Анализ тональности текста с использованием word2vec и реализацией в pipeline Python
Выше плинтуса, но не так, чтобы очень.


Bag of words

Давайте сравним на тех же данных технику bag of words для сравнения. Я дам код, но уже без комментариев, вы можете а) использовать его в работе и б) найти ошибки у меня. Под bag of words я понимаю и использую TfIdf векторизатор.

import pymorphy2
morph = pymorphy2.MorphAnalyzer()
def wrk_words(sent):
    words=word_tokenize(sent.lower())

    arr=[]
    for i in range(len(words)):
        if re.search(u'[а-яА-Яa-zA-Z&]',words[i]):
              arr.append(morph.parse(words[i])[0].normal_form)
    words1=[w for w in arr  ]
    
    return " ".join(words1)
class norm():
    
    def transform(self, X, y=None, **fit_params):
        X['Отзыв о компании'] = X['Отзыв о компании'].apply(lambda row: wrk_words(row))
        X['Отзыв о компании'] = X['Отзыв о компании'].replace('', 'пусто', regex = True)
        return X['Отзыв о компании'] 

    def fit_transform(self, X, y=None, **fit_params):
        self.fit(X, y, **fit_params)
        return self.transform(X) 

    def fit(self, X, y=None, **fit_params):
        return self
from sklearn.feature_extraction.text import TfidfVectorizer
pipeline = Pipeline([
        ('selector', norm()),
      ("word2vec", TfidfVectorizer()),
   
     ('model_fitting',  xgb)])
Результаты получились вот такими
pd.crosstab(y_test, pred)
   0   1
0  86 27
1  45 80
Confusion matrix
Анализ тональности текста с использованием word2vec и реализацией в pipeline Python
Prrecision точность модели = 0, 75, что выше 0, 63 в случае word2vec.
ROC кривая
Площадь под кривой 0, 76 против 0, 67 в случае word2vec.

Итого, на наших данных мы не смогли показать преимущество метода word2vec над методом Bag of words.

__________________________________________________________

На этом все, читайте нас в телеграмме и вконтакте


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

Отправить комментарий