nykergoto’s blog

機械学習とpythonをメインに

ニューラルネットは何を見ているか ~ ブラックボックスモデルの解釈可能性

ディープラーニングによる予測は特に画像分野において顕著な性能を示していることはご案内のとおりです。これは ResNet や BatchNormalization といった技術の開発により多数のレイヤが重なった大きなモデルに対しても学習を行うことが可能になったことが理由の一つです。

一方で、ネットワーク構造が複雑になってしまったがゆえに別の問題も生じています。 それはなぜディープラーニングが性能が良いのか、どうやって判別を行っているのかということについてよくわからない、という問題です。 最近では人間が見ると元の画像とはほとんど変わらないのに出力結果が大きく変化してしまう画像(敵対的サンプル)を生成できてしまうことを示した論文なども登場しています。

というわけで今回は、内部でどうなっているかよくわからないモデルに対して、その解釈性を与えることに関する論文 Interpretable Explanations of Black Boxes by Meaningful Perturbation を紹介します。

解釈性とは何か

さて僕はさらっと解釈性、という単語を用いましたが、一体解釈性とは何でしょうか。ここで中身がどうなっているかわからないブラックボックスな予測モデル $f$ を以下のように定義します。

$$ f: X \to Y $$

ここで $X$ は入力値の取りうる空間, $Y$ は出力の空間です. 例えば画像の二値分類であれば入力は rgb の画像 $x : {\cal A} \times \mathbb{R}^3$ となります。ここで ${\cal A} = \left\{1, \ldots, H \right\} \times \left\{1, \ldots, W \right\}$ で $H, W \in \mathbb{N}$ は画像の縦横のピクセル数です. 一方出力 $Y$ は $\left\{-1, +1\right\}$ のラベルとなります.

解釈性とは特定の入力画像にたいしての出力が予測するルールのこと、と表現することができます。 例えば以下のような条件を満たす $Q_1$ というルールを得ることができた!とします。

$$ Q_1(x ;f) = \left\{ x \in X_c \iff f(x) = +1 \right\} $$

ここで $X_c \subset X$ は入力画像のうち $+1$ を取る画像集合であり, $X$ の部分集合です. このルール $Q_1$ の要素が $1$ の値を取る画像であれば $f$ も 1 を返し, 逆も成り立ちます。

以下の期待値を考えること、このルールがどの程度正しいのかを知ることも可能です.

$$ {\cal L_1} = \mathbb{E}_{x \sim p(x)} [1 - \delta_{Q_1}] $$

ここで $\delta_Q$ は $Q$ に$x$ が含まれているときに 1 その他のとき 0 を返す標示関数です.

${\cal L_1}$ は $Q_1$ が成立しない確率です。すなわち $f$ が $+1$ の画像に対して正しく分類ができてるの? という解釈性になっている、というわけです。そしてなぜ $f$ に仮定をおかず解釈性の議論をすることが出来るかというと条件の左側 $x \in X_c$ という関係が明瞭であるからなのです。

同じような形式で、出力同士を比べることでも、解釈性を考えることは可能です。 例として, $f$ がある回転角度 $\theta$ に対して不変な出力を持つのかということを確かめたいとします. この時のルール $Q_2$ は以下のようになります.

