<jupyter_start><jupyter_text>Открытый курс по машинному обучениюАвтор материала: <NAME> студент 4 курса механико-математического факультета МГУ. Можно использовать в любых целях (редактировать, поправлять и брать за основу), кроме коммерческих, но с обязательным упоминанием автора материала.  Word2Vec Всем привет! На протяжении данного курса мы рассмотрели различные алгоритмы машинного обучения. Но что делать если нам даны нечисловые данные и не хочется сильно раздувать пространство признаков? Для этой задачи мы рассмотрим популярную технологию Word2Vec. Опишем теорию и испытаем силы алгоритма в конкурсах ["Catch Me If You Can"](https://inclass.kaggle.com/c/catch-me-if-you-can-intruder-detection-through-webpage-session-tracking) и [Прогноз популярности статьи на Хабре](https://inclass.kaggle.com/c/howpop-habrahabr-favs), которые проводятся в рамках данного курса.  План:1. Что такое Word2Vec?    - описание    - косинусная мера    - гипепараметры модели2. Архитектура нейронной сети    - Continuous Bag of Words (CBOW)    - Skip-Gram3. Уменьшение вычислительной сложности    - Hierarchical Softmax    - Negative Sampling4. Применение на данных    - предобработка    - обучение модели    - тестирование5. Вывод6. Полезные ссылки  Что такое Word2Vec? Word2Vec – одна из технологий анализа семантики естественных языков, которая основана на векторном представлении слов согласно их семантической близости. Был разработан группой исследователей Google в 2013 году, активно применяется для семантического анализа текстов и похожих задачах.Word2vec в качестве входных данных принимает текстовый корпус и гиперпараметры, о которых поговорим позднее. Далее названия гиперпараметров и их дефолтные значения будут указаны в соответсвии с реализацией Word2Vec на open-source платформе Gensim.Суть алгоритма заключается в том, что он каждому слову сопоставляет числовой вектор определенной длины таким образом, чтобы близкие слова соответствуют близким векторам. Мерой близости слов выступает их контекстная близость т.е. близкие слова встречаются в тексте рядом с одинаковыми словами.  Например слова "девушка" и "женщина" будут часто употреблятся со словами: "красивая", "прекрасная", "чудесная", "заботливая", а слова "мужчина" и "парень" будут часто употреблятся со словами: "храбрый", "сильный", "смелый", "могучий".  Расстоянием между векторами измеряется при помощи [косинусного сходства](https://ru.wikipedia.org/wiki/%D0%92%D0%B5%D0%BA%D1%82%D0%BE%D1%80%D0%BD%D0%B0%D1%8F_%D0%BC%D0%BE%D0%B4%D0%B5%D0%BB%D1%8C) (cosine similarity). Косинусное сходство - это мера сходства между двумя векторами предгильбертового пространства, которая используется для измерения косинуса угла между ними. Косинусная мера между векторами x и y длины n вычисляется по формуле: $$cos(\theta)=\frac{(x,y)}{|x| |y|}=\frac{\sum\limits_{i=1}^{n}x_i y_i}{\sqrt{\sum\limits_{i=1}^{n}x_i^2} \sqrt{\sum\limits_{i=1}^{n}y_i^2}}$$ Обучаясь, Word2Vec максимизирует косинусную меру близости между векторами слов, которые встречаются в похожих контекстах и минимизирует косинусную меру между словами, которые не встречаются рядом.Word2Vec получает на вход слово, а на выход передает координаты вектора, соответствующие данному слову.Полученные вектора можно складывать или вычитать друг из друга, сохраняя семантические связи.  Некоторый недостаток заключается в том, что мы не имеем понятия за что отвечают полученные признаки. Т.е. у нас нет интерпретации, как на картинке ниже.   В Word2Vec можно использовать две различных архитектуры нейронной сети с помощью которых осуществляется перевод слова в вектор: - Continuous Bag of Words (CBOW);- Skip-gram.Выбор одной из этих моделей выполняется с помощью гиперпараметра ‘sg’. По умолчанию sg=0 и используется модель CBOW, при sg=1 используется модель Skip-gram. Другим гиперпараметром является размер окна, в котором рассматривается контекст данного слова. В данной реализации используется параметр ‘window’, который определяет максимальное количество слов между данным словом и соседним внутри предложения. Слова стоящие от данного дальше этого значения не будут рассматриваться как его контекст. Какое слово стоит в тексте ближе , а какое дальше от данного слова, не учитывается, при условии, что оба слова попали в окно. Вот пример с параметром window = 2  Еще один важный гиперпараметр ‘size’ - размерность векторов, соответствующих словам. Если его величина мала, то модель получается грубой и плохо отображает связь между словами внутри данного массива текстов. А при большом значении роль машинного обучения теряется, и сопоставление словам векторов может превратиться в унитарное кодирование слов (one-hot encoding). Помимо этого есть такие параметры как:alpha - начальный коэффициент скорости обучения (будет линейно падать в ходе обучения).seed = семя для воспроизводимости результатов;min_count = минимальная частота слова, чтобы оно было учтено; max_vocab_size = ограничение на выделение ОЗУ для словаря. 10млн. слов занимают примерно 1GB RAM;workers = число ядер используемых для обучения, повышает скорость;iter = число итераций на каждый текст (по умолчанию 5);sorted_vocab = 1 (значение по умолчанию),то сортирует словарь по убыванию частоты перед назначением слову индекса.  Архитектура нейронной сети Теперь более подробно рассмотрим процесс обучения и архитектуру нейронных сетей.Пусть $V$ - количество слов в словаре, $h$ - размер окна (количество соседних слов, рассматриваемых как контекст данного слова), $N$ - размерность искомых векторов.   Continuous Bag of Words (CBOW) Когда данных мало лучше использовать CBOW, т.к. она менее склонна к переобучению. Отметич, что CBOW работает быстрее, чем skip-gram, но хуже учитывает редкие слова. В данном подходе нейронная сеть предсказывает исходное слово по его контексту ($h$ соседним словам). Нейронная сеть состоит из трех слоев: входной, скрытый, выходной.На вход сети подаются $h$ векторов размерности $V$: $x_i=(x_i^1,x_i^2,...,x_i^V), i=1...h$, где $x_i^j=1$, если данное слово является $j$-ым словом из словаря, $x_i^k=0$ для $k\neq j$. На выходе имеем один вектор размерности $V$: $y=(y^1,y^2,...,y^V)$. На обучающейся выборке $y^j=1$, если предсказываемое слово является $j$-ым словом из словаря, $y^k=0$ для $k\neq j$. То есть нейронная сеть имеет $h\times V$ нейронов на входном слое и $V$ нейронов на выходном слое.На скрытом слое сети $N$ нейронов. Именно с помощью весов, расставленных перед нейронами этого слоя, мы получим координаты векторных представлений слов. Функция активации на скрытом уровне — линейная, на выходном уровне — софтмакс (softmax).Сначала рассмотрим простейший случай, когда  $h=1$.То есть будем предсказывать слово $y$ только по одному его соседу $x$. Тогда архитектура сети примет вид, изображенный на схеме:  Обозначим веса между входным и скрытым уровнями за $W$ - матрица размерности $V\times N$. Учитывая, что функция активации на скрытом слое линейная, вектор выходов размерности $N$ на скрытом уровне имеет вид: $$v=Wx$$Обозначим веса между скрытым и выходным уровнями за $W'$ - матрица размерности $N\times V$. Пусть $w'_j$ - $j$-ая строка матрицы $W'$. Тогда входной сигнал $j$-ого нейрона на выходном уровне имеет вид: $$u^j=w'_j v$$Так как на выходном уровне используется функция активации softmax, выходной сигнал на  $j$-ом нейроне выходного слоя принимает вид:$$y^j=\frac{exp(u^j)}{\sum\limits_{k=1}^{V} exp(u^k)}$$Такое представление имеет вероятностную интерпретацию. Пусть $w_I$ - входное слово, а $w_O$ - выходное слово. Обозначим получившееся выражение для $y^j$ за $p(w_j|w_I)$. Обучаясь, сеть максимизирует $y^{j*}=\frac{exp(u^{j*})}{\sum\limits_{k=1}^{V} exp(u^k)}=p(w_O|w_I)$, при условии что $w_O = w_{j*}$.Сеть обучается методом обратного распространения ошибки (backpropagation), то есть сначала корректируются веса $W'$, а затем $W$. Минимизируемый функционал потерь имеет вид:$$L=-log \ p(w_O|w_I)$$После того, как процесс обучения завершился, $V$ строк длины $N$ матрицы $W'$ дадут нам  координаты векторов, представляющих слова из словаря.Теперь рассмотрим общий случай для произвольного $h$. То есть будем предсказывать слово $y$ только по соседним словам $x_i, i=1...h$. Тогда архитектура сети примет вид, изображенный на схеме:  Обозначим веса между входными и скрытым уровнями за $W$ - матрица размерности $Vh\times N$. Учитывая, что функция активации на скрытом слое линейная, вектор выходов размерности $N$ на скрытом уровне имеет вид: $$v= \frac{1}{h} W (x_1+...+x_h)$$Далее, аналогично случаю $h=1$ получаем входной сигнал $j$ -ого нейрона на выходном слое сети $$u^j=w'_j v,$$где $w'_j$ - $j$-ая строка матрицы весов между скрытым и выходным уровнями $W'$.Пусть входные слова (соседние слова предсказываемого) - $w_{I,1},...,w_{I,h}$. Тогда выходной сигнал на  $j$-ом нейроне выходного слоя принимает вид:$$y^j=\frac{exp(u^j)}{\sum\limits_{k=1}^{V} exp(u^k)}=p(w_j|w_{I,1},...,w_{I,h})$$Как и ранее, обучаясь, сеть максимизирует $y^{j*}=\frac{exp(u^{j*})}{\sum\limits_{k=1}^{V} exp(u^k)}=p(w_O= w_{j*}|w_{I,1},...,w_{I,h})$, при условии что $w_O = w_{j*}$.Минимизируемый функционал потерь имеет вид:$$L=-log \ p(w_O|w_{I,1},...,w_{I,h})$$  Skip-Gram В случае когда данных много, то лучше использовать Skip-Gram. Данный подход обратен CBOW: по заданному слову предсказывается его контекст ($h$ соседних слов). Сеть также состоит из трех слоев: входной, скрытый, выходной.На вход сети подается вектор размерности $V$: $x=(x^1,x^2,...,x^V)$, где $x^j=1$, если данное слово является $j$-ым словом из словаря, $x^k=0$ для $k\neq j$. На выходе имеем $h$ векторов размерности $V$: $y_i=(y_i^1,y_i^2,...,y_i^V), i=1...h$. На обучающейся выборке $y_i^j=1$, если предсказываемое $i$-е слово из окна является $j$-ым словом из словаря, $y_i^k=0$ для $k\neq j$. То есть нейронная сеть имеет $V$ нейронов на входном слое и $h\times V$ нейронов на выходном слое.На скрытом слое сети, как и ранее, $N$ нейронов, а функции активации на скрытом уровне — линейная, на выходном уровне — софтмакс (softmax).Схема архитектуры сети изображена на схеме:  Обозначим веса между входным и скрытым уровнем за $W$ - матрица размерности $V\times N$. Учитывая, что функция активации на скрытом слое линейная, вектор выходов размерности $N$ на скрытом уровне имеет вид: $$v=Wx.$$Обозначим веса между скрытым и выходными уровнями за $W'$ - матрица размерности $N \times hV$. Пусть $w'_j$ - $j$-ая строка матрицы $W'$. Тогда входной сигнал $(i V+j)$-ого нейрона (соответсвует вероятности того, что $i$-е слово из контекста это $j$ слово из словаря) на выходном уровне имеет вид: $$u^{i,j}=u^j=w'_j v , i=1...h, j=1...V$$$u^{i,j}$ одинаково для всех $i$, так как слова в контексте заданного слова равнозначны, то есть все нейроны имеют одинаковые веса.Пусть $w_I$ - входное слово, а $w_{O,i}$ - $i$-е выходное слово, тогда выходной сигнал на  $(i V+j)$-ом нейроне выходного слоя имеет вид:$$y^j=\frac{exp(u^j)}{\sum\limits_{n=1}^{V} exp(u^{i V+n})}=p(w_{O,i}=w_j|w_I)$$Обучаясь, сеть максимизирует $p(w_{O,1}=w_{j_1},...,w_{O,h}=w_{j_h}|w_I)=\prod\limits_{k=1}^{h}p(w_{O,k}=w_{j_k}|w_I)$, при условии что $w_{O,k} = w_{j_k}, k=1...h$.Минимизируемый функционал потерь имеет вид:$$L=-log \ p(w_{O,1},...,w_{O,h}|w_I)$$Воспользуемся формулой Байеса:$$ p(w_{O,1},...,w_{O,h}|w_I)=\frac{p(w_I|w_{O,1},...,w_{O,h}) p(w_{O,1},...,w_{O,h})}{p(w_I)}$$Из этого выражения видно, что увеличение  $p(w_{O,1},...,w_{O,h}|w_I)$  влечет увеличение $p(w_I|w_{O,1},...,w_{O,h})$. Это соображение говорит о схожетсти описанных подходов.  Уменьшение вычислительной сложности Несложно догадаться, что без дополнительных доработок алгоритм будет работать очень медленно,  так как размерность словаря $V$ может достигать очень больших значений (нескольких сотен тысяч). Так как сеть обучается методом обратного распространения ошибки, необходимо вычислять градиент на двух шагах, что очень затратно вычислительно.Рассмотрим два метода решения этой проблемы: иерархический софтмакс (Hierarchical Softmax) и Negative Sampling.   Hierarchical Softmax Этот метод помогает эффективно вычислить значение softmax, используя бинарное дерево. По всем словам в словаре строится дерево Хаффмана. В полученном дереве $V$ слов располагаются на листьях дерева.  На рисунке изображен пример бинарного дерева. Жирным выделен путь от корня до слова $w_2$. Длину пути обозначим $L(w)$, а $j$-ую вершину на пути к слову $w$ обозначим через $n(w,j)$. Можно доказать, что внутренних вершин (не листьев) $V − 1$.С помощью иерархического softmax вектора $v_{n(w,j)}$ предсказывается для $V-1$ внутренних вершин. А вероятность того, что слово $w$ будет выходным словом (в зависимости от того, что мы предсказываем: слово из контекста или заданное слово по контексту) вычисляется по формуле:$$p(w=w_o)=\prod_{j=1}^{L(w)-1}\sigma([n(w,j+1)=lch(n(w,j))] v_{n(w,j)}^T u)$$где $\sigma()$ - функция softmax; $[true]=1,[false]=-1$; $lch(n)$ - левый сын вершины $n$; $u=v_{w_I}$, если используется метод skip-gram, $u=\frac{1}{h} \sum\limits_{k=1}^{h} v_{w_{I,k}}$, если используется CBOW.Формула можно интуитивно понять, представив, что на каждом шаге мы можем пойти налево или направо с веротяностями:$$p(n,left)=\sigma(v_n^T u)$$$$p(n,right)=1-p(n,left)=1-\sigma(v_n^T u)=\sigma(-v_n^T u)$$Затем на каждм шаге вероятности перемножаются ($L(w)-1$ шагов) и получается искомая формула.При использовании простого softmax для подсчета вероятности слова, приходилось вычислять номирующую сумму по всем словам из словаря, требовалось $O(V)$ операций. Теперь же вероятность слова можно вычислить при помощи последовательных вычислений, которые требуют $O(log(V))$.  Negative Sampling Идея этого метода состоит в том, чтобы пересчитывать функционал потерь не по всем словам из словаря, а только по меньшему количеству слов. Тем самым пересчитываться будут вектора не всех слов из словаря, а только некоторого подмножества.Для реализации данной идеи функционал потерь был изменен следующим образом: выходной вектор $w_O$ остается и обновляется, также нужно случайно выбрать  $K$ векторов слов (negative samples), которые не являются подходящими нам (не из контекста входного слова или входных слов). Предполагается, что модель устойчива к шуму и что negative samples имеют некоторое распределение $P_n (w)$, вид распределения задается произвольно.Функция потерь принимает вид:$$L=- log\sigma(v_{w_O}^T u)-\sum_{w \in W_{neg}} log\sigma(-v_{w}^T u)$$где $W_{neg}=\{w_j|j=1,...,K\}$ - $K$ случайно выбранных из словаря слов согласно распределению $P_n (w)$; $u=v_{w_I}$, если используется метод skip-gram, $u=\frac{1}{h} \sum\limits_{k=1}^{h} v_{w_{I,k}}$, если используется CBOW.После нексольких случайных генераций слов negative samples алгоритм сходится. Если параметр модели hs = 1, то будет использован hierarchical softmax . Если hs = 0 (по умолчанию),то будет использован negative sampling.  Применим на наборе данных Если вы не имеете достаточно много данных, то ваша модель может ничего полезного не выучить. Поэтому иногда имеет смысл воспользоваться уже предобученной моделью, например на wiki или новостях. Скачать можно например c [code.google](https://code.google.com/archive/p/word2vec/) Но в нашем случае необходимо учитывать особенности последовательностей сайтов. Поэтому предобученный Word2Vec нам не поможет.<jupyter_code># загрузим библиотеки и установим опции
from __future__ import division, print_function
# отключим всякие предупреждения Anaconda
import warnings
warnings.filterwarnings('ignore')
#%matplotlib inline
import numpy as np
import pandas as pd
from sklearn.metrics import roc_auc_score<jupyter_output><empty_output><jupyter_text>скачать данные можно со страницы соревнования ["Catch Me If You Can"](https://inclass.kaggle.com/c/catch-me-if-you-can-intruder-detection-through-webpage-session-tracking)<jupyter_code># загрузим обучающую и тестовую выборки
train_df = pd.read_csv('data/train_sessions.csv')#,index_col='session_id')
test_df = pd.read_csv('data/test_sessions.csv')#, index_col='session_id')

# приведем колонки time1, ..., time10 к временному формату
times = ['time%s' % i for i in range(1, 11)]
train_df[times] = train_df[times].apply(pd.to_datetime)
test_df[times] = test_df[times].apply(pd.to_datetime)

# отсортируем данные по времени
train_df = train_df.sort_values(by='time1')

# посмотрим на заголовок обучающей выборки
train_df.head()
sites = ['site%s' % i for i in range(1, 11)]
#заменим nan на 0
train_df[sites] = train_df[sites].fillna(0).astype('int').astype('str')
test_df[sites] = test_df[sites].fillna(0).astype('int').astype('str')
#создадим тексты необходимые для обучения word2vec
train_df['list'] = train_df['site1']
test_df['list'] = test_df['site1']
for s in sites[1:]:
    train_df['list'] = train_df['list']+","+train_df[s]
    test_df['list'] = test_df['list']+","+test_df[s]
train_df['list_w'] = train_df['list'].apply(lambda x: x.split(','))
test_df['list_w'] = test_df['list'].apply(lambda x: x.split(','))
#В нашем случае предложение это набор сайтов, которые посещал пользователь
#нам необязательно переводить цифры в названия сайтов, т.к. алгоритм будем выявлять взаимоствязь их друг с другом
train_df['list_w'][10]
# подключим word2vec
from gensim.models import word2vec
#объединим обучающую и тестовую выборки и обучим нашу модель на всех данных 
#с размером окна в 6=3*2(длина предложения 10 слов) и итоговыми векторами размерности 300, параметр workers отвечает за колчество ядер
test_df['target'] = -1
data = pd.concat([train_df,test_df],axis=0)

model = word2vec.Word2Vec(data['list_w'], size=300, window=3, workers=4)
#создадим словарь со словами и соответсвующими им векторами
w2v = dict(zip(model.wv.index2word, model.wv.syn0))<jupyter_output><empty_output><jupyter_text>Т.к. сейчас мы каждому слову сопоставили вектор, то нужно решить что сопоставить целому предложению из слов.Один из возможных вариантов это просто усреднить все слова в предложении и получить некоторый смысл всего предложения (если слова нет в тексте, то берем нулевой вектор).<jupyter_code>class mean_vectorizer(object):
    def __init__(self, word2vec):
        self.word2vec = word2vec
        self.dim = len(next(iter(w2v.values())))
    
    def fit(self, X):
        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(self.dim)], axis=0)
            for words in X
        ])
data_mean=mean_vectorizer(w2v).fit(train_df['list_w']).transform(train_df['list_w'])
data_mean.shape<jupyter_output><empty_output><jupyter_text>Т.к. мы получили distributed representation, то никакая фича ничего не значит, а значит лучше всего покажут себя линейные комбинации. Например нейронные сети<jupyter_code>#Воспользуемся валидацией, как в 4 дз курса ODS
def split(train,y,ratio):
    idx = round(train.shape[0] * ratio)
    return train[:idx, :], train[idx:, :], y[:idx], y[idx:]
y = train_df['target']
Xtr, Xval, ytr, yval = split(data_mean, y,0.8)
Xtr.shape,Xval.shape,ytr.mean(),yval.mean()
# подключим библиотеки keras 
from keras.models import Sequential, Model
from keras.layers import Dense, Dropout, Activation, Input
from keras.preprocessing.text import Tokenizer
from keras import regularizers
#опишем нейронную сеть
model = Sequential()
model.add(Dense(128, input_dim=(Xtr.shape[1])))
model.add(Activation('relu'))
model.add(Dropout(0.5))
model.add(Dense(1))
model.add(Activation('sigmoid'))
model.compile(loss='binary_crossentropy',
              optimizer='adam',
              metrics=['binary_accuracy'])
