マイブログ リスト

医療言語処理講座

2019年2月25日月曜日

Pyゼミ3.07 ChainerでGPUを使って学習してみる

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点の修正を加えます。

  1. numpyの代わりにcupyを使う
  2. modelをGPUに対応させる
  3. 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が使えることがわかりました。
  1. numpyの代わりにcupyを使う
  2. modelをGPUに対応させる
  3. UpdaterをGPUに対応させる
まだ,GPU未導入の方はぜひ導入を検討するとよいでしょう。

また,使っていない古い読影端末やワークステーションにGPU搭載しているものがあります。
とりあえずこれを流用する方法もあると思います。

0 件のコメント:

コメントを投稿