菅間修正済み 2025/05/15
【原題】TRAINING A CLASSIFIER
【原著】Soumith Chintala
【元URL】https://pytorch.org/tutorials/beginner/blitz/cifar10_tutorial.html
【翻訳】電通国際情報サービスISID AIトランスフォーメーションセンター 徳原 光
【日付】2020年11月5日
【チュートリアル概要】
データセット「CIFAR-10」の画像分類を実装しながら、PyTorchを用いてニューラルネットワークを構築し、モデルを訓練する方法を解説します。
本チュートリアルは「Deep Learning with PyTorch: A 60 Minute Blitz」(合計4本)の最後となります。
これまでに、ニューラルネットワークを定義し、損失を計算し、ネットワークの重みを更新する方法を解説してきました。
今、皆様は次のような疑問を持っているかもしれません。
「データはどうするのだろうか?」
一般的に、画像、テキスト、音声、動画などのデータを扱う場合は、numpy配列にデータを読み込むことや、標準的なPythonパッケージを利用することができます。
そして、numpy配列からtorch.*Tensorを用いて、テンソルに変換することが可能です。
PyTorchでは、特に画像データのために、ImagenetやCIFAR10、MNISTといったデータセットの読み込み機能と画像データの変換機能を持つ、torchvisionと呼ばれるパッケージを開発しました。
パッケージには、torchvision.datasets やtorch.utils.data.DataLoaderが含まれています。
これらはユーザーに多くの便利な機能を提供します。また、定型的なコードの記述を省略することもできます。
本チュートリアルでは、「CIFAR10データセット」を使用します。
このデータセットには「飛行機」、「自動車」、「鳥」、「猫」、「鹿」、「犬」、「カエル」、「馬」、「船」、「トラック」のクラスが含まれています。
また、CIFAR-10の画像はサイズが3×32×32、すなわち3つの色チャネルを持つ32×32ピクセルの画像になります。
cifar10
画像分類器の訓練
以下の手順に従って実施します:
torchvisionを用いた、CIFAR10の訓練データとテストデータの読み込みと正規化torchvisionを使えば、CIFAR10の読み込みを非常に簡単に行うことができます。
import torch
import torchvision
import torchvision.transforms as transforms
torchvisionデータセットの出力は、値が0から1の範囲のPILImageイメージになります。
これを値が-1から1の範囲に付近に正規化されたTensor(テンソル)に変換します。
(日本語訳注:PILImageイメージは前述の画像処理ライブラリPillowで扱われる画像データのことを指します。)
注意:
Windows環境で実行していて、BrokenPipeErrorが発生した場合は、torch.utils.data.DataLoader()のnum_workerを0に設定してください
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')
Downloading https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz to ./data/cifar-10-python.tar.gz
HBox(children=(FloatProgress(value=1.0, bar_style='info', max=1.0), HTML(value='')))
Extracting ./data/cifar-10-python.tar.gz to ./data Files already downloaded and verified
それでは訓練画像を楽しく眺めてみましょう。
import matplotlib.pyplot as plt
import numpy as np
# 画像の表示関数
def imshow(img, name):
img = img / 2 + 0.5 # 正規化を戻す
npimg = img.numpy()
plt.imshow(np.transpose(npimg, (1, 2, 0)))
plt.savefig(name)
# 適当な訓練セットの画像を取得
for images, labels in trainloader:
break
# 画像の表示
imshow(torchvision.utils.make_grid(images), 'cifar_training_images.png')
# ラベルの表示
print(' '.join('%5s' % classes[labels[j]] for j in range(4)))
horse plane deer frog
前回のニューラルネットワークのチュートリアルからニューラルネットワークをコピーし、3chのカラー画像を入力にとるように修正します(前回は1chで定義されていました)。
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()
分類用にクロスエントロピー誤差関数と、momentum有りSGDを使用しましょう。
(日本語訳注: MomentumはSGDを改良した最適化アルゴリズムになります。
optim.SGDを実行する際にmomentumを引数として設定することで、パラメータを更新する際の更新量を制御します。)
import torch.optim as optim
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)
for epoch in range(2): # エポック数分ループを回します
running_loss = 0.0
for i, data in enumerate(trainloader, 0):
# データセットのデータを [inputs, labels]の形で取得
inputs, labels = data
# パラメータの勾配をリセット
optimizer.zero_grad()
# 順伝搬+逆伝搬+パラメータ更新
outputs = net(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
# 統計情報の表示
running_loss += loss.item()
if i % 2000 == 1999: # 2,000ミニバッチにつき1度表示
print('[%d, %5d] loss: %.3f' %
(epoch + 1, i + 1, running_loss / 2000))
running_loss = 0.0
print('Finished Training')
[1, 2000] loss: 2.196 [1, 4000] loss: 1.877 [1, 6000] loss: 1.682 [1, 8000] loss: 1.607 [1, 10000] loss: 1.510 [1, 12000] loss: 1.441 [2, 2000] loss: 1.381 [2, 4000] loss: 1.348 [2, 6000] loss: 1.339 [2, 8000] loss: 1.308 [2, 10000] loss: 1.310 [2, 12000] loss: 1.284 Finished Training
学習したモデルをすぐに保存しましょう:
PATH = './cifar_net.pth'
torch.save(net.state_dict(), PATH)
PyTorchモデルの保存方法の詳細については、こちらもご覧ください。
学習データセットを2巡、モデルに入力し、ネットワークを訓練しました。
ですが、ネットワークがきちんと学習したかどうかを確かめる必要があります。
ニューラルネットワークの出力である画像のカテゴリラベルを予測し、正解ラベルと比較します。
予測結果が正しければ、そのサンプルを正しい予測結果リストに追加します。
では始めましょう。
まずはテストセットに慣れるために、テスト画像を表示してみましょう。
for images, labels in trainloader:
break
# 画像と正解ラベルの表示
imshow(torchvision.utils.make_grid(images), 'cifar_test_images.png')
print('GroundTruth: ', ' '.join('%5s' % classes[labels[j]] for j in range(4)))
GroundTruth: cat ship ship plane
次に、保存したモデルを読み込んでみましょう:
(注:ここでは本来、モデルの保存と再読み込みは必要ありません。ですが、その方法を紹介するために記載しています)
net = Net()
net.load_state_dict(torch.load(PATH))
<All keys matched successfully>
そして、ニューラルネットワークが上記の入力を、どのように捉えたのか確認しましょう。
outputs = net(images)
出力は入力画像に対する10個のカテゴリの"エネルギー"(のようなもの)を表しています。
とあるカテゴリのエネルギーが高いほど、ネットワークはその画像がそのカテゴリに属すると考えます。
ですので、エネルギーが最も高いカテゴリを取得しましょう:
_, predicted = torch.max(outputs, 1)
print('Predicted: ', ' '.join('%5s' % classes[predicted[j]]
for j in range(4)))
Predicted: cat car ship ship
かなり良い結果です。
では、ネットワークがデータセット全体に対しどの程度の性能になっているか確認しましょう。
correct = 0
total = 0
with torch.no_grad():
for data in testloader:
images, labels = data
outputs = net(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
print('Accuracy of the network on the 10000 test images: %d %%' % (
100 * correct / total))
Accuracy of the network on the 10000 test images: 55 %
チャンスレベル(10クラスの中からランダムにクラスを選ぶ場合)の10%より良い結果です。
ネットワークが何らかを学習しているようです。
ではうまく分類できたクラス、できなかったクラスはそれぞれ何だったのでしょうか:
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]))
Accuracy of plane : 42 % Accuracy of car : 73 % Accuracy of bird : 38 % Accuracy of cat : 43 % Accuracy of deer : 38 % Accuracy of dog : 45 % Accuracy of frog : 74 % Accuracy of horse : 63 % Accuracy of ship : 73 % Accuracy of truck : 65 %
さて、次はどうしましょうか。
ところで、どのようにして、ニューラルネットワークをGPUで実行するのでしょうか?
テンソルをGPUに転送するのと同じように、ニューラルネットワークをGPUに転送します。
CUDAが利用可能な環境でしたら、自分のマシンを1つ目のcudaデバイスとして定義してみましょう。
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
# GPU搭載のCUDA環境を前提としており、その場合はcuda:0と出力されるはずです
print(device)
cuda:0
このチュートリアルの残りの部分は、deviceがCUDAデバイスであることを前提としています。
※日本語訳注:
仮に、google Colaboratoryで本チュートリアルを実行し、print(device)の結果がcpuだった場合は、GPU環境に切り替えます。
画面上段のツールバーから「編集」→「ノートブックの設定」→「ハードウェアアクセラレータ」の項目でGPUを選択することで、GPUを利用することができます。
以下のメソッドは、ネットワーク内の全モジュールを再帰的に調べ、全モジュールのパラメータとバッファをCUDAテンソルに変換します:
net.to(device)
Net( (conv1): Conv2d(3, 6, kernel_size=(5, 5), stride=(1, 1)) (pool): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False) (conv2): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1)) (fc1): Linear(in_features=400, 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) )
ループ内では、以下のように、毎ステップ、入力データと正解ラベルもGPUに送る必要があることも忘れないでください:
inputs, labels = data[0].to(device), data[1].to(device)
ですが、今回はCPUで実行した際と比較して、大幅な高速化にはならないかと思います。 その理由は、今回のネットワークのサイズが小さいからです。
(日本語訳注:以下にGPUで実行するプログラムを記載しています。
またネットワークサイズが小さいと、ネットワーク内の順伝搬、逆伝搬、パラメータ更新の時間はあまり問題とならず、データをロードするCPU部分の処理時間の方が支配的になります。)
# 日本語訳注:GPU版で訓練を実行した場合
# optimizerを再定義(netがGPU上に移動したので)
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)
for epoch in range(2): # データセットを何巡繰り返すか
running_loss = 0.0
for i, data in enumerate(trainloader, 0):
# 入力を取得します; 変数dataはリスト[inputs, labels]です
# inputs, labels = data # cpuの場合をコメントアウト
inputs, labels = data[0].to(device), data[1].to(device)
# 勾配を0に初期化
optimizer.zero_grad()
# 順伝搬、逆伝搬、パラメータ更新
outputs = net(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
# 統計情報を出力
running_loss += loss.item()
if i % 2000 == 1999: # 2000ミニバッチごとに出力
print('[%d, %5d] loss: %.3f' %
(epoch + 1, i + 1, running_loss / 2000))
running_loss = 0.0
print('Finished Training')
[1, 2000] loss: 1.204 [1, 4000] loss: 1.208 [1, 6000] loss: 1.207 [1, 8000] loss: 1.195 [1, 10000] loss: 1.174 [1, 12000] loss: 1.179 [2, 2000] loss: 1.104 [2, 4000] loss: 1.110 [2, 6000] loss: 1.111 [2, 8000] loss: 1.093 [2, 10000] loss: 1.108 [2, 12000] loss: 1.116 Finished Training
演習:ネットワークの幅を広げてみてください
(ただし、最初のnn.Conv2dの2つ目の引数と、2番目のnn.Conv2dの1つ目の引数は同じ数である必要があります)。
幅を広げると、CPU版とGPU版を比較した際に、どのような高速化が得られるか試してください。
本チュートリアルで達成した目標: