アホが自動運転をしたら盛大に事故るので気をつけたほうがいい

はじめに

最近は社会的にも自動運転という技術が注目されており、多くの自動車メーカーが必死で研究開発を行なっている。
Googleも手を出してはいたが、諦めた的なニュースをどこかで見たのも記憶に新しい現時点では色々な課題があり、まだまだ実現が難しいと考えられている分野だと思う。
私は免許こそ持ってはいるが、自分への自信が全くないので人をはねてしまうのではないかと不安になってしまうため、車を運転することが大の苦手だ。そんな私にとっては自動運転とは夢の技術であり、マイスイートハニーと私を乗せ、綺麗な海岸沿いを華麗に自動運転してくれる超クールな車を手に入れたいと思うのは仕方がないことだと思う。
そこで、今回は車の購入なしでかつ、年間8万円の自動車保険に入らずに自動運転を楽しめる超エキサイティンなツール(?)を紹介する。

udacity drive講座

udacity driveとは

知っている人は知っているかとは思うが、現在udacity driveという下記の講座があり、そこでは自動運転とそれに必要な機械学習の技術を学べるような内容となっているっぽい(受ける予定も特にないので、あまり詳しく調べていない)
www.udacity.com
この講座の受講料は800ドルと比較的お安い講座な方ではあると思うが、日々クレジットカードの引き落としで毎月15000円くらいしか手持ちに残らない極貧民には少しお高い。
だが、現在この講座で使用されるっぽい自動車シミュレーターのソースコードが一部github(下記)にて公開されている。本当に素晴らしい。
github.com

講座で使う機械学習の環境構築

