お問い合わせはこちら

Chainerを使ったディープラーニング【第4回:GPUコンテナで機械学習する】

公開
更新

AI初学者にとってハードルのひとつになっているGPU環境構築をできるだけ効率的に行うために、コンテナを有効活用することを目的に連載をスタートしました。前回はディープラーニングのフレームワークとしてメジャーなTensorFlowとKerasを使い、手書き文字の分類を行いました。今回はこちらもメジャーなフレームワークのひとつであるChainer(チェイナー)を使って、前回同様に手書き文字画像の分類を行います。

この連載について

本連載は、全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 入門」のチュートリアルを取り上げました。

●GPUコンテナで画像解析~実践編~(第3回)
https://www.kagoya.jp/howto/cloud/gpu-container3/

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

ディープラーニングのフレームワークとして有名なChainer(チェイナー)の利用方法を紹介します。コンテナからPythonのプログラム実行結果を通して確認します。C言語に比べて処理時間がかかると言われているPythonですが、数値計算を効率的に行うための拡張モジュールであるNumPy(ナムパイ)も利用します。今回扱う題材としてChainerのチュートリアルを取り上げます。

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

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

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

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

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

前回は、Docker環境でGPUコンテナからTensroFlowとKerasを動かし、手書き文字の認識を行いました。
【 第3回の連載記事はこちら https://www.kagoya.jp/howto/cloud/gpu-container3/

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

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ピクセルの「画像データ」から構成されています。

ニューラルネットワークのモデルを定義

MNISTの画像を分類するためのニューラルネットワークを構築します。このチュートリアルでは「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)
])

上記のモデルで予測する

前段で定義したモデルを使って予測を行います。この時点ではトレーニング前です。
以下、詳しい説明は省略していますので、前回の記事を参照してください。

>>>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)
>>>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)
>>>loss_fn = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)
>>>loss_fn(y_train[:1], predictions).numpy()
2.4091496
>>>model.compile(optimizer='adam',
loss=loss_fn,
metrics=['accuracy'])

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

学習モデルに対し、トレーニングデータ、教師ラベルデータを与え、画像とラベルの対応関係を学習します。モデルに対し、テスト用データセットの予測を行わせ、予測結果とテスト用教師ラベルを照合します。エポック回数を5と設定し、model.fit関数を使い、トレーニングを開始します。

>>>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回のエポックでモデルの正解率が示されましたが、トレーニングで利用しなかったテスト用データとテスト用教師ラベルデータを使い、誤差と正解率を求めます。

>>>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で合致していることがわかりました。

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

Chainerを使った機械学習

前回、TensorFlowKerasを使って手書き文字の分類を行いましたが、今回はChainerの開発元であるPFN社のサイトにあるChainerチュートリアルに沿って機械学習を行います。

Chainer(チェイナー)とは

今回、とりあげる Chainer はPFN(Preferred Networks:プリファードネットワークス)という日本の企業が開発したPythonベースのディープラーニング向けフレームワークです。オープンソースソフトウエアであることや、複雑なニューラルネットワークを比較的シンプルに定義できることなど、使い勝手の良さから日本の多くのユーザーから支持されてきました。

今回行うことの概要

AI初学者の方が親しみやすい課題を扱うことを意図し、Chainerの公式サイトに記載されているChainerチュートリアルの中から14.Chainerの基礎を取り上げました。
チュートリアルの内容を補足しながら進めていきますので、下記の記載事項もあわせて見ていただけると理解が深まると思います。
【参考サイト:https://tutorials.chainer.org/ja/14_Basics_of_Chainer.html?hl=ja

チュートリアルで行っている以下の内容を実践します。

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

Chainerのコンテナを準備する

早速、docker hubからchainerのコンテナイメージをpullしましょう。
下記のURLから pip install コマンドで chainerをインストールします。
Dockerhub 参照サイト:https://hub.docker.com/r/chainer/chainer/

$ pip install chainer

コンテナイメージを確認します。

$ docker images chainer/chainer
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
chainer/chainer     latest              3ce7a8bfbbab        3 months ago        4.24GB

