20190502のDeepLearningに関する記事は9件です。

TensorRT 5.0.6とJETSON NANOで推論の高速化

TensorRT 5.0.6とJETSON NANOで推論の高速化

これは何?

GPU上でのDeep Learningの推論処理の高速化に用いられるライブラリTensorRTを用いて、NVIDIA Jetson Nano上での推論の高速化を図る。画像認識を対象とし、PyTorchを用いた場合との推論速度を比較する。

動機

  • Jetson Nanoを手に入れたので、何か試したい
  • 以前、所属している会社のアドベントカレンダーで、TensorRT 5.0という記事を書かせていただきました。この際、TensorRTがうまいこと使えずONNXのモデルを読み込むのを断念したりしたのですが、その後TensorRTもマイナーアップデートが行われたようなので、使い勝手を確認したい
    • Jetson上でもPython APIが使えるようになった
    • ONNXモデルを読み込む際の対応レイヤが増えた?

モデルの変換の流れ

Pytorchを用いて画像認識もDeep Learningモデルを学習することを想定して、PytorchのモデルをTensorRTを用いて推論できるように変換していきます。ただしONNX, TensorRTで対応していないレイヤがある場合など、以降の変換がうまくいかないモデルもあります。

  1. PyTorch : 人気上昇中のDeep Learningフレームワーク。Define by runで動的な計算グラフも簡単に書ける。
  2. ONNX: Deep Learningフレームワーク間でモデルの交換するための共通フォーマットを目指している
  3. TensorRT: GPUベンダであるNVIDIAが提供している、GPUでDeep Learningの推論を高速化するためのライブラリ。浮動小数点精度を下げる(FP32->FP16->int8)、グラフの構造を解析してGPUでの処理を高速化するなどを行ってくれる。

環境: NVIDIA Jetson Nano

$99で買える、GPU搭載のエッジ向けのシングルボードコンピュータ。ラズパイみたいに小さく手軽なデバイスで、簡単にDeep Learningの推論ができます。
公式のセットアップ手順に従ってセットアップします。

ソフトウェアは以下の通り。

  • Jetpack 4.2
    • python 3.6
      • TensorRT==5.0.6
      • Pytorch==1.1.0
      • torchvision==0.2.1
        • こちらの情報から、0.2.2だとvgg, alexnetのonnx化に失敗するようなので、0.2.1を利用しています。

その他の依存パッケージは以下の手順でインストール

sudo apt-get install ptyhon3-dev
# dependencies for Onnx
sudo apt-get install protobuf-compiler libprotoc-dev
# dependencies for Pillow
sudo apt-get install libtiff5-dev libjpeg8-dev zlib1g-dev \
    libfreetype6-dev liblcms2-dev libwebp-dev libharfbuzz-dev libfribidi-dev \
    tcl8.6-dev tk8.6-dev python-tk

vim ~/.bashrc
# 最後に以下を追加
# export PATH=${PATH}:/usr/local/cuda-10.0/bin
# export LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:/usr/local/cuda-10.0/lib64
source ~/.bashrc

sudo apt install python3-pip
pip3 install --user -r /usr/src/tensorrt/samples/python/yolov3_onnx/requirements.txt


# pytorchのインストール(https://devtalk.nvidia.com/default/topic/1049071/pytorch-for-jetson-nano/)
wget https://nvidia.box.com/shared/static/veo87trfaawj5pfwuqvhl6mzc5b55fbj.whl -O torch-1.1.0a0+b457266-cp36-cp36m-linux_aarch64.whl
pip3 install --user numpy torch-1.1.0a0+b457266-cp36-cp36m-linux_aarch64.whl

sudo apt-get install -y git
git clone https://github.com/pytorch/vision
cd vision
git checkout v0.2.1
pip3 install --user -e ./

実験

以下の順番でJetson NANO上で作業します。

  1. PyTorchでのモデルの読み込みおよび速度計測
  2. ONNX形式でのモデルの保存
  3. ONNXモデルのTensorRTへの読み込み、保存
  4. TensorRTを用いた推論および速度計測

1. PyTorchでのモデルの読み込みおよび速度計測

PyTorch上で提供されている学習済みモデルを用います。自作の学習モデルがある場合は、以降のmodelを自作のモデルに置き換えてください。

今回、テスト用画像はこちらから猫の画像をお借りしました。

step1.py
import time

import torch
from torch.autograd import Variable
from torchvision import models, transforms
from PIL import Image
device = torch.device('cuda')

# 学習済みモデルの読み込み
resnet18 = models.resnet18(pretrained=True)
resnet18 = resnet18.to(device)
resnet18.eval()

# 画像データの読み込みおよび前処理

