私的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