はじめに
この記事の目的は、ディープラーニングを応用する人が具体的にどのように取り組んでいけば良いのかという方針と、その取り組みに必要な知識がどのようなものであるかを簡単にまとめることです。
「ディープラーニングを応用する人」に当てはまるのは、具体例を挙げると
・ディープラーニングを実問題に活用しようと言う人
ということであり、「ディープラーニングの理論や新たなモデルを考案する人」は当てはまりません。つまり、この記事ではディープラーニングに対する深い理解を促す内容や数学的に高度な内容は含まれません。
目標の設定と指標の決定
まず行わなければならないのは、ディープラーニングを何に応用し、どこまで行ったら上手くいったと言えるのかを決めなければなりません。場合によっては、ディープラーニングの活用前に、もっと単純なモデルを使ってもいいはずです(ディープラーニングを使う事自体が目的になることはない)。ディープラーニングを用いることにしたとして、以後、その取り組みの姿勢が、最初に決定した目標と指標に大きく依存します。
目標の設定
どのような目標を設定するかは当然立場や状況で異なるので、ここは自身で決定するしかありません。例えば「センサから得られる時系列データからの機械の異常検知」であったり、「これまで通った飲食店のデータからの、新たな飲食店の推薦システム」かもしれません。
まずは、どのようなデータを元に何を達成したいのかをハッキリと決めないことには前に進むことができません。
指標の決定
さて、異常検知を例に取ります。
センサのデータから、正常であれば「0」を、異常であれば「1」を出力するようなシステムを機械学習により獲得することを考えます。この場合、機械学習により得られたシステムの評価をする上で「正解率(accuracy)」は正しい指標と言えるでしょうか。
異常が「2時間(120分)に1分間くらいの割合」でしか起こらない場合(これでも機械としては十分に多いのだが)、学習データのほとんど正常データということになります。仮に機械学習を行った結果、「常に正常」という判断をシステムが行ったとしても、訓練データに対して正答率が99%以上の値を叩きだしてしまうのです。
この場合、明らかに「正解率(accuracy)」でシステムを評価するのはおかしいということになります。
例えば人命に関わるような機械の異常検知を行おうという場合には、異常が発生するのは非常に危険であるため、「正常なのに異常だと言ってしまう」ことより「異常を見逃してしまう」ことのほうが重大なミスであると言えます。
つまり、少しでも怪しかったらひとまず異常だと判断して、機械を停止させるようなことが必要かも知れないのです。このようなことは、目標を設定する上で明確にしておくべきであり、そうしてそれに合わせた評価指標を適切に定める必要があります。
すなわち目標を達成したかを上手く評価できる指標を適切に設定しなければならないのです(目標と評価指標はセット!)。
評価指標に対する最低限の知識
先ほどの異常検知の例で一旦考えましょう。「異常を取り逃すことが非常に危険である」という考えに基づいてみたいと思います。正常か異常という2値分類の問題に関しては、事実と判断結果に対して以下の4つの組み合わせが考えられます。
正常に対して正常と判断
正常に対して異常と判断
異常に対して正常と判断
異常に対して異常と判断
このように組み合わせを考えると、正解率を見るだけでは分からない概念を見ることができます。
今は「事実として異常であるデータ」に対して特に敏感になってほしいのです。正常を異常と言ってしまうことが増えてでも、異常を取り逃したくないのです(場合によっては正解率は下がる。しかしそれでも優先したいことがあるということ)。
この場合は「再現率」という評価指標が有効に働きます。再現率は各事実それぞれに対して定義されます。Aの再現率とは、Aが事実として起こっているとして、判断結果として正しくAを出力する割合です。
すなわち異常の再現率が100%であるということは、絶対に異常を取り逃がさないということなのです。このとき正常に対してどのような判断をしているかは定かではありません。極端な話、この場合は「常に異常」という判断をしておけば、異常の再現率は100%になるに決まっているからです。
したがって、これに対して精度というものを見ることも重要です。
また、この両方をバランスよく見ることのできるF値などもあります。
以下の記事では最低限知っておくべき評価指標について書いています。
知らない人は必ず頭に入れるようにしてください。
キーワードとしては
正解率(Accuracy)
再現率(Recall)
精度(Precision)
F値(F-value)
網羅率(coverage)
などがあります(精度は適合率とも言います。日本語だと訳にばらつきがあるので、英語で覚えると良いです)。評価指標は複数準備してもよく、Bの再現率を90%以上に保った状態で正解率を95%以上にするなどとしてもいいです。
機械学習における知識(補足)
評価の指標を決めることで、目標を達成できたかを正しく判断するのは絶対に必要なことです。一方で、先ほどの異常検知の例に戻ると、では実際どのような学習させればそれを達成することができるのかという問題にぶつかります。
当然、異常データが数が全体の「1/120」だけであり、正常データが全体の「119/120」であるとなれば、明らかに正常データに対する判断にばかり機敏になってしまいます(なぜなら、与えられたデータに対して間違わないように学習が行われるから)。このケースでは機械学習システムは、正常データに対して異常データの119倍もの学習を行うことになるのです。
そこで、大抵はデータ数を均一にすることを第一に考えます。しかし、異常検知の場合は、異常データが非常に少ない(そもそも異常とは稀に起こる事象である)ことが多いため、異常データの数に合わせようとすると、単にデータ数不足に陥りかねません。
もしもデータの不均一を解消できない場合は、
正常データのみを教師なし学習で学習させ(例えば自己符号化器)、異常検知時には正常データが入力されたときとは明らかに異なる出力が得られるようなシステムを構築しておく方法があります(そしてその出力に適当にしきい値を設ける)。
教師あり学習を用いる場合は、損失に対して重みを付与することが考えられます。今回は正常データが明らかに多いため、正常データに対する損失を小さめに見積もるようにするのです。
ニューラルネットワークの学習
最初に使うニューラルネットワーク
さて、目標と評価指標が決まったら、いよいよ学習をさせたいということになります。問題によってはディープラーニングではない方法を試してみるのもいいでしょう。ディープラーニングを使うと決めたら、どのようなニューラルネットワークを最初に使うのかを決めなければなりません。
ニューラルネットワークは柔軟に構造を変えられるため、特徴量を抽出しうるモデルを構築しやすく、End to Endの学習に一役買ってきました(とはいってもディープラーニングの専売特許ではないが)。
まずは各分野で活躍しているニューラルネットワークの基本的な構造を知り、自分の問題と最も近そうなものを選ぶことになります。
基本的に
時間的にも空間的にも独立である複数の特徴量を持つデータ
→全結合ニューラルネット(普通のニューラルネット)
空間の局所的な構造に意味のある多次元配列データ(例えば画像)
→畳み込みニューラルネット
時間的な変動に意味のあるデータ(例えば音声、自然言語)
→リカレントニューラルネット
という方針で行くと良いように思います。
ニューラルネットワークの細かい設定
使うニューラルネットワークのパーツを取り揃えたところで、まだまだ決めなければならないことはあります。それはユニットの数や層の数、正則化の強さやドロップアウト、最適化手法など、従来の機械学習では考えられないほど多いです。
ユニットの数と層の数
「ユニットの数は特徴量の数が5つくらいになりそうだから5つにしておこう」などという正確な見積もりは基本的には必要ありません。
ココラヘンは冗長な表現力をとりあえず持たせておいて、正則化などで制限を掛けていくというのが基本的な方針です。正直当てずっぽうに大きめの値を取らせておくことにします(かと言って100000個とかにしてもただ計算が重いだけです)。
層の数も具体的にいくつということは言えません。まずは計算を素早く回せて、かつ表現力も持たせられるように3層〜5層くらいで試しておいて、あとから増やしてみても良いでしょう。
正則化
正則化は主にL2正則化とL1正則化が用いられます。L2はパラメータの二乗に、L1はパラメータの絶対値に比例した損失を与えます。いずれも無尽蔵にパラメータの値が大きくなることを防ぎ、あり得ないほどの表現力に制限を掛けることができます。
L1ノルムの方は単に大きい値になることを防ぐのではなく、多くのパラメータが0になるように学習が進みます(損失の形状が、そうなりやすい形になる)。
どの程度の大きさの損失を与えるかは明確な指針はありません。大体は0.001などの小さな値から始めることになります。過学習が起こるようであればこの値を大きくする必要が出てきます。
この正則化の強さ自体を最適化するような方法も現在研究がされている段階です。
活性化関数
多層の場合はReLU関数が最も好まれます。もともと多層化を困難にしていた勾配消失問題が、ReLUによって劇的に軽減されることが知られており、もはや通常はReLUが用いられることが当たり前となってきました(計算速度的にも有利)。
順伝搬においてReLUでは負の入力を得た場合は、出力を0にします。そして、逆伝搬においては出力に比例した誤差を伝えていくため、一度出力が0になったユニットは二度と使われることはありません(以後ずっと0。パラメータが更新されないため。)。これは良く働くことも悪く働くこともあります。
良い効果としてはは不要な特徴量をネットワークが取り除いてくれるという効果が期待できます。
一方で、不運にも必要な特徴量が消されてしまい、以後、学習がずっと上手く進まないということも考えられます。それでもニューラルネットワークはもともと冗長な表現力を有しており、他のユニットが必要な特徴量を見出してくれる可能性もあるため、ReLUは好まれています。
ドロップアウト
ドロップアウトは学習の最中に用いるユニットをランダムに消去してしまいます(実際には出力をランダムに0にする)。これによってニューラルネットワークの自由度を制限することになります。消去するユニットは毎回変わるため、事実上、色んなパターンのニューラルネットでその都度学習をしていることになります。
学習終了後はドロップアウトを用いること無く、全てのユニットを使い、色んなパターンのニューラルネットの知恵を総動員して予測や分類を行うことになります(したがってアンサンブル学習と見ることもできる)。
また、近年はドロップアウトが変分ベイズ学習であることが証明され、より一層その効果に期待が寄せられるようになると思われます。ただし、ベイズ学習の結果を有効に活用する場合は、予測や分類に用いるときにもドロップアウトを掛けたまま利用することになります(もしもドロップアウトを掛けない場合は、パラメータを点推定したことになり、ベイズの旨味は消える)。
具体的には1つのデータに対して、順伝搬を複数回行います。その際毎回ドロップアウトするユニットを変えることで、毎回異なるニューラルネットの構造からの出力を獲得することができます。
これは見方を変えれば、ドロップアウトによってパラメータをサンプリングしていることに相当し(ドロップアウトされるユニットに付随したパラメータが、確率的に0になるというパラメータの確率モデルからのサンプリング)、これらのサンプリングを複数回行った全ての結果を考慮して予測や分類を行うことで、パラメータの周辺化を用いたベイズ予測を行うことができるということです(もちろん厳密には無限回のサンプリングが必要になるが、適当に打ち切る)。
ベイズという立場でドロップアウトを用いるかはともかくとして、ドロップアウトは正則化として非常に強力であることがもともと知られており、かつ、過学習が起こらないベイズ学習との関連も見出されたところで、ますます活用の場が広がると見ていいでしょう。
ベイズについて概要を知っておきたい人は以下の記事を読んでみてください。
バッチ正規化
バッチ正規化も積極的に使うと良いでしょう。
バッチ正規化は学習の進行を正しくしてくれるという考察があり(興味がアレば内部共変量シフトや自然勾配法などとセットで調べてみてください)、今後何かしら理論的裏付けが出てくる可能性もあります。
正則化としての効果もあり、ドロップアウトなどの必要性をなくすという意見もありますが、ドロップアウトの理論的な背景が明らかになってきたところで上手く組み合わせて使うことを考えてもいいでしょう(結局はタスク依存なところもあるので、どちらか一方で良い性能が出るならそれでも良いでしょう)。
学習の早期終了
早期終了とは学習を途中で打ち切るということです。
ニューラルネットは大抵過学習を起こします。しかし、検証データの評価指標の値が良い内に、学習を止めてしまうことで、ニューラルネットが過学習をする前に、ちょうどいいパラメータを獲得できてしまうこともあります(その点では、パラメータの更新には使われなくとも、検証データがハイパーパラメータの調整や、学習の終了に関与しているわけであり、ディープラーニングでは検証データとテストデータを明確に分ける重要性がより一層高まる)。
早期終了は例外なく常に使われるべきものです。そもそもエポック数をいくつにするべきかなどを決めることは不可能であり、検証データに対して良い性能が出る回数に設定されるべきだからです。
多くの場合は、検証データに対する評価指標が複数回悪化していった場合に学習を止めます。悪化が1回だけで学習を止めてしまうのはもったいないかも知れません。ディープラーニングでは停滞、あるいは悪化した後に、急に良くなることも起こるからです。
性能が出ない場合
性能が出ない場合は
・データを追加収集する
・ニューラルネットの設定をいじる
・用いるニューラルネット自体を変更する
・新たなニューラルネットを考案する
などの方法があります。
データの追加収集
ディープラーニングをやる上では(というか普通の統計でも)、データがしっかりと沢山準備出来ているに越したことはないので、可能であればデータの追加収集は常に最優先に考えてもいいでしょう。
もしかしたらデータが足りない、あるいは偏っているが故に上手く学習が行っていないだけの可能性も十分に考えられます。また、データを収集することで、それなりの性能が、更にいい性能になることもあります(Mnistのデータ100件とMnistのデータ50000件で学習を比べてみてください)。
ニューラルネットの設定をいじる
これはハイパーパラメータを変えるということに相当すると思っていいでしょう。ハイパーパラメータを上手く調整するには何度も何度も学習を行い比較する必要があります。
大抵はランダムサーチと呼ばれ、当てずっぽうでハイパーパラメータを変えていき、その結果を記録していく方法を取ります。これに対して、ある値の感覚で少しずつハイパーパラメータを変えていく方法をグリッドサーチと言います。綿密に調べられますが、ディープラーニングの学習は時間が掛かるため、サーチに膨大な時間が必要となります。
ハイパーパラメータの探索を自動化する方法も考案されています。
用いるニューラルネット自体を変更する
これはなかなか勇気のいる決断です。上記までで済むのならばそれに越したことはありません。ただ、一方で、もしもニューラルネットワークのモデルがそもそも問題に合致していなかったとさうれば、どれだけ細かい工夫を凝らしても一生良い結果は出ないでしょう。
例えばデータが系列として意味があることを見落としており、全結合のニューラルネットに各時間個別に入力していたとすれば絶望的です(そのようなことが無いよう、まずは基本的なニューラルネットワークの型を学ばなければならない)。
あるいは系列データに対して畳み込み、画像データにたいさいてリカレントネットという応用のされ方は近年沢山出てきているので、すこしそれらの手法を真似てみるのも手でしょう。
新たなニューラルネットワークの考案
これは研究者レベルで初めてできることであるので、下手にあれこれ弄り回してもそれほど良い効果は得られないと思われます。
ニューラルネットワークの設計それ自体は非常に簡単にできてしまうため、つい完全オリジナルのネットワークを構成したくもなりますが、目の前の目標に集中して、既に提案されているものの中で応用できるものがないかを真剣に考えたほうが実りが豊かでしょう。
コードを書くにあたって
データ成形
自分の手持ちのデータを、これから用いうるニューラルネットワークに入力できるように、データを整形する関数、あるいはクラスを必ず作っておきましょう。データのインポートから成形までをその都度コピペで書いていたら時間と行数の無駄遣いになります。
結果を記録するコード
フレームワークには結果を記録する実装が既に含まれている事が多いです。ChainerにもTrainerのExtentionがありますし、TensorFlowにはTensorBoardがあります。
しかし結果を記録するコードは一度書いてしまえば今後もずっと使いまわせるわけですから、自分の見やすい、あるいはフォーマットがあるのならばそれにあったものを出力してくれるコードを自分で書いておいた方が良いと思います。
フレームワークの利用
ニューラルネットワークの学習をサポートするフレームワークは沢山あるので、是非利用していきましょう。既に提案され、学習が済んでいるモデルをそのまま流用するような方法も、各フレームワークで実装されているので、応用上欠かせない存在になっています。
自身でニューラルネットワークを考案する必要がない限り、学習に関する全てのことがフレームワークの中で完結できます。
プログラミングの勉強のほとんどはフレームワークの使い方に費やされることになります(これは近年当たり前のようなものかも知れませんが)。
フレームワークの選択
ChainerとPyTorch
ニューラルネットワークを作るという観点において、最も応用範囲が広いのはChainerとPyTorchになります。Define by Runのフレームワークはニューラルネットワークの構築を、データが渡された段階で初めて行い、更にデータが渡されるたびに構築をしなおします(パラメータの値だけ保存しておいて、都度呼び出すということになる)。
データに応じてニューラルネットが構造や処理を変えなければならない場合にも簡単にコードを書くことができます。
一方で、データが渡されるたびにニューラルネットを構築するというのは、ニューラルネットが学習中に一切変更されることのないケースにおいて単なる無駄となってしまいます。計算速度の面では、フレームワークの実装の都合上不利だと言えます(ただTensorFlowと言うほど差はないので安心してもらいたい)。
問題はむしろ、柔軟に設計できてしまうことが、無駄なコードにつながってしまう可能性があるということです。データ構造を工夫すれば条件分岐を必要としないような場合にも、条件分岐を使えば簡単だからと、学習の最中にそれを頻繁に入れ込んでしまえば、学習の速度は極端に低下します(条件分岐のコンピュータへの負荷は尋常じゃない)。
これらのフレームワークを生産性高く使う場合は、しっかりプログラミングの基本を携えて、コンピュータの気持ちになって書くことも重要だと思われます。
ツリーLSTMなどを使う場合は、現状ChainerやPyTorchを使うしか無いため、選択の余地はありません(自然言語処理をやる方々はほとんどこれらを選ぶ。たまにDynetもいる)。
実装がPythonで完結しており、普通のPythonプログラムと同じようにインタラクティブでコードの実行ができるため、バグ取りが非常に楽です。ニューラルネットを途中まで書いてみて、本当に設計が上手く行っているか、望み通りの型が出てくるのかということが簡単に確認できます。
これは複雑なニューラルネットを構築するときにコードが膨大になってしまう場合にも、それらを簡単に分割できるということでもあり、非常に取り回しが楽です。
TensorFlow
ディープラーニングフレームワークの一大巨塔と言えるTensorFlowは、ChainerやPyTorchに比べればプログラミングの柔軟性は(かなり)低いです。Pythonでニューラルネットワークを書いたら、それをTensorFlowに渡して、以後学習ではそのニューラルネットワークの構造のみが繰り返し使われます。学習の最中で構造を変更したりすることはできません。
宣言的なプログラミングのパラダイムを有していると言え、命令的なプログラミング言語に慣れている場合には、最初から非常に手間取るところです。TensorFlowはあくまでC++の実装であり、PythonはTensorFlowと人間のコミュニケーションのツールに過ぎないためです。何が起こっているのかをPythonの世界で完全に理解することは困難極まります。
Pythonではこれで良いと思っているコードも、実際には全く違う形で受け取られている可能性もあります(恐らく、名前空間のことでバグが発生することは、一度は経験するはず)。
しかし、かなり多くの参加者がいるため、たいていの疑問は既に解決案がインターネット上で見つけられるため、それほど深刻に長時間悩み続けることは無いでしょう。またTensorBoardのおかげで、TensorFlowがどのようにニューラルネットの構造を認識したのかを視覚的に確認することができます。
TensorFlowは一度慣れてしまえば、あとはGPU周りのことも高速な計算の処理も全て丸投げできるため、動的にニューラルネットを変更する必要がない場合は、利用するフレームワークの第一候補に上がってくることでしょう。
以下の記事では、独特なTensorFlowについて初歩的な理解が深まるように順を追って説明しています。
Keras
TensorFlowの困難さを諸々隠蔽したまま、最も手軽にディープラーニングを利用できるのがKerasになります。もはや説明不要の大人気ラッパーです。バックエンドとしてTheanoやMxnetも選ぶことができます。
最後に
ディープラーニングを応用することを考える場合には、最低限必要な数学的な知識は「線形代数」だけだと思っていいでしょう。線形代数を学んだら、すぐに実装の方で試していくのが良いと思います。
その中で理論的な背景との関連を知りたくなった場合は「確率・統計」、最適化手法について詳しく知る必要が出てきた場合は「微分・積分」が必要になってきます。しかし、実装の場面では多くの場合順伝搬を記述するだけで済むため、線形代数を知っていれば事足りるでしょう。
今回はディープラーニングを応用していく立場で書きましたが、当然のこと深く理解しておくことには越したことがないので、時間が許せば私のブログや他の人のブログ、Slide Shareや書籍に目を通してもらえると良いと思います。