chainerのコンテナ内からnvidia-smiを実行し、GPUが認識できているかを確認します。

$ docker run --gpus all chainer/chainer nvidia-smi
Fri Nov 20 05:30:28 2020
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 440.64.00    Driver Version: 440.64.00    CUDA Version: 10.2     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|===============================+======================+======================|
|   0  Tesla P40           Off  | 00000000:03:00.0 Off |                    0 |
| N/A   38C    P0    50W / 250W |  21697MiB / 22919MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+

+-----------------------------------------------------------------------------+
| Processes:                                                       GPU Memory |
|  GPU       PID   Type   Process name                             Usage      |
|=============================================================================|
+-----------------------------------------------------------------------------+

上記のようにGPU関連の情報が表示されることが確認できました。

Pythonのライブラリをインストールする

あらためてChainerのコンテナイメージからbashをたちあげ、pip3 listコマンドでインストール済のパッケージ一覧を表示します。

$ docker run --gpus all -it chainer/chainer:latest bash
root@790e21b683b1:/# pip3 list
chainer (7.7.0)
cupy-cuda92 (7.7.0)
fastrlock (0.5)
filelock (3.0.12)
numpy (1.18.5)
pip (8.1.1)
protobuf (3.12.4)
setuptools (20.7.0)
six (1.15.0)
typing-extensions (3.7.4.2)
wheel (0.29.0)
You are using pip version 8.1.1, however version 20.2.4 is available.
You should consider upgrading via the 'pip install --upgrade pip' command.

pipのアップグレードを促すメッセージが表示されたので実施します。(アップグレードの必要がない方は無視して先に進んでください)

root@790e21b683b1:/# pip3 install --upgrade pip
Collecting pip
  Downloading https://files.pythonhosted.org/packages/cb/28/91f26bd088ce8e22169032100d4260614fc3da435025ff389ef1d396a433/pip-20.2.4-py2.py3-none-any.whl (1.5MB)
    100% |################################| 1.5MB 652kB/s
Installing collected packages: pip
  Found existing installation: pip 8.1.1
    Not uninstalling pip at /usr/lib/python3/dist-packages, outside environment /usr
Successfully installed pip-20.2.4

アップグレードが終わったので、もう一度pip3 listコマンドでインストール済のパッケージ一覧を表示します。

root@790e21b683b1:/# pip3 list
WARNING: pip is being invoked by an old script wrapper. This will fail in a future version of pip.
Please see https://github.com/pypa/pip/issues/5599 for advice on fixing the underlying issue.
To avoid this problem you can invoke Python with '-m pip' instead of running pip directly.
DEPRECATION: Python 3.5 reached the end of its life on September 13th, 2020. Please upgrade your Python as Python 3.5 is no longer maintained. pip 21.0 will drop support for Python 3.5 in January 2021. pip 21.0 will remove support for this functionality.
Package           Version
----------------- -------
chainer           7.7.0
cupy-cuda92       7.7.0
fastrlock         0.5
filelock          3.0.12
numpy             1.18.5
pip               20.2.4
protobuf          3.12.4
setuptools        20.7.0
six               1.15.0
typing-extensions 3.7.4.2
wheel             0.29.0

今回の演習では、計算結果をグラフ化するために「matplotlib」というグラフ描画ライブラリを利用しますので、このタイミングでインストールしておきます。

root@790e21b683b1:/# pip3 install matplotlib
WARNING: pip is being invoked by an old script wrapper. This will fail in a future version of pip.
Please see https://github.com/pypa/pip/issues/5599 for advice on fixing the underlying issue.
To avoid this problem you can invoke Python with '-m pip' instead of running pip directly.
DEPRECATION: Python 3.5 reached the end of its life on September 13th, 2020. Please upgrade your Python as Python 3.5 is no longer maintained. pip 21.0 will drop support for Python 3.5 in January 2021. pip 21.0 will remove support for this functionality.
Collecting matplotlib
  Downloading matplotlib-3.0.3-cp35-cp35m-manylinux1_x86_64.whl (13.0 MB)
     |################################| 13.0 MB 13.3 MB/s