image = Image.open('cat.jpg')
transformation = transforms.Compose(
    [
        transforms.Resize([224, 224]),
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ]
)
image_tensor = transformation(image).float()
image_tensor = image_tensor.unsqueeze_(0)
input_var = Variable(image_tensor)

# 推論用関数
def predict_image(model, var):
    input = var.to(device)
    start = time.time()
    output = model(input)
    forward_time = time.time()-start
    return output.cpu().data.numpy(), forward_time

#jitを使う場合はコメントアウト
#resnet18 = torch.jit.trace(resnet18, torch.rand(1, 3, 224, 224).to(device))

with open('pytorch.csv', 'w') as fout:
    for idx in range(20):
        start = time.time()
        result, forward_time = predict_image(resnet18, input_var)
        fout.write(','.join(map(str, [idx, forward_time, time.time()-start]))+'\n')
print('Prediction: {}(Score: {})'.format(result.argmax(), result.max()))

推論の結果、カテゴリIDは281(スコア9.820850372314453)が得られました。カテゴリ281は'tabby, tabby cat'なので、妥当な推論結果です。処理速度はtextに書き出しているので、あとで集計します。

2. ONNX形式でのモデルの保存

以下の通り実行することで、resnet18.onnxというモデルファイルが保存されます。input_namesおよびoutput_namesはinputの変数とoutputの変数がわかりやすいよう、可読性を上げるために任意でつけます。

また、クラスを継承してforward関数を再定義していますが、これはforward関数のviewの変換後のshapeをハードコードするためです。元々はxのバッチサイズを取ってきて、(x.size(0), -1)の形状にflattenする処理になっているのですが、バッチサイズを取ってくるx.sizeの処理が入ると、onnxに変換した際にgather layerを使う形で保存されてしまいます。tensorrt5.0.6だとGatherがサポートされていないため、変更が必要となりました。

step2.py
import torch
from torch.autograd import Variable
from torchvision import models
from PIL import Image

class CustomModel(models.ResNet):
    def forward(self, x):
        x = self.conv1(x)
        x = self.bn1(x)
        x = self.relu(x)
        x = self.maxpool(x)

        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)

        x = self.avgpool(x)
        x = x.view(-1, 512) # batch_size, channel
        x = self.fc(x)

        return x

device = torch.device('cuda')

# 学習済みモデルの読み込み
resnet18 = CustomModel(models.resnet.BasicBlock, [2, 2, 2, 2])
resnet18.load_state_dict(
        torch.utils.model_zoo.load_url(
        models.resnet.model_urls['resnet18']
    )
)
resnet18 = resnet18.to(device)

# 画像データの読み込みおよび前処理

dummy_input = torch.randn(1, 3, 224, 224, device=device)
input_names = [ "actual_input_1" ]
output_names = [ "output1" ]

torch.onnx.export(
    resnet18, dummy_input, "resnet18.onnx", verbose=True,
    input_names=input_names, output_names=output_names
)

3. ONNXモデルのTensorRTへの読み込み、保存

以下の手順に従って、ONNXのモデルをTensorRTのengineに変換し、保存します。

import tensorrt as trt
TRT_LOGGER = trt.Logger(trt.Logger.INFO)

def build_engine(onnx_model_path, engine_path):                                                
    with trt.Builder(TRT_LOGGER) as builder, builder.create_network() as network, trt.OnnxParser(network, TRT_LOGGER) as parser:
        builder.max_workspace_size = 1 << 30 # 1GB
        builder.max_batch_size = 1
        #fp16を用いる場合はコメントを外す 
        #builder.fp16_mode = True
        with open(onnx_model_path, 'rb') as model:
               parser.parse(model.read())
    if parser.num_errors > 0:
            print(parser.get_error(0).desc())
            raise Exception
            engine = builder.build_cuda_engine(network)
        with open(engine_path, "wb") as f:
            f.write(engine.serialize())

build_engine('resnet18.onnx', 'resnet18.engine')

ここで保存したエンジンをplan fileと呼びます。上記build処理は処理に時間がかかるため、通常推論をする際には、毎回buildをするのではなく、このplan fileを読み込んでengineとして使います。plan fileはエンジンのbuildを行ったGPUおよびTensorRTのバージョンに特化しているので、他のGPUを搭載したマシン上で使いたい場合などは、当該マシン上で再度engineを作成し、plan fileを保存する必要があります。

例)Jetson Nano -> Tesla V100のようにplan fileを使いまわすことはできません。

また、fp16_modeをオンにすると、buildの時間がめちゃくちゃ伸びます。

4. TensorRTを用いた推論および速度計測

plan fileからエンジンを読み込み、推論を行います。

step4.py
import time
import tensorrt as trt

TRT_LOGGER = trt.Logger(trt.Logger.INFO)

import pycuda.driver as cuda
import pycuda.autoinit

