Pull to refresh

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

Reading time4 min
Views8.8K
Привет, Хабр!

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

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

Как-то так. Спасибо всем, кто дочитал. И будьте внимательны в мелочах, эффект от них может быть существенным.
Tags:
Hubs:
+17
Comments3

Articles