Collecting kiwisolver>=1.0.1
  Downloading kiwisolver-1.1.0-cp35-cp35m-manylinux1_x86_64.whl (90 kB)
     |################################| 90 kB 10.5 MB/s
(略)
Successfully installed cycler-0.10.0 kiwisolver-1.1.0 matplotlib-3.0.3 pyparsing-2.4.7 python-dateutil-2.8.1

同様に「scikit-learn」というPythonの機械学習ライブラリを利用しますのでインストールします。

root@790e21b683b1:/# pip3 install scikit-learn
WARNING: pip is being invoked by an old script wrapper. This will fail in a future version of pip.
Please see https://github.com/pypa/pip/issues/5599 for advice on fixing the underlying issue.
To avoid this problem you can invoke Python with '-m pip' instead of running pip directly.
DEPRECATION: Python 3.5 reached the end of its life on September 13th, 2020. Please upgrade your Python as Python 3.5 is no longer maintained. pip 21.0 will drop support for Python 3.5 in January 2021. pip 21.0 will remove support for this functionality.
Collecting scikit-learn
  Downloading scikit_learn-0.22.2.post1-cp35-cp35m-manylinux1_x86_64.whl (7.0 MB)
     |################################| 7.0 MB 8.6 MB/s
Collecting scipy>=0.17.0
  Downloading scipy-1.4.1-cp35-cp35m-manylinux1_x86_64.whl (26.0 MB)
     |################################| 26.0 MB 53.8 MB/s
(略)
Successfully installed joblib-0.14.1 scikit-learn-0.22.2.post1 scipy-1.4.

pip3 listコマンドでインストール済のパッケージ一覧を表示します。「matplotlib」「scikit-learn」関連のライブラリがインストールされたことが確認できました。

root@790e21b683b1:/# pip3 list
WARNING: pip is being invoked by an old script wrapper. This will fail in a future version of pip.
Please see https://github.com/pypa/pip/issues/5599 for advice on fixing the underlying issue.
To avoid this problem you can invoke Python with '-m pip' instead of running pip directly.
DEPRECATION: Python 3.5 reached the end of its life on September 13th, 2020. Please upgrade your Python as Python 3.5 is no longer maintained. pip 21.0 will drop support for Python 3.5 in January 2021. pip 21.0 will remove support for this functionality.
Package           Version
----------------- ------------
chainer           7.7.0
cupy-cuda92       7.7.0
cycler            0.10.0
fastrlock         0.5
filelock          3.0.12
joblib            0.14.1
kiwisolver        1.1.0
matplotlib        3.0.3
numpy             1.18.5
pip               20.2.4
protobuf          3.12.4
pyparsing         2.4.7
python-dateutil   2.8.1
scikit-learn      0.22.2.post1
scipy             1.4.1
setuptools        20.7.0
six               1.15.0
typing-extensions 3.7.4.2
wheel             0.29.0

コンテナからPythonを起動し、chainerを実行する環境の情報を表示します。

root@790e21b683b1:/# python3
Python 3.5.2 (default, Oct  8 2019, 13:06:37)
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import chainer
>>> chainer.print_runtime_info()
Platform: Linux-4.4.0-154-generic-x86_64-with-Ubuntu-16.04-xenial
Chainer: 7.7.0
ChainerX: Available
NumPy: 1.18.5
CuPy:
  CuPy Version          : 7.7.0
  CUDA Root             : /usr/local/cuda
  CUDA Build Version    : 9020
  CUDA Driver Version   : 10020
  CUDA Runtime Version  : 9020
  cuBLAS Version        : 9020
  cuFFT Version         : 9020
  cuRAND Version        : 9020
  cuSOLVER Version      : (9, 2, 0)
  cuSPARSE Version      : 9020
  NVRTC Version         : (9, 2)
  cuDNN Build Version   : 7600
  cuDNN Version         : 7605
  NCCL Build Version    : 2408
  NCCL Runtime Version  : 2408
  CUB Version           : None
  cuTENSOR Version      : None
