PyTorchとKerasの簡単な手作り判別器を作ったので使い心地比較

　何回かPyTorchとKerasを使ったことはありますが、サンプルプロジェクトの改変だけだったので、理解を深めるために簡単なニューラルネットワークをいちから構成して、書きやすさ・挙動等を比較しました。

　解析対象となるデータは、以前自分がやった自主研究を使いまわします。あるゲームに出てくるキャラクターのパラメータから性別は判定できるのかというものです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288125/d84b6cd3-118a-1735-02b2-c358995682ba.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288125/7b7dd9f4-4866-e320-6024-51b338835fb0.png)

※本来は8つのパラメータを持つが、図示のため2つの主成分に集約して表示

　正解データはこのような感じ。このように入り組んでいるので、精度100%を出すのは不可能で、おおよそどのような手法を使っても予想モデルは6割～7割程度の精度になります。

# 線形判別分析

　まず、一番単純な例として線形判別分析。各パラメータの線形結合により、性別で分けられたグループを一番正答率よく分けられるような判別式を決定します。空間をまっすぐな仕切りで区切る感じです。

```python
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import sklearn
import time
from sklearn.decomposition import PCA
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split

# 時間計測
start = time.time()

# データの作成
df = pd.read_csv('growth_rate.csv')
X = df.iloc[:, 3:].values
t = df['sex'].replace('Male', 1).replace('Female', 0)
x_train = X[:400]
t_train = df.sex[:400].replace('Male', 0).replace('Female', 1)
x_test = X[400:]
t_test = df.sex[400:].replace('Male', 0).replace('Female', 1)
print(x_train.shape, t_train.shape)

# ここで線形判別分析
lda = LinearDiscriminantAnalysis()
lda.fit(x_train, t_train)

# 予測精度
print("経過時間: {0} [sec]".format(time.time()-start))
print("予測精度: {0}".format(accuracy_score(t_test, lda.predict(x_test))))

# ヒートマップ
grange = 150
plt.figure(figsize=(10, 10))
pca = PCA(n_components=2)
tx = pca.fit_transform(X)
px, py = np.meshgrid(np.arange(-grange, grange, 1),
                     np.arange(-grange, grange, 1))
x = px.reshape((4*grange*grange, 1))
y = py.reshape((4*grange*grange, 1))
xy = np.concatenate([x, y], 1)
invX = pca.inverse_transform(xy)
z = lda.transform(invX)
z = z.reshape(2*grange, 2*grange)
plt.contourf(px, py, z, alpha=1.0, cmap='coolwarm')
Y = lda.predict(X)
col_sex = ['b' if y == 0 else 'r' for y in Y]
plt.scatter(tx[:, 0], tx[:, 1], color=col_sex)
plt.ylim((-grange, grange))
plt.xlim((-grange, grange))
plt.show()
```

結果

```python
経過時間: 0.01602792739868164 [sec]
予測精度: 0.6276595744680851
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288125/a71268cd-c3bb-1f6d-6cf4-15bbd847170d.png)

　たかだか$588*8$のデータなので、一瞬で完了します。精度は6割程度。まっすぐ区切られずに境界がややデコボコしているのは、8次元のパラメータを無理矢理2次元に押し込めているからです（本当は他にも奥行きがある）。ヒートマップの作り方は[過去記事](https://qiita.com/hibit/items/71d8874eaf448267c6a3)で解説しています。

# PyTorchとKerasの比較

　いよいよ本題。上と同じようなことをPyTorchとKerasのセミスクラッチコードでやっていきます。また、今回のような規模では**全く恩恵はないですが**あえてGPUを使った計算を行います。また、このような限られたかつ不規則なデータでは、どのような判別式を作ろうと（テストケースで）良い精度が出せないことは明らかなので、これは深層学習を使ってパフォーマンスの向上を図るもの**ではありません**。念の為。

## GPUの確認

```python:PyTorch
device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
print(device)  # 使えるなら「cuda:0」
```
```python:Keras
print(device_lib.list_local_devices()) # 使えるならname: GeForce RTX 以下略
```

　PyTorchは最初に明示的に`device`を指示する必要があります。Kerasの方は、`pip`した段階で`tensorflow-gpu`をインストールできていれば、コードは全く同じでもGPUを使って計算してくれます。その代わり（？）インストールが一筋縄ではいかなかったりするのですが……。

## データの作成

```python:PyTorch&Keras
df = pd.read_csv('growth_rate.csv')
X = df.iloc[:, 3:].values
x_train = X[:400]
t_train = df.sex[:400].replace('Male', 0).replace('Female', 1)
x_test = X[400:]
t_test = df.sex[400:].replace('Male', 0).replace('Female', 1)
```

　これはどちらも同じです。ただ、PyTorchは更に以下の変換が必要になります。

```python:PyTorch
x_train = torch.Tensor(x_train)
t_train = torch.Tensor(t_train.values).reshape(-1, 1)
x_test = torch.Tensor(x_test)
t_test = torch.Tensor(t_test.values).reshape(-1, 1)

