Цель этой статьи — научить нейронную сеть играть в игру "Жизнь", не обучая ее правилам игры.


Привет, Хабр! Представляю вашему вниманию перевод статьи "Using a Convolutional Neural Network to Play Conway's Game of Life with Keras" автора kylewbanks.


Если вы не знакомы с игрой под названием Жизнь (это клеточный автомат, придуманный английским математиком Джоном Конвеем в 1970 году), правила таковы.


Вселенная игры представляет собой бесконечную, двумерную сетку квадратных ячеек, каждая из которых находится в одном из двух возможных состояний: живая или мертвая (или населенная и незаселенная, соответственно). Каждая ячейка взаимодействует со своими восемью соседями по горизонтали, вертикали или диагонали. На каждом шаге во времени происходят следующие переходы:


  • Любая живая клетка с менее чем двумя живыми соседями умирает.
  • Любая живая клетка с двумя или тремя живыми соседями доживает до следующего поколения.
  • Любая живая клетка с более чем тремя живыми соседями умирает.
  • Любая мертвая клетка с ровно тремя живыми соседями становится живой клеткой.

Первое поколение создается путем применения вышеуказанных правил одновременно к каждой ячейке в начальном состоянии, рождения и смерти происходят одновременно в дискретные моменты времени. Каждое поколение — это чистая функция предыдущего. Правила продолжают применяться к новому поколению, чтобы создать следующие поколения.


Подробнее см. Википедию.


Зачем это делать? Главным образом для развлечения, и чтобы немного узнать о сверточных нейронных сетях.


Итак...


Игровая логика


Первое, что нужно сделать — это определить функцию, которая принимает игровое поле в качестве входных данных и возвращает следующее состояние.


К счастью, в Интернете доступно множество реализаций, таких как: https://jakevdp.github.io/blog/2013/08/07/conways-game-of-life/.


По сути, он принимает матрицу игрового поля в качестве входных данных, где 0 представляет мертвую ячейку, а 1 представляет живую ячейку и возвращает матрицу того же размера, но содержащую состояние каждой ячейки на следующей итерации игры.


import numpy as np

def life_step(X):
    live_neighbors = sum(np.roll(np.roll(X, i, 0), j, 1)
                     for i in (-1, 0, 1) for j in (-1, 0, 1)
                     if (i != 0 or j != 0))
    return (live_neighbors == 3) | (X & (live_neighbors == 2)).astype(int)

Генерация игрового поля


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


Функция generate_frames создает num_frames случайных игровых полей с определенной формой и предопределенной вероятностью того, что каждая ячейка будет "живой", а render_frames рисует представления изображений двух игровых полей рядом для сравнения (живые ячейки белые, а мертвые ячейки черные):


import matplotlib.pyplot as plt

def generate_frames(num_frames, board_shape=(100,100), prob_alive=0.15):
    return np.array([
        np.random.choice([False, True], size=board_shape, p=[1-prob_alive, prob_alive])
        for _ in range(num_frames)
    ]).astype(int)

def render_frames(frame1, frame2):
    plt.subplot(1, 2, 1)
    plt.imshow(frame1.flatten().reshape(board_shape), cmap='gray')

    plt.subplot(1, 2, 2)
    plt.imshow(frame2.flatten().reshape(board_shape), cmap='gray')

Давайте посмотрим, как выглядят эти поля:


board_shape = (20, 20)
board_size = board_shape[0] * board_shape[1]
probability_alive = 0.15

frames = generate_frames(10, board_shape=board_shape, prob_alive=probability_alive)
print(frames.shape) # (num_frames, board_w, board_h)

(10, 20, 20)

print(frames[0])