$$ Q_2(x, x'; f) = \left\{ x \sim_{\theta} x' \Rightarrow f(x) = f(x') \right\} $$

先と同様に期待値を取ればルールの正しさを知ることができます。

$$ {\cal L_2} = \mathbb{E}[1 - \delta_{Q_2}] $$

この ${\cal L_2}$ は $f$ が $\theta$ の回転に対して出力が変わらないのか? という解釈がどの程度成立するか、ということの表現になっています。

ではこれを拡張して, 画像の特定の領域を削ってしまうような関数 $h: X \to X$ があるとします。 そしてこの $h$ が中身がよくわからない $f$ の出力に関与しているのか? ということを知りたいとしましょう。

先程までの議論同様に考えると, 以下のようなルール $Q_3$ を考えれば良いことがわかります.

$$ Q_3(x; f, h) = \left\{ x' = h(x) \Rightarrow f(x) \neq f(x') \right\} $$

このルールが成立する確率が高くなるような $h$ が削っている領域が $f$ の出力をより決定している領域です。 ざっくりと言ってしまうと $f$ がよく見ている部分 とでも言うことが出来るでしょうか。

ルールが決まったのであとはロスを考えれば良いのですが、$h$ が極端な値を取られると困ります。例えば画像を 1px 間隔で削るような関数があったとします。自然に考えて人間はそのように画像を解釈していませんから、解としてはあまりよろしくないですよね。このような不自然な削り方に対してペナルティを与えるため一般的な機械学習同様に $h$ に対して正則化の効果を与えるような項を付け加えます。

結局以下の最小化問題を解けば良いことになります。

$$ \min_{h \in {\cal H}} {\cal L}_3 := \mathbb{E}_{x \sim p}[1 - \delta_{Q_3(x; h, f)}] + {\cal R} (h) $$

ここで ${\cal H}$ は関数 $h$ が取りうる範囲を決める関数集合, ${\cal R}: {\cal H} \to \mathbb{R}$ はペナルティを計算する正則化を計算する関数です。

論文での目的関数

前置きが長くなりましたが論文の話に戻ります。

論文中では $h$ をどの領域をどのぐらい削るかで表現するために画像の各ピクセルに対して 0 から 1 の値を取るような $m: {\cal A} \to [0, 1]$ を定義し以下のような関数 $\Phi: X \to X$ を用いて画像情報を削除しています。

$$ [\Phi(x_0; m)](u) = m(u) x_0(u) + (1- m(u))\mu_0 $$

ここで $u \in {\cal A}$ は画像の各要素を表しており, $\mu_0$ は平均の色の値を表します。 これを用いたロス関数は以下のようになっています。 論文ではこれの他にガウスノイズを乗せたものや ぼかしフィルタによる定式化も提案されていますが、ノリとしてはほぼ一緒です。

これを用いて目的関数を記述すると以下のようになります。

$$ {\cal L}(x) = \lambda_1 \| 1 - m\|_1 + \lambda_2 \sum_{u \in X} \| \nabla m(u) \| + \mathbb{E}_{\tau \sim [0, 4]} \left[ f(\Phi(x_0(- \bullet \tau): m)) \right] $$

第一項は必要な部分だけを 0 にしているか (すべての要素を 0 にすれば入力の情報がゼロになり出力が変わるのは自明) という意味です。

第二項は $m$ の変化が自然かどうか (画像方向に対して勾配がきびしくないか) という正則化項です。

第三項は $\Phi$ による入力画像の加工に加えて, $[0, 4)$ の一様分布に従う確率変数 $\tau$ によって入力画像の信号が弱まる様になっています。これは入力画像の一部分に対する過学習を起こさないために付け加えられています。

この目的関数を用いて勾配法をつかって最適化を行います。

実験

github 上に pytorch 実装が上がっていたので, fork していろいろいじってみました。

https://github.com/nyk510/pytorch-explain-black-box

pytorch が動く環境であれば以下のコマンドで実験を行います。第一引数が対象となる画像です. サンプルに入っている画像は下の猿の画像です。吠えてます。

f:id:dette:20180207170713j:plain

これを使って学習をしてみます。

python main.py examples/macaque.jpg
run with gpu
------------------------------
Category with highest probability: 373 - 96.07 %
Optimizing..
finish!
------------------------------
after masked probability: 1.26 %
save to output/macaqueperturbated.png
save to output/macaqueheat_map.png
save to output/macaquemask.png
save to output/macaquecam.png

はじめ 96% で分類できていた画像が加工後には 1,26% にまで落ち込んでいます。

学習されたマスクをかけた画像は以下のようになっています。

f:id:dette:20180207170827p:plain

人間の目にはほとんど変化が無いように見えます。敵対的サンプルの画像と似たような画像を生成していることがわかります。 マスクを掛けている領域はこんな感じ。猿の頭部分を見て判定しているのかな?といった印象ですね。

f:id:dette:20180207171220p:plain

感想

  • 正則化の与え方が論文を読んだ感じかなりヒューリスティックなので改良の余地はあるかも
  • 最適化方法を二次の情報を用いた手法 (たとえば L-BFGS など)にすればより早く収束しそう

Pycharmの便利機能まとめ

ショートカット

一般

コマンド 内容
Shift + ⌘ + Delete 最後に更新した場所にジャンプ
Shift + ⌘ + F7 現在開いているファイル内で、カーソルが当てられている変数が使用されている場所をハイライト。その状態で ⌘ + G を押すとつぎに使われている場所にジャンプできる。
⌘ + ↑ ナビゲーションバーにカーソルが移動する。同じフォルダの他のファイルを開きたいときに, 左のディレクトGUIをマウス操作しなくて済む。⌘+ ↓でソースにカーソルを戻せる。
⌘ + K コミットの変更と差分をGUIで見れる。

関数, クラスの情報

この関数の定義どうなってるのか, 変数何使えばいいのか, とかのときに使えるコマンド

コマンド 内容
⌘ + P パラメータ情報の取得
⌘ + B 関数が定義されているファイルを開く
F1 クイックドキュメントを開く

Code

コマンド 内容
Option + ⌘ + T その部分を制御構文で囲う。ある処理を try/except でエラーキャッチしたいときに便利。(他にも while とか try/finally とかもある。)

Refactor

コマンド 内容
Control + O 継承クラスでの関数のオーバーライドをGUI上で選択できる。
Shift + F6 変数名の変更。その変数が使われているファイルを自動で検索してすべて置換できるのでとても便利🍺

補間

コマンド 内容
Control + O 変数名の補間。

便利機能

編集履歴

git とは別に Pycharm 自体が編集履歴を保存している。 VCS -> Local History -> Show History を開くと過去の編集ログを一括でみることができる.

git には残らない細かい粒度でのログを見たいときや, 前の状態に戻したいけど commit してない... みたいな事故が起こったときに使える 。(本当はそういうことが起こってはだめですが)

.gitignore の自動生成

⌘+N で新規ファイル作成をする際, 下の方にある .ignore を選択し git を選ぶと gitignore ファイルの生成を行える。 用いている言語によってそれぞれ ignore が設定されているので、必要な物を検索してチェックボックスにチェックを入れるとよしなな ignore が完成する。

Terminal

Option + F12 でプロジェクトフォルダに

pycharm での docstring の書き方

参考: https://www.jetbrains.com/help/pycharm/2017.1/type-hinting-in-pycharm.html

基本的に python において docstring の書き方は一通りではないが、 pycharm が推奨する docstring の形式があり, それに準拠すると型チェックを自動でおこなってくれる.

シンタックス 意味
Foo Class Foo visible in the current scope
x.y.Bar Class Bar from x.y module
Foo | Bar Foo or Bar
(Foo, Bar) Tuple of Foo and Bar
list[Foo] List of Foo elements
dict[Foo, Bar] Dict from Foo to Bar
T Generic type (T-Z are reserved for generics)
T <= Foo Generic type with upper bound Foo
Foo[T] Foo parametrized with T
(Foo, Bar) -> Baz Function of Foo and Bar that returns Baz
list[dict[str, datetime]] List of dicts from str to datetime (nested arguments)

def put_image_to_s3(image,
                    bucket_name,
                    directory="image",
                    extension="JPEG"
                    logdir=None):
    """
    PIL形式の画像を s3 に upload する
    :param Image.Image image: PIL image file
    :param str bucket_name: upload先のバケット名
    :param str directory: バケットでのディレクトリ
    :param str extension: ファイルの拡張子. 今は `JPEG` のみ対応
    :param str | None logdir: 
        アップロード中のログを出力するディレクトリへのパス. 
        `None` のときログを出力しない. 
    :return: url to uploaded image file
    :rtype str
    """
    # 以下実装 ~~

Dockerによる深層学習及び機械学習環境構築

機械学習だろうがなんだろうが同じですが、環境構築するところだけで結構めんどくさいことが多いです。 今回はそのへんをまるっと管理できる仮想環境ツールのDockerを用いた環境構築の手順を記していきます。

目次

  • 執筆環境
  • Dockerとは
    • 機械学習を行う上でDockerを使うメリット
    • Dockerのインストール
  • Nvidia Docker
    • 必要条件
    • Install
    • Nvidia-Dockerを使う
    • つくる
    • 二回目以降
    • オプション
  • まとめ

書いていないこと: GeForce GTX 1080 Ti にドライバを入れるまでの作業

執筆環境

Dockerとは

一言でいうと仮想環境です。 今使っているOSとは別のOSを起動するようなものなので、自分の環境を汚すことなくアプリケーションを導入することが可能になります。 Dockerはアプリケーションの単位で仮想環境を作るので

  • 起動時にディスクやメモリを消費しない(使った分だけ消費する)
  • OSレベルの仮想化を行っているため、起動が早い

ことなどが特徴です。 Docker ではイメージとコンテナという概念があり、ざっくり言ってしまうと

  • イメージ: 設計図
  • コンテナ: 設計図を元に作った実際の環境

という関係です。 (プログラム的に言うと、イメージがクラスに、コンテナがインスタンスと言った感じでしょうか) ユーザーは

  1. 設計図を Dockerfile に記述し
  2. docker build でイメージとして登録し
  3. docker run でコンテナを作成

と言ったフローで利用することになります。 とてもざっくりとした説明で厳密には間違っている気しかしないので、詳細はチュートリアルやドキュメントを参照してください。

機械学習を行う上でDockerを使うメリット

一番のメリットは OSの環境設定をコードとして管理できる という点にあります。

機械学習や深層学習を行う際には主に python を用いることが多いですが、はじめに pyenv, vertualenv, anaconda などの環境設定をし、その次に pip で必要なライブラリを導入し、その際に必要なパッケージは apt-get してエラーが出たらググッてパス設定やパッケージ導入をして… など再現性に乏しい環境が出来やすいです。 そのため。新しいサーバーやPCを購入した際、以前と全く同じ環境を用意することができず、前は動いたのに今は動かない、何がおかしいのかよくわからない、などということが発生しがちです。

一方で Docker では、インストールの手順やどのOSを使うかなどは Dockerfile に記述することができます。 そのため Dockerfile さえあれば全く同じ環境を任意のPCで作成することができます。 しかも docker build だけで。これはとてもうれしいですね。なのでやりましょう。

Dockerのインストール

$ sudo apt-get -y install apt-transport-https ca-certificates curl software-properties-common
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
$ sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
$ sudo apt-get update
$ sudo apt-get install docker-ce

docker に実行権限を与えるため docker グループにユーザーを追加します。

$ sudo adduser <user-name> docker

例えばユーザー名が tiger なら以下のように実行します

[tiger@local ]$ sudo adduser tiger docker
[tiger@local ]$ sudo reboot # 再起動して反映させる
[tiger@local ]$ groups
tiger adm cdrom sudo dip plugdev lpadmin sambashare docker

インストールが終わったら docker が動くことを確認します。 docker run -it hello-world を実行します。 これは hello-world イメージのコンテナを作るコマンドです。 基本的に docker run <image-name> でコンテナが起動します。 あとはオプションがごにょごにょつくだけなのでとても簡単ですね。

$ docker run -it hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
5b0f327be733: Pull complete 
Digest: sha256:175735360662078abd70dacb73c5518d5b3ae7c1ed069d22def5da57c3e917d6
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://cloud.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/engine/userguide/

Nvidia Docker

基本的に丁寧に wiki にまとまっているので困った時は wiki を見ると良いかなと思います。

必要条件

  • kernel version > 3.10
  • Docker >= 1.90
  • NVIDIA drivers >= 340.29

これにプラスアルファで、自分のドライバがどのバージョンか、というのはどの docker image を用いるのかに関係してきます。 ドライバの確認は nvidia-smi すると確認できます。 以下は僕の環境ですが、これだと 381.22 がドライバのバージョンです。

[tiger@local ]$ nvidia-smi
Sat Nov 11 06:15:33 2017       
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 381.22                 Driver Version: 381.22                    |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|===============================+======================+======================|
|   0  GeForce GTX 108...  Off  | 0000:01:00.0      On |                  N/A |
| 58%   70C    P2   257W / 280W |  10918MiB / 11171MiB |     98%      Default |
+-------------------------------+----------------------+----------------------+
                                                                               
+-----------------------------------------------------------------------------+
| Processes:                                                       GPU Memory |
|  GPU       PID  Type  Process name                               Usage      |
|=============================================================================|
|    0      1074    G   /usr/lib/xorg/Xorg                             885MiB |
|    0      2201    G   compiz                                         331MiB |
|    0      2611    G   ...el-token=469C79572BB0987284DA4C6B5EF5678D   112MiB |
|    0     13986    G   unity-control-center                             2MiB |
|    0     24944    C   python                                        9497MiB |
|    0     26329    G   ...el-token=EC61E31D5E5DEE53F6E56703F908C373    82MiB |
+-----------------------------------------------------------------------------+

GPU がどのバージョンの CUDA にまで対応しているのか、は wiki にもまとまっています。 (CUDA > Requirements)

CUDA toolkit version Driver version GPU architecture
6.5 >= 340.29 >= 2.0 (Fermi)
7.0 >= 346.46 >= 2.0 (Fermi)
7.5 >= 352.39 >= 2.0 (Fermi)
8.0 == 361.93 or >= 375.51 == 6.0 (P100)
8.0 >= 367.48 >= 2.0 (Fermi)
9.0 >= 384.81 >= 3.0 (Kepler)

これを見ると 381.22 では CUDA8 までしか利用できないことがわかります。 (僕は Keras & Backend で Tensorflow を用いることが多く、現時点においては Tensorflow は CUDA8 までしかサポートされていない為、バージョンをあげていません。)

Install

Nvidia-Docker の公式 のまんまです。 (僕の場合特に何事もなくインストールできましたが、ディストリビューションによっては大変らしいのでここでコケると辛そう)

wget -P /tmp https://github.com/NVIDIA/nvidia-docker/releases/download/v1.0.1/nvidia-docker_1.0.1-1_amd64.deb
sudo dpkg -i /tmp/nvidia-docker*.deb && rm /tmp/nvidia-docker*.deb

最新版入れたいよーとか昔のやつ試すーという時は release ページ から rpmwget してきます。

Nvidia-Dockerを使う

docker コンテナを作成するときのコマンドを docker run から nvidia-docker run に置き換えるだけです。 そうするとよしなに内部でホスト側の GPU 環境とコンテナ内の環境を橋渡ししてくれるので、コンテナ内部でも GPU が使えるよーという仕組み(なはず)です。

試しに nvidia-smi がコンテナ内部でも動くか確認します。 以下はCUDA8のイメージをpullしているので、CUDA8対応以下のドライバが入っている場合には、タグの部分 8.0-devel を書き換える必要があります。

# Test nvidia-smi
$ nvidia-docker run --rm nvidia/cuda:8.0-devel nvidia-smi
8.0-devel: Pulling from nvidia/cuda
ae79f2514705: Already exists 
c59d01a7e4ca: Pull complete 
41ba73a9054d: Pull complete 
f1bbfd495cc1: Pull complete 
0c346f7223e2: Pull complete 
5dcd01667896: Pull complete 
ca677f607487: Pull complete 
b4637619a887: Pull complete 
8c644ff287da: Pull complete 
Digest: sha256:10edf770586c33dde900618c929f7e1c49197a618c7133452a4bed0bfdfa0478
Status: Downloaded newer image for nvidia/cuda:8.0-devel
Fri Nov 10 21:27:16 2017       
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 381.22                 Driver Version: 381.22                    |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|===============================+======================+======================|
|   0  GeForce GTX 108...  Off  | 0000:01:00.0      On |                  N/A |
| 57%   70C    P2   251W / 280W |  11041MiB / 11171MiB |     96%      Default |
+-------------------------------+----------------------+----------------------+
                                                                               
+-----------------------------------------------------------------------------+
| Processes:                                                       GPU Memory |
|  GPU       PID  Type  Process name                               Usage      |
|=============================================================================|
+-----------------------------------------------------------------------------+

ちゃんとGPUを認識しているのでOKです。

つくる

では実際に環境を作ります。

git clone git@github.com:nyk510/docker-zoo.git
cd docker-zoo
docker build -t penguin ./penguin

Dockerfile の中身は以下のようになっています。

FROM nvidia/cuda:8.0-cudnn6-devel-ubuntu16.04

LABEL maintainer="nyker <nykergoto@gmail.com>"

# update
RUN apt-get update

# install utilities
RUN apt-get install -y sudo git zsh openssh-server wget gcc libatlas-base-dev

# userの追加
RUN groupadd -g 1000 developer

# user:penguin, password:highway
RUN useradd -g developer -G sudo -m -s /bin/bash penguin
RUN echo "penguin:highway" | chpasswd

# 以下は penguin での操作
USER penguin
WORKDIR /home/penguin/

# docker build のディレクトリにあるファイル (.) を, カレントディレクトリ (`./`) にコピー
ADD . ./

# pyenv
RUN git clone git://github.com/yyuu/pyenv.git .pyenv
ENV HOME /home/penguin
ENV PYENV_ROOT ${HOME}/.pyenv
ENV PATH ${PYENV_ROOT}/shims:${PYENV_ROOT}/bin:${PATH}

RUN pyenv install anaconda3-5.0.1
RUN pyenv rehash
RUN pyenv install miniconda3-4.3.30
RUN pyenv rehash
RUN pyenv global anaconda3-5.0.1

# mxnet
RUN pip install mxnet-cu80

# keras
RUN pip install keras==2.0.9 tensorflow-gpu

# peco
RUN wget https://github.com/peco/peco/releases/download/v0.5.0/peco_linux_amd64.tar.gz
RUN tar -zxvf peco_linux_amd64.tar.gz
ENV PATH ${HOME}/peco_linux_amd64/:${PATH}

やってることはユーザーの作成と pyenvでの環境 + anaconda + miniconda / mxnet(gpu) / keras-tensorflow(gpu) のインストールです。 基本的には, シェルを記述しているだけなのでとてもシンプルです。 自分で好きなライブラリを入れまくりましょう。

build が終わったらコンテナを作成して、コンテナ内部でちゃんと GPU を認識しているか確認しましょう。 この時 nvidia-docker で run することをお忘れなく。 nvidia-smi が動けば成功です。

[tiger@local]$ nvidia-docker run -it --name penguin-sample penguin zsh
[penguin@ed5b133fe0fd ~]$ nvidia-smi
Sat Nov 11 01:16:02 2017
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 381.22                 Driver Version: 381.22                    |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|===============================+======================+======================|
|   0  GeForce GTX 108...  Off  | 0000:01:00.0      On |                  N/A |
| 54%   69C    P2   154W / 280W |  11101MiB / 11171MiB |     72%      Default |
+-------------------------------+----------------------+----------------------+

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

二回目以降

nvidia-docker run でコンテナをつくってログアウトするとコンテナは停止状態になります。 もう一度コンテナに入りたいときは

  1. docker start <container-name> でコンテナを起動
  2. docker exec -it <command> でコマンドを実行

という手順を取ります。 二回目以降は nvidia-docker で入る必要はありません。 なので先の penguin-sample コンテナの例で行くと以下のようになります。

[tiger@local ]$ docker start penguin-sample
penguin-sample
[tiger@local ]$ docker exec -it penguin-sample zsh
[penguin@ed5b133fe0fd ~]$

またこの時コンテナは一度目の情報を保存しています。なのでコンテナ内部で何かファイルを作成したり、環境変数を変えたり、apt-getしたりするとそれは次回以降も反映されます。

オプション

普通にコンテナを起動することはできました。 でも今の penguin-smaple コンテナはローカル環境と完全に閉じているのでファイルのやり取りなどができない状態になっていてちょっと不便です。 docker では -v を用いてディレクトリを bind することができます。 使い方は -t <path/to/local>:<path/to/container> です。 この時パスはフルパスを指定する必要があることに注意してください。 ~/hoge:/hoge とかは動きません。

例えばローカル環境で /data というディレクトリがあり、そこにデータセットなどがダウンロードされているとします。これをコンテナと接続しようと思うと以下のようになります。

[tiger@local ]$ nvidia-docker run -it -v /data/:/data --name penguin-with-data penguin zsh
[penguin@397c1018a407 ~]$ cd /data
[penguin@397c1018a407 /data]$ ls
data1   data2   mnist

まとめ

僕も内部の事とかよくわかってませんが、さくっと使うだけでもとても価値のあるサービスだと思うので、インフラ以外でも流行るといいなと思います。

環境構築って大変ですしね。

ニューラルネットにおける変数の初期化について

最近 keras をつかって色々とやることにはまってます。

tensorflow や chainer と比較して keras だとネットワーク記述から fit までが完結に記述できてとても気に入っています。 そのときにドキュメントや実装を読んだりもしますが、ネットで自分がやりたいタスクで検索をかけて似たようなことをやっている人のコードを参考にしています。

その時、keras レイヤーの重みの初期化の引数に he_normal というのを指定しているのを見かけました。
ドキュメントを参照すればこの関数の定義式自体はわかりましたが、この関数が何をやってるのか、なぜこの関数にするとうれしいのか、平均0分散1のガウス分布ではだめなのか、ということがわからず困ってしまいました。

ニューラルネットワークにおける変数の初期化の方法について全く知らなかったので、ちょうどいい機会とおもい各種論文を読んでみたので、その内容をまとめていきます。

黎明期

黎明期、という言い方はあれかもしれませんが、まだ relu も登場していない時代の話から始めます。 この頃は、ヒューリスティックに以下のような分布が一般に用いられていたようです。

$$ W_{j, k}^{i} \sim U \left[ - \frac{1}{\sqrt{n_i}}, \frac{1}{\sqrt{n_i}} \right] $$

ここで $W_{j,k}$ は 第 $i$ レイヤーの重みの $(j, k)$ 成分、 $U[-a, a]$ は下限 $-a$, 上限 $a$ の一様分布, $n_i$ は第 $i$ 番目の隠れ層の数を表しています。またバイアス項はすべて 0 とします。

この関数をどのぐらい積極的に使っていたのかは、その当時に僕が機械学習をやっていなかったのでわかりませんが、んとなくこういう分布が経験的によいと知られていたのでしょうか。

この経験則に対し、活性化関数が linear なときという仮定のもとで議論を行い、新しい重みの分布の提案を行った論文が Understanding the difficulty of training deep feedforward neural networks で、これは今では一般に Glorot の一様分布と呼ばれているものです。

Glorot の一様分布 (Glorot Uniform)

Glorot の一様分布と呼ばれている初期化の方法では、第 $i$ 番目のレイヤーに対して以下のような分布から重みを選びます。

$$ W \sim U \left[ -\frac{ \sqrt{6}}{\sqrt{n_i + n_{i+1}} }, \frac{ \sqrt{6}}{\sqrt{n_i + n_{i+1}} } \right] $$

ここで $n_i$ は第 $i$ 番目の隠れ層の数を表します。 これだけ言われても「その6どこからきたの?」とかいろいろ聞きたいことがありますよね。 以下では提案論文を元に、なぜこの分布が導出されたかを追っていきましょう。

導出

ニューラルネットワーク上の第 $i$ 番目のレイヤーについて考えます。 第 $i$ レイヤーの活性化関数を $f$, レイヤーへの入力ベクトルを $x^i \in {\mathbb R^{n_i}}$, 活性化関数を通す前の出力ベクトルを $y^i \in \mathbb R^{n_{i+1}}$, レイヤーの重みを $W^i \in \mathbb R^{n_{i+1}, n_{i}}$ とします。ここで $n_i \in \mathbb N$ は第 $i$ レイヤーの入力次元を表しています。 以上の定義を用いると、以下の関係が成り立ちます。

$$ y^{i} = W^{i} x^i + b^i \\ x^{i+1} = f(y^i) $$

つぎにネットワークの学習について考えたいので、backpropagation で必要とされるネットワークの誤差 $C$ に対する偏微分を考えていきます。すると以下を得ます。

$$ \frac{\partial C}{\partial y_j^i} = f'(y_k^i) W_j^{i+1} \frac {\partial C}{\partial y^{i+1}} \\ \frac{\partial C}{\partial w_{j, k}^i} = x_k^i \frac{\partial C}{\partial y_j^i} $$

ここで、初期化された段階において, 活性化関数は線形性を持っていて, 重みは独立に初期化されていて, 入力変数はすべて同じ分散 ($Var[x]$) を持つ, という仮定を加えます。

すなわち活性化関数に関しては $f(x) = x, f'(0) = 1$ が成り立っている, ということを意味します(これは yigmoid や tanh など原点対称な活性化関数で成り立ちます)。 また重みに関しては独立に初期化されているので, 第$i$ 層の重みに対して $Var[w^i_{j, k}]$ が一定です。これを以下では $Var[W^i]$ と表記します。

このとき第 $i$ 層の出力の分散 $Var[x^i]$ を考えてみましょう. はじめに第 $i+1$ のレイヤと $i$ における $Var[W^i]$ の関係性を考えると

$$ \begin{aligned} Var[x^{i+1}] &= Var[f(y^i)]\\ &= Var[y^i] \\ &= Var[W^i x^i + b^i] \\ &= Var[W^i x^i] + Var[b^i] \\ &= n_i Var[W^i] Var [x^i] \end{aligned} $$

となります. これを繰り返し用いると

$$ Var[x^i] = Var[x] \prod_{l=0}^{i-1} n_{l} Var[W^l] $$

を得ます。 したがって全てで $d$ 層あるネットワークにおいては, 第 $i$ 層の出力に対する微分について以下が成立します。

$$ \begin{aligned} Var \left[\frac{\partial C} {\partial y^i} \right] &= Var\left[f'(y_k^i) W_j^{i+1} \frac {\partial C}{\partial y^{i+1}} \right] \\ &= n_{i+2} Var\left[W^{i+1} \right] Var \left[\frac {\partial C}{\partial y^{i+1}} \right] \\ &= \left( \prod_{l=i}^{d} n_{l+1} Var[W^l] \right) Var\left[ \frac{\partial C}{\partial y^{d}} \right] \end{aligned} $$

また重みに対する微分に対して、以下が成立します。

$$ \begin{aligned} Var \left[ \frac{\partial C}{\partial W^i} \right] &= Var\left[x_k^i \right] \times Var \left[ \frac{\partial C}{\partial y_j^i} \right] \\ &= Var[x] \prod_{l=0}^{i-1} n_{l} Var[W^l] \times \left( \prod_{l=i}^{d} n_{l+1} Var[W^l] \right) Var\left[ \frac{\partial C}{\partial y^{d}} \right] \end{aligned} $$

今度は、ネットワークの出力の伝搬について考えて行きます。 出力の伝搬のとき、各レイヤーを通してもその出力は無限に発散したり、0に減衰したりするとただしくロスを計算できないため困ります。その為各レイヤーの出力 $x^i$ は同じ分散を持つことが好ましいです。つまり

$$ \forall (i, i'), Var[x^i] = Var[x^{i'}] $$

が成り立っていてほしい、ということになります。 また逆伝搬についても同様に

$$ \forall (i, i'), Var\left[ \frac{\partial C}{\partial y^i}\right] = Var\left[ \frac{\partial C}{\partial y^{i'}}\right] $$

