Делаем нейронную сеть: как не сломать мозг

в 16:00, , рубрики: deep learning, keras, machine learning, neural networks, python, TensorFlow, машинное обучение

Привет!

В этой небольшой заметке расскажу о двух подводных камнях, с которыми как легко столкнуться, так и легко о них разбиться.

Речь пойдет о создании тривиальной нейронной сети на 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

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js