def load_engine(engine_path):    with open(engine_path, "rb") as f, trt.Runtime(TRT_LOGGER) as runtime:
        return runtime.deserialize_cuda_engine(f.read())

# 後述するcommonモジュールの推論用の関数を、推論時間が測定できるように修正
def do_inference(context, bindings, inputs, outputs, stream, batch_size=1):
    [cuda.memcpy_htod_async(inp.device, inp.host, stream) for inp in inputs]
    start = time.time()
    context.execute_async(
        batch_size=batch_size, bindings=bindings, stream_handle=stream.handle
    )
    infer_time = time.time() - start
    [cuda.memcpy_dtoh_async(out.host, out.device, stream) for out in outputs]
    stream.synchronize()
    return [out.host for out in outputs], infer_time

import sys
sys.path.append('/usr/src/tensorrt/samples/python/')
#tensorrtをインストールすると手に入るpythonのサンプルコード集
import common #推論用の共通関数

from PIL import Image
from torchvision import transforms
image = Image.open('cat.jpg')
transformation = transforms.Compose(
    [
        transforms.Resize([224, 224]),
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ]
)
image_tensor = transformation(image).float()
image = image_tensor.numpy()

inference_times = []
with load_engine('resnet18.engine') as engine, engine.create_execution_context() as context:
    inputs, outputs, bindings, stream = common.allocate_buffers(engine)
    inputs[0].host = image
    for idx in range(20):
        start = time.time()
        trt_outputs, infer_time = do_inference(
            context, bindings=bindings, inputs=inputs, outputs=outputs, stream=stream
        )
        inference_times.append(list(map(str, [idx, infer_time, time.time()-start])))
trt_outputs = trt_outputs[0].reshape((1, 1000))
print('Prediction: {}(Score: {})'.format(trt_outputs.argmax(), trt_outputs.max()))
with open('trt.csv', 'w') as fout:
    for l in inference_times:
        fout.write(','.join(l)+'\n')

推論の結果は、カテゴリIDは281(スコア9.820852279663086)が得られました。スコアが若干変わっていますが、pytorchのサイトほぼ同じ結果が得られました。

fp16での推論も試したところ、カテゴリIDは281(スコア9.8203125)となりました。こちらもほぼ同じ結果となったといえると思います。

処理速度比較

推論時間

infer.png

pytorchでは、forwardにかかる時間は、jitなしでは20ms、jitありでは15ms程度でしたが、tensorrtを使うことで、4-6ms程度まで減らすことができました(fp16にした際に速くなっていない原因は不明)。

トータルでの処理時間

total.png

一方で、トータルでの処理時間はで比べると、pytorchのjitとtrt_fp32で大幅な速度向上は見られませんでした。入力・出力をCPUのメモリ空間-GPUのメモリ空間で移動する部分のオーバーヘッドが大きいのではないかと思います(fp16で分散がとても大きくなっているのは、fp32->fp16への変換処理も入るから?)。画像のデコードまでcudaを用いて行う、後処理もGPU側で行うと言った方式にすることで、CPU-GPU間でのコピーを減らす事が重要になってきそうです。

結論

PyTorchのモデルをTensorRT化し、速度向上することを確認できました。これで自作のモデルのTensorRT化を試せそうです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ElixirでDeep Learningに再挑戦 (8) 並列動作

はじめに

行列計算の一部に並列動作を取り入れました。

ElixirでDeep Learningに再挑戦 (1)
|> ElixirでDeep Learningに再挑戦 (2)
|> ElixirでDeep Learningに再挑戦 (3)
|> ElixirでDeep Learningに再挑戦 (4) 誤差逆伝播法
|> ElixirでDeep Learningに再挑戦 (5) 畳み込み
|> ElixirでDeep Learningに再挑戦 (6)勾配確認 バックプロパゲーション完全動作
|> ElixirでDeep Learningに再挑戦 (7)MNIST

行列積の並列動作

MNISTデータは入力層は784次元あります。784次元の行ベクトルと784*50の行列積は計算時間がかかっているものと予想し、この部分を並列動作に変更しました。spawnを使う方式です。以前、試してみた行列積の並列動作を改造して行ベクトルと行列との積に並列動作させるようにしたものです。

参考 https://qiita.com/sym_num/items/6517bba0b3f18bc34f14

行ベクトルが100次元以上の場合に10分割して並列動作するようにしました。

結果

(7)のMNISTでの学習を100回繰り返すことを比較しました。

通常
CPU稼働率38%前後

7 7
"time: 394588266 micro second"
"-------------"
true

並列
CPU稼働率45%

4 4
"time: 373573155 micro second"
"-------------"
true

考察

21秒ほど、時間短縮になっています。MNISTの学習ではデータをファイルから読み込む動作などに多くの時間がとられているので大きな差にはなりませんでした。

CPUの稼働率がそれほど上昇しないことからみて、プロセスに渡す計算量が小さいのだろうと思います。計算量の適切な分割ができていないのだろうと思います。

まだまだ改善の余地があります。

改良案

1つのデータについて勾配計算をすることを1つのプロセスに割り当てたら計算量はちょうど良くなるように思います。これなら数百プロセスが一斉に動き出すことになるはずです。それならマルチコアのメリットが活きるように予想しています。
追記 この方法ではだめですね。勾配は直前のネットワークに依存しています。却下

追記 バッチ処理による行列計算の手法が既にありました。文献1だとp50から、文献2だとp151からです。これによりElixirの並列機能が十分に活用できると思います。

参考文献

1「深層学習」 岡谷貴之 著 
2「ゼロから作る Deep Learning」 斉藤 康毅 著
3「Excelでわかるディープラーニング 超入門」 涌井良幸 涌井貞美 著

全コード

GitHubにおいてあります。
https://github.com/sasagawa888/DeepLearning

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ChainerでOctave Convolutionを実装してみた

動機

トレンドに上がっていたOctave Convolutionがなんだかよさそうだったのですが、Keras実装だったのでChainerしか知らない僕にはちょっと不便です。なので自分で実装してみることにしました。
「間違ってるよ」とか「ここはこう書くといいよ」的なアドバイスよろしくお願いします。

実装

from chainer import Chain
import chainer.functions as F
import chainer.links as L
import numpy as np


class OctConv(Chain):

    def __init__(self, in_ch, out_ch, ksize, stride, pad, alpha0=0.25, alpha1=0.25):
        super().__init__()
        with self.init_scope():
            self.h2h = L.Convolution2D(int(in_ch * (1 - alpha0)), int(out_ch * (1 - alpha1)), ksize, stride, pad)
            self.h2l = L.Convolution2D(int(in_ch * (1 - alpha0)), int(out_ch * alpha1), ksize, stride, pad)
            self.l2h = L.Convolution2D(int(in_ch * alpha0), int(out_ch * (1 - alpha1)), ksize, stride, pad)
            self.l2l = L.Convolution2D(int(in_ch * alpha0), int(out_ch * alpha1), ksize, stride, pad)

    def __call__(self, high, low):
        _, _, H, W = high.shape
        h2h = self.h2h(high)
        h2l = self.h2l(F.average_pooling_2d(high, ksize=2))
        l2h = F.unpooling_2d(self.l2h(low), ksize=2, outsize=(H, W))
        l2l = self.l2l(low)

        h = h2h + l2h
        l = h2l + l2l
        return h, l

まだこのコードで実験していないので、後で追記します。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【EC2】Deep Learning AMI (Ubuntu) Version 22.0のインスタンスを立てる際の注意点【ボリューム】

はじめに

EC2のDeep Learning AMI (Ubuntu)を使って開発をすることが多くなり、詰まったところをメモ程度に残します。

デフォルトの75GBでは絶対に容量が足りない

Filesystem     Type     1K-blocks     Used Available Use% Mounted on
udev           devtmpfs    499264        0    499264   0% /dev
tmpfs          tmpfs       101444     3324     98120   4% /run
/dev/xvda1     ext4      76171508 75792680    362444 100% /
tmpfs          tmpfs       507212        0    507212   0% /dev/shm
tmpfs          tmpfs         5120        0      5120   0% /run/lock
tmpfs          tmpfs       507212        0    507212   0% /sys/fs/cgroup
/dev/loop1     squashfs     18304    18304         0 100% /snap/amazon-ssm-agent/1068
/dev/loop0     squashfs     16896    16896         0 100% /snap/amazon-ssm-agent/784
/dev/loop2     squashfs     93312    93312         0 100% /snap/core/6531
/dev/loop3     squashfs     91648    91648         0 100% /snap/core/6818
/dev/loop4     squashfs     89984    89984         0 100% /snap/core/5742
tmpfs          tmpfs       101444        0    101444   0% /run/user/1000

/dev/xvda1がメインのボリュームですが、インスタンスを建てたときにすでに容量がほぼいっぱいになっています。

$ conda create -n new_env --clone pytorch_p36

などで新しい仮想環境をつくっただけでも途中で容量がいっぱいになります。

インスタンスを立てる際のボリューム設定

スクリーンショット 2019-05-02 12.27.00.png

ここがデフォルトでは75GBになっているので、適宜必要な値を入力します。自分は100GBで作成しました。

インスタンスを建ててしまった後の対応

スクリーンショット 2019-05-02 12.35.02.png
サイドバーのボリュームを選択後にインスタンスを選び、アクションからボリュームの変更でサイズを変更します。

$ lsblk

で確認しても反映されていないことが確認されます。