がなりたっている必要があります。 これらを先の方程式に代入すると以下の条件に変形できます。

$$ \forall i, n_i Var[W^i] = 1 \\ \forall i, n_{i+1} Var[W^i] = 1 $$

この2つの条件の中間をとろう、というのが Glorot の提案する分布です。すなわち

$$ \forall i,\ Var\left[W^i \right] = \frac{2}{n_i + n_{i+1}} $$

を満たすような確率分布から重みを初期化します。 ではこのような分散を持つような一様分布とはどんな分布でしょうか、というのに答えるのが、この章の先頭に示した分布です。再掲すると以下の分布でした。

$$ W \sim U \left[ -\frac{ \sqrt{6}}{\sqrt{n_i + n_{i+1}} }, \frac{ \sqrt{6}}{\sqrt{n_i + n_{i+1}} } \right] $$

$[-a, a]$ の範囲の一様分布の分散は $ a^2/3 $ ですから

$$ \frac{1}{3} \left(\frac{ \sqrt{6}}{\sqrt{n_i + n_{i+1}}} \right)^2 = \frac{2}{n_i + n_{i+1}} $$

となって、ピッタリさきの分散に一致するように選ばれています。 一見どこから来たかわからない 6 という数字にも意味があったのですね。

一様分布以外にも, この分散を取り平均が 0 であるガウス分布 (Glorotのガウス分布) も提案されています。

