TensorFlowしか使ったことない初心者がPytorchも触って比較した感想

# はじめに
[[第3版]Python機械学習プログラミング 達人データサイエンティストによる理論と実践](https://book.impress.co.jp/books/1120101017)を使用して機械学習を2ヶ月ほど勉強しております。

深層学習を学ぶにあたって、大きくフレームワークとしては`TensorFlow`と`Pytorch`の2つがあると思います。この書籍では`TensorFlow`を用いていたため、それに従って`TensorFlow`を学び、ある程度は使えるようになったかと思います。ただ`Pytorch`はノータッチ。

そのためKaggleなどのコードが`Pytorch`で書かれてたりすると「ん、`Pytorch`かスルーしよ」ってなってました。
ただ最近GANに興味があり、最新のモデルの論文が`Pytorch`で書かれていることが多く、まぁ使えるライブラリは多いに越したことないよなってことで[コチラ](https://yutaroogawa.github.io/pytorch_tutorials_jp/)の公式チュートリアルを日本語訳版にして頂いたサイトを全力で感謝しながら、とりあえずCIFAR-10の画像分類までやってみました。

すでに`Pytorch`をメインで触っている方の[比較記事](https://qiita.com/Rahariku/items/69b512fad60242fbd6ac)もありますが、
`TensorFlow`（と`keras`)をメインで触っている初心者の比較ということで見て頂けたら。

また`TensorFlow`単体で書くことはほとんどないので、`TensorFlow/keras` と `Pytorch`の比較として見てください。

# 環境・バージョン・データ
AnacondaのJupyter labを使用。

バージョンは
・TensorFlow : 2.5.0
・Pytorch : 1.9.0

データは先述したようにCIFAR-10を使います。


# import

## TensorFlow
```python
import tensorflow as tf
from tensorflow.keras.layers import *
from tensorflow.keras.models import Model, Sequential
from tensorflow.keras.datasets import cifar10
from tensorflow.keras.datasets.cifar10 import load_data
```


## Pytorch
```python
import torch
import torchvision
import torchvision.transforms as transforms
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
```

`TensorFlow`の中の`keras`から必要なものは持ってこれます。
`keras.layers`がCNNモデル構築の時のさまざまな層(Dense, Conv2Dなど）を表現
`keras.models`がモデルを表現

`TensorFlow`単体というより`keras`とセットで使うことがほとんどですね

---
`Pytorch`は大きく`torch`と`torchvision`の2つから必要なものを引っ張ってくるのかな。
`torch`がモデルや層やアルゴリズムなどモデル構築に必要なもの。
`torchvision`はデータセットの用意などに使う印象です。

# データの用意・前処理
## TensorFlow
```Python
# データセットを取得
(X_train, y_train), (X_test, y_test) = load_data()

# [-1,1]スケールに変換
X_train = (X_train - 127.5 ) / 127.5
X_test = (X_test - 127.5) / 127.5
```
## Pytorch
```Python
# データに対してどういう変換をするかまとめて定義
transform = transforms.Compose(
    [transforms.ToTensor(),
     transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])

# データセットのDL,バッチ・シャッフル
trainset = torchvision.datasets.CIFAR10(
    root = './data', train = True, download=True, transform=transform)

trainloader = torch.utils.data.DataLoader(
    trainset, batch_size=4, shuffle=True)

testset = torchvision.datasets.CIFAR10(
    root = './data', train = False, download=True, transform=transform)

testloader = torch.utils.data.DataLoader(
    testset, batch_size=4, shuffle=False)
```

`TensorFlow`の方は`load_data`をimportしているのもありますが、すぐにデータを用意してくれますね。

CIFAR-10の場合、データを取得した段階で既に4次元データ(データ数、高さ、幅、チャネル数)なので楽です。

この後`tf.data.Dataset.from_tensor_slices`を使えばデータセット化、バッチ、シャッフルなどできますが、別にこのままでもfitできるので、、、

とにかく楽という印象。

---

`Pytorch`はtrainとtest別々にデータセットを読み取って`Dataloader`でバッチ、シャッフルする形。

`transform`で行いたい前処理をまとめてできる。

そう考えるとTensorflowでいう、`tf.data.Dataset.from_tensor_slices`とやってることは似てるのかな。

どちらにしても慣れてしまえば簡単そう。

# モデル構築
## TensorFlow
```python
model = Sequential()
model.add(Conv2D(filters=6, kernel_size=5, input_shape=(32, 32, 3))) # 畳み込み層
model.add(Activation('relu')) # 活性化関数
model.add(MaxPooling2D(2, 2)) # プーリング層

model.add(Conv2D(filters=16, kernel_size=5))
model.add(Activation('relu'))
model.add(MaxPooling2D(2, 2))

model.add(Flatten()) # 1次元化
model.add(Dense(120)) # 全結合層
model.add(Activation('relu'))
model.add(Dense(84))
model.add(Activation('relu'))
model.add(Dense(10))
model.add(Activation('softmax'))
```

## Pytorch
```python
class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(3, 6, 5) # 畳み込み層
        self.pool = nn.MaxPool2d(2, 2) # プーリング層
        self.conv2 = nn.Conv2d(6, 16, 5)
        self.fc1 = nn.Linear(16 * 5 * 5, 120) # 全結合層
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, 10)        
        
    def forward(self, x):
        x = self.pool(F.relu(self.conv1(x)))
        x = self.pool(F.relu(self.conv2(x)))
        x = x.view(-1, 16 * 5 * 5)
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))    
        x = self.fc3(x)
        return x

net = Net()
```
一緒のモデル構造になるようにしています。

`TensorFlow`の方は、Sequential()でインスタンス化した後に
addで層を上から追加していくイメージで書きやすい。

ただ同じことを何回も書くのでめんどくさいと言えばめんどくさいか。

---

`Pythoch`はクラス定義を行って、__init__関数の中に使う層をまず定義しておきます。

その後forward関数で、入力から出力までの流れを書いていく。

ちゃんと理解していないと`Pytorch`は書けなさそう。

`TensorFlow`の場合とりあえずConv2D, Activation, MaxPoolingって書いてても、成立するけど、`Pytorch`の場合、入力と出力の形がどうなるかわかってないと書けないかなと。

`TensorFlow`は楽だけど、知識・理論的な部分は曖昧でも動くものは書ける。
`Pytorch`はしっかり理解していないと難しいけど、入力から出力までの流れがわかって良いかも。

# モデル概要
## TensorFlow
```python
model.summary()

# ->
Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d (Conv2D)              (None, 28, 28, 6)         456       
_________________________________________________________________
activation (Activation)      (None, 28, 28, 6)         0         
_________________________________________________________________
max_pooling2d (MaxPooling2D) (None, 14, 14, 6)         0         
_________________________________________________________________
conv2d_1 (Conv2D)            (None, 10, 10, 16)        2416      
_________________________________________________________________
activation_1 (Activation)    (None, 10, 10, 16)        0         
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 5, 5, 16)          0         
_________________________________________________________________
flatten (Flatten)            (None, 400)               0         
_________________________________________________________________
dense (Dense)                (None, 120)               48120     
_________________________________________________________________
activation_2 (Activation)    (None, 120)               0         
_________________________________________________________________
dense_1 (Dense)              (None, 84)                10164     
_________________________________________________________________
activation_3 (Activation)    (None, 84)                0         
_________________________________________________________________
dense_2 (Dense)              (None, 10)                850       
_________________________________________________________________
activation_4 (Activation)    (None, 10)                0         
=================================================================
Total params: 62,006
Trainable params: 62,006
Non-trainable params: 0
_________________________________________________________________
```
## Pytorch
```python
print(net)

# ->
Net(
  (conv1): Conv2d(3, 6, kernel_size=(5, 5), stride=(1, 1))
  (pool): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  (conv2): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1))
  (fc1): Linear(in_features=400, out_features=120, bias=True)
  (fc2): Linear(in_features=120, out_features=84, bias=True)
  (fc3): Linear(in_features=84, out_features=10, bias=True)
)
```
付属のメソッドで見る分にはTensorFlowの方が情報量は多いのかな。
拡張パッケージを使えばどちらも、細かいところまで見れるし変わらないかも。

# 損失関数・アルゴリズム
## TensorFlow
```python
optimizer = tf.keras.optimizers.Adam(learning_rate=0.001)
model.compile(loss='sparse_categorical_crossentropy', optimizer = optimizer, metrics=['accuracy'])
```

## Pytorch
```python
loss_fn = nn.CrossEntropyLoss()
optimizer = optim.Adam(net.parameters(), lr = 0.001)
```

大きく変わらないかな。

# 学習
## TensorFlow
```python
history = model.fit(X_train, y_train, batch_size=4, epochs = 10)
```

## Pytorch
```python
for epoch in range(10):
    print(f'Epoch {epoch + 1} start')
    start = time.time()
    running_loss = 0.0
    for idx, data in enumerate(trainloader, 0):
        # データセットのデータを [inputs, labels]の形で取得
        inputs, labels = data

        # パラメータの勾配をリセット
        optimizer.zero_grad()

        # 順伝搬＋逆伝搬＋パラメータ更新
        outputs = net(inputs)
        loss = loss_fn(outputs, labels)
        loss.backward()
        optimizer.step()

        # 統計情報の表示
        running_loss += loss.item()
        if idx % 2000 == 1999:  
            print('[%d, %5d] loss: %.3f' %
                  (epoch + 1, idx + 1, running_loss / 2000))
            running_loss = 0.0
    print(f'Time: {time.time() - start}')
print('Finished Training')
```

`TensorFlow`はfitメソッドが勝手にやってくれるので楽ですね。エポック数やデータの定義などをこっちが指定すれば勝手にやってくれる。

`Pytorch`は色々と定義する必要がある。
この辺りは`TensorFlow`の`tf.GradientTape`で勾配や損失を計算していく過程を書いていくのと似ているのかなと。

`TensorFlow`は楽だけど色々ブラックボックス化されているので、理論的な部分が曖昧でも動いちゃう。
`Pytorch`はデータ入力から誤差逆伝播の一連の流れを省略せずに書く必要があるので、理解が深まる。めんどくさいと言えばめんどくさいけど丁寧。

# （参考）学習時間
## TensorFlow
```python
Epoch 1/10
12500/12500 [==============================] - 69s 5ms/step - loss: 1.5127 - accuracy: 0.4510
Epoch 2/10
12500/12500 [==============================] - 64s 5ms/step - loss: 1.2761 - accuracy: 0.5453
Epoch 3/10
12500/12500 [==============================] - 67s 5ms/step - loss: 1.1790 - accuracy: 0.5819
Epoch 4/10
12500/12500 [==============================] - 65s 5ms/step - loss: 1.1090 - accuracy: 0.6098
Epoch 5/10
12500/12500 [==============================] - 66s 5ms/step - loss: 1.0573 - accuracy: 0.6280
Epoch 6/10
12500/12500 [==============================] - 69s 6ms/step - loss: 1.0182 - accuracy: 0.6410
Epoch 7/10
12500/12500 [==============================] - 68s 5ms/step - loss: 0.9842 - accuracy: 0.6560
Epoch 8/10
12500/12500 [==============================] - 68s 5ms/step - loss: 0.9560 - accuracy: 0.6657
Epoch 9/10
12500/12500 [==============================] - 67s 5ms/step - loss: 0.9325 - accuracy: 0.6746
Epoch 10/10
12500/12500 [==============================] - 74s 6ms/step - loss: 0.9113 - accuracy: 0.6834
```

## Pytorch
```python
Epoch 1 start
[1,  2000] loss: 1.164
[1,  4000] loss: 1.161
[1,  6000] loss: 1.162
[1,  8000] loss: 1.156
[1, 10000] loss: 1.201
[1, 12000] loss: 1.161
Time: 27.156450986862183
Epoch 2 start
[2,  2000] loss: 1.100
[2,  4000] loss: 1.105
[2,  6000] loss: 1.098
[2,  8000] loss: 1.135
[2, 10000] loss: 1.103
[2, 12000] loss: 1.122
Time: 27.26793599128723
Epoch 3 start
[3,  2000] loss: 1.038
[3,  4000] loss: 1.049
[3,  6000] loss: 1.049
[3,  8000] loss: 1.073
[3, 10000] loss: 1.064
[3, 12000] loss: 1.084
Time: 29.154211282730103
Epoch 4 start
[4,  2000] loss: 0.993
[4,  4000] loss: 1.018
[4,  6000] loss: 1.036
[4,  8000] loss: 1.027
[4, 10000] loss: 1.028
[4, 12000] loss: 1.020
Time: 27.163395643234253
Epoch 5 start
[5,  2000] loss: 0.945
[5,  4000] loss: 0.976
[5,  6000] loss: 0.975
[5,  8000] loss: 0.998
[5, 10000] loss: 1.011
[5, 12000] loss: 1.033
Time: 27.090753078460693
Epoch 6 start
[6,  2000] loss: 0.933
[6,  4000] loss: 0.958
[6,  6000] loss: 0.962
[6,  8000] loss: 0.956
[6, 10000] loss: 0.981
[6, 12000] loss: 0.988
Time: 27.317527055740356
Epoch 7 start
[7,  2000] loss: 0.906
[7,  4000] loss: 0.922
[7,  6000] loss: 0.936
[7,  8000] loss: 0.955
[7, 10000] loss: 0.968
[7, 12000] loss: 0.971
Time: 29.902886152267456
Epoch 8 start
[8,  2000] loss: 0.908
[8,  4000] loss: 0.917
[8,  6000] loss: 0.926
[8,  8000] loss: 0.927
[8, 10000] loss: 0.939
[8, 12000] loss: 0.952
Time: 27.228978872299194
Epoch 9 start
[9,  2000] loss: 0.875
[9,  4000] loss: 0.893
[9,  6000] loss: 0.912
[9,  8000] loss: 0.920
[9, 10000] loss: 0.930
[9, 12000] loss: 0.924
Time: 26.909887075424194
Epoch 10 start
[10,  2000] loss: 0.851
[10,  4000] loss: 0.908
[10,  6000] loss: 0.868
[10,  8000] loss: 0.892
[10, 10000] loss: 0.915
[10, 12000] loss: 0.902
Time: 26.92044472694397
Finished Training
```

GPUの関係もあるのかもですけど、`TensorFlow`が遅い...(多分自分のローカル環境がゴミだからですね）

# まとめ
印象としては細かく丁寧に書く`Pytorch`、簡単な`TensorFlow`という感じですかね。

個人的には楽なので`TensorFlow`が好きですが、その弊害として理論的な部分が曖昧になりやすいことも実感しているので、しっかりと理解した上で`TensorFlow`の簡潔さを享受するのがいいのかなという感じです。

`Pytorch`はニューラルネットワークの一連の流れを省略せずに書いていくので、学んだことを復習しながら書けるというのが強みかなと思いました。省略せずに書いていくというのをめんどくさいと感じるかは人それぞれだと思います。

とにかく画像分類してみたい、深層学習触れてみたいとかだったら`TensorFlow`の方が楽だと思いますし、理論的な部分も含めて丁寧に理解しながら実装したいとかだったら`Pytorch`の方がいいのかなと。

幸いどちらも公式チュートリアル的なものはあるので、一から学ぶことのハードルは低いかなと思います。

**結論、どっちもいいところがあるので好きなとこ選ぼう**ということで雑にまとめさせていただきます（）

何かの参考になれば幸いです。