はじめに
CodinGameでは自分で作成したbotを使って他の人が作ったbotと対戦することができます。botに深層強化学習などのディープラーニング手法を使ってみたかったのですが、PyTorchを使うことはできず、また外部のリソースを用いることもできません。そのため、別の手段を用いる必要があります。
手順は以下の通りです。
- PyTorchでネットワークを作成
- 1のネットワークをCodinGame内で使用可能な形式に変換
- Numpyで推論用のNNを作る
PyTorchでのネットワークを作成
以下のようなConvolution、ReLU、Flatten、Linearのみからなるネットワークを作成しました。オセロのbot用に使うつもりなので入力サイズが64x64、出力クラス数を64としています。学習については割愛します。
import torch.nn as nn
class SLTrainingPolicy(nn.Module):
def __init__(self):
super().__init__()
flatten_dim = 4 * 4 * 256
self.network = nn.Sequential(
nn.Conv2d(1, 256, 3, 1, 0),
nn.ReLU(),
nn.Conv2d(256, 256, 3, 1, 0),
nn.ReLU(),
nn.Flatten(),
nn.Linear(flatten_dim, 128),
nn.ReLU(),
nn.Linear(128, 64)
)
def forward(self, board):
return self.network(board)
重みの変換
ネットワークの各レイヤーについて、レイヤーの種類およびパラメータをリストに順番に格納します。パラメータはNumpyのarrayに変換しておきます。
layer_list = []
for i, l in enumerate(model.network):
# layerの種類を判別する
if type(l) == torch.nn.modules.conv.Conv2d:
layer_type = "Conv2d"
elif type(l) == torch.nn.modules.linear.Linear:
layer_type = "Linear"
elif type(l) == torch.nn.modules.activation.ReLU:
layer_type = "ReLU"
elif type(l) == torch.nn.modules.flatten.Flatten:
layer_type = "Flatten"
else:
raise NotImplementationError
# Conv2dとLinearにはパラメータがあるので、Numpyのarrayに変換しておく
if layer_type in ["Conv2d", "Linear"]:
weight = model.state_dict()[f"network.{i}.weight"].numpy().astype(np.float16)
bias = model.state_dict()[f"network.{i}.bias"].numpy().astype(np.float16)
layer = {
"number": i,
"type": layer_type,
"weight": weight,
"bias": bias
}
else:
layer = {
"number": i,
"type": layer_type
}
layer_list.append(layer)
このオブジェクトをそのままCodinGame内に持ち込むことはできません。そこで、base64でエンコードしてその文字列をCodinGameの提出用コードに貼り付けることで持ち込みます。
import pickle
import zlib
import base64
e = base64.b64encode(zlib.compress(pickle.dumps(layer_list)))
print(e)
-> b'eJxkd1V4FFnULRDcNVg8bdVdc(中略)S8HB3tDNp2PCbsyCS7BsILgcN5G2N98'
変換後の文字列をCodinGameの提出用コードにコピペしてデコードします。
import pickle
import zlib
import base64
encode_string = b'eJxkd1V4FFnULRDcNVg8bdVdc(中略)S8HB3tDNp2PCbsyCS7BsILgcN5G2N98'
layers_list = pickle.loads(zlib.decompress(base64.b64decode(encode_string)))
注意点として、CodinGameではコードの文字数に約10万文字の制限があります。そのため、あまりパラメータ数の多いネットワークを持ち込むことはできません。
Numpyで推論用NNを作る
上のネットワークで使用したConv2d、Linear、ReLU、Flattenに対応するレイヤーをNumpyで定義します。今回は推論だけできればいいのでforwardのみ定義します。実装に際しては
ゼロから作るDeep Learning ――Pythonで学ぶディープラーニングの理論と実装を参考にしました。
これらの内容もすべてCodinGameの提出用コードにベタ書きします。
class ReLU:
def __init__(self):
pass
def forward(self, x):
mask = (x <= 0)
out = x.copy()
out[mask] = 0
return out
class Flatten:
def __init__(self):
pass
def forward(self, x):
N, C, H, W = x.shape
out = x.reshape(N, -1)
return out
class Linear:
def __init__(self, W, b):
self.W = W.T
self.b = b
def forward(self, x):
out = np.dot(x, self.W) + self.b
return out
# Conv2dの畳み込み処理のための補助関数
def im2col(input_data, filter_h, filter_w, stride=1, pad=0):
N, C, H, W = input_data.shape
out_h = (H + 2*pad - filter_h)//stride + 1
out_w = (W + 2*pad - filter_w)//stride + 1
img = np.pad(input_data, [(0,0), (0,0), (pad, pad), (pad, pad)], 'constant')
col = np.zeros((N, C, filter_h, filter_w, out_h, out_w))
for y in range(filter_h):
y_max = y + stride*out_h
for x in range(filter_w):
x_max = x + stride*out_w
col[:, :, y, x, :, :] = img[:, :, y:y_max:stride, x:x_max:stride]
col = col.transpose(0, 4, 5, 1, 2, 3).reshape(N*out_h*out_w, -1)
return col
class Conv2d:
def __init__(self, W, b, stride=1, pad=0):
self.W = W
self.b = b
self.stride = stride
self.pad = pad
def forward(self, x):
FN, C, FH, FW = self.W.shape
N, C, H, W = x.shape
out_h = int(1 + (H + 2 * self.pad - FH) / self.stride)
out_w = int(1 + (W + 2 * self.pad - FW) / self.stride)
col = im2col(x, FH, FW, self.stride, self.pad)
col_W = self.W.reshape(FN, -1).T
out = np.dot(col, col_W) + self.b
out = out.reshape(N, out_h, out_w, -1).transpose(0, 3, 1, 2)
return out
class Model:
def __init__(self, layer_list):
self.layers = []
for l in layer_list:
layer_type = l["type"]
if layer_type in ["Conv2d", "Linear"]:
weight = l["weight"]
bias = l["bias"]
if layer_type == "Conv2d":
self.layers.append(Conv2d(weight, bias))
elif layer_type == "Linear":
self.layers.append(Linear(weight, bias))
elif layer_type == "ReLU":
self.layers.append(ReLU())
elif layer_type == "Flatten":
self.layers.append(Flatten())
def forward(self, x):
for i, l in enumerate(self.layers):
x = l.forward(x)
return x
encode_string = b'eJxkd1V4FFnULRDcNVg8bdVdc(中略)S8HB3tDNp2PCbsyCS7BsILgcN5G2N98'
layers_list = pickle.loads(zlib.decompress(base64.b64decode(encode_string)))
model = Model(layer_list)