まとめ

Glorot の分布の議論の仮定、特徴をまとめると以下のようになります。

  • 活性化関数が原点対称かつ原点周りで線形であることを仮定して議論
  • 順伝搬, 逆伝搬で各層の値がおなじになる、ということを制約としている
  • $i$ 番目のレイヤーの初期重みを $2 / (n_i + n_{i+1})$ を分散に持つような分布から選ぶ

問題点

きれいに片付いたかに見えた初期化分布問題ですが、Glorot は活性化関数に大きな仮定をおいていました。それは

  • 原点対称である
  • 原点付近で線形である

ということでした。 またネットワークの構造についても全結合のネットワークが対象であり CNN のようなカーネルを用いたレイヤーは想定されていません。 というわけで relu とか CNN で解析してみたよ、というのが次に紹介する He の正規分布と呼ばれているものです。*1

He の正規分布 (He normal)

He の正規分布は第 $i$レイヤーの初期重みを 平均0, 分散 $2/n_i$ の正規分布から設定します。 すなわち

$$ Var[W^i] = \frac{2}{n_i} $$

を満たします。 この分散を持つべき理由を導出していきましょう。

導出

まずネットワークの順伝搬 (Forward Propagation) について考えていきます。 第 $i$ 層の CNNの出力は

$$ y^i = W^i x^i + b^i $$