history = model.fit(Xtr, ytr,
                    batch_size=128,
                    epochs=10,
                    validation_data=(Xval, yval),
                    class_weight='auto',
                    verbose=0)
classes = model.predict(Xval, batch_size=128)
roc_auc_score(yval, classes)<jupyter_output><empty_output><jupyter_text>Получили результат сопоставимый, с нашим дедлайном 4 дз(0.919524709674), где использовали logistic regressiom на one-hot-encoding сайтов. Но при этом мы уменьшили размерность пространства до 300. Значит Word2Vec смог выявить зависимости между сессиями.Посмотрим, что произойдет с деревянным алгоритмом.<jupyter_code>import xgboost as xgb
dtr = xgb.DMatrix(Xtr, label= ytr,missing = np.nan)
dval = xgb.DMatrix(Xval, label= yval,missing = np.nan)
watchlist = [(dtr, 'train'), (dval, 'eval')]
history = dict()
params = {
    'max_depth': 26,
    'eta': 0.025,
    'nthread': 4,
    'gamma' : 1,
    'alpha' : 1,
    'subsample': 0.85,
    'eval_metric': ['auc'],
    'objective': 'binary:logistic',
    'colsample_bytree': 0.9,
    'min_child_weight': 100,
    'scale_pos_weight':(1)/y.mean(),
    'seed':7
}
model_new = xgb.train(params, dtr, num_boost_round=200, evals=watchlist,evals_result=history, verbose_eval=20)<jupyter_output>[0]	train-auc:0.954886	eval-auc:0.85383
[20]	train-auc:0.989848	eval-auc:0.910808
[40]	train-auc:0.992086	eval-auc:0.916371
[60]	train-auc:0.993658	eval-auc:0.917753
[80]	train-auc:0.994874	eval-auc:0.918254
[100]	train-auc:0.995743	eval-auc:0.917947
[120]	train-auc:0.996396	eval-auc:0.917735
[140]	train-auc:0.996964	eval-auc:0.918503
[160]	train-auc:0.997368	eval-auc:0.919341
[180]	train-auc:0.997682	eval-auc:0.920183<jupyter_text>Видим, что алгоритм сильно подстраивается под обучающую выборку, поэтому возможно лучше использовать линейные алгоритмы.Посмотрим, что покажет обычный LogisticRegression.<jupyter_code>from sklearn.linear_model import LogisticRegression
def get_auc_lr_valid(X, y, C=1, seed=7, ratio = 0.8):
    # разделим выборку на обучающую и валидационную
    idx = round(X.shape[0] * ratio)
    # обучение классификатора
    lr = LogisticRegression(C=C, random_state=seed, n_jobs=-1).fit(X[:idx], y[:idx])
    # прогноз для валидационной выборки
    y_pred = lr.predict_proba(X[idx:, :])[:, 1]
    # считаем качество
    score = roc_auc_score(y[idx:], y_pred)
    
    return score
