вторник, 4 сентября 2018 г.

TensorFlow: классификация текста с отзывами к фильмам, часть 2

В этом посте мы продолжим руководство по классификации текста на TensorFlow из прошлого поста.

Построение модели

Нейронная сеть создается складыванием друг на друга слоев - этот процесс требует двух главных архитектурных решений:

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

В этом примере входные данные состоят из массива слов-индексов. Метки, которые необходимо спрогнозировать - 0 или 1. Построим модель для этой задачи:

# вводная форма - размер словаря, используемого для отзывов (10 000 слов)
vocab_size = 10000

model = keras.Sequential()
model.add(keras.layers.Embedding(vocab_size, 16))
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()

_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
embedding (Embedding)        (None, None, 16)          160000    
_________________________________________________________________
global_average_pooling1d (Gl (None, 16)                0         
_________________________________________________________________
dense (Dense)                (None, 16)                272       
_________________________________________________________________
dense_1 (Dense)              (None, 1)                 17        
=================================================================
Total params: 160,289
Trainable params: 160,289
Non-trainable params: 0

Слои складываются друг на друга последовательно для постороения классификатора:

  1. Первый слой - это встроенный слой (Embedding layer). Этот слой берет численно-кодированный словарь и ищет встроенный вектор для каждого слова-индекса. Эти вектора обучаются в ходе тренировки модели. Вектора добавляют измерение для массива вывода. Итоговые измерения следующие: (пакет, последовательность, встроенность) (batch, sequence, embedding).
  2. Далее GlobalAveragePooling1D слой возвращает выводной вектор фиксированной длины для каждого примера путем усреднения по размерности последовательности. Это позволяет модели обрабатывать ввод переменной длины простейшим возможным способом.
  3. Этот выводной вектор фиксированной длины проходят через полносвязанный (Dense) слой с 16 скрытыми элементами.
  4. Последний слой тесно связан с единственным выводным узлом, использующий сигмоидную функцию активации. Это значение нецельночисловое между 0 и 1, представляющее вероятность, или уровень уверенности.

Скрытые элементы

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

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

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

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

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

Теперь, сконфигурируем модель для использования оптимизатора и функции потери:

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

Создание валидационного набора

В ходе тренировки нам необходимо проверять аккуратность модели на данных, которые она еще не видела. Создадим валидационный набор, установив отдельно 10 000 примеров из изначально тренировочных данных. (Почему не использовать сразу тестовый набор? Наша цель - разработать и настроить нашу модель, используя только тренировочные данные, затем использовать тестовые данные только единожды, чтобы оценить аккуратность.)

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 примерах из валидационного набора:

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

Train on 15000 samples, validate on 10000 samples
Epoch 1/40
15000/15000 [==============================] - 1s 43us/step - loss: 0.6987 - acc: 0.5008 - val_loss: 0.6940 - val_acc: 0.4948
Epoch 2/40
15000/15000 [==============================] - 0s 27us/step - loss: 0.6912 - acc: 0.5518 - val_loss: 0.6901 - val_acc: 0.5675
Epoch 3/40
15000/15000 [==============================] - 0s 26us/step - loss: 0.6886 - acc: 0.5916 - val_loss: 0.6880 - val_acc: 0.5749
Epoch 4/40
15000/15000 [==============================] - 0s 27us/step - loss: 0.6857 - acc: 0.6337 - val_loss: 0.6855 - val_acc: 0.5404
Epoch 5/40
15000/15000 [==============================] - 0s 28us/step - loss: 0.6820 - acc: 0.6339 - val_loss: 0.6808 - val_acc: 0.6975
Epoch 6/40
15000/15000 [==============================] - 0s 26us/step - loss: 0.6770 - acc: 0.7135 - val_loss: 0.6754 - val_acc: 0.7310
Epoch 7/40
15000/15000 [==============================] - 0s 27us/step - loss: 0.6698 - acc: 0.7352 - val_loss: 0.6681 - val_acc: 0.6996
Epoch 8/40
15000/15000 [==============================] - 0s 26us/step - loss: 0.6596 - acc: 0.7466 - val_loss: 0.6572 - val_acc: 0.7132
Epoch 9/40
15000/15000 [==============================] - 0s 27us/step - loss: 0.6455 - acc: 0.7612 - val_loss: 0.6415 - val_acc: 0.7518
Epoch 10/40
15000/15000 [==============================] - 0s 28us/step - loss: 0.6262 - acc: 0.7749 - val_loss: 0.6216 - val_acc: 0.7638
Epoch 11/40
15000/15000 [==============================] - 0s 27us/step - loss: 0.6025 - acc: 0.7725 - val_loss: 0.5989 - val_acc: 0.7632
Epoch 12/40
15000/15000 [==============================] - 0s 27us/step - loss: 0.5740 - acc: 0.7879 - val_loss: 0.5700 - val_acc: 0.7802
Epoch 13/40
15000/15000 [==============================] - 0s 27us/step - loss: 0.5415 - acc: 0.8047 - val_loss: 0.5409 - val_acc: 0.7927
Epoch 14/40
15000/15000 [==============================] - 0s 27us/step - loss: 0.5085 - acc: 0.8151 - val_loss: 0.5110 - val_acc: 0.8043
Epoch 15/40
15000/15000 [==============================] - 0s 27us/step - loss: 0.4757 - acc: 0.8285 - val_loss: 0.4818 - val_acc: 0.8161
Epoch 16/40
15000/15000 [==============================] - 0s 26us/step - loss: 0.4452 - acc: 0.8395 - val_loss: 0.4551 - val_acc: 0.8286
Epoch 17/40
15000/15000 [==============================] - 0s 26us/step - loss: 0.4158 - acc: 0.8521 - val_loss: 0.4314 - val_acc: 0.8380
Epoch 18/40
15000/15000 [==============================] - 0s 26us/step - loss: 0.3900 - acc: 0.8624 - val_loss: 0.4101 - val_acc: 0.8464
Epoch 19/40
15000/15000 [==============================] - 0s 27us/step - loss: 0.3673 - acc: 0.8713 - val_loss: 0.3920 - val_acc: 0.8529
Epoch 20/40
15000/15000 [==============================] - 0s 26us/step - loss: 0.3472 - acc: 0.8787 - val_loss: 0.3765 - val_acc: 0.8571
Epoch 21/40
15000/15000 [==============================] - 0s 26us/step - loss: 0.3297 - acc: 0.8843 - val_loss: 0.3635 - val_acc: 0.8616
Epoch 22/40
15000/15000 [==============================] - 0s 27us/step - loss: 0.3142 - acc: 0.8899 - val_loss: 0.3525 - val_acc: 0.8666
Epoch 23/40
15000/15000 [==============================] - 0s 26us/step - loss: 0.3008 - acc: 0.8923 - val_loss: 0.3435 - val_acc: 0.8695
Epoch 24/40
15000/15000 [==============================] - 0s 27us/step - loss: 0.2883 - acc: 0.8971 - val_loss: 0.3351 - val_acc: 0.8714
Epoch 25/40
15000/15000 [==============================] - 0s 26us/step - loss: 0.2772 - acc: 0.9017 - val_loss: 0.3281 - val_acc: 0.8743
Epoch 26/40
15000/15000 [==============================] - 0s 26us/step - loss: 0.2671 - acc: 0.9051 - val_loss: 0.3226 - val_acc: 0.8749
Epoch 27/40
15000/15000 [==============================] - 0s 27us/step - loss: 0.2583 - acc: 0.9072 - val_loss: 0.3173 - val_acc: 0.8760
Epoch 28/40
15000/15000 [==============================] - 0s 26us/step - loss: 0.2492 - acc: 0.9108 - val_loss: 0.3122 - val_acc: 0.8784
Epoch 29/40
15000/15000 [==============================] - 0s 26us/step - loss: 0.2414 - acc: 0.9121 - val_loss: 0.3087 - val_acc: 0.8790
Epoch 30/40
15000/15000 [==============================] - 0s 26us/step - loss: 0.2349 - acc: 0.9144 - val_loss: 0.3049 - val_acc: 0.8809
Epoch 31/40
15000/15000 [==============================] - 0s 28us/step - loss: 0.2268 - acc: 0.9183 - val_loss: 0.3021 - val_acc: 0.8804
Epoch 32/40
15000/15000 [==============================] - 0s 28us/step - loss: 0.2211 - acc: 0.9214 - val_loss: 0.2994 - val_acc: 0.8808
Epoch 33/40
15000/15000 [==============================] - 0s 26us/step - loss: 0.2139 - acc: 0.9240 - val_loss: 0.2970 - val_acc: 0.8816
Epoch 34/40
15000/15000 [==============================] - 0s 27us/step - loss: 0.2081 - acc: 0.9253 - val_loss: 0.2955 - val_acc: 0.8816
Epoch 35/40
15000/15000 [==============================] - 0s 26us/step - loss: 0.2030 - acc: 0.9265 - val_loss: 0.2933 - val_acc: 0.8833
Epoch 36/40
15000/15000 [==============================] - 0s 26us/step - loss: 0.1970 - acc: 0.9297 - val_loss: 0.2919 - val_acc: 0.8838
Epoch 37/40
15000/15000 [==============================] - 0s 27us/step - loss: 0.1919 - acc: 0.9319 - val_loss: 0.2907 - val_acc: 0.8852
Epoch 38/40
15000/15000 [==============================] - 0s 26us/step - loss: 0.1871 - acc: 0.9335 - val_loss: 0.2895 - val_acc: 0.8849
Epoch 39/40
15000/15000 [==============================] - 0s 26us/step - loss: 0.1819 - acc: 0.9370 - val_loss: 0.2885 - val_acc: 0.8860
Epoch 40/40
15000/15000 [==============================] - 0s 27us/step - loss: 0.1773 - acc: 0.9395 - val_loss: 0.2876 - val_acc: 0.8854

Оценка модели

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

results = model.evaluate(test_data, test_labels)

print(results)

25000/25000 [==============================] - 0s 14us/step
[0.30335798323631286, 0.87656]

Этот по-настоящему простой подход достиг аккуратности около 87%. С более продвинутыми подходами модель должна приблизиться к 95%.

Создание графа аккуратности и потери с течением времени

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

history_dict = history.history
history_dict.keys()

dict_keys(['val_loss', 'acc', 'loss', 'val_acc'])

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

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" is for "blue dot"
plt.plot(epochs, loss, 'bo', label='Training loss')
# b is for "solid blue line"
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend()

plt.show()

plt.clf()   # очистка фигуры
acc_values = history_dict['acc']
val_acc_values = history_dict['val_acc']

plt.plot(epochs, acc, 'bo', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title('Training and validation accuracy')
plt.xlabel('Epochs')
plt.ylabel('Accuracy')
plt.legend()

plt.show()

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

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

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

Для этого конкретного случая мы можем предупредить переобучение просто остановив тренировку после двадцати эпох.

На этом наше руководство о текстовой классификации с TensorFlow завершается. О других методах использования TensorFlow читайте в последующих постах.