LoginSignup
4
2

More than 1 year has passed since last update.

CodinGameでNNを使う

Posted at

はじめに

CodinGameでは自分で作成したbotを使って他の人が作ったbotと対戦することができます。botに深層強化学習などのディープラーニング手法を使ってみたかったのですが、PyTorchを使うことはできず、また外部のリソースを用いることもできません。そのため、別の手段を用いる必要があります。
手順は以下の通りです。

  1. PyTorchでネットワークを作成
  2. 1のネットワークをCodinGame内で使用可能な形式に変換
  3. 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)

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2