iDeep: Not Available

OSやChainer、Numpyのバージョン情報などが表示されます。CuPy(GPUを使いNumPy互換の機能を提供するライブラリ)も利用できることがわかります。

機械学習用のデータセットを準備する

今回の演習では、scikit-learnに付属している『Iris plants dataset』を使い、3種類のあやめ(アイリス)の花の測定値から、どの品種に属するかを分類する機械学習モデルを構築します。
参考:『Iris plants dataset』の仕様はこちら

使用する『Iris plants dataset』を読み込んで、内容について確認しましょう。
sklearn.datasets モジュールにある load_iris() 関数を実行することでデータセットを読み込むことができます。

>>> from sklearn.datasets import load_iris
>>> iris = load_iris()

load_iris() 関数の説明のページを参照すると、詳細説明がDESCRという引数で照会できると記載されていますので、確認します。

>>> print(iris.DESCR)
.. _iris_dataset:

Iris plants dataset
--------------------

**Data Set Characteristics:**

    :Number of Instances: 150 (50 in each of three classes)
    :Number of Attributes: 4 numeric, predictive attributes and the class
    :Attribute Information:
        - sepal length in cm
        - sepal width in cm
        - petal length in cm
        - petal width in cm
        - class:
                - Iris-Setosa
                - Iris-Versicolour
                - Iris-Virginica

    :Summary Statistics:

    ============== ==== ==== ======= ===== ====================
                    Min  Max   Mean    SD   Class Correlation
    ============== ==== ==== ======= ===== ====================
    sepal length:   4.3  7.9   5.84   0.83    0.7826
    sepal width:    2.0  4.4   3.05   0.43   -0.4194
    petal length:   1.0  6.9   3.76   1.76    0.9490  (high!)
    petal width:    0.1  2.5   1.20   0.76    0.9565  (high!)
    ============== ==== ==== ======= ===== ====================

    :Missing Attribute Values: None
    :Class Distribution: 33.3% for each of 3 classes.
    :Creator: R.A. Fisher
    :Donor: Michael Marshall (MARSHALL%PLU@io.arc.nasa.gov)
    :Date: July, 1988

**Data Set Characteristics:**と書かれた部分に注目します。9行目に150 個のインスタンス(あやめのデータ)が存在し、各々50個ずつの3つのクラスから構成されていると記載されています。
10行目、11行目の記載内容を見ると、このデータセットには「Iris-Setosa(ヒオウギアヤメ)」「Iris-Versicolour(アイリスバージカラー)」「Iris-Virginica(アイリスバージニカ)」という3種類のあやめの測定データが含まれており、「sepal(がく片)」「petal(花弁)」の「length(長さ)」「width(幅)」を計測したデータ(単位:センチメートル)が含まれていることがわかります。load_iris()関数で150 個のあやめのがく片の長さ・幅、花弁の長さ・幅のデータと、各々のあやめの種類を示すクラス IDを取得し、それぞれ xt という変数で受け取り、各々の配列の形を表示します。
【参考ページ:Visualizing Datasets

Iris
>>> x, t = load_iris(return_X_y=True)
>>> print('x:', x.shape)
x: (150, 4)
>>> print('t:', t.shape)
t: (150,)

データの形式を Chainer に合わせます。
Chainer で学習用モデルにデータを引き渡すために、入力値のデータ型を numpy.float32(32ビット浮動小数点数)に、分類問題では目標値のデータ型を numpy.int32(32ビットの符号付き整数) にする必要があります。 NumPy配列ndarrayではデータ型dtypeを保持しており、これらの型に合わせるため astype() 関数を使ってデータ型を変更します。

>>> x = x.astype('float32')
>>> t = t.astype('int32')

データセットを分割する

