
Tujuan artikel ini adalah untuk mengajarkan jaringan saraf untuk memainkan game Life tanpa mengajarkannya aturan permainan.
Halo, Habr! Saya mempersembahkan kepada Anda terjemahan artikel "Menggunakan Jaringan Saraf Konvolusional untuk Memainkan Game of Conway's Life with Keras" oleh kylewbanks.
Jika Anda tidak terbiasa dengan permainan yang disebut Life ( ini adalah otomat seluler, diciptakan oleh ahli matematika Inggris John Conway pada tahun 1970 ), aturannya adalah sebagai berikut.
Gim semesta adalah kotak sel persegi dua dimensi yang tak terbatas, masing-masingnya ada di salah satu dari dua kemungkinan keadaan: hidup atau mati (atau masing-masing dihuni dan tidak berpenghuni). Setiap sel berinteraksi dengan delapan tetangganya secara horizontal, vertikal atau diagonal. Pada setiap langkah waktu, transisi berikut terjadi:
- Setiap sel hidup dengan kurang dari dua tetangga yang hidup mati.
- Setiap sel hidup dengan dua atau tiga tetangga yang hidup bertahan hingga generasi berikutnya.
- Setiap sel hidup dengan lebih dari tiga tetangga yang hidup mati.
- Setiap sel mati dengan tiga tetangga yang hidup menjadi sel hidup.
Generasi pertama diciptakan dengan menerapkan aturan-aturan di atas secara bersamaan ke setiap sel dalam keadaan awal, kelahiran dan kematian terjadi secara simultan pada titik-titik waktu yang berbeda. Setiap generasi adalah fungsi murni dari sebelumnya. Aturan terus berlaku untuk generasi baru untuk membuat generasi berikutnya.
Lihat Wikipedia untuk detailnya.
Kenapa melakukan ini? Terutama untuk hiburan, dan untuk belajar sedikit tentang jaringan saraf convolutional.
Jadi ...
Logika game
Hal pertama yang harus dilakukan adalah mendefinisikan fungsi yang menggunakan bidang bermain sebagai input dan mengembalikan status berikutnya.
Untungnya, banyak implementasi yang tersedia di Internet, seperti: https://jakevdp.imtqy.com/blog/2013/08/07/conways-game-of-life/ .
Bahkan, dibutuhkan matriks dari lapangan bermain sebagai input, di mana 0 mewakili sel mati, dan 1 mewakili sel hidup dan mengembalikan matriks dengan ukuran yang sama, tetapi berisi keadaan masing-masing sel pada iterasi permainan berikutnya.
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)
Bermain generasi lapangan
Mengikuti logika game, kita membutuhkan cara untuk menghasilkan bidang game secara acak dan cara memvisualisasikannya.
Fungsi generate_frames
menciptakan num_frames
bidang game acak dengan bentuk tertentu dan probabilitas yang telah ditentukan bahwa setiap sel akan "hidup", dan render_frames
menggambar representasi gambar dari dua bidang game secara berdampingan untuk perbandingan (sel hidup berwarna putih dan sel mati berwarna hitam):
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')
Mari kita lihat seperti apa bidang-bidang ini:
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)
(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]])
Selanjutnya, representasi bilangan bulat dari bidang bermain diambil dan ditampilkan sebagai gambar.
Keadaan lapangan bermain berikut juga ditampilkan di sebelah kanan menggunakan fungsi life_step
:
ender_frames(frames[1], life_step(frames[1]))

Pelatihan bangunan dan set tes
Sekarang kita dapat menghasilkan data untuk pelatihan, verifikasi, dan pengujian.
Setiap elemen dalam y_train
/ y_val
/ y_test
akan mewakili bidang game berikutnya untuk setiap frame bidang dalam 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)
Konstruksi jaringan saraf convolutional
Sekarang kita dapat mengambil langkah pertama menuju membangun jaringan saraf convolutional menggunakan Keras. Titik kunci di sini adalah ukuran kernel (3, 3) dan langkah 1. Mereka memberitahu CNN untuk menggunakan matriks 3x3 sel di sekitarnya untuk setiap sel di bidang yang dilihatnya, termasuk sel saat ini.
Misalnya, jika berikut ini adalah bidang permainan, dan kami berada di sel tengah x
, dia akan melihat semua sel yang ditandai dengan tanda seru !
dan sel
. Kemudian jaringan bergerak di sepanjang sel ke kanan dan melakukan hal yang sama, mengulanginya berulang-ulang sampai memproses setiap sel dan tetangganya di seluruh bidang.
0 0 0 0 0 0! ! ! 0 0! x ! 0 0! ! ! 0 0 0 0 0 0
Sisa dari jaringan ini cukup sederhana, jadi saya tidak akan memerinci. Jika Anda tertarik pada sesuatu, saya sarankan membaca dokumentasinya.
from keras.models import Sequential from keras.layers import Dense, Dropout, Activation, Conv2D, MaxPool2D
Lihatlah output dari fungsi 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 _________________________________________________________________
Melatih dan menyimpan model
Setelah membangun CNN, mari latih model dan simpan ke disk:
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
Model ini memberikan akurasi lebih dari 98% untuk pelatihan dan set tes, yang sangat baik untuk lintasan pertama. Mari kita coba mencari tahu di mana kita membuat kesalahan.
Coba
Mari kita lihat ramalan lapangan bermain acak dan cara kerjanya. Pertama, buat satu bidang bermain dan lihat bingkai berikutnya yang benar:
X, y = generate_dataset(1, board_shape=board_shape, prob_alive=probability_alive) render_frames(X[0].flatten().reshape(board_shape), y)