get_auc_lr_valid(data_mean, y, C=1, seed=7, ratio = 0.8)<jupyter_output><empty_output><jupyter_text>Результат LogisticRegression отличается от NN на 0.02, что достаточно существенно.Попробуем улучшить результаты. Теперь вместо обычного среднего, чтобы учесть частоту с которой слово встречается в тексте, возьмем взвешенное среднее. В качестве весов возьмем idf меру слова.  Idf это инверсия частоты, с которой некоторое слово встречается в других документах. Учёт idf уменьшает вес широкоупотребительных слов и увеличивает вес более уникальных слов, которые могут достаточно точно указать на то к какому классу относится текст. В нашем случае, кому принадлежит последовательность посещенных сайтов.$$idf(w,D)=log \frac{|D|}{|{\{d \in D | w \in d\}}|}$$где $|D|$ - общее число документов, $\{d \in D | w \in d\}$ - число документов из $D$, в которых встречается слово $w$.<jupyter_code>#пропишем класс выполняющий tfidf преобразование.
from sklearn.feature_extraction.text import TfidfVectorizer
from collections import defaultdict

class tfidf_vectorizer(object):
    def __init__(self, word2vec):
        self.word2vec = word2vec
        self.word2weight = None
        self.dim = len(next(iter(w2v.values())))

    def fit(self, X):
        tfidf = TfidfVectorizer(analyzer=lambda x: x)
        tfidf.fit(X)
        # if a word was never seen - it must be at least as infrequent
        # as any of the known words - so the default idf is the max of 
        # known idf's
        max_idf = max(tfidf.idf_)
        self.word2weight = defaultdict(
            lambda: max_idf,
            [(w, tfidf.idf_[i]) for w, i in tfidf.vocabulary_.items()])

        return self

    def transform(self, X):
        return np.array([
                np.mean([self.word2vec[w] * self.word2weight[w]
                         for w in words if w in self.word2vec] or
                        [np.zeros(self.dim)], axis=0)
                for words in X
            ])
