Анализ настроения с помощью Keras и Python

Добрый день, уважаемые читатели. Сегодня мы реализуем анализ настроения, а именно определим, является ли комментарий на русском языке «токсичным».

Мы будем использовать библиотеки Keras для построения модели, pandas для обработки данных, некоторые функции sklearn и matplotlib для визуализации процесса обучения нейронной сети.

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

Подпишись на группу Вконтакте и Телеграм-канал. Там еще больше полезного контента для программистов.

А на моем YouTube-канале ты найдешь обучающие видео по программированию. Подписывайся!

Начало работы

Я создал notebook в Kaggle, взяв данные из этого датасета.

Для начала импортируем все необходимые нам модули.

In[1]:
import os

import numpy as np

# For DataFrame object
import pandas as pd

# Neural Network
from keras.models import Sequential
from keras.layers import Dense, Dropout
from keras.optimizers import RMSprop

# Text Vectorizing
from keras.preprocessing.text import Tokenizer

# Train-test-split
from sklearn.model_selection import train_test_split

# History visualization
%matplotlib inline
import matplotlib.pyplot as plt

# Normalize
from sklearn.preprocessing import normalize

print(os.listdir("../input"))
...
Using TensorFlow backend.
['labeled.csv']

Начнём с начала:

  • os — для работы с файловой системой
  • numpy — для работы с массивами
  • pandas — для загрузки данных из .csv файла и обработки их с помощью DataFrame
  • keras — для построения модели
  • keras.preprocessing.Text — для обработки текста, чтобы подать его в числовом виде для обучения нейронной сети
  • sklearn.train_test_split — для отделения тестовых данных от тренировочных
  • matplotlib — для визуализации процесса обучения
  • sklearn.normalize — для нормализации тестовых и обучающих данных

Загрузим данные, узнав имя файла благодаря конструкции:

print(os.listdir("../input"))

Сама загрузка осуществляется с помощью функции read_csv:

In [2]:
path = '../input/labeled.csv'
df = pd.read_csv(path)
df.head()

Обработка данных

Теперь удалим символы новой строки из текстовых данных:

In [3]:
def delete_new_line_symbols(text):
    text = text.replace('\n', ' ')
    return text
In [4]:
df['comment'] = df['comment'].apply(delete_new_line_symbols)
df.head()

Из первых пяти строк таблицы стало ясно, что колонка toxic имеет вещественный тип. А ведь именно это и является нашей «целью». Приведём их к целочисленному типу и сохраним в отдельную переменную.

In [5]:
target = np.array(df['toxic'].astype('uint8'))
target[:5]

Out[5]:
array([1, 1, 1, 1, 1], dtype=uint8)

Теперь про обработку текста. Мы могли бы создать кучу функций, которые бы разбивали текст на слова, подсчитывали их кол-во, частоту и т. д. Однако Keras обладает улучшенным программным интерфейсом (хотя отдельные функции в том же подмодуле так же имеются) в виде класса Tokenizer. Создадим его экземпляр:

In [6]:
tokenizer = Tokenizer(num_words=30000, 
                      filters='!"#$%&()*+,-./:;<=>?@[\\]^_`{|}~\t\n', 
                      lower=True, 
                      split=' ', 
                      char_level=False)

Кратко пройдёмся по параметрам:

  • num_words — кол-во фиксируемых слов (самых часто встречающихся)
  • filters — последовательность символов, которые будут удаляться.
  • lower — булевый параметр, отвечающий за то, будет ли переведён текст в нижний регистр
  • split — основной символ разбиения предложения
  • char_level — указывает на то, будет ли считаться отдельный символ словом.

Документацию по этому классу можно найти здесь.

Теперь преобразуем наш набор текста с помощью этого класса:

In [7]:
tokenizer.fit_on_texts(df['comment'])
matrix = tokenizer.texts_to_matrix(df['comment'], mode='count')
matrix.shape

Out[7]:
(14412, 30000)