Selanjutnya, mari kita lakukan prediksi dan lihat berapa banyak sel yang diprediksi secara salah:
pred = model.predict_classes(X) print(np.count_nonzero(pred.flatten() - y.flatten()), "incorrect cells.")
4 incorrect cells.
Selanjutnya, mari kita bandingkan langkah selanjutnya yang benar dengan langkah yang diperkirakan:
render_frames(y, pred.flatten().reshape(board_shape))

Itu tidak menakutkan, tetapi Anda melihat di mana prediksi gagal? Tampaknya jaringan tidak dapat memprediksi sel di tepi lapangan bermain. Mari kita lihat di mana nilai bukan nol menunjukkan prediksi yang salah:
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]]
Seperti yang Anda lihat, semua nilai bukan nol terletak di tepi lapangan bermain. Mari kita lihat suite tes lengkap dan konfirmasikan bahwa pengamatan ini benar.
Lihat bug menggunakan test suite
Kami akan menulis fungsi yang menampilkan peta panas yang menunjukkan di mana model melakukan kesalahan, dan menyebutnya dengan menggunakan seluruh rangkaian uji:
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)

Semua kesalahan ada di tepi dan sudut. Yang logis, karena CNN tidak bisa melihat-lihat, tetapi logika permainan di life_step
melakukan ini. Sebagai contoh, pertimbangkan hal berikut. Melihat sel tepi x
bawah ini, CNN hanya melihat x
dan !
sel:
0 0 0 0 0 ! ! 0 0 0 x ! 0 0 0 ! ! 0 0 0 0 0 0 0 0
Tapi apa yang kita inginkan dan apa yang life_step
lakukan adalah melihat sel-sel dari sisi yang berlawanan:
0 0 0 0 0 ! ! 0 0 ! x ! 0 0 ! ! ! 0 0 ! 0 0 0 0 0
Situasi serupa di sudut:
x ! 0 0 ! ! ! 0 0 ! 0 0 0 0 0 0 0 0 0 0 ! 0 0 0 !
Untuk memperbaikinya, Conv2D
entah bagaimana harus melihat sisi berlawanan dari lapangan bermain. Atau, setiap bidang input dapat diproses untuk mengisi tepi di sisi yang berlawanan, dan kemudian Conv2D dapat dengan mudah menghapus kolom dan baris pertama atau terakhir. Karena kita berada pada belas kasihan Keras dan fungsionalitas isi yang disediakannya yang tidak mendukung apa yang kita cari, kita harus terpaksa menambahkan isi kita sendiri.
Koreksi cacat tepi menggunakan pengisian
Kita perlu melengkapi setiap bidang bermain dengan nilai yang berlawanan untuk meniru bagaimana life_step
bekerja untuk nilai tepi. Kita dapat menggunakan np.pad
dengan mode = 'wrap'
untuk ini. Sebagai contoh, pertimbangkan array berikut dan hasil tambahan di bawah ini:
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]]
Perhatikan bahwa kolom / baris pertama dan kolom / baris terakhir mencerminkan sisi berlawanan dari matriks asli, dan matriks 3x3 tengah adalah nilai x
asli. Misalnya, sel [1] [1] disalin di sisi yang berlawanan dalam sel [4] [1], dan seperti [0] [1] berisi [3] [1]. Di semua arah dan bahkan di sudut, array dikoreksi sehingga mengandung sisi yang berlawanan. Ini akan memungkinkan CNN untuk meninjau seluruh lapangan bermain dan menangani kasus ekstrim dengan benar.
Sekarang kita dapat menulis fungsi untuk mengisi semua matriks input kami:
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)
Semua dataset sekarang dilengkapi dengan kolom / baris yang dibungkus, yang memungkinkan CNN untuk melihat sisi berlawanan dari lapangan bermain, seperti halnya life_step
. Karena itu, setiap lapangan bermain sekarang memiliki ukuran 22x22 bukan 20x20 asli.
Kemudian, CNN harus dibangun kembali untuk membuang bantalan menggunakan padding = 'valid'
(yang memberitahu Conv2D untuk membuang tepi, meskipun ini tidak segera jelas), dan menangani input_shape
baru. Jadi, ketika kita melewatkan bidang bermain dengan ukuran 22x22, kita masih mendapatkan ukuran 20x20 sebagai output, karena kita membuang kolom / baris pertama dan terakhir. Sisanya tetap identik:
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 _________________________________________________________________
Sekarang kita bisa belajar menggunakan bidang yang disejajarkan:
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
Keakuratan prediksi adalah dari 98% hingga 100%, yang kami terima sebelum menambahkan lekukan. Mari kita lihat kesalahan pada test case:
view_prediction_errors(model_padded, X_test_padded, y_test)

Hebat! Peta panas hitam menunjukkan bahwa tidak ada perbedaan dalam nilai, dan ini berarti bahwa kami telah berhasil memprediksi setiap sel untuk setiap game.
Itu adalah latihan kecil yang menyenangkan untuk bermain dengan jaringan saraf convolutional tanpa menggunakan dataset besar. Silakan cek di GitHub .