学習モデルの正解率を正しく判断するために、トレーニング用とテスト用に分割する必要があります。トレーニング用に準備したデータをテスト用にも使うと、既知のデータであることから、正解率が高くなってしまうため、それを避けるためにデータセットを「トレーニング用」と「テスト用」に分割します。ここでは前述の2つに加え「検証用」の3つにデータセットを分割します。
まずは全体を『「トレーニング用データセット」 「検証用データセット」』と『「テスト用データセット」』の2つに分割します。データセットを分割するためにscikit-learntrain_test_split()関数を使用します。
このとき「テスト用データセット」のサイズを全体の30%にするために関数の引数 test_size=0.3 と指定します。これで、「トレーニング用データセット」と「検証用データセット」を合わせたデータが全体の70%、テスト用データセットが30%に分割されます。 引数 random_state=0 は、乱数のシード値を指定しない設定です。train_test_split()関数を使い、データセットを分割する処理を複数回行った際に、毎回異なる数字の並びになるように実行するたびランダムに分割したい場合は乱数のシード値を指定しません。

>>> from sklearn.model_selection import train_test_split
>>> x_train_val, x_test, t_train_val, t_test = train_test_split(x, t, test_size=0.3, random_state=0)

続いて「トレーニング用データセット」と「検証用データセット」に分割します。 「検証用データセット」のサイズは「トレーニング用データセット」「検証用データセット」を合わせたサイズの30%になるようtest_size=0.3と指定します。

>>> x_train, x_val, t_train, t_val = train_test_split(x_train_val, t_train_val, test_size=0.3, random_state=0)

ニューラルネットワークのモデル定義

続けて、あやめの種類を分類するためのニューラルネットワークを構築します。今回とりあげたChainerのチュートリアルでは「Sequentialモデル」と呼ばれる、各層を順番につなげていくモデルを定義しています。
chainerでモデルの定義をするにあたって、chainer.links(パラメータを持つ関数) 、chainer.functions(パラメータを持たない関数) という関数を利用できるよう準備します。
import chainer.links as Limport chainer.functions as F のように別名を付与する記述の仕方については、chainerを使う際のお作法のようなものと考えましょう。

>>> import chainer.links as L
>>> import chainer.functions as F

先ず、入力次元数が 3、出力次元数が 2 の全結合層を L.Linear クラスで定義します。第1引数は入力信号数、第2引数は出力信号数です。全結合層とは、すべてのノードが次の層のすべてのノードにつながっている層なので、3個のノードから2個のノードに接続する記述は以下になります。

>>> l = L.Linear(3, 2)

次に Sequential クラスと Linear クラス、relu() を使ってネットワークを定義します。
入力層、中間層、出力層の3つの層について下記の設定を行います。
Iris のデータは入力変数が 4 つ(「sepal(がく片)」「petal(花弁)」の「length(長さ)」「width(幅)」)ですので、最初の全結合層の入力次元数は 4 になります。
2つ目の全結合層の入力次元数は任意の値を指定可能ですが、ここでは 10 とします。
Iris のクラス数は 3 (あやめの種類が3つ)なので、最後の全結合層の出力次元数は 3 です。
活性化関数としてReLU関数(ランプ関数)を適用すると、0以下なら「0」を、0より大きければ「入力値そのもの」が返ってきますので、マイナス値を切り捨て、特徴がはっきりとしたデータとして扱うことが可能になります。

>>> from chainer import Sequential
>>>
>>> n_input = 4
>>> n_hidden = 10
>>> n_output = 3
>>>
>>> net = Sequential(
... L.Linear(n_input, n_hidden), F.relu,
... L.Linear(n_hidden, n_hidden), F.relu,
... L.Linear(n_hidden, n_output)
... )
>>>

最適化手法を決定する

機械学習でトレーニングを行うための最適化手法を決めますが、この演習では、確率的勾配降下法 (SGD:Stochastic Gradient Descent) を利用します。 Chainer には chainer.optimizers にいくつかの最適化手法を利用するためのクラスが用意されていて、確率的勾配降下法は SGD という名前で定義されています。

学習率 lr を 0.01と指定し、前述のchainer.optimizers.SGDを利用し、optimizer という名前のインスタンスを作成します。

>>> optimizer = chainer.optimizers.SGD(lr=0.01)

