CodinGameでNNを使う

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

1. PyTorchでネットワークを作成
2. 1のネットワークをCodinGame内で使用可能な形式に変換
3. Numpyで推論用のNNを作る


# PyTorchでのネットワークを作成
以下のようなConvolution、ReLU、Flatten、Linearのみからなるネットワークを作成しました。オセロのbot用に使うつもりなので入力サイズが64x64、出力クラス数を64としています。学習については割愛します。

```python
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に変換しておきます。

```python
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の提出用コードに貼り付けることで持ち込みます。

```python
import pickle
import zlib
import base64

e = base64.b64encode(zlib.compress(pickle.dumps(layer_list)))
print(e)

-> b'eJxkd1V4FFnULRDcNVg8bdVdc(中略)S8HB3tDNp2PCbsyCS7BsILgcN5G2N98'
```

変換後の文字列をCodinGameの提出用コードにコピペしてデコードします。

```python
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で学ぶディープラーニングの理論と実装](https://www.oreilly.co.jp/books/9784873117584/)を参考にしました。
これらの内容もすべてCodinGameの提出用コードにベタ書きします。

```python
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)

```