2019/9/29 投稿
2019/11/8 やや見やすく編集(主観)
2020/2/1 SGDの解説Link追加
2020/8/17 図がないとよくわからないと思い図を追加
2020/9/21 コメントより補足説明追加と図の更新
0. この記事の対象者
- pythonを触ったことがあり,実行環境が整っている人
- pyTorchをある程度触ったことがある人
- pyTorchによる機械学習で意味をより詳しく知りたい人
- pyTorchを初めて触り,このCNNsチュートリアルを動かしたい人でこの記事を読み続ける根気がある人
1. はじめに
昨今では機械学習に対してpython言語による研究が主である.なぜならpythonにはデータ分析や計算を高速で行うためのライブラリ(moduleと呼ばれる)がたくさん存在するからだ.
その中でも今回はpyTorchと呼ばれるmoduleを使用し,Convolutional Neural Networks(CNNs)の exampleコードを徹底的に解説していく.
全体のコードは最後に貼っておくので,説明が煩わしい方はそちらから見てほしい.
ただしこの記事は自身のメモのようなもので,あくまで参考程度にしてほしいということと,簡潔に言うために間違った表現や言い回しをしている場合があるかもしれないが,そこはどうかご了承していただきたい.
2. 事前知識
pythonには他言語同様,「型」というものが定義した変数には割り当てられており,中でも「list型」,「tuple型」,「dictionary型」がよく出てくるように思う.さらに,moduleとして「numpy」というものもあり,このnumpyが持つ特殊な型,「ndarray型」もよく出てくる.
ここではあえて説明はしないが,わからない人は是非検索をしてそれぞれをしっかり理解しておいてほしい.
3. pyTorchのインストール
pyTorchを初めて使用する場合,pythonにはpyTorchがまだインストールされていないためcmdでのインストールをしなければならない.
下記のLinkに飛び,ページの下の方にある「QUICK START LOCALLY」で自身の環境のものを選択し,現れたコマンドをcmd等で入力する(コマンドをコピペして実行で良いはず).
さらに,今回は「torchvision」というmoduleも使用するためこちらもインストールしておいてほしい.
コマンドは以下に示す(condaを使用している場合).
conda install torchvision
4. pyTorchによるCNNs
4-0. CNNsとは?
CNNs(Convolutional Neural Networks)は直訳すると畳み込みニューラルネットワークというもので入力されるdataに関して畳み込みという処理を複数回行うことでその入力data(例えば画像)から特徴を抽出していく.
特にこのCNNsは画像の特徴を抽出するのに非常に優れており,昨今では様々な画像を用いた機械学習に用いられている.
畳み込みについて説明すると,以下の図のようなことが行われている.
kernel (parameter)と書いてある青い枠が畳み込みをするためのパラメータである.
このkernelが入力画像の左上の領域と重なり,重なった部分の要素ごとに掛け算をしてそれらをすべて足したものが特徴画像の1ピクセルとなる.
その計算の詳細を吹き出し内に書いている.
そして次の1ピクセルを計算する際は入力画像の左上の領域が右に1ピクセルだけずれて同じ計算をする.
これが入力画像の一番右まで行くと今度は最初に戻って下に1ピクセルずれて同じことが繰り返される.
こうして特徴画像が出来上がるのだ.
ここで見るように特徴画像のサイズがやや小さくなっていることに注意する.
4-1. pyTorchに用意されている特殊な型
numpyにはndarrayという型があるようにpyTorchには「Tensor型」という型が存在する.
ndarray型のように行列計算などができ,互いにかなり似ているのだが,Tensor型はGPUを使用できるという点で機械学習に優れている.
なぜなら機械学習はかなりの計算量が必要なため計算速度が早いGPUを使用するからだ.
Tensor型の操作や説明は下記Linkより参照していただきたい.
ただし,機械学習においてグラフの出力や画像処理などでnumpyも重要な役割を持つ.そのためndarrayとTensorを交互に行き来できるようにしておくことがとても大切である.
4-2. pyTorchのimport
ここからはcmd等ではなくpythonファイルに書き込んでいく.
下記のコードを書くことでmoduleの使用をする.
import torch
import torchvision
ついでにnumpyもimportしておく.
import numpy
4-3. Datasetの取得
pyTorchのtorchvision moduleには主要なDatasetがすでに用意されており,たった数行のコードでDatasetのダウンロードから前処理までを可能とする.
結論から言うと3行のコードでDatasetの運用が可能となり,ステップごとに言えば,
- transformsによる前処理の定義
- Datasetsによる前処理&ダウンロード
- DataloaderによるDatasetの使用
という流れになる.
ここでは簡単に解説するがそれぞれのステップについて詳しく知りたい場合は以下のLinkを参照してほしい.
pyTorchのtransforms,Datasets,Dataloaderの説明と自作Datasetの作成と使用
4-3-1. transformsによる前処理の定義
以下に前処理の定義を示す.
trans = torchvision.transforms.ToTensor()
この1行は,これから取得するDatasetの中身がndarray型のデータ集合であるため,前処理でTensor型にしたい.
そのためこのtransという変数をndarrayからTensorへと変換する関数のようなものにした(本当はtransはクラスインスタンスであるが気にしない).
もう少し詳しく言うと,以下のようにこのtransを使用でき,
a = trans(ndarrayのdata)
ndarrayのdataを各画像のピクセル値(輝度)を[0,1]の範囲にしつつTensor型に変換し,変数aに渡している.
ここで例えばTensor変換だけでなく正規化を同時にしたい場合は以下のようにする.
trans = torchvision.transforms.Compose([torchvision.transforms.ToTensor(),torchvision.transforms.Normalize((0.5,), (0.5,))])
torchvision.transforms.Composeは引数で渡されたlist型の[~~,~~,...]というのを先頭から順に実行していくものである.そのためlist内の前処理の順番には十分注意する.
こうすることでtransという変数はTensor変換と正規化を一気にしてくれるハイブリッドな変数になった.
その他にもたくさんのtransformsがあり,それらは以下のLinkより参照してほしい.
4-3-2. Datasetsによる前処理&ダウンロード
以下にダウンロードを示す.
trainset = torchvision.datasets.MNIST(root = 'path', train = True, download = True, transform = trans)
まずは引数の説明をしていく.
「root」はDatasetを参照(または保存)するディレクトリを「path」の部分に指定する.そのディレクトリに取得したいDatasetが存在すればダウンロードせずにそれを使用する.
「train」はTraining用のdataを取得するかどうかを選択する.FalseにすればTest用のdataを取得するが,この2つの違いはdata数の違いと思ってくれて良い.
「download」は参照したディレクトリにDatasetがない場合ダウンロードするかどうかを決めることができる.
「transform」は定義した前処理を渡す.こうすることでDataset内のdataを参照する際にその前処理を自動で行ってくれる.
今回はMNISTを使用したが,他の使用できるDatasetは下記のLinkより参照して使用して欲しい.その時のコードも大体同じである.
取得したtrainsetをそのまま出力してみると以下のようなDatasetの内容が表示されるはずだ.
print(trainset)
------'''以下出力結果'''--------
Dataset MNIST
Number of datapoints: 60000
Root location: rootで指定したpathが出るはず
Split: Train
StandardTransform
Transform: Compose(
ToTensor()
Normalize(mean=(0.5,), std=(0.5,))
)
これだけ見るとDatasetなのにどうやってdataを見ているの?となるが,dataの参照は以下のようにすれば良い.
print(trainset[0])
------'''以下出力結果'''--------
(tensor([data内容]), そのdataに対応する正解label)
これでDatasetの0番目のdataを参照できる.
実は実際にDatasetのdataを使用するときも配列の参照のようにdataを参照している.
4-3-3. DataloaderによるDatasetの使用
DataloaderによるDatasetの使用は下記のコードで実行する.
trainloader = torch.utils.data.DataLoader(trainset, batch_size = 100, shuffle = True, num_workers = 2)
まずは引数の説明をしていく.
第1引数は先程取得したDatasetを入れる.
「batch_size」は1回のtrainingまたはtest時に一気に何個のdataを使用するかを選択.datasetの全data数を割り切れる値にしなければならない.
「shuffle」はdataの参照の仕方をランダムにする.
「num_workers」は複数処理をするかどうかで,2以上の場合その値だけ並行処理をする. コメントで寄せられたので記述しておくがWindows OSを使用している方はこの値が2以上だとエラーが起こることがあるらしい.エラーが起こる場合は1や0とするのがよいだろう.
取得したtrainloaderを出力しても以下のようなオブジェクトタイプしか表示されない.
print(trainloader)
------'''以下出力結果'''--------
<torch.utils.data.dataloader.DataLoader object at 0x7fdffa11ada0>
Datasetのように中身が見たい場合,配列の参照のようにするとエラーが起こる.なぜならDataLoaderは配列ではなくiteratorというものを返しているためである.
無理やり中身を見ようとするならば以下のようにすれば良い.
for data,label in trainloader:
break
print(data)
print(label)
------'''以下出力結果'''--------
tensor([[data1], [data2],..., [data100]])
tensor([label1, label2,..., label100])
このように1回の取得でdataとlabelはバッチサイズ(今で言うと100)だけ取得され,もちろん各dataとlabelは対応しあっている.
図で見てみると
このようにtrainloaderは全dataと対応するlabelをBatch毎に分けてセットで持ち,それを変数detaとlabelに渡している.
ただし,この確認は絶対に学習する前に同じプログラム内ではやってはいけない.
なぜならtrainloaderはiteratorであるため今回呼び出したdataは全データを見きるまで二度と見られることがなくなってしまう(iteratorは最初の100個を取り出すと次回の呼び出しからは次の100個を取り出す).
つまりこの参照をしてしまった100個のdataは2周目に入るまで見られなくなってしまう.
そこに十分注意してほしい.
4-3-4. test用datasetの取得
training用のdataset取得がわかればtest用もほとんど同じである.
以下にコードを示す.
testset = torchvision.datasets.MNIST(root = '~/Documents/work/MNISTDataset/data', train = False, download = True, transform = trans)
testloader = torch.utils.data.DataLoader(testset, batch_size = 100, shuffle = False, num_workers = 2)
違う点とすればdatasetsのtrainがFalseであることと,dataloaderのshuffleがFalseとなっているところだけである.
shuffleがFalseなのはTestの時にはdataをランダムに取得する必要がないからである.
transformは同様のtransを使用する.
4-4. Networkの定義
今回はCNNsを作成するが,作成にはいくつかの手法がある.
その中でも私はclassを使用した方法で作っていく.
以下に簡単なCNNsを作った.
ただし,これはMNISTを使用する上で作成したもので他のDatasetによって細かいパラメータ等が変わるので注意する.
import torch.nn as nn
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.relu = nn.ReLU()
self.pool = nn.MaxPool2d(2, stride=2)
self.conv1 = nn.Conv2d(1,16,3)
self.conv2 = nn.Conv2d(16,32,3)
self.fc1 = nn.Linear(32 * 5 * 5, 120)
self.fc2 = nn.Linear(120, 10)
def forward(self, x):
x = self.conv1(x)
x = self.relu(x)
x = self.pool(x)
x = self.conv2(x)
x = self.relu(x)
x = self.pool(x)
x = x.view(x.size()[0], -1)
x = self.fc1(x)
x = self.relu(x)
x = self.fc2(x)
return x
まず最初にtorch module内の「torch.nn」をimportする.その際「as nn」と書くことで以降使用するときに「nn.xxxx」と書くだけで使用できるようになる.
続いて以下にnetworkの中身を上から順に説明していく.
4-4-1. classとしてNetworkを作成
Networkをclassとして作成する.
雛形で言えばこんな形になる.
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
......
def forward(self, x):
......
以下各行の説明.
1行目の 「Net」はただの名前だから好きなもので良い.
その名前の後の「nn.Module」はこのclassがnn.Moduleというclassを継承していることを意味する.
なぜ継承するかというとnn.ModuleがNetworkを操作する上でパラメータ操作などの重要な機能を持つためである.2行目の「def __init__(self)」は初期化関数の定義で,コンストラクタなどと呼ばれる.初めてclassを呼び出したときにこの中身のものが実行される.
3行目の「super(Net, self).__init__()」は継承したnn.Moduleの初期化関数(nn.Moduleの中の__init__())を起動している.super()の引数の「Net」はもちろん自身が定義したclassの名前である.
最後の「def forward(self, x)」には実際の処理を書いていく.
4-4-2. 初期化関数の定義
以下に初期化関数部分のみを再掲する.
def __init__(self):
super(Net, self).__init__()
self.relu = nn.ReLU()
self.pool = nn.MaxPool2d(2, stride=2)
self.conv1 = nn.Conv2d(1,16,3)
self.conv2 = nn.Conv2d(16,32,3)
self.fc1 = nn.Linear(32 * 5 * 5, 120)
self.fc2 = nn.Linear(120, 10)
この中で定義した「self.xxxxx」という変数はずっと保持される.
なので,convolutionやfully connectなどの学習に必要なパラメータを保持したいものなどをここに書いていく.
以下にコードの説明をする.
「nn.ReLU()」は活性化関数というもので,各層の後に必ずと言っていいほど使用されている.種類はたくさんある.
「nn.MaxPool2d(2, stride=2)」はpooling用でこのパラメータの場合データサイズが半分になるような処理をする(サイズの小数点以下は切り下げられる).
「nn.Conv2d(1,16,3)」はconvolution(畳み込み)の定義で,第1引数はその入力のチャネル数,第2引数は畳み込み後のチャネル数,第3引数は畳み込みをするための正方形フィルタ(カーネルとも言う)の1辺のサイズである.
最初の畳み込みの例でも言ったが入力dataの画像サイズH×Wと出力の画像サイズH×Wは通常異なるものとなり,Convolutionのカーネルサイズなどから計算できる.計算方法は以下Linkを見てほしい.
nn.Conv2dの公式ページ「nn.Linear(32 * 5 * 5, 120)」はfully connectの定義で,第1引数は入力のサイズ(ただし入力は行列ではなくベクトルでなければならない),第2引数は出力後のベクトルサイズを示す.
この場合の第1引数の意味は入力ベクトルがベクトルになる前のdataでは32Channel × 5Height× 5Width(5×5の画像が32枚)となっていたことがわかる.どうやってベクトルにしたかは4-4-3で説明する.
注意してほしいのは,この「nn.Linear(32 * 5 * 5, 120)」より32ChannelというのはConvolutionの最後の出力を見ればわかるが,5と5については入力画像のサイズからConvolutionとpoolingを経てどれだけサイズが小さくなるかを事前に計算して得なければならないのである.
これらの他にも多数の関数をpyTorchは用意しており,それらは以下のLinkから参照してほしい.
4-4-3. 処理内容の定義
以下に処理部分のみ再掲する.
def forward(self, x):
x = self.conv1(x)
x = self.relu(x)
x = self.pool(x)
x = self.conv2(x)
x = self.relu(x)
x = self.pool(x)
x = x.view(x.size()[0], -1)
x = self.fc1(x)
x = self.relu(x)
x = self.fc2(x)
return x
「def forward(self, x)」の引数xがこのネットワークに対しての入力dataである.もちろんこのxはdataloaderから取得したバッチサイズ数のdata群である.
このように入力xを__init__()で定義した各層に上から順に入力していく.
このxは「x = xxxx(x)」のように毎回更新されていることに注意して欲しい.
まず上から3つのconv1~poolまでの処理を図で説明する.
まずバッチサイズ分の1 channelの入力画像が16個のkernel(parameter)を通じて16 channelの特徴を得る.このkernelはそれぞれが3x3のパラメータを持ち,16個全てのkernelが異なるパラメータを持っている.つまりここでのパラメータの総数は16x3x3=144である(ほんとはバイアス16個分もさらにある).
これがchannel数が増えているカラクリとなっている.
それにReLUという活性化関数(グラフのような0以下は全て0になる関数)を適用させる.
そしてpoolingを行うことで特徴画像サイズが半分となる.
同様にconv2に関しても見てみる.
このように16 channelの入力に対してconv2は16個のkernelを1セットしたものを32個持っており,1セットと16 channelの入力を用いて出力の特徴画像1枚を求めている(これが32個あるから32 channelとなる).ちなみにここでも各kernelは3x3のパラメータを持ち,全てのkernelのパラメータは異なる.ここでのパラメータの総数は32x16x3x3=4608である(ほんとはバイアス32x16=512個分もさらにある).
ここで「x.view(x.size()[0], -1)」はTensor型の形を変換するもので,引数を 縦×横 とみた行列に変換される(この-1の意味は片方のサイズになるようにもう片方を自動でサイズ調節するということである).
これが先程言った行列をベクトルの形に変換する操作である.
詳しく説明すると「x.size()」というのはxのサイズを返すもので(Batchsize,Channel,Height,Width)というtuple型が返ってくる.
その0番目をx.size()[0]で指定しているから,書き換えるとこの文は「x.view(Batchsize, -1)」ということになっている.
ということは縦がBatchsize(今の場合100個)になるように横を調節すると,100個の行ベクトルができたことと同じになっているのである(つまり100個の行列データを100個のベクトルデータにした).
上ではバッチサイズ云々で説明したが下に1枚の画像を例にとって考えてみる.
ある入力がCNNを通って特徴画像を得るわけだが,最終的に図の真ん中のような5x5の特徴画像が32 channel分取得できた時,これをx.view()で一本にすると800(5x5x32)次元のベクトルと解釈できる.
この際のベクトル化はもちろん,行列のサイズで言えば[1x800]のサイズになればよいわけだからx.view(1,-1)とすれば得られる.
これは1つのサンプルの例だから,バッチサイズ分の特徴画像に対してそれぞれをベクトル化するのが「x.view(Batchsize, -1)」である.
こうすることで特徴画像をベクトルにできたのでfc1に適応させることができる.
fc1であるfully connect層は下のようなことを行っている.
ここでは見やすくするために先ほどの例で使った800次元のベクトルを縦に書いている.
このベクトルの各値をニューロンといい拡大して簡単に真ん中のようなオレンジの丸で見てみる.
このすべてのニューロンから青色の線に沿って出力側の一つのニューロンになっている.
詳しい計算は吹き出し内に書いている.
同様に他のニューロンも求まる.
4-5. Networkの準備とloss関数と最適化手法
以下にコードを示す.
import torch.optim as optim
device = torch.device("cuda:0")
net = Net()
net = net.to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.0001, momentum=0.9, weight_decay=0.005)
まず最初に,最適化を定義するために「torch.optim」をimportした.このmoduleは最適化の手法を多数持っている.
以下各行の説明.
3行目の「device = torch.device("cuda:0")」はGPUを使用する際にどのGPUを使用するかを決めている.この場合"cuda:0"を使用するということを変数deviceにもたせている.使っているPCの環境にGPUが複数あるなら「"cuda:番号"」とすればそのGPUを使用できる(または単に「"cuda"」とすれば番号など気にせずにGPUを使用できる).
またcpuを使用したい場合は「"cpu"」とすれば良い.4行目の「net = Net()」は先程定義したNetworkを作成している(インスタンスの生成という).netがNetwork内の変数や演算用のパラメータなどを保持することとなる.また,この瞬間にNet()の初期化関数__init__()が起動している.netを出力するとnetの詳細が見れる.
print(net)
------'''以下出力結果'''--------
Net(
(relu): ReLU()
(pool): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(conv1): Conv2d(1, 16, kernel_size=(3, 3), stride=(1, 1))
(conv2): Conv2d(16, 32, kernel_size=(3, 3), stride=(1, 1))
(fc1): Linear(in_features=800, out_features=120, bias=True)
(fc2): Linear(in_features=120, out_features=10, bias=True)
)
5行目の「net = net.to(device)」は作成したNetworkであるnetが3行目で指定した「device」を使用するようにしている.この「xxx.to(device)」はGPUとCPUを行き来するときにもよく使用する.
6行目の「criterion = nn.CrossEntropyLoss()」はloss関数を定義している.このnnは先程importしたtorch.nnのことで,実はtorch.nnはNetwork内の演算以外にもlossを計算するための関数も多数用意しくれている.今回はその中でも「nn.CrossEntropyLoss()」を使用し,criterionという変数にその機能をもたせる.criterionを出力すると何を使用しているかがわかる.
print(criterion)
------'''以下出力結果'''--------
CrossEntropyLoss()
-
7行目の「optimizer = optim.SGD(net.parameters(), lr=0.0001, momentum=0.9, weight_decay=0.005)」は最適化手法であり,このoptim moduleが多数の手法を用意している.最適化手法が一体何をするのかというと,netがもつパラメータが学習のたびに更新されていくのだが,その更新の仕方を決めているのである.この更新方法は最適化手法により異なるのだ.今回は確率的勾配降下法(SGD)を使うため「optim.SGD()」を使用する.
以下引数の説明.- 第1引数はnetのパラメータを渡す.
- 第2引数の「lr=0.0001」は学習率(learning rate)を意味し,簡単に言えば学習の速度を定義する.これはなるべく極小であることが望まれるが,学習がなかなか進まない場合はこの値を大きくしていく.
- 第3引数の「momentum=0.9」は慣性項を意味し,前回の更新量にmomentumの値倍をした項を更新時にさらに加算する.これもパラメータの更新に関わるもので,値が大きいほどパラメータは大きく更新される.
- 第4引数の「weight_decay=0.005」は正則化項を意味し,更新を抑制することで過学習(over fitting)を抑制してくれる.これは値が大きいほど学習が遅くなる.
optimizerを出力するとそれぞれの引数の情報を見ることができる
print(optimizer)
------'''以下出力結果'''--------
SGD (
Parameter Group 0
dampening: 0
lr: 0.0001
momentum: 0.9
nesterov: False
weight_decay: 0.005
)
注意してほしいのは,optimizerの引数で渡した「net.parameters()」はnetのパラメータを参照しているわけではなく,netのパラメータのiteratorを返している.
その証拠に,試しに出力をしてみてもパラメータの表示はされない.
print(net.parameters())
------'''以下出力結果'''--------
<generator object Module.parameters at 0x7fb663b45c00>
詳しいNetworkのパラメータの閲覧や操作方法などが気になる方は以下のLinkより参照してほしい.
また,詳しいSGDの操作やどう動いているかなどは以下のLinkより参照してほしい.
4-6. Training&Test
以下にコードを示す.
for epoch in range(100):
for (inputs, labels) in trainloader:
inputs, labels = inputs.to(device), labels.to(device)
optimizer.zero_grad()
outputs = net(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
for (inputs, labels) in testloader:
inputs, labels = inputs.to(device), labels.to(device)
optimizer.zero_grad()
outputs = net(inputs)
loss = criterion(outputs, labels)
1番外側のループはepoch数だけ回り,epochの数だけ学習をする.
ここで注意してほしいのだがこの記事では学習回数とパラメータの更新回数は別のものとして考える.
epochは学習回数を意味している.
このコードはTrainingとTestをepochのループ内に各ループとして書いており,1ループ毎にそれぞれを実行している.
それぞれの説明を以下に示す.
4-6-1. Training
以下にTrainingのループを示す.
for (inputs, labels) in trainloader:
inputs, labels = inputs.to(device), labels.to(device)
optimizer.zero_grad()
outputs = net(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
1行目はtrainloaderの説明の時のように,中身のdataとlabelをバッチサイズ分取得している.この時,一回のループではバッチサイズ分の100個の値をとっているわけだから,このループは総data数60000を100で割った600回 回ることがわかる.この回数が先程言ったパラメータの更新回数であり,iterationと呼ばれる.
もしバッチサイズが減ればiteration回数が増えることは当然のようにわかる.2行目は取得したdataとlabelをGPUで使用するようにする.
3行目は勾配(gradient)情報を0に初期化をしている.この勾配情報とは後のbackward()という関数により求められるもので,パラメータを更新する際に使用する.
iterationのたびに前の勾配情報を見ないようにするため0にする.4行目は取得したdataをNetworkに入力している.このようにNetworkの使用は「net(data)」とすればnetのforward()が実行される.outputsはNetworkの返り値である.
今回の場合は10要素あり,outputs=[0.1, 0.2, 0.35, 0.05, 0.05, 0.03, 0.07, 0.05, 0.04, 0.06]のようなTensor型ベクトルなっているとする.
今3番目の値が最も大きいから,単純に考えればこのdataは3番目のlabelを予想していることとなる.
通常のclassificationの場合softmaxという関数によりこの出力の要素は確率値に変換される.5行目は先ほど定義したloss関数を使用する.「criterion(outputs, labels)」とすればoutputsとlabelsとのlossを計算してくれる.返り値はTensor型の値である.
ここでもしかしたら読者様の中に「softmaxを最後に使用しなくてよいのか?」と思う方がいるかもしれないが,実はcriterionを定義した「nn.CrossEntropyLoss()」には内部でsoftmaxが実装されているのだ.
もし他のloss関数を使用している場合はその関数がsoftmax等を使用しているかをしっかり確認してほしい.6行目は計算したlossから勾配情報を計算している.この計算をどうやっているかなどの詳しい情報はここでは割愛する.
この勾配情報を用いてlossが小さくなるように更新をしていくのである.7行目は実際に勾配情報を使用してパラメータの更新をする.何故「optimizer.step()」によりパラメータが更新されるかというとoptimizerの宣言時にnet.parameters()を渡しているからである.実は勾配情報はnetのパラメータが一緒に保持している.
4-6-2. Test
以下にTestのループを示す.
for (inputs, labels) in testloader:
inputs, labels = inputs.to(device), labels.to(device)
optimizer.zero_grad()
outputs = net(inputs)
loss = criterion(outputs, labels)
Trainingとほとんど同じなので説明は省く.大きく違う点はTestではパラメータの更新がないためbackward()とstep()が必要ない.
5. まとめソースコード
以下に上の説明を踏まえ,実行中に見やすいよう出力等を追加したまとめのソースコードを示す.
import torch
import torchvision
import torch.nn as nn
import torch.optim as optim
import numpy as np
import matplotlib.pyplot as plt #グラフ出力用module
BATCH_SIZE = 100
WEIGHT_DECAY = 0.005
LEARNING_RATE = 0.0001
EPOCH = 100
PATH = "Datasetのディレクトリpath"
transform = torchvision.transforms.Compose([torchvision.transforms.ToTensor(),torchvision.transforms.Normalize((0.5,), (0.5,))])
trainset = torchvision.datasets.MNIST(root = PATH, train = True, download = True, transform = transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size = BATCH_SIZE,
shuffle = True, num_workers = 2) #Windows Osの方はnum_workers=1 または 0が良いかも
testset = torchvision.datasets.MNIST(root = PATH, train = False, download = True, transform = transform)
testloader = torch.utils.data.DataLoader(testset, batch_size = BATCH_SIZE,
shuffle = False, num_workers = 2) #Windows Osの方はnum_workers=1 または 0が良いかも
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.relu = nn.ReLU()
self.pool = nn.MaxPool2d(2, stride=2)
self.conv1 = nn.Conv2d(1,16,3)
self.conv2 = nn.Conv2d(16,32,3)
self.fc1 = nn.Linear(32 * 5 * 5, 120)
self.fc2 = nn.Linear(120, 10)
def forward(self, x):
x = self.conv1(x)
x = self.relu(x)
x = self.pool(x)
x = self.conv2(x)
x = self.relu(x)
x = self.pool(x)
x = x.view(x.size()[0], -1)
x = self.fc1(x)
x = self.relu(x)
x = self.fc2(x)
return x
device = torch.device("cuda:0")
net = Net()
net = net.to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=LEARNING_RATE, momentum=0.9, weight_decay=WEIGHT_DECAY)
train_loss_value=[] #trainingのlossを保持するlist
train_acc_value=[] #trainingのaccuracyを保持するlist
test_loss_value=[] #testのlossを保持するlist
test_acc_value=[] #testのaccuracyを保持するlist
for epoch in range(EPOCH):
print('epoch', epoch+1) #epoch数の出力
for (inputs, labels) in trainloader:
inputs, labels = inputs.to(device), labels.to(device)
optimizer.zero_grad()
outputs = net(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
sum_loss = 0.0 #lossの合計
sum_correct = 0 #正解率の合計
sum_total = 0 #dataの数の合計
#train dataを使ってテストをする(パラメータ更新がないようになっている)
for (inputs, labels) in trainloader:
inputs, labels = inputs.to(device), labels.to(device)
optimizer.zero_grad()
outputs = net(inputs)
loss = criterion(outputs, labels)
sum_loss += loss.item() #lossを足していく
_, predicted = outputs.max(1) #出力の最大値の添字(予想位置)を取得
sum_total += labels.size(0) #labelの数を足していくことでデータの総和を取る
sum_correct += (predicted == labels).sum().item() #予想位置と実際の正解を比べ,正解している数だけ足す
print("train mean loss={}, accuracy={}"
.format(sum_loss*BATCH_SIZE/len(trainloader.dataset), float(sum_correct/sum_total))) #lossとaccuracy出力
train_loss_value.append(sum_loss*BATCH_SIZE/len(trainloader.dataset)) #traindataのlossをグラフ描画のためにlistに保持
train_acc_value.append(float(sum_correct/sum_total)) #traindataのaccuracyをグラフ描画のためにlistに保持
sum_loss = 0.0
sum_correct = 0
sum_total = 0
#test dataを使ってテストをする
for (inputs, labels) in testloader:
inputs, labels = inputs.to(device), labels.to(device)
optimizer.zero_grad()
outputs = net(inputs)
loss = criterion(outputs, labels)
sum_loss += loss.item()
_, predicted = outputs.max(1)
sum_total += labels.size(0)
sum_correct += (predicted == labels).sum().item()
print("test mean loss={}, accuracy={}"
.format(sum_loss*BATCH_SIZE/len(testloader.dataset), float(sum_correct/sum_total)))
test_loss_value.append(sum_loss*BATCH_SIZE/len(testloader.dataset))
test_acc_value.append(float(sum_correct/sum_total))
plt.figure(figsize=(6,6)) #グラフ描画用
#以下グラフ描画
plt.plot(range(EPOCH), train_loss_value)
plt.plot(range(EPOCH), test_loss_value, c='#00ff00')
plt.xlim(0, EPOCH)
plt.ylim(0, 2.5)
plt.xlabel('EPOCH')
plt.ylabel('LOSS')
plt.legend(['train loss', 'test loss'])
plt.title('loss')
plt.savefig("loss_image.png")
plt.clf()
plt.plot(range(EPOCH), train_acc_value)
plt.plot(range(EPOCH), test_acc_value, c='#00ff00')
plt.xlim(0, EPOCH)
plt.ylim(0, 1)
plt.xlabel('EPOCH')
plt.ylabel('ACCURACY')
plt.legend(['train acc', 'test acc'])
plt.title('accuracy')
plt.savefig("accuracy_image.png")
学習をするループ中とその前後に色々なものが追加されているが,それぞれグラフを出力したり実行中の結果を見たりするためのものである.
また,test時にtestloaderのみでなくtrainloaderも使用してtestを行っている(trainloaderが2回使われているが2つ目では更新がない).
どういうことかというと,学習時に使用した画像をもう一度ランダムに使用して制度がどれくらい出るかを見ているのである.
もちろんこのことからtestloaderの中の画像は学習では使用していない未知のデータという扱いである.
ところどころにある「xxx.item()」というのはTensor型のdataから通常のintやdoubleの数字として取り出している.
実際にグラフの結果を見ると以下のようなっている.
lossが下がるにつれてaccuracy(分類精度)が向上しているのがわかる.
このネットワークで今回使ったパラメータだとMNISTでは分類精度97%くらいは出ることがわかった.
6. ひとこと
今回はpyTorchを使用してCNNsを作成し,疑問に思ったことや少しずつ理解していく過程で分かったことなどを踏まえて徹底解説とさせて頂いた.
読みづらい点も多かったと思うが読んでいただきありがとうございます.