終末 A.I.

Deep Learning を中心に、機械学習するエンジニアのブログ

「AIって何ができるの?」に答えることがなぜ難しいのか

一応、機械学習エンジニアという肩書で働いているということもあって「AIって何できるの?」と仕事中に質問を受けることがよくあります。

画像認識などの具体的なものを示して、「こういうタスクならできます」と返すことが多いのですが、 具体的過ぎて、AI関連技術の可能性を感じてもらう、面倒臭さを知ってもらうには今ひとつ説明不足だなと感じています。

一方で、何をどう説明するとそれをイメージできるようになるかというのも難しい問題で、AIという言葉が持つややこしさ、機械学習技術の厄介さが絡まって、非常に説明しにくいことになっています。

この記事は、その辺りの説明が難しいと感じている部分を、自分の中でも整理するためにまとめたものです。

同じ悩みを抱えている方、「AIって何ができるの?」と疑問に思われている方のご参考になれば幸いです。

目次

AIとはそもそも何なのか

そもそもAI・人工知能とは何でしょうか?

その疑問について調べるため、人工知能学会が監修した「人工知能とは」を参照してみましょう。

人工知能とは (監修:人工知能学会)

人工知能とは (監修:人工知能学会)

この本では、トップレベルの研究者の人工知能に対する考えがまとめられており、「知能とはなにか?」や「人工知能とはなにか?」といった質問に研究者一人一人が答える形式でまとめられた本になります。

松尾先生執筆の「はじめに」には「人工知能とは、人間のような知能を、コンピューターを使って実現することを目指した技術あるいは研究分野」という定義が紹介されています。

しかしこの本を読み進めていくと(「はじめに」も書かれているように)、研究者間でも人工知能に対する意見の統一がなされていないということがよく分かります。

何を作る事ができたら人工知能と呼べるものになるかだけでなく、そもそも「知能」とは何かというレイヤーでの意見の違いがあることが見て取れます。

そしてこの傾向は、研究者だけでなく一般の人たちの間でも同様ではないかと思います。

そのため、「こういうことができたら人工知能って言えるよね」や、「人工知能というからにはこれくらいできるだろう」など、「AIって何ができるの?」に対する説明の出発点と着地点から意識合わせを始めなければいけません。

このことは、「AIって何ができるの?」に答えることを難しくしている要因の一つになっていると考えられます。

AIに必要な要素

では、「人工知能には統一の見解がないなら、何ができるかわからないのは仕方ないな」となるかというと、残念ながら当然そうはいきません。

説明が求められているのは抽象的な概念の話ではなく、巷で話題になっているAIについてです。実際に説明を求めている人も、そこに焦点を当てて「何ができるのか?」と質問されてきていることでしょう。

こうなると一番簡単なのは、AIと呼称されているものの事例をあげることですが、始めに書いたとおりこの方法ではあまり本質的なことを伝えることはできません。

説明を求めている人もそこまで深い理解は欲していないかもしれませんが、可能であればより深い理解を持って頂き、活用方法などの次のステップの議論をスムーズに行えるようになって欲しいわけです。

そこで、AIを捉えるために共通に使用しやすいフレームを導入したいと思います。それは「認識・判断・行動」という3つのモジュールから知能は構成されるというフレームです。

これは、三宅陽一郎さんの「人工知能の作り方」で、エージェント・アーキテクチャに関する説明で紹介されている、知能とは「認識の形成」「意思の決定」「運動の構成」を行うモジュールで構成されている説明をより単純化したものです。

また、対話システムでは「自然言語理解」「対話管理」「自然言語生成」の各モジュールにシステムの構成を分けることが一般的で、この構成も「認識・判断・行動」のフレームにまとめて捉えることができます。

人工知能の作り方 ――「おもしろい」ゲームAIはいかにして動くのか

人工知能の作り方 ――「おもしろい」ゲームAIはいかにして動くのか

先程「人工知能のに関する統一の見解は存在しない」と述べたばかりでなんのことやとなられたかと思いますが、 このフレームは現実に「AI的」と人に判断されるために試行錯誤している分野のシステム(エージェントと呼ばれるようなシステム)を包括的に説明する際に非常に扱いやすいものとして使われており、また、いわゆるAI系のシステムの多くもこの枠組で説明することができます。

例えば、見に付けている洋服の画像をもとにコメントを返す、ファッションチェックシステムを考えてみましょう。

非常に「AI的」なシステムですが、このシステムは以下の3つのモジュールから構成されます。

  • 認識:服の認識(種類や色、サイズ感など)
  • 判断:服の組み合わせによる得点の決定
  • 行動:得点や認識した服に基づいてコメントを生成

このシステムは、非常に良いバランスで今のAIシステムを表していて、認識・行動・判断のそれぞれのモジュールで必ずしも機械学習技術が使用されているわけではありません。

認識は機械学習を利用した画像認識モジュールを使われることも多いでしょうが、 判断の部分は機械学習にするかルールベースにするか五分五分、 行動の部分は人手でテンプレートを用意して、それに変数をはめていくルールベースのプログラムで実装されることがほとんどだと思われます。

AI≒機械学習と捉えることも最近では多いですが、上記のようにシステム全体として見たときに全て機械学習で構成されていることはかなり珍しいと言っても良いでしょう。

むしろ、チャットボットやゲームAIのように、振る舞いをセンシティブにコントロールしたいようなシステムや機械学習がそこまで得意でない領域のシステムは、全てのモジュールが人手で作ったルールやアルゴリズムをプログラムで実装していることの方が多いのではないでしょうか。

また、組合せ最適化のような高度な情報科学の技術が使用されているが、問題にアプローチするためのアルゴリズムは人手で与えているというようなシステムも多くあります。

