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

МЕНЮ


Искусственный интеллект
Поиск
Регистрация на сайте
Помощь проекту

ТЕМЫ


Новости ИИРазработка ИИВнедрение ИИРабота разума и сознаниеМодель мозгаРобототехника, БПЛАТрансгуманизмОбработка текстаТеория эволюцииДополненная реальностьЖелезоКиберугрозыНаучный мирИТ индустрияРазработка ПОТеория информацииМатематикаЦифровая экономика

Авторизация



RSS


RSS новости


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

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

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

Источник: habr.com

Комментарии: