終末 A.I.

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

ニューラルネットワークを使用した対話システム (2)〜機械読解質問応答システム〜

本記事は、「Neural Approaches to Conversational AI*1」を元に、ニューラルネットワークを使用した対話システムについて解説する記事の二回目です。

前回の記事では、対話システムの概要とKnowledge Base質問応答システムについて説明しました。

ksksksks2.hatenadiary.jp

今回は、二つ目の質問応答システムである、機械読解質問応答システムについて説明します。

機械読解を利用した質問応答システムは、Watsonに代表されるような、大量の文書をもとにユーザーの質問に対して応答を返すシステムのことを指します。

まず、機械読解とは何か、それをどのようにニューラルネットにより実現するかについて説明します。

続いて、機械読解を組み込んだ質問応答システムをどのように実現するかについて、システムの例も見ながら説明していきます。

目次

機械読解をニューラルネットで実現する

機械読解とは何か

機械読解とは、パッセージ  P質問文  Q から応答  A を返すようなタスクを指します。

パッセージ  P は、SQuAD*2のように一つの文書で構成されることが一般的です。一方で、MS MARCO*3のように複数の文書から構成されることもあります。

質問文  Q や応答  A はこちらも一般的には一つずつですが、質問と応答の繰り返しを扱い、対話のコンテキストを考慮した応答を行うことに注目したデータセットもあります。

応答の形式としては、パッセージ内に含まれる文や単語を選択することにより応答を返すものが一般的です。
CNN/Daily Mail*4のように選択肢があらかじめ指定されている場合や、パッセージ内の特定の範囲を選択肢する場合があります。

システムの応答をより自然に、より実用的にするために、応答の形式には様々な例外が提案されています。例えば、下記のような応答を行うシステムがあります。

  • Yes/Noといった質問文への肯定や否定を返すもの
  • 与えられているパッセージだけでは答えが返せないというNo Answerという形式を返すもの
  • 回答が含まれていそうな文書を並べかえるもの
  • 自然な対話になるように応答文を生成することを求めるもの

これらの機械読解システムは、ニューラルネットの登場により飛躍的な発展を遂げました。
BERT*5に代表される文理解モデルにより、パッセージと質問文のマッチングで応答可能なデータセットでは、精度が大幅に向上したのです。

その性能は驚愕に値するもので、SQuADデータセットにおいて完全一致および部分一致のいずれでも、人間の成績を大きく上回るものでした。

機械読解をニューラルネットで実現する方法

以降では、ニューラルネットワークを用いて、どのように機械読解を実現しているかを、下記二つの観点から説明します。

  1. パッセージおよび質問文を、どのようにしてニューラルネットが扱いやすい形式に変換しているか
  2. 変換したパッセージおよび質問文を、どのようにマッチングして応答を選択しているか

ニューラルネット自然言語を扱う場合、一般的に文字や単語を、埋め込み表現と呼ばれるベクトルの形式に変換したものを使用します。

BiDAF*6と呼ばれる機械読解のベースラインモデルでは、単語単位のCharacter Embeddingと、文字単位のWord Embeddingの両方を使用しています。
ベクトルに変換した文字列および単語列を結合し、ネットワークの入力とします。

次に、上記で作成した埋め込み表現をニューラルネットモデルに入力して、文章全体のコンテキストも加味したベクトル表現に変換します。

一般的にこのフェーズでは双方向RNN(Bidirectional RNN)がよく使用されます。BERTのようにTransformerライクなself-attentionを含む層を利用することもあります。

ここまででの流れで、パッセージおよび質問文をニューラルネットで扱いやすいベクトル表現に変換していくことができました。

続いて、このベクトル表現を用いて応答を選択していきます。

ここで非常に重要になるのが、attentionと呼ばれる、コンテキストの重要度を決定する機構です。

attentionは、seq2seqモデルでにおいて、入力文の各語の影響度合いを、出力文として生成される単語のポジションによって変更するために誕生したものです。

機械読解モデルでは、質問文に含まれる単語をもとに、パッセージに含まれる単語の重要度を計算したり、逆にパッセージに含まれる単語をもとに質問文に含まれる単語の重要度を計算したりするために使用されます。

また、Transformerのようなself-attentionを利用する層では、パッセージならパッセージ自身の、質問文なら質問文自身の、文全体のコンテキストを考慮して文に含まれる各単語の重要度を計算します。

このattentionの仕組みによって、パッセージと質問文をニューラルネットが自動的にマッチングしているものと考えられます。

最後に、attentionを考慮したあとのコンテキストベクトルを使用して出力を選択します。

単語選択式の回答の場合は、上記のコンテキストベクトルをもとに別のニュールネットに通して、それぞれの選択肢の尤度を計算します。

範囲選択式の回答の場合は、開始位置と終了位置、それぞれの尤度を上記のコンテキストベクトルをもとに計算します。

解答を選択するためのニューラルネットワークとしても、一般的に双方向RNNがよく使用されます。他には、線形モデルやTransofrmerのエンコーダー層を使用するケースもあります。

様々な機械読解モデル

ここまで、機械読解の一般的なモデルについて説明してきました。
ここからは、上記で紹介した一般的な構成では対応が難しい問題に対する、様々なモデル構成についてみていきたいと思います。

まず最初に紹介するのは、DrQA*7です。
DrQAは、Wikipediaの全ドキュメントを対象に、ユーザーからの質問に応答するために設計されたモデルです。

WikipediaベースのデータセットであるSQuADと、追加のデータセットを使用したマルチタスクラーニングで、最大36.5%の精度で完全一致の応答を返すことを実現しました。

