In [ ]:
#@title Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

In [ ]:
#@title MIT License
#
# Copyright (c) 2017 François Chollet
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.

Классификация текста обзоров фильмов

Note: Вся информация в этом разделе переведена с помощью русскоговорящего Tensorflow сообщества на общественных началах. Поскольку этот перевод не является официальным, мы не гарантируем что он на 100% аккуратен и соответствует официальной документации на английском языке. Если у вас есть предложение как исправить этот перевод, мы будем очень рады увидеть pull request в tensorflow/docs репозиторий GitHub. Если вы хотите помочь сделать документацию по Tensorflow лучше (сделать сам перевод или проверить перевод подготовленный кем-то другим), напишите нам на docs-ru@tensorflow.org list.

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

Мы воспользуемся датасетом IMDB, который содержит тексты 50,000 обзоров фильмов из Internet Movie Database. Они разделены на 25,000 обзоров для обучения, и 25,000 для проверки модели. Тренировочные и проверочные датасеты сбалансированы, т.е. содержат одинаковое количество позитивных и негативных обзоров.

Данное руководство использует tf.keras, высокоуровневый API для создания и обучения моделей в TensorFlow. Чтобы сделать более сложную по структуре классификацую текста при помощи tf.keras, читай Руководство по классификации текстов.


In [ ]:
# keras.datasets.imdb is broken in 1.13 and 1.14, by np 1.16.3
!pip install tf_nightly

In [ ]:
import tensorflow.compat.v1 as tf

from tensorflow import keras

import numpy as np

print(tf.__version__)

Загружаем датасет IMDB

Датасет IMDB доступен сразу в TensorFlow при помощи метода load_data. Он уже подготовлен таким образом, что обзоры (последовательности слов) были конвертированы в последовательность целых чисел, где каждое целое представляет конкретное слово в массиве.

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


In [ ]:
imdb = keras.datasets.imdb

(train_data, train_labels), (test_data, test_labels) = imdb.load_data(num_words=10000)

Аргумент num_words=10000 позволяет нам ограничиться только 10,000 наиболее часто встречающимися словами из тренировочного сета. Все редкие слова исключаются. Это поможет нам держать объем данных в разумных пределах.

Знакомимся с данными

Давай посмотрим какая информация нам доступна. Данные уже подготовлены: каждый пример - это массив целых чисел, которые представляют слова из обзоров. Каждая метка label является целым числом 0 или 1:

0 - негативный обзор, 1 - позитивный.


In [ ]:
print("Тренировочных записей: {}, меток: {}".format(len(train_data), len(train_labels)))

Текст обзоров уже был конвертирован в целые числа, где каждое целок представляет слово из словаря. Вот пример того, как выглядит первый обзор:


In [ ]:
print(train_data[0])

Разные обзоры фильмов также имеют разное количество слов. Код ниже поможет нам узнать количество слов в первом и втором обзоре. Поскольку нейросеть может принимать только данные одинаковой длины, то нам предстоит как-то решить эту задачу.


In [ ]:
len(train_data[0]), len(train_data[1])

Конвертируем целые обратно в слова

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


In [ ]:
# Назначим словарь, который будет отображать слова из массива данных
word_index = imdb.get_word_index()

# Зарезервируем первые несколько значений
word_index = {k:(v+3) for k,v in word_index.items()}
word_index["<PAD>"] = 0
word_index["<START>"] = 1
word_index["<UNK>"] = 2  # Вместо редких слов, не вошедших в набор из 10,000, будет указано UNK
word_index["<UNUSED>"] = 3

reverse_word_index = dict([(value, key) for (key, value) in word_index.items()])

def decode_review(text):
    return ' '.join([reverse_word_index.get(i, '?') for i in text])

Теперь мы можем легко воспользоваться функцией decode_review для отображения текста первого обзора фильма:


In [ ]:
decode_review(train_data[0])

Подготавливаем данные