NAME    MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
xvda    202:0    0  100G  0 disk 
└─xvda1 202:1    0   75G  0 part /
loop0     7:0    0 16.5M  1 loop /snap/amazon-ssm-agent/784
loop1     7:1    0 17.9M  1 loop /snap/amazon-ssm-agent/1068
loop2     7:2    0 91.1M  1 loop /snap/core/6531
loop3     7:3    0 89.4M  1 loop /snap/core/6818
loop4     7:4    0 87.9M  1 loop /snap/core/5742

以下のコマンドを打ちます。

$ sudo growpart /dev/xvda 1

するとボリュームのパーティションが変更されます。

NAME    MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
xvda    202:0    0  100G  0 disk 
└─xvda1 202:1    0  100G  0 part /
loop0     7:0    0 16.5M  1 loop /snap/amazon-ssm-agent/784
loop1     7:1    0 17.9M  1 loop /snap/amazon-ssm-agent/1068
loop2     7:2    0 91.1M  1 loop /snap/core/6531
loop3     7:3    0 89.4M  1 loop /snap/core/6818
loop4     7:4    0 87.9M  1 loop /snap/core/5742

次にファイルシステムに拡張になります。

$ df -h

で確認すると、まだ75GBのままです。

Filesystem      Size  Used Avail Use% Mounted on
udev            488M     0  488M   0% /dev
tmpfs           100M  3.3M   96M   4% /run
/dev/xvda1       73G   73G     0 100% /
tmpfs           496M     0  496M   0% /dev/shm
tmpfs           5.0M     0  5.0M   0% /run/lock
tmpfs           496M     0  496M   0% /sys/fs/cgroup
/dev/loop1       18M   18M     0 100% /snap/amazon-ssm-agent/1068
/dev/loop0       17M   17M     0 100% /snap/amazon-ssm-agent/784
/dev/loop2       92M   92M     0 100% /snap/core/6531
/dev/loop3       90M   90M     0 100% /snap/core/6818
/dev/loop4       88M   88M     0 100% /snap/core/5742
tmpfs           100M     0  100M   0% /run/user/1000

そこで以下のコマンドでファイルの拡張を行います。

$ sudo resize2fs /dev/xvda1

もう一度確認すると100GBになっているはずです。

Filesystem      Size  Used Avail Use% Mounted on
udev            488M     0  488M   0% /dev
tmpfs           100M  3.3M   96M   4% /run
/dev/xvda1       97G   73G   25G  75% /
tmpfs           496M     0  496M   0% /dev/shm
tmpfs           5.0M     0  5.0M   0% /run/lock
tmpfs           496M     0  496M   0% /sys/fs/cgroup
/dev/loop1       18M   18M     0 100% /snap/amazon-ssm-agent/1068
/dev/loop0       17M   17M     0 100% /snap/amazon-ssm-agent/784
/dev/loop2       92M   92M     0 100% /snap/core/6531
/dev/loop3       90M   90M     0 100% /snap/core/6818
/dev/loop4       88M   88M     0 100% /snap/core/5742
tmpfs           100M     0  100M   0% /run/user/1000

これで仮想環境作っても容量不足になりません。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ElixirでDeep Learningに再挑戦 (7)MNIST

はじめに

前回(6)で12画素の小規模なデータなら学習するところまでになりました。今度はMNISTデータで計算実験をしました。

ElixirでDeep Learningに再挑戦 (1)
|> ElixirでDeep Learningに再挑戦 (2)
|> ElixirでDeep Learningに再挑戦 (3)
|> ElixirでDeep Learningに再挑戦 (4) 誤差逆伝播法
|> ElixirでDeep Learningに再挑戦 (5) 畳み込み
|> ElixirでDeep Learningに再挑戦 (6)勾配確認 バックプロパゲーション完全動作

設定

バックプロパゲーションの実装は前回のままです。当初、初期値の設定がまずくてなかなか損失が小さくなりませんでした。文献2を参考にして初期値を次のように設定したところ、まあまあのところまで損失を小さくさせることができました。

784 -> 50 -> 100 -> 10 隠れ層、2つのネットワーク

初期値 Box-Muller法によりガウス分布の乱数を生成、これに0.1倍したものを重み行列の初期値としています。

活性化関数 シグモイド関数

学習率 入力層側より1.2、1.1、1.0 

損失関数 2乗和

MNISTの訓練データから100個を乱数で選択し、これを200回学習させました。

その後testデータから10個を乱数で選択し、推論した値と、正しい値とを表示しています。

iex(1)> DL.mnist(200)
count error
途中略
3 0.008468322764363476
2 0.008419139167818454
1 0.00837050909066567
predict correct
9 9
6 6
2 2
9 8
9 7
0 0
9 4
3 3
8 8
4 4
true