DrQAは、Document RetrieverとDocument Readerの二つのコンポーネントからなります。

Document Retrieverでは、大量のWikipediaドキュメントから、5つの候補ドキュメントを抽出します。
このコンポーネントにはニューラルネットは使用されておらず、質問文と似通ったバイグラム構造を持つドキュメントを抽出します。

Document Readerでは、抽出されたドキュメントをパラグラフ単位で質問文とマッチングします。

パラグラフベクトルは、Word Embeddingや種々の特徴量を結合して構成されます。
質問文ベクトルは、Word Embeddingを一層のRNNに通し、その出力をself-attentionでならしたものを使用します。

上記のように作成したパラグラフベクトルと質問文ベクトルから、パラグラフ単位で開始位置と終了位置の尤度を計算します。
最後に、最も尤度の高い開始位置と終了位置の組み合わせを答えとして出力するという仕組みです。

f:id:KSKSKSKS2:20190516001715p:plain
Chen 2017より

ReasonNet*8およびReasonNet++*9のように、パッセージと質問文を複数回マッチングするモデルもあります。人間が文章を読むときのように、重要そうな部分を繰り返し読解し答えを生成することを試みているモデルです。

これらのモデルは、Memory Networks*10を拡張したもので、Memoryに保存されたパッセージと、マッチングに重要そうな質問文のコンテキストを繰り返し更新しながらマッチングします。

ReasonNetとReasonNet++は、さらにターミネーションフラグを内部に持ちます。このターミネーションフラグがTrueになる、もしくは規定された最大回数に達するまでマッチングを行います。
動的な回数マッチングすることにより、問題に合わせたマッチングを実現することを試みているのです。

パッセージと質問文のベクトル化は、前述した埋め込み表現、双方向RNN(もしくはTransformer)、attention機構を使用して生成します。
その後、パッセージベクトルをメモリーに、質問文ベクトルを答えを探索する(Reasoningを行う)RNNの初期状態に設定し、終了されるまでRNNの状態を更新しながらマッチングを行います。

ターミネーションフラグを持つ場合、学習は通常のSGDでは無理であるため、強化学習の枠組みを利用してニューラルネットでのモデリングを行っています。

応答は、最終ステップのマッチング結果を利用して、開始位置と終了位置の尤度を計算することにより生成します。
一方、SAN*11やR.M-Reader*12のように、マルチステップのマッチングを行いつつ、各ステップで計算した尤度を総合して最終的な出力を行うモデルもあります。

f:id:KSKSKSKS2:20190519114354p:plain
Shen 2017 より

機械読解質問応答システム

機械読解質問応答システムの問題設定

機械読解を利用した質問応答システムは、まだまだ新しい分野です。2018年ごろから続々と専用のデータセットが提案され、今盛り上がりを見せだしている分野でもあります。

複数のデータセットがあり、それぞれ細かい部分では異なりますが、共通する問題設定は下記のようになります。

  1. 質問者は、パッセージを見ずに、周辺情報のみを与えられて質問を行う
  2. 回答者は、パッセージを見ながら、抽出もしくは極力語彙を変えない形で応答を行う
  3. 質問者は、以前の質問の情報を必要とする質問を行う
  4. 上記を数ターン繰り返し、一つの対話データとする

対話というほど自然で流暢なやり取りではありませんが、数ターン前までの履歴を考慮して応答ができるかを問うています。
具体的には、直前の質問文や回答文への参照が含まれているような文が質問文に含まれることになります。

その他の機械読解データセットと比較して、タスクとしての難しさは主にこの参照解析にあると考えられています。
以前の質問および回答のどの部分を参照しているのかを把握する必要があり、単純なパターンマッチだけでは解けない問題が現れるためです。

一方で、データセットの分析結果は必ずしもこの仮説に沿わない結果となっています。
質問に応答するために参照解析が必要かどうかに関係なく回答精度はあまり変わらないという結果や、参照解析とは別の要因で回答精度が変化するという結果が示されています。

機械読解質問応答システムの例

CoQA*13は、パッセージからの範囲抽出だけでなく、それを要約した形式で応答を行うことを要求するデータセットです。
質問文や応答文が、それぞれ平均5.5単語および2.7単語とSQuAD並に短いのも特徴の一つです。

CoQAの論文の中では、上記で紹介したDrQAによる回答範囲選択と、Pointer-Generator Network*14を利用した回答文の要約を組み合わせた手法を、最も性能の良かったモデルとして提示しています。
最も性能の良かったモデルでもF1が65.1%と、人間の88.8%という性能を大きく下回ることが示されています。

CoQAは、登場して約一年ほど経過した2019年5月現在、性能が大幅に向上しました。LeaderBoard上では、人間に匹敵する性能のモデルが登場しています。
BERTの登場が大きく影響しており、その他の範囲選択型、抽出型の機械読解タスクでも同様の現象が発生しています。

QuAC*15は、範囲選択型の応答と、応答の意図をダイアログアクトとして指定した、二種類の応答を返すことを要求するデータセットです。

ダイアログアクトは、範囲選択だけでは意図が取りづらい応答を、質問者が理解するために付与しているものです。
このデータセットでは、ダイアログアクトとして、答えの自信度を示すフラグ、Yes/No、No Answerを示すフラグの3種類のフラグを組み合わせたものを応答します。

またQuACは、パッセージの長さが396.8単語、応答の長さが平均14.6単語と、SQuADに比べて3倍以上長めであることも特徴です。

