はじめに
著者が大学院の研究でPyTorchを使ってモデル構築の必要があったので、勉強したことをまとめました!他の入門記事は私には難易度が高いと感じたので、この記事はものすご〜く基礎から書いてます笑
Pythonのフレームワークを学ぶにあたって クラス や インスタンス や メソッド といった概念を理解しているかどうかは習得速度に大きく影響します。一見遠回りに見えるかもしれないですが、これらの概念がまだ理解できていない方はぜひ Pythonの基礎から学習をしてみましょう!
おすすめの書籍はみんなのPython -柴田 淳(著)- です!Pythonの基礎を網羅的に学習したい人は読んでみてください!
目次
- 環境構築
- テンソルの操作
- データの処理
- モデルの構築
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 が環境構築不要なので、ご活用ください!
2. テンソルの操作
PyTorchではNumpyのようにテンソル(行列やベクトル)を処理するクラスが Tensorクラス として実装されています。Numpyを使ったことがある方には共通点が多いので理解がしやすいと思います。
モジュールのインポート
import torch
import torchvision
上記のコードを実行して問題なければ、(たぶん)無事に環境構築ができてます!もしエラーが発生してしまったら、再度仮想環境を構築してみましょう!
テンソルの生成と転送
テンソルの生成
まずは、tensorクラスからインスタンスを生成してみましょう。
#テンソルの生成
#tensor([リスト])とするとリストに合わせた形で生成される。
x1 = torch.tensor([i for i in range(10)])#リスト内包表記
print(x1)
#実行結果
tensor([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
生成したテンソルの形は shape変数 としてインスタンスに格納されています。多次元配列を生成して確認してみましょう。
x2 = torch.tensor([[1, 2],[3, 4]])#2×2の二次元テンソル
print(x2.shape)
#実行結果
torch.Size([2, 2])
テンソルの転送
生成したテンソルはデフォルトではCPUで処理を行うために、メインメモリに生成されます。機械学習タスクではGPUを用いて高速な計算を行うことも多いため、PyTorchでは GPUメモリへの転送 ができます。
その前にお使いのPCでCudaが利用可能か確認してみましょう。
torch.cuda.is_available()
#実行結果
False
はい(笑)。私のPCはM1 Macを使用しているためCudaを利用することができません( Trueと出た人はcudaを利用しましょう! )。MacOSを使っている方は以下のようにデバイスを取得してPC内のGPUを利用しましょう。
#mpsが利用できるかを確認
print(torch.backends.mps.is_available())
#デバイスの取得
device = torch.device("mps")
それでは作成してテンソルをGPUに転送してみましょう。転送には toメソッド を用います。
#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と同じです。
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と同じで ブロードキャスト により配列の形が異なるテンソルにおいても、自動的にテンソルの軸を拡張させることで、 演算が可能 になります。ただ、ブロードキャストできる条件はあるので無条件にテンソルの軸が拡張されるわけではありません。
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クラスとは?
- Datasetクラス
画像などの配列データそのものと、そのデータに対応するラベルを 一括で管理するクラス です。
イテレータとして利用することもでき 、データとラベルが一組になった配列を返します。
また、Datasetクラスをインスタンス化する時に引数にtransformを渡すことでデータの 前処理 を行ったデータが格納されてインスタンス化されます。
また、Datasetクラスには__len__や__getitem__が実装されているので、 lenで長さを取得したり、dataset[1]などしてデータとラベルを取得することができます!
- DataLoaderクラス
機械学習モデルを学習する際にデータをバッチサイズに固めてモデルへ渡し、学習を行うことが多くあります。DataLoaderクラスではDatasetクラスの データを指定したバッチサイズにまとめて 、モデルへ渡してくれる役割をしてくれます。
また、DataLoaderクラスも イテレータ として扱うことができます。
大まかな流れを理解するために、下記に擬似言語的な感じでデータの処理の流れを記します。 以下のコードは実行できない のでご注意ださい!
#データセットクラスのインスタンス化(データのロードと前処理を行う)
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の関数アノテーションという機能で、引数として想定されるデータ型の注釈を入れることができます。注釈を入れることで、コードの可読性が上がります。表記が気になった方は是非調べてみてください!
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
ここで標準化を行うにあたって、データセットから平均と標準偏差を計算する必要があります。これらを求めて戻り値として返す関数も同様に作成しましょう。
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オブジェクトを生成 していきましょう。
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)
このコードで行っていることを簡単にまとめます。
- CIFAR-10データセットの読み込みをしてDatasetオブジェクトを生成(前処理として平坦化したデータが格納)
- チャネル軸に対するデータセット全体の平均値を算出
- 2で求めた平均と標準偏差を用いて平坦化と標準化を行う関数を作成
- 3で作成した関数をtransformに指定して再度Datasetオブジェクトを生成
こうして取得したデータセットを用いてDatasetクラスの性質を見ていこうと思います!
#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クラスのインスタンス化を行います。
from torch.utils.data import DataLoader
#DataLoaderオブジェクトの生成
train_loader = DataLoader(train_dataset, batch_size=32,shuffle=True)
前述のように引数には先ほど生成したDatasetオブジェクトと、まとめたいバッチサイズを指定していきます。ここで shuffleをTrue としたので、順番にバッチ化されるのではなく、 無作為に抽出されたデータ がバッチサイズにまとまります。
このようにして生成したDataLoaderオブジェクトの性質を確認していきます。
#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つ1つのモデルに対してクラスを定義していきます。このクラスはPyTorchのModuleクラスを継承して定義を行います。
Moduleクラスを継承した簡単なモデルの実装
まずは簡単な多クラスロジスティック回帰を行うモデルの構築を行っていきます。前述のように、 PyTorchのModuleクラスを継承 して実装します。
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クラス です。
- Sequentialクラス
Sequentialモデルは 複数の処理を直列にまとめる ことができます。kerasなどにも同じ名前で直列モデルの構築を行えるクラスがありますね。
- ModuleListクラス
ModuleListクラスは モデルの構築を行う層をリストで渡すことで、層のイテレータを作って くれます。
Sequentialクラスによる多層パーセプトロンの構築
Sequentialモデルを用いて多層パーセプトロンの構築を行っていきます。これも先ほどと同様にModuleクラスを継承してクラスの定義を行っていきます。
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クラスによる多層パーセプトロンの構築
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クラスを継承したモデルの構築を紹介しました。
少しでも参考になったら嬉しいです!
次回はこれらによって構築したデータセットとモデルを用いて学習と評価を行う方法を紹介したいと思います!