お問い合わせはこちら

TensorFlowとKerasによるディープラーニング②【第3回:GPUコンテナで画像解析〜実践編〜】

公開
更新

AI初学者にとってハードルのひとつになっているGPU環境構築をできるだけ効率的に行うために、コンテナを有効活用することを目的に連載をスタートしました。前回はディープラーニングのフレームワークとしてメジャーなTensorFlowをDokcerコンテナの中から起動させるところまで行いました。今回はTensorFlowといっしょに使われることが多い、Kerasを取り上げます。

この連載について

本連載は、全6回のシリーズを通してできるだけ効率的に、GPUの環境構築を行うためにコンテナの活用を行っていきます。「機械学習やディープラーニングをGPUで実行してみたいけど難しそう…」など導入にハードルを感じられている方に、コンテナを活用することで、環境構築に要する工数を圧倒的に削減し、即座に課題に取り組むことができるメリットを感じていただきます。そのために必要な知識や操作方法を、当社のGPUサーバーを使い解説していきます。
連載を読み終えるころには、TensorFlowやPyTorchなどのメジャーなフレームワークを使った演習ができるようになっているはずです。

本連載は、こちらの手順で進めています

・GPUコンテナとは何か?何が便利なのか?(第1回)

AI初学者がGPUを使って機械学習やディープラーニングに取り組みたい場合、環境構築に想像以上の工数が発生することがあります。セットアップ作業に要する時間を極力削減するためにコンテナ技術を適用し、コンテナ内からGPUを利用するための準備と手順について紹介します。

●GPUコンテナとは何か?何が便利なのか?(第1回)
https://www.kagoya.jp/howto/cloud/gpu-container1/

・TensorFlowとKerasによるディープラーニング①(第2回)

OSS(オープンソースソフトウエア)の機械学習ライブラリの中からTensorFlow(テンソルフロー)、Keras(ケラス)を取り上げ、これらが稼働するコンテナを作成し、コンテナ内からGPUを指定する方法について紹介します。TensorFlowはKerasを取り込む形で公開されていて、ディープラーニングをする際の使い勝手の良さから、多くのユーザーに利用されています。この回ではコンテナ内のTensorFlowでGPUを利用できる状態まで確認します。

●GPUコンテナで画像解析〜準備編〜(第2回)
https://www.kagoya.jp/howto/cloud/gpu-container2/

・TensorFlowとKerasによるディープラーニング②(第3回)今回の記事

TensorFlow(テンソルフロー)やTheano(テアノ)/CNTK(Cognitive Toolkit)の複数のバックエンドとして利用可能なKeras(ケラス)を取り上げ、TensorFlowとKerasを使ったディープラーニングを行います。ここではAI初学者の方が親しみやすい課題を扱うことを意図し、TensorFlowの公式ガイドに記載されている「初心者のための TensorFlow 2.0 入門」のチュートリアルを取り上げました。

・Chainerを使ったディープラーニング(第4回)

ディープラーニングのフレームワークとして有名なChainer(チェイナー)の利用方法を紹介します。コンテナからGPUを利用する手順と、GPUによってどれだけ高速化に寄与できたかについて、Pythonのプログラム実行結果を通して確認します。C言語に比べて処理時間がかかると言われているPythonですが、数値計算を効率的に行うための拡張モジュールであるNumPy(ナムパイ)についても取り上げます。

・Pytorchでニューラルネットワーク(第5回)

Pythonの機械学習用フレームワークであるPytorch(パイトーチ)を取り上げます。PyTorchではTensor(テンソル)という型で行列を表現します。Tensorは多次元配列を扱うためのデータ構造であり、GPUをサポートしていることから、Pytorchが稼働するコンテナを利用し、GPUによる高速処理を行う手順について紹介します。

・OpenPoseによる関節点抽出・姿勢推定(第6回)

カメラ画像のAI画像認識と言えば「顔認証」を思い浮かべる人が多いと思いますが、最近は一歩進み、人が映った静止画や動画から関節点抽出・姿勢推定に取り組むケースが増えています。人体、顔、手足などのキーポイントを画像から検出する技術がディープラーニングにより、実用レベルまで向上しているからです。この回ではOpenPose(オープンポーズ)というライブラリをGPU上で動かすコンテナを使い、動画ファイルの関節点抽出手順を紹介します。

前回(第2回)の振り返り

前回は、Dockerを使ってGPUコンテナでTensroFlowを動かすところまで行いました。
【 第2回の連載記事はこちら https://www.kagoya.jp/howto/cloud/gpu-container2/