QuACの論文では、コンテキストを考慮するように改造したBiDAF++を最も性能が良かったモデルとして提示しています。
人間のF1値が80.8%だったところ、モデルの性能は60.6%ほどでだいぶ開きがあります。

ダイアログアクトのうち、Yes/NoフラグのF1は、人間の性能と近く86.1%の性能を示しました。一方、答えの自信度を示すフラグのF1は61.6%程度でした。

QuACは、CoQAと同じ時期に登場したデータセットですが、LeaderBoard上は、BERTを応用したモデルでも、まだまだ人間の性能には及ばない状態です。論文内で提示された性能をまあまあ改善する程度に留まっているのです。

QuAC論文の分析では、以前の質問と現在の質問の応答の選択範囲が離れていればいるほど、応答選択のF1が最低40%ほどにも下がることが示されています。
また、答えが含まれる文章が分かっている場合のF1は72.4%と大幅に向上することも示されています。

このことから、QuACに含まれるパッセージが他のデータセットより非常に長く、マッチングする文を適切に選択できていないことにより、識別精度がなかなか向上しないのではないかと考えられます。

まとめ

本記事では、機械読解というタスク、そして機械読解を利用した質問応答システムについて説明してきました。

機械読解というタスク自体は、昨年のBERTの登場以降、特定の文章に対するシンプルな質問に対して、範囲指定型の応答を行う問題では、非常に高精度で回答を生成できることが示されています。

一方で、質問応答システム用の機械読解は、昨年ようやくデータセットが出てき始めたという状態です。 それゆえ、機械読解を単純にマルチターンに拡張したような問題形式であっても、人間の精度に到達できていないようなケースも存在します。

さらに、複数の文書を参照したり、応答を生成する必要がある問題は、まだまだ人間の精度に達していないというのが現状です。

上記を考慮すると、実用レベルの機械読解型の質問応答システムを構築することは、まだまだ先の話になりそうです。

しかし、根拠となる文書と、質問・回答のペアさえあればシステムが構築できるという利点は、今まで人間のオペレーターが応答してきた回答が大量にあるような環境にとっては、喉から手が出るほどほしいシステムであることは間違いないでしょう。

機械読解は研究領域でも非常に注目を集めており、昨年だけで10数個の新たなデータセットが提案されています。
対話システムだけでなく、マルチモーダルな意図理解、高度なNLIが必要なタスクなど、人間でも少し難しいタスクを、ニューラルネットでも行えるのではないかという期待が高まっています。

設定を物語形式で与えることにより、雑談に回答するような対話システムも構築できるかもしれません。

機械読解を利用した質問応答システムが、いたるところで利用される日まで、さほど時間がかからないのではと期待できます。

トポロジカルソートと強連結成分分解でWikipediaの特定カテゴリー配下のページをすべて取得する

Wikipediaの特定カテゴリー配下のページをすべて取得するためには、整理されていないグラフデータ特有のいくつかの問題に向き合う必要があります。

一つは、Category:カツラ科糸井の大カツラのように、サブカテゴリーにはページへのリンクが含まれているが、カテゴリー本体にはページへのリンクが含まれていないケースがあるという問題。

もう一つは、Category:インフォグラム・エンターテインメントームソフトCategory:アタリのゲームソフトのように、お互いがお互いのサブカテゴリーに含まれてしまっているケースがあるという問題です。

これらの問題は、以下の手順を踏むことで解決できます。

  1. カテゴリーにリンクされているページだけでなく、サブカテゴリー内のリンクを順にたどって含まれるすべてのページを収集する

  2. ただし、一度たどったカテゴリーに再度到達した場合、それ以上はそのルートを探索しない

日本語Wikipediaのカテゴリー数は21万以上あります。ナイーブに上記のアプローチを実行すると、配下のサブカテゴリーの深さや数量に応じて時間がかかってしまいます。

そこで、事前にカテゴリー - サブカテゴリーのリンク構造を洗い出しておくことを検討します。グラフに関するアルゴリズムである、トポロジカルソート強連結成分分解を使用することにより、シンプルにこの処理を実装することができます。

以下では、Wikipediaデータからカテゴリーリンクの一覧を取得する方法から、トポロジカルソートと強連結成分分解を使用して、カテゴリー配下の全てのページを取得するアプローチについて紹介します。

紹介する処理の実装は、下記のリポジトリcategory.py にまとめています。

目次

Wikipediaからカテゴリーリンク一覧データを取得する

まず、Wikipediaのカテゴリーリンク構造を表すデータを取得します。Wikipediaのカテゴリー関係のデータには2種類あります。

1つ目は、カテゴリーとページ、カテゴリーとカテゴリーの親子関係を表したカテゴリーリンクデータ。
2つ目は、ページIDとページ名の一覧を含むページ一覧データです。

詳細は後述しますが、カテゴリーリンクデータだけではカテゴリーの親子関係を完全にたどることはできません。ページ一覧データも使用することにより、ようやくカテゴリーの親子関係を洗い出せます。

カテゴリーリンクデータの取得

カテゴリーリンクデータは、Wikipedia内のカテゴリーとページ、カテゴリーとサブカテゴリーのリンク関係を表すデータです。*1
SQLのテーブルデータとして提供されており、3つの重要なカラムを持ちます。

  • cl_fromリンク元(配下のページもしくはサブカテゴリー)を示すカラムで、ページIDが格納されている

  • cl_to はリンク先(親カテゴリ)を示すカラムで、カテゴリーの名称が格納されている

  • cl_typeリンク元のページ種類を示すカラムで、ページかサブカテゴリーかを示す値が格納されている

