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

В предыдущей статье была показана задача бинарной классификации и её решение с помощью полносвязной нейронной сети. Эта статья тоже посвящена задаче классификации, но в этот раз количество классов не 2, а 46. Мы построим модель для классификации новостей международного агентства Рейтер на 46 отдельных тем, то есть 1 новость относится только к 1 теме.

Датасет Рейтер

Датасет представляет собой набор статей по разным темам. Каждая тема представлена как минимум 10 примерами в обучающей выборке. Этот датасет так же как и датасеты IMDB и MNIST есть в Keras.

Загрузим датасет Рейтер. num_words=10000 ограничивает подвыборку 10000 наиболее часто встречающимися словами в выборке.

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

Обучающая выборка состоит из 8982 примеров, тестовая — 2246.

len(train_data)
8982

len(test_data)
2246

Аналогично IMDB, каждый пример состоит из списка целых чисел (индексов слов):

print(train_data[100])
[1, 367, 1394, 169, 65, 87, 209, ..., 382, 2, 2, 1574, 6928, 17, 12]

Для получения слов из примера #100 обучающей выборки:

word_index = reuters.get_word_index()
reverse_word_index = dict([(value, key) for (key, value) in word_index.items()])
decoded_newswire = ' '.join([reverse_word_index.get(i - 3, '?') for i in train_data[100]])
print(decoded_newswire)

Текст статьи #100:

? opec believes world oil prices should be set around a fixed average price of 18 dlrs a barrel ? assistant general secretary ? al wattari said today in a speech to a european community ec ? opec seminar in luxembourg released here al wattari said opec believes the world energy trade should be kept without restrictions and should be built around a fixed average price of 18 dlrs but he warned that defense of the 18 dlr a barrel level had caused hardship for opec countries who had been forced to curtail production and he warned that such cutbacks by opec states could not be sustained in some cases for opec to stabilize the world oil price at what is now considered the optimal level of 18 dlrs a barrel its member countries have had to undergo severe hardship in ? production al wattari said such cutbacks cannot in certain cases be sustained al wattari said as well as financial and marketing pressures some states depended on associated gas output for domestic use and oil cutbacks had left insufficient gas supplies he added al wattari noted that total opec output was below the organization's agreed ceiling for all member countries in february although this had meant sacrifices the effect of these sacrifices meant that market stability though restored to a good level was still under pressure al wattari said a lasting stability in the world market requires a wider scope of international cooperation he added he said some non opec oil producing countries had shown a political willingness after 1986 to cooperate with opec but although cutbacks announced by these states were politically significant and welcomed by opec they were insufficient in terms of volume he added the overall majority of non opec producers have not responded sufficiently to opec's calls for supply regulation he said al wattari said an 18 dlr a barrel price was optimal as it allowed investment in the oil industry outside opec to continue while not generating excessive cash flow for otherwise ? high cost areas outside opec such a price would no longer encourage protectionist measures he added ? al chalabi opec deputy secretary general also addressing the seminar added that discipline was still needed to prevent ? fluctuations in the oil market cooperation between arab states and europe was advantageous for both sides al chalabi said adding he hoped cooperation would ultimately lead to full ? ? arab dialogue reuter 3

Статья #100 относится к теме #20

print(train_labels[100])
20

Для получения названия темы #20, необходимо

topics = ['cocoa','grain','veg-oil','earn','acq','wheat','copper','housing','money-supply', 'coffee','sugar','trade','reserves','ship','cotton','carcass','crude','nat-gas', 'cpi','money-fx','interest','gnp','meal-feed','alum','oilseed','gold','tin', 'strategic-metal','livestock','retail','ipi','iron-steel','rubber','heat','jobs', 'lei','bop','zinc','orange','pet-chem','dlr','gas','silver','wpi','hog','lead']

print (topics [20])
interest

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

Закодировать (векторизовать) данные можно тем же самым кодом, что и в примере с IMDB

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)

Чтобы векторизовать заголовки (темы статей) можно пойти двумя путями: преобразовать список тем в целочисленный тензор или использовать унитарный код (one-hot encoding). Унитарный код широко применяется для кодирования категорий. Он представляет собой число, состоящее из 0 и 1. Единица ставится только в разряде, чей номер совпадает с индексом категории (темы). К примеру:

import numpy as np

def to_one_hot(labels, dimension=46):
    results = np.zeros((len(labels), dimension))
    for i, label in enumerate(labels):
        results[i, label] = 1.
    return results
one_hot_train_labels = to_one_hot(train_labels)
one_hot_test_labels = to_one_hot(test_labels)

print(one_hot_train_labels[100])

Статья №100 относится к теме №20 (interest), число 20 в унитарном коде записывается как

[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 1. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]

В Keras также есть встроенная функция унитарного кодирования

