Основы работы с TensorFlow и Keras на Python. Часть 2

В предыдущей статье были описаны основы работы с Tensorflow и Keras API на Python. Эта статья посвящена задаче бинарной классификации — возможно, одной из самых частых задач машинного обучения.  Задача бинарной классификации будет рассматриваться на примере набора данных IMDB dataset. Необходимо отнести рецензию о фильме к положительной или отрицательной по используемым в рецензии словам. Набор данных разделён на обучающую и тестовую выборки. Половина отзывов положительные, половина отрицательные.

Для начала, загрузим IMDB dataset:

from keras.datasets import imdb
(train_data, train_labels), (test_data, test_labels) = imdb.load_data(num_words=10000)

num_words=10000 означает, что мы возьмём 10000 наиболее часто встречающихся в рецензиях слов.
Переменные train_data и test_data - списки рецензий. Каждая рецензия представляет собой индексы (номера) слов из словаря.
train_labels и test_labels - списки 0 и 1, где 0 означает отрицательный, а 1 - положительный отзыв.

Например,

print (train_data[10])
[1, 785, ..., 2068, 545]

Означает, что рецензия №10 из обучающей выборки train_data состоит из слов с номерами 1, 785, ..., 2068, 545 А сам отзыв №10 является положительным

print (train_labels[10])
1

Если необходимо получить само слово (на английском языке), необходимо сделать следующее:

word_index = imdb.get_word_index()
word_index = dict([(key, value + 3) for (key, value) in word_index.items()])
reverse_word_index = dict([(value, key) for (key, value) in word_index.items()])

decoded_review = ' '.join([reverse_word_index.get(i, '?') for i in train_data[10]])
print (decoded_review)

Это выведет все слова из рецензии 10:

? french horror cinema has seen something of a revival over the last couple of years...

И если ввести эти слова в поисковик, мы получим ссылку на фильм Maléfique (2002).

Подготовка данных

Мы не можем передать нейронной сети просто список целых чисел. Их необходимо преобразовать в тензоры. Есть два способа сделать это:
1.Разметить списки так, чтобы все они имели одну длину, затем преобразовать список в тензор с формой (samples, word_indeces). Потом сделать первый слой в сети таким, чтобы он принимал на вход целочисленные тензоры.
2. Преобразовать списки целых чисел в векторы 0 и 1. Это будет означать, к примеру, преобразование списка [3, 5]  в 10000-мерный вектор, почти весь состоящий из 0, за исключением индексов 3 и 5, которые будут хранить значение 1. Далее можно использовать полносвязный слой Dense, который умеет работать с числами с плавающей запятой в виде вектора.
Используем вариант 2. Закодируем последовательности целых чисел в бинарную матрицу:

import numpy as np

def vectorize_sequences(sequences, dimension=10000):
    results = np.zeros((len(sequences), dimension))
    for i, sequence in enumerate(sequences):
        results[i, sequence] = 1.
    return results

x_train = vectorize_sequences(train_data)
x_test = vectorize_sequences(test_data)

После кодирования исходные данные выглядят так:

>>> x_train[0]
array([ 0., 1., 1., ..., 0., 0., 0.])

Так же векторизуем выходные значения (ответы):

y_train = np.asarray(train_labels).astype('float32')
y_test = np.asarray(test_labels).astype('float32')

Теперь данные подготовлены для нейронной сети.

Построение сети

Входные данные — это векторы, выходные данные (ответы) — это скаляры (0 и 1).  Сеть, которая хорошо решает задачу бинарной классификации, — это простой набор полносвязных слоёв с функцией активации relu: Dense(16,activation='relu'). 16 — это размер слоя. Каждый слой работает по следующей формуле: output = relu(dot(W, input) + b), где dot - скалярное произведение. Если задать размер слоя 16, тогда матрица весов W будет формы (input_dimension, 16).
Необходимо принять два архитектурных решения: 1) сколько всего слоёв использовать; 2) какой будет размер слоёв. Мы используем 3 слоя: 1 входной + 1 скрытый (размер 16, активация relu) и выходной слой с активацией sigmoid для получения выходной вероятности (число между 0 и 1):

Архитектура сети для задачи бинарной классификации

Задание архитектуры сети:

from keras import models
from keras import layers
from keras import optimizers

model = models.Sequential()
model.add(layers.Dense(16, activation='relu', input_shape=(10000,)))
model.add(layers.Dense(16, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))

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