と書くことができます。 ここで $x \in \mathbb R^{c k^2}$ は $c$ チャネルの $(k, k)$のピクセルを持つ入力です (以下では簡易化のため $n = c k^2$ と記述します)。 また $W \in \mathbb R^{d, n}$ は各行がフィルタの重みとなるような $d$ のフィルタを表します。 $b \in \mathbb R^n$ はバイアス項, $y\in \mathbb R^d$ は出力マップの値です。 $f$ を活性化関数とすると

$$ x^{i+1} = f(y^i) $$

が成り立ちます。 ここでつぎの3つを仮定として加えます。

  • バイアス $b$ は 0 とする。
  • $x^i$ の各要素が独立同分布である
  • $x^i$ と $W^i$ の各要素は独立である。
  • $W^i$ の分布の期待値は 0 である。

すると

$$ \begin{aligned} Var[y^i] &= Var[W^i x^i + b^i] \\ &= Var[W^i x^i] \\ &= n_i Var[w^i x^i] \\ &= n_i \mathbb E[ (w^i x^i)^2] - n_i \mathbb E [w^i x^i]^2 \\ &= n_i \mathbb E[ (w^i)^2] \mathbb E[ (x^i)^2] - 0 \\ &= n_i Var[w^i] \mathbb E[ (x^i)^2] \end{aligned} $$