復習を兼ねて、GPUコンテナからPythonのプログラムを動かします。
(コンテナ内でbashシェルをたちあげ、pythonを起動してから、TensorFlowとtf.Kerasのバージョンを確認する簡単なスクリプトを流します)
まずは、gpuコンテナを起動し、シェルに入ります。

$ docker run --gpus all -it --rm tensorflow/tensorflow:latest-gpu
 Subsystem: NVIDIA Corporation Device 11d9
________                               _______________
___  __/__________________________________  ____/__  /________      __
__  /  _  _ \_  __ \_  ___/  __ \_  ___/_  /_   __  /_  __ \_ | /| / /
_  /   /  __/  / / /(__  )/ /_/ /  /   _  __/   _  / / /_/ /_ |/ |/ /
/_/    \___//_/ /_//____/ \____//_/    /_/      /_/  \____/____/|__/

WARNING: You are running this container as root, which can cause new files in
mounted volumes to be created as the root user on your host machine.
To avoid this, run the container by specifying your user's userid:
$ docker run -u $(id -u):$(id -g) args...
root@bec5aca64bf7:/#

以降、コンテナ内で行っている内容について細かく見ていきたいので、bashシェルから操作しています。
Python起動後にtensorflowをインポートし、バージョンを確認します。

root@bec5aca64bf7:/# python
Python 3.6.9 (default, Apr 18 2020, 01:56:04)
[GCC 8.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import tensorflow as tf
>>> print('TensorFlow', tf.__version__)
TensorFlow 2.2.0

tensorflowのバージョンは2.2.0であることが確認できました。

続いて、tf.kerasのバージョンを確認します。

>>> print('TensorFlow', tf.keras.__version__)
TensorFlow 2.3.0-tf

tf.kerasのバージョンは2.3.0であることが確認できました。

GPUに対応しているコンテナ内でインポートしたtensorflowとtf.kerasのバージョンを確認することができました。前回の復習は以上になります。

TensorFlowとKerasを使った画像認識

2種類のKerasについて

今回、とりあげる keras には「スタンドアロンkeras」「TensorFlow付属keras」の2種類があります。もともとkerasはTensorFlow以外にも、Theano(テアノ)やCNTK(コグニティブツールキット)など複数のフレームワークをバックエンドに利用できる汎用性のあるものでしたが、この文章を書いている2020年8月時点で「TensorFlow付属keras」つまり、tf.kerasに一本化されました。
マルチバックエンドKeras(スタンドアロンkeras)で作成したスクリプトは、その時点でのバージョンを使うことで稼働させることが可能です。
ただ、この連載の趣旨は、できるだけ効率的にGPUを動かすことにあるので、公式に「TensorFlow付属keras」一本化が表明されたことをうけ、tf.kerasを利用することにします。

tf.kerasと記述する理由

前述のスクリプト内で、

>>> print('TensorFlow', tf.keras.__version__)

と記述して、kerasのバージョンを確認しています。ここを tf.keras.__version__でなく、下記のようにkeras.__version__として実行すると

>>> print('TensorFlow', keras.__version__)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'keras' is not defined

このようなエラーが表示されます。それは、このコンテナでは「スタンドアロンkeras」をインポートして、keras が利用できる状態になっていないからです。

「tf.keras」をどうやってインストールした?

tf.kerasのインストールは、pip install tensorflow になります。
「tf.keras」はTensorFlowに付属する(言い方を変えれば同梱されている)kerasなので、最新のTensorFlowをインストールすることでkerasも利用できるようになっています。
プログラムの中からTensorFlow付属kerasを呼び出すには、前述の

print('TensorFlow', tf.keras.__version__)

のような方法で可能です。
ちなみに「スタンドアロンkeras」のインストールは、pip install keras になることから想像できるように「スタンドアロンkeras」と「TensorFlow付属keras」はまったく別物になっていますので、プログラムからの呼び出し方が違うことがあります。
と言っても「tf.keras」と「スタンドアロンkeras」は互換性が高いため、プログラム内のimport部を書き換えれば「tf.keras」に対応可能です。
こちらの記事では「TensorFlow付属keras」を利用してディープラーニングを行います。
以降の文章でkerasと書いている部分は、tf.kerasを利用しているのだなと思って、読み進めてください。
「tf.keras」についての詳細はTensorFlowの公式サイトに掲載されていますので、参照されることをお薦めします。
https://www.tensorflow.org/guide/keras

MNIST(エムニスト)を使った手書き文字の判別処理

ここから本題であるGPUコンテナからディープラーニングを行いますが、AI初学者の方が親しみやすい課題を扱うことを意図し、TensorFlowの公式サイトに記載されている「初心者のための TensorFlow 2.0 入門」のチュートリアルを取り上げました。
【 https://www.tensorflow.org/tutorials/quickstart/beginner?hl=ja 】

公式ガイドでは説明を端折っている 部分を補足するかたちで進めていきたいと思います。
このチュートリアルで行っていることの概要は以下になります

  1. 手書き文字画像を分類するためのニューラルネットワークのモデルを定義する
  2. 構築したモデルに対し、トレーニング用画像を使い、学習を行う
  3. テスト用画像を使い、モデルを性能評価する

MNISTとは

0から9の手書き数字文字の画像データセットです。機械学習の領域でサンプルとして利用されることが多い使い勝手のよいデータ集であり、トレーニング用データ60,000枚とテスト用データ10,000枚が含まれます。MNISTのデータは、手書き文字の画像データが何の数字かを示す「ラベルデータ」と縦・横 各々28ピクセルの「画像データ」から構成されています。

TensorFlow公式ガイドのチュートリアルでも、MNISTを使った演習を行っていますので、詳細をみていきましょう。
前段で行っているbashシェルからの一連の操作の続きになります。先ず最初にmnistのデータをtf.kerasから呼び出します。

>>>mnist = tf.keras.datasets.mnist
>>>(x_train, y_train), (x_test, y_test) = mnist.load_data()

2行目ではmnistデータセットに含まれているデータをトレーニング用データとテスト用データに分割しています。
x_trainはトレーニング用画像データ、y_trainはトレーニング用ラベルデータ(0から9のどの数字の画像かを示す数値データ)、x_testはテスト用画像データ、y_testはテスト用ラベルデータになります。

上記のスクリプトを実行すると下記のように約11.5Mのデータがダウンロードされました。

Downloading data from https://storage.googleapis.com/tensorflow/tf-keras-datasets/mnist.npz
11493376/11490434 [==============================] - 1s 0us/step

次にデータの正規化処理を行います。
MNISTの 28ピクセル×28ピクセル の手書き文字画像は、各ピクセルごとの白黒諧調を0から255の範囲内であらわすデータとして持っていますが、これを0から1の範囲内に収まるよう正規化します。(整数から浮動小数点に変換することで、データを扱いやすくするためです)

>>>x_train, x_test = x_train / 255.0, x_test / 255.0

ニューラルネットワークを構築

続けて、画像を分類するためのニューラルネットワークを構築します。今回とりあげたTensorFlow公式サイトのチュートリアルでは「Sequentialモデル」と呼ばれる、各層を順番につなげていくモデルを定義しています。

>>>model = tf.keras.models.Sequential([
tf.keras.layers.Flatten(input_shape=(28, 28)),
tf.keras.layers.Dense(128, activation='relu'),
tf.keras.layers.Dropout(0.2),
tf.keras.layers.Dense(10)
])

2行目の
tf.keras.layers.Flatten(input_shape=(28, 28))
では、入力データのフォーマットを変換しています。Flatten関数はn次元の配列を1次元の配列に変換する関数で、手書き文字数値を示すための 28ピクセル X 28ピクセル の2次元配列データから 754(28 X 28)の1次元配列データへの変換処理を行っています。これにより、最初の層(入力層)が、754のノードを持つモデルであることが定義できました。

3行目の
tf.keras.layers.Dense(128, activation=’relu’)
では、入力層につながっている次の層(中間層/隠れ層)の定義を行っています。この層は128のノードを持ち、前後の層にあるすべてのノードとつながっています。活性化関数としてReLU関数(ランプ関数)を適用すると、0以下なら「0」を、0より大きければ「入力値そのもの」が返ってきますので、マイナス値を切り捨て、特徴がはっきりとしたデータとして扱うことが可能になります(画像データなのでマイナスを示すデータは意味の無いノイズとみなして処理します)

4行目の
tf.keras.layers.Dropout(0.2)
では、ドロップアウト率を0.2と設定しています。ニューラルネットワークで過学習(オーバーフィッティング)を防ぐための手法のひとつとしてドロップアウトを追加することがあり、tf.kerasでは直前の層に対して、ドロップアウト率を設定することで、モデルの精度を向上させることが期待できます。トレーニング用データでいくら精度が高いモデルが構築できたとしても、未知のテストデータを使ったときに精度が出ない(正解率が低い)モデルでは実用性がないため、敢えて20%のノードを無効にして過学習を防ごうという意図があります。

5行目の
tf.keras.layers.Dense(10)
では、最後の層(このモデルの出力層)として10個のノードをもつネットワークとして定義しています。(画像データが0から9のどの数値であると判定するかを出力するために10個のノードを利用するためです)

このあたりの詳細を知りたい方はTensorFlow公式ガイド内の
モデルの構築 > 層の設定 という項目を参照ください。
https://www.tensorflow.org/tutorials/keras/classification?hl=ja
前述のスクリプトを流すと下記のように実行されました。

2020-09-04 04:42:18.824202: I tensorflow/stream_executor/platform/default/dso_loader.cc:44]
 Successfully opened dynamic library libcuda.so.1