from keras import losses
from keras import metrics

model.compile(optimizer=optimizers.RMSprop(lr=0.001),
              loss=losses.binary_crossentropy,
              metrics=[metrics.binary_accuracy])

Чтобы следить за точностью модели во время обучения на данных, которые не участвовали в обучении, возьмём из исходной тренировочной выборки 10000 примеров для проверочной выборки:

x_val = x_train[:10000]
partial_x_train = x_train[10000:]
y_val = y_train[:10000]
partial_y_train = y_train[10000:]

Теперь обучим модель на 20 итерациях подвыборками с размером 512. Следить за потерями и точностью будем с помощью проверочный выборки из 10000 примеров:

history = model.fit(partial_x_train,
                    partial_y_train,
                    epochs=20,
                    batch_size=512,
                    validation_data=(x_val, y_val))

Python выведет в STDOUT примерно следующее:

Train on 15000 samples, validate on 10000 samples
Epoch 1/20
15000/15000 [==============================] - 2s 132us/step - loss: 0.5213 - binary_accuracy: 0.7958 - val_loss: 0.4019 - val_binary_accuracy: 0.8541
Epoch 2/20
...
Epoch 19/20
15000/15000 [==============================] - 1s 99us/step - loss: 0.0091 - binary_accuracy: 0.9992 - val_loss: 0.6600 - val_binary_accuracy: 0.8670
Epoch 20/20
15000/15000 [==============================] - 1s 99us/step - loss: 0.0064 - binary_accuracy: 0.9996 - val_loss: 0.7630 - val_binary_accuracy: 0.8629

history — это объект, содержащий словарь history, в котором хранятся данные обо всём, произошедшем во время обучения:

>>> history_dict = history.history
>>> history_dict.keys()
dict_keys(['val_loss', 'val_binary_accuracy', 'loss', 'binary_accuracy'])
 

Нарисуем графики тренировочных и проверочных точности и потерь:

import matplotlib.pyplot as plt

history_dict = history.history
loss_values = history_dict['loss']
val_loss_values = history_dict['val_loss']

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

plt.plot(epochs, loss_values, 'bo', label='Потери при обучении')
plt.plot(epochs, val_loss_values, 'b', label='Потери при проверке')
plt.title('Потери при обучении и проверке',  fontsize=18)
plt.xlabel('Итерации',  fontsize=16)
plt.ylabel('Потери',  fontsize=16)
plt.legend(fontsize=14)

plt.show()
График потерь
plt.clf()
acc_values = history_dict['binary_accuracy']
val_acc_values = history_dict['val_binary_accuracy']

plt.plot(epochs, acc, 'bo', label='Тренировочная точность')
plt.plot(epochs, val_acc, 'b', label='Проверочная точность')
plt.title('Тренировочная и проверочная точность')
plt.xlabel('Итерации')
plt.ylabel('Потери')
plt.legend()

plt.show()
График точности

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

Теперь попробуем заново обучить сеть с нуля на четырёх итерациях и затем посмотреть результат на тестовых данных:

model = models.Sequential()
model.add(layers.Dense(16, activation='relu', input_shape=(10000,)))
model.add(layers.Dense(16, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))

model.compile(optimizer='rmsprop', loss='binary_crossentropy',metrics=['accuracy'])

model.fit(x_train, y_train, epochs=4, batch_size=512)
results = model.evaluate(x_test, y_test)
print (results)

Python выведет в STDOUT:

Epoch 1/4
25000/25000 [==============================] - 8s 319us/step - loss: 0.4577 - accuracy: 0.8116
Epoch 2/4
25000/25000 [==============================] - 2s 73us/step - loss: 0.2608 - accuracy: 0.9088
Epoch 3/4
25000/25000 [==============================] - 2s 73us/step - loss: 0.2026 - accuracy: 0.9281
Epoch 4/4
25000/25000 [==============================] - 2s 73us/step - loss: 0.1695 - accuracy: 0.9400
25000/25000 [==============================] - 4s 164us/step
[0.30060292099952696, 0.8820000290870667]

Такой простой подход позволяет достичь точности 88%. Для использования обученной сети на практике (когда есть только входные данные, а ответ не известен):

model.predict(x_test)
array([[0.22884989],
       [0.9998193 ],
       [0.9301637 ],
       ...,
       [0.11618775],
       [0.11960122],
       [0.72682405]], dtype=float32)