注意が必要なのは、 cl_fromcl_to とでは格納されている値の種類が異なる点です。このままでは、サブカテゴリー配下のページおよびサブカテゴリーを探索する際に問題がでてしまいます。

そこで、下記のページ一覧データを使用することにより、 cl_fromcl_to に格納されている値のマッチングを行います。

ページ一覧データの取得

ページ一覧データは、Wikipedia内のページに関する基本情報を表すデータです。このページ一覧データにはカテゴリーページのデータも含まれています。*2
カテゴリーリンクデータと同じくSQLのテーブルデータとして提供されており、2つの重要なカラムを持ちます。

  • page_id は各ページのページIDを示すカラムで、カテゴリーリンクデータの cl_from に対応する値が格納されている

  • page_titleは各ページのタイトルを示すカラムで、カテゴリーリンクデータの cl_to に対応する値が格納されている

上記からわかるように、ページ一覧データを使用してページIDとページタイトルの対応関係を取得することができます。

この情報を利用して、カテゴリーリンクデータに含まれる cl_from をページタイトルに(もしくは、 cl_to をページIDに)変換することにより、カテゴリーの親子関係をたどることができます。

カテゴリーリンク一覧データの生成

上述のカテゴリーリンクデータとページ一覧データを用いて、カテゴリーリンク一覧データを作成します。

このとき、カテゴリーからサブカテゴリーへのリンクを示すデータと、カテゴリーからページへのリンクを示すデータを別々に作成することがポイントです。理由は2つあります。

一つ目の理由は、ページとカテゴリーの名称が重複することがあるためです。
Wikipediaでは、名前空間内の名称の一意性は担保されています。しかし、ページ名称とカテゴリー名称とは、お互いに独立の名前空間を持つ運用が行われているため、名称が衝突する可能性があります。
実際に、Category:カツラ科カツラ科のように衝突している例が多く存在します。

二つ目の理由は、後述するトポロジカルソートと強連結成分分解の計算量を削減するためです。
これらのアルゴリズムは、ノード数とエッジ数の合計に比例した計算量がかかります。また、入口も出口もあるようなノードを整理するために使用されるものです。
このため、確実に処理の対象にならいないことが分かっているページを表すノードは、トポロジカルソートと強連結成分分解の対象になるカテゴリーを表すノードとは、分けて管理しておく方が効率よく扱う事ができます。

GitHubの実装では、カテゴリー名からそのカテゴリー配下のページのセットを返す categorypages オブジェクトと、カテゴリー名からそのカテゴリー配下のサブカテゴリーのセットを返す categorygraph オブジェクトを生成するようにしています。

def show_category_directlinks(categorypages, category):
    if category in categorypages:
        print("Pages:", categorypages[category])

現時点では、上記の show_category_directlinks 関数を使用し、 category に「カツラ科」と入力すると、配下のページとして「カツラ_(植物)」と「カツラ科」が取得できるだけです。
「カツラ科」カテゴリーのサブカテゴリーである、「著名なカツラ」に配下のページは残念ながら取得することはできません。

def show_category_alllinks_with_dfs(categorypages, categorygraph, category):
    class Node(object):
        def __init__(self):
            self.visited = False

    V = defaultdict(Node)
    pages = set()

    def visit(v):
        nonlocal pages

        if V[v].visited:
            return
        V[v].visited = True
        if v in categorypages:
            pages |= categorypages[v]
        if v in categorygraph:
            for m in categorygraph[v]:
                visit(m)

    visit(category)
    print("Pages:", pages)

一方、深さ優先探索ですべての配下のサブカテゴリーを探索する show_category_alllinks_with_dfs 関数を使用することにより、すべての配下のページにアクセスすることができます。
しかしこの方法では、「科学」カテゴリーのようなサブカテゴリーおよび後述するSCCが大量にあるカテゴリーの探索に、それなりの時間を要してしまいます。

実際、手元のMBP環境では20秒ほどかかりました。これでは、頻繁にカテゴリー配下のページを取得する必要がある際には使い物になりません。配下のサブカテゴリー一覧を事前に洗い出しおくことを検討する必要があります。

カテゴリーリンク一覧データから配下のページ一覧を洗い出す

以下では、カテゴリーリンク一覧データから配下のページ一覧を取得する方法について説明していきます。方針は、下記の2ステップのみです。

  1. トポロジカルソートを利用して、カテゴリーをサブカテゴリーが親カテゴリーより後ろにくるようなリストに並び替える

  2. 上記で作成したリストを後ろから順に処理し、サブカテゴリーのページ一覧を親カテゴリーのページ一覧にマージする

このとき、Wikipediaのカテゴリーリンクに巡回が存在することが大きな壁として立ちはだかります。
そこで強連結成分分解を使用して、この巡回するルートを削除することにより、処理が問題なく実現できるように試みます。

グラフの基礎

グラフとは、ノードエッジの組で表せるデータ構造のことを指す用語です。

ノードは、グラフ内の要素を表し、Wikipediaのデータではページやカテゴリーに相当します。
エッジは、ノード間の接続情報を表し、Wikipediaのデータではカテゴリーリンクデータに相当します。

グラフは、エッジに向きがあるか否かで大きく二つに分けられます。有向グラフ無向グラフです。

有向グラフは、連絡網やTwitterのフォロー関係のように、エッジに向きがあるグラフのことです。この記事で扱っているカテゴリーリンク一覧データも、まさに有向グラフの一つです。