10個のうち7個が正解しています。

1000回の学習も動作しました。損失は十分に小さくなったのですが、推論の正解確率はまだまだです。Elixirは十分に耐えています。

3 0.0013986892976679487
2 0.0013972051584290142
1 0.0013957240954059055
predict correct

6 6
7 5
7 7
0 0
7 5
8 8
8 0
4 4
4 4
2 2
true

Elixirでの所要時間など

Elixirは関数型言語でありイミュータブルです。破壊的代入が使えません。そこで勾配から重み行列を更新する都度新たに行列を生成しています。このため当初、メモリ不足になるのではないか? あるいはGC(ガベージコレクション)が多発するのではないかと懸念していました。Elixirは十分に頑健でありその心配はありませんでした。200回の学習で10分ちょっとくらいです。

現在、行列積の計算は通常の逐次計算のものを使っています。入力層では784次元の行ベクトルと784*50の行列との積を計算するために時間がかかっていると思います。ここを並列処理にするとおよそ3.2倍程度高速化する見込みです。pmatrixという並列積の実験的なモジュールを作りicore5のマシンでおよそ3.2倍ほど性能が向上することを確認しています。

どうやらElixirでもDLは実用的に使えることがわかってきました。

主要部分のコード

  def mnist(n) do
    image = MNIST.train_image()
    label = MNIST.train_label()
    network = Test.init_network1()
    seq = rand_sequence(100,length(image))
    IO.puts("count error")
    network1 = batch(network,image,label,100,n,seq)
    test_image = MNIST.test_image()
    test_label = MNIST.test_label()
    seq1 = rand_sequence(10,length(test_image))
    IO.puts("predict correct")
    mnist1(network1,test_image,test_label,0,seq1)
  end
  # print predict of test data
  def mnist1(_,_,_,_,[]) do true end
  def mnist1(network,[image|irest],[label|lrest],n,[n|srest]) do
    print(MNIST.onehot_to_num(forward(network,MNIST.normalize(image,255))))
    IO.write(" ")
    print(label)
    newline()
    mnist1(network,irest,lrest,n+1,srest)
  end
  def mnist1(network,[_|irest],[_|lrest],n,[s|srest]) do
    mnist1(network,irest,lrest,n+1,[s|srest])
  end

  def batch(network,_,_,_,0,_) do network end
  def batch(network,image,label,n,c,seq) do
    {network1,error} = batch1(network,image,label,0,seq,0)
    print(c)
    IO.write(" ")
    print(error)
    newline()
    batch(network1,image,label,n,c-1,seq)
  end

  def batch1(network,_,_,_,[],error) do
    {network,error}
  end
  def batch1(network,[image|irest],[label|lrest],n,[n|srest],error) do
    x = MNIST.normalize(image,255)
    t = MNIST.to_onehot(label)
    network1 = gradient(network,x,t)
    network2 = learning(network,network1)
    x1 = forward(network2,x)
    error1 = mean_square(x1,t)
    batch1(network2,irest,lrest,n+1,srest,error1+error)
  end
  def batch1(network,[_|irest],[_|lrest],n,[seq|srest],error) do
    batch1(network,irest,lrest,n+1,[seq|srest],error)
  end


展望

並列計算による学習時間短縮に取り組む予定です。

参考文献

1「深層学習」 岡谷貴之 著 
2「ゼロから作る Deep Learning」 斉藤 康毅 著
3「Excelでわかるディープラーニング 超入門」 涌井良幸 涌井貞美 著

全コード

GitHubにおいてあります。
https://github.com/sasagawa888/DeepLearning

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Kerasを勉強した後にPyTorchを勉強して躓いたこと

概要

DeppLearningのフレームワークで最初にKerasを勉強した後に、Define by RunのPyTorch勉強してみて躓いたポイントをまとめてみる。

この記事の対象読者

Kerasの次にPyTorchを勉強してみようと思っている人。

はじめに

今回いくつか挙げている躓いたポイントはPyTorchに限らないものがある。またKerasといえばバックエンドはTensorFlowのものを指す。バックエンドがTensorFlowでない場合は話が当てはまらないものもあるので注意。

今回挙げたポイントは以下の5つ
1. Channel First
2. GPUへの転送
3. CrossEntropyがSoftmax+CrossEntropyになっている
4. CrossEntropyがone-hot-vectorに対応していない
5. 学習と評価を区別する

以下、各ポイントの詳細について説明していく。

Channel First

PyTorchではモデルの入力と出力がChannel Firstの形式になっている。Channel Firstとは画像の次元の並びが(C, H, W)のようにChannelの次元が最初になっていること。
KerasではChannel Lastになっているため、(H, W, C)のようにChannelの次元が最後にくる。

