Привет, Хабр!
В этой небольшой заметке расскажу о двух подводных камнях, с которыми как легко столкнуться, так и легко о них разбиться.
Речь пойдет о создании тривиальной нейронной сети на Keras, с помощью которой будем предсказывать среднее арифметическое двух чисел.
Казалось бы, что может быть проще. И действительно, ничего сложного, но есть нюансы.
Кому тема интересна, добро пожаловать под кат, здесь не будет долгих занудных описаний, просто короткий код и комментарии к нему.
Решение выглядит примерно следующим образом:
Пытаемся учить… но ничего не выходит. И вот в этом месте можно устраивать танцы с бубном и потерять много времени.
Предсказалось 49, что совсем далеко не 65.
Но стоит нам немного переделать генератор, как все начинает сразу работать.
И видно, что уже буквально на третьей эпохе сеть сходится.
Основное отличие в том, что в первом случае у нас объект x_mean каждый раз создается в памяти, а во втором он появляется при создании генератора и дальше только переиспользуется.
Разбираемся дальше, все ли верно в этом генераторе. Оказывается, что не совсем.
Следующий пример показывает, что что-то идет не так.
Среднее значение в первом вызове итератора не совпадает с числами, на основе которых оно посчитано. На самом деле среднее значение было посчитано правильно, но т.к. массив был передан по ссылке, то при втором вызове итератора значения в массиве перезаписались, и функция print() выдала, что и было в массиве, а не то, что мы ожидали.
Есть два способо это исправить. Оба затратные, но корректные.
1. Перенести создание переменной x внутрь цикла while, чтобы массив при каждом yield создавался новый.
2. Возвращать копию массива.
Теперь все отлично. Идем дальше.
Нужно ли делать expand_dims? Попробуем убрать эту строку и новый код будет такой:
Все прекрасно учится, хотя возвращаемые данные имеют другой shape.
Например, было [[49.]], а стало [49.], но внутри Keras это, видимо, корректно приводится к нужной размерности.
Итак, мы знаем, как должен выглядеть правильный генератор данных, теперь поиграемся с lambda функцией, и посмотрим на поведение expand_dims там.
Ничего предсказывать не будем, просто считаем внитри lambda правильное значение.
Код следующий:
Запускаем и видим, что все прекрасно:
Попробуем теперь немного изменить нашу lambda функцию и убрать expand_dims.
При компиляции модели никаких ошибок на размерность не появилось, но результат уже другой, лосс считается непонятно как. Таким образом, здесь expand_dims нужно делать, ничего автоматически не произойдет.
И если посмотреть на возвращаемый результат predict(), то видно, что размерность неправильная, выход равен [46.], а ожидается [[46.]].
Как-то так. Спасибо всем, кто дочитал. И будьте внимательны в мелочах, эффект от них может быть существенным.
В этой небольшой заметке расскажу о двух подводных камнях, с которыми как легко столкнуться, так и легко о них разбиться.
Речь пойдет о создании тривиальной нейронной сети на Keras, с помощью которой будем предсказывать среднее арифметическое двух чисел.
Казалось бы, что может быть проще. И действительно, ничего сложного, но есть нюансы.
Кому тема интересна, добро пожаловать под кат, здесь не будет долгих занудных описаний, просто короткий код и комментарии к нему.
Решение выглядит примерно следующим образом:
import numpy as np
from keras.layers import Input, Dense, Lambda
from keras.models import Model
import keras.backend as K
# генератор данных
def train_iterator(batch_size=64):
x = np.zeros((batch_size, 2))
while True:
for i in range(batch_size):
x[i][0] = np.random.randint(0, 100)
x[i][1] = np.random.randint(0, 100)
x_mean = (x[::,0] + x[::,1]) / 2
x_mean_ex = np.expand_dims(x_mean, -1)
yield [x], [x_mean_ex]
# модель
def create_model():
x = Input(name = 'x', shape=(2,))
x_mean = Dense(1)(x)
model = Model(inputs=x, outputs=x_mean)
return model
# создаем и учим
model = create_model()
model.compile(loss=['mse'], optimizer = 'rmsprop')
model.fit_generator(train_iterator(), steps_per_epoch = 1000, epochs = 100, verbose = 1)
# предсказываем
x, x_mean = next(train_iterator(1))
print(x, x_mean, model.predict(x))
Пытаемся учить… но ничего не выходит. И вот в этом месте можно устраивать танцы с бубном и потерять много времени.
Epoch 1/100
1000/1000 [==============================] - 2s 2ms/step - loss: 1044.0806
Epoch 2/100
1000/1000 [==============================] - 2s 2ms/step - loss: 713.5198
Epoch 3/100
1000/1000 [==============================] - 3s 3ms/step - loss: 708.1110
...
Epoch 98/100
1000/1000 [==============================] - 2s 2ms/step - loss: 415.0479
Epoch 99/100
1000/1000 [==============================] - 2s 2ms/step - loss: 416.6932
Epoch 100/100
1000/1000 [==============================] - 2s 2ms/step - loss: 417.2400
[array([[73., 57.]])] [array([[65.]])] [[49.650894]]
Предсказалось 49, что совсем далеко не 65.
Но стоит нам немного переделать генератор, как все начинает сразу работать.
def train_iterator_1(batch_size=64):
x = np.zeros((batch_size, 2))
x_mean = np.zeros((batch_size,))
while True:
for i in range(batch_size):
x[i][0] = np.random.randint(0, 100)
x[i][1] = np.random.randint(0, 100)
x_mean[::] = (x[::,0] + x[::,1]) / 2
x_mean_ex = np.expand_dims(x_mean, -1)
yield [x], [x_mean_ex]
И видно, что уже буквально на третьей эпохе сеть сходится.
Epoch 1/5
1000/1000 [==============================] - 2s 2ms/step - loss: 648.9184
Epoch 2/5
1000/1000 [==============================] - 2s 2ms/step - loss: 0.0177
Epoch 3/5
1000/1000 [==============================] - 2s 2ms/step - loss: 0.0030
Основное отличие в том, что в первом случае у нас объект x_mean каждый раз создается в памяти, а во втором он появляется при создании генератора и дальше только переиспользуется.
Разбираемся дальше, все ли верно в этом генераторе. Оказывается, что не совсем.
Следующий пример показывает, что что-то идет не так.
def train_iterator(batch_size=1):
x = np.zeros((batch_size, 2))
while True:
for i in range(batch_size):
x[i][0] = np.random.randint(0, 100)
x[i][1] = np.random.randint(0, 100)
x_mean = (x[::,0] + x[::,1]) / 2
yield x, x_mean
it = train_iterator()
print(next(it), next(it))
(array([[44., 2.]]), array([10.])) (array([[44., 2.]]), array([23.]))
Среднее значение в первом вызове итератора не совпадает с числами, на основе которых оно посчитано. На самом деле среднее значение было посчитано правильно, но т.к. массив был передан по ссылке, то при втором вызове итератора значения в массиве перезаписались, и функция print() выдала, что и было в массиве, а не то, что мы ожидали.
Есть два способо это исправить. Оба затратные, но корректные.
1. Перенести создание переменной x внутрь цикла while, чтобы массив при каждом yield создавался новый.
def train_iterator_1(batch_size=1):
while True:
x = np.zeros((batch_size, 2))
for i in range(batch_size):
x[i][0] = np.random.randint(0, 100)
x[i][1] = np.random.randint(0, 100)
x_mean = (x[::,0] + x[::,1]) / 2
yield x, x_mean
it_1 = train_iterator_1()
print(next(it_1), next(it_1))
(array([[82., 4.]]), array([43.])) (array([[77., 34.]]), array([55.5]))
2. Возвращать копию массива.
def train_iterator_2(batch_size=1):
x = np.zeros((batch_size, 2))
while True:
x = np.zeros((batch_size, 2))
for i in range(batch_size):
x[i][0] = np.random.randint(0, 100)
x[i][1] = np.random.randint(0, 100)
x_mean = (x[::,0] + x[::,1]) / 2
yield np.copy(x), x_mean
it_2 = train_iterator_2()
print(next(it_2), next(it_2))
(array([[63., 31.]]), array([47.])) (array([[94., 25.]]), array([59.5]))
Теперь все отлично. Идем дальше.
Нужно ли делать expand_dims? Попробуем убрать эту строку и новый код будет такой:
def train_iterator(batch_size=64):
while True:
x = np.zeros((batch_size, 2))
for i in range(batch_size):
x[i][0] = np.random.randint(0, 100)
x[i][1] = np.random.randint(0, 100)
x_mean = (x[::,0] + x[::,1]) / 2
yield [x], [x_mean]
Все прекрасно учится, хотя возвращаемые данные имеют другой shape.
Например, было [[49.]], а стало [49.], но внутри Keras это, видимо, корректно приводится к нужной размерности.
Итак, мы знаем, как должен выглядеть правильный генератор данных, теперь поиграемся с lambda функцией, и посмотрим на поведение expand_dims там.
Ничего предсказывать не будем, просто считаем внитри lambda правильное значение.
Код следующий:
def calc_mean(x):
res = (x[::,0] + x[::,1]) / 2
res = K.expand_dims(res, -1)
return res
def create_model():
x = Input(name = 'x', shape=(2,))
x_mean = Lambda(lambda x: calc_mean(x), output_shape=(1,))(x)
model = Model(inputs=x, outputs=x_mean)
return model
Запускаем и видим, что все прекрасно:
Epoch 1/5
100/100 [==============================] - 0s 3ms/step - loss: 0.0000e+00
Epoch 2/5
100/100 [==============================] - 0s 2ms/step - loss: 0.0000e+00
Epoch 3/5
100/100 [==============================] - 0s 3ms/step - loss: 0.0000e+00
Попробуем теперь немного изменить нашу lambda функцию и убрать expand_dims.
def calc_mean(x):
res = (x[::,0] + x[::,1]) / 2
return res
При компиляции модели никаких ошибок на размерность не появилось, но результат уже другой, лосс считается непонятно как. Таким образом, здесь expand_dims нужно делать, ничего автоматически не произойдет.
Epoch 1/5
100/100 [==============================] - 0s 3ms/step - loss: 871.6299
Epoch 2/5
100/100 [==============================] - 0s 3ms/step - loss: 830.2568
Epoch 3/5
100/100 [==============================] - 0s 2ms/step - loss: 830.8041
И если посмотреть на возвращаемый результат predict(), то видно, что размерность неправильная, выход равен [46.], а ожидается [[46.]].
Как-то так. Спасибо всем, кто дочитал. И будьте внимательны в мелочах, эффект от них может быть существенным.