一方、無向グラフは、路線図のように、エッジに向きがないグラフのことです。

有向グラフは、閉路と呼ばれる巡回構造を持つものがあります。一方で閉路を持たない有向グラフも存在し、このようなグラフを有向非巡回グラフ(DAG:Directed Acyclic Graph)と呼びます。

このDAGには非常に便利な性質があり、下記で説明するトポロジカルソートを適用できることもその一つです。

トポロジカルソートによるノードの並び替え

トポロジカルソートは、上述のDAGに適用することにより、グラフ内のノードを子ノードが親ノードより後ろにくるように並べ替えることができるアルゴリズムです。*3
お気持ちだけ簡単に説明すると、下記の手順でソートを実現します。

  1. グラフ内の探索されていない任意のノードを選択する
  2. 選択したノートからたどれるルートを深さ優先で探索する
  3. 子ノードがないノードは、グラフから除きリストの前方に追加する
  4. すべての子ノードをグラフから取り除いたノードも、グラフから除きリストの前方に追加する
  5. グラフからノードがなくなるまで、上記1-4の作業を繰り返す

このトポロジカルソートを、カテゴリー名からそのカテゴリー配下のサブカテゴリーのセットを返す categorygraph に適用します。期待値はソートされたカテゴリー一覧が返されることですが、残念ながらそのままではうまくいきません。

なぜなら、 categorygraph オブジェクトには、Category:インフォグラム・エンターテインメントームソフトCategory:アタリのゲームソフトのように、お互いがお互いのサブカテゴリーであるケースを含むためです。

つまり、グラフに閉路が存在するため、非巡回なグラフにのみ適用できるトポロジカルソートが上手く機能しないのです。

この問題を解決するためには、なんとかしてグラフ内から閉路を検出して取り除いてやる必要があります。その方法として、下記の強連結成分分解を使用します。

強連結成分分解による閉路の検出と除去

強連結成分(SCC:Strongly Connected Components)分解は、有向グラフ内の強連結成分を検出し、それらを一つにまとめるために用いることができる手法です。

強連結とは、互いから互いにたどり着くためのルートが存在するノードのペアを指す用語です。SCCは、強連結である全てのノードを含んだノード群のことを指します。

つまり、グラフ内に閉路がある場合、その閉路上の任意のノードのペアは強連結になります。また、お互いが強連結なノード同士を集めたものがSCCに当たります。

そして、グラフ上のすべてのSCCをもれなく洗い出す方法が強連結成分分解です。*4簡単に説明すると、下記の手順でSCCを洗い出します。

  1. グラフ内の探索されていない任意のノードを選択する
  2. 選択したノートからたどれるルートを深さ優先で探索し、マークを付けていく
  3. ノードを探索中にすでに探索済みのノードを検出した場合、ノードのマークを更新してそのルートの探索を終了する
  4. ルートをたどれるノードがなくなった場合、同じマークのついたノードをSCCとしてリストに追加する
  5. 上記1-4の手順を、探索されていないノードがなくなるまで繰り返す

Tarjan's Algorithm Animation.gif
By LynX - Own work, CC BY-SA 3.0, Link

SCCと閉路は異なる概念ですが、すべての閉路は何らかのSCCに含まれます。そのため、SCCをすべて抽出できれば、すべての閉路を構成するノード群を抽出できます。

また、このSCCを一つのノードとして扱うことにより、有向グラフを閉路の存在しないDAGに変換できます。

振り返りになりますが、上記のトポロジカルソートで問題になっていたのは、有向グラフに閉路が含まれているせいで、アルゴリズムがうまく機能しないことでした。

つまり、強連結成分分解を適用しDAGに変換した後の有向グラフであれば、トポロジカルソートを問題なく適用することができるのです。

配下のページ一覧の取得

ここまでくればあと一息です。下記の手順でカテゴリーリンク一覧データを変換することにより、配下のページをいつでも簡単に引き出す事ができるようになります。

  1. カテゴリーリンク一覧データを、強連結成分分解する
  2. カテゴリーリンク一覧データを、強連結成分分解した結果を元にDAGに変換する
  3. 変換したDAGのノードを、トポロジカルソートする
  4. カテゴリーリンク一覧データを、トポロジカルソートの結果を元に更新する

GitHubコード内の update_categorygraph 関数を呼び出すことによりこれらの処理を実現できます。

この関数では3つのオブジェクトを出力とします。

  • Wikipediaに記載のカテゴリー名から、内部的に付与したカテゴリーIDに変換する category2indices オブジェクト

  • カテゴリーIDから、すべての配下のサブカテゴリーセットを返すように更新した categorygraph オブジェクト

  • カテゴリーIDから、カテゴリー配下のページセットを返す categorypages オブジェクト

categorypages オブジェクトはWikipediaに記載の情報のままです。しかし、 categorygraph オブジェクトで簡単に全てのサブカテゴリーを取得できるため、すべての配下のページに短い時間でアクセスすることができます。

def show_category_alllinks(categorypages, categorygraph, category2indices, category):
    if category in category2indices:
        index = category2indices[category]
        pages = [categorypages[index]]
        for sub_c in categorygraph[index]:
            pages.append(categorypages[sub_c])
        pages = set().union(*pages)
        print("Pages:", pages)

上記の show_category_alllinks 関数を使用し、 category に「カツラ科」と入力すると、「カツラ_(植物)」と「カツラ科」だけでなく、サブカテゴリーである「著名なカツラ」の配下のページも取得できます。

