私的AI研究会 > PyLearn
「機械学習]と「PyTorch」の学習過程で見つけた新山 祐介氏のサイト 『真面目なプログラマのためのディープラーニング入門』 の学習メモ。
『真面目なプログラマのためのディープラーニング入門』 第6回 GPU の仕組みと PyTorch 入門 より
PyTorch は「機械学習フレームワーク」と 呼ばれるソフトウェアの一種であり、効率のよいニューラルネットワークを 簡単に実装するために開発された。
PyTorch を使う利点は:
>>> torch.tensor([1,2,3,4]) # 4要素のPythonリストからTensorを作成 >>> torch.tensor([[1,2,3], [4,5,6]]) # 2×3要素のPythonリストからTensorを作成 >>> torch.zeros(4) # 4要素すべてゼロ >>> torch.zeros((2, 3)) # 2列3行すべてゼロ >>> torch.rand(4) # 4要素の乱数 (0〜1の範囲) >>> torch.rand((2, 3)) # 2列3行の乱数 (0〜1の範囲)
>>> 5 + torch.tensor([1,2,3]) # 左→右に分配 (broadcast) tensor([6, 7, 8]) >>> torch.tensor([1,2,3]) * 5 # 左←右に分配 (broadcast) tensor([ 5, 10, 15]) >>> 5 + torch.tensor([[1,2,3], [4,5,6]]) # 行と列に分配 tensor([[ 6, 7, 8], [ 9, 10, 11]]) >>> torch.tensor([1,2,3]) + torch.tensor([4,5,6]) # 要素ごと (element-wise) tensor([5, 7, 9]) >>> torch.tensor([[-1],[1]]) * torch.tensor([[1,2,3], [4,5,6]]) tensor([[-1, -2, -3], [ 4, 5, 6]])・Tensorの参照・変更
>>> x = torch.tensor([[1,2,3], [4,5,6]]) >>> x[0] # 0行目を取得 tensor([1, 2, 3]) >>> x[1][2] # 1行2列目の値を取得 6 >>> x[1][1:3] # 1行1〜2列目の値を取得 tensor([5, 6]) >>> x[1,2] # 上と同じ 6 >>> x[0,1] = 0 # 0行1列目の値を変更 >>> x tensor([[1, 0, 3], [4, 5, 6]])・配列の大きさ確認や形状変換なども同じ
>>> x = torch.tensor([[1,2,3], [4,5,6]]) >>> len(x) # リストとして見たときの要素数 (行数) 2 >>> x.shape # 配列の「形状」 (2, 3) >>> x.reshape(3,2) # 3行×2列の配列に変換 tensor([[1, 2], [3, 4], [5, 6]]) >>> x.reshape(6) # フラットな1次元配列に変換 tensor([1, 2, 3, 4, 5, 6])・Tensorと ndarray配列は 相互に変換することが可能
>>> np.array(torch.tensor([1,2,3])) # Tensorをndarrayに変換 array([1, 2, 3]) >>> torch.tensor(np.array([1,2,3])) # ndarrayをTensorに変換 tensor([1, 2, 3])
# (2×2×3) のテンソルを作成。 >>> x = torch.tensor([ [[1,2,3], [1,2,3]], [[4,5,6],[4,5,6]] ]) # (0,1,2)番目の次元を、それぞれ(1,0,2)番目に並び換える >>> x.permute(1,0,2) tensor([[[1, 2, 3], [4, 5, 6]], [[1, 2, 3], [4, 5, 6]]])
>>> x = torch.tensor(1.0, requires_grad=True) >>> y = x**3 + 2*x + 1 # y = x3 + 2x + 1 を計算 >>> y tensor(4., grad_fn=<AddBackward0>) >>> y.backward() # dy/dx を計算 >>> x.grad # dy/dx を表示 tensor(5.) >>> y = x**3 + 2*x + 1 # もう一度計算 >>> y.backward() >>> x.grad tensor(10.) # 値が増えている >>> x.grad = None # 勾配をクリアする
>>> x = torch.tensor(2.0, requires_grad=True) >>> y = math.sqrt(x) # math.sqrt() は Tensor を通常の値に変換してしまう >>> y 1.4142135623730951 >>> y = torch.sqrt(x) # torch.sqrt() は Tensor のままで計算する >>> y tensor(1.4142, grad_fn=<SqrtBackward>)
>>> a = np.array([1,2,3]) >>> a[1] # ndarrayの要素を取得 2 >>> x = torch.tensor([1,2,3]) >>> x[1] # Tensorの要素を取得 tensor(2) >>> x[1].item() # Tensorの要素を通常の数値に変換 (勾配は失われる) 2
>>> torch.cuda.is_available()
True # CUDAが利用可能
>>> x1 = torch.tensor([1,2,3]) # x1はCPU上に作成される
>>> x1
tensor([1, 2, 3])
>>> x2 = x1.to('cuda') # x1をGPUに転送し、x2とする
>>> x2
tensor([1, 2, 3], device='cuda:0')
>>> x3 = x2.to('cpu') # x2をCPUに転送し、x3とする
>>> x3
tensor([1, 4, 9])
>>> x2*x2 # GPU上で計算をおこなう tensor([1, 4, 9], device='cuda:0') >>> x1*x2 # CPU上とGPU上にあるデータは互いに計算できない RuntimeError: Expected all tensors to be on the same device, but found at least two devices, cuda:0 and cpu!・基本的には、PyTorch で GPU を使う際にはほとんど何もする必要がない
# ニューラルネットワークを定義する model = ... # ニューラルネットワークを訓練モードにする model.train() # ミニバッチごとの訓練データを用意する minibatches = [ ... ] # 最適化器と学習率を定義する optimizer = optim.SGD(model.parameters(), lr=0.01) # 各ミニバッチを処理する for (inputs, targets) in minibatches: # すべての勾配(.grad)をクリアしておく optimizer.zero_grad() # 与えられたミニバッチをニューラルネットワークに処理させる output = model(inputs) # 損失を計算する。 loss = F.mse_loss(output, targets) # 勾配を計算する loss.backward() # 重み・バイアスを更新する optimizer.step()・PyTorch のコードは大抵どれもこのパターンに従っている
import torch import torch.nn as nn import torch.nn.functional as F import torch.optim as optim # MNISTを処理するニューラルネットワーク class MNISTNet(nn.Module): # 各レイヤーの初期化。 def __init__(self): nn.Module.__init__(self) # 畳み込み: 入力1チャンネル、出力10チャンネル、カーネル3×3 self.conv1 = nn.Conv2d(1, 10, 3) # Max Pooling: 1/2に縮める。 self.pool1 = nn.MaxPool2d(2) # 畳み込み: 入力10チャンネル、出力20チャンネル、カーネル3×3 self.conv2 = nn.Conv2d(10, 20, 3) # Max Pooling: 1/2に縮める。 self.pool2 = nn.MaxPool2d(2) # 全接続 (fully connected): 入力500ノード、出力10ノード self.fc1 = nn.Linear(20*5*5, 10) return # 与えらえたミニバッチ x を処理する def forward(self, x): # x: (N × 1 × 28 × 28) x = self.conv1(x) x = F.relu(x) # x: (N × 10 × 26 × 26) x = self.pool1(x) # x: (N × 10 × 13 × 13) x = self.conv2(x) x = F.relu(x) # x: (N × 20 × 11 × 11) x = self.pool2(x) # x: (N × 20 × 5 × 5) x = x.reshape(len(x), 20*5*5) # x: (N × 500) x = self.fc1(x) # x: (N × 10) return x # 実際のインスタンスを作成。 model = MNISTNet()・PyTorch におけるニューラルネットワークは、すべて nn.Module の派生クラスとして定義する
nn.Linear(入力ノード数, 出力ノード数) … 全接続レイヤーを作成する nn.Conv2d(入力チャンネル数, 出力チャンネル数, カーネル幅) … 畳み込みレイヤーを作成する nn.MaxPool2d(カーネル幅) … Max poolingレイヤーを作成する。カーネル幅は縮小率を表す・nn.Linear および nn.Conv2d インスタンスはどちらも 内部に重み・バイアスを保持しており、これらはインスタンス作成時に ランダムに初期化されている
x = self.conv1(x) # 正しい x = self.conv1.forward(x) # 間違い・ F.relu() は ReLU 関数である
# ニューラルネットワークを定義する model = MNISTNet() # ニューラルネットワークを使用する(x: 入力テンソル) x = model(x)・PyTorch におけるニューラルネットワークは、入れ子になった nn.Moduleクラス (の派生クラス) と考えることができる
>>> print(model) MNISTNet( (conv1): Conv2d(1, 10, kernel_size=(3, 3), stride=(1, 1)) (pool1): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False) (conv2): Conv2d(10, 20, kernel_size=(3, 3), stride=(1, 1)) (pool2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False) (fc1): Linear(in_features=500, out_features=10, bias=True) )
# ミニバッチごとの訓練データを用意する。
train_images = splitarray3d(32, load_mnist('train-images-idx3-ubyte.gz'))
train_labels = splitarray1d(32, load_mnist('train-labels-idx1-ubyte.gz'))
# ニューラルネットワークを訓練モードにする。
model.train()
# 最適化器と学習率を定義する
optimizer = optim.SGD(model.parameters(), lr=0.01)
n = 0
# 各ミニバッチを処理する
for (images,labels) in zip(train_images, train_labels):
images = images.reshape(len(images), 1, 28, 28)
# 入力をfloat型のテンソルに変換
inputs = torch.tensor(images).float()
# 正解をlong型のテンソルに変換
targets = torch.tensor(labels).long()
# すべての勾配(.grad)をクリアしておく
optimizer.zero_grad()
# 与えられたミニバッチをニューラルネットワークに処理させる
output = model(inputs)
# 損失を計算する
loss = F.cross_entropy(output, labels)
# 勾配を計算する
loss.backward()
# 重み・バイアスを更新する
optimizer.step()
n += len(images)
print(n, loss.item())
・F.cross_entropy() という関数は 交差エントロピー誤差を計算するもので、実際にはloss = F.nll_loss(F.log_softmax(output, dim=1), labels)と等価である (F.log_softmax() の最後に dim=1 という 部分があるが、これは入力が (N×10) の2次元配列なので、2番目の次元に対して LogSoftmax 関数を適用せよという意味)
# splitarray1d: 与えられて1次元配列をn要素ごとに区切る def splitarray1d(n, a): for i in range(0, len(a), n): yield np.array(a[i:i+n]) return # splitarray3d: 与えられて3次元配列をn要素ごとに区切る def splitarray3d(n, a): for i in range(0, len(a), n): yield np.array(a[i:i+n,:,:]) return・訓練したニューラルネットワークを評価するには、以下のようにする。ここでもミニバッチごとに評価している以外は、以前のコードとほどんど変わっていない
# ミニバッチごとのテストデータを用意する。
test_images = splitarray3d(32, load_mnist('t10k-images-idx3-ubyte.gz'))
test_labels = splitarray1d(32, load_mnist('t10k-labels-idx1-ubyte.gz'))
# ニューラルネットワークを評価モードにする。
model.eval()
correct = 0
for (images,labels) in zip(test_images, test_labels):
images = images.reshape(len(images), 1, 28, 28)
# 入力をfloat型のテンソルに変換。
inputs = torch.tensor(images).float()
# 与えられたミニバッチをニューラルネットワークに処理させる。
outputs = model(inputs)
# 正解かどうかを判定する。
for (y,label) in zip(outputs, labels):
i = torch.argmax(y)
if i == label:
correct += 1
print(correct)
・「ニューラルネットワークを訓練モードに」model.train()「評価モードに」model.eval() という部分があるが、これは PyTorch における 一部のレイヤー (後で説明する BatchNorm など) の挙動が 訓練時と推論時で変わるためであるmodel = MNISTNet() # ニューラルネットワークを訓練する。 model.train() ... # 訓練した重み・バイアスをファイルに保存する。 torch.save(model.state_dict(), 'model.pt')
model = MNISTNet()
# 保存しておいた重み・バイアスを読み込む。
model.load_state_dict(torch.load('model.pt'))
# ニューラルネットワークを使用する。
model.eval()
...
・model.state_dict() メソッドは、MNISTNet クラス内部で定義されている 各レイヤー (nn.Conv2d、nn.Linear) の 重み・バイアスを再帰的に列挙し、ひとつの巨大な Python 辞書として返すものである PyTorch では、計算に使うテンソルが GPU 上にあれば GPU 上で計算が行われる
# model = MNISTNet()
model = MNISTNet().to('cuda')inputs = inputs.to('cuda')outputs = model(inputs)
出力結果 (outputs) を CPU に転送する
from torch.utils.data import Dataset, DataLoader
## MNISTDataset
## 指定されたファイルから入力と正解を読み込む
##
class MNISTDataset(Dataset):
def __init__(self, images_path, labels_path):
# データセットを初期化する
Dataset.__init__(self)
self.images = load_mnist(images_path)
self.labels = load_mnist(labels_path)
return
def __len__(self):
# データの個数を返す
return len(self.images)
def __getitem__(self, i):
# i番目の (入力, 正解) タプルを返す
return (self.images[i], self.labels[i])
# 実際のインスタンスを作成
dataset = MNISTDataset('t10k-images-idx3-ubyte.gz', 't10k-labels-idx1-ubyte.gz')
print(len(dataset)) # データの個数を返す
print(dataset[0]) # 0番目の (入力, 正解) タプルを返す# バッチサイズ32 でデータを利用する loader = DataLoader(dataset, batch_size=32) for (images, labels) in loader: # images: 32個の入力画像 # labels: 32個の正解ラベル ...
# 単純なSGD w1 -= alpha * dw1 w2 -= alpha * dw2 w3 -= alpha * dw3 ...
# 最適化器と学習率を定義する # optimizer = optim.SGD(model.parameters(), lr=0.01) optimizer = optim.Adam(model.parameters(), lr=0.01)
必要なモジュールのインポート import torch ... # Datasetの定義 class MNISTDataset(Dataset): ... # モデルの定義 class MNISTNet(nn.Module): ... # train: 1エポック分の訓練をおこなう関数 def train(model, device, loader, optimizer, ...): ... # test: テストをおこなう関数 def test(model, device, loader): ... # main: 最初に実行される関数 def main(): ... if __name__ == '__main__': main()
usage: mnist_torch.py [-h] [--verbose] [--batch-size N] [--test-batch-size N] [--no-shuffle] [--epochs N] [--lr LR] [--seed S] [--no-cuda] [--dry-run] [--log-interval N] [--save-model path] datadir
| オプション | 説明 | 初期値 |
| --verbose | 詳細なログを表示する | - |
| --batch-size n | 訓練時のバッチサイズを指定する | 32個 |
| --test-batch-size n | テスト時のバッチサイズを指定する | 1000個 |
| --no-shuffle | 訓練データをシャッフルしない | - |
| --epochs N | 訓練時のエポック数を指定する | 10回 |
| --lr rate | 学習率を指定する | 0.01 |
| --seed seed | 乱数のシードを指定する | 1 |
| --no-cuda | GPUがある場合でもCUDAを使用しない | - |
| --dry-run | デバッグ用に1バッチのみ実行する | - |
| --log-interval n | 進捗状況を表示する間隔 | 10バッチごと |
| --save-model path | モデルを保存・読み込むパス名 | なし |
python mnist_torch.py --lr=0.005 --epochs=100 --save-model=mnist_net.pt ./MNIST