Обзоры фильмов из массива целых чисел должны быть конвертированы в тензоры прежде, чем они будут пропущены через нейросеть. Эта конвертация может быть сделана несколькими способами:

  • One-hot encoding конвертирует массивы в векторы 0 и 1. Например, последовательность [3, 5] станет 10,000-мерным вектором, полностью состоящим из нулей кроме показателей 3 и 5, которые будут представлены единицами. Затем, нам нужно будет создать первый Dense слой в нашей сети, который сможет принимать векторые данные с плавающей запятой. Такой подход очень требователен к объему памяти, несмотря на то, что требует указать размеры матрицы num_words * num_reviews

  • Другой способ - сделать все массивы одинаковыми по длине, а затем создать тензор целых чисел с указанием max_length * num_reviews. Мы можем использовать Embedding (пер. "Встроенный") слой, который может использовать эти параметры в качестве первого слоя нашей сети

В этом руководстве мы попробуем второй способ.

Поскольку все обзоры фильмов должны быть одинаковой длины, то мы используем функцию pad_sequences, чтобы привести все длины к одному значению:


In [ ]:
train_data = keras.preprocessing.sequence.pad_sequences(train_data,
                                                        value=word_index["<PAD>"],
                                                        padding='post',
                                                        maxlen=256)

test_data = keras.preprocessing.sequence.pad_sequences(test_data,
                                                       value=word_index["<PAD>"],
                                                       padding='post',
                                                       maxlen=256)

Давай теперь посмотрим на длину наших примеров:


In [ ]:
len(train_data[0]), len(train_data[1])

А также проверим как выглядит первый стандартизированный по длине обзор фильма:


In [ ]:
print(train_data[0])

Строим модель

Нейронная сеть создается посредством стека (наложения) слоев - это требует ответов на два вопроса об архитектуре самой модели:

  • Сколько слоев будет использовано в модели?
  • Сколько скрытых блоков будет использовано для каждого слоя?

В этом примере, входные данные состоят из массива слов (целых чисел). Получаемые предсказания будут в виде меток 0 или 1. Давай построим модель, которая будет решать нашу задачу:


In [ ]:
# Размер входных данных - количество слов, использованных в обзорах фильмов (10,000 слов)
vocab_size = 10000

model = keras.Sequential()
model.add(keras.layers.Embedding(vocab_size, 16, input_shape=(None,)))
model.add(keras.layers.GlobalAveragePooling1D())
model.add(keras.layers.Dense(16, activation=tf.nn.relu))
model.add(keras.layers.Dense(1, activation=tf.nn.sigmoid))

model.summary()

Для создания классификатора все слои проходят процесс стека, или наложения:

  1. Первый Embedding слой принимает переведенные в целые числа слова и ищет соответствующий вектор для каждой пары слово/число. Модель обучается на этих векторах. Векторы увеличивают размер получаемого массива на 1, в результате чего мы получаем измерения: (batch, sequence, embedding)

  2. Следующий слой GlobalAveragePooling1D возвращает получаемый вектор заданной длины для каждого примера, усредняя размер ряда. Это позволит модели легко принимать данные разной длины

  3. Этот вектор пропускается через полносвязный Dense слой с 16 скрытыми блоками

  4. Последний слой также является полносвязным, но с всего одним выходящим нодом. При помощи функции активации sigmoid (Сигмоида) мы будем получать число с плавающей запятой между 0 и 1, которое будет показывать вероятность или уверенность модели

Скрытые блоки

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

Если модель имеет больше скрытых блоков, и/или больше слоев, то тогда нейросеть может обучиться более сложным представлениям. Однако в этом случае это будет дороже с точки зрения вычислительных ресурсов и может привести к обучению нежелательных паттернов - паттернов, которые улучшают показатели на тренировочных данных, но не на проверочных. Это называется переобучением, и мы обязательно познакомимся с этим явлением далее.

Функция потерь и оптимизатор

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

Это не единственный выбор для нашей функции потерь: ты можешь, например, выбрать mean_squared_error. Но обычно binary_crossentropy лучше справляется с вероятностями - она измеряет "дистанцию" между распределениями вероятностей, или, как в нашем случае, между эталоном и предсказаниями.