と変形できます。 ここで $\mathbb E [x^i]^2$ は $x^i$ の期待値が 0 でない限り分散に一致しません。 というのも $ Var [x^i] = \mathbb E [(x^i)^2] + (\mathbb E [x^i])^2 $ ですから

$$ Var[x^i] - \mathbb E[ (x^i)^2] = (\mathbb E [x^i])^2 \ne 0 $$

となるのです。 これはたとえば活性化関数が relu のときに起こります。 なぜならば relu 関数は $ {\rm relu}(y) = \max (0, y) $ですから $x^i = {\rm relu}(y^{i-1}) = \max (0, y^{i-1})$ となりこれは明らかに期待値が 0 にはなりません.

ここで $w^{i-1}$ が 0 のまわりで対称な分布であったとし、また $b^{i-1} =0$とします。すると $y^{i-1} = W^{i-1} x^{i-1}$ と $W, x$ の独立性の仮定より $y^{i-1}$の各成分は平均 0 の対称な分布となります。 したがって

$$ \mathbb E (x^i)^2 = \mathbb E [\max (0, {y^{i-1}}^2)] = 0 + \frac{1}{2} \mathbb E[(y^{i-1})^2] = \frac{1}{2} Var[y^{i-1}] $$

と変形することができます。 これより

$$ Var[y^i] =\frac{1}{2} n_i Var[w^i] Var[y^{i-1}] $$