x_train = x_train.to(device)
t_train = t_train.to(device)
x_test = x_test.to(device)
t_test = t_test.to(device)
```

　上の4つでは、`numpy.array`型のデータをPyTorchで使う用に`torch.Tensor`型に変換しています。下の4つでは、生の`torch.Tensor`型ではGPUの計算に使えないため、`to(device)`というメソッドによりGPU用のデータへ変換します。**CPU用のデータとGPU用のデータは混在して計算できません**。不便ですね。Kerasはこの点、常に`numpy.array`で統一しているので便利です。

　参考として、ここで`shape`（これは`numpy.array`にも`torch.Tensor`にもあります）を見てみると違いがわかります。

```python:PyTorch&Keras
print(x_train.shape)
print(t_train.shape)
print(x_test.shape)
print(t_test.shape)
```

```python:Output(PyTorch)
torch.Size([400, 8])
torch.Size([400, 1])
torch.Size([188, 8])
torch.Size([188, 1])
```

```python:Output(Keras)
(400, 8)
(400,)
(188, 8)
(188,)
```

　また、PyTorchではミニバッチ学習のためにデータローダーを作成することが必要です。Kerasは特にこの手間は必要なし。

```python:PyTorch
dataset = TensorDataset(x_train, t_train)
loader = DataLoader(dataset, batch_size=64, shuffle=True)
```

## モデルの定義

```python:PyTorch
model = nn.Sequential(
    nn.Linear(8, 64),
    nn.BatchNorm1d(64),
    nn.Sigmoid(),
    nn.Linear(64, 1),
    nn.Sigmoid(),
)
model.to(device) #CPU -> GPU
loss_fn = nn.MSELoss()
optimizer = optim.SGD(model.parameters(), lr=0.01)
```

```python:Keras
model = Sequential()
model.add(Dense(activation='sigmoid', input_dim=8, units=64))
model.add(Dense(units=1, activation='sigmoid'))
model.compile(loss='mean_squared_error',
              optimizer=SGD(lr=0.01), metrics=['accuracy'])
```

　3層の単純な全結合モデルなので、違いが出ようがないような状況ですが、両者とも、数行でモデルを定義できます。両者とも、8 -> 64 -> 1 の全結合モデル、活性化関数はいずれもシグモイド、ロスは平均二乗誤差、最適化は勾配降下法、学習率は$0.01$にしています。

#学習開始
```python:PyTorch
loss_train = []
loss_test = []
epoch_num = 500
for epoch in tqdm(range(epoch_num)):
    model.train()
    for x, t in loader:
        x.to(device) #CPU -> GPU
        optimizer.zero_grad()
        y = model(x)
        loss = loss_fn(y, t)
        loss.backward()
        optimizer.step()
    loss_train.append(loss_fn(model(x_train), t_train))
    loss_test.append(loss_fn(model(x_test), t_test))
```

```python:Keras
epoch_num = 500
history = model.fit(x_train, t_train, batch_size=64,
                    epochs=epoch_num, verbose=1, validation_data=(x_test, t_test))