ここで重要なことは、知的に見えるかどうかということに(つまり人工知能と言えるかどうかに)、必ずしも実装技術は関係ないということです。Deep Learning機械学習バズワード化してしまったため、この認識を合わせることが必ずしも容易ではないことが、説明を難しくしている要因の一つだなと感じています。

機械学習技術とはどういうものなのか

さて上記まででは、主に人工知能に焦点を当てていましたが、ここからは機械学習についての話に移りたいと思います。なぜなら、機械学習の代替語としてAIが使用されることも多くなってきたためです。

とはいえ、機械学習についての説明が簡単かというとそうではありません。

私は、機械学習を「データを説明するためのルールの一部をデータをもとに生成する技術」と認識していますが、この説明は観念的で、機械学習が目指すものは説明できていますが、「今の」機械学習技術については説明できていません。

一方で、それを説明しようとすると、事例をもとに長々と説明する必要に迫られます。なぜなら、機械学習研究者も、自身が用いている手法について必ずしも挙動を全て説明することはできず、経験論的な説明しかできないためです。

アプリケーションとしてどのような振る舞いになるのかだけでなく、なぜその手法でデータに最適化できるのかといった手法の根本的な部分ですら、数学的な論理性で説明することができていないものも多く存在しています。

つまり、機械学習とは絶対こうなるという保証が難しい技術分野なのです。

一方で、事例ベースの説明だけでは、機械学習の可能性や困難さをうまく伝えきれません。一例ですが、下記のような点をうまく扱えないと感じています。

  • 実現されていないシステムであっても、データさえあれば実現できるかもしれない
  • 実現したいことに合わせて、データからルールを生成するためのルールを、新たに作る必要がある
  • 類似のシステムであっても、データの内容や種類が変わるだけで、そのまま適用するだけではうまくいかなくなる

まとめると、機械学習は、人間の試行錯誤とコンピューターの試行錯誤(計算処理)の両方が必要になる分野ということです。また、実現したいことに合わせてその試行錯誤が必要で、汎用的に適用できる手法があるわけではないという点も重要です。

これらは、「人工知能」ひいては「知能」という響きのもつ汎用性から大きく乖離してしまっています。

何でもできるわけではないが、何ができるかも断言できない。この点は、AIができることを説明することを難しくしている、最大の要因ではないかと思われます。

最後に

「AIって何ができるの?」に答えることがなぜ難しいのか、というテーマで、本記事では以下の3点を説明を難しくしているポイントとして挙げました。

  • 人工知能というからにはこれくらいできるだろう」という認識が人によって違う
  • 知的と感じる振る舞い(人工知能)を実現する方法は、機械学習に限らず、人手で設計したアルゴリズムでも可能
  • 機械学習技術は、何でもできるわけではないが、こんな事ができるとも断言できないものである

逆に、この3点をうまく伝えて、「AIって何ができるの?」に答えることは難しいんだ、と知ってもらうことでしか本質的に理解してもらう方法はないかなとも感じています。

いや、もっと難しい点はいっぱいあるだろや、こう説明すれば結構うまく伝わるなどあれば、ご指摘いただければ幸いです。

Deep Learningでターゲットに狙いを定めるWEBアプリを1時間で構築する

先月から年の瀬まで土日もあるようなないような忙しさで、 アドベントカレンダーを楽しむ間もなく気づいたら年の瀬です。

死蔵させるのももったいないので、記事を書く余裕があればやりたいなと思っていたクソアプリネタを晒させていただければと思います。

出来上がりはこちらソースコードは以下においてあります。

https://github.com/katsugeneration/tfjs-yolo-tiny-targeting-object

とりあえずターゲットに狙いを定めるWEBアプリを作る

今回は tensorflow.js を使用して、WEBアプリに物体検出を実装していきます。 tensorflow.js では既存の TensorFlow モデルをコンバートしてWEBブラウザや Node.js 上で使用することができます。

このモジュールを使用する基本的なフローは、

  1. 学習済みモデルを用意

  2. tensorflow.js 用にコンバート

  3. 必要な推論コードを実装

のようになりますが、世の中大変便利なもので、 tensorflow-models/coco-ssdtfjs-yolo-tinyなど、物体検出の用途を満たすtensorflow.js 用のモデルがサンプル付きで公開されているという状況です。

というわけで、この記事内でやることはもうほとんどありません。

今回は(本家のtensorflow-models/coco-ssdはなぜかメモリーリークが激しいため)、tfjs-yolo-tinyを使用して上記のWEBアプリを実装しましたが、

デモアプリの認識範囲の矩形を表示しているところを、ターゲットを示す画像に差し替えるだけでアプリとして必要な作業は完了です。

正直実装だけなら1時間もかからずにできてしまいます。

tensorflow-models/coco-ssd と tfjs-yolo-tiny の速度比較

tensorflow-models/coco-ssd には lite_mobilenet_v2 という ssd_mobilenet の SSD部分を軽量にし、

モデルサイズ及び推論時間をさらに軽くしたモデルが存在します。

また、yolo-tiny は高速な物体検出アルゴリズムである YOLO のネットワークを小さくし、さらなる高速化を実現したものです。

それぞれベースとなるモデルの速度は、こちらこちらにあるように、yolo-tiny の方が高速に動作するとされています。

tensorflow.js 上での速度を比較するために、ありもののモデルですが、以下の設定で速度比較をしてみましょう。

  • 入力:416×416 のWEBカメラ映像

  • 出力:COCOの80クラスの物体検出結果

結果は、 lite_mobilenet_v2 が約4FPS、yolo-tiny が約2FPSとなりました。ちなみに mobilenet_v2 は約2PSほどと yolo-tiny とほぼ同じ位の速度となりました。

tensorflow-models/coco-ssd のモデルは TensorFlow 公式のモデルであるため、tesnorflow.js に比較的最適化されているであろうことが考えられます。

また、本家のモデルの方も動作環境を実際にそろえた上での測定結果ではないということも考慮が必要でしょう。

