Привет!
В этой небольшой заметке расскажу о двух подводных камнях, с которыми как легко столкнуться, так и легко о них разбиться.
Речь пойдет о создании тривиальной нейронной сети на 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 каждый раз создается в памяти, а во втором он появляется при создании генератора и дальше только переиспользуется. Обычный запуск генератора на питоне работает прекрасно в обоих случаях, а вот когда это отправляется в потрошки Keras/Tensorflow, что-то идет не так. Я написал об этом команде keras-team, посмотрим, что они ответят.
Идем дальше.
Нужно ли делать expand_dims? Попробуем убрать эту строку и новый код будет такой:
def train_iterator(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
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.]].
Как-то так. Спасибо всем, кто дочитал. И будьте внимательны в мелочах, эффект от них может быть существенным.
Автор: StanSemenoff