(中略)
2020-09-04 04:42:19.015061: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1247]
 Created TensorFlow device (/job:localhost/replica:0/task:0/device:GPU:0 with 851 MB memory) -> 
 physical GPU (device: 0, name: Tesla P40, pci bus id: 0000:03:00.0, compute capability: 6.1)
>>>

上記のモデルで予測する

前段で定義したモデルを使って予測を行います。この時点ではトレーニング前です。

公式ガイドには「モデルはそれぞれの標本について、クラスごとに”ロジット”や”対数オッズ比”と呼ばれるスコアを算出します」と記載されていますが、ロジット(logits)とはある事象が発生する確率pを発生しない確率(1-p)で割ったオッズの自然対数です。

次のスクリプトを実行すると以下のような結果が返ってきました。

>>>predictions = model(x_train[:1]).numpy()
2020-09-04 04:51:53.991893:
I tensorflow/stream_executor/platform/default/dso_loader.cc:44]
Successfully opened dynamic library libcublas.so.10
>>>
>>>predictions
  array([[-0.0729934 , -0.5482341 , 0.3433068 , -0.15514854, 0.44248402,
        0.24744856, -0.15032545, 0.24870253, 0.16561234, -0.56604826]],
        dtype=float32)

次のスクリプトでは活性化関数としてソフトマックス関数を適用していますが、前段で算出したロジットの値を手書き文字の0~9である確率が何パーセントなのかを示すために利用しています。(ソフトマックス関数は出力値を確率として表現できる性質があるため利用しています)

