PyTorch によるニューラルネットワーク
以下の公式リファレンスを参考にした.
Neural Networks -- PyTorch Tutorials 1.4.0 documentation
ニューラルネットワークの訓練の一般的な手順は以下の通り.
1. データ(訓練データ・テストデータ)を準備する.
2. 訓練可能なパラメータを持つニューラルネットワークを定義する.(Define the network)
3. ネットワークに訓練データを入力したときの損失関数を計算する.(Loss function)
4. ネットワークのパラメータに関する損失関数の勾配を計算する.(backward)
5. 損失関数の勾配に基づいてパラメータを更新する.(optimize)
6. 3 ~ 6 を何度も繰り返すことで訓練を行う.
手順通りにニューラルネットワークを構築していく.
1. データの準備
ニューラルネットワークの訓練に使うデータは,既にパッケージで用意されているものを使用するか,自分で準備したデータを使う.
既に用意されているものを使用する場合はtorchvision
パッケージを利用すると便利である.
機械学習でよく使用される MNIST や CIFAR10 などのデータセットtorchvision.datasets
が準備されているほか,汎用的な機械学習モデルtorchvision.models
やデータ処理を行うモジュールtorchvision.transforms
が用意されている.
詳しくは公式ドキュメントを参照 --> torchvision
訓練を実行するときにはtorch.utils.data.DataLoader
というデータの箱を用意する.DataLoader
は,入力データとそのラベルを合わせたデータセットを,バッチサイズだけひとまとめにしたものである.
準備の手順は以下の通り.
(1) データの前処理を行うtransforms
を用意する.
(2) transforms
を引数として Dataset クラスをインスタンス化してDataset
を用意する.
(3) Dataset
を引数として DataLoader クラスをインスタンス化してDataLoader
を用意する.
(4) 訓練時にはDataLoader
を用いて,訓練データとラベルをバッチサイズのかたまりで取得する.
2. ニューラルネットワークの定義
ニューラルネットワークはtorch.nn
パッケージを用いて構築できる.
nn
は自動微分autograd
を利用することでモデルの定義や微分を実行する.
nn.Module
はニューラルネットワークの各種レイヤーやforward(input)
メソッドを持っている.
そのため,新しいネットワークを構築する際にはnn.Module
クラスを継承すればよい.
import torch
import torch.nn as nn
import torch.nn.functional as F
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
# 1 input image channel, 6 output channels, 3x3 square convolution
# kernel
self.conv1 = nn.Conv2d(1, 6, 3)
self.conv2 = nn.Conv2d(6, 16, 3)
# an affine operation: y = Wx + b
self.fc1 = nn.Linear(16 * 6 * 6, 120) # 6*6 from image dimension
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, 10)
def forward(self, x):
# Max pooling over a (2, 2) window
x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2))
# If the size is a square you can only specify a single number
x = F.max_pool2d(F.relu(self.conv2(x)), 2)
x = x.view(-1, self.num_flat_features(x))
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x
def num_flat_features(self, x):
size = x.size()[1:] # all dimensions except the batch dimension
num_features = 1
for s in size:
num_features *= s
return num_features
net = Net()
print(net)
# ---Output---
#Net(
# (conv1): Conv2d(1, 6, kernel_size=(3, 3), stride=(1, 1))
# (conv2): Conv2d(6, 16, kernel_size=(3, 3), stride=(1, 1))
# (fc1): Linear(in_features=576, 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)
#)
__init__()
メソッドにて,ネットワークが保持するレイヤーを定義する.
Linear
やConv2d
などのよく使用するほとんどのレイヤーがtorch.nn
で定義されている.
詳しくは公式ドキュメントを参照 --> torch.nn
同様に,relu
やmax_pool2d
などの処理はtorch.nn.functional
で定義されている.
処理が必要な場面で適宜呼び出して使用すればよい.
詳しくは公式ドキュメントを参照 --> torch.nn.functional
forward()
メソッドにて,ネットワークの順伝播を定義する.
入力x
が出力されるまでに通るレイヤーや実行される処理を順番に定義していく.
なお,ネットワークの逆伝播であるbackward()
は定義する必要はない.
forward()
を定義しautograd
を使用することで.自動的に逆伝播が求まるようになっている.
訓練可能なパラメータはnet.parameters()
で取得できる.
重みパラメータとバイアスパラメータで別々に取得されるため,定義したレイヤーの数 $\times$ 2 の長さのパラメータのリストが得られる.
params = list(net.parameters())
print(len(params))
print(params[0].size()) # conv1's weight
print(params[1].size()) # conv1's bias
print(params[0][0,:,:,:]) # conv1's weights on the first dimension
# ---Output---
#10
#torch.Size([6, 1, 3, 3])
#torch.Size([6])
#tensor([[[-0.0146, -0.0219, 0.0491],
# [-0.3047, -0.0137, 0.0954],
# [-0.2612, -0.2972, -0.2798]]], grad_fn=<SliceBackward>)
このネットワークに対して,$32 \times 32$ の適当なデータを入力してみる.
input = torch.randn(1, 1, 32, 32)
out = net(input)
print(out)
# ---Output---
#tensor([[-0.0703, 0.0575, -0.0679, -0.1168, -0.1093, 0.0815, -0.0085, 0.0408,
# 0.1275, 0.0472]], grad_fn=<AddmmBackward>)
入力した乱数が初期パラメータを持つレイヤーを通って出力されている.
zero_grad()
メソッドで全てのパラメータの勾配をゼロにできる.予期せぬパラメータ更新を避けるためにbackward()
を実行する前にzero_grad()
を実行することが推奨される.
torch.nn
はミニバッチが入力されることを前提としている.例えば,nn.Conv2d
は入力として4次元の Tensor ($\rm{nSamples} \times nChannels \times Height \times Width$) を用意する必要がある.
3. 損失関数
MSELoss()
やCrossEntropyLoss()
などのよく使用する損失関数はnn
パッケージに用意されている.
以下では,乱数を入力した際の出力値と,同じサイズの乱数列を用いてMSELoss
を計算している.
input = torch.randn(1, 1, 32, 32)
output = net(input)
target = torch.randn(10) # a dummy target, for example
target = target.view(1,-1) # make it the same shape as output
criterion = nn.MSELoss()
loss = criterion(output, target)
print(loss)
# ---Output---
#tensor(0.5322, grad_fn=<MseLossBackward>)
ここまでの順伝播を追ってみると,
input -> conv2d -> relu -> maxpool2d -> conv2d -> relu -> maxpool2d
-> view -> linear -> relu -> linear -> relu -> linear
-> MSELoss
-> loss
となっており,grad_fn
属性を見ることで確認できる.
print(loss.grad_fn) # MSELoss
print(loss.grad_fn.next_functions[0][0]) # Linear
print(loss.grad_fn.next_functions[0][0].next_functions[0][0]) # ReLU
# ---Output---
#<MseLossBackward object at 0x7f5008a1c4e0>
#<AddmmBackward object at 0x7f5008a1c5c0>
#<AccumulateGrad object at 0x7f5008a1c4e0>
4. 勾配計算
パラメータ更新のために誤差逆伝播を行うには,損失関数の勾配が必要である.PyTorch では,損失関数loss
に対してloss.backward()
を実行すれば勾配を自動的に計算してくれる.
勾配が蓄積されてしまうのを避けるために,訓練時には iteration ごとにnet.zero_grad()
を実行し,勾配を消去することが推奨される.
net.zero_grad() # zeroes the gradient buffers of all parameters
print("conv1.bias.grad before backward")
print(net.conv1.bias.grad)
loss.backward()
print("conv1.bias.grad after backward")
print(net.conv1.bias.grad)
# ---Output---
#conv1.bias.grad before backward
#tensor([0., 0., 0., 0., 0., 0.])
#conv1.bias.grad after backward
#tensor([ 0.0072, -0.0051, -0.0008, -0.0017, 0.0043, -0.0030])
5. パラメータの更新
パラメータ更新(最適化)はtorch.optim
から引用すればよい.
ここでは,以下の式で定義される確率的勾配降下法(SGD)を使用してみる.
詳しくは公式ドキュメントを参照 --> torch.optim
weight -> weight - learning_rate * gradient
import torch.optim as optim
# create your optimizer
optimizer = optim.SGD(net.parameters(), lr=0.01)
# in your training loop:
optimizer.zero_grad() # zero the gradient buffers
output = net(input)
loss = criterion(output,target)
loss.backward()
optimizer.step() # do the update
6. 訓練
上記の 3 ~ 6 の手順を繰り返すことでネットワークの訓練を実行する.
CIFAR10を用いた実装
例として,CIFAR10 を用いた画像分類を行うニューラルネットワークを訓練する.
以下の公式リファレンスを参照した.
Training a Classifier -- PyTorch Tutorials 1.4.0 documentation
データの準備
torchvision.datasets
に用意されている CIFAR10 のデータを取得し標準化する.
torchvision dataset のデータは [0,1] の範囲の値を持つ PILImage であるため,ここでは [-1,1] の範囲の値を持つ Tensor に標準化している.
import torchvision
import torchvision.transforms as transforms
transform = transforms.Compose(
[transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
trainset = torchvision.datasets.CIFAR10(root='./data', train=True,
download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=4,
shuffle=True, num_workers=2)
testset = torchvision.datasets.CIFAR10(root='./data', train=False,
download=True, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=4,
shuffle=False, num_workers=2)
classes = ('plane', 'car', 'bird', 'cat',
'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
準備したデータを表示してみる.
import matplotlib.pyplot as plt
import numpy as np
def imshow(img):
img = img/2 + 0.5 # unnormalize
npimg = img.numpy()
plt.imshow(np.transpose(npimg, (1,2,0)))
plt.show()
# get some random training imges
dataiter = iter(trainloader)
images, labels = dataiter.next()
# show images
imshow(torchvision.utils.make_grid(images))
# print labels
print(''.join('%5s' % classes[labels[j]] for j in range(4)))
ネットワークの構築
続いて,画像の分類を行うネットワークを構築する.
import torch.nn as nn
import torch.nn.functional as F
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()
損失関数と最適化手法の定義
ネットワークが構築できたら,損失関数と最適化手法を定義する.
import torch.optim as optim
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)
訓練
ネットワーク・損失関数・最適化手法が定義できたら,訓練データを使って訓練を開始する.
for epoch in range(2): # loop over the dataset multiple times
running_loss = 0.0
for i, data in enumerate(trainloader, 0):
# get the inputs; data is a list of [inputs, labels]
inputs, labels = data
# zero the parameter gradients
optimizer.zero_grad()
# forward + backward + optimize
outputs = net(inputs)
loss = criterion(outputs,labels)
loss.backward()
optimizer.step()
# print statistics
running_loss += loss.item()
if i%2000==1999: # print every 2000 mini-batches
print('[%d, %5d] loss: %.3f' % (epoch+1, i+1, running_loss/2000))
running_loss = 0.0
print('Finished Training')
# ---Output---
#[1, 2000] loss: 2.149
#[1, 4000] loss: 1.832
#[1, 6000] loss: 1.651
#[1, 8000] loss: 1.573
#[1, 10000] loss: 1.514
#[1, 12000] loss: 1.458
#[2, 2000] loss: 1.420
#[2, 4000] loss: 1.371
#[2, 6000] loss: 1.348
#[2, 8000] loss: 1.333
#[2, 10000] loss: 1.326
#[2, 12000] loss: 1.293
#Finished Training
ここでは,12000個の訓練データすべてを使った訓練を2回行っている.訓練に使用するデータが増えるほど,損失関数 loss が小さくなっているため,学習が進行している様子が観察できる.(学習はまだ完了していないと思われるが,今回はここで中断し先に進む.)
モデルパラメータの保存
訓練済みのモデルのパラメータはtorch.save()
で保存できる.
PATH = './cifar_net.pth'
torch.save(net.state_dict(), PATH)
テストデータに適用
テストデータに対して,訓練済みのネットワークを適用してみる.
まずは,テストデータの中身を確認する.
dataiter = iter(testloader)
images, labels = dataiter.next()
imshow(torchvision.utils.make_grid(images))
print('GroundTruth: ', ' '.join('%5s' % classes[labels[j]] for j in range(4)))
続いて,保存したネットワークのパラメータを読み込む.
その後,読み込んだモデルにテストデータを入力し,分類結果を表示してみる.
net = Net()
net.load_state_dict(torch.load(PATH))
# ---Output---
# <All keys matched successfully>
outputs = net(images)
_, predicted = torch.max(outputs, 1)
print('Predicted: ', ' '.join('%5s' % classes[predicted[j]] for j in range(4)))
# ---Output---
# Predicted: cat ship plane plane
3枚目の画像を ship ではなく plane と誤判定しているが,それ以外の3つは正しく分類できていることがわかる.
10000枚のテストデータすべてに対する正解率を計算してみる.
correct = 0
total = 0
with torch.no_grad():
for data in testloader:
images, labels = data
outputs = net(images)
_, predicted = torch.max(outputs, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
print('Accuracy of the network on the 10000 test images: %d %%' % (100*correct/total))
# ---Output---
# Accuracy of the network on the 10000 test images: 52 %
正解率は 52% となり,画像分類器としてはあまり精度の良いものではないといえる.
続いて,分類の種類ごとに正解率を取得してみる.
class_correct = list(0. for i in range(10))
class_total = list(0. for i in range(10))
with torch.no_grad():
for data in testloader:
images, labels = data
outputs = net(images)
_, predicted = torch.max(outputs,1)
c = (predicted == labels).squeeze()
for i in range(4):
label = labels[i]
class_correct[label] += c[i].item()
class_total[label] += 1
for i in range(10):
print('Accuracy of %5s : %2d %%' % ( classes[i], 100*class_correct[i]/class_total[i]))
# ---Output---
# Accuracy of plane : 61 %
# Accuracy of car : 61 %
# Accuracy of bird : 52 %
# Accuracy of cat : 26 %
# Accuracy of deer : 34 %
# Accuracy of dog : 51 %
# Accuracy of frog : 67 %
# Accuracy of horse : 43 %
# Accuracy of ship : 76 %
# Accuracy of truck : 50 %
ここから,cat の分類は苦手だが ship の分類は得意であることがわかる.
GPUを用いる場合
GPU上で訓練を行う場合には,device
で CUDA device を指定する必要がある.
まず,GPU が利用可能かどうか調べる.以下のコードでcuda:0
と表示されれば GPU が利用可能である.
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
# Assuming that we are on a CUDA machine, this should print a CUDA device:
print(device)
# ---Output---
# cuda:0
.to(device)
でネットワークやデータを GPU 上に移動できる.
訓練の際には,イテレーションごとにデータを GPU に移動することを忘れないように.
net.to(device)
inputs, labels = data[0].to(device), data[1].to(device)
まとめ
最後に,以上の手順を一つのコードでまとめておく.
# import packages -------------------------------
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
# prepare data ----------------------------------
transform = transforms.Compose(
[transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
trainset = torchvision.datasets.CIFAR10(root='./data', train=True,
download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=4,
shuffle=True, num_workers=2)
testset = torchvision.datasets.CIFAR10(root='./data', train=False,
download=True, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=4,
shuffle=False, num_workers=2)
classes = ('plane', 'car', 'bird', 'cat',
'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
# define a network ------------------------------
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()
# define loss function and optimizer -------------
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)
# start training ---------------------------------
for epoch in range(2): # loop over the dataset multiple times
running_loss = 0.0
for i, data in enumerate(trainloader, 0):
# get the inputs; data is a list of [inputs, labels]
inputs, labels = data
# zero the parameter gradients
optimizer.zero_grad()
# forward + backward + optimize
outputs = net(inputs)
loss = criterion(outputs,labels)
loss.backward()
optimizer.step()
# print statistics
running_loss += loss.item()
if i%2000==1999: # print every 2000 mini-batches
print('[%d, %5d] loss: %.3f' % (epoch+1, i+1, running_loss/2000))
running_loss = 0.0
print('Finished Training')
# check on test data ----------------------------
correct = 0
total = 0
with torch.no_grad():
for data in testloader:
images, labels = data
outputs = net(images)
_, predicted = torch.max(outputs, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
print('Accuracy of the network on the 10000 test images: %d %%' % (100*correct/total))