実際にモデルに入力するときは、バッチサイズも合わせた4次元で表現する必要があるため、
PyTorch:(N, C, H, W)
Keras:(N, H, W, C)
となる。

記号の意味は
N:バッチサイズ
C:チャネル数
H:画像のHeight
W:画像のWidth

画像を読み込む際は、OpenCVかPILを使用する場合が多いが、これらのモジュールはChannel Lastで画像を扱う仕様になっている。なので、PyTorchのモデルに入力する前に以下のコードのようにChannel Firstに変換する必要がある。

img = cv2.imread(img_path)
img = img.transpose((2, 0, 1)) # H x W x C -> C x H x W

モデルの出力もChannel Firstなのでmatplotlibなどで表示したい場合はChannel Lastに変換してから表示する。

output = output.numpy() # tensor -> ndarray
output = output.transpose(1, 2, 0) # C x H x W -> H x W x C

GPUへの転送

KerasではGPUを使う場合、GPU側のメモリを意識することがなかったが、PyTorchではGPUを使用する場合、明示的に学習するパラメータや入力データをGPU側のメモリに転送しなければならない。
以下のコードではモデルと入力データをGPUに転送している。

device = torch.device("cuda:0")
# modelはnn.Moduleを継承したクラス
model = model.to(device) # GPUへ転送



for imgs, labels in train_loader:
    imgs, labels = imgs.to(device), labels.to(device) # GPUへ転送

GPU上にあるデータCPUに転送したい場合も以下のようにコードを書く必要がある。

device = torch.device("cpu")
model.to(device)

CrossEntropyがSoftmax+CrossEntropyになっている

Kerasで多クラスの識別モデルを学習するときは、モデルの最終層でsoftmaxを実行してからcategorical_crossentropyでロスを計算する流れになっている。
一方PyTorchではロス関数であるtorch.nn.CrossEntropyの中でSoftmaxの計算も一緒に行っているので、モデルの最終層でSoftmaxは不要になる。

たまにPyTorchのサンプルコードで最終層にtorch.nn.LogSoftmaxを置いて、ロス関数にtorch.nn.NLLLossを指定している場合がある。これは最終層を恒等関数にしてtorch.nn.CrossEntropyを使っているのと同じになる。
つまり、
torch.nn.CrossEntropy=torch.nn.LogSoftmaxtorch.nn.NLLLoss
という関係になっている。

torch.nn.LogSoftmaxは名前の通りSoftmaxの計算にLogをかぶせたものになっている。

LogSoftmax=log(\frac{e^{xj}}{\sum_{i=1}^{n} e^{xi}})

このLogはCrossEntropyの式にあるLogを持ってきているのだが、LogとSoftmaxを先に一緒に計算しておくことで、計算結果を安定させている。
なぜLog+Softmaxが計算的に安定するかは以下のページで解説されている。
Tricks of the Trade: LogSumExp

ちなみにtorch.nn.NLLLossはCrossEntropyのLogを抜いた他の計算を行っている。

CrossEntropyがone-hot-vectorに対応していない

Kerasではロスを計算するときに、labelはone-hot-vector形式で渡す必要があるがPyTorchでは正解の値をそのまま渡す。

例えば、3クラスの分類で正解が2番目のクラスの場合、Kerasでは[0, 1, 0]というリストをロス関数に渡すが、PyTorchでは2という値を渡す。

学習と評価を区別する

PyTorchでは、モデルを動作させるときに学習中なのか評価中なのかを明示的にコードで示す必要がある。なぜこれが必要なのかは理由が2つある。

1.学習中と評価中に挙動が変わるレイヤーがあるから
2.学習中には必要で評価中には不必要な計算があるから

1は、DropOutやBatchNormalizationなどのことで、これらのレイヤーは学習中と評価中で動作が変わる。よって、コードでこれから動作するのが学習なのか評価なのかを知らせる必要がある。
具体的には以下のようなコードになる。

# modelはnn.Moduleを継承したクラス
model.train() # 学習モードに遷移
model.eval() # 評価モードに遷移

2の不必要な計算とは計算グラフを作ることである。学習中は計算グラフを作って、誤差逆伝播法で誤差を計算グラフ上に伝播させて重みを更新する必要がある。しかし、学習以外の処理ではこの計算グラフの構築が不要になるので「計算グラフを作りません」とコードで示す必要がある。
具体的にはwith torch.no_grad()を使う。

model.eval() # 評価モードに遷移
with torch.no_grad(): # この中では計算グラフは作らない
    output = model(imgs)

まとめ

そもそもDefine and RunとDefine by Runで根本の思想が違うのでこれまでに挙げてきたポイントの他に色々と大きな違いがあるのだが、今回は個人的にコードを書く上でついつい忘れがちなところを挙げてみた。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

機械学習論文読みメモ_165

