GPUによる高速な学習を試してみる。
keyword: Chainer, GPU, cupy
大量のデータを使って学習するにはGPU(Graphics Processing Uni)は必須です。
CPUの演算より約10倍も早くなります。
いろいろパラメータやデータなどを変えて実験するにはGPUはなくてはならないハードウェアです。
これまで作成したAIのプログラム(Pyゼミ3.05など)の修正点について説明します。
GPUを使うための環境の構築については説明しません。
環境の構築はインターネット上にたくさんの情報がりますので,OS,CUDAやその他のドライバーなどのバージョンを確認しながら構築してください。
前回Pyゼミ3.05のプログラムをGPU対応にする
ChainerのプログラムをGPU対応するには次の3点の修正を加えます。
- numpyの代わりにcupyを使う
- modelをGPUに対応させる
- UpdaterをGPUに対応させる
これだけです。それぞれについて具体的に説明します。
1. numpyの代わりにcupyを使う
cupyはGPUで演算させるためのライブラリで,行列演算などを効率的に行うnumpyと互換性を持っています。つまり,numpyで記述しているプログラムをcupyに書き換えればよいわけです。
numpyは次のようにnpという名前でインポートしています。
import numpy as np
また,GPUの使用,未使用をスイッチするために,uses_device変数を初期化します。
未使用のときはー1を,使用時は0以上の値(通常は0)を設定します。
uses_device = 0 # GPU 0〜、CPU -1
さて,GPU使用時にnumpyをcupyに書き換える方法ですが,
次のように,GPU使用時(>=0)に,cupyをcpとしてインポートし,
npにcpを代入するだけで,プログラムはGPU用に書き換えられました。
if uses_device >= 0:
import cupy as cp
np = cp
2. modelをGPUに対応させる
ニューラルネットワークのモデルをいつも通りに生成します。model = Alexnet(nlbl)
このモデルをGPUに対応させます。
GPU使用時(>=0)には以下の行を実行し,モデルがGPUを使って演算できるようにします。
if uses_device >= 0:
chainer.cuda.get_device_from_id(0).use()
chainer.cuda.check_cuda_available()
model.to_gpu()
3. UpdaterをGPUに対応させる
パラメータ更新を担当するUpdaterをGPUに対応させます。
引数にdevice=uses_deviceを追加するだけです。
updater = training.StandardUpdater(t_iterator, self.opt, device=uses_device)
また,Pyゼミ3.05のプログラム中のEvaluatorも同様に引数にdevice=uses_deviceを追加します。
trainer.extend(extensions.Evaluator(v_iterator, self.model, device=uses_device))
以上,3つの追加修正によりGPUに対応したプログラムになります。
プログラムを入力して実行してみよう
このプログラムはPy3.05が基本です。これに3つの修正が加わっています。
dicomAlexnetTrainGPU.py
import os, sys import numpy as np import cv2 import chainer from chainer import Function, report, training, utils, Variable from chainer import datasets, iterators, optimizers, serializers from chainer import Link, Chain, ChainList import chainer.functions as F import chainer.links as L from chainer.datasets import tuple_dataset from chainer.training import extensions from chainer.datasets import split_dataset_random #new import pydicom uses_device = 0 # GPU 0〜、CPU -1 if uses_device >= 0: import cupy as cp np = cp def constractTrainData( fname , resz ): image = [] label = [] for i, line in enumerate(open(fname, 'r')): data = line.split(",") print("Train",i, data[0], "Label#",data[1]) img = readDicom2png(data[0], "tmp.png") if resz > 0: img = cv2.resize(img,(resz, resz)) img = img.astype(np.float32) img = (img - 128)/128 #画素値0-255を-1.0から+1.0に変換 image.append([img]) t = np.array(int(data[1]), dtype=np.int32) label.append(t) train = tuple_dataset.TupleDataset(image, label) return train def readDicom2png(dcmfnm, tmpfnm): ds = pydicom.read_file(dcmfnm) wc = ds.WindowCenter ww = ds.WindowWidth img = ds.pixel_array max = wc + ww / 2 min = wc - ww / 2 img = 255 * (img - min)/(max - min) img[img > 255] = 255 img[img < 0 ] = 0 img = img.astype(np.uint8) cv2.imwrite(tmpfnm, img) return img def getLabelName(fname): lblnm = [] for line in open(fname, 'r'): data = line.split(",") lblnm.append(data[1].strip()) #lblnm[int(data[0])]= data[1] print("LabelName:",lblnm) return len(lblnm), lblnm class Alexnet(Chain): ### AlexNet ### def __init__(self, n_label, n_channel=1): super(Alexnet, self).__init__( conv1 = L.Convolution2D(n_channel, 96, 11, stride=4), conv2 = L.Convolution2D(96, 256, 5, pad=2), conv3 = L.Convolution2D(256, 384, 3, pad=1), conv4 = L.Convolution2D(384, 384, 3, pad=1), conv5 = L.Convolution2D(384, 256, 3, pad=1), fc6 = L.Linear(None, 4096), fc7 = L.Linear(4096, 4096), fc8 = L.Linear(4096, n_label), ) def __call__(self, x): h = F.max_pooling_2d(F.local_response_normalization( F.relu(self.conv1(x))), 3, stride=2) h = F.max_pooling_2d(F.local_response_normalization( F.relu(self.conv2(h))), 3, stride=2) h = F.relu(self.conv3(h)) h = F.relu(self.conv4(h)) h = F.max_pooling_2d(F.relu(self.conv5(h)), 2, stride=2) h = F.dropout(F.relu(self.fc6(h))) h = F.dropout(F.relu(self.fc7(h))) return self.fc8(h) class Classifier(): def __init__(self, mdlnm, nlbl): model = Alexnet(nlbl) if uses_device >= 0: # GPUを使う chainer.cuda.get_device_from_id(0).use() chainer.cuda.check_cuda_available() model.to_gpu() # GPU用データ形式に変換 self.model = L.Classifier(model,lossfun=F.softmax_cross_entropy) self.opt = optimizers.Adam() self.opt.setup(self.model) self.modelName = mdlnm def doTrain(self, nLbl, train, batch_size, n_epoch, modelName): trn, vld = split_dataset_random(train, int(len(train)*0.8), seed=0) t_iterator = iterators.SerialIterator(trn, batch_size, shuffle=True) v_iterator = iterators.SerialIterator(vld, batch_size, repeat=False, shuffle=False) updater = training.StandardUpdater(t_iterator, self.opt, device=uses_device) #GPU trainer = training.Trainer(updater, (n_epoch, 'epoch'), out='result') trainer.extend(extensions.Evaluator(v_iterator, self.model, device=uses_device)) #GPU trainer.extend(extensions.LogReport()) trainer.extend(extensions.PrintReport(['epoch', 'main/loss', 'main/accuracy', 'validation/main/accuracy'])) trainer.extend(extensions.ProgressBar()) trainer.extend(extensions.PlotReport(['main/loss', 'validation/main/loss'], x_key='epoch', file_name='loss.png')) trainer.extend(extensions.PlotReport(['main/accuracy', 'validation/main/accuracy'], x_key='epoch', file_name='accuracy.png')) trainer.run() print("学習モデル",self.modelName,"を保存します... . .") chainer.serializers.save_hdf5( self.modelName, self.model ) print("\t... . .学習モデルを保存しました") if __name__=='__main__': trainfnm = "./TrainTestList/TrainData.0.3.csv" labelfnm = "./TrainTestList/labelName.csv" resize = 128 #画像リサイズ 0のときそのままの画素サイズ modelnm = "Alexnet.0.3.hdf5" #モデル保存名 batch_size = 10 # バッチサイズ n_epoch = 30 # エポック数 nLbl, lblName = getLabelName(labelfnm) train = constractTrainData(trainfnm, resize) print("\ntrain\n") print("画像データ数:",len(train),"画像/ラベル:",len(train[0])) print("画像Chanel数:",len(train[0][0]),"高さ:",len(train[0][0][0]),"幅:",len(train[0][0][0][0])) dl = Classifier(modelnm, nLbl) dl.doTrain(nLbl, train, batch_size, n_epoch, modelnm)
実行結果
プログラムは,実効時間を計測するためtimeコマンドを付けて実行します。
プログラムはラベルファイルを読み込みラベル名を表示し,DICOM画像を読み込みます。
学習がはじまり30回のエポック終了後に実行時間が表示されます。
timeコマンドは,プログラムの起動から終了までの時間をrealの値で示しています。
わたしのGefoece GTX 1060では0m29.636sでした。約30秒でした。
(実行環境:OS Ubuntu 16.04 LTS,CPU Core i5-6400 2.7GHz, メモリ 8GB,GPU GeForce GTX 1060 6GB)
$ time␣python␣dicomAlexnetTrainGPU.py⏎ LabelName: ['Lung', 'Abdomen', 'Chest', 'Head'] Train 0 ../dcmdir2/Lung/W5305890 Label# 0 Train 1 ../dcmdir2/Lung/K5303593 Label# 0 Train 2 ../dcmdir2/Lung/R5305640 Label# 0 Train 5 ../dcmdir2/Lung/D5304468 Label# 0 ・・・・・ Train 139 ../dcmdir2/Head/G5221187 Label# 3 Train 140 ../dcmdir2/Head/Q5221671 Label# 3 Train 141 ../dcmdir2/Head/E5221093 Label# 3 train 画像データ数: 142 画像/ラベル: 2 画像Chanel数: 1 高さ: 128 幅: 128 epoch main/loss main/accuracy validation/main/accuracy 1 1.8203 0.283333 0.481481 2 1.06403 0.545455 0.725926 3 0.741141 0.581818 0.759259 4 0.735347 0.7 0.725926 5 0.639062 0.581818 0.825926 ・・・・・ 28 0.00639354 1 1 29 0.0178032 0.990909 1 30 0.0548964 0.981818 0.933333 学習モデル Alexnet.0.3.hdf5 を保存します... . . ... . .学習モデルを保存しました real 0m29.636s user 0m31.316s sys 0m17.240s
GPU使用・未使用の演算時間の比較
GPU使用時では約30秒の実行時間でしたが,
uses_device = -1 にして,GPUを使用しないで実行時間を計測すると
6分18秒でした。
その差は約12倍です。
明らかに実行速度が向上しています。
real 6m18.688s user 9m16.004s sys 8m6.400s
GPUの稼働状況はNVIDIAのツールで確認できます。
引数はハイフン,エルです。
$ nvidia-smi␣-l⏎
中段の右にGPU-Utilが96%と表示され,GPUがフル稼働しているのが分かります。
まとめ
今回はGPUに対応したプログラムの作り方について説明しました。
次の3つについて修正するだけで簡単にGPUが使えることがわかりました。
- numpyの代わりにcupyを使う
- modelをGPUに対応させる
- UpdaterをGPUに対応させる
また,使っていない古い読影端末やワークステーションにGPU搭載しているものがあります。
とりあえずこれを流用する方法もあると思います。
0 件のコメント:
コメントを投稿