また、大量にサブカテゴリーやSCCが存在する「科学」カテゴリーでも、一瞬で配下のページすべてを取得できるようになっています。
手元のMBP環境では500msほどで処理が完了します。これは、深さ優先探索を逐一適用していた時に比べ、40分の1程度の時間です。

まとめ

この記事では、トポロジカルソートと強連結成分分解を使用することにより、Wikipediaの特定カテゴリー配下のサブカテゴリーを洗い出す方法を紹介しました。

また、洗い出したサブカテゴリー情報を使用することにより、現実的な時間でカテゴリー配下のすべてのページを洗い出す事ができることも示しました。

今回、紹介した方法は任意の有向グラフに適用することができます。巡回構造がある、入口と出口が入り組んでいるような有向グラフに出会った際はぜひ活用してみてください。

*1:カテゴリーリンクデータの最新版はこちら

*2:ページ一覧データの最新版はこちら

*3:深さ優先探索によるトポロジカルソートアルゴリズムの詳細はこちら

*4:Tarjan's algorithm による強連結成分分解の詳細はこちら

ニューラルネットワークを使用した対話システム (1)〜Knowledge Base質問応答システム〜

対話システムは、QAチャットや音声アシスタントなど、様々なところで使用されており、 また、GoogleのDialogflowを始め多くの独自対話システムを構築できるプラットフォームが数年前から続々と登場してきています。

しかし、これらの公開されているシステムは、たくさんある対話システム構成の中でもタスク指向型(特にスロットフィリング型)の設計にのっとっているものが多いのが現状で、作りたいシステムをそのまま構成することが難しいケースが存在します。

この記事シリーズは、ニューラルネットワークを使用している対話システムについて、どのようなシステム設計がありうるのか、どのように機械学習でそのシステムを実現しようとしているのかを、「Neural Approaches to Conversational AI*1」を元に、元資料の引用だけでなく、中で説明されている論文についても、可能な限り概観できるようにまとめたものです。

第一回目は、質問応答対話システムのうち、Knowledge Baseを元に応答を返すシステムについてまとめています。

前提として、元資料では対話システムは、質問応答システム、タスク指向型システム、雑談システムの3つに分けられていて、質問応答システムはさらに、Knowledge Baseを参照して答えるものとドキュメントを参照して(機械読解の技術を使用して)答えるものに2つに分類されています。

目次

Knowledge Base質問応答システムとは何か

Knowledge Base質問応答システムは、下記のようにKnowledge Baseをデータソースにして、ユーザーからの「日本の首都はどこ?」のような質問に、「東京です」のように答えるシステムのことを指します。

f:id:KSKSKSKS2:20190328221131p:plain
Dhingra 2017より

Knowledge Baseについては詳しくは次節に記載しますが、大量の知識を保管しているDBのようなものだと一旦考えてもらえれば差し支えありません。

言ってしまえば、このKnowledge Base内の大量の知識から答えを探して出力するだけなのですが、下記のように難しい点がいくつかあります

  1. ユーザーが入力した自然文を元にユーザーが何について質問しているかを理解する必要がある
  2. 理解した入力文をもとにKnowledge Baseから情報を抽出する必要がある
  3. 情報が足りない場合はユーザーに追加の情報を入力するように促す必要がある

1は対話システム全体に言えることですが、Knowledge Base質問応答システムの場合は、Knowledge Baseのクエリを生成することとほぼ等価な処理を行う必要があります。この時に使用される技術のことを一般に Semantic Parsing と呼びます。

2は同じ質問応答システムであるテキスト質問応答システムでも必要な技術ですが、テキスト質問応答システムは非構造な大量のテキストから情報を抽出する必要があるのに対し、Knowledge Base質問応答システムは構造化されたデータから情報を抽出します。

そのため、1でうまいことクエリに変換することができればテキストから情報を抽出するより難易度が低く、大規模なデータソースを効率よく扱え、ピンポイントで質問されたことを答えることが容易であるという利点があります。

一方で、大規模なKnowledge Baseを構築することは非常にコストがかかるため、データ収集の面ではテキスト質問応答システムの方が大いに有利な点となります。

3はタスク指向型対話システムの一つであるスロットフィリング型システムで同じような処理が必要になりますが、スロットフィリング型システムとは異なり、答えを返すために必要な情報がシステム開発者により明示的に定義されていないことがほとんどです。そのため、Knowledge Baseを元に答えを絞り込むためにどのような情報を必要とするかをシステム自身で導出する必要があります。

一方、上記の点は必ずしも悪いことではなく、スロットフィリング型システムよりもかなり柔軟に絞り込み条件を設定する事ができ、ユーザーがどのような質問をしてくるかわからないようなシステムとは非常に相性の良い仕組みになります。

Knowledge Baseとは何か

Knowledge Baseとは、知識を保持しそれをコンピューターを用いて検索、演算できるようなものを指します。

RDBが用いられることもありますが、一般的にはエッジとノードをつなげたグラフ構造によりノード間の関係として「知識」を表現したものが使用されます。

具体的には、以下のようなトリプルと呼ばれる3つの値を持つデータの組として一つの知識を表現します。

triple(h, r, t)

hはhead、rはrelation、tはtailを表しており、headとtailはエンティティー(グラフのノード)、relationはリレーション(グラフのエッジ)を表します。リレーションには方向があり、エンティティーがリレーションのどこにつながっているかによって意味が変わる有向グラフとなります。

例えば、以下のようなトリプルを考えることができます。

triple(日本, 首都, 東京)