14 тысяч строк (образцов) и 30000 столбцов — признаков. Метод text_to_matrix имеет параметр mode, который может принимать 4 значения:

  • binary — вернёт массив, состоящий из 0 и 1, где каждый флаг будет отвечать за то, присутствует определённое слово в тексте.
  • count — простой счетчик слов
  • tfidf — текстовая обратная оценка частоты документа (TF-IDF) для каждого слова
  • freq — частота каждого слова в соответствии с другими

Теперь построим модель из двух слоёв: Dense и Dropout. Про оба эти слоя рассказывалось в предыдущих статья, советую их прочитать, если не ознакомлены с материалом.

In [8]:
def get_model():
    
    model = Sequential()
    
    model.add(Dense(32, activation='relu'))
    model.add(Dropout(0.3))
    model.add(Dense(16, activation='relu'))
    model.add(Dropout(0.3))
    model.add(Dense(16, activation='relu'))
    model.add(Dense(1, activation='sigmoid'))
    
    model.compile(optimizer=RMSprop(lr=0.0001), 
                  loss='binary_crossentropy',
                  metrics=['accuracy'])
    
    return model

Нормализируем нашу матрицу и разобьём данные на тестовые и тренировочные:

In [9]:
X = normalize(matrix)
y = target

X_train, X_test, y_train, y_test = train_test_split(X, 
                                                    y,
                                                    test_size=0.2)

X_train.shape, y_train.shape

Out[9]:
((11529, 30000), (11529,))

Обучение модели анализ настроения

Теперь приступим к обучению модели, выполняющую анализ настроения, сохранив историю в отдельную переменную. Установим счетчик эпох, равным 150, а размер батча — 500.

In [10]:
model = get_model()

history = model.fit(X_train, 
                    y_train, 
                    epochs=150, 
                    batch_size=500,
                    validation_data=(X_test, y_test))

Не буду демонстрировать полный отчёт про обучение, покажу лишь сведения о последней итерации:

Epoch 150/150
11529/11529 [==============================] - 5s 434us/step - loss: 0.0562 - acc: 0.9847 - val_loss: 0.4854 - val_acc: 0.8724

Точность на тренировочных данных составила 0.87, т. е. 87%.

Визуализируем процесс обучения с помощью matplotlib.

In [11]:
history = history.history

fig = plt.figure(figsize=(20, 10))

ax1 = fig.add_subplot(221)
ax2 = fig.add_subplot(223)

x = range(150)

ax1.plot(x, history['acc'], 'b-', label='Accuracy')
ax1.plot(x, history['val_acc'], 'r-', label='Validation accuracy')
ax1.legend(loc='lower right')

ax2.plot(x, history['loss'], 'b-', label='Losses')
ax2.plot(x, history['val_loss'], 'r-', label='Validation losses')
ax2.legend(loc='upper right')

Здесь переменная history — словарь с четырьмя ключами:

  • acc
  • val_acc
  • loss
  • val_loss

По каждому ключу имеется доступ к массиву, который имеет размерность (epochs, ).

С помощью простых манипуляций получаем вывод:

Out[11]:

<matplotlib.legend.Legend at 0x7f29440d14a8>

Как мы видим, модель вышла примерно на 75-ой эпохе, а дальше кол-во ошибок росло с каждой эпохой. Попробуйте изменить гиперпараметры модели, кол-во слоёв и посмотреть на результат. Так сказать, добавим интерактива.

Заключение

Сегодня мы рассмотрели простейший анализ тональности текста, предварительно обработав его и обучив на полученных данных модель Keras.

Результат не расстроил: точность в 0.85 довольно неплохой результат. Сказать честно, я, прочитав некоторые комментарии из набора, сам не смог определить их к нормальным или токсичным :)

Документ .ipynb с кодом из статьи можно найти здесь.

Также рекомендую прочитать статью Ансамбль с мажоритарным голосованием на Python. Подпишитесь на группу ВКонтакте, Telegram и YouTube-канал. Там еще больше полезного и интересного для разработчиков.