【入門】PyTorch 〜環境構築から簡単なモデル構築まで〜

## はじめに
　著者が大学院の研究でPyTorchを使ってモデル構築の必要があったので、勉強したことをまとめました！他の入門記事は私には難易度が高いと感じたので、この記事はものすご〜く基礎から書いてます笑

　Pythonのフレームワークを学ぶにあたって __クラス__ や __インスタンス__ や __メソッド__ といった概念を理解しているかどうかは習得速度に大きく影響します。一見遠回りに見えるかもしれないですが、これらの概念がまだ理解できていない方はぜひ __Pythonの基礎から学習をしてみましょう！__ 

おすすめの書籍は[みんなのPython　-柴田 淳(著)-](https://www.amazon.co.jp/%E3%81%BF%E3%82%93%E3%81%AA%E3%81%AEPython-%E7%AC%AC4%E7%89%88-%E6%9F%B4%E7%94%B0-%E6%B7%B3/dp/479738946X/ref=asc_df_479738946X/?tag=jpgo-22&linkCode=df0&hvadid=295639570041&hvpos=&hvnetw=g&hvrand=7269731064899634267&hvpone=&hvptwo=&hvqmt=&hvdev=c&hvdvcmdl=&hvlocint=&hvlocphy=1009343&hvtargid=pla-526022375433&psc=1&th=1&psc=1) です！Pythonの基礎を網羅的に学習したい人は読んでみてください！

## 目次
<ol>
    <li>環境構築</li>
    <li>テンソルの操作</li>
    <li>データの処理</li>
    <li>モデルの構築</li>
</ol>

## 1. 環境構築
　私はこの辺の知識が疎く、失敗しても何度もやり直せるpythonの __venvで仮想環境を作成__ しています。condaやpipの共存がどうたらとかで難しいので、今回はpythonに標準搭載されているvenvを用いてpipでライブラリをインストールして仮想環境を構築してみましょう。
### venvによる仮想環境の構築
　まずは環境構築をしたいディレクトリ(フォルダ)を作成してターミナルで移動しましょう
```
$ cd [作成したディレクトリ]
$ python -m venv [仮想環境名]
```
実行すると移動したディレクトリに新しいディレクトリができたと思います。
次に作成した環境をアクティベートしましょう。使用しているOSごとに実行のコマンドが異なります。

#### MacOS
```
$ source [仮想環境名]/bin/activate
```

#### Windows
```
$ .\[仮想環境名]\Scripts\activate.bat
```

### ライブラリのインストール
アクティベートができたらライブラリのインストールを行います。
```
[仮想環境名]$ pip install numpy scipy pandas matplotlib
[仮想環境名]$ pip install scikit-learn
[仮想環境名]$ pip install torch torchvision torchaudio
```
　必要そうな科学計算ライブラリや機械学習ライブラリをまとめてインストールしてます。上記はあくまで一例なので、自分の好みでインストールしてください！

インストールしたライブラリをrequirement.txtに書き出しておきましょう。
```
$ pip freeze > requirement.txt
```

最後にディアクティベートしましょう！
```
[仮想環境名]　$ deactivate
```

それでは、このディレクトリをお好みのエディタで開いて __PyTorchの学習を開始しましょう！__
環境構築が難しい場合は [google colaboratory](https://colab.research.google.com/?hl=ja) が環境構築不要なので、ご活用ください！




## 2. テンソルの操作
　PyTorchではNumpyのようにテンソル(行列やベクトル)を処理するクラスが __Tensorクラス__ として実装されています。Numpyを使ったことがある方には共通点が多いので理解がしやすいと思います。

### モジュールのインポート
```python
import torch
import torchvision
```
上記のコードを実行して問題なければ、(たぶん)無事に環境構築ができてます！もしエラーが発生してしまったら、再度仮想環境を構築してみましょう！

### テンソルの生成と転送
#### テンソルの生成
まずは、tensorクラスからインスタンスを生成してみましょう。
```python
#テンソルの生成
#tensor([リスト])とするとリストに合わせた形で生成される。
x1 = torch.tensor([i for i in range(10)])#リスト内包表記
print(x1)
```
```
#実行結果
tensor([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
```

　生成したテンソルの形は __shape変数__ としてインスタンスに格納されています。多次元配列を生成して確認してみましょう。
```python
x2 = torch.tensor([[1, 2],[3, 4]])#2×2の二次元テンソル
print(x2.shape)
```
```
#実行結果
torch.Size([2, 2])
```

#### テンソルの転送
　生成したテンソルはデフォルトではCPUで処理を行うために、メインメモリに生成されます。機械学習タスクではGPUを用いて高速な計算を行うことも多いため、PyTorchでは __GPUメモリへの転送__ ができます。

その前にお使いのPCでCudaが利用可能か確認してみましょう。
```python
torch.cuda.is_available()
```
```
#実行結果
False
```

はい(笑)。私のPCはM1 Macを使用しているためCudaを利用することができません( Trueと出た人はcudaを利用しましょう！ )。MacOSを使っている方は以下のようにデバイスを取得してPC内のGPUを利用しましょう。


```python
#mpsが利用できるかを確認
print(torch.backends.mps.is_available())

#デバイスの取得
device = torch.device("mps")
```
それでは作成してテンソルをGPUに転送してみましょう。転送には __toメソッド__ を用います。
```python
#Cudaを利用できる人
x1 = torch.tensor([1, 2, 3])#生成(メインメモリ上)
x1 = x1.to("cuda")#GPUメモリへ転送

#できなかった人(Mac OS)
x1 = torch.tensor([1, 2, 3])#生成(メインメモリ上)
x1 = x1.to(device)#GPUメモリへ転送
```
これで生成したテンソルがtoメソッドによってGPUメモリへ転送されました！

### テンソルの演算
#### pythonの演算子を用いた演算
pythonの演算子を用いて演算を行った場合、テンソルの __各要素ごとに計算が行われます。__ Numpyと同じです。
```python
x1 = torch.tensor([1, 3, 5, 7])
x2 = torch.tensor([2, 4, 6, 8])

print(f"x1 + x2 = {x1 + x2}")#fストリング
print(f"x1 * x2 = {x1 * x2}")
```
```
#実行結果
x1 + x2 = tensor([ 3,  7, 11, 15])
x1 * x2 = tensor([ 2, 12, 30, 56])
```
実行結果を見ると要素ごとに計算されていることがわかります！

#### ブロードキャストを使った演算
　これまたNumpyと同じで __ブロードキャスト__ により配列の形が異なるテンソルにおいても、自動的にテンソルの軸を拡張させることで、 __演算が可能__ になります。ただ、ブロードキャストできる条件はあるので無条件にテンソルの軸が拡張されるわけではありません。
```python
x1 = torch.tensor([[0],
                   [1]])#(2,1)の二次元テンソル

x2 = torch.tensor([2, 3, 4])#(3,)の一次元テンソル
""" 
x1,x2はそれぞれ以下のように軸が拡張されている
x1:

[[0],  →  [[0, 0, 0],
 [1]]      [1, 1, 1]]

(2,1)  →   (2, 3)


x2:
[2, 3, 4]  →  [[2, 3, 4],
               [2, 3, 4]]

(3,)       →     (2, 3)
"""
print(f"x1 + x2 = \n{x1 + x2}")
```
```
#実行結果
x1 + x2 = 
tensor([[2, 3, 4],
        [3, 4, 5]])
```
上記のように、テンソルのサイズが異ってる場合でも、ブロードキャストによって自動的に同じ値で埋めながらテンソルの軸を拡張して演算が可能になっています。


## 3. データの処理
　機械学習タスクにおいて、データの前処理やバッチ処理は不可欠です。PyTorchではデータの前処理やバッチ処理やデータそのものを扱うために、 __Datasetクラス__ や __DataLoaderクラス__ が用意されています。
### DataSetクラスとDataLoaderクラスとは？

<ul>
<li><b>Datasetクラス</b></li>
</ul>

　画像などの配列データそのものと、そのデータに対応するラベルを __一括で管理するクラス__ です。
__イテレータとして利用することもでき__ 、データとラベルが一組になった配列を返します。
また、Datasetクラスをインスタンス化する時に引数にtransformを渡すことでデータの __前処理__ を行ったデータが格納されてインスタンス化されます。
また、Datasetクラスには__len__や__getitem__が実装されているので、 __lenで長さを取得したり、dataset[1]などしてデータとラベルを取得することができます！__

<ul>
<li><b>DataLoaderクラス</b></li>
</ul>

　機械学習モデルを学習する際にデータをバッチサイズに固めてモデルへ渡し、学習を行うことが多くあります。DataLoaderクラスではDatasetクラスの __データを指定したバッチサイズにまとめて__ 、モデルへ渡してくれる役割をしてくれます。
また、DataLoaderクラスも __イテレータ__ として扱うことができます。


　大まかな流れを理解するために、下記に擬似言語的な感じでデータの処理の流れを記します。 __以下のコードは実行できない__ のでご注意ださい！
```python
#データセットクラスのインスタンス化(データのロードと前処理を行う)
dataset = Dataset(...,transform=(前処理を行う関数))

#DatasetオブジェクトをDataLoaderクラスのコンストラクタの引数へ渡し、インスタンス化(バッチサイズに固める)
dataloader = DataLoader(dataset, batch_size=(バッチサイズを指定),...)

for _ in range(エポック数(どのくらい学習したいか)):
    for data_batched, label_batched in dataloader:
        """1イテレーションごとの処理"""
```
このような流れでデータの処理、学習を進めていきます。簡単にまとめると

1.Datasetをインスタンス化する時にtransformを指定することで任意の前処理を行う
2.DataLoaderクラスをインスタンス化するときにDatasetオブジェクトを渡し、バッチサイズでまとめる
3.DataLoaderによってまとまったバッチごとのデータでモデルの学習をする

となります。擬似的に書いてもピンとこないと思いますで、実際にデータを用いてこれらのクラスをインスタンス化してみましょう！

### CIFAR-10データセットを用いたデータの処理
CIFAR-10データセットとは飛行機やカエルなどの十種類クラスがある画像データセットです。このデータセットをtorchvisionからロードしてインスタンス化していきます。

#### 前処理関数transformの作成
　前述のようにデータセットをロードするときに、transformを指定することで前処理を行います。そこで、まずは前処理を行うtransform関数を作成していきます。具体的には、データセットに格納されている __(32,32,3)のデータを(3072,)に平坦化させる__ と同時に、データセット全体のピクセルごとの平均値と標準偏差を与えることで __データの標準化(平均0,標準偏差1にスケールング)を行う__ 前処理関数を作成します。

　完全に余談ですが以下のように関数を定義するときにdef func(引数: "データ形")という表記はPythonの関数アノテーションという機能で、引数として想定されるデータ型の注釈を入れることができます。注釈を入れることで、コードの可読性が上がります。表記が気になった方は是非調べてみてください！

```python
from PIL import Image
import numpy as np

#前処理を行う関数
def transform(img: Image.Image, mean: np.ndarray=None, std: np.ndarray=None):
    #PILライブラリのImageクラスからNumpyへ変換を行う
    img = np.asarray(img, dtype="float32")

    #平坦化
    x = img.flatten()

    #各次元をデータセット全体のピクセルごと(チャネル軸で計算した)平均と標準偏差でスケーリング
    if mean is not None and std is not None:
        x = (x - mean) / std
    return x
```

　ここで標準化を行うにあたって、データセットから平均と標準偏差を計算する必要があります。これらを求めて戻り値として返す関数も同様に作成しましょう。
```python
def calc_dataset_mean_and_std(dataset:"Datasetオブジェクト") -> "平均値, 標準偏差":
    data = []#Datasetから画像データの配列のみを格納する

    #Datasetオブジェクトは__len__と__getitem__が実装されている
    for i in range(len(dataset)):
        img_flat = dataset[i][0]#dataset[i]の戻り値は(画像配列, 対応するラベル)であるため
        data.append(img_flat)

    data = np.stack(data)#データを第0軸で連結

    #チャネル軸(第0軸)で計算してピクセルごとの平均と標準偏差を取得する
    mean = np.mean(data, axis=0)
    std = np.std(data, axis=0)

    return mean, std
```


#### CIFAR-10データセットの読み込み
　これで前処理を行う関数ができたので、CIFAR-10データセットを読み込んで __Datasetオブジェクトを生成__ していきましょう。
```python
import torchvision

#データセットの読み込み(この時点ではmeanとstdを取得できていないので、平坦化だけしたデータを取得)
dataset = torchvision.datasets.CIFAR10(
    root="データを保存するディレクトリ名", train=True, download=True,
    transform=transform)

#取得したデータセットから平均と標準偏差を算出
mean, std = calc_dataset_mean_and_std(dataset)

#平坦化と標準化を行う前処理関数を無名関数として変数に格納
img_transform = lambda x: transform(x, mean, std)

#Datsetオブジェクトを取得
train_dataset = torchvision.datasets.CIFAR10(
    root="データを保存するディレクトリ名", train=True, download=True, transform=img_transform)
```
このコードで行っていることを簡単にまとめます。

1. CIFAR-10データセットの読み込みをしてDatasetオブジェクトを生成(前処理として平坦化したデータが格納)
2. チャネル軸に対するデータセット全体の平均値を算出
3. 2で求めた平均と標準偏差を用いて平坦化と標準化を行う関数を作成
4. 3で作成した関数をtransformに指定して再度Datasetオブジェクトを生成

こうして取得したデータセットを用いてDatasetクラスの性質を見ていこうと思います！
```python
#Datasetクラスは__getitem__が実装されており、データとラベルを返す。
img, label = train_dataset[0]

#Datasetクラスは__len__が実装されており、サンプルの数を返す。
sample_nums = len(train_dataset)

print(f"画像: {img}\n")
print(f"ラベル: {label}\n")
print(f"サンプル数: {sample_nums}\n")
```
```
#実行結果
画像: [-0.97683984 -1.015768   -0.8645673  ... -0.05604946 -0.54032654
 -0.64139867]

ラベル: 6

サンプル数: 50000
```
以上から、3章の初めの方で説明したような、 __Datasetクラスの__len__や__getitem__の性質__ が確認できました！

#### CIFAR-10データセットのバッチ化
続いてDataLoaderクラスのインスタンス化を行います。
```python
from torch.utils.data import DataLoader

#DataLoaderオブジェクトの生成
train_loader = DataLoader(train_dataset, batch_size=32,shuffle=True)
```
　前述のように引数には先ほど生成したDatasetオブジェクトと、まとめたいバッチサイズを指定していきます。ここで __shuffleをTrue__ としたので、順番にバッチ化されるのではなく、 __無作為に抽出されたデータ__ がバッチサイズにまとまります。

このようにして生成したDataLoaderオブジェクトの性質を確認していきます。
```python
#DataLoaderオブジェクトはイテレータとして使える
for batched_images, batched_labels in train_loader:
    print(f"バッチサイズにまとまった画像:\n{batched_images}")
    print(f"shape of batched_images: {batched_images.shape}\n")

    print(f"バッチサイズにまとまったラベル:\n{batched_labels}")
    print(f"Shape of batched_labels: {batched_labels.shape}\n")
    
    print(f"バッチ化されたサイズ: {len(batched_images)}")
    break
```
```
#実行結果
バッチサイズにまとまった画像:
tensor([[ 1.2163,  1.2748,  1.1616,  ...,  1.9000,  1.9817,  2.0373],
        [-0.5818, -0.5357, -0.7651,  ...,  0.1750,  0.2578, -0.2479],
        [-0.5546, -0.3300, -0.1188,  ..., -1.1034, -1.1309, -0.8684],
        ...,
        [ 0.5216,  0.4656,  0.5525,  ...,  0.3444, -0.1892, -0.1420],
        [-0.4047, -0.4671, -0.2803,  ...,  1.9770,  2.0615,  2.1281],
        [ 0.5897,  0.5204,  0.5152,  ..., -0.1177, -0.1093,  0.0699]])
shape of batched_images: torch.Size([32, 3072])

バッチサイズにまとまったラベル:
tensor([8, 6, 5, 0, 2, 1, 3, 4, 8, 3, 1, 8, 6, 3, 0, 8, 6, 9, 5, 6, 5, 3, 6, 6,
        0, 9, 4, 8, 1, 7, 4, 0])
Shape of batched_labels: torch.Size([32])

バッチ化されたサイズ: 32
```
　実行結果から、(3072,)の画像データが32枚(指定したバッチサイズ)にまとまって、(32, 3072)となっていることが確認できました！ラベルも同様に(1,)が32(指定したバッチサイズ)にまとまって、(32,)となっていること確認できます！

__上記のようにDatasetオブジェクトはデータトラベルを一括で管理して、DataLoaderオブジェクトはDatasetオブジェクトを指定したバッチにまとめます！__

## 4. モデルの構築
　PyTorchでは１つ1つのモデルに対してクラスを定義していきます。このクラスはPyTorchのModuleクラスを継承して定義を行います。
### Moduleクラスを継承した簡単なモデルの実装
　まずは簡単な多クラスロジスティック回帰を行うモデルの構築を行っていきます。前述のように、 __PyTorchのModuleクラスを継承__ して実装します。
```python
from torch import nn

class MultiClassLogisticRegression(nn.Module):
    """
    dim_input: 入力したデータの次元
    num_classes: 分類対象の物体クラス数(幾つのクラスに分類するか)
    """
    def __init__(self, dim__input: int, num_classes: int):
        #super()を用いて継承元のコンストラクタを呼び出す
        super().__init__()

        #線形結合を行う(wx + bとしてw,bがパラメータとして保持される)
        self.linear = nn.Linear(dim_input, num_classes)

    """
    順伝播関数
    Moduleクラスのインスタンスは__call__が定義されており、呼び出すとforwardメソッドが呼び出され、順伝播が行われる。
    x: 入力データ
    """
    def forward(self, x: torch.Tensor):
        #コンストラクタで定義した線形結合メソッドを用いて、入力データをロジットとする
        l = self.linear(x)
        #Tensorクラスが持つsoftmax関数を利用して確率を出力する
        y = l.softmax(dim=1)

        return y
```
いきなりコードを出されても何が何やら...となると思うので、一つずつ解説していきます。

　まず、継承元のModuleクラスのコンストラクタをsuper()を用いて呼び出しています。(これは必ず行ってください。)

　次に定義しているクラスのコンストラクタの中でself.linearという変数(属性)を定義して、Linearクラスのインスタンスを格納しています。このLinearクラスはModuleオブジェクトを継承したクラスであり、線形関数(wx + bとして入力データを線形結合する)を実装することができます。 __このクラスはコンストラクタでパラメータ w(重み),b(バイアス)を保持しており、__ 学習をしていく上で更新されていきます。

　次にもう一つ必要なのが、順伝播を行う __forwardメソッド__ です。forwardメソッドは継承元のModuleオブジェクトのクラスに実装されている __ __call__メソッドから呼び出され、順伝播が行われます。__ 今回はforwardメソッドで、入力データの線型結合を行い、これをロジットとして、Tensorクラスの持つsoftmaxメソッドにより分類される確率が出力されます。


### SequentialクラスとModuleListクラス
PyTorchには上記のようModuleクラスのインスタンスを一つのインスタンスにまとめるためのクラスが二つ用意されています。タイトルにもある __Sequentialクラス__ と __ModuleListクラス__ です。

<ul>
<li><b>Sequentialクラス</b></li>
</ul>

　Sequentialモデルは __複数の処理を直列にまとめる__ ことができます。kerasなどにも同じ名前で直列モデルの構築を行えるクラスがありますね。

<ul>
<li><b>ModuleListクラス</b></li>
</ul>

　ModuleListクラスは __モデルの構築を行う層をリストで渡すことで、層のイテレータを作って__ くれます。

#### Sequentialクラスによる多層パーセプトロンの構築
　Sequentialモデルを用いて多層パーセプトロンの構築を行っていきます。これも先ほどと同様にModuleクラスを継承してクラスの定義を行っていきます。
```python
class MLPSequential(nn.Module):
    '''
    多層パーセプトロンのSequentialクラスによる実装。
    dim_input  : 入力次元
    num_classes: 分類対象の物体クラス数
    '''
    def __init__(self, dim_input: int, num_classes: int):
        super().__init__()#継承元のコンストラクタを呼び出す
        
        #Sequentialクラスに複数のModuleクラスを格納している
        self.layers = nn.Sequential(
            nn.Linear(dim_input, 256),#入力次元→256次元に出力。線形結合
            nn.ReLU(inplace=True),#活性化関数としてReluを使用。inplace=Trueでメモリを節約できる
            nn.Linear(256, 256),
            nn.ReLU(inplace=True),
            nn.Linear(256, 256),
            nn.ReLU(inplace=True),
            nn.Linear(256, num_classes)
        )

    '''
    順伝播関数
    x: 入力データ, [バッチサイズ, 入力次元]
    '''
    def forward(self, x):
        #入力を先ほどのSequentialクラスを呼び出し、forward処理を行いロジットを計算
        l = self.layers(x)
        #Tensorクラスが持つsoftmax関数を利用して確率を出力する
        y = l.softmax(dim=1)

        return y
```
　MLPSequentialクラスのコンストラクタの中でSequentialクラスを使用して、 __複数のModuleクラス(LinearクラスやReLUクラスなど)をまとめています。__ このSequentialモデルをlayers属性に格納して、forwardメソッドの中でクラスのインスタンスに __入力xを渡してSequentialモデルの中にある全てのModuleクラス全てがforward処理した結果を返します。__ これをロジットとしてsoftmaxメソッドで確率を計算しています。


#### ModuleListクラスによる多層パーセプトロンの構築
```python
import torch.nn.functional as F

class MLPModuleList(nn.Module):
    '''
    多層パーセプトロンのModuleListクラスによる実装。
    dim_input  : 入力次元
    num_classes: 分類対象の物体クラス数
    '''
    def __init__(self, dim_input: int, num_classes: int):
        super().__init__()#継承元のコンストラクタを呼び出す

        #ModuleListクラスに複数のModuleクラスをリストとして格納している
        layers = [nn.Linear(dim_input, 256)]
        layers += [nn.Linear(256, 256) for _ in range(2)]
        layers.append(nn.Linear(256, num_classes))
        self.layers = nn.ModuleList(layers)

    '''
    順伝播関数
    x: 入力データ, [バッチサイズ, 入力次元]
    '''
    def forward(self, x):
        
        for layer in self.layers[:-1]:
            x = F.relu(layer(x))
        l = self.layers[-1](x)
        y = l.softmax(dim=1)
        
        return y
```
　ModuleListクラスのコンストラクタには必要な処理(Moduleクラス)をリストに格納したものを渡します。forwardメソッドでは、for文を用いてリストから一つずつ取り出していき(最後の要素以外)、入力値xを渡して活性化関数としてReluを用いて非線形化しています。

　__Moduleクラスを継承したモデルの構築、必要な処理(Moduleクラス)をまとめて扱うためのSequentialクラス, ModuleListクラスを簡単に紹介しました。__

## まとめ
　今回はPyTorchを利用するための環境構築の方法、torch.Tensorクラスの生成や転送や演算、torch.utils.dataの中に実装されているDatasetクラス, DataLoaderクラスの生成方法と性質、Moduleクラスを継承したモデルの構築を紹介しました。

少しでも参考になったら嬉しいです！

__次回はこれらによって構築したデータセットとモデルを用いて学習と評価を行う方法を紹介したいと思います！__


## 参考文献
1. [Pythonで学ぶ画像認識](https://www.amazon.co.jp/Python%E3%81%A7%E5%AD%A6%E3%81%B6%E7%94%BB%E5%83%8F%E8%AA%8D%E8%AD%98-%E6%A9%9F%E6%A2%B0%E5%AD%A6%E7%BF%92%E5%AE%9F%E8%B7%B5%E3%82%B7%E3%83%AA%E3%83%BC%E3%82%BA-%E7%94%B0%E6%9D%91%E9%9B%85%E4%BA%BA/dp/4295015997)

2. [PyTorch transforms/Dataset/DataLoaderの基本動作を確認する](https://qiita.com/takurooo/items/e4c91c5d78059f92e76d)

3. [Pytorchのニューラルネットの書き方](https://www.hellocybernetics.tech/entry/2017/10/20/025702)