を得ます。これを繰り返し用いると第 $L$ 層の出力 $y^L$ の分散は

$$ Var[y^L] = Var[y^1] \left( \prod_{i=2}^L \frac{1}{2} n_i Var[w^i] \right) $$

となります。 この後半の積の部分が 1 でなければ、層が増えていくにしたがって、指数的に無限大に発散もしくは0に収束してしまうことになります。 よってこの部分は 1 にっていることが望ましいです。 したがって

$$ Var[w^i] = \frac{2}{n_i}\ \forall i $$

を満たすような分布を用いて、重みの初期化を行うことが望ましいといえます。 今回は順伝搬についての議論でしたが、逆伝搬に対してもほぼ同様の議論ができ

$$ Var[w^i] = \frac{2}{n^{i+1}}\ \forall i $$

が得られます。 Glorotのときにも同じような結果となったので、まあそうなるよね、といったところでしょうか。 このような分散を満たす分布を keras では he_normal と呼んでいます。 Glorot のときと同様, これの一様分布 version の he_uniform もあったりします。

ちなみに、議論中 relu を通した期待値を計算するところで prelu を用いると以下が得られます。

$$ Var[w^i] = \frac{1}{1 + a^2} \frac{2}{n^i}\ \forall i $$

ここで $a$ は prelu の係数です。 prelu は $a=1$ のとき linear な関数に一致しますが, その時 Glorot のときに出てきた式と一致します。 また $a=0$ のとき prelu は relu と一致し, 初期重みの式も一致しています。