```

　Kerasのコードの短さが際立ちます。Kerasはこのグラフ化も`history`の内部に組み込んで自動化してくれていますが、反面かなりブラックボックス化しているので、例えば学習の様子を確認したいとか、epochの途中で何らかの操作をしたいとかの応用は逆に難しそうです。

　また、Kerasはこのコードを回すだけでもコンソールに進捗率を書いてくれますが（親切！）、PyTorchは特にそういうことはしてくれないので`tqdm`で別個に進捗率を表示してくれるようにしています。今回の学習は一瞬で終わりますが、一般的に、進捗率がわからないまま長時間の計算をさせるのは、ストレスになるだけでなく、エラーが起こった時にわからないので危険です。

#学習の様子

```python:PyTorch
 28%|█████████████████████████████████████████████████████                                                                                                                                     | 285/1000 [00:08<00:22, 32.39it/s]
```

　これはPyTorchの機能ではないですが、`tqdm`で進捗率を表示しています。もちろん、コードで書き加えればloss等も逐一表示してくれますが、いずれにせよ手作業が必要です。

```python:Keras
Train on 400 samples, validate on 188 samples
Epoch 1/1000
400/400 [==============================] - 0s 509us/step - loss: 0.2521 - accuracy: 0.5000 - val_loss: 0.2596 - val_accuracy: 0.5585
Epoch 2/1000
400/400 [==============================] - 0s 25us/step - loss: 0.2447 - accuracy: 0.5775 - val_loss: 0.2606 - val_accuracy: 0.5585
Epoch 3/1000
400/400 [==============================] - 0s 23us/step - loss: 0.2388 - accuracy: 0.6225 - val_loss: 0.2620 - val_accuracy: 0.5585
Epoch 4/1000
400/400 [==============================] - 0s 22us/step - loss: 0.2335 - accuracy: 0.6475 - val_loss: 0.2631 - val_accuracy: 0.5585
Epoch 5/1000
400/400 [==============================] - 0s 25us/step - loss: 0.2302 - accuracy: 0.6675 - val_loss: 0.2634 - val_accuracy: 0.5585
（後略）
```
　
　めちゃくちゃわかりやすく進捗率を出してくれます。

#学習率のグラフ

PyTorch

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288125/aba2aed3-f0e4-c882-2809-e4a7d9e99455.png)

```python
経過時間: 23.741425275802612 [sec]
```

Keras

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288125/6996e20b-34d4-498c-4651-51f6d4567a6b.png)

```python
経過時間: 19.76099944114685 [sec]
```

　両者ともtestロスは0.21ぐらいで頭打ちです（大きな違いが出たらそれこそ怖いですが）。Kerasの方はなぜ線が振動しています。SGDの学習率が大きくて安定していないのかなと思いましたが、PyTorchの方では特にそのような傾向は見られないので、謎です。時間はわずかにKerasの方が速かったです。

# ヒートマップ

PyTorch

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288125/ed1da7f4-6f91-eee7-732b-96ec2a0e3571.png)

Keras

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288125/7a9c6502-5189-0a33-83a2-6b88846962a1.png)

　線形判別分析のヒートマップとさほど変わりはないですが、線形判別分析の時と違い、ヒートマップが曲線的になっていることがわかります。今回はデータにそこまで峻別性がないのであれですが、非線形なグループ分けがある場合はおそらく大きな力を発揮するでしょう。乱数によって実行結果が違うので、また実行すれば違う結果が返ってきます。

　今回の本筋ではないですが、このようなヒートマップを作ろうとした場合、PyTorchではCPUとGPUの行き来がややめんどくさいです。

```python
# ヒートマップ描画
plt.figure(figsize=(10, 10))
grange = 150
pca = PCA(n_components=2)
tx = pca.fit_transform(X)
px, py = np.meshgrid(np.arange(-grange, grange, 1),
                     np.arange(-grange, grange, 1))