これは、日本の首都は東京であるということを表すトリプルで、このように一つのトリプルが一つの知識を表すことになります。

このようなKnowledge Baseを使用した質問応答システムは、主に以下の2つのモジュールから構成されることになります。

  • 自然言語を理解しKnowledge Baseのクエリに変換するモジュール
  • クエリを元にKnowledge Baseから情報を引き出すモジュール

Knowledge Baseへの入力を理解する

自然言語を理解しKnowledge Baseのクエリに変換する分野は Semantic Parsing と呼ばれ、それだけで一つの研究分野として独立するほどには活発で難しい分野です。

この分野では、

日本の首都はどこ?

というような自然文の入力を元に、

SELECT 首都 FROM 首都一覧 WHERE 国='日本'

もしくは

triple(日本, 首都, ?)

のようなコンピューターがKBから情報を取得できる形式のクエリを出力することが目的となります。

元々は自然文と変換語のクエリのペアを元に、機械学習やルールベースによりクエリの生成が試みられていましたが、近年ではより容易に集めることができる、自然文と答えのペアを収集して教師あり学習強化学習でクエリを生成するような手法も提案されています。

Berant*2らの提案手法では、ヒューリスティック構文解析の結果を元に候補クエリを大量に作成し、候補クエリの枝数や自然文の特定の単語のPOSタグ情報など特徴量にして、入力文の変換結果が出力文である妥当性を予測するように学習します。

YaoおよびVan Durme *3らの手法では、Berantらの手法と同じように候補クエリをまず大量に生成し入力文と候補クエリのマッチングを予測するように学習しますが、外部のAPIを利用した自然文のトピックや質問に使用されている疑問詞を特徴量として使用することにより、10%以上F1値を向上させました。

Bao*4らの手法は、上記2つと異なりまず質問文をヒューリスティックにより複数の部分質問に分割し、それぞれをクエリとなるトリプルに分割し最終的にマージすることによりSemantic Parsingを実現します。

Yih*5らは、強化学習を用いて逐次的にグラフ上を答えのエンティティーに進んでいくことで、答えの選択とSemantic Parsingを同時に達成する方法を提案しました。

このように一口にSemantic Parsingといってもアプローチは様々です。より詳細に知りたい方は、 ACL2018のSemantic Parsingのチュートリアル資料がとても参考になります。

Knowledge Baseから情報を引き出す

さて上記のSemantic Parsingにより、自然文をクエリの形式に変換する事ができたとします。

一般的なRDBを思い浮かべれば、後はクエリをシステムに問い合わせればよいだけのように思えますが、Knowledge Baseの問題設定においては残念ながらそこまでスムーズにことは運びません。

Knowledge Baseは実は不完全であり、必ずしもクエリから直接答えを応答できないようなケースが十分に想定できます。

例えば、以下のようなクエリがあった時に

triple(東京駅, ある国, ?)

Knowledge Baseにはそのトリプルがある可能性はとても低いでしょう。その場合、下記の2つのクエリをつなぎ合わせて答えを返すことになります。

triple(東京駅, ある場所, 東京)

triple(日本, 首都, 東京)

人間であれば上記の2つの知識を結びつけて、東京駅が日本にあることは簡単に推測できますが、コンピューターでそれを実現することは一筋縄では行きません。

Link Prediction と呼ばれるこの研究分野の問題をナイーブに解こうとすると、

あるリレーションとあるリレーションの組み合わせを、別のリレーションに変換するためのルールをを作ったり、その変換ルールを適用できるエンティティーのタイプを制限するなど、膨大なルールを設計する必要があります。

これに対して、埋め込み表現やニューラルネットを使用した方法により、これらの問題を効率よく解く手法が提案されています。

埋め込み表現を利用した手法として、まず代表として挙げられるのがTransEとNeural Tensor Networks(NTN)です。

TransE*6はBordesらが提案した手法で、エンティティーとリレーションをいずれもベクトルとして表し、Link Predictionを既存手法より10ポイント以上改善しました。

Neural Tensor Networks*7は同時期に(全く同じ会議で!)Socherらによって提案された手法で、エンティティーをベクトルで、リレーションを行列で表現した手法で、こちらもベースラインを上回る結果を示しました。

Neural Tensor Networksは双線形モデル(Bilinear model)と呼ばれる式をカスタマイズした式でスコアを計算し、 { triple(東京駅, ある国, ?)}?の部分に入る可能性のある全てのエンティティーについてそのスコアを比較、最も高いスコアのエンティティーを答えとするランキング形式で答えを探索するアプローチを取ります。

TransEもスコア関数の形こそ違うものの基本的な考えは同じで、スコアに基づいたランキングで最終的なエンティティーを決定します。

両手法は様々な派生系が提案され、現在では主要なデータセットで80〜90%近いMRRを達成するようになってきています。

ニューラルネットベースの手法は、スコアを計算することなく、エンドツーエンドで対象のエンティティーを出力する構成が多く提案されています。

特に、強化学習ベースの手法は最終的な答えのエンティティーだけでなく、どういう経路をたどってそのエンティティーを答えと判断したのかがわかり、エンドツーエンドなシステムの中では説明性の非常に高い手法となっています。

DeepPath*8はXiongらが提案した、初期の強化学習を利用したKnowledge Baseの探索手法です。下記のようにKnowledge Baseの位置をステート、次にどのエンティティーに移動するかをアクションと捉えるシンプルな問題設計で、REINFORCEというシンプルな強化学習手法をLink Predictionに適用し、(当時の)埋め込み表現ベースの手法を上回る性能を示しました。