比較

さて大きくわけて2つの初期化分布を紹介しました。 これの違いは単純に分散が二倍ちがうだけです。

$$ \begin{aligned} Glorot&: \frac{2}{n_i + n_{i+1}}\\ He&: \frac{2}{n_i} \end{aligned} $$

対して違わないようですがこの初期化分布のズレは、層を重ねるごとに掛け算されていきますから、指数的に影響を及ぼします。ですから深いネットワークになればなるほど、この違いが顕著に現れるのです。 というのを実験している結果が以下になります。(参考文献 2 Figure 3 より引用)

f:id:dette:20171015120652p:plain:w600
n_layer = 22 のとき

赤いラインが He の正規化を行ったネットワークでの学習を表しています。 青いラインは凡例では Xavier となっていますがこれは Glorot の初期化の別名です。 これを見ると 22 layers のときは He のほうがよさそうですがまあ甲乙つけがたい感じとなっています。 しかしこれが 30 layers になると差が明らかになります。

f:id:dette:20171015120816p:plain:w600
n_layer = 30 のとき

Glorot の初期化では学習が全く進んでいない一方、 He の初期化では epoch 数はかかっているものの、先と同様に学習ができている事がわかります。 このことから層が深くなると初期化の影響が大きくなるという主張がある程度裏付けられた形となっています。 初期化って適当に選んでいましたが、大事なんですね。

まとめ

ざっくりというと以下の事がわかりました。

  • 初期化アルゴリズム Glorot と He は活性化関数に対する仮定が違う
    • Glorot: 原点周りにおいて線形な活性化関数
    • He: relu
  • ちゃんと初期化アルゴリズムを選ばないと、特に深いネットワークの学習を行う際に痛い目をみる

適当に使うとだめってことがわかったので、調べたかいがあったかな?

参考文献

  1. Understanding the difficulty of training deep feedforward neural networks, Xavier Glorot, Yoshua Bengio ; Proceedings of the Thirteenth International Conference on Artificial Intelligence and Statistics, PMLR 9:249-256, 2010.
  2. Delving Deep into Rectifiers: Surpassing Human-Level Performance on ImageNet Classification, Kaiming He, Xiangyu Zhang, Shaoqing Ren, Jian Sun

*1:https://arxiv.org/pdf/1502.01852.pdf この論文は活性化関数 prelu の提案論文でもあったりします。