>>>tf.nn.softmax(predictions).numpy()

結果は以下のようになりました。

array([[0.15495376, 0.12319947, 0.12194902, 0.07117186, 0.05812015,
     0.0898917 , 0.08244707, 0.04264582, 0.13558292, 0.12003825]],
     dtype=float32)

モデルをトレーニングさせるために必要なパラメータの設定をコンパイルという処理の際に行います。その1つに損失関数があり、任意の関数を利用できますが、このチュートリアルでは交差エントロピーを利用しています。トレーニング前のモデルではランダムに近い確率(それぞれのクラスについて 1/10) を出力します。最初の損失は -tf.log(1/10)~= 2.3 に近い値になるはずです。

>>>loss_fn = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)
>>>loss_fn(y_train[:1], predictions).numpy()
2.4091496

モデルのコンパイルを下記のパラメータで設定し、どんな学習を行うかを定義します。ひとつめのパラメータは最適化関数、2つめは損失関数、3つめはメトリクスになります。

optimizer:最適化関数としてAdam(Adaptive Moment Estimation)を指定。SGDと比較すると収束が速いと言われています。

loss:損失関数は前段で指定した”交差エントロピー”を適用しています。

metrics:メトリクス(評価関数のリスト)として、今回のような手書き文字の分類がテーマの場合、よく用いられるaccuracyを指定。

>>>model.compile(optimizer='adam',
loss=loss_fn,
metrics=['accuracy'])

モデルのトレーニング(学習)を行います

手順としては、これまでに定義した学習モデルに対し、トレーニングデータ、教師ラベルデータを与え、画像とラベルの対応関係を学習します。
モデルに対し、テスト用データセットの予測を行わせ、予測結果とテスト用教師ラベルを照合します。
チュートリアルではエポック回数を5と設定しています。
model.fit関数を使い、トレーニングを開始します。下記のようにトレーニング過程の損失値(loss)と正解率(accuracy)が随時、表示されます。