f:id:KSKSKSKS2:20190327234002p:plain
Xiong 2017より

MINERVA*9はDasおよびDhuliawalaおよびZaheerらが提案した手法で、大規模なKnowledge BaseデータセットでDeepPathを含む既存のLink Prediction手法を上回る性能を示しました。

M-Walk*10はShenおよびChenおよびHuangらが提案した強化学習ベースのグラフ探索手法で、Alpha Goのようにモンテカルロ木探索とQ学習を使用し、埋め込み表現ベースの手法にはやや及ばないものの(発表当時はほぼ同じだったのですが)、他の強化学習ベースの手法を上回る性能を示しました。

これらのニューラルネットベースの手法は、エンドツーエンドで計算するため探索するエンティティーの数によらず一定の速度で推論することができ、通常、埋め込み表現を使用した手法に比べ3~4倍の速度で答えとなるエンティティーを出力することができます。

Knowledge Base質問応答システムの構造

さてここまでで、最初に示した以下2つのモジュールを準備できたことになります。

  • 自然言語を理解しKnowledge Baseのクエリに変換するモジュール
  • クエリを元にKnowledge Baseから情報を引き出すモジュール

後はこれをつなげればKnowledge Baseを元に質問応答を行うシステムを構成できるかに思われますが、さらに2つの壁に阻まれることになります。

1つ目は、Semantic ParsingとLink Predictionの二つのモジュールがそれぞれに誤った処理をしてしまうことに起因し、モジュール単独の誤りよりも不正解が蓄積し増幅される問題があります。

2つ目は、通常の会話は複雑なクエリを一回の自然文で発話されることはなく、複数のターンで少しずつ最終的な答えの範囲を絞るようなクエリとなるという点です。Semantic ParsingやLink Predictionのデータセットは、一つの複雑なクエリに焦点が当てられることが多く、そのままでは対話システムとしては扱いにくいという課題があります。

1つ目の問題にはエンドツーエンドなニューラルネットの構成で、2つ目の問題にはRNNや強化学習などのマルチステップの問題に適用しやすいモデルや学習方法を使用することにより、問題を解決しようという試みが多く見られます。

KB-InfoBot*11はDhingraらが提案した、マルチターンなKnowledge Baseを利用する質問応答システムを、エンドツーエンドなニューラルネット強化学習により構成することを試みたシステムです。

このシステムは下図のように、Belief Trackersでユーザーが入力した自然文を擬似的にSemantic Parsingを行い、DBのアクセスもニューラルネットの出力を元にユーザーが注目しているだろうと推定するDB内のレコードの分布を更新し、その情報を元に最終的にPolicy Networkで応答すべき内容(追加の質問or出力する答え)を決定します。このように全てを確率的な構造で扱うことにより、ルールベースの手法を上回る対話成功率を達成しました(最も大規模なKnowledge Baseで60%ほどの成功率)。

f:id:KSKSKSKS2:20190328003559p:plain
Dhingra 2017より

CSQA*12はSahaおよびPahujaらが提案したマルチターンの処理が必要で、複雑な質問が含まれるKnowledge Baseを利用した質問応答システム用のデータセットです。

このデータセットはKnowledge Baseからの単純な情報抽出だけでなく、条件に合うトリプルの統計情報や複数のトリプルを比較した答えを返す必要があり、また平均8ターンという非常に長いコンテキストを扱う必要があり、非常に難しいデータセットとなっています。

データセットの検証用として、下図のようなモジュールをリニアに結合した構成で実験が行われています。まずRNNで過去の質問文や応答文も含めてエンコードを行い、その後メモリーネットワークで擬似的にエンティティーの検索が行われ、デコーダーで最終的な応答を生成します。

既存のDeep Learningの手法をナイーブにつなげた構成ということもありますが、質問種類ごとの正答率が最大30%ほどと非常に低く、Knowledge Baseを利用した自然な会話を行う質問応答システムを構成することが難しい問題であることを示す結果となっています。

f:id:KSKSKSKS2:20190328004605p:plain
Saha and Pahuja 2018より

まとめ

Knowledge Baseを利用した質問応答システムは、Semantic ParsingとLink Predictionというそれぞれに難しい二つのキーとなる技術に支えられ、その二つを、マルチターンでの質問応答という自然な会話を実現するためにうまく接続する必要がある非常に難しいシステムであることをここまで見てきました。

一方で、シングルターンかつシンプルな質問であれば、Semantic ParsingおよびLink Prediction、それぞれの精度も非常に高く、ナイーブに接続してもそれなりの性能が出るシステムを構成する事ができます。

Knowledge Baseそのものを構成するという重い作業がありますが、一度構成してしまえば「じゃがいもが使われている評価が4以上のレシピを教えて」や「今のアメリカの大統領は誰?」などの質問に答えることはそこまで難しくない状況と言えるでしょう。

Knowledge Base質問応答システムを作成できる既存サービスを残念ながら知らないのですが(次回説明するテキスト質問応答システムを構築できるサービスはWatson DiscoveryやQnA Maker、Dialog Flow Knowledge Connectorsなど各社出していますが)、スロットフィリング型対話システムのスロットのような変数を取りうる質問に、ドキュメントを元にしたシステムよりも柔軟に応えることができる構成であることは間違いありません。

Knowledge Baseを構築することがどうしてもボトルネックになってしまいますが、Knowledge Base質問応答システムは堅実に使い勝手の良い対話システムを構成できるアプローチなのです。

「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:計算方法、描画方法ともにちょっと微妙な気がしてはいる。