x = px.reshape((4*grange*grange, 1))
y = py.reshape((4*grange*grange, 1))
xy = np.concatenate([x, y], 1)
invX = pca.inverse_transform(xy)
z = model(torch.Tensor(invX).to(device))  # CPU -> GPU
z = z.reshape(2*grange, 2*grange)
z = z.to('cpu').detach().numpy()  # GPU -> CPU
plt.contourf(px, py, z, alpha=1.0, cmap='coolwarm')
Y = model(torch.Tensor(X).to(device))  # CPU -> GPU
Y = Y.to('cpu').detach().numpy()  # GPU -> CPU
col_sex = ['b' if y < 0.4 else 'r' for y in Y]
plt.scatter(tx[:, 0], tx[:, 1], color=col_sex)
plt.ylim((-grange, grange))
plt.xlim((-grange, grange))
plt.show()
```

　`numpy.adarray`を前提にした操作をする際にいちいち再変換が必要になるというのはPyTorchのさりげにめんどくさいポイントかもしれません。

#GPUの使用率

PyTorch

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288125/b70ef662-90eb-f66d-c405-1e731b300bbb.png)

　徐々に上がる。

Keras

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288125/2f8cbb7b-0957-0e58-d746-d7d7c5d669aa.png)

　一気に上限まで上がる。
　ただし、これは性能とはあまり関係ないです。

# 感想

　単純なモデルでの比較でしたが、新しく勉強したPyTorchが思ったより書きづらくなかったので（CPUとGPUの切り替えはややめんどうですが）、今後はこちらを中心に使っていこうと思います。複雑なモデルになるとPyTorchの方が対応しやすいらしいですし、何より**最近の論文ではPyTorchを使っている方が多い**ので。論文界隈ではなく、会社でのデータ解析とかだとまたシェアは違うのかもしれません。

　環境構築の際に微妙にネックとなるGPU対応については、むしろPyTorchの方がやりやすいような気がしました。これは個々人の慣れや環境や**運やPCの体調**によっても左右されるので何とも言えません。

# ソースコード全文

　コピーして再現したい人という奇特な方むけ。データソースは[GitHubリポジトリ](https://github.com/hibit-at/FE_growth_rate)にあります。というか、GitHubからそのままクローンした方が確実だと思います。

```python:PyTorch
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import sklearn
import torch
import time
from sklearn.decomposition import PCA
from torch import nn, optim
from tqdm import tqdm
from torch.utils.data import TensorDataset, DataLoader

# Pytorch ... GPUが使えるならGPU
device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
print(device)  # 使えるなら「cuda:0」

# 時間計測
start = time.time()

# データの作成
df = pd.read_csv('growth_rate.csv')
X = df.iloc[:, 3:].values
x_train = X[:400]
t_train = df.sex[:400].replace('Male', 0).replace('Female', 1)
x_test = X[400:]
t_test = df.sex[400:].replace('Male', 0).replace('Female', 1)

# データ変換　numpy.array -> torch.Tensor
x_train = torch.Tensor(x_train)
t_train = torch.Tensor(t_train.values).reshape(-1, 1)
x_test = torch.Tensor(x_test)
t_test = torch.Tensor(t_test.values).reshape(-1, 1)

# データ変換　CPU -> GPU
x_train = x_train.to(device)
t_train = t_train.to(device)
x_test = x_test.to(device)
t_test = t_test.to(device)

print(x_train.shape)
print(t_train.shape)
print(x_test.shape)
print(t_test.shape)

dataset = TensorDataset(x_train, t_train)
loader = DataLoader(dataset, batch_size=64, shuffle=True)

# モデルの定義
model = nn.Sequential(
    nn.Linear(8, 64),
    nn.BatchNorm1d(64),
    nn.Sigmoid(),
    nn.Linear(64, 1),
    nn.Sigmoid(),
)
model.to(device)
loss_fn = nn.MSELoss()
optimizer = optim.SGD(model.parameters(), lr=0.01)

# 学習開始
loss_train = []
loss_test = []
epoch_num = 1000
for epoch in tqdm(range(epoch_num)):
    model.train()
    for x, t in loader:
        x.to(device)
        optimizer.zero_grad()
        y = model(x)
        loss = loss_fn(y, t)
        loss.backward()
        optimizer.step()
    loss_train.append(loss_fn(model(x_train), t_train))
    loss_test.append(loss_fn(model(x_test), t_test))

