Sentiment Analysis Report
=========================
Содержание
[toc]
# Sentiment analysis русскоязычных твитов при помощи TensorFlow.
Ссылка на [github](https://github.com/b0noI/ml-lessons/tree/master/sentiments_rus) и [видео](https://www.youtube.com/watch?v=CDpbJIbDhys&list=PLsQAG1V_t58AmRRhu-UUKvDSy-gY3Vmdh&index=4).
**Разбор кода:**
## <center>Imports</center>
```python
import pandas as pd
import numpy as np
import tensorflow as tf
import tflearn
import re
from collections import Counter
from sklearn.model_selection import train_test_split
from tflearn.data_utils import to_categorical
from nltk.stem.snowball import RussianStemmer
from nltk.tokenize import TweetTokenizer
```
## <center>Constants</center>
```python
POSITIVE_TWEETS_CSV = 'positive.csv' # имя файла, где хранятся позитивные комментарии
NEGATIVE_TWEETS_CSV = 'negative.csv' # имя файла, где хранятся негативные комментарии
VOCAB_SIZE = 5000 # размер словаря
```
## <center>Load data</center>
Загружаем данные в оперативную память (с помощью пандас).
> :information_source: Данные можно найти по ссылке [github](https://github.com/b0noI/ml-lessons/tree/master/sentiments_rus).
```python
tweets_col_number = 3
# список всех негативных и позитивных твитов
negative_tweets = pd.read_csv(
'negative.csv', header=None, delimiter=';')[[tweets_col_number]]
positive_tweets = pd.read_csv(
'positive.csv', header=None, delimiter=';')[[tweets_col_number]]
print('negative_tweets: ',negative_tweets)
print('positive_tweets: ',positive_tweets)
```
**Output:**
```
negative_tweets: 3
0 на работе был полный пиддес :| и так каждое за...
1 Коллеги сидят рубятся в Urban terror, а я из-з...
2 @elina_4post как говорят обещаного три года жд...
3 Желаю хорошего полёта и удачной посадки,я буду...
4 Обновил за каким-то лешим surf, теперь не рабо...
... ...
111918 Но не каждый хочет что то исправлять:( http://...
111919 скучаю так :-( только @taaannyaaa вправляет мо...
111920 Вот и в школу, в говно это идти уже надо(
111921 RT @_Them__: @LisaBeroud Тауриэль, не грусти :...
111922 Такси везет меня на работу. Раздумываю приплат...
[111923 rows x 1 columns]
positive_tweets: 3
0 @first_timee хоть я и школота, но поверь, у на...
1 Да, все-таки он немного похож на него. Но мой ...
2 RT @KatiaCheh: Ну ты идиотка) я испугалась за ...
3 RT @digger2912: "Кто то в углу сидит и погибае...
4 @irina_dyshkant Вот что значит страшилка :D\nН...
... ...
114906 Спала в родительском доме, на своей кровати......
114907 RT @jebesilofyt: Эх... Мы немного решили сокра...
114908 Что происходит со мной, когда в эфире #proacti...
114909 "Любимая,я подарю тебе эту звезду..." Имя како...
114910 @Ma_che_rie посмотри #непытайтесьпокинутьомск ...
[114911 rows x 1 columns]
```
## <center>Stemmer</center>
Стеммер - модуль, который находит для каждого слова его базовую единицу.
Например, для собака/собакой/собаке будет одно базовое слово "собак". (Чтобы при новом падеже слова, нейронная сеть не игнорировала слово)
```python
stemer = RussianStemmer()
# отсеивание не кириллических символов
# т.к. во многих твитах есть смайлы, и мл запоминает, что если есть смайл, то позитивный, если нет, то негативный твит
regex = re.compile('[^а-яА-Я ]')
stem_cache = {}
# token это единичка
def get_stem(token):
stem = stem_cache.get(token, None) # из кэша достает результат работы стэма
if stem: # если в кэш есть этот стеммер, то возвращает
return stem
token = regex.sub('', token).lower() # если стэма нет, то отрезает не кириллические символы
stem = stemer.stem(token) # берет стэм
stem_cache[token] = stem # записывает в кэш
return stem
```
## <center>Vocabulary creation</center>
Создадим словарь.
```python
stem_count = Counter()
tokenizer = TweetTokenizer()
# словарик - уникальные слова, которые встречались в твитах
def count_unique_tokens_in_tweets(tweets): # получаем серию твитов
for _, tweet_series in tweets.iterrows(): # пробегаемся по каждому твиту
tweet = tweet_series[3]
tokens = tokenizer.tokenize(tweet) # токинизируем (разбитие на слова, на части)
print('tokens: ',tokens)
for token in tokens: # для каждого токена берем стэм
stem = get_stem(token)
stem_count[stem] += 1 # добавляем 1
print('stem: ',stem)
count_unique_tokens_in_tweets(negative_tweets) # кол-во негативных
count_unique_tokens_in_tweets(positive_tweets) # кол-во позитивных твитов
```
**Output:**
```
tokens: ['на', 'работе', 'был', 'полный', 'пиддес', ':|', 'и', 'так', 'каждое', 'закрытие', 'месяца', ',', 'я', 'же', 'свихнусь', 'так', 'D:']
stem:
tokens: ['Коллеги', 'сидят', 'рубятся', 'в', 'Urban', 'terror', ',', 'а', 'я', 'из-за', 'долбанной', 'винды', 'не', 'могу', ':(']
stem:
tokens: ['@elina_4post', 'как', 'говорят', 'обещаного', 'три', 'года', 'ждут', '...', '(', '(']
stem:
tokens: ['Желаю', 'хорошего', 'полёта', 'и', 'удачной', 'посадки', ',', 'я', 'буду', 'очень', 'сильно', 'скучать', '(', 'http://t.co/jCLNzVNv3S']
stem:
tokens: ['Обновил', 'за', 'каким-то', 'лешим', 'surf', ',', 'теперь', 'не', 'работает', 'простоплеер', ':(']
stem:
tokens: ['Котёнка', 'вчера', 'носик', 'разбила', ',', 'плакала', 'и', 'расстраивалась', ':(']
stem:
tokens: ['@juliamayko', '@O_nika55', '@and_Possum', 'Зашли', ',', 'а', 'то', 'он', 'опять', 'затихарился', ',', 'я', 'прямо', 'физически', 'страдаю', ',', 'когда', 'он', 'долго', 'молчит', '!', '(', '(', '(']
stem:
```
Отсортируем по полуряности (частоте) и возьмем первые 5000. По причине того, что нам не нужно нейронной сети отправлять стэмы, которые встречались очень мало (т.к. нейронная сеть не сможет определить, как эти стэмы влияют на результат).
Нейронная связь может запомнить, что, к примеру, стэм был в отрцательном твите, то и в следующем твите будет негативный, не проанализировав полностью твит.
> :information_source: Нейронная связь, как студент, пытается "схалтурить".
```python
vocab = sorted(stem_count, key=stem_count.get, reverse=True)[:VOCAB_SIZE]
print(vocab[:100])
```
**Output:**
```
['', 'не', 'я', 'и', 'в', 'на', 'а', 'что', 'так', 'с', 'эт', 'как', 'у', 'мен', 'мне', 'все', 'но', 'он', 'ты', 'теб', 'ну', 'мо', 'то', 'уж', 'по', 'был', 'ещ', 'за', 'да', 'вот', 'же', 'тольк', 'нет', 'сегодн', 'о', 'прост', 'бы', 'над', 'когд', 'хоч', 'очен', 'к', 'сам', 'ден', 'будет', 'мы', 'от', 'хорош', 'из', 'есл', 'тепер', 'тож', 'буд', 'сво', 'год', 'даж', 'завтр', 'нов', 'дом', 'до', 'там', 'ест', 'вообщ', 'ег', 'вс', 'дела', 'пот', 'одн', 'для', 'больш', 'хот', 'спасиб', 'мог', 'сейчас', 'е', 'себ', 'нас', 'блин', 'раз', 'кто', 'дума', 'утр', 'котор', 'любл', 'поч', 'зна', 'говор', 'лучш', 'нич', 'без', 'ил', 'вы', 'друг', 'тут', 'чтоб', 'всем', 'бол', 'люд', 'сдела', 'сказа']
```
*Пример:*
```python
idx = 2
print("stem: {}, count: {}"
.format(vocab[idx], stem_count.get(vocab[idx])))
```
**Output:**
```
stem: я, count: 66045
```
Создадим словарь, где каждой стемме, противопоставим число, чтобы преобразовать токен в число.
```python
token_2_idx = {vocab[i] : i for i in range(VOCAB_SIZE)} # противопоставим числа до размера словаря
len(token_2_idx)
```
**Output:**
```
5000
```
*Пример:*
```python
token_2_idx['сказа'] #
```
**Output:**
```
99
```
Преобразование целого твита в вектор 0 и 1 (вектор будет передаваться на вход нейросети).
```python
def tweet_to_vector(tweet, show_unknowns=False):
vector = np.zeros(VOCAB_SIZE, dtype=np.int_) # создается вектор из нулей, который имеет длину словаря VOCAB_SIZE
for token in tokenizer.tokenize(tweet): # токенезирует твит
stem = get_stem(token) # для каждого токера берет стэмму
idx = token_2_idx.get(stem, None) # получает индекс этой стэммы, если нет в словаре, то возвращает None
if idx is not None: # если индекс найден, ставим 1
vector[idx] = 1
elif show_unknowns: # для дебагинга, показывает ненайденный стэм
print("Unknown token: {}".format(token))
return vector
```
*Пример:*
```python
# проверим
# какой-нибудь твит по индексу 3
tweet = negative_tweets.iloc[1][3]
# выведем на экран твит
print("tweet: {}".format(tweet))
# и первые 10 элементов вектора
print("vector: {}".format(tweet_to_vector(tweet)[:10]))
print(vocab[5])
```
**Output:**
```
tweet: Коллеги сидят рубятся в Urban terror, а я из-за долбанной винды не могу :(
vector: [1 1 1 0 1 0 1 0 0 0]
на
```
## <center>Converting Tweets to vectors</center>
Cоздаем огромную двумерную матрицу, в ширину = длина словаря, в длину = размер количества твитов.
В оперативной памяти преобразуем все твиты в векторы 0 и 1.
Т.к. для обучения, нужны все твиты сразу (несколько 100 тыс).
```python
tweet_vectors = np.zeros(
(len(negative_tweets) + len(positive_tweets), VOCAB_SIZE),
dtype=np.int_)
tweets = []
for ii, (_, tweet) in enumerate(negative_tweets.iterrows()):
tweets.append(tweet[3])
tweet_vectors[ii] = tweet_to_vector(tweet[3])
for ii, (_, tweet) in enumerate(positive_tweets.iterrows()):
tweets.append(tweet[3])
tweet_vectors[ii + len(negative_tweets)] = tweet_to_vector(tweet[3])
tweet_vectors
# это входные твиты
```
**Output:**
```
array([[1, 0, 1, ..., 0, 0, 0],
[1, 1, 1, ..., 0, 0, 0],
[1, 0, 0, ..., 0, 0, 0],
...,
[1, 0, 0, ..., 0, 0, 0],
[1, 0, 1, ..., 0, 0, 0],
[1, 0, 1, ..., 0, 0, 0]])
```
## <center>Preparing labels</center>
label - это вектор, который состоит из нулей (длиной негативных твитов) + единичек (длиной позитивных твитов).
```python
labels = np.append(
np.zeros(len(negative_tweets), dtype=np.int_),
np.ones(len(positive_tweets), dtype=np.int_))
# label это одномерный вектор, в каждой ячейке или 0, или 1
```
Начало вектора
```python
labels[:10] # начинается нулями
```
**Output:**
```
array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
```
Конец вектора
```python
labels[-10:] # заканчивается единичками
```
**Output:**
```
array([1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
```
## <center>Preparing the data for the training</center>
Последняя преподготовка перед обучением сети.
(положено переименовывать в Х и у)
Х = input
y = output
(т.к. функция преобразуется из х в у)
```python
X = tweet_vectors
y = to_categorical(labels, 2) # преобразование label в категориальную дату
# выход имеет либо 1 0, либо 0 1
# если в label был 0, то выдаст 0 1, если 1, то 1 0
# рандомно разбиваем Х, у на тестовые и тренинговые подмножества
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3)
```
Важно взять небольшую часть твитов на тестирование, которую сеть никогда не видела, и на них проверять качество обучения сети (т.к. сеть может запомнить все предыдущие твит).
```python
# первые 10 элеметов у
print(y_test[:10])
```
**Output:**
```
[[1. 0.]
[1. 0.]
[1. 0.]
[1. 0.]
[0. 1.]
[1. 0.]
[1. 0.]
[1. 0.]
[0. 1.]
[1. 0.]]
```
## <center>Building the NN</center>
Начнем использовать tensorflow
и строить нейросеть.
```python
def build_model(learning_rate=0.1): # подаем скорость обучения сети
# создаем новый граф нейросети
tf.compat.v1.reset_default_graph() #tf.reset_default_graph()
# создаем входной слой, передаем 2 значения
# VOCAB_SIZE - кол-во нейронов (у нас он равен размеру словаря)
# None - размер батча, в тенсорфлоу можно проводить обучение, одновременно подавая несколько твитов на вход
# default batch имеет размер 128 элементов, менять не будем
net = tflearn.input_data([None, VOCAB_SIZE])
# создаем один уровень со 125 нейронами
# fully_connected lare означает, что каждый нейрон этого уровня соединен с каждым из предыдущих
net = tflearn.fully_connected(net, 125, activation='ReLU')
# еще один связанный уровень с 25 нейронами
net = tflearn.fully_connected(net, 25, activation='ReLU')
# выходной уровень с 2 нейронами
net = tflearn.fully_connected(net, 2, activation='softmax')
# регрессия - алгоритм обучения нашей нейросети
regression = tflearn.regression(
net,
optimizer='sgd',
learning_rate=learning_rate,
loss='categorical_crossentropy')
# создаем финальную версию нашей сети и возвращаем наружу
model = tflearn.DNN(net)
return model
```
```python
model = build_model(learning_rate=0.75)
```
Начало обучения. Эмпирическим методом установлено, что для нормального обучения необходимо около 30 эпох
т.е. итераций обучения (кол-во прогонов).
```python
model.fit(
X_train,
y_train,
validation_set=0.1,
show_metric=True,
batch_size=128,
n_epoch=30)
```
**Output:**
```
Training Step: 33509 | total loss: 0.50075 | time: 14.289s
| SGD | epoch: 030 | loss: 0.50075 - acc: 0.9024 -- iter: 142848/142904
Training Step: 33510 | total loss: 0.48763 | time: 15.666s
| SGD | epoch: 030 | loss: 0.48763 - acc: 0.8919 | val_loss: 0.98446 - val_acc: 0.6822 -- iter: 142904/142904
--
```
* epoch - номер эпохи
* iter - кол-во итераиий в этой эпохе (1 итерация это полный цикл обучения)
* acc - (accuracy) точность, которую нейросеть предугадывает твиты (если = 1, значит нейросеть нашла какое-то простое правило и неправильно считает)
* total loss - функция потери
> :information_source: acc = 0.93 => не бывает такой точности у человека.
Вероятнее, в этой сетке больше нейронов, чем нужно, умнее чем надо.
Если acc близко к 1, это означает, что нейросеть нашла какое-то несложное правило.
## <center>Testing</center>
Выполним тестирование и выясним, какое будет accuracy для тестовой выборки.
```python
predictions = (np.array(model.predict(X_test))[:,0] >= 0.5).astype(np.int_)
accuracy = np.mean(predictions == y_test[:,0], axis=0)
print("Accuracy: ", accuracy)
```
**Output:**
```
Accuracy: 0.6814154090314617
```
acc равен около 0.7, следовательно 20% (93% train acc - 70% test acc) это переобучение нашей сети.
> :warning: Цель обучения, чтобы нейросеть нашла общее правило (формулу), вместо запоминания всех вариантов ответа.
Теперь сделаем такой метод, который получает на вход твит.
```python
def test_tweet(tweet):
tweet_vector = tweet_to_vector(tweet, True) # преобразовывает в вектор
positive_prob = model.predict([tweet_vector])[0][1] # просит нашу сетку предсказать
print('Original tweet: {}'.format(tweet)) # выводит оригинальный твит
print('P(positive) = {:.5f}. Result: '.format(positive_prob),
'Positive' if positive_prob > 0.5 else 'Negative') # пишет позитивный/негативный результат
```
```python
def test_tweet_number(idx):
test_tweet(tweets[idx])
```
*Пример:*
```
test_tweet_number(120705)
```
**Output:**
```
Unknown token: обладает
Unknown token: извлечь
Unknown token: выгоду
Original tweet: Он, якобы, обладает информацией, и может извлечь из нее выгоду. ::-) #RU_FF #FF_RU
P(positive) = 0.94379. Result: Positive
```
## <center>Real life testing</center>
```python
tweets_for_testing = [
"Хуже этого фееричного ужастика может быть только игра главного актера",
"Вы там все что, с ума сошли что-ли?",
"Может хватит?",
"мой друг поднялся на еверест и установил там флаг",
"Можно выдыхать спокойно, новые Star Wars олдскульно отличные. Абрамс - крутой, как и всегда.",
"меня оштрафовали по дороге домой",
"я люблю ванильное мороженое",
"меня оштрафовал контроллер"
]
for tweet in tweets_for_testing:
test_tweet(tweet)
print("---------")
```
**Output:**
```
Unknown token: фееричного
Original tweet: Хуже этого фееричного ужастика может быть только игра главного актера
P(positive) = 0.00223. Result: Negative
---------
Original tweet: Вы там все что, с ума сошли что-ли?
P(positive) = 0.67757. Result: Positive
---------
Original tweet: Может хватит?
P(positive) = 0.37129. Result: Negative
---------
Unknown token: еверест
Original tweet: мой друг поднялся на еверест и установил там флаг
P(positive) = 0.87727. Result: Positive
---------
Unknown token: выдыхать
Unknown token: олдскульно
Unknown token: Абрамс
Original tweet: Можно выдыхать спокойно, новые Star Wars олдскульно отличные. Абрамс - крутой, как и всегда.
P(positive) = 0.99995. Result: Positive
---------
Unknown token: оштрафовали
Original tweet: меня оштрафовали по дороге домой
P(positive) = 0.89308. Result: Positive
---------
Original tweet: я люблю ванильное мороженое
P(positive) = 0.82168. Result: Positive
---------
Unknown token: оштрафовал
Unknown token: контроллер
Original tweet: меня оштрафовал контроллер
P(positive) = 0.04029. Result: Negative
---------
```
Для того, чтобы понять, правильно ли прошло обучение, accuracy на тренировочных и тестовых выборках должны быть одинаковые.
Данная схема сети не самый лучший вариант для сентимент анализа твитов.
Можно добиться лучших результатов с помощью **word embedding**.
> :warning: Сложность заключается в том, что даже человек не может дать такую точность.
Например, восход на Еверест может быть позитивным твитом для одних, а для тех, у кого плохие ассоциации с Еверестом (умер друг), то твит перестает быть позитивным.
# Плотное векторное представление слов для определения тональности текста отзывов на фильмы из IMDb (Internet Movie Database)
Ссылка на [файл](https://colab.research.google.com/drive/19b8owNo7vALRTU8hbhAuJbFU-mHlwB7U) и [видео](https://www.youtube.com/watch?v=bpcr2uFBZog&list=PLtPJ9lKvJ4ojSWFe18CSKnhmmPHHIaTx-&index=5).
**Плотное векторное представление:**
Каждому токену текста (символу, слову или предложению) ставится в соответствии вектор, относительно небольшой длины и, в которой могут использоваться любые значения
(а не только 0 и 1).
Будем рассматривать на примере задач определения тональности отзывов на фильмы из набора данных IMDb.
## Набор данных IMDb movie review
[Набор данных IMDb movie review](https://ai.stanford.edu/~amaas/data/sentiment/) создан для задач определения тональности текста. Набор включает отзывы на фильмы с сайта [IMDb](https://www.imdb.com). Отзывы только явно положительные (оценка >= 7) или отрицательные (оценка <= 4), нейтральные отзывы в набор данных не включались.
Размер набора данных 50 тыс. отзывов:
- Набор данных для обучения - 25 тыс. отзывов
- Набор данных для тестирования - 25 тыс. отзывов
Количество положительных и отрицательных отзывов одинаковое.
Разметка набора данных:
- 0 - отзыв отрицательный
- 1 - отзыв положительный
С точки зрения машинного обучения это задача бинарной классификации.
Набор данных описан в статье: [Andrew L. Maas, Raymond E. Daly, Peter T. Pham, Dan Huang, Andrew Y. Ng, and Christopher Potts. (2011). Learning Word Vectors for Sentiment Analysis. The 49th Annual Meeting of the Association for Computational Linguistics (ACL 2011)](https://ai.stanford.edu/~amaas/papers/wvSent_acl2011.pdf).

**Разбор кода:**
Подключаем необходимые модули, в том числе слой Embedding, который используется для создания плотных векторных представлений в keras.
```python
from tensorflow.keras.datasets import imdb
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Embedding, Flatten, Dropout
from tensorflow.keras import utils
from tensorflow.keras.preprocessing.sequence import pad_sequences
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
```
## <center>Загружаем данные</center>
Отзывы на фильмы из набора данных imdb, с максимальнным кол-вом слов 10000.
```python
max_words=10000
```
```python
(x_train, y_train), (x_test, y_test) = imdb.load_data(num_words=max_words)
```
## <center>Просмотр данных</center>
Посмотрим исходный формат данных, здесь отзывы разделены на отдельные слова и каждое слово представлено числом, которое соответствует частоте, с которым это слово встречается в наборе данных IMDb.
Рецензия
```python
x_train[3]
```
**Output:**
```
[1,
4,
2,
2,
33,
2804,
4,
2040,
432,
111,
153,
....
....
....
30,
579,
21,
64,
2574]
```
Правильные ответы, в бинарном формате
```python
y_train[3]
```
**Output:**
```
1
```
## <center>Подготовка данных для обучения</center>
Наша нейронная сеть, может обрабатывать данные фиксированной длины, поэтому мы взяли длину макс отзыва была не более 200 слов (с помощью функции pad_sequences).
Если отзыв больше 200, обрезает, если меньше, то дополняется специальными символами заполнителями.
```pythonpython
maxlen = 200
```
```python
x_train = pad_sequences(x_train, maxlen=maxlen, padding='post')
x_test = pad_sequences(x_test, maxlen=maxlen, padding='post')
```
Наш отзыв в модифицированном варианте, где осталось 200 слов
```python
x_train[3]
```
**Output:**
```
array([ 4, 341, 7, 27, 846, 10, 10, 29, 9, 1906, 8,
97, 6, 236, 2, 1311, 8, 4, 2, 7, 31, 7,
2, 91, 2, 3987, 70, 4, 882, 30, 579, 42, 9,
12, 32, 11, 537, 10, 10, 11, 14, 65, 44, 537,
75, 2, 1775, 3353, 2, 1846, 4, 2, 7, 154, 5,
4, 518, 53, 2, 2, 7, 3211, 882, 11, 399, 38,
75, 257, 3807, 19, 2, 17, 29, 456, 4, 65, 7,
27, 205, 113, 10, 10, 2, 4, 2, 2, 9, 242,
4, 91, 1202, 2, 5, 2070, 307, 22, 7, 5168, 126,
93, 40, 2, 13, 188, 1076, 3222, 19, 4, 2, 7,
2348, 537, 23, 53, 537, 21, 82, 40, 2, 13, 2,
14, 280, 13, 219, 4, 2, 431, 758, 859, 4, 953,
1052, 2, 7, 5991, 5, 94, 40, 25, 238, 60, 2,
4, 2, 804, 2, 7, 4, 9941, 132, 8, 67, 6,
22, 15, 9, 283, 8, 5168, 14, 31, 9, 242, 955,
48, 25, 279, 2, 23, 12, 1685, 195, 25, 238, 60,
796, 2, 4, 671, 7, 2804, 5, 4, 559, 154, 888,
7, 726, 50, 26, 49, 7008, 15, 566, 30, 579, 21,
64, 2574], dtype=int32)
```
```python
y_train[3]
```
Правильный ответ остался без изменения, 1 - отзыв положительный
**Output:**
```
1
```
## <center>Создание нейронной сети</center>
В которой будем использовать плотные векторные представления
```python
# Создадим модель
model = Sequential() # нейронная сеть последовательная
model.add(Embedding(max_words, 2, input_length=maxlen)) # первый слой, который добавляем в сеть - тип Embedding, слой плотных векторных представлений слов
# 1й параметр - кол-во слов (10000),
# 2й - длина вектора (2), в котором будут представляться слова, для простоты и удобства в визуализации исп 2
# 3й - размер входных данных (maxlen=200 слов)
model.add(Dropout(0.25)) # слой, для снижения переобучения
# Embedding выдает массив, где 200 элементов размерности 2
model.add(Flatten()) # слой, преобразовывает массив в плоский вектор
model.add(Dense(1, activation='sigmoid')) # на выходе 1 нейрон, который соответствуеет задаче бинарной классификации
# функция активации 'sigmoid', который используется для бинарной классификации
```
Каким образом слой Embedding определяет векторы для представления слов?
Эти векторы определяются в процессе обучения нейронной сети. На первом этапе обучения, как и в других слоях нейронной сети, векторы в слое Embedding инициализируются случайными значениями, затем выполняется обычное обучение нейронной сети с помощью с помощью алгоритма обратного распространения ошибки.
```python
# Скомпилируем
model.compile(optimizer='adam', # оптимизатор адам
loss='binary_crossentropy', # функция ошибки бинарная перекрестная энтропия, которая подходит для задачи бинарной классификации
metrics=['accuracy']) # метрика качества обучения, доля правильных ответов
```
## <center>Обучаем нейронную сеть</center>
```python
history = model.fit(x_train, # набор данных для обучения
y_train, # данные правильных ответов
epochs=15,
batch_size=128, # размер мини выборки
validation_split=0.1) # для проверочной выборки используется 10% из набора x_train и y_train
```
**Output:**
```
Train on 22500 samples, validate on 2500 samples
Epoch 1/15
22500/22500 [==============================] - 1s 32us/sample - loss: 0.6917 - acc: 0.5206 - val_loss: 0.6854 - val_acc: 0.6048
Epoch 2/15
22500/22500 [==============================] - 1s 28us/sample - loss: 0.6307 - acc: 0.7133 - val_loss: 0.5418 - val_acc: 0.7896
Epoch 3/15
22500/22500 [==============================] - 1s 26us/sample - loss: 0.4482 - acc: 0.8316 - val_loss: 0.3975 - val_acc: 0.8460
Epoch 4/15
22500/22500 [==============================] - 1s 26us/sample - loss: 0.3428 - acc: 0.8719 - val_loss: 0.3443 - val_acc: 0.8640
Epoch 5/15
22500/22500 [==============================] - 1s 28us/sample - loss: 0.2940 - acc: 0.8883 - val_loss: 0.3183 - val_acc: 0.8708
Epoch 6/15
22500/22500 [==============================] - 1s 27us/sample - loss: 0.2619 - acc: 0.9012 - val_loss: 0.3038 - val_acc: 0.8768
Epoch 7/15
22500/22500 [==============================] - 1s 26us/sample - loss: 0.2389 - acc: 0.9120 - val_loss: 0.2937 - val_acc: 0.8792
Epoch 8/15
22500/22500 [==============================] - 1s 27us/sample - loss: 0.2234 - acc: 0.9174 - val_loss: 0.2895 - val_acc: 0.8860
Epoch 9/15
22500/22500 [==============================] - 1s 26us/sample - loss: 0.2088 - acc: 0.9215 - val_loss: 0.2855 - val_acc: 0.8840
Epoch 10/15
22500/22500 [==============================] - 1s 27us/sample - loss: 0.1955 - acc: 0.9279 - val_loss: 0.2824 - val_acc: 0.8844
Epoch 11/15
22500/22500 [==============================] - 1s 27us/sample - loss: 0.1850 - acc: 0.9332 - val_loss: 0.2821 - val_acc: 0.8836
Epoch 12/15
22500/22500 [==============================] - 1s 26us/sample - loss: 0.1763 - acc: 0.9380 - val_loss: 0.2827 - val_acc: 0.8832
Epoch 13/15
22500/22500 [==============================] - 1s 27us/sample - loss: 0.1698 - acc: 0.9401 - val_loss: 0.2832 - val_acc: 0.8836
Epoch 14/15
22500/22500 [==============================] - 1s 27us/sample - loss: 0.1623 - acc: 0.9427 - val_loss: 0.2845 - val_acc: 0.8852
Epoch 15/15
22500/22500 [==============================] - 1s 27us/sample - loss: 0.1558 - acc: 0.9450 - val_loss: 0.2867 - val_acc: 0.8872
```
```python
plt.plot(history.history['accuracy'],
label='Доля верных ответов на обучающем наборе')
plt.plot(history.history['val_accuracy'],
label='Доля верных ответов на проверочном наборе')
plt.xlabel('Эпоха обучения')
plt.ylabel('Доля верных ответов')
plt.legend()
plt.show()
```

На графике и в логах обучения видно, что мы близки к переобучению.
Доля правильных ответов на проверочном наборе перестало изменяться, и, если увеличить кол-во эпох, то вероятно, что качество распознавания начнет снижаться.
Можно сделать вывод, что плотные векторные представления слов, хорошо подходит для представления текстов в цифровом виде, чтобы его потом, можно было обрабатывать с помощью нейронной сети.
Если посмотреть на нашу нейронную сеть, то она очень простая. Содержит всего лишь 1 нейрон, который используется для классификации, и даже такая простая сеть, показывает достаточно хорошие результаты на наборе данных IMDb.
## <center>Проверяем работу сети на тестовом наборе данных</center>
Доля правильных ответов на тестовом наборе = 87%
```python
scores = model.evaluate(x_test, y_test, verbose=1)
```
**Output:**
```
25000/25000 [==============================] - 1s 38us/sample - loss: 0.3030 - acc: 0.8733
```
## <center>Исследуем обученное плотное векторное представление слов</center>
**Получаем матрицу плотных векторных представлений слов**
Теперь посмотрим на векторное представление слов, которому научилась наша нейронная сеть.
В keras векторное представление это просто веса слоя embedding, мы можем получить отдельный слой модели, с помощью вызова layers.
layers[0] - самый первый слой, у которого тип embedding
и вызываем .get_weights()[0]
Результат записываем в embedding_matrix - **переменная плотных векторных представлений слов**
```python
embedding_matrix = model.layers[0].get_weights()[0]
```
```python
embedding_matrix[:5] # векторы из 2х элементов, каждый вектор соответствует какому-то слову
```
**Output::**
```
array([[-0.01810081, -0.01080754],
[ 0.08826496, -0.08724205],
[ 0.0278171 , -0.02975242],
[ 0.01640681, -0.03693285],
[-0.01309797, 0.03457265]], dtype=float32)
```
Нулевая строка матрица соответствует слову с кодом 0, это специальный символ заполнитель.
* 2й вектор - слову с кодом 1.
* 3й - слово с кодом 2.
* 4й - слово с кодом 3.
* 5й - слово с кодом 4 и т.д.
**Загружаем словарь с номерами слов**
Попробуем получить вектор для каких-нибудь интересующих нас слов, для этого загрузим словарь, который использовался для кодирования текста в наборе данных IMDb.
```python
word_index_org = imdb.get_word_index()
```
Дополняем словарь служебными символами
Символ с кодом 0 используется для заполнения и т.д.
```python
word_index = dict()
for word,number in word_index_org.items():
word_index[word] = number + 3
word_index["<Заполнитель>"] = 0
word_index["<Начало последовательности>"] = 1
word_index["<Неизвестное слово>"] = 2
word_index["<Не используется>"] = 3
```
**Ищем векторы для слов**
В качестве примера получим вектор для слова good.
```python
word = 'good'
word_number = word_index[word]
print('Номер слова', word_number)
print('Вектор для слова', embedding_matrix[word_number])
```
**Output:**
```
Номер слова 52
Вектор для слова [-0.16588722 0.15730934]
```
Мы можем сохранить обученные плотные векторные предствления в файл, для последующего использования.
## <center>Сохраняем обученные плотные векторные представления в файл</center>
**Составляем реверсивный словарь токенов (слов)**
```python
reverse_word_index = dict()
for key, value in word_index.items():
reverse_word_index[value] = key
```
**Записываем плотные векторные представления в файл**
```python
filename = 'imdb_embeddings.csv' # задаем имя файла
```
```python
with open(filename, 'w', encoding = 'UTF-8') as f:
for word_num in range(max_words): # в цикле проходим по словарю
word = reverse_word_index[word_num] # получаем слово
vec = embedding_matrix[word_num] # и вектор
f.write(word + ",") # записываем в файл слово
f.write(','.join([str(x) for x in vec]) + "\n") # затем вектор
```
```python
import pandas as pd
df = pd.read_csv(filename)
print(df[:20])
```
**Output:**
```
<Заполнитель>,-0.018100811,-0.010807536
<Начало последовательности>,0.08826496,-0.08724205
<Неизвестное слово>,0.027817102,-0.029752418
<Не используется>,0.016406808,-0.03693285
the,-0.013097967,0.034572646
and,-0.033376634,0.08939072
a,-0.0010535739,0.010263854
of,-0.0076864795,-0.031185996
to,0.005916276,0.010063415
is,-0.029501064,0.04398097
br,0.032622937,-0.012411324
in,-0.00853795,0.014011138
it,-0.030156253,0.084226154
i,-0.014873622,0.010922928
this,-0.010680796,-0.047810882
that,0.020756848,0.01780349
was,0.012138906,-0.05804113
as,-0.023956735,-0.0013690474
for,-0.0029819263,-0.038603384
with,-0.010972076,-0.0077637797
```
## <center>Визуализация плотных векторных представлений слов</center>
Для начала визуализируем все векторы из нашей матрицы embedding_matrix.
Здесь показано 10 000 векторов, в них сложно что-то разобрать.
```python
plt.scatter(embedding_matrix[:,0], embedding_matrix[:,1])
```

Выбираем коды слов, по которым можно определить тональность отзыва (эмоциональную окраску текста)
*Пример:*
```python
review = ['brilliant', 'fantastic', 'amazing', 'good',
'bad', 'awful','crap', 'terrible', 'trash']
enc_review = []
for word in review:
enc_review.append(word_index[word])
enc_review
```
**Output:**
```
[530, 777, 480, 52, 78, 373, 595, 394, 1157]
```
Получаем векторное представление интересующих нас слов
```python
review_vectors = embedding_matrix[enc_review]
review_vectors
```
**Output:**
```
array([[-0.48637104, 0.47641388],
[-0.52597755, 0.5208416 ],
[-0.5749784 , 0.54873264],
[-0.16588722, 0.15730934],
[ 0.49321252, -0.41877759],
[ 1.0940588 , -1.0372338 ],
[ 0.57186013, -0.42061222],
[ 0.74706984, -0.6985454 ],
[ 0.24538721, -0.17148855]], dtype=float32)
```
Визуализация обученного плотного векторного представления слов, по которым можно определить эмоциональную окраску текста
```python
plt.scatter(review_vectors[:,0], review_vectors[:,1])
for i, txt in enumerate(review):
plt.annotate(txt, (review_vectors[i,0], review_vectors[i,1]))
```

Мы можем увидеть, что обучение плотных векторных представлений прошло достаточно успешно, мы видим, что в верхнем левом углу находятся прилагательные отвечающие за положительную эмоциональную окраску.
В противоположном конце прилагательные, которые отвечают за отрицательную эмоциональную окраску.
Чуть ближе к центру bad, crap, которые тоже имеют отрицательную окраску, но не такую сильную, как terrible и awful.
Еще ближе находится trash. И совсем недалеко от центра координат находится слово good. Потому что часто в отзывах пишут "not good".
# Дополнительные ресурсы
## Как делать сентимент-анализ рекуррентной LSTM сетью
[Видео](https://www.youtube.com/watch?v=Y96vctxr5GU) и [github](https://github.com/selfedu-rus/neural-network/blob/master/lesson%2024.%20LSTM%20sentiment%20analysis.py).
## Представление текста вектором One Hot Encoding
[Видео](https://youtu.be/MEalcn_iD00) и [файл](https://colab.research.google.com/drive/1pw07gkY_axF5J5qtGbUwPXS36G5tW-bO).
## Анализ тональности текста рекуррентной нейросетью
[Видео](https://youtu.be/I9RMwvyzGpM) и [файл](https://colab.research.google.com/drive/19RAxAnIV0fDXAcE1T23TPkzi4ZPFQXdB).