インスタンス optimizer に対し、前段でネットワークの定義をした net をセットして、net のパラメータが確率的勾配降下法によって更新されるようにします。

>>> optimizer.setup(net)

ネットワークのトレーニングを実行する

ここまでに定義したネットワークを使ってトレーニングを行いますが、そのためのエポック数とバッチサイズを各々 30 と 16 に設定します。元になるデータセットのサイズ自体が小さいことから、バッチサイズも 16 と小さく設定し、30回の学習回数でトレーニングを行います。

>>> n_epoch = 30
>>> n_batchsize = 16

トレーニングの実行
トレーニングは以下の処理を繰り返します。

  • 訓練用のバッチを準備
  • 予測値を計算し、目的関数を適用 (順伝播)
  • 勾配を計算 (逆伝播)
  • パラメータを更新

これに加えて、トレーニングがうまくいっているか判断するために「トレーニング用データ」を利用した分類精度と「検証データ」を利用した目的関数の値と分類精度を計算します。
numpy を別名 np でインポートした後、トレーニングを行います。

>>> import numpy as np
>>>
>>> iteration = 0
>>># ログの保存用
>>> results_train = {
...     'loss': [],
...     'accuracy': []
... }
>>> results_valid = {
...     'loss': [],
...     'accuracy': []
... }
>>>
>>> for epoch in range(n_epoch):
... # データセット並べ替えた順番を取得
...     order = np.random.permutation(range(len(x_train)))
... # 各バッチ毎の目的関数の出力と分類精度の保存用
...     loss_list = []
...     accuracy_list = []
... #
...     for i in range(0, len(order), n_batchsize):
...         # バッチを準備
...         index = order[i:i+n_batchsize]
...         x_train_batch = x_train[index,:]
...         t_train_batch = t_train[index]
...         # 予測値を出力
...         y_train_batch = net(x_train_batch)
...         # 目的関数を適用し、分類精度を計算
...         loss_train_batch = F.softmax_cross_entropy(y_train_batch, t_train_batch)
...         accuracy_train_batch = F.accuracy(y_train_batch, t_train_batch)
... #
...         loss_list.append(loss_train_batch.array)
...         accuracy_list.append(accuracy_train_batch.array)
...         # 勾配のリセットと勾配の計算
...         net.cleargrads()
...         loss_train_batch.backward()
...         # パラメータの更新
...         optimizer.update()
...         # カウントアップ
...         iteration += 1
...     # 訓練データに対する目的関数の出力と分類精度を集計
...     loss_train = np.mean(loss_list)
...     accuracy_train = np.mean(accuracy_list)
...     # 1エポック終えたら、検証データで評価
...     # 検証データで予測値を出力
...     with chainer.using_config('train', False), chainer.using_config('enable_backprop', False):
...         y_val = net(x_val)
...     # 目的関数を適用し、分類精度を計算
...     loss_val = F.softmax_cross_entropy(y_val, t_val)
...     accuracy_val = F.accuracy(y_val, t_val)
...     # 結果の表示
...     print('epoch: {}, iteration: {}, loss (train): {:.4f}, loss (valid): {:.4f}'.format(
...         epoch, iteration, loss_train, loss_val.array))
...     # ログを保存
...     results_train['loss'] .append(loss_train)
...     results_train['accuracy'] .append(accuracy_train)
...     results_valid['loss'].append(loss_val.array)
...     results_valid['accuracy'].append(accuracy_val.array)
...
epoch: 0, iteration: 5, loss (train): 2.0148, loss (valid): 1.3990
epoch: 1, iteration: 10, loss (train): 1.3596, loss (valid): 1.2077
epoch: 2, iteration: 15, loss (train): 1.1410, loss (valid): 1.1454
epoch: 3, iteration: 20, loss (train): 1.1005, loss (valid): 1.1080
epoch: 4, iteration: 25, loss (train): 1.0440, loss (valid): 1.0905
epoch: 5, iteration: 30, loss (train): 1.0267, loss (valid): 1.0754
epoch: 6, iteration: 35, loss (train): 1.0065, loss (valid): 1.0648
epoch: 7, iteration: 40, loss (train): 0.9888, loss (valid): 1.0561
epoch: 8, iteration: 45, loss (train): 0.9728, loss (valid): 1.0495
epoch: 9, iteration: 50, loss (train): 0.9644, loss (valid): 1.0435
epoch: 10, iteration: 55, loss (train): 0.9523, loss (valid): 1.0390
epoch: 11, iteration: 60, loss (train): 0.9513, loss (valid): 1.0311
epoch: 12, iteration: 65, loss (train): 0.9559, loss (valid): 1.0225
epoch: 13, iteration: 70, loss (train): 0.9353, loss (valid): 1.0170
epoch: 14, iteration: 75, loss (train): 0.9267, loss (valid): 1.0104
epoch: 15, iteration: 80, loss (train): 0.9171, loss (valid): 1.0026
epoch: 16, iteration: 85, loss (train): 0.9142, loss (valid): 0.9918
epoch: 17, iteration: 90, loss (train): 0.9190, loss (valid): 0.9785
epoch: 18, iteration: 95, loss (train): 0.9013, loss (valid): 0.9652
epoch: 19, iteration: 100, loss (train): 0.8918, loss (valid): 0.9508
epoch: 20, iteration: 105, loss (train): 0.8834, loss (valid): 0.9364
epoch: 21, iteration: 110, loss (train): 0.8678, loss (valid): 0.9217
epoch: 22, iteration: 115, loss (train): 0.8653, loss (valid): 0.9038
epoch: 23, iteration: 120, loss (train): 0.8406, loss (valid): 0.8899
epoch: 24, iteration: 125, loss (train): 0.8401, loss (valid): 0.8718
epoch: 25, iteration: 130, loss (train): 0.8218, loss (valid): 0.8577
epoch: 26, iteration: 135, loss (train): 0.8110, loss (valid): 0.8416
epoch: 27, iteration: 140, loss (train): 0.8112, loss (valid): 0.8259
epoch: 28, iteration: 145, loss (train): 0.7880, loss (valid): 0.8126
epoch: 29, iteration: 150, loss (train): 0.7768, loss (valid): 0.7979
>>>

