aboutsummaryrefslogtreecommitdiff
path: root/src/cnn_train.py
diff options
context:
space:
mode:
authortslil clingman <tslil@posteo.de>2021-01-29 14:05:06 -0500
committertslil <tslil@posteo.de>2026-08-28 19:37:41 +0100
commitfcface1079b1c44c7ccca3d117c698347c978f20 (patch)
treef0bb660537801a05d591e34db9f705db985dd3d3 /src/cnn_train.py
parent6c1154d80e7f7a04444d01d97942ac9e8138297a (diff)
Syntax errors, small tweak to training data generation
Diffstat (limited to 'src/cnn_train.py')
-rw-r--r--src/cnn_train.py189
1 files changed, 189 insertions, 0 deletions
diff --git a/src/cnn_train.py b/src/cnn_train.py
new file mode 100644
index 0000000..0580b03
--- /dev/null
+++ b/src/cnn_train.py
@@ -0,0 +1,189 @@
+# train5.py, train a small CNN to recognise winning Tak positions
+#
+# Copyright (C) 2021, tslil clingman
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+
+# Loading data
+import pandas as pd
+import numpy as np
+
+# Output of weights
+from tensorflow import transpose
+
+# Custom activation function
+from tensorflow import constant as k
+from tensorflow import clip_by_value
+from tensorflow.math import add, divide, multiply, square
+
+# Computing size
+from tensorflow.keras.backend import get_value
+
+# Building models
+from tensorflow.keras.models import Model
+from tensorflow.keras.layers import Concatenate
+from tensorflow.keras.layers import Input
+from tensorflow.keras.layers import Dense
+from tensorflow.keras.layers import Convolution2D
+from tensorflow.keras.layers import Flatten
+
+
+# We need something that's close to logistic, but cheaper to compute.
+# (12.0+x+50.0*x/(x*x+10.0))/24.0, clipped between 0 and 1
+# as it would otherwise exceed this range at +- 4.6 or so
+def clipped_pade_logistic(x):
+ val = add(k(12.0),
+ add(x, multiply(k(50.0),
+ divide(x, add(square(x), k(10.0))))))
+ return clip_by_value(divide(val, k(24.0)), 0.0, 1.0)
+
+
+# A cheaper version of tanh, x/6+25*x/(6*(2*x*x+5)),
+# clipped between -1 and 1.
+def clipped_pade_tanh(x):
+ val = add(divide(x, k(6.0)),
+ multiply(k(25.0),
+ divide(x, multiply(k(6.0), add(k(5.0),
+ multiply(k(2.0), square(x)))))))
+ return clip_by_value(val, -1.0, 1.0)
+
+
+def make_model(size, magic=[12, 9, 9]):
+ # Our model for the stacks, a small CNN
+ stack_shape = (size, size, 6)
+ stack_input = Input(shape=stack_shape)
+ stack_model = Convolution2D(magic[0], kernel_size=(3, 3), strides=(1, 1),
+ padding='valid', activation="relu",
+ use_bias=True)(stack_input)
+ stack_model = Flatten()(stack_model)
+ stack_model = Model(inputs=stack_input, outputs=stack_model)
+ # The overall model
+ flats_input = Input(shape=(2,))
+ combn_input = Concatenate()([stack_model.output, flats_input])
+ model = Dense(magic[1], activation="relu", use_bias=True)(combn_input)
+ model = Dense(magic[2], activation="relu", use_bias=True)(model)
+ model = Dense(1, activation=clipped_pade_logistic, use_bias=True)(model)
+ model = Model(inputs=[stack_model.input, flats_input], outputs=model)
+ model.compile(optimizer='adam',
+ loss='mean_squared_error',
+ # loss='mean_absolute_error',
+ # loss='mean_absolute_percentage_error',
+ metrics=['accuracy'])
+ model.summary()
+ return model
+
+
+def load_data(size):
+ shape = (-1, size, size, 6)
+ # Load training data
+ tr_fn = "training-"+str(size)+".csv"
+ training_csv = pd.read_csv(tr_fn) #.head(100000)
+ training_data = training_csv
+ training_stack_input = np.array(training_data.iloc[:, 2:-1]).reshape(shape, order='F')
+ training_flats_input = np.array(training_data.iloc[:, 0:2])
+ training_input = [training_stack_input, training_flats_input]
+ training_outcome = training_data.iloc[:, -1:]
+ # Load validation data
+ val_fn = "validation-"+str(size)+".csv"
+ val_data = pd.read_csv(val_fn).tail(20000)
+ val_stack_input = np.array(val_data.iloc[:, 2:-1]).reshape(shape, order='F')
+ val_flats_input = np.array(val_data.iloc[:, 0:2])
+ val_input = [val_stack_input, val_flats_input]
+ val_outcome = val_data.iloc[:, -1:]
+ return [(training_input, training_outcome), (val_input, val_outcome)]
+
+
+def train(size, model, data, iterations=1, epochs=10):
+ (training_input, training_outcome), (val_input, val_outcome) = data
+ results = []
+ for i in range(0, iterations):
+ print("Iteration {0}/{1}".format(i+1, iterations))
+ model.fit(training_input, training_outcome, epochs=epochs,
+ validation_data=(val_input, val_outcome),
+ verbose=True)
+ v_loss, v_accuracy = model.evaluate(val_input, val_outcome,
+ verbose=False, batch_size=16)
+ t_loss, t_accuracy = model.evaluate(training_input, training_outcome,
+ verbose=False, batch_size=32)
+ results += [((v_loss, t_loss), (v_accuracy, t_accuracy))]
+ print("\nScores")
+ for i in range(len(results)):
+ print("Iteration {0}: {1}".format(i+1, results[i]))
+ return results
+
+
+def model_size(model):
+ return sum([np.prod(get_value(w).shape) for w in model.trainable_weights])
+
+
+def magic_search(data):
+ results = []
+ for width in range(9, 18):
+ for dense1 in range(9, 16):
+ for dense2 in range(9, max(32, dense1*2)):
+ m = make_model(5, [width, dense1, dense2])
+ if model_size(m) < 1990:
+ results += [([width, dense1, dense2],
+ train(5, m, data, iterations=1, epochs=20))]
+ print("\nSummary")
+ for r in results:
+ print("Parameters {0}: {1}".format(r[0], r[1][0]))
+ return results
+
+
+def write_weights(model):
+ def fix(val):
+ string = str(np.array(val).tolist())
+ string = string.replace("[", "{").replace("]", "}")
+ return string
+ # Prepare everything in a sane memory order This isn't exactly in
+ # the correct order that tensorflow uses, because memory access
+ # out of order is an eyesore. Compared to tensorflow, the C
+ # implementation has the board reflected about the diagonal.
+ conv2d_weights = transpose(model.trainable_variables[0], perm=[3, 0, 1, 2])
+ conv2d_biases = model.trainable_variables[1]
+ dense1_weights = transpose(model.trainable_variables[2], perm=[1, 0])
+ dense1_biases = model.trainable_variables[3]
+ dense2_weights = transpose(model.trainable_variables[4], perm=[1, 0])
+ dense2_biases = model.trainable_variables[5]
+ output_weights = transpose(model.trainable_variables[6], perm=[1, 0])[0]
+ output_bias = model.trainable_variables[7][0]
+ # Prepare formatting
+ names = ["conv2d_weights[KERN_NUM][KERN_SIZE][KERN_SIZE][KERN_CHAN]",
+ "conv2d_biases[KERN_NUM]",
+ "dense1_weights[DENSE1_NUM][CONV_NUM+2]",
+ "dense1_biases[DENSE1_NUM]",
+ "dense2_weights[DENSE2_NUM][DENSE1_NUM]",
+ "dense2_biases[DENSE2_NUM]",
+ "output_weights[DENSE2_NUM]",
+ "output_bias"]
+ variables = [conv2d_weights, conv2d_biases,
+ dense1_weights, dense1_biases,
+ dense2_weights, dense2_biases,
+ output_weights, output_bias]
+ # Write to file
+ f = open("weights.txt", "w")
+ f.write("#include \"weights.h\"\n\n")
+ for (name, val) in zip(names, variables):
+ f.write("const float "+name+" =\n"+fix(val)+";\n\n")
+ f.close()
+
+
+data = load_data(5)
+model = make_model(5, [12, 11, 8])
+results = train(5, model, data, iterations=5, epochs=10)
+write_weights(model)
+
+# results = magic_search(data)