Далее, по мере знакомства с задачами регрессии (например, предсказание цен на недвижимость), мы посмотрим как использовать другую функцию потерь, которая называется среднеквадратичская ошибка (MSE).

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


In [ ]:
model.compile(optimizer=tf.train.AdamOptimizer(),
              loss='binary_crossentropy',
              metrics=['accuracy'])

Создадим проверочный набор данных

Во время обучения мы хотим проверить точность нашей модели на данных, которых она еще не видела. Давай создадим проверочный сет данных, выделив 10,000 примеров из оригинального тренировочного сета в отдельный набор.

Почему мы не используем проверочный набор прямо сейчас? Наша цель - разработать и настроить нашу модель, используя только данные для обучения, и только потом использовать проверочный сет всего один раз чтобы оценить точность.


In [ ]:
x_val = train_data[:10000]
partial_x_train = train_data[10000:]

y_val = train_labels[:10000]
partial_y_train = train_labels[10000:]

Обучаем модель

Начнем тренировку нашей модели с 40 эпох при помощи мини-батчей по 512 образцов (батч - набор, пакет данных). Это означает, что мы сделаем 40 итераций (или проходов) по всем образцам данных в тензорах x_train и y_train. После обучения мы узнаем потери и точность нашей модели, показав ей 10,000 образцов из проверочного набора данных:


In [ ]:
history = model.fit(partial_x_train,
                    partial_y_train,
                    epochs=40,
                    batch_size=512,
                    validation_data=(x_val, y_val),
                    verbose=1)

Оценим точность модели

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

Она будет возвращать 2 значения: потери loss (число, которое показывает ошибку, чем оно ниже, тем лучше), и точность accuracy.


In [ ]:
results = model.evaluate(test_data,  test_labels, verbose=2)

print(results)

Как мы видим, этот достаточно наивный подход достиг точности около 87%. Если бы мы использовали более сложные методы, то модель приблизилась бы к отметке в 95%.

Построим временной график точности и потерь

Метод model.fit() возвращает объект History, который содержит все показатели, которые были записаны в лог во время обучения:


In [ ]:
history_dict = history.history
history_dict.keys()

Здесь всего четыре показателя, по одному для каждой отслеживаемой метрики во время обучения и проверки. Мы можем использовать их, чтобы построить графики потерь и точности обоих стадий для сравнения:


In [ ]:
import matplotlib.pyplot as plt

acc = history.history['acc']
val_acc = history.history['val_acc']
loss = history.history['loss']
val_loss = history.history['val_loss']

epochs = range(1, len(acc) + 1)

# "bo" означает "blue dot", синяя точка
plt.plot(epochs, loss, 'bo', label='Потери обучения')
# "b" означает "solid blue line", непрерывная синяя линия
plt.plot(epochs, val_loss, 'b', label='Потери проверки')
plt.title('Потери во время обучения и проверки')
plt.xlabel('Эпохи')
plt.ylabel('Потери')
plt.legend()

plt.show()

In [ ]:
plt.clf()   # Очистим график
acc_values = history_dict['acc']
val_acc_values = history_dict['val_acc']

plt.plot(epochs, acc, 'bo', label='Точность обучения')
plt.plot(epochs, val_acc, 'b', label='Точность проверки')
plt.title('Точность во время обучения и проверки')
plt.xlabel('Эпохи')
plt.ylabel('Точность')
plt.legend()

plt.show()

На графиках точками отмечены потери и точность модели во время обучения, а линией - во время проверки.

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

Но это совсем не тот случай если мы посмотрим на потери и точность во время проверки модели: после приблизительно 20 эпох они находятся на пике. Это явный пример переобучения: модель показывает более лучшие показатели на данных для обучения, нежели на новых, которых она еще не видела. После этого момента модель начинает переоптимизироваться и обучается представлениям, которые являются свойственны только данным для обучения. Таким образом, модель не учится обобщать новые, проверочные данные.

Именно здесь мы можем предотвратить переобучение просто прекратив тренировку сразу после 20 эпох обучения. Далее мы посмотрим, как это можно сделать автоматически при помощи callback, функции обратного вызова.