トレーニングが終了したので、目的関数の出力値(交差エントロピー誤差)と分類精度を各々グラフ化します。先ず目的関数の出力値を表示します。

>>> 'exec(%matplotlib inline)'
'exec(%matplotlib inline)'
>>> import matplotlib
>>> matplotlib.use('Agg')
>>> import matplotlib.pyplot as plt
>>>

4行目の matplotlib.use(‘Agg’) は、私はこの演習をCLI(Command Line Interface)環境で作業しているため実行結果をウィンドウ表示できないので、matplotlib のバックエンドをAggにすることで回避し、あとで plt.savefig() で任意の画像ファイルに保存しています。GUI環境で作業されている方は無視してください。

>>>  # 目的関数の出力 (loss)
>>> plt.figure() # 何も描画されていない新しいウィンドウを作成
<Figure size 640x480 with 0 Axes>
>>> plt.plot(results_train['loss'], label='train') # label で凡例の設定
[<matplotlib.lines.Line2D object at 0x7f6b5e656dd8>]
>>> plt.plot(results_valid['loss'], label='valid') # label で凡例の設定
[<matplotlib.lines.Line2D object at 0x7f6b5e5e2198>]
>>> plt.legend() # 凡例の表示
<matplotlib.legend.Legend object at 0x7f6b5e8e9b38>
>>> plt.savefig( 'fig1_Loss.png' ) # fig1_Loss.png として保存
fig1_Loss.png

目的関数の出力値(交差エントロピー誤差)をグラフ化したものは以下になります。

横軸がエポック数、縦軸が目的関数の出力値です。トレーニングが進むにつれて目的関数の出力値(誤差)が減っていることから、トレーニングがうまくいっていることがわかります。
次に、分類精度(accuracy)のグラフを作成します。

