3
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

PyTorchの基礎 (2) -ニューラルネットワークの作り方-

Posted at

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__()メソッドにて,ネットワークが保持するレイヤーを定義する.
LinearConv2dなどのよく使用するほとんどのレイヤーがtorch.nnで定義されている.
詳しくは公式ドキュメントを参照 --> torch.nn

同様に,relumax_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)))

[Output]
image.png

ネットワークの構築

続いて,画像の分類を行うネットワークを構築する.

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)))

[Output]
image.png

続いて,保存したネットワークのパラメータを読み込む.
その後,読み込んだモデルにテストデータを入力し,分類結果を表示してみる.

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))
3
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?