data_mean = tfidf_vectorizer(w2v).fit(train_df['list_w']).transform(train_df['list_w'])<jupyter_output><empty_output><jupyter_text>Проверим изменилось ли качество LogisticRegression.<jupyter_code>get_auc_lr_valid(data_mean, y, C=1, seed=7, ratio = 0.8)<jupyter_output><empty_output><jupyter_text>видим прирост на 0.07, значит скорее всего взвешенное среднее помогает лучше отобразить смысл всего предложения через word2vec  habr Испробуем силы алгоритма непосредственно на  текстовых данных статей хабра. Я преобразовал данные в csv таблицы. Скачать их вы можете с [train](https://yadi.sk/d/hAhCuetI3JPouk),[test](https://yadi.sk/d/mLMZZtN63JPouc)<jupyter_code>Xtrain = pd.read_csv('data/train_content.csv')
Xtest = pd.read_csv('data/test_content.csv')
print(Xtrain.shape,Xtest.shape)
Xtrain.head()<jupyter_output>(172913, 13) (5405, 12)<jupyter_text>Будем обучать модель на всем содержании статьи. Для этого совершим некоторые преобразования над текстом. Напишем функцию, которая будет преобразовывать тестовую статью в лист из слов необходимый для обучения Word2Vec.Функция получает строку, в которой содержится весь текстовый документ.1)Сначала функция будет удалять все символы кроме букв верхнего и нижнего регистра;2)Затем преобразовывает слова к нижнему регистру;3)После чего удаляет стоп слова из текста, т.к. они не несут никакой информации о содержании;4)Лемматизация, процесс приведения словоформы к лемме — её нормальной (словарной) форме.Функция возвращает лист из слов<jupyter_code>#подключим необходимые библиотеки
#
from sklearn.metrics import mean_squared_error
import re
from nltk.corpus import stopwords
import pymorphy2
morph = pymorphy2.MorphAnalyzer()