Tracking Emerges by Colorizing Videos
ラベルなしビデオを教師なし学習する事で、visual trackingを実現する手法を提案する。
時間方向の色情報の一貫性を利用する事で、参照フレームの色をコピーする事を通した
グレースケールビデオの色付けタスクをモデルに学習させる。
この学習を通して視覚的の領域のトラッキングをモデルは実現できるようになる。
このモデルはoptical flowよりも高性能を実現できる。

[Gradient Acceleration in Activation Functions][https://arxiv.org/abs/1806.09783]
Dropoutはニューロン同士のco-adaptationを防ぐ事により
過剰適合を防ぐ効果があると考えられてきた。
本論ではDropoutの効果に別の理由つけを提案する。
それは、dropoutは非線形活性化関数のsaturation areaに
入力に対する反応をより押し込むような勾配のaccelerationを
行っている、というものである。
この仮説に基づき、saturation areaへの押し込みを加速させる
gradient acceleration in activation function (GAAF)を提案し、
その効果を確かめた。

Guided evolutionary strategies: escaping the curse of dimensionality in random search
多くの機械学習における最適化でsurrogate gradientがtrue gradientの代わりに
用いられてきた。
本論では同様にしてrandom searchに関するsurrogate gradientを提案し、
gradient方向に張られる部分空間への探索を行う事で計算効率を改善する。
gradientは得られたサンプルに対してガウス分布でモデル化しながら計算を
行う。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

機械学習論文読みメモ_164

Self-Imitation Learning
本論ではself-imitation learningを提案する。
この手法はエージェントの過去の良い決定を再現するよう学習を行う
off-policy actor-critic algorithmである。
本論の仮説である、より深い探索は過去の良い経験によって間接的に導かれる、
という考えの正しさを本論は示した。

Evolving simple programs for playing Atari games
Cartesian Genetic Programming (CGP)は関数集合に対する選択と組合せを通して
プログラムを構築プロセスに関し、それを遺伝的アルゴリズムにより発展させていく手法である。
CGPは特に画像処理に関してその可能性を示してきた。
本論ではCGPをAtariゲームのプレイに適用する。
提案するCGPでは、行列計算に関する処理を関数集合に含め、
結果画像処理や制御挙動を可能にする。
プログラムサイズは小さいが、SOTAモデルと同等の性能を持つ事が
可能である。

DARTS: Differentiable Architecture Search
本論ではneural architecture searchを微分可能形式に定式化する事で
スケーラビリティを改善する手法を提案する。
離散あるいは微分不可能な探索空間に対する強化学習や遺伝的アルゴリズムに基づいた
手法と違い、構造表現の連続緩和に基づいてSGDによる効率的な探索を提案手法は実現する。
提案手法はネットワーク構造をdirected acyclic graph (DAG)で表現し、そのDAGにおける
接続パターンをsoftmax関数を用いた連続緩和表現を利用することで微分可能な形にしている。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

https://onegold88.com/scr888-online/

Another star that suffered through money trouble in the fifties was Marilyn Monroe. Tired of playing dumb blondes, she bolted from her studio Twentieth Century Fox to start Marilyn Monroe Productions. Actors are often advised not to use their own Scr888 name in their personal ventures; it makes other ego-driven stars less willing to work with them. Marilyn's film output slowed down and by 1959 her husband, playwright Arthur Miller, was telling her she should accept the dumb blonde role in Some Like It Hot, they needed the money. "I can't see through Jack Lemmon and Tony Curtis in drag? Oh my God, I've been dumb before but never that dumb.
There's also a special ferry trip for residents from different retirement communities in New York-this trip will take the best route in New London, the humble town of Mystic, Mohegan Sun (the best place for shopping, casino, and other amusement centers), and other best places in the city.Beverly Hills is a place where you spend a lot of money you don't have to impress a lot of people you don't like!"- - Anonymous Hollywood Producer

Another star that suffered through money trouble in the fifties was Marilyn Monroe. Tired of playing dumb blondes, she bolted from her studio Twentieth Century Fox to start Marilyn Monroe Productions. Actors are often advised not to use their own name in their personal ventures; it makes other ego-driven stars less willing to work with them. Marilyn's film output slowed down and by 1959 her husband, playwright Arthur Miller, was telling her she should accept the dumb blonde role in Some Like It Hot, they needed the money. "I can't see through Jack Lemmon and Tony Curtis in drag? Oh my God, I've been dumb before but never that dumb.

She went to her well renowned acting teacher, Lee Strasberg, to ask how she could make the audience believe her character. Strasberg suggested that Marilyn, always a man's woman, play the part as someone so desperate for female friendship, she simply didn't pay attention to her co-star's masculine features. She took his advice and the result was a comedy classic.
https://onegold88.com/scr888-online/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む