from tensorflow.keras.utils import to_categorical
one_hot_train_labels = to_categorical(train_labels)
one_hot_test_labels = to_categorical(test_labels)

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

В этой задаче размер выходного пространства (или слоя) равен 46, поэтому для качественного обучения будем использовать промежуточные слои размером 64 (против 16 в задаче бинарной классификации)

Зададим модель

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

model = models.Sequential([
  layers.Dense(64, activation='relu'),
  layers.Dense(64, activation='relu'),
  layers.Dense(46, activation='softmax')
])

Нужно помнить две вещи:
1. Размер выходного полносвязного (Dense) слоя равен 46. Это значит, что для каждого входного примера, сеть выведет вектор с размерностью 46.
2. Каждый i-ый элемент этого вектора output[i] содержит вероятность, с который неизвестный пример принадлежит к i-му классу. Используется функция активации softmax, то есть все вероятности в сумме равны 1.

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

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

Возьмём 1000 примеров из обучающей выборки для создания проверочной выборки

x_val = x_train[:1000]
partial_x_train = x_train[1000:]
y_val = one_hot_train_labels[:1000]
partial_y_train = one_hot_train_labels[1000:]

Обучим модель на 20 эпохах:

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

Возможный вывод в STDOUT:

Epoch 1/20
16/16 [==============================] - 1s 61ms/step - loss: 2.6204 - accuracy: 0.5645 - val_loss: 1.7114 - val_accuracy: 0.6480
Epoch 2/20
16/16 [==============================] - 1s 49ms/step - loss: 1.3935 - accuracy: 0.7156 - val_loss: 1.2826 - val_accuracy: 0.7210
...
Epoch 19/20
16/16 [==============================] - 1s 48ms/step - loss: 0.1159 - accuracy: 0.9559 - val_loss: 1.0810 - val_accuracy: 0.7990
Epoch 20/20
16/16 [==============================] - 1s 47ms/step - loss: 0.1112 - accuracy: 0.9580 - val_loss: 1.0819 - val_accuracy: 0.8060

Выведем график потерь:

import matplotlib.pyplot as plt

loss = history.history['loss']
val_loss = history.history['val_loss']
epochs = range(1, len(loss) + 1)
plt.plot(epochs, loss, 'bo', label='Потери при обучении')
plt.plot(epochs, val_loss, 'b', label='Потери при проверке')
plt.title('Потери при обучении и проверке',  fontsize=18)
plt.xlabel('Итерации',  fontsize=16)
plt.ylabel('Потери',  fontsize=16)
plt.legend()
plt.show()
Потери при обучении и проверке

Выведем график точности:

plt.clf()
acc = history.history['accuracy']
val_acc = history.history['val_accuracy']
plt.plot(epochs, acc, 'bo', label='Точность при обучении')
plt.plot(epochs, val_acc, 'b', label='Точность при проверке')
plt.title('Точность при обучении и проверке',  fontsize=18)
plt.xlabel('Итерации',  fontsize=16)
plt.ylabel('Точность',  fontsize=16)
plt.legend()
plt.show()
Точность при обучении и проверке

Как видно из графиков, модель переобучается после 9-й итерации. Обучим модель заново на 9 итерациях, а затем запустим её на тестовой выборке:

model = models.Sequential([
  layers.Dense(64, activation='relu'),
  layers.Dense(64, activation='relu'),
  layers.Dense(46, activation='softmax')
])
model.compile(optimizer='rmsprop',
              loss='categorical_crossentropy',
              metrics=['accuracy'])
model.fit(partial_x_train,
          partial_y_train,
          epochs=9,
          batch_size=512,
          validation_data=(x_val, y_val))
results = model.evaluate(x_test, one_hot_test_labels)

Возможный вывод в STDOUT

Epoch 1/9
16/16 [==============================] - 1s 53ms/step - loss: 2.6138 - accuracy: 0.5541 - val_loss: 1.7246 - val_accuracy: 0.6520
...
Epoch 9/9
16/16 [==============================] - 1s 44ms/step - loss: 0.2774 - accuracy: 0.9394 - val_loss: 0.9022 - val_accuracy: 0.8170

71/71 [==============================] - 0s 2ms/step - loss: 0.9729 - accuracy: 0.7898

Данный подход позволил добиться точности ~79%.

Предсказания на новых данных

Метод predict возвращает распределение вероятности для 46 тем для каждого примера. Создадим предсказания для всей тестовой выборки:

predictions = model.predict(x_test)

Десятый пример из тестовой выборки относится к теме #1:

print (np.argmax(predictions[10]))

1

Как упоминалось ранее, другой способ закодировать ответы (labels) — преобразовать их в целочисленные тензоры, например так:

y_train = np.array(train_labels)
y_test = np.array(test_labels)

Такой подход меняет только выбор функции потерь. Вместо categorical_crossentropy необходимо использовать sparse_categorical_crossentropy:

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