環境構築は極めて簡単。下記でanacondaの環境は整う。
(参照:https://github.com/udacity/CarND-Term1-Starter-Kit/blob/master/doc/configure_via_anaconda.md)

git clone https://github.com/udacity/CarND-Ter​​m1-Starter-Kit.git
cd CarND-Ter​​m1-Starter-Kit
conda env create -f environment.yml
conda info --envs
source activate carnd-term1

anacondaが嫌な方向けにdockerイメージも用意されている。
dockerはあまりよくわからないので、やり方は下記を参照して欲しい。
CarND-Term1-Starter-Kit/configure_via_docker.md at master · udacity/CarND-Term1-Starter-Kit · GitHub

講座で使う自動車シミュレーターのインストール

さて、次は自動車シミュレーターのインストールに移りたいと思う。
下記にインストールの仕方が載っている。コンパイル済みのものもあるが、Mac初心者なので「身元がわからないから開いてやらねー」という感じのエラーが出てよくわからなかったので、ここでは説明しない。
github.com

1. 下記のgit lfsをインストールしてローカルにダウンロードする。
Git Large File Storage - Git Large File Storage (LFS) replaces large files such as audio samples, videos, datasets, and graphics with text pointers inside Git, while storing the file contents on a remote server like GitHub.com or GitHub Enterprise.

git lfs clone https://github.com/udacity/self-driving-car-sim.git

2. Unityを使っているので、インストールがまだなようであれば、Unityをインストールする。
3. Unityを起動して、self-driving-car-simフォルダを選択してロードする。
4. 左下の[プロジェクト]タブに移動し、Asset/1_SelfDrivingCar/Scenesフォルダに移動してSceneをロードします。
湖トラックを走りたい場合は、LakeTrackTraining.unityファイルをロードする。
5. 再生ボタンのようなものをクリックすると、ゲームがスタートする。

とここまでで簡単にゲームができるまでにはなっていると思う。
ちなみに操作方法は上矢印で進む、下矢印でバック&ブレーキ、左右で曲がるというシンプルなものになっている。

運転データの作り方

ゲームを開始すると、右上にRECORDと記載された赤丸がある。そこをクリックすると運転データの保存先の選択画面が出る。
ここで、保存したい場所を選択して、[select]ボタンを押し、もう一度右上の赤丸をクリックすると赤丸が一時停止のアイコンに変わる。そうなったら、運転データの保存が開始されている状況となっている。
ある程度運転データが取れたら、右上の一時停止のアイコンをクリックする。すると、リプレイみたいなのが流れ始めて、順次ファイルへと保存されていく。

自動運転とは?

自動運転をさせるためのスクリプト

この自動車シミュレーターを用いて、自動運転を実現するためのスクリプトは下記にある。
github.com

具体的には、次の2つのファイルがアップロードされている。
・video.py(録画用のスクリプト
・drive.py(車を運転するスクリプト

video.pyを動かすには下記のパッケージが必要なので、インストールすべし。
Installing imageio — imageio 2.1.2dev documentation
インストールしたら、pythonを起動して、下記を実行する。

>>> import imageio
>>> imageio.plugins.ffmpeg.download()

README.mdを読む限りはこの2つだけではなく、下記のファイルを作ってもらいたいらしい。
・model.py(モデルの作成と訓練に使用されるスクリプト
・model.h5(訓練されたKerasモデル)

このmodel.pyとmodel.h5をどのようにして作ればいいのかを考える。
drive.pyを確認してみると、何やらそれっぽいmodel.predictがある。

@sio.on('telemetry')
def telemetry(sid, data):
    if data:
        # The current steering angle of the car
        steering_angle = data["steering_angle"]
        # The current throttle of the car
        throttle = data["throttle"]
        # The current speed of the car
        speed = data["speed"]
        # The current image from the center camera of the car
        imgString = data["image"]
        image = Image.open(BytesIO(base64.b64decode(imgString)))
        image_array = np.asarray(image)
        steering_angle = float(model.predict(image_array[None, :, :, :], batch_size=1))

        throttle = controller.update(float(speed))

        print(steering_angle, throttle)
        send_control(steering_angle, throttle)

send_controlに操舵角度(steering_angle)とスロットル(throttle)の情報を送れば動くっぽいことがわかる。
READMEにも画像データから操舵角度を予測するモデルを設計、訓練、検証すると書いているし、drive.pyにmodel.predictによる予測値が格納されているのは操舵角度だけなので、操舵角度を予測するのが目的で正しいはず。
※操舵角度だけでなく、スロットルとかも予測させることで難易度を高めることはできそうだが、ここでは取り扱わないことにする。

KerasどころかChainerもろくに触っていない初心者がとりあえず、書いてみたのが下記のコードとなる。

import numpy as np
from PIL import Image
import csv

from keras import backend as K
from keras.models import Sequential
from keras.layers import Dense, Dropout, Activation, Flatten
from keras.layers import Convolution2D, MaxPooling2D
from keras.optimizers import Adam
from keras.preprocessing.image import load_img, img_to_array

batch_size = 32
nb_classes = 1
nb_epoch = 5 

# 入力画像の大きさ
img_rows, img_cols = 160, 320
# 畳み込みフィルタの値
nb_filters = 32
# max poolingのサイズ
pool_size = (2, 2)
# 畳み込みカーネルのサイズ
kernel_size = (3, 3)

imgs = []
val = []
# とりあえずcsvのログを開く
with open("./data/driving_log.csv","r") as f:
  reader = csv.reader(f)
  for row in reader:
    # リストで画像を格納する
    imgs.append(np.array(Image.open(row[0])))
    # 操舵角度の情報を格納する
    val.append(np.float32(row[3]))
# numpyでappendを頑なに拒み、ここで変換するアプローチをとった
val = np.array(val)
imgs = np.array(imgs)

input_shape = (img_rows, img_cols, 3)

# この辺はKerasのチュートリアルのCNNの実装をパクった記憶がある
model = Sequential()
model.add(Convolution2D(nb_filters, kernel_size[0], kernel_size[1],
                        border_mode='valid',
                        input_shape=input_shape))
model.add(Activation('relu'))
model.add(Convolution2D(nb_filters, kernel_size[0], kernel_size[1]))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=pool_size))
model.add(Dropout(0.25))

model.add(Flatten())
model.add(Dense(128))
model.add(Activation('relu'))
model.add(Dropout(0.5))
# よくわからんけどとりあえず、Denseで1次元の出力作ればいいだろと思った結果
model.add(Dense(nb_classes))
# よくわからんけど(ry
model.add(Activation('linear'))
adam = Adam()
model.compile(loss='mean_squared_error', optimizer=adam)
# なんか画像のCNNで正規化してるのを見つけたので、なんとなく追加した気がする
imgs = imgs.astype('float32')
imgs /= 255

model.fit(imgs, val, batch_size=32, nb_epoch=nb_epoch)
model.save('model_data.h5') 

ログはdriving_log.csvという名前のcsvファイルとして指定した場所に保存され、中身の要素は多分下記のように並んでいる。

列の情報 左側のカメラ 中央のカメラ 右側のカメラ 多分操舵角 わからん わからん 多分速度
データの型 画像ファイルへの絶対パス(文字列) 画像ファイルへの絶対パス(文字列) 画像ファイルへの絶対パス(文字列) 数値 数値 数値 数値

7000枚程度の画像データをCPUで走らせて、待つこと約4日。モデルが完成!
実際に走らせてみる。走らせる方法は簡単で、シミュレーターを起動し、[ESC]キーを押して、AUTONOMOUS MODEを選択し、下記のコマンドを実行する。(順序は逆でも問題ない)

python drive.py [作成したKerasのモデル] [ここにディレクトリを指定すると正面のカメラ画像が出力される]

python video.py [正面カメラの画像を置いてあるディレクトリ]を指定することで、mp4の動画データを作成できる。

結果

出来上がった録画データをGoogleフォトにアップロードした結果がこれである。
https://goo.gl/photos/pXqraNuZz24CV5W98

心なしか左に曲がってはいる気がするが、最終的には急カーブを曲がり切れず、湖にドボンしてしまった。
ディープラーニング弱者には自動運転なんて夢のまた夢ということがわかったので、これから精進していきたいと思った。
とりあえず、皆さんもチャレンジしてみてはいかがだろうか。私はもうDeep Learningはいいです。GPU買ったらやります。

参考文献

Kerasのドキュメント
Keras Documentation

機械学習モデルの予測結果を説明するための力が欲しいか...?

はじめに

最近はAIや機械学習などの単語がビジネスで流行っていて、世はAI時代を迎えている。QiitaやTwitterを眺めているとその影響を受けて、世の多くのエンジニアがAIの勉強を始め出しているように見受けられる。

さらに、近年では機械学習のライブラリも充実しており、誰でも機械学習を実装することができる良い時代になってきた。

その一方で、特徴選択を行い精度を向上させたり、機械学習の出した答えがどの特徴に基づいて判断されたのかを理解したりするには、モデルに対する理解やテクニックが必要となる場合も多々ある。複雑なモデルになると人間には解釈が困難で説明が難しい。近頃流行りのDeep Learning系のモデルだと頻繁に「なんかよくわからないけどうまくいきました」となっていると思う。

一般的なエンジニアとしては、この点が割と課題なんじゃないかと勝手に思っている。というか、私が課題に感じている。(特に実業務で機械学習していない上に、エンジニアでもないが)

そんなわけで、今回はこの課題を解決するためのツールであるLIME(Local Interpretable Model-agnostic Explainations)が興味深かったので、紹介していこうかと思う。
※本記事はLIMEのアルゴリズムの説明となるため、LIMEを実際に利用したい方はGitHub - marcotcr/lime: Lime: Explaining the predictions of any machine learning classifierpythonのライブラリインストール方法とチュートリアルが載っているので、そちらをご参照ください。

モデルの説明とは何か

LIMEの紹介に移る前に機械学習モデルを説明するとはどういうことなのか整理していきたい。
機械学習モデルの説明には下記の説明の2種類が考えられる。

  • explaining prediction(予測の説明)

データ一つに対する機械学習モデルの分類器による予測結果に対して、どうして分類が行われたのかを説明すること。(下記の図はイメージ)
f:id:gat-chin321:20170107164420p:plain
(出典: https://arxiv.org/pdf/1602.04938.pdf)

  • explaining models(モデルの説明)

分類器がどういう性質を持っているのかを説明すること。
https://d3ansictanv2wj.cloudfront.net/figure2-802e0856e423b6bf8862843102243a8b.jpg
(出典: Introduction to Local Interpretable Model-Agnostic Explanations (LIME) - O'Reilly Media)

LIMEはこのうち、explaining predictionを行うためのアルゴリズムである。
explaining modelsについては、SP-LIMEと呼ばれるアルゴリズムが論文に記載されているので、そちらを参照されたし。(気が向けば、SP-LIMEについても記事を書く)

LIME(Local Interpretable Model-agnostic Explainations)の紹介

LIMEとは?

KDD2016で採択された『“Why Should I Trust You?” Explaining the Predictions of Any Classifier』というタイトルの論文で発表されたアルゴリズム。分類器がどのように判断してラベリングを行なったのかを人間でも解釈できるような形で提示してくれる。
このアルゴリズムはあるデータを分類した結果、それぞれの特徴がどの程度分類に貢献しているかを調べることで分類器の予測結果を説明している。また、分類器の予測結果を用いるため、任意の分類器に適用できる特徴がある。

LIMEのアイデア

データxの周辺からサンプリングしたデータを用いて、説明したい分類器の出力と近似するように解釈可能な(かつ単純な)モデルを学習させる。その後、得られた分類器を用いて分類結果の解釈を行う。下記がイメージ図(論文から抜粋した図を編集)。

f:id:gat-chin321:20170106191925p:plain
(出典: https://arxiv.org/pdf/1602.04938.pdf)

説明用分類器の学習方法

説明用分類器 gはデータxの周辺でfの結果と近似するようにしたい。
そうするために、下記の目的関数を利用して学習する。

{\displaystyle 
\DeclareMathOperator*{\argmin}{arg\,min}
\begin{equation}
\xi(x) = \argmin_{g \in G} L(f, g, \pi_x) + \Omega(g)
\end{equation}
}

  • G : 解釈可能なモデルの集合
  • g : Gのうちの一つのモデル。例えば、線形モデルなど
  • f : 説明したい分類器
  • \pi_x : データxとの距離
  • {\displaystyle L(f, g, \pi_x)} : データxの周辺でfgの結果がどれだけ違っているか(Lは損失関数ともいう)
  • {\displaystyle \Omega(g)} : 説明用分類器gの複雑さ

上記の内容から、\xi(x)はデータxの周辺でfgの結果についての食い違い{\displaystyle L(f, g, \pi_x)}と説明用分類器gの複雑さの和を最小にする g の集合を求めるものであると言える。
ここで、{\displaystyle \Omega(g)}はテキスト分類の場合、解釈可能なモデルの特徴表現を単語の有無{0,1}のBag-of-Words法(単語袋詰め)とし、単語の数(次元数)に限度Kを設定することで、説明が解釈可能であることを保証するためのものらしい。
画像データの場合はsuper-pixelsと呼ばれる任意のアルゴリズムを使用して計算されるものを用いて解釈可能なモデルの特徴表現とする。
ここで、この特徴表現は{0,1}の2値で表され、1は元のsuper-pixels、0はグレーアウトされたsuper-pixelsを示す。
ここまでで\xi(x)について、何となくというレベルでは理解ができたと思いたい。
そこで、次は{\displaystyle L(f, g, \pi_x)} の数式についても見ていこう。

{\displaystyle 
L(f, g, \pi_x) = \sum_{z,z' \in Z } \pi_x (z) (f(z) - g(z'))^2
}

  •  Z :  xの周辺のデータの集合
  •  z' : 非ゼロ要素を一部だけ含むサンプリングにより生成された2値のスパースな点。

  z' \in \{0,1\}^dで定義される

  •  z :  z'を用いて復元された元のサンプルの特徴表現。 z \in R^dで定義される

この式を見る限り、 xの周辺のデータにおける\pi_x (z)で重み付けした残差平方和を出している。
残差平方和自体は正解データ(今回の場合、説明したい分類モデルの予測結果)と推定モデルの予測結果との間の不一致を評価する尺度なので、わかりやすいかと思う。
また、\pi_x (z)で重み付けしている理由について理解するため、\pi_x (z)の式を見ていこう。

{\displaystyle 
\pi_x (z) = exp\Bigl(\frac{-D(x,z)^2}{\sigma^2}\Bigr)
}

  •  D(x,z) :  x zとの距離関数(例えば、テキストならコサイン類似度、画像ならL2ノルムなどを利用する)
  •  \sigma : 指数カーネルカーネル

\pi_x (z)の式はカーネル関数であり、xとzの2変数間の類似度を算出している。\pi_x (z)はテキトーに0から1までの値を入れて見て計算すればわかると思うが、サンプルが近ければ近いほど値が小さくなる。これで重み付けすることで、 x zとの距離が近いサンプルの場合は損失{\displaystyle L(f, g, \pi_x)}が小さくなりやすくなり、逆に距離が遠いサンプルの場合は損失が高くなる。この重み付けのおかげで、ロバストなモデルとなっている。
最後は\Omega(g)について掘り下げていく。\Omega(g)の式を見ていこう。

{\displaystyle 
\Omega(g) = \infty \mathbb{1} [||w_g||_0 > K ]
}

\Omega(g)は利用する特徴がたかだか単語数(もしくはsuper-pixels)K程度だけとすることを示しているっぽい。
利用する特徴\Omega(g)の選択は、方程式\xi(x)から直接解くことで実現することは難しい。
そのため、まず著者らがK-Lassoと呼んでいる、Lassoで正則化パスを使用して利用する特徴をK個選択し、最小二乗法を介して重みを学習する方法によって、利用する特徴\Omega(g)の選択についての解と近似させる。
これにより、方程式\xi(x)を解くことができるようになるため、線形モデル(Githubのコードを読む限りではRidge回帰)で学習を行う。
この学習した線形モデルの偏回帰係数を確認することで、選択された特徴について、どれだけ分類に貢献しているかの説明を行うことができる。

ここまで、説明した内容が下記の図のAlgorithm 1 である。
f:id:gat-chin321:20170107152919p:plain
(出典: https://arxiv.org/pdf/1602.04938.pdf)

Algorithm 1 は個々の予測についての説明を生成するので、その複雑さはデータセットのサイズに依存するのではなく、 f(x)を計算する時間とサンプル数 Nに依存するらしい。

検証と考察

検証もどき

今回はマルウェアと正常なプログラムのAPIコールのデータセットが手元にあったので、著者らのLIMEパッケージを使ってみることにした。
データセットは下記からとってきたものだ。
Malicious datasets * - Csmining Group

データセットの内容はファイル形式がcsvマルウェアの数が320検体、正常なプログラムの数が68検体という微妙な数となっている。

簡単な検証の結果は下記の通りだった。
github.com

検証用にランダムフォレストを使ってマルウェアと正常なプログラムを分類した。
ランダムフォレストを選んだ理由は、直線ではない分離境界を引いてくれてかつ、そのモデル自体が重視している特徴を出せるからだ。他にいい分類器があれば教えていただきたいところ。
脳細胞が死んでいるので、データを学習用(マルウェアが310検体と正常なプログラム68検体)とテスト用(マルウェアが10検体)に手で分けた。
そのテスト用の予測結果はf1_scoreが1.0となった。マルウェアと正常なプログラムのAPIコールを用いた分類は割と線形分離可能なものが多い印象なので、交差検証とかしていない上に、テスト数も少ないのでこんなもんではあるとは思う。

考察もどき

結果の考察だが、LIMEで出力された "GetFileAttributesW"、"GetFullPathNameW"、"GetLongPathNameW"
などの特徴が、ランダムフォレストの特徴ランキングの上位に食い込んでいることがわかる。
LIMEで出力されるのは、そのデータ単体のどの特徴を重視して分類したかであるexplaining predictionsにあたり、ランダムフォレストの特徴ランキングは多分explaining modelsなので、厳密に比較すべきではないかもしれない。
しかし、モデル自体が重視している特徴はexplaining predictionsの上位に来ても直感的にはおかしくないと思うので、いい感じになんか説明できている気がする。

参考文献

LIME論文:

“Why Should I Trust You?” Explaining the Predictions of Any Classifier
https://arxiv.org/pdf/1602.04938.pdf

LIMEコード:

github.com