# 経過時間
print("経過時間: {0} [sec]".format(time.time()-start))

# 学習率のグラフ
x = np.arange(1, epoch_num+1, 1)
plt.plot(x, loss_train)
plt.plot(x, loss_test)
plt.show()

# ヒートマップ描画
plt.figure(figsize=(10, 10))
grange = 150
pca = PCA(n_components=2)
tx = pca.fit_transform(X)
px, py = np.meshgrid(np.arange(-grange, grange, 1),
                     np.arange(-grange, grange, 1))
x = px.reshape((4*grange*grange, 1))
y = py.reshape((4*grange*grange, 1))
xy = np.concatenate([x, y], 1)
invX = pca.inverse_transform(xy)
z = model(torch.Tensor(invX).to(device))  # CPU -> GPU
z = z.reshape(2*grange, 2*grange)
z = z.to('cpu').detach().numpy()  # GPU -> CPU
plt.contourf(px, py, z, alpha=1.0, cmap='coolwarm')
Y = model(torch.Tensor(X).to(device))  # CPU -> GPU
Y = Y.to('cpu').detach().numpy()  # GPU -> CPU
col_sex = ['b' if y < 0.4 else 'r' for y in Y]
plt.scatter(tx[:, 0], tx[:, 1], color=col_sex)
plt.ylim((-grange, grange))
plt.xlim((-grange, grange))
plt.show()
```

```python:keras
import time
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from keras.layers import Activation, BatchNormalization, Dense
from keras.models import Sequential, model_from_json
from keras.optimizers import SGD, RMSprop
from sklearn.decomposition import PCA
from tensorflow.python.client import device_lib

# Tensorflow ... GPUが使えるかの確認
print(device_lib.list_local_devices())

# 時間計測
start = time.time()

# データの作成
df = pd.read_csv('growth_rate.csv')
X = df.iloc[:, 3:].values
x_train = X[:400]
t_train = df.sex[:400].replace('Male', 0).replace('Female', 1)
x_test = X[400:]
t_test = df.sex[400:].replace('Male', 0).replace('Female', 1)

print(x_train.shape)
print(t_train.shape)
print(x_test.shape)
print(t_test.shape)

# モデルの定義
model = Sequential()
model.add(Dense(64, input_dim=8))
model.add(BatchNormalization())
model.add(Activation('sigmoid'))
model.add(Dense(1, activation='sigmoid'))
model.compile(loss='mean_squared_error',
              optimizer=SGD(lr=0.01), metrics=['accuracy'])


# 学習開始
epoch_num = 1000
history = model.fit(x_train, t_train, batch_size=64,
                    epochs=epoch_num, verbose=1, validation_data=(x_test, t_test))

# 経過時間
print("経過時間: {0} [sec]".format(time.time()-start))

# 学習率のグラフ
plt.plot(range(1, epoch_num+1), history.history['loss'], label='training')
plt.plot(range(1, epoch_num+1),
         history.history['val_loss'], label='validation')
plt.show()

# ヒートマップ
grange = 150
plt.figure(figsize=(10, 10))
pca = PCA(n_components=2)
tx = pca.fit_transform(X)
px, py = np.meshgrid(np.arange(-grange, grange, 1),
                     np.arange(-grange, grange, 1))
x = px.reshape((4*grange*grange, 1))
y = py.reshape((4*grange*grange, 1))
xy = np.concatenate([x, y], 1)
invX = pca.inverse_transform(xy)
z = model.predict(invX)
z = z.reshape(2*grange, 2*grange)
plt.contourf(px, py, z, alpha=1.0, cmap='coolwarm')
Y = model.predict(X)
col_sex = ['b' if y < 0.4 else 'r' for y in Y]
plt.scatter(tx[:, 0], tx[:, 1], color=col_sex)
plt.ylim((-grange, grange))
plt.xlim((-grange, grange))
plt.show()
```