>>> # 分類精度の出力 (accuracy)
>>> plt.figure()
<Figure size 640x480 with 0 Axes>
>>> plt.plot(results_train['accuracy'], label='train')
[<matplotlib.lines.Line2D object at 0x7f6b5c5ccc88>]
>>> plt.plot(results_valid['accuracy'], label='valid') 
[<matplotlib.lines.Line2D object at 0x7f6b5c5d2080>]
>>> plt.legend()
<matplotlib.legend.Legend object at 0x7f6b5c5a8e10>
>>> plt.savefig( 'fig2_Accuracy.png' )
fig2_Accuracy.png


横軸がエポック数、縦軸が分類精度です。こらのグラフでもトレーニングが進むにつれて分類精度が上昇していることから、トレーニングがうまくいっていることがわかります。

テストデータを用いた評価
前段でトレーニングが終わったので、トレーニング済ネットワークを使い、テスト用データに対する評価を行います。先ず、テスト用データで予測を行いますが、検証用データのとき同様 chainer.using_config(‘train’, False)chainer.using_config(‘enable_backprop’, False) を使います。

>>> with chainer.using_config('train', False), chainer.using_config('enable_backprop', False):
...     y_test = net(x_test)

予測ができたら分類精度を計算し、確認します。

>>> accuracy_test = F.accuracy(y_test, t_test)
>>> accuracy_test.array
array(0.5555556, dtype=float32)

ネットワークの保存
一通り作業が終了したので、トレーニング済のネットワークを my_iris.net という名前で保存します。

>>> chainer.serializers.save_npz('my_iris.net', net)

念のため、保存されたかを確認します。

root@790e21b683b1:/# ls -a
.  ..  .dockerenv  bin  boot  dev  etc  home  lib  lib64  media  mnt  my_iris.net  opt  proc  root  run  sbin  srv  sys  tmp  usr  var
root@790e21b683b1:/#

トレーニング済ネットワークを用いた推論処理

最後にトレーニング済ネットワークを用いて、テスト用データに対する推論処理を行います。
先ず、保存したネットワーク my_iris.net を読み込みます。 そのためにトレーニング済ネットワークと同様のクラスのインスタンスを作成します。

>>> loaded_net = Sequential(
...     L.Linear(n_input, n_hidden), F.relu,
...     L.Linear(n_hidden, n_hidden), F.relu,
...     L.Linear(n_hidden, n_output)
... )

このインスタンスに対し、トレーニング済ネットワークのパラメータを読み込ませます。

>>> chainer.serializers.load_npz('my_iris.net', loaded_net)

トレーニング済ネットワークの準備ができたら、実際に推論処理を行います。推論するときには検証用データのとき同様、 chainer.using_config(‘train’, False)chainer.using_config(‘enable_backprop’, False) を使用します。

>>> with chainer.using_config('train', False), chainer.using_config('enable_backprop', False):
...  y_test = loaded_net(x_test)
...

テスト用データに対する推論処理が終了したらテスト用データの 0 番目のサンプルの予測結果を確認します。
分類では以下のようにして、予測されたラベルを出力できます。

>>> np.argmax(y_test[0,:].array)
2

予測結果は 2 であることが確認できました。

Chainerを使って、あやめを分類をするためにモデル定義、学習、推論という一連の流れを見てきました。今回はここで終了です。
前回同様にフレームワークを利用することで効率的にディープラーニングできることが理解できたのではないかと思います。

今回の演習で使用したデータセットはデータの件数、パラメータが少ないため、CPUで実行しても短時間で処理が終了しましたが、大規模なデータセットであったり、中間層が多いようなネットワークの場合には処理時間が長くなります。そのようなときはGPUを利用することで高速化が可能になります。
Chainerのチュートリアルの応用編(15. Chainer の応用)にはGPUを使ったトレーニングの方法が記載されていますので、ご参照ください。
参考:15. Chainer の応用

次回予告 第5回:Pytorchでニューラルネットワーク

これまで利用してきたフレームワークと比べると比較的新しいフレームワークであるPytorch(パイトーチ)の利用方法を紹介します。

GPUサーバー