実際、OpenCV の機能で各モデルを比較した記事では、YOLO Tiny V2 と SSD_Mobilenet_V2 の処理速度が同じくらいという結果になっています。

まとめ

機械学習周りは、発展スピードの速さもありますが、モデルや実装のオープン化が非常に進んでおり、学習結果を使うだけの敷居はどんどん下がってきているなという印象です。

一方で、オリジナルのデータを集めて学習させて、というあたりは Google の AutoML の進化はありますが、まだまだ普通のプログラミングに比べると敷居が高い印象です。

とはいえ、つい最近もこのような論文が話題を集めるなど、Meta Learning やこの技術を利用した Few Shot Learning 系の技術は恐ろしい速度で発展をとげています。

このあたりのハードルが来年以降どんどん下がっていくことも期待できるのではないでしょうか。

2018年風TensorFlowでの学習処理の記述方法

TensorFlowが登場して早いことで3年近く経とうとしています。 Deep Learning自体がブームになってからだと、それ以上の月日が経っているわけで、人工知能ブームも以外と続いているなあというのが正直な感想です。

Theanoやtorch、chainerに遅れをとって立ち上がったTensorFlowでしたが、はじめのうちはチュートリアルコードですらこのようなありさまで、とてもではありませんが簡単に誰もが使えるというような状態ではありませんでした。 1年ほど前からようやく、Keras の取り込みや Dataset API の実装、MonitoredTrainingSession のようなリッチな Session オブジェクトの導入などで、少し凝ったことをする場合でもかなり簡単に書けるようになってきました。

一方で公式のチュートリアルでは、データセットの読み込みはありもののAPIを使用するのが基本で、モデルの構築や学習処理もKerasのAPIのみや Eager Execution だけで解決できるようなシンプルな実装が多く、実践的にはどう書くとかゆい所に手が届きやすくなるのかがイマイチ掴みづらいところがあります。

どのような方法が現状でのベストプラクティスなのかわかりにくい状況ですが、自分用のメモも兼ねて、今回は自分がどのような考えで、どのように TensorFlow の学習処理を記述しているかを晒してみることにします。

学習処理の実装方針

今回のソースコードは以下にアップしています。

TensorFlow での学習処理を大まかに分けると、データの読み込み、モデルと学習処理の定義、学習環境の定義および学習の実行の3つに分けることができます。

一つずつどのような要件があれば良さそうかを考えてみます。

まず、「データの読み込み」で必要になるのは、様々な形式で保存されているデータセットを、適切な形に前処理しながらバッチ単位で読み出すというのがもりもりの要件になるかと思います。

データセットの読み込みや前処理自体は、あらかじめ tfrecord 形式に変更する際に実行したり、ファイルやDBへの書き込み内容そのものを事前に処理する方法もありますが、今回はそのような処理もまとめて学習時に実行する場合を想定します。

この時に活躍するのが tf.data.DataSet APIです。様々な形式からの読み込み、ストリーム処理による前処理、バッチサイズの指定やデータセットの繰り返しなど、データの読み込み時に発生する様々な処理をシームレスに実行する事ができます。

次に、「モデルと学習処理の定義」では、モデルグラフの定義および、ロスと学習処理の定義を行います。

モデルグラフの定義は、CNNやRNNなど実際に組むことになります。基本的には Keras の Layers API を使用することでほぼほぼ対応できますが、必要に応じて tf.Variable や tf.nn API を利用して自身でスクラッチで計算グラフを組んでいく必要もあります。

ロスと学習処理の定義は、ロスの計算の定義と tf.train.Optimizer を利用した勾配学習の定義を行える必要があります。 このあたりは、ニューラルネット系のフレームワークではクリティカルな部分でもあるので、簡単に実装できるようになっており、変わったレイヤー定義でもしない限りそこまで難しい部分ではなくなっています。

最後に、「学習環境の定義および実行」ですが、こちらを以下にシンプルに使いまわしやすく書いておくかが、学習処理をとっつきやすく開始できるかの鍵になるのではないでしょうか。必要なのは、学習の実行だけでなく、学習時のサマリーの記録、モデルの定期的な保存、定期的な学習ログの出力など学習処理に紐づく細かい実装が必要になります。

これらの実装には、MonitoredTrainingSessiontf.train.SessionRunHook を活用することでわかりやすく実現することができます。

Dataset API を利用したパイプラインの構築

上であげたように、データの読み込みパイプラインに必要な要素は以下のようになります。

  • ファイルなどからのデータの読み込み
  • データの前処理
  • バッチおよびデータの繰り返しなど学習上必要な処理

具体的にどのように TensorFlow の Dataset API で実現するかを一個ずつ見てみましょう。

ファイルからのデータの読み込み

まず、ファイルからのデータの読み込みは下記のようになります。

image_dataset = tf.data.FixedLengthRecordDataset(str(image_path), record_bytes=28*28, header_bytes=16)
label_dataset = tf.data.FixedLengthRecordDataset(str(label_path), record_bytes=1, header_bytes=8)
dataset = tf.data.Dataset.zip((image_dataset, label_dataset))

上記はローカルに保存した MNIST のデータセットを読み込んでいる箇所になります。 MNISTのデータはイメージデータとラベルデータがバラバラのファイルで管理され、それぞれにバイナリ形式で一定のバイト単位でデータが保存されています。

このようなデータを読み込む際に役に立つのが tf.data.FixedLengthRecordDataset クラスです。このクラスはまさに一定のバイト単位で保存されているデータを切り取って順番に読み出すことができます。

また、MNISTでは画像とラベルが別々のファイルに分かれているため実際の処理を行う前に結合しておく必要があります。tf.data.Dataset.zip メソッドは、python の組み込み関数である zip のように、複数のデータセットの出力をタプル形式でまとめて出力できる Dataset オブジェクトを返してくれます。

データの前処理

