PyTorch を用いることで、様々なニューラルネットワークを容易に構築できる。このページでは、画像分類を行うための畳み込みニューラルネットワークを PyTorch で構築して、学習と検証を行う例を示す。
画像データセット
サンプル画像として Kaggle にある Blood Cell Images を使用する。このデータセットをダウンロードして展開すると、dataset-master と dataset2-master の二つのフォルダが得られる。このうち、dataset2-master の方のフォルダの下にある images フォルダ中の画像を使用する。
ls dataset2-master/dataset2-master/images
## TEST TEST_SIMPLE TRAIN
ls dataset2-master/dataset2-master/images/TRAIN
## EOSINOPHIL LYMPHOCYTE MONOCYTE NEUTROPHIL
ls dataset2-master/dataset2-master/images/TEST
## EOSINOPHIL LYMPHOCYTE MONOCYTE NEUTROPHIL
Blood Cell Images データセットは、EOSINOPHIL、LYMPHOCYTE、MONOCYTE、および NEUTROPHIL の 4 つのカテゴリに整理されている。このデータセットを使用して、分類モデル(畳み込みニューラルネットワーク)を構築する例を示す。まず、dataset2-master/images の下にあるデータセットを PyTorch に認識させる必要がある。この操作は、以下のような手順で行う。
- 画像のデータ前処理を行うための手順を定義する。
- 畳み込みニューラルネットワークによる画像分類では、入力画像のサイズを揃える必要がある。ここでは、すべての入力画像を幅 320 ピクセル、縦 240 ピクセルに揃えることにする(
transforms.Resize
)。 - PyTorch では、行列データとして読み込まれた画像データをそのまま利用できないので、これをテンソル型に変換する(
transforms.ToTensor
)。 - また、画像データは 0 から 255 までの整数によって記述されている。このままで学習を進めるには値の範囲が大きすぎて収束しにくいため、これらの数値を小さな数値に変換するための正規化を行う(
transforms.Normalize
)。
- 畳み込みニューラルネットワークによる画像分類では、入力画像のサイズを揃える必要がある。ここでは、すべての入力画像を幅 320 ピクセル、縦 240 ピクセルに揃えることにする(
- 画像フォルダへのパスを
ImageFolder 関数に与える。 ImageFolder 関数が、フォルダ構造を解釈し、カテゴリ数や画像の枚数などの集計を自動的に行う。また、画像を読み込む時に、どのような前処理を行うのかについても、この時点で準備する(手順 1 で定義した処理を ImageFolder
関数に代入する必要がある)。 ImageFolder
関数によって整理された画像をシャッフルしてから数枚ずつ取り出せるように学習体制を整える(DataLoader
)。以下の例では、4 ずつ取り出せるように設定してある。
data_dir_path = 'dataset2-master/dataset2-master/images/'
data_transforms = torchvision.transforms.Compose([
torchvision.transforms.Resize((240, 320)),
torchvision.transforms.ToTensor(),
torchvision.transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])
train_dataset = torchvision.datasets.ImageFolder(root=data_dir_path + 'TRAIN',
transform=data_transforms)
train_dataloader = torch.utils.data.DataLoader(train_dataset, batch_size=4,
shuffle=True, num_workers=1)
ニューラルネットワークの定義
PyTorch でニューラルネットワークを構築する時は、Python の「クラス」と呼ばれる概念を利用する。クラスは以下の例のように class
から始まる文型で定義し、いわば関数をまとめたようなものである。クラスの名前(以下の例では Net
である)はニューラルネットワークの設計書の名前になる。このクラスの名前を任意に変更することができる。
このページでは、畳み込み層、プーリング層、畳み込み層、プーリング層、全結合層、全結合層、そして出力層からなる 7 層のニューラルネットワークを構築する方法を示す。PyTorch を使ってネットワークを構築するとき、まずネットワークの部品を作成し、それから部品ごとをつなげていく手順を踏む必要がある。ネットワークの部品は __init__
関数の中で作成する。この例では畳み込み層、プーリング層、畳み込み層、プーリング層、全結合層、全結合層という 7 つの部品を用意する必要がある。それぞれの部品に独自のパラメータを持っているため、部品の共有はできない。つまり、この例では 1 層目の畳み込み層と 3 層目の畳み込み層は互いに独立で、1 つの畳み込み層を両者で共有できない。そのため、部品を作成するときに、両者を区別できるように conv1 や conv2 のように識別できるような名前で作成する必要がある。名前の付け方は自由であるが、わかりやすい名前をつけた方がいい。
forward
は、__init__
で作った部品のつなぎ方を定義する。畳み込みとプーリングの繰り返しの後のデータは、入力画像と同様に行列になっている。これに対して、全結合層(fc)はベクトルの入力を要求する。そこで、畳み込み演算結果を全結合層に入力する間に、行列をベクトルに変換するために view 関数を使用する必要がある。下の例では、view
メソッドの第 2 引数に 32 * 57 * 77 を指定したが、これは入力カラー画像が 3x320x240 で、畳み込み演算とプーリング演算を繰り返した結果 32x57x77 となっているためである。
import torch
import torchvision
class Net(torch.nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = torch.nn.Conv2d(3, 16, 5)
self.pool1 = torch.nn.MaxPool2d(2, 2)
self.conv2 = torch.nn.Conv2d(16, 32, 5)
self.pool2 = torch.nn.MaxPool2d(2, 2)
self.fc1 = torch.nn.Linear(32 * 57 * 77, 512)
self.fc2 = torch.nn.Linear(512, 64)
self.fc3 = torch.nn.Linear(64, 4)
def forward(self, x):
x = torch.nn.functional.relu(self.conv1(x))
x = self.pool1(x)
x = torch.nn.functional.relu(self.conv2(x))
x = self.pool2(x)
#print(x.shape)
x = x.view(-1, 32 * 57 * 77)
#print(x.shape)
x = torch.nn.functional.relu(self.fc1(x))
x = torch.nn.functional.relu(self.fc2(x))
x = self.fc3(x)
return x
ニューラルネットワークの設計書を定義したので、この設計書から実体(net
)を一つ作り出す。後ほど、実体 net
に対して学習と検証を行う。
net = Net()
学習
データの準備が完了し、ネットワークの設計書を制作し、その設計書からモデルの実体 net を作成した。次に、このモデルに学習データを代入して学習させる方法を示す。学習を行うには、損失関数と学習アルゴリズムを決める必要がある。
人は、テストを受けた後に、間違った問題を見直すことで、その問題を覚える。機械学習(深層学習)も同様で、間違って予測したものを見直すことによって学習を行なっている。そのために、採点基準を設定する必要がある。一般に、分類問題では、交差エントロピー損失を使用する。ここでも交差エントロピー損失を使う。
またh、人が英単語を覚えるとき、書きながら覚えるたり、音読しながら覚えたり、接頭語・接尾語を駆使して覚えたりして、与えられた単語帳の難易度に応じて覚え方を切り替えたり、人によって得意・不得意の覚え方が違っていたりする。これと似た感じで、モデルに画像を学習させるときには、様々な学習方法が存在する。この学習方法は、画像の複雑さやネットワークの構造によって、得意・不得意がある。そのため、多くの学習方法は存在するものの、どれが最適なのかは試してみないとわからない。ここでは Adam と呼ばれる方法を使うことにする。
criterion = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(net.parameters(), lr=0.01, momentum=0.9)
続いて学習を行う。Pytorch のモデルには訓練モードと検証モードがある。はじめにモデルを訓練モードに切り替える。
net.train()
学習データを使用してモデルの学習を行う。同じデータセットに対して学習を 20 回繰り返す。各回に、学習成果として損失を出力する。損失は予測が間違っている画像の枚数のようなものであるため、値が小さければ小さいほどがいい。
n_epochs = 50
for epoch in range(n_epochs):
# loss in this epoch
running_loss = 0.0
for inputs, labels in train_dataloader:
optimizer.zero_grad()
outputs = net(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
running_loss += loss.item()
print('Epoch: {}; Loss: {}'.format(epoch, running_loss))
print('Finished Training')
検証・予測
訓練データで学習を済ませたモデルにテストデータを代入して検証する。テストデータも、訓練データと同様にミニバッチでテストできる。この際に、ネットワークを検証モードに切り替える。なお、ネットワークを検証・テストモードで使用するためには、ネットワークに対して eval()
メソッドを実行する。今回の例は train()
と eval()
を使い分けなくてもよいが、ネットワーク中にバッチ正規化やドロップアウトなどを定義している場合に、切り替えが必要である。また、検証時に、自動微分などの情報が必要ないため、無駄な計算を行わないように torch.no_grad
の状態下で行う。
net.eval()
test_dataset = torchvision.datasets.ImageFolder(root=data_dir_path + 'TEST',
transform=data_transforms)
test_dataloader = torch.utils.data.DataLoader(test_dataset, batch_size=4,
num_workers=1)
n_correct = 0
n_total = 0
with torch.no_grad():
for data in test_dataloader:
images, labels = data
outputs = net(images)
_, predicted = torch.max(outputs.data, 1)
n_total += labels.size(0)
n_correct += (predicted == labels).sum().item()
print('#images: {}; acc: {}'.format(n_total, 100 * n_correct / n_total))