[[0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0],
 [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
 [0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 1],
 [1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
 [1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0],
 [0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0],
 [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
 [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0]])

Далее берется целочисленное представление игрового поля и отображается, как изображение.
Справа также показано следующее состояние игрового поля с помощью функции life_step:


ender_frames(frames[1], life_step(frames[1]))


Построение обучающего и тестового наборов


Теперь мы можем сгенерировать данные для обучения, проверки и тестирования.


Каждый элемент в массивах y_train/y_val/y_test будет представлять следующее поле игры для каждого кадра поля в X_train/X_val/X_test.


def reshape_input(X):
    return X.reshape(X.shape[0], X.shape[1], X.shape[2], 1)

def generate_dataset(num_frames, board_shape, prob_alive):
    X = generate_frames(num_frames, board_shape=board_shape, prob_alive=prob_alive)
    X = reshape_input(X)
    y = np.array([
        life_step(frame) 
        for frame in X
    ])
    return X, y

train_size = 70000
val_size   = 10000
test_size  = 20000

print("Training Set:")
X_train, y_train = generate_dataset(train_size, board_shape, probability_alive)
print(X_train.shape)
print(y_train.shape)

Training Set:
(70000, 20, 20, 1)
(70000, 20, 20, 1)

print("Validation Set:")
X_val, y_val = generate_dataset(val_size, board_shape, probability_alive)
print(X_val.shape)
print(y_val.shape)

Validation Set:
(10000, 20, 20, 1)
(10000, 20, 20, 1)

print("Test Set:")
X_test, y_test = generate_dataset(test_size, board_shape, probability_alive)
print(X_test.shape)
print(y_test.shape)

Test Set:
(20000, 20, 20, 1)
(20000, 20, 20, 1)

Построение сверточной нейронной сети


Теперь мы можем сделать первый шаг к построению сверточной нейронной сети с использованием Keras. Ключевым моментом здесь являются размер ядра (3, 3) и шаг 1. Они указывают CNN использовать матрицу 3x3 окружающих ячеек для каждой ячейки поля, на которую она смотрит, включая текущую ячейку.


Например, если бы нижеследующее было игровым полем, а мы были в средней ячейке x, она бы посмотрела на все ячейки, отмеченные восклицательным знаком ! и ячейку х. Затем сеть двигается вдоль ячейки вправо и делает то же самое, повторяя снова и снова, пока не обработает каждую ячейку и ее соседей по всему полю.


0 0 0 0 0
0! ! ! 0
0! x ! 0
0! ! ! 0
0 0 0 0 0

Остальная сеть довольно проста, поэтому я не буду вдаваться в подробности. Если вам что-нибудь интересно, я рекомендую почитать документацию.


from keras.models import Sequential
from keras.layers import Dense, Dropout, Activation, Conv2D, MaxPool2D

# CNN Properties
filters = 50
kernel_size = (3, 3) # look at all 8 neighboring cells, plus itself
strides = 1
hidden_dims = 100

model = Sequential()
model.add(Conv2D(
    filters, 
    kernel_size,
    padding='same',
    activation='relu',
    strides=strides,
    input_shape=(board_shape[0], board_shape[1], 1)
))
model.add(Dense(hidden_dims))
model.add(Dense(1))
model.add(Activation('sigmoid'))

model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])

Взглянем на вывод функции summary:


model.summary()

_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_9 (Conv2D)            (None, 20, 20, 50)        500       
_________________________________________________________________
dense_17 (Dense)             (None, 20, 20, 100)       5100      
_________________________________________________________________
dense_18 (Dense)             (None, 20, 20, 1)         101       
_________________________________________________________________
activation_9 (Activation)    (None, 20, 20, 1)         0         
=================================================================
Total params: 5,701
Trainable params: 5,701
Non-trainable params: 0
_________________________________________________________________

Обучение и сохранение модели


Построив CNN, давайте обучим модель и сохраним ее на диск:


def train(model, X_train, y_train, X_val, y_val, batch_size=50, epochs=2, filename_suffix=''):
    model.fit(
        X_train, y_train, 
        batch_size=batch_size, 
        epochs=epochs,
        validation_data=(X_val, y_val)
    )

    with open('cgol_cnn{}.json'.format(filename_suffix), 'w') as file:
        file.write(model.to_json())
    model.save_weights('cgol_cnn{}.h5'.format(filename_suffix))

train(model, X_train, y_train, X_val, y_val, filename_suffix='_basic')

Train on 70000 samples, validate on 10000 samples
Epoch 1/2
70000/70000 [==============================] - 27s 388us/step 
    - loss: 0.1324 - acc: 0.9651 - val_loss: 0.0833 - val_acc: 0.9815
Epoch 2/2
70000/70000 [==============================] - 27s 383us/step 
    - loss: 0.0819 - acc: 0.9817 - val_loss: 0.0823 - val_acc: 0.9816

Эта модель обеспечивает точность чуть более 98% как для тренировочных, так и для проверочных наборов, что очень хорошо для первого прохода. Давайте попробуем выяснить, где мы делаем ошибки.


Пробуем


Давайте посмотрим на прогноз для случайного игрового поля и на то, как он работает. Сначала создайте одно игровое поле и посмотрите на правильный следующий кадр:


X, y = generate_dataset(1, board_shape=board_shape, prob_alive=probability_alive)

render_frames(X[0].flatten().reshape(board_shape), y)


Далее, давайте выполним предсказание и посмотрим, сколько ячеек было неправильно предсказано:


pred = model.predict_classes(X)
print(np.count_nonzero(pred.flatten() - y.flatten()), "incorrect cells.")

4 incorrect cells.

Далее, давайте сравним правильный следующий шаг с предсказанным шагом:


render_frames(y, pred.flatten().reshape(board_shape))


Это не страшно, но вы видите, где предсказание не удалось? Кажется, что сеть не может предсказать клетки по краям игрового поля. Посмотрим туда, где ненулевые значения указывают на неправильные предсказания:


print(pred.flatten().reshape(board_shape) - y.flatten().reshape(board_shape))

[[ 0  0  0  0  0  0  0 -1  0  0  0  0  0  0  0  0  0 -1 -1  0]
 [ 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [ 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [ 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [ 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [ 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [ 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [ 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [ 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [ 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [ 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [ 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [ 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [ 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [ 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [ 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [ 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [ 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [ 0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [ 0  0  0  0  0  0 -1  0  0  0  0  0  0  0  0  0  0  0  0  0]]

Как видите, все ненулевые значения расположены по краям игрового поля. Давайте посмотрим на полный тестовый набор и подтвердим, что это наблюдение верно.


Просмотр ошибок, используя тестовый набор


Мы напишем функцию, которая отображает тепловую карту, показывающую, где модель делает ошибки, и вызовем ее, используя весь тестовый набор:


def view_prediction_errors(model, X, y):
    y_pred = model.predict_classes(X)
    sum_y_pred = np.sum(y_pred, axis=0).flatten().reshape(board_shape)
    sum_y = np.sum(y, axis=0).flatten().reshape(board_shape)

    plt.imshow(sum_y_pred - sum_y, cmap='hot', interpolation='nearest')
    plt.show()

view_prediction_errors(model, X_test, y_test)


Все ошибки на краях и в углах. Что логично, так как CNN не может смотреть по сторонам, но логика игры в life_step это делает. Например, рассмотрим следующее. Глядя на краевую ячейку x ниже, CNN видит только x и ! клетки:


0 0 0 0 0
! ! 0 0 0 
x ! 0 0 0
! ! 0 0 0 
0 0 0 0 0

Но что мы действительно хотим, и что делает life_step, так это посмотреть на ячейки с противоположной стороны:


0 0 0 0 0
! ! 0 0 ! 
x ! 0 0 !
! ! 0 0 ! 
0 0 0 0 0

Похожая ситуация в углах:


x ! 0 0 !
! ! 0 0 ! 
0 0 0 0 0
0 0 0 0 0
! 0 0 0 !

Чтобы это исправить, Conv2D должен как-то смотреть на противоположную сторону игрового поля. В качестве альтернативы, каждая входное поле может быть предварительно обработано для заполнения краев с противоположной стороны, и тогда Conv2D может просто удалить первый или последний столбец и строку. Так как мы находимся во власти Keras и предоставляемых им функциональных возможностей заполнения, которые не поддерживают то, что мы ищем, нам придется прибегнуть к добавлению нашего собственного заполнения.


Исправление краевых дефектов с помощью заполнения


Нам нужно дополнить каждую игровое поле противоположным значением, чтобы имитировать то, как life_step работает для краевых значений. Мы можем использовать np.pad с mode = ’wrap’ для этого. Например, рассмотрим следующий массив и дополненный вывод ниже:


x = np.array([
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
])

print(np.pad(x, (1, 1), mode='wrap'))

[[9, 7, 8, 9, 7],
 [3, 1, 2, 3, 1],
 [6, 4, 5, 6, 4],
 [9, 7, 8, 9, 7],
 [3, 1, 2, 3, 1]]

Обратите внимание, что первый столбец/строка и последний столбец/строка отзеркаливают противоположную сторону исходной матрицы, а средняя матрица 3x3 является исходным значением x. Например, ячейка [1] [1] была скопирована на противоположной стороне в ячейке [4] [1], и аналогично [0] [1] содержит [3] [1]. Во всех направлениях и даже в углах массив был исправлен так, чтобы он содержал противоположную сторону. Это позволит CNN рассмотреть все игровое поле и правильно обработать крайние случаи.


Теперь мы можем написать функцию для заполнения всех наших входных матриц:


def pad_input(X):
    return reshape_input(np.array([
        np.pad(x.reshape(board_shape), (1,1), mode='wrap')
        for x in X
    ]))

X_train_padded = pad_input(X_train)
X_val_padded = pad_input(X_val)
X_test_padded = pad_input(X_test)

print(X_train_padded.shape)
print(X_val_padded.shape)
print(X_test_padded.shape)

(70000, 22, 22, 1)
(10000, 22, 22, 1)
(20000, 22, 22, 1)

Все наборы данных теперь дополнены обернутыми столбцами/строками, что позволяет CNN видеть противоположную сторону игрового поля, как это делает life_step. Из-за этого каждое игровое поле теперь имеет размер 22x22 вместо оригинальных 20x20.


Затем, CNN должен быть перестроен так, чтобы отбрасывать заполнение, используя padding = 'valid' (что говорит Conv2D отбрасывать края, хотя это не сразу очевидно), и обработки нового input_shape. Таким образом, когда мы пропускаем игровые поля с размером 22x22, мы по-прежнему получаем размер 20x20 в качестве выходного, поскольку отбрасываем первый и последний столбец/строку. Остальное остается идентичным:


model_padded = Sequential()
model_padded.add(Conv2D(
    filters, 
    kernel_size,
    padding='valid',
    activation='relu',
    strides=strides,
    input_shape=(board_shape[0] + 2, board_shape[1] + 2, 1)
))
model_padded.add(Dense(hidden_dims))
model_padded.add(Dense(1))
model_padded.add(Activation('sigmoid'))

model_padded.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
model_padded.summary()

_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_10 (Conv2D)           (None, 20, 20, 50)        500       
_________________________________________________________________
dense_19 (Dense)             (None, 20, 20, 100)       5100      
_________________________________________________________________
dense_20 (Dense)             (None, 20, 20, 1)         101       
_________________________________________________________________
activation_10 (Activation)   (None, 20, 20, 1)         0         
=================================================================
Total params: 5,701
Trainable params: 5,701
Non-trainable params: 0
_________________________________________________________________

Теперь мы можем обучиться, используя выровненное поле:


train(
    model_padded, 
    X_train_padded, y_train, X_val_padded, y_val, 
    filename_suffix='_padded'
)

Train on 70000 samples, validate on 10000 samples
Epoch 1/2
70000/70000 [==============================] - 27s 389us/step - loss: 0.0604 - acc: 0.9807 - val_loss: 4.5475e-04 - val_acc: 1.0000
Epoch 2/2
70000/70000 [==============================] - 27s 382us/step - loss: 1.7058e-04 - acc: 1.0000 - val_loss: 5.9932e-05 - val_acc: 1.0000

Точность предсказания составляет от 98% до 100%, которые мы получили до добавления отступов. Давайте посмотрим на ошибку на тестовом наборе:


view_prediction_errors(model_padded, X_test_padded, y_test)


Отлично! Черная тепловая карта указывает на то, что нет различий в значениях, и это означает, что мы успешно предсказали каждую ячейку для каждой игры.


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

Комментарии (23)


  1. sgjurano
    23.12.2019 01:23

    Отличная статья, спасибо!


    Глаз зацепился за padding='same', я даже собрался вам написать об ошибке на краях, а у вас дальше такой классный разбор :)


    1. worker_sam Автор
      23.12.2019 09:34

      Это перевод. Делал сам нечто подобное. С padding='same' точность достигает порядка 91%. Не знаю, как автор получил 98%.


  1. Closius
    23.12.2019 01:49
    +1

    Хм… а сможет ли сеть делать предсказания на несколько шагов вперед за время выше чем классический алгоритм игры жизнь?


    1. worker_sam Автор
      23.12.2019 09:35

      За время меньше? Теоретически — да. Если скорость предсказания выше (например, за счет аппаратуры, когда сеть на GPU, алгоритм на CPU, либо за счет оптимизаций), а пересчет всего поля сетью ведется параллельно.


  1. Bel_Riose
    23.12.2019 05:55

    Моделировать такой простой детерминированный алгоритм нейросеткой — это как то грустно, хотя я и не специалист чтобы судить.
    А если попробовать скармливать в обучающем наборе последующий шаг как input и требовать от нейросети предыдущий как output?(тогда наверное все поле должно быть в качестве input layer). Получится ли таким способом искать сады Эдема?


    1. worker_sam Автор
      23.12.2019 09:38

      Моделировать такой простой детерминированный алгоритм нейросеткой — это как то грустно, хотя я и не специалист чтобы судить.

      Смотрите эпиграф.


      А если попробовать скармливать в обучающем наборе последующий шаг как input и требовать от нейросети предыдущий как output?

      Имеется ввиду, предсказывать временной ряд?


      (тогда наверное все поле должно быть в качестве input layer). Получится ли таким способом искать сады Эдема?

      Не знаю, сам не занимался, статей на эту тему не видел, и сам этот клеточный автомат вижу первый раз, почитаю, спасибо за наводку.


    1. lightcaster
      23.12.2019 12:58

      Было соревнование на кагле по инвертированию игры: www.kaggle.com/c/conway-s-reverse-game-of-life/overview/description
      Но не сказал бы что что-то интересное или прорывное там было.


    1. Pochemuk
      23.12.2019 17:29

      Инвертирование не является однозначным. Т.е. у одной фигуры может быть несколько различных потомков.
      Как в таком случае будет вести себя ИНС — не знаю. Но что-то подсказывает мне, что ничего хорошего не выйдет.


    1. muhaa
      24.12.2019 00:01

      Сама по себе задача довольно интересна. Можно создавать случайные миры, запускать их до стабилизации а потом обучать сеть на записи обратного развития.
      Интересно здесь следующее. На самом деле игра жизнь необратима, но ходу развития автомата, на неком этапе развития ему всегда свойственны определенные паттерны со своими вероятностями.
      Если сеть будет очень хорошо знать все эти паттерны, она сможет довольно точно предсказывать прошлое для необратимого автомата.
      Если это действительно возможно, то тогда возможны интересные аналогии с физикой реального мира, которая тоже вроде-бы обратима, хотя один из базовых принципов квантовой механики необратим…


  1. Temmokan
    23.12.2019 07:11

    Правила продолжают применяться рекурсивно


    Рекурсия — это всё же вызов функцией самой себя («Рекурсия: см. Рекурсия»). В данном случае просто повторное применение правил, чтобы получить новое поколение.


    1. worker_sam Автор
      23.12.2019 09:40

      Исправил.


    1. viktprog
      24.12.2019 08:38

      Состояние задается рекуррентной формулой


      state(T) = transition(state(T - 1)), state(0) = state_0

      Функция state(t) вполне себе рекурсивная


  1. alexander-shustanov
    23.12.2019 09:18

    А почему filters=50? Как подбирался этот параметр? Что-то мне подсказывает, что можно сильно меньше взять, без просадки точности. Тоже и к другим параметрам относится.


    1. worker_sam Автор
      23.12.2019 09:42

      Напомню, что это перевод. Но параметр, вероятно, подбирался эмпирически. Некоторые параметры очевидно, почему такие, например размер ядра 3x3 — потому, что клетка в центре зависит от соседей по бокам.


      1. Pochemuk
        23.12.2019 20:16

        А метка «Перевод» не указана. Поэтому создается впечатление, что это собственная наработка.


        1. worker_sam Автор
          24.12.2019 08:43

          Добавил.


  1. arTk_ev
    23.12.2019 20:12

    А сразу на несколько шагов сеть может предсказать, без промежуточных шагов?


    1. worker_sam Автор
      24.12.2019 08:43

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


  1. stasiche
    24.12.2019 08:38

    Решение самой «Жизни» есть куда более красивое) Взято с

    def iterate(Z):
        # Count neighbours
        N = (Z[0:-2,0:-2] + Z[0:-2,1:-1] + Z[0:-2,2:] +
             Z[1:-1,0:-2]                + Z[1:-1,2:] +
             Z[2:  ,0:-2] + Z[2:  ,1:-1] + Z[2:  ,2:])
    
        # Apply rules
        birth = (N == 3) & (Z[1:-1,1:-1]==0)
        survive = ((N == 2) | (N == 3)) & (Z[1:-1,1:-1] == 1)
        Z[...] = 0
        Z[1:-1,1:-1][birth | survive] = 1
        return Z
    
    


  1. vovak1919
    24.12.2019 08:39

    Что такое model.predict_classes? Не нашел описания на сайте keras.io. Я так понял, статья довольно старая, т.к. на том-же keras.io и в книге самого Шолле активация уже не выделяется в отдельный слой, а передается параметром слоя.


    1. worker_sam Автор
      24.12.2019 08:41

      Что такое model.predict_classes? Не нашел описания на сайте keras.io.

      https://kite.com/python/docs/tensorflow.keras.Sequential.predict_classes


      Я так понял, статья довольно старая, т.к. на том-же keras.io и в книге самого Шолле активация уже не выделяется в отдельный слой, а передается параметром слоя.

      Нет, вывод неверный. Статья от 02.2019. Активацию возможно указать без отдельного слоя года с 2016, если не ошибаюсь, тут видимо привычка.


  1. pdima
    24.12.2019 10:56

    Похоже либо пропущена нелинейность либо промежуточный слой лишний:
    model.add(Dense(hidden_dims))
    model.add(Dense(1))


    без нелинейности после первого уровня оба уровня заменяются одним Dense(1)


    1. worker_sam Автор
      24.12.2019 20:23

      Да, похоже вы правы. Без нелинейности два слоя возможно свести к линейной функции, которую может представить и один слой. Разве что, параметров больше.
      Но у автора так, видимо он забыл.
      Добавление активации (например, 'tanh') немного улучшает качество предсказания.