>>>model.fit(x_train, y_train, epochs=5)
Epoch 1/5
1875/1875 [==============================] - 4s 2ms/step - loss: 0.2965 - accuracy: 0.9138
Epoch 2/5
1875/1875 [==============================] - 3s 2ms/step - loss: 0.1459 - accuracy: 0.9571
Epoch 3/5
1875/1875 [==============================] - 4s 2ms/step - loss: 0.1082 - accuracy: 0.9674
Epoch 4/5
1875/1875 [==============================] - 4s 2ms/step - loss: 0.0881 - accuracy: 0.9724
Epoch 5/5
1875/1875 [==============================] - 4s 2ms/step - loss: 0.0754 - accuracy: 0.9768
<tensorflow.python.keras.callbacks.History object at 0x7f03d0b977b8>

正解率の評価

引き続きモデルの検証を行います。5回のエポックでモデルの正解率が示されましたが、トレーニングで利用しなかったテスト用データとテスト用教師ラベルデータを使い、誤差と正解率を求めます。(このモデルの評価を行います)
3番目のパラーメータ verbose=2 はプログレスバー表示をしない指定です。(エポックごとのログはあり)

>>>model.evaluate(x_test, y_test, verbose=2)
313/313 - 1s - loss: 0.0737 - accuracy: 0.9774
[0.07369402796030045, 0.977400004863739]

検証結果は上記のように誤差が約 0.07正解率は約 0.98 と表示されました。
5回の学習で高い正解率が出たことで、ここで学習は終了しています。

テスト用画像で推論

学習が終わったので、最後にトレーニング済モデルを使って推論処理しますが、数値ではなく確率を出力したい場合には前段で使ったソフトマックス関数をモデル全体に対して指定します。テスト用画像5つについて推論します。

>>>probability_model = tf.keras.Sequential([
  model,
  tf.keras.layers.Softmax()
])
>>>probability_model(x_test[:5])

以下のような結果になりました。

<tf.Tensor: shape=(5, 10), dtype=float32, numpy=
   array([[7.41418518e-08, 7.91325783e-10, 3.75941590e-06, 2.61097477e-04,
        9.48091952e-11, 3.16857694e-07, 4.86965676e-11, 9.99731362e-01,
        4.52538757e-07, 2.88244155e-06],
       [1.33913982e-11, 2.29599886e-04, 9.99751031e-01, 1.92872030e-05,
        1.66144136e-15, 1.58800315e-08, 2.13381668e-08, 9.98626540e-16,
        3.95744095e-08, 4.06508526e-14],
       [4.50110349e-07, 9.99431074e-01, 3.84905434e-05, 1.16055037e-06,
        3.32167328e-05, 5.31233536e-06, 1.03985847e-04, 3.38415528e-04,
        4.75658890e-05, 1.63871476e-07],
       [9.99950290e-01, 7.11293871e-12, 3.84013947e-05, 2.12064151e-07,
        1.02987372e-07, 4.20437999e-07, 2.38990742e-06, 2.65470180e-06,
        1.09450021e-07, 5.40031533e-06],
       [7.92320952e-06, 2.07563033e-09, 8.12820872e-06, 2.62512536e-08,
        9.88838971e-01, 1.17090956e-07, 1.72804573e-06, 2.94550318e-05,
        5.51807261e-06, 1.11081181e-02]], dtype=float32)>

配列の中身を確認します。
1つ目のテスト画像に対する推論結果は、数字の7である確率が約99%と最も高くなりました。(1番目の配列の並び順に数字の0,1,2,3,4,5,6,7,8,9である確率を示しており、9.99731362e-01 と一番高い確率を示しているのが、数字の7に対応したノードであるから)
以下、同様に見ていきます。
2つ目のテスト画像の推論結果は数字の2である確率が約99%
3つ目のテスト画像の推論結果は数字の1である確率が約99%
4つ目のテスト画像の推論結果は数字の0である確率が約99%
5つ目のテスト画像の推論結果は数字の4である確率が約99%
と算出されました。

テスト用データセットの画像ファイルの中身の一部は下記のようなっています。

左上端から右方向に1つ目から5つ目の画像と推論結果を照らし合わせると、7、2、1、0、4で合致していることがわかりました。

公式ガイドのチュートリアルに沿って、手書き文字認識のモデル構築、学習、推論という一連の流れを見てきました。今回はここで終了です。
コンテナ内部でフレームワークを利用することで、ほとんどプログラミングすることなく、関数の呼び出しとパラメータの設定により、ディープラーニングできることが確認できました。

次回予告 第4回:Chainerを使ったディープラーニング

ディープラーニングのフレームワークとして有名なChainer(チェイナー)の利用方法を紹介します。

GPUサーバー