stops = set(stopwords.words("english")) | set(stopwords.words("russian"))
def review_to_wordlist(review):
    #1)
    review_text = re.sub("[^а-яА-Яa-zA-Z]"," ", review)
    #2)
    words = review_text.lower().split()
    #3)
    words = [w for w in words if not w in stops]
    #4)
    words = [morph.parse(w)[0].normal_form for w in words ]
    return(words)<jupyter_output><empty_output><jupyter_text>Лемматизация занимает много врмени, поэтому ее можно убрать в целях более быстрых подсчетов.<jupyter_code>#Преобразуем время
Xtrain['date'] = Xtrain['date'].apply(pd.to_datetime)
Xtrain['year'] = Xtrain['date'].apply(lambda x: x.year)
Xtrain['month'] = Xtrain['date'].apply(lambda x: x.month)<jupyter_output><empty_output><jupyter_text>Будем обучаться на 2015 году, а валидироваться на первым 4 месяцам 2016, т.к. в нашей тестовой выборке представлены данные за первые 4 месяца 2017 года. Более правдивую валидацию можно сделать идя по годам увеличивая нашу обучающую выборку и смотря качество на первых четырех месяцах следующего года<jupyter_code>Xtr = Xtrain[Xtrain['year']==2015]
Xval = Xtrain[(Xtrain['year']==2016)& (Xtrain['month']<=4)]
ytr = Xtr['favs_lognorm']
yval = Xval['favs_lognorm']
Xtr.shape,Xval.shape,ytr.mean(),yval.mean()
data = pd.concat([Xtr,Xval],axis = 0,ignore_index = True)
#у нас есть nan, поэтому преобразуем их к строке
data['content_clear'] = data['content'].apply(str)
%%time
data['content_clear'] = data['content_clear'].apply(review_to_wordlist)<jupyter_output>CPU times: user 16min 25s, sys: 1.96 s, total: 16min 27s
Wall time: 16min 27s<jupyter_text>моя оперативная память закончилась, я сохранил data и подгрузил ее после очистки памяти. Но нужный формат для обучения word2vec из list превратился в str, чтобы избавится от этой проблемы я воспользуюсь следующей библиотекой,которая превратит строку от списка в список.<jupyter_code>%%time
import ast
def get_list(x):
    return ast.literal_eval(x)