データの前処理で必要なものは、主にフィルタリングと学習で使用する形式への変換(Data Augmentation 含む)です。 以下は、Dataset の filter メソッドおよび map メソッドを利用してそれらを実現している例です。 呼び出し自体は、それぞれ実際の処理を行う関数を渡すだけで実現できます。

dataset = (dataset
           .filter(converter.filter)
           .map(converter.convert, num_parallel_calls=threads))

実際の処理は、下記のように Dataset の出力を引数に受けて、filter の場合 bool 値を、map の場合は変換したデータを出力する必要があります。 Dataset の出力は、今回の場合は zip で2つの Dataset をまとめていますので、処理を行う関数の引数は2つになっています。 また、tf.data.FixedLengthRecordDataset で読み込んでいるためそれぞれのデータはバイナリ形式になっており、 tf.decode_raw メソッドで事前に変換しています。

前処理の実際の処理は、 tf.py_func メソッドで生 python の関数を呼び出すようにしています。今回の実装では、必要な処理ではないですが、このようにしておくと処理をいくらでも柔軟に組み替えることが可能になります(ただしCPUパワーが必要になってきますが)。

tf.py_func メソッドは、処理をする関数、TensorFlow オブジェクトと出力する型を指定して、処理を行った関数の出力を戻り値として受け取ります。 戻り値は shape が指定されていないので、あとあとの処理のために set_shape で設定しておく必要があります。 また、tf.py_func 内の処理の組み方によっては、入力データを複数に分割して出力するような処理も実現することができます。

def _filter(self, images, labels):
    return True

def filter(self, images, labels):
    predicts = tf.py_func(
                self._filter,
                [images, labels],
                [tf.bool])[0]
    return predicts

def _convert(self, images, labels):
    images = images.reshape((28, 28, 1))
    images = images.astype(np.float32)

    labels = labels.astype(np.uint8)
    labels = labels.reshape((1, ))

    return images, labels

def convert(self, images, labels):
    images = tf.decode_raw(images, tf.uint8)
    labels = tf.decode_raw(labels, tf.uint8)
    images, labels = tf.py_func(
                self._convert,
                [images, labels],
                [tf.float32, tf.uint8])
    images.set_shape((28, 28, 1))
    labels.set_shape((1, ))
    return images, labels

バッチおよびデータの繰り返し

バッチ化およびデータの繰り返しは、 shuffle、repeat、batch(padded_batch)の3つのメソッドで実現します。

shuffle メソッドは、文字通り指定したファイルから順番に読み出したデータを buffer_size 分メモリに貯めて、その中でランダムに batch_size 分のデータを返すような挙動になります。

上述のようにこのメソッドは、メモリにデータをロードすることになるので、(GPUで学習していればGPUの)メモリが必要になりますが buffer_size の値が大きければ大きいほど、基本的にはランダムの質はよくなります(ファイルの前の方のデータと後ろの方のデータが混ざりやすくなります)。ロスの値などの metric がバッチ毎に偏ってしまっているならば、buffer_size の値を増やすことを検討してみるべきです。

repeat メソッドは、それこそそのままでデータを繰り返し読み出してくれます。引数に回数を指定した場合その回数分、何も指定しない場合エンドレスでデータを読み出し続けてくれます。

batch メソッドは、指定した batch_size 分の値を各ステップで読み出してくれます。padded_batch メソッドは、バッチ単位でデータの不揃いを整形してくれます。padding する値も引数で指定することができます。

if is_training:
    dataset = dataset.shuffle(buffer_size=buffer_size).repeat()

dataset = dataset.padded_batch(batch_size, dataset.output_shapes)

モデルと学習処理の定義

モデルと学習処理の定義は、Keras の Layers API やありものの Optimizer およびロスの定義用に、 tf.nn API や一部の数値計算用のメソッドを使用することでほぼ問題なく実装することができます。

自分のモデルクラスの実装は、Keras の Layers に準じて基本的に build と call で構成され、それぞれパラメーターやレイヤーの定義と計算グラフの構築を担当します。別途モデルに関連しているメソッドとして、loss 計算用のメソッドと学習処理(最適化処理)を構築するメソッド、予測を実行するメソッドをインスタンスに含めています。

def call(self, inputs, is_train=True):
    outputs = tf.reshape(inputs, (-1, 28*28)) / 255.0
    outputs = self.dense1(outputs)
    if is_train:
        outputs = tf.nn.dropout(outputs, 0.2)
    outputs = self.dense2(outputs)
    return outputs

def loss(self, logits, labels):
    labels = tf.reshape(tf.cast(labels, tf.int32), (-1, ))
    loss = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=logits, labels=labels)
    loss = tf.reduce_mean(loss)
    return loss

def optimize(self, loss, clipped_value=1.0):
    grads = self.optimizer.compute_gradients(loss, self.variables)
    clipped_grads = [(tf.clip_by_value(g, -clipped_value, clipped_value), v) for g, v in grads]
    train_op = self.optimizer.apply_gradients(clipped_grads)
    return train_op

def predict(self, logits):
    _, indices = tf.nn.top_k(logits, 1, sorted=False)
    return indices

callメソッドでどこまで担当するかいくつ値を返すか、lossやoptimize、predictメソッドの引数に何を取るかはモデルによって変わってしまう構成をとっていますが、大まかな関数の呼び出し順は、どのようなモデルを構築する際もこれをベースに行うことができます。 例えば、GANの実装では、call メソッドで descriminator の logits 元画像及び生成画像に対して計算して返し、loss で generator および descriminator のロスを計算、optimize でそれぞれの勾配計算を tf.control_dependencies や tf.group などで関連付けて返せば良いという実装になります。

ここで主に注意する点は、ロスの計算周りと最適化計算処理です。

ロスの計算では、 cross_entropy の計算などで log に渡す値や割る値が 0 になり、出力として -inf や NaN を返す可能性が十分にあります。keras の categorical_cross_entropy の実装などでは、log の計算で -inf が返らないように内部的に微小な値(epsilon値)を使って調整してくれていますが、生のTensorFlow はそこまで親切ではないので、その辺りを予防するために必要があればロス関数を自力実装する必要性があります。

また、最適化処理も、keras の Oprimizer のように勾配爆発を防ぐために勾配を clipping するのが一般的です(学習率の調整やデータの前処理でどうにかできる場合はそれでも問題ないです)。 clipping だけでは勾配消失に対応できませんが、こと画像認識においては Residual Block などのテクニックで大幅にそのリスクを減らすことができます。

学習環境の定義及び実行

学習の実行処理で必要になる処理は細かくも様々です。なんとなしに必要なものを洗い出すと、ただ学習をさせたいだけなのに色々と必要になってきます。

  • データの取得
  • モデルの定義
  • サマリーの保存
  • モデルの保存
  • 学習の監視
  • 学習処理の実行

このうち上の2つは、上記の2項目で実装した関数を呼び出すだけで実行できるようにしておくことがベストです。そうなっていない場合は、該当部分のクラス構造や関数定義を見直してみるほうが良いでしょう。

サマリーの保存は、session として MonitoredTrainingSession を使用していれば、tf.summary の各 API を使用して監視したいオブジェクトを設定しておくだけで勝手に summary_dir に指定したディレクトリにサマリーを保存してくれます。 保存する間隔も save_summaries_steps もしくは save_summaries_secs で指定することが可能です。

モデルの保存は、tf.train.Scaffold で tf.train.Saver をラップすることにより、同じく MonitoredTrainingSession に渡すことで実現できるようになります。 tf.train.Saver の初期化パラメーターで、最大いくつのチェックポイントを残すか(max_to_keep)と、何時間に一回のチェックポイントを残すか(keep_checkpoint_every_n_hours)を指定できます。 また、MonitoredTrainingSession のパラメーターとして、save_checkpoint_secs もしくは save_checkpoint_steps でチェックポイントを保存する間隔を、checkpoint_dir でチェックポイントを保存するディレクトリを指定することができます。

scaffold = tf.train.Scaffold(
    saver=tf.train.Saver(
        max_to_keep=checkpoints_to_keep,
        keep_checkpoint_every_n_hours=keep_checkpoint_every_n_hours))

学習の監視は、tf.train.SessionRunHook の派生オブジェクトのリストを、MonitoredTrainingSession にわたすことにより実現できます。 定期的に指定したオブジェクトの値をログとして出力してくれる tf.train.LoggingTensorHook。 指定したオブジェクトの値が NaN になった際にエラーを発生させてくれる tf.train.NanTensorHook。 global_step が指定の値以上になった時に、 session.shoud_stop メソッドを True にしてくれる tf.train.StopAtStepHook。 など、さまざまな Hook が用意されています。Hook 自体は自作もそこまで難しくありませんので、必要に応じて作ることも可能です。

hooks.append(tf.train.LoggingTensorHook(metrics, every_n_iter=100))
hooks.append(tf.train.NanTensorHook(loss))
if max_steps:
    hooks.append(tf.train.StopAtStepHook(last_step=max_steps)

ここまでくれば学習の実行は非常に簡単で、下記のようにひたすら session.run を実行するだけですみます。

with session:
    while not session.should_stop():
        session.run([train_op])

他にも、tf.ConfigProto で GPU の実行設定を行ったり、tf.train.ClusterSpec および tf.train.Server を使用した複数サーバーでの分散学習も設定することができます。

終わりに

近年の TensorFlow は、フレームワークにそれなりに足を突っ込めば、学習時にやりたいことは一通りできるようになってきています。それでも学習コストは十分にあるのですが、以前の時間をかけて理解しても結局よくわからないという状態に比べれば天と地ほどの差があるのは見てのとおりです。

TensorFlow 2.0 ではこの辺りが整理されてよりわかりやすくより使いやすくなることを期待します。

参考

DNNで渦巻きデータを学習して、決定境界を可視化してみる

TJO さんの下記のブログに触発されまして、NNで渦巻きデータを分類するタスクをやってみました。

使用したデータは、下記のコードにより適当に生成した渦巻きデータです。Neural Network Playgroundの渦巻きデータのように、中心と周辺でデータの分布が近しくなるようにちょっと調整しています。

K = 2
N = 200
X = np.zeros((N * K, 2))

for i in range(K):
    ix = range(N * i, N * (i+1))
    a = int(N/4)
    r = np.concatenate((
        np.linspace(0.0, 1.50, a),
        np.linspace(1.50, 2.75, a),
        np.linspace(2.75, 3.90, a),
        np.linspace(3.90, 4.76, a)))
    theta = 3 * (i + r)
    X[ix] = np.c_[r * np.sin(theta), r * np.cos(theta)]

生成されたデータを適当に半分に分割して、訓練データとテストデータとします。左から、データ全体、訓練データ、テストデータとなります。

f:id:KSKSKSKS2:20170827175504p:plain:w200 f:id:KSKSKSKS2:20170827175627p:plain:w200 f:id:KSKSKSKS2:20170827175639p:plain:w200

出オチですが、普通こうするよねというの先に書いておくと、カーネルSVM で RBFカーネルを使用した場合、きれいなロール型の識別境界を学習してくれます。テストデータでの精度は 0.96 と上々の結果になります。

svc = SVC(C=10.0, probability=True)
svc.fit(X_train, y_train)

f:id:KSKSKSKS2:20170827180906p:plain:w200

シンプルな極座標で表せるデータの生成プロセスと、別の空間にデータを写像しそれらを分離する超平面を学習するカーネルSVMの特性を考えれば、上記の結果はそこまで驚くものではないと思います。

前置きが長くなりましたが、このデータをNNで分類するとどうなるかやってみたいと思います。 類似のデータでは、スタンフォード大学の授業資料内で3クラス分類をやって、非線形な分類が行える例を示しているものがあります。

この記事とSVMで学習した際の識別境界を比べても分かるように、前処理もしていないこういった形式のデータの学習に対して、NNがそこまで向いていなさそうなことは、なんとなく察することができます。

とはいえ、どれだけできるのか?どうフィッテイングしようとするのか?は試してみなければ分かりません。

というわけで、TensorFlow 1.3.0 から追加された DNNClasifier クラスを使って上記のデータを学習してみました。DNNClasifier クラスは、TensorFlow の内部構造に合わせて作られているため、よく意図がわからない変なインターフェースですが、生のAPIよりはお気軽にDNNを組むことができます。

cls = tf.estimator.DNNClassifier(
    hidden_units=[20, 20, 20, 20],
    feature_columns=[tf.feature_column.numeric_column("x", shape=[2])],
    n_classes=2,
    optimizer=tf.train.ProximalAdagradOptimizer(
      learning_rate=0.05,
      l2_regularization_strength=0.001),
    activation_fn=tf.nn.relu,
    dropout=0.2)

train_input_fn = tf.estimator.inputs.numpy_input_fn(
    x={"x": np.array(X_train)},
    y=np.array(y_train),
    batch_size=32,
    num_epochs=2000,
    shuffle=True)

cls.train(input_fn=train_input_fn)

上記のコードでは、隠れ層が4層でそれぞれ20次元、アクティベーション関数として ReLUを用い、入力データ2次元の2クラス分類を行うネットワークを構築しています。 L2正規化とドロップアウトを用い、学習アルゴリズムはAdagrad、バッチサイズは32、2000エポックの学習を行わせています。

learning_rate と dropout の値を変えながら学習した結果のうち比較的うまくいったケースを描画すると以下のようになります。

左から、learning_rate が 0.01 dropout なし、learning_rate が 0.05 dropout なし、learning_rate が 0.05 dropout 0.2、learning_rate が 0.05 dropout 0.5 の場合の識別境界と訓練データを描画したものになります。

f:id:KSKSKSKS2:20170827184009p:plain:w200 f:id:KSKSKSKS2:20170827184028p:plain:w200 f:id:KSKSKSKS2:20170827203506p:plain:w200 f:id:KSKSKSKS2:20170827203841p:plain:w200

learning_rate が 0.01 のものでは、学習がちゃんと完了できていないようで、識別境界があいまいになってしまっています。

learning_rate を 0.05 にした場合は、学習が完了し、むしろ訓練データに対して過学習してしまっている傾向が見られます。

過学習防止用に dropuout を追加すると、識別境界が緩やかになり、0.2 でちょうど渦巻きの形を再現するような識別境界を学習できる傾向にありました。

この場合のテストデータでの精度は、0.93 くらいですのでこのあたりがまあ限界ではないかと思います。

SVMの結果と比較してみますと、識別境界をきれいな渦巻状としては学習できておらず、細かい矩形を組み合わせてなんとかデータからそれっぽい境界を学習しているように見えます。

もしかするともっと良い設定があるかもしれませんが、正直なんとか頑張ってみました感がぬぐえず、それならSVMの方が良いなといったところではないでしょうか。

また、この論文にあるように、Dropout を用いて学習した重みで予測時もDropoutを使用すると、その出力はベイズ推定で得られた事後分布からサンプリングしたものと等しくなります。*1

これを利用して、上記までの結果も推定値をもとにサンプリングして、どれくらい自信のある予測結果なのかを見てみることにします。*2

以下は、dropout を使用してそれぞれの座標で予測結果を100サンプル取得して、その平均をもとに描画したものになります。*3

上から、learning_rate が 0.05 dropout 0.2、learning_rate が 0.05 dropout 0.5 の場合の通常の識別境界の描画と、dropout を予測時にも適用した場合の識別境界を描画したものです。

f:id:KSKSKSKS2:20170827203506p:plain:w200 f:id:KSKSKSKS2:20170827204859p:plain:w200

f:id:KSKSKSKS2:20170827203841p:plain:w200 f:id:KSKSKSKS2:20170827204915p:plain:w200

特に dropout 0.2 の時が分かりやすいですが、二つの点が混じってどっちか分かりにくい部分に関しては実はあまり予測結果に自身がないことが分かります。

渦巻状の識別境界になんとかフィッティングしているように見えますが、実のところ、「なんとなくこんな感じになっている気がする」といった具合に予測しているのでしょう。

このあたりは、DNNの柔軟性とも捉えることができますが、上記までの結果は、「その柔軟性が一番良いアプローチではない時もある」ということを示しているように思います。

*1:こちらのブログ記事で詳細が丁寧に説明されていますが、数学的に変分推定した結果と等しいと考えることができます。

*2:DNNClasifier を用いていると、残念なことに予測時にDropoutを使用する場合、ややトリッキーなことをする必要があります。詳しくはコードを参照ください。

*3:計算方法、描画方法ともにちょっと微妙な気がしてはいる。

畳み込みニューラルネットワークが見ている世界を可視化してみる(1)

VISUALIZING DEEP NEURAL NETWORK DECISIONS: PREDICTION DIFFERENCE ANALYSIS など、ニューラルネットワークの内部でいったいどんな処理が行われているのかを調べている論文も多く、アルゴリズムの理論的な解明を考えると、このような論文はまだまだたくさん出てくるであろうなと思われます。

実際、なぜうまくいっているのか、なぜうまくいっていないのかを調べるためには、可能であれば処理の結果や過程を可視化してみることは、機械学習分野では重要になってきます。 ニューラルネットワークの場合は、これがパラメーターやロスの変化で見る以外の方法が基本的にはなく、上記のような研究で学習の状態をなんとなく人間の側で観察できることは、実作業においても役に立ちそうだなと思います。

畳み込みニューラルネットワークは、その名前の通り画像処理における線形フィルタリングを多層に適用して画像から特徴量を抽出するわけですが、どういったフィルターを適用してどういった特徴量を使用しているのかは、基本的には特に気にせずに使用していることが多いかなと思います。

今回は、このフィルターを適用した結果、どんな風に画像が変換されるかを可視化してみます。

tensorflow/models に含まれるInception v3の学習済みモデルを使用して、このモデルの畳み込み層の一番最初の層のパラメーターで画像を変換してみます。

Inception v3 の最初の層は、3x3 の32個のカーネルを使用します。このフィルターを利用してそれぞれ一つのカーネルをカラー画像に適用したモノクロ画像を生成してみます。対象の画像は以下のくまの画像を使用しました。

f:id:KSKSKSKS2:20170806205426j:plain:w300

結果を見てみますと、セグメンテーションをしようとしているようなものや、ネガポジ反転を行おうとしているものや、元の画像をただぼかしたような画像が生成されます。

f:id:KSKSKSKS2:20170806210205j:plain:w300 f:id:KSKSKSKS2:20170806210346j:plain:w300 f:id:KSKSKSKS2:20170806210452j:plain:w300 f:id:KSKSKSKS2:20170806210613j:plain:w300

畳み込みニューラルネットワークの肝はここからで、生成された複数のチャネルにさらにカーネルを適用する事により、多様な特徴量を自動抽出することにあります。

今回は、単純に二つのカーネルの生成結果を合成したものを可視化してみました。

結果としては、単一のカーネルだけではぼやっとしていた特徴がより鮮明に浮かび上がっているようなものが見られました。

例えば、下記はの画像は上記の画像の1番目と2番目の画像、1番目と3番目の画像の合成になりますが、より画像内の線が浮かび上がるような画像が見られました。

f:id:KSKSKSKS2:20170806211017j:plain:w300 f:id:KSKSKSKS2:20170806211426j:plain:w300

実際の畳み込みニューラルネットワークでは、より複雑な演算になりますが、基本的には上記に挙げた結果をより強調したような結果が出力されるのであろうなということが予測されます。

次回は、学習によってこの可視化結果がどのように変化するかを見てみたいと思います。

CNNを利用した自然言語処理技術まとめ(2017年1月)

年末に Language Modeling with Gated Convolutional Networks が一部界隈でバズったこともあり、CNNを用いた自然言語処理が注目を集め始めています。今年の後半あたりには、派生手法や関連手法が多く登場していくのではないかと思われます。

CNNはRNNに比べて並列処理に優れているため、処理速度が圧倒的に速いという利点がありますが、時系列データの処理に特化したRNNと比べると、特に言語モデルにおいては最終性能がやや劣っているという理解が一般的でした(テキストクラシフィケーションではタスクによってはCNNのほうが性能がいいものもありました)。

Gated Convolutional Networks では、Gated Linear Unit および Residual 層を利用し学習を効率化することにより、WikiText-103 のタスクで state-of-the-art の性能を達成しました。

テキストクラシフィケーションの分野では、Gated Convolutional Networks が登場する前にも state-fo-the-art を達成した手法がいくつかありました。この記事では、それらの手法を中心にCNNを自然言語処理に利用するにあたって使われている技術をご紹介していきたいと思います。

A Convolutional Neural Network for Modelling Sentences(2014)

A Convolutional Neural Network for Modelling Sentences は、Dynamic k-Max Pooling 層を使うことにより、センチメント解析のタスクで既存のモデルより(RNNは含まれていませんが)性能を出すことに成功しています。

この手法では、まずエンベッティング層で単語系列をベクトル系列に変換します。その後、Wide Coonvolution 層で畳み込み操作を行い、Dynamic k-Max Pooling 層をはさむ処理を複数回繰り返して、最後に全結合層でクラシフィケーションタスクに適用します。Convolution 操作も Pooling 操作も時系列方向にだけスライドし、ベクトルの次元方向の長さは変化させないように行うことに注意してください。

Dynamic k-Max Pooling 層は、名前の通りk個のMax値をPooling操作により取得する操作なのですが、kの値が層により可変で、Lはネットワークの層の総数、sは入力されるセンテンスの長さ、{k_{top}} は最後のプーリング層でのkの値とした時に、l層でのkの値は下記のような式で決定します。

{ \displaystyle
k_l =  max(k_{top}, \lceil \frac{L-l}{L}s \rceil)
}

式を見ていただくと分かる通り、入力長によってピックアップする数を変えることにより、過不足なく必要な特徴を抽出することを目標としています。

Convolutional Neural Networks for Sentence Classification(2014)

Convolutional Neural Networks for Sentence Classification は、word2vec で pre-trained したエンベッティングを利用したシンプルなCNNのモデルで、テキストクラシフィケーションのタスクで RNNよりも性能を出したモデルです。

このモデルのポイントは二つで、

  1. pre-trained なエンベッティングとそれを fine-tuning したエンベッティングの二つのエンベッティング層を利用
  2. 2つのエンベッティングにそれぞれいくつかのウィンドウサイズを持ついくつかのフューチャーマップを出力するConvolution層を適用した後、時系列方向 にmax-poolingを行う

これにより、入力するセンテンス長を制限せず、さらにsemi-supervisedなトレーニングのような効果を得ることができています。

fine-tuning したエンベッティングの結果は、元の word2vec で得られたベクトルよりも、タスクによって与えられる単語の意味を反映することもできたと論文中では述べられています。

Character-Aware Neural Language Models(2015)

Character-Aware Neural Language Models は、Charcter-Aware なエンベッティング操作をCNNで行い、その出力をLSTMに入力することで、Penn Treebank で作成したランゲージモデルで、LSTM単体で実現された state-ofo-the-art な手法より60%少ないパラメーター数で肉薄するPLLを実現することができています。

この手法の肝は、もちろんCNNによる Charcter-Aware な単語エンベッティングの取得です。モデルの全体像は下記の図の通りになりますが、後半は単語エンベッティングを用いたただのLSTMになります。

f:id:KSKSKSKS2:20170122133509p:plain:h400

※ 論文より抜粋

エンベッティングを行うCNNには、単語を文字単位で入力しその単語のエンベッティングを出力します。まず、文字のエンベッティングを出力する層を通した後、複数のウィンドウサイズのコンボリューション処理を通した後、時系列方向のmax-poolingを通して、固定長のベクトルを生成します。そのベクトルに highway network を通したものをエンベッティング層の出力とします。

LSTMを利用していますが、2層のLSTM相当の性能を1層のLSTMで出すことができており、CNNを用いた言語モデルの中では、Gated Convolutional Networks に通じる成果だと言えるでしょう。

Character-level Convolutional Networks for Text Classification(2015)

Character-level Convolutional Networks for Text Classification は、文字レベルの入力をエンベッティング層を通した後、複数のコンボリューション層とプーリング層を通した結果を全結合層に入力してテキストクラシフィケーションを行います。

入力長が限られてしまう手法ではあるのですが、いくつかのタスクで state-of-the-art な性能を達成することができています。

Language Modeling with Gated Convolutional Networks(2016)

さて最後に、金字塔を打ち立てた Language Modeling with Gated Convolutional Networks ですが、Gated Linear Unit と Residual 層を利用することにより、WikiText-103 のタスクで state-of-the-art の性能をもつランゲージモデルの作成を達成した手法です。

手法の全体図は下記のとおりですが、ある層の出力のフューチャーマップに二つのコンボリューション処理を適用し、その出力を Gated Linear Function を通してその層の出力とします。この Gated Linear Unit を Residual 処理でラップしたものを一つの層として積んでいきます。

Gated Convolutional Networks では、一切の RNN 処理を挟まないため一層のLSTMの20倍の速度で、state-of-the-art なPLLを達成しています。

f:id:KSKSKSKS2:20170122144729p:plain:h500

※ 論文より抜粋

まとめ

Gated Convolutional Networks の結果をもってRNNが下火になるかと言われればそうはならないでしょうが、さくっと商業ベースで使う方や個人で使うような火力がない方にはCNNでやる方が良い、という認識が広まっていくのはないだろうかと思います。

この記事では主に自然言語処理で必要となる技術を紹介しましたが、CNNには独自に発展した技術も多くあります。Gated Convolutional Networks で使われているような Residual 層がその代表例でしょう。CNNが自然言語処理でどんどん使われるようになっていき、タスクに依存しない技術の交流が活発に行われていくようになると思うと胸熱ですね。

個人的には、Word Representation の技術が発達するのか、Character-Base な手法が発達するのかも注目ポイントです。畳み込み処理で一気に片付けられるという点では、RNNよりはCNNの方が Character-Base の手法を有効活用できそうで、形態素解析に伴うもろもろの問題を回避できそうかなとも感じています。

いずれにせよ、自然言語処理からますます目が離せなくなるのは必至ですね。

テキスト生成モデル -SeqGAN-

この記事は、DeepLearning Advent Calendar 2016の20日目です。

今回は、時系列データに GAN の手法を適用した SeqGAN をご紹介したいと思います。SeqGAN は分かりやすく時系列データに GAN を適用しているためアルゴリズムが理解しやすく、公式の TensorFlow コードもあるので試しに動かしてみたい方にオススメできる手法です。

SeqGAN では、GAN と同じく生成モデルと識別モデルの両方を用いて生成モデルを学習させていきますが、時系列モデルに適用するにあたり( { Y_{1:t-1} } から { y_t } を求める生成モデルを作成するにあたり)、下図のように { y_{t+1} } 以降をモンテカルロ法により生成し、その結果も含めて本物か生成したものかを識別モデルに判定させることにより、{ Y_{1:t-1} } から { y_t } を求めた結果の評価値を決定します。

f:id:KSKSKSKS2:20161218112745p:plain

※ 論文から抜粋

この部分を式で表すと、{ G_{\theta} } を生成モデル、 { D_{\phi} } を識別モデル、{ Q } をリワード関数とすると、生成モデルの誤差関数は下記の用に表すことができます。

{ \displaystyle
J_{\theta} = \sum G_{\theta}(y_t|Y_{1:t-1}) Q^{G_{\theta}}_{D_{\phi}}(Y_{1:t-1}, y_t)
}

{ \displaystyle
Q^{G_{\theta}}_{D_{\phi}} = \frac{1}{N} \sum D_{\phi}(Y^{n}_{1:T}), Y^{n}_{1:T} \in MC^{G_{\beta}}(Y_{1:T};N)
}

誤差関数は、{ Y_{1:t-1} } から { y_t } を生成される最もらしさをリワード関数で表し、生成モデルにより{ Y_{1:t-1} } から { y_t } が生成される確率のもとに期待値をとった値となります。

リワード関数は、{ Y_{1:t-1} } から { y_t } を生成した後、{ y_{t+1} } 以降をパラメーター({\beta})を更新する前の生成モデルから、マルコフ連鎖により複数生成し、その生成結果を識別モデルで評価した平均を返します。

論文中では、生成モデルは LSTM を利用した RNN で、識別モデルは CNN で作成しています。

評価結果としては、最尤法で最適化した言語モデルで作成したテキストより、人間評価および oracle による評価でも改善することができました(oracle 評価に関しては学習曲線がかなり特殊なため再現性があるのかは微妙なのですが)。

テキスト生成をうまいことやるためには2つも3つも壁を超えなければならないように思いますが、時系列データの生成モデルとしては一つの参考となるモデルになるのではないでしょうか。