Время прочтения: 6 мин.

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

Давайте представим, что у нас есть набор, например, отзывов на новый фильм или публикаций из социальной сети. А наша задача проанализировать каждый из них на предмет тональной оценки.  Можно просматривать все вручную – это вариант, бесспорно. Только есть несколько «но». Даже на небольшой набор, порядка 100-200 записей, уже уйдет уйма времени, а кроме того всегда будет иметь место человеческий фактор, поскольку велика вероятность, о чем-то забыть, запутаться или что-то упустить.  Можно ли как-то упростить эту задачу? Данным вопросом люди занимаются уже не один год, и было придумано несколько способов классификации, а именно: методы, использующие правила и тезаурусы (словари), в которых вручную или машинными методами произведен тональный анализ слов; машинное обучение с учителем и без него, а также методы, использующие теоретико-графовые модели — идея которых заключается в построении графа с ранжированием его вершин по весу, где вершинами являются слова из анализируемого текста.

Как вы уже могли заметить сфера применения тонального анализа довольно обширна, в Интернете в общем доступе находятся самые различные корпусы данных для обучения, например, датасеты отзывов с Amazon и Rotten Tomatoes, спам-письма, публикации из различных соц.сетей, записи дебатов и многое другое. Мы рассмотрим обучение модели с учителем на основе готового корпуса размеченных публикаций, собранных из социальной сети Twitter.

Корпус размещен на ресурсе study.mokoron.com, здесь вы можете найти данные в двух форматах — csv и sql. Для работы использовались публикации, сохраненные в формате csv.  Далее для унификации требовалось решить задачу предобработки данных. Для начала необходимо было удалить все ненужные символы, пунктуацию и тд. Здесь удобно использовать регулярные выражения с импортом модуля re. Пример метода представлен ниже, здесь и далее в переменной data хранятся публикации, другими словами твиты:

def delete_rubbish(data):
    return [re.sub('[^А-Яа-яё| ]', ' ', i) for i in data]

В речи мы часто используем много слов, не несущих никакой эмоциональной окраски, но для нас они могут быть нужны для лучшего понимания смысла высказывания. У модели же такой потребности нет, поэтому все подобные речевые единицы лучше удалять. Корпус таких стоп-слов можно найти в библиотеки для работы с естественным языком NLTK. Кроме того, в методе при обработке данных используется токенизация данных из этой же библиотеки. Токенизация —  разбиение предложения на отдельные языковые единицы.

from nltk.corpus import stopwords
from nltk.tokenize import word_tokenize

Кроме того, к стоп-словам можно отнести персональные имена. Набор персональных русских имен можно найти в Интернете. 

def delete_stop_words(data):
    #stopwords и PERSON_NAMES - заранее подготовленные наборы слов
    stop_words = stopwords.words("russian")
    names = PERSON_NAMES
    result = []
    for item in data:
        # word_tokenize - токенизация
        tokens = [token for token in word_tokenize(item)
                    if token not in stop_words
                    and token not in names
                    and token.isalpha() and len(token) > 1]
        clear_data = " ".join(tokens)
        result.append(clear_data)
    return result

Переходим к следующему этапу – лемматизация, то есть приведение токенов к нормальной словарной форме. Поскольку для модели важна смысловая окраска слова, а не его форма. Например, оценка для токенов «прекрасный» и «прекрасная» должна быть одинаковая. Для лемматизации используется библиотека pymystem3 от Яндекс:

from pymystem3 import Mystem

Лемматизация каждого отдельного предложения довольно затратна по времени, так, например, чтобы обработать наш корпус публикаций понадобится порядка 2-3 часов, в лучшем случае. С целью ускорения этого процесса, данные объединяются в группы вида:
«Публикация1 flag Публикация2 flag … flag Публикация1000», где flag – служит разделителем, с помощью которого мы сможем обратно разбить наши данные. Размерность групп в примере кода ниже была равна 1000, но ее также можно менять под свои запросы:

def stemmer_lemmatizer(data):
    my_stem = Mystem()

    def function(lst, sz):
        return [lst[i:i + sz] for i in range(0, len(lst), sz)]

    tweet = function(data, 1000)
    result = []
    for temp in tweet:
        all_tweets = ' '.join([txt + ' flag ' for txt in temp])

        # лемматизация набора слов
        words = my_stem.lemmatize(all_tweets)

        current = []
        for word in words:
            if word != '\n' and word.strip() != '':
                if word == 'flag':
                    result.append(current)
                    current = []
                else:
                    current.append(word)
    return result

Пример работы:

На этом этапе данные полностью предобработаны, теперь мы можем привести текстовый формат к численному представлению, чтобы начать обучение модели. Для этого воспользуемся методами библиотеки Keras (обеспечивает удобное взаимодействие с нейронными сетями, представляет собой надстройку над фреймворком TensorFlow для машинного обучения), такими как:

from keras.preprocessing.text import Tokenizer
from keras.preprocessing.sequence import pad_sequence

Значение переменных max_word и max_len выбирается в зависимости от задачи. Далее представлен код векторизации данных:

max_word = 5000
max_len = 200
tokenizer = Tokenizer(num_words=max_words)
tokenizer.fit_on_texts(data)
sequences = tokenizer.texts_to_sequences(data)
tweets = pad_sequences(sequences, maxlen=max_len)

Также векторизуем значения лейблов-оценок:

labels = np.array(data['label'])
y = []
for i in range(len(labels)):
    if labels[i] == 'neutral':
        y.append(0)
    elif labels[i] == 'negative':
        y.append(1)
    elif labels[i] == 'positive':
        y.append(2)
y = np.array(y)
labels = tf.keras.utils.to_categorical(y, 3, dtype="float32")
del y

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

X_train, X_test, y_train, y_test = train_test_split(tweets,labels, random_state=0)

Далее представлено обучение нейронной сети с глубиной 1 на 70 эпохах, здесь мы добавляем необходимое количество слоев, проверяем точность на каждой эпохе и сохраняем модель с лучшей точностью:

model1 = Sequential()
model1.add(layers.Embedding(max_words, 20))
model1.add(layers.LSTM(15,dropout=0.5))
model1.add(layers.Dense(3,activation='softmax'))


model1.compile(optimizer='rmsprop',loss='categorical_crossentropy', metrics=['accuracy'])
checkpoint1 = ModelCheckpoint("best_model1.hdf5", monitor='val_accuracy', verbose=1,save_best_only=True, mode='auto', period=1,save_weights_only=False)
history = model1.fit(X_train, y_train, epochs=70,validation_data=(X_test, y_test),callbacks=[checkpoint1])

Аналогично обучение нейронной сети с глубиной 2 на 70 эпохах:

model2 = Sequential()
model2.add(layers.Embedding(max_words, 40, input_length=max_len))
model2.add(layers.Bidirectional(layers.LSTM(20,dropout=0.6)))
model2.add(layers.Dense(3,activation='softmax'))


model2.compile(optimizer='rmsprop',loss='categorical_crossentropy', metrics=['accuracy'])
checkpoint2 = ModelCheckpoint("best_model2.hdf5", monitor='val_accuracy', verbose=1,save_best_only=True, mode='auto', period=1,save_weights_only=False)
history = model2.fit(X_train, y_train, epochs=70,validation_data=(X_test, y_test),callbacks=[checkpoint2])

По итогу обучения были получены две модели с точностями работы ~65% (первая модель) и ~75%(вторая модель). Для дальнейшей работы, конечно, выбирается модель с большей точностью, в нашем случае это нейронная сеть с глубиной 2. Пример ее работы представлен ниже:

from keras.models import load_model

model = load_model("best_model2.hdf5")
result = model.predict(text)

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