data['content_clear'] = data['content_clear'].apply(lambda x: ast.literal_eval(x))
%%time
model = word2vec.Word2Vec(data['content_clear'], size=300, window=10, workers=4)
w2v = dict(zip(model.wv.index2word, model.wv.syn0))<jupyter_output>CPU times: user 19min 56s, sys: 3.48 s, total: 20min
Wall time: 5min 43s<jupyter_text>Посмотрим чему выучилась модель<jupyter_code>model.wv.most_similar(positive=['open', 'data','science','best'])<jupyter_output><empty_output><jupyter_text>модель обучилась достаточно неплохо, посмотрим на результаты алгоритмов<jupyter_code>%%time
data_mean = mean_vectorizer(w2v).fit(data['content_clear']).transform(data['content_clear'])
data_mean.shape
def split(train,y,ratio):
    idx = ratio
    return train[:idx, :], train[idx:, :], y[:idx], y[idx:]
y = data['favs_lognorm']
Xtr, Xval, ytr, yval = split(data_mean, y,23425)
Xtr.shape,Xval.shape,ytr.mean(),yval.mean()
from sklearn.linear_model import Ridge
from sklearn.metrics import mean_squared_error
model = Ridge(alpha = 1,random_state=7)
model.fit(Xtr, ytr)
train_preds = model.predict(Xtr)
valid_preds = model.predict(Xval)
ymed = np.ones(len(valid_preds))*ytr.median()
print('Ошибка на трейне',mean_squared_error(ytr, train_preds))
print('Ошибка на валидации',mean_squared_error(yval, valid_preds))
print('Ошибка на валидации предсказываем медиану',mean_squared_error(yval, ymed))
%%time
data_mean_tfidf = tfidf_vectorizer(w2v).fit(data['content_clear']).transform(data['content_clear'])
y = data['favs_lognorm']
Xtr, Xval, ytr, yval = split(data_mean_tfidf, y,23425)
Xtr.shape,Xval.shape,ytr.mean(),yval.mean()
model = Ridge(alpha = 1,random_state=7)
model.fit(Xtr, ytr)
train_preds = model.predict(Xtr)
valid_preds = model.predict(Xval)
ymed = np.ones(len(valid_preds))*ytr.median()
print('Ошибка на трейне',mean_squared_error(ytr, train_preds))
print('Ошибка на валидации',mean_squared_error(yval, valid_preds))
print('Ошибка на валидации предсказываем медиану',mean_squared_error(yval, ymed))<jupyter_output>Ошибка на трейне 0.743623730976
Ошибка на валидации 0.675584372744
Ошибка на валидации предсказываем медиану 1.44601638512<jupyter_text>Попробуем нейронные сети.<jupyter_code># подключим библиотеки keras 
from keras.models import Sequential, Model
from keras.layers import Dense, Dropout, Activation, Input
from keras.preprocessing.text import Tokenizer
from keras import regularizers
from keras.wrappers.scikit_learn import KerasRegressor
def baseline_model():
    model = Sequential()
    model.add(Dense(128, input_dim=Xtr.shape[1], kernel_initializer='normal', activation='relu'))
    model.add(Dropout(0.2))
    model.add(Dense(64, activation='relu'))
    model.add(Dropout(0.5))
    model.add(Dense(1, kernel_initializer='normal'))

    model.compile(loss='mean_squared_error', optimizer='adam')
    return model
estimator = KerasRegressor(build_fn=baseline_model,epochs=20, nb_epoch=20, batch_size=64,validation_data=(Xval, yval), verbose=2)
estimator.fit(Xtr, ytr)<jupyter_output>Train on 23425 samples, validate on 7556 samples
Epoch 1/20
1s - loss: 1.7292 - val_loss: 0.7336
Epoch 2/20
0s - loss: 1.2382 - val_loss: 0.6738
Epoch 3/20
0s - loss: 1.1379 - val_loss: 0.6916
Epoch 4/20
0s - loss: 1.0785 - val_loss: 0.6963
Epoch 5/20
0s - loss: 1.0362 - val_loss: 0.6256
Epoch 6/20
0s - loss: 0.9858 - val_loss: 0.6393
Epoch 7/20
0s - loss: 0.9508 - val_loss: 0.6424
Epoch 8/20
0s - loss: 0.9066 - val_loss: 0.6231
Epoch 9/20
0s - loss: 0.8819 - val_loss: 0.6207
Epoch 10/20
0s - loss: 0.8634 - val_loss: 0.5993
Epoch 11/20
1s - loss: 0.8401 - val_loss: 0.6093
Epoch 12/20
1s - loss: 0.8152 - val_loss: 0.6006
Epoch 13/20
0s - loss: 0.8005 - val_loss: 0.5931
Epoch 14/20
0s - loss: 0.7736 - val_loss: 0.6245
Epoch 15/20
0s - loss: 0.7599 - val_loss: 0.5978
Epoch 16/20
1s - loss: 0.7407 - val_loss: 0.6593
Epoch 17/20
1s - loss: 0.7339 - val_loss: 0.5906
Epoch 18/20
1s - loss: 0.7256 - val_loss: 0.5878
Epoch 19/20
1s - loss: 0.7117 - val_loss: 0.6123
Epoch 20/20
0s - loss: 0.7069[...]
{'fraction_non_alphanumeric': 0.1140828477727895, 'fraction_numerical': 0.028369471773808386, 'mean_word_length': 5.311897106109325, 'pattern_counts': {'":': 0, '<': 61, '<?xml version=': 0, '>': 60, 'https://': 7, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '16', 'dir': 'jupyter-structured-clean-dedup', 'id': '241077', 'license': 'MIT', 'n_tokens_mistral': 13386, 'n_tokens_neox': 14499, 'n_words': 3248, 'path': 'jupyter/tutorials/word2vec_demonzheg.ipynb', 'repo_name': 'ivan-magda/mlcourse_open_homeworks', 'size': '31415'}