ニューラルネットワークの最近の記事

概要

先日の勉強会にてインターン生の1人が物体検出について発表してくれました。これまで物体検出は学習済みのモデルを使うことが多く、仕組みを知る機会がなかったのでとても良い機会になりました。今回の記事では発表してくれた内容をシェアしていきたいと思います。
あくまで物体検出の入門ということで理論の深堀りや実装までは扱いませんが悪しからず。


物体検出とは

ディープラーニングによる画像タスクといえば画像の分類タスクがよく挙げられます。例としては以下の犬の画像から犬種を識別するタスクなどです。

ディープラーニングで識別してみると

  • コーギー: 75%
  • ポメラニアン: 11%
  • チワワ: 6%
  • ...


のようにどの犬種か、確率としては出てくるものの画像内に犬が2匹以上いた場合は対応できなくなってしまいます。

example of image classification


この問題を解決するために物体検出のアルゴリズムが開発されました。物体検出の技術を使えば画像中の複数の物体の位置を特定して矩形(バウンディングボックス)で囲み、更にそれぞれの矩形について物体の識別を行うことが可能になります。

物体検出の例が以下になります。犬と猫がバウンディングボックスで囲まれ、それぞれ犬か猫か識別されていることがわかります。

example of image detection

物体検出モデルの歴史は深くR-CNNから始まりFast R-CNN、Faster R-CNNと精度と処理速度が改善されてきました。これらの手法は基本的に以下の動画のようにバウンディングボックスを画像内で色々と動かして物体が検出される良い場所を見つけ出そうというものでした。

example of image detection

物体検出についての歴史まとめより引用

その後、精度と処理速度とともにFaster R-CNNを上回るSSD(Single Shot Multibox Detector)が提案されました。
今回の勉強会ではこのSSDを解説してくれましたので、復習がてらこちらで私が解説させていただきます。


SSD (Single Shot Multibox Detector)

R-CNNではバウンディングボックスを色々と動かしてそのたびにCNNによる演算を行っていたので、1枚の画像から物体検出を行うのにかなりの処理時間がかかっていました。一方でSSDでは"Single Shot"という名前が暗示しているように、1度のCNN演算で物体の「領域候補検出」と「クラス分類」の両方を行います。これにより物体検出処理の高速化を可能にしました。


全体の構造

structure of SSD

SSD: Single Shot MultiBox Detectorより引用

SSDのネットワークは最初のレイヤー(ベースネットワーク)に画像分類に使用されるモデルを用いています。論文ではVGG-16をベースネットワークとしています。ベースネットワークの全結合層を切り取り、上のように畳み込み層を追加したものがSSDの構造になります。

予測の際はそれぞれのレイヤーから特徴マップを抽出して物体検出を行います。具体的にはそれぞれの特徴レイヤーに3×3の畳み込みフィルタを適用してクラス特徴と位置特徴を抽出します


検出の仕組み

SSD framework

SSD: Single Shot MultiBox Detectorより引用

(a)が入力画像と各物体の正解ボックスです。(b)と(c)のマス目は特徴マップの位置を表しており、各位置においてデフォルトボックスと呼ばれる異なるアスペクト比の矩形を複数設定します。各位置の各デフォルトボックスについてスコア(confidence)の高いクラスを検出します。

訓練時には各クラスの誤差と、デフォルトボックスと正解ボックスの位置の誤差を元にモデルの学習を行います。

勉強会のスライドがわかりやすかったのでこちらも参考にしていただければと思います。

a slide about how to detect objects using SSD


様々なスケールの物体を検出する仕組み

検出の仕組みを示した上の図で(b)と(c)を見ると4×4の特徴マップの方がデフォルトボックスが大きく、各特徴マップではデフォルトボックスの大きさが異なることがわかります。
SSDでは、サイズの違う畳み込み層をベースネットワークの後に追加することで様々なスケールで物体を検出することができます。

how to detect objects with various size


以下のように各特徴マップにおいてクラス特徴量と位置特徴量を算出します。

feature map at each layer


そして各畳み込み層で違うスケールで物体検出を行います。

result of detection as each layer


これにより各層からの検出結果が得られるので次の図のように重複が生じてしまいます。

result of detection with duplicate


そこで次のような手順を踏んで重複を除去します。

  1. スコアが高いデフォルトボックスのみ抽出。
  2. 各クラスごとにデフォルトボックスの重なり率IoUを計算
  3. IoUが高い場合はスコアの低いデフォルトボックスを除去


この処理の結果、1つの物体に複数のバウンディングボックスが付与されることを回避できます。

result of post-processing


ちなみに重なり率IoUは次のように計算されます。

how to calculate IoU

学習の仕組み

各特徴マップについて各クラスのスコアの誤差と、デフォルトボックスの位置誤差との合成関数から正解データとの誤差を計算します。クラス誤差と位置誤差それぞれの具体的な計算は論文を参照していただきたいですが、最終的な損失関数は2つの損失関数を重み付けしたものになります。

$$ L(x, c, l, g) = \frac{1}{N}(L_{CLS}(x, c) + \alpha L_{LOC}(x, l, g)) $$

この計算結果を元に誤差逆伝播法によりモデルの重みを更新します。

training SSD


まとめ

個人的に勉強会が行われた時期に物体検出の技術を使っていたので非常に勉強になりました。やはり使っている技術の中身を知ることは大切ですので、SSD以前の物体検出の手法についても勉強していきたいですね。次回はSSDと並んでよく利用されるYOLOの解説と実装をしてくれるとのことで楽しみです。


参考文献

  • Liu, W., Anguelov, D., Erhan, D., Szegedy, C., Reed, S., Fu, C. Y., & Berg, A. C. (2016, October). Ssd: Single shot multibox detector. In European conference on computer vision (pp. 21-37). Springer, Cham..
  • その他特に言及のない画像は勉強会のスライドより引用


Twitter・Facebookで定期的に情報発信しています!

概要

小説を丸ごと理解できるAIとしてReformerモデルが発表され話題になっています。今回はこのReforerモデルが発表された論文の解説を行います。

自然言語や音楽、動画などのSequentialデータを理解するには広範囲における文脈の依存関係を理解する必要があり困難なタスクです。"Attention is all you need"の論文で紹介されたTransformerモデルは広くこれらの分野で用いられ、優秀な結果を出しています。 例えば機械翻訳などで有名なBERTはTransferモデルが基になっており、数千語にも及ぶコンテキストウィンドウが使われLSTMに比べて長い文脈を考慮することができます。しかし、このようにモデルの規模が大きくなってくるとリソースの問題が発生し、大きな研究機関以外はモデルの訓練が行えない状況です。

ReformerはTransferを改良し軽量化することで、1つのアクセラレータ、16GBのメモリで100万ワードに及ぶ文章を扱えるようにしました。

この記事ではTransformerモデルがどう改善されたかを解説していきますので、Transformerモデル自体の解説については過去の記事を参考にして下さい。

【論文】"Attention is all you need"の解説


大規模なTransformerモデルが抱える問題

まずは従来のTransformerモデルを大規模化したときにどういった問題が起こるか説明します。


Attentionの問題

Transformerモデル内のAttentionの計算にはscaled dot-product attentionが使われています。

queryとkeyの次元を\(d_k\)としすべてのqueryをまとめた行列を\(Q\)で表し、keyとvalueも同様に行列\(K\)で\(V\)と表すとAttentionの計算は

$$ \rm{Attention}(Q, K, V) = \rm{softmax}(\frac{QK^T}{\sqrt{d_k}})V $$
トークンの長さを\(L\)とすると内積\(QK^T\)は計算量、メモリ量ともに\(O(L^2)\)となります。

例えば、\(Q\)、\(K\)、\(V\)のサイズがすべて\([batch\_size, length, d_k]\)だと仮定すると、トークンの長さが64Kの場合バッチサイズが1だったとしても

$$ 64\rm{K} \times 64\rm{K} \times 32 \rm{bit} \ \rm{float} \approx 16 GB $$

とメモリ消費が激しくなります。

このように小説のような非常に長い文章をTransformerで扱いたい場合に、Attention層で必要な計算量、メモリ量が問題になります。


Activationを保持しておくためのメモリ量の問題

もう1つの問題が逆伝播に必要な各層のActivationを保持しておくためのメモリ量の問題です。
まずTransformerモデル全体で32-bit floatのパラメータ数0.5B (Billion) を保持するのに約2GB必要です。
これに加えてトークンの長さが64KでEmbeddingのサイズが1024、バッチサイズが8とすると

$$ 64\rm{K} \times 1\rm{K} \times 8 \approx 0.5B \ floats $$


となるので、1つの層についてActivationを保持するためにさらに2GMのメモリが必要になります。
典型的なTransformerモデルが12個以上の層を持つので、Activationを保持しておくだけで24GB以上必要になります。

このようにモデルに与える文章が長くなるとすぐにメモリを使い果たしてしまいます。


Attentionの計算を効率化 (LSH Attention)

Attention層での処理で問題となるのは、内積\(QK^T\)の処理でした。Reformerではこれをlocality-sensitive hashing (LSH)によって解決します。

内積\(QK^T\)の処理が問題ですが、結局のところ知りたいのは\(softmax(QK^T)\)の結果です。

softmaxの結果は内積の値が大きい要素に寄与するため、queryとkeyの内積が大きくなるペアの計算結果だけを用いて近似値を求めることができます。つまりqueryとkeyのすべてのペアについて内積を計算するのではなく、queryに対して類似したkeyだけを考慮すればよいので処理が効率化されます。

このとき、queryと類似したkeyを選ぶのにLSHを使います。詳しい解説はここでは避けますが、LSHは高次元のデータを確立的処理によって次元圧縮する手法です。Reformerではハッシュ値の計算は次のようにして求めます。

以下の画像で示すようにベクトルをランダムに回転させてどの領域に移るかによってどのバケットに入れるかを決定します。画像内の上の例ではRandom Rotation 1 以外、xとyの移る先が異なるためそれぞれ違うバケットに入ります。一方で下の例では、xとyが3回の回転ですべて同じ領域に移るため同じバケットに入ります。

example of hashing function

次にLSHを利用してどのようにAttentionの計算を効率化するか具体的なステップを以下の図とともに説明します。図中の色はどのハッシュに割り当てられたかを示しており、似た単語は同じ色で表されます。

まずLSHによってハッシュを割り当てて、ハッシュ値によって単語を並べ替えます。並べ替えた後、小さな塊に分割することで並列処理を可能にします。そしてAttentionの処理を同じ塊の中および1つ前の塊に対して行うため大幅な計算不可の軽減ができます。

description of LSH attention

LSH Attentionの計算を式で表すと

$$ o_i = \sum_{j \in \mathcal{P}_i} \rm{exp}(q_i \cdot k_j - \mathcal{z}(i, \mathcal{P}_i))v_i \qquad \qquad \rm{where} \ \mathcal{P}_i = \{j:i \geq j \} $$
\(i\) はQの\(i\)番目の要素を表しており、\(\mathcal{P}_i\)はi番目のqueryに近いkeyの集合を表しています。また、\(\mathcal{z}\)はsoftmaxの分母の部分だと考えて下さい。

これらの処理により、計算量を\(O(n^2)\)から\(O(n\log n)\)まで削減することができます。これでAttentionの問題を解決することができました。


Activationを保持しておくためのメモリ量の削減 (Reversible Transformer)

Attentionの問題は解決されましたが、学習時に逆伝播のためにActivationを保持しておかなければならずメモリ消費が大きくなる問題が残っています。Reformerではこの問題を解決するために順伝播時にActivationをメモリ内に保持しておくのではなく、逆伝播時に再計算する方法を取りました。

この方法を実現させるためにGomez et al. (2017)で示されたReversible Residual Network (RevNet)を応用します。RevNetでは出力側から順次1つ先の層の結果を元にActivationを再計算します。

structure of reversible residual network

通常のネットワークでは(a)のようにベクトルが通過するスタックに各層が追加されていきます。一方でRevNetでは各層において2つのActivaltionを持ちます。そして(b)のようにそのうち1つだけが通常のネットワークと同じように更新されます。もう1つのActivationは(c)のようにもう一方のActivationとの差分を捉えるために使われます。

これにより順伝播時にActivationを保持する必要がなくなり、逆伝播時に出力側から順次再計算することでActivationを再現することができます。これでActivationを保持するためのメモリを削減することができました。


実験

最後にReformerのパフォーマンスを見てみましょう。実験には以下の入力のサイズが非常に大きいタスクに対して行われました。

  • enwik (テキストタスク) - 入力トークンの長さ64K
  • imagenet64 (画像生成タスク) - 入力トークンの長さ12K


まず、Reversible Transformerと通常のTransformerの性能を比べてみましょう。

experiment result of reversible transformer

グラフを見て分かるようにテキストと画像どちらのタスクにおいてもReversible Transformerが通常のTransformerとほぼ等しい性能を見せました。
Reversible Transformerによってメモリ消費を抑えても性能が犠牲になることはないとわかります。

次にLSH AttentionがTransformerの性能にどう影響するかみていきましょう。
次のグラフはfull attention (通常のAttention)とLSH Attentionとの比較です。グラフ中のn hashesはLSHでのハッシュの割り当て処理を何回行うか示しています。割り当てを複数回行った後、それぞれのラウンドでqueryと同じバケットに入ったすべてのkeyをAttentionの計算に利用します。なぜこのようなことをするかというと、LSHでは確率的にハッシュを割り当てるので、確率は低いですが類似した要素がちがうバケットに入る可能性があるからです。

グラフを見ると8 hashes以上でfull attentionと同じ性能になっています。

experiment result of LSH attention
最後に、以下のグラフは入力シーケンスの長さに対する学習にかかる時間を示しています。
入力シーケンスが長くなるとfull attentionでは指数関数的にかかる時間が増加する一方で、LSH Attentionはほぼ一定になっていることが見てとれます。

experiment result of attention speed


まとめ

ReformerはTransformerモデルをリソース面で改善することで16GBのメモリ、単一のアクセラレータで最大で100万語の文章の処理を可能にしました。また文章だけでなく画像や動画を扱うタスクへの応用も期待できます。さらに大きな研究機関以外でも非常に長いシーケンスを扱えるようになる可能性も秘めており、AIの民主化という観点でも今後期待が高まりそうです。


参考文献

  • Kitaev, N., Kaiser, Ł., & Levskaya, A. (2020). Reformer: The Efficient Transformer. arXiv preprint arXiv:2001.04451.
  • Gomez, A. N., Ren, M., Urtasun, R., & Grosse, R. B. (2017). The reversible residual network: Backpropagation without storing activations. In Advances in neural information processing systems (pp. 2214-2224).
  • Kitaev, N., Kaiser, Ł. (2020). Reformer: The Efficient Transformer. Retrieved from https://ai.googleblog.com/2020/01/reformer-efficient-transformer.html?m=1



Twitter・Facebookで定期的に情報発信しています!

機械学習では、訓練データとテストデータの違いによって、一部のテストデータに対する精度が上がらないことがあります。

例えば、水辺の鳥と野原の鳥を分類するCUB(Caltech-UCSD Birds-200-2011)データセットに対する画像認識の問題が挙げられます。意図的にではありますが訓練データを、

  • 水辺の鳥が写っている画像は背景が水辺のものが90%、野原のものが10%
  • 野原の鳥が写っている画像は背景が水辺のものが10%、野原のものが90%

となるように分割します。このときに、訓練データの中で「背景が野原で水辺の鳥」の画像や「背景が水辺で野原の鳥」の画像が少なく、同じようなテストデータに対して精度が上がらないことがあります。

Alt text

以降では、テストデータ全体の精度をaverage accuracyと呼ぶのに対して、このようなデータに対する精度をrobust accuracyと呼ぶことにします。パラメータの数が訓練データの数よりも多い(overparameterized)ニューラルネットワークでは、モデルの複雑度が高いために過学習しやすく、average accuracyは高くともrobust accuracyは低くなりがちです。

論文"Distributionally Robust Neural Networks"では、上記のような訓練データとテストデータの分布が異なるときのrobust accuracyを上げる最適化手法について説明されています。パラメータの多いニューラルネットワークがよく使われる画像認識や自然言語処理などのタスクに対して同じような最適化手法を適用でき、今後も広く使われる手法かもしれません。

この記事では論文で説明されていた手法について、簡単に概要を説明しようと思います。

"distributionally robust optimization"とは?

訓練データよりも多くパラメータが存在するニューラルネットワークでは、学習データにおけるロスの消失による過学習が問題となっていました。そのようなときは一般には平均的に汎化誤差(generalization gap)が小さくなるように最適化するのですが、どうしてもロスが最も大きいworst-case groupに対しては、依然汎化誤差大きいままになってしまいます。

そこで考えられた手法がdistributionally robust optimization(以下DRO)です。DROは一言で言えば最も大きいロスでの最小化です。

\[ \min_{\theta \in \Theta} \sup_{Q \in \mathcal{Q}} {\rm E}_{(x,y)\sim Q}[l(\theta;(x,y))] \]

\( \sup {\rm E}[l(\theta;(x,y))]\)が表すのがworst-case groupの平均のロスとなります。\(Q\)が表すのが分類する各グループ\(g\)ごとの分布の線形結合となるのですが、最小化は線形計画法のアルゴリズム(単体法)で行われます。そのため、最適解は実行可能領域の頂点、すなわちworst-case groupのみの分布における最適解と一致します。

\[ \min_{\theta \in \Theta} \max_{g \in \mathcal{G}} {\rm E}_{(x,y)\sim P_{g}}[l(\theta;(x,y))] \]

worst-case groupの分布では平均のロスが最も大きくなります。DROはその分布でのロスの最小化を目的とするアルゴリズムだと言えます。これまでの機械学習は平均的な汎化誤差を正則化(regularization)などによって低減させていましたが、DROはworst-case groupの精度の向上、つまりrobust accuracyの向上が目的です。

では、実際にどのようにしてworst-case groupの精度を向上させているのでしょうか?次節からはその具体的な手法について説明します。

従来手法によるrobust accuracyの向上

DROで使われる正則化の1つとして重み減衰(weight decay)が挙げられます。すでに様々な機械学習の中で使われている手法であり、例えば有名なものではL2正則化が挙げられます。

\[ E(w) + \cfrac{\lambda ||w||^{2}_{2}}{2} \]

論文では、画像認識のモデルであるResNet50においてL2正則化をするとき、\(\lambda\)は通常小さな値\((\lambda=0.0001)\)が設定されるが、この値を大きくするとrobust accuracyが上がるということが述べられています。つまり、強い重み減衰(strong weight decay)が手法の1つとして考えられます。

また、もう1つの正則化としてearly stoppingが挙げられます。こちらも機械学習でよく使われる手法ですが、想定されるニューラルネットワークのパラメータ数が多く学習数が大きいとすぐに過学習するため、early stoppingが有効だと言えます。

検証

それではDROの検証結果について見てみましょう。ベンチマークとしてERMモデルとの比較を行います。最初に述べたCUBのWaterbirdsの分類タスク(ResNet50による)の他に、CelebAデータセットにおける髪色の分類タスク(ResNet50による)と、MultiNLIデータセットにおける自然言語推論タスク(BERTによる)で比較しています。一般的な正則化(Standard Regularization)と前節で述べた2つの手法を試したときのaverage accuracy、robust accuracyは以下のとおりです。

Alt text

この結果からどのタスクにおいても、ERMではaverage accuracyに比べてrobust accuracyが大幅に低下したのに対して、DROのstrong weight decayとearly stoppingによってrobust accuracyが大幅に低下するのを防いでいることが確認できると思います。

また、CelebAに関してaccuracyの収束性についても見てみましょう。

Alt text

worst-case groupである「Blondの髪で性別がmale」の判別において、strong weight decayを用いたDROがそれぞれのグループで他の手法よりも良い収束性を持つことが確認できます。

グループサイズを利用した正則化

今回の論文では分類するグループの大きさを利用したDRO(group-adjusted DRO)についても述べられています。グループ内のデータの数を\(n_{g}\)とすると、最適化するべき目的関数は、

\[ \min_{\theta \in \Theta} \max_{g \in \mathcal{G}} {\rm E}_{(x,y)\sim P_{g}}[l(\theta;(x,y))] + \cfrac{C}{\sqrt{n_{g}}} \]

となります。ハイパーパラメータ\(C\)を用いた正則化項を付け加えるアイディアです。\(n_{g}\)の平方根の逆数を掛けることで、グループごとのデータ数を考慮した汎化をおこなうことができるようです。このgroup-adjusted DROのaccuracyは以下のようになっています。

Alt text

robust accuracyにおいてさらなる改善が見られますね。バリデーションによって\(C\)の値さえうまく決めることができればかなり役に立つ手法だと言えます。

importance weightingとの比較

実は同じような解決策として、importance weightingという従来手法が存在します。これはロスに重み付けした上で最小化を行う手法です。これはミニバッチからデータを等確率でリサンプリング(RS)することでrobust accuracyを上げる手法だそうです。こちらについてもベンチマークを見てみましょう。

Alt text

ERMよりもrobust accuracyが上がっていますが、DROほどではありません。DROは従来手法よりもrobust accuracyの向上に有効だと言えます。

終わりに

今回紹介した論文では訓練データとテストデータの分布の違いを考慮した手法であるDROについて簡単に紹介しました。従来よりもrobust accuracyを大きく上げたという点でより注目される手法だと思います。

しかし、なぜこのように正則化をすると精度が向上するのかという問いに対する明確な答えがまだない状態です。この論文を足がかりにrobust accuracyが上がる数理的なメカニズムが解明されれば、様々なモデルで汎化性能の向上が期待できるでしょう。

参考文献

DISTRIBUTIONALLY ROBUST NEURAL NETWORKS


Twitter・Facebookで定期的に情報発信しています!

はじめに

この記事では物体検出に興味がある初学者向けに、最新技術をデモンストレーションを通して体感的に知ってもらうことを目的としています。今回紹介するのはAAAI19というカンファレンスにて精度と速度を高水準で叩き出した「M2Det」です。one-stage手法の中では最強モデル候補の一つとなっており、以下の図を見ても分かるようにYOLO,SSD,Refine-Net等と比較しても同程度の速度を保ちつつ、精度が上がっていることがわかります。
M2Det

https://arxiv.org/pdf/1811.04533.pdfより引用

物体検出デモ

それではM2Detでの物体検出をしていきたいのですがひとつ問題が
著者がgithubに公開しているソースコードはCUDAを使用する前提のため、NVIDIAのGPUが搭載していない私のPCではすぐに動かす事ができません。
そのため今回はGPUなしでも動かせる環境を提供してくれるGoogle先生の力をお借りします!
ということでGoogle Colaboratoryを使ってM2Detを動かしていきます。
例のごとくqijiezhao/M2Detの説明を参考に進めていきます。


Step1.前準備

Google Colaboratoryを開いたら先ずハードウェアのアクセラレータをGPUに設定しましょう。メニューバーの「ランタイム」→「ランタイムのタイプを変更」をクリック。「ハードウェア アクセラレータ」のプルダウンからGPUを選択して「保存」します。これで設定は完了です。
M2Det
それではコードを書いていきます。必要なモジュールをインストールし、上記githubからクローンを作成します。M2Detファイルに移動したらシェルを以下のように実行します。

!pip install torch torchvision
!pip install opencv-python tqdm addict
!git clone https://github.com/qijiezhao/M2Det.git
%cd M2Det
!sh make.sh

Step2.学習済モデルをGoogle Driveからダウンロードする(引用)

次に学習済モデルを入手します(とてつもなく学習に時間がかかるので出来合いのものを使用させていただきます)。githubの説明にも書いてあるようにbackbornはVGG-16とし、指定のGoogle Driveからダウンロードしてきます。ダウンロードが簡単にできる便利なもの(nsadawi/Download-Large-File-From-Google-Drive-Using-Python)を見つけたので引用させていただきます。

#引用開始
import requests
def download_file_from_google_drive(id, destination):
  URL = "https://docs.google.com/uc?export=download"
  session = requests.Session()
  response = session.get(URL, params = { 'id' : id }, stream = True)
  token = get_confirm_token(response)
  if token:
    params = { 'id' : id, 'confirm' : token }
    response = session.get(URL, params = params, stream = True)
  save_response_content(response, destination)
def get_confirm_token(response):
  for key, value in response.cookies.items():
    if key.startswith('download_warning'):
      return value
    return None
def save_response_content(response, destination):
  CHUNK_SIZE = 32768
  with open(destination, "wb") as f:
    for chunk in response.iter_content(CHUNK_SIZE):
      if chunk: # filter out keep-alive new chunks
        f.write(chunk)
#引用終了
%mkdir weights
directory = '1NM1UDdZnwHwiNDxhcP-nndaWj24m-90L'
adress = './weights/m2det512_vgg.pth'
download_file_from_google_drive(directory, adress)

これでM2Detフォルダの中に学習済モデルm2det512_vgg.pthがダウンロードできました。


Step3.判別の閾値を設定する(任意)

ここで一旦パラメータの調整を挟みます。YOLOでは判別の閾値が0.5以上の時にアノテーションすることにしていたのでM2Detでも同じ値にします。demo.pyの63行目を確認すると

def draw_detection(im, bboxes, scores, cls_inds, fps, thr=0.2):

thrの値がデフォルトで0.2になっているのでこの値を0.5に以下のように書き換えます。

!sed -i -e "63c def draw_detection(im, bboxes, scores, cls_inds, fps, thr=0.5):" demo.py

Step4.Google Driveにアップロードした動画ファイルをM2Detフォルダの指定ディレクトリにコピーする

後は判別させたい画像または動画をGoogle Driveからimgsフォルダに移動させてきましょう。なのでご自分で予めGoogle Driveに画像または動画をアップロードしておきましょう。今回は動画を使用しているので以下のようにマウントした後、任意の動画をコピーします。実行の際にオースコード(Auth code)が要求されるのでURLをクリックして表示されるコードを貼り付けます。

from google.colab import drive
drive.mount('/content/drive')
!cp /content/drive/My\ Drive/*.mp4 ./imgs #動画用

M2Det
これで必要なものは全て揃いました。

Step5.デモ

ここでdemo.pyの引数について確認してみます。demo.pyの17行目から24行目を確認すると以下のように記述されています。

parser = argparse.ArgumentParser(description='M2Det Testing')
parser.add_argument('-c', '--config', default='configs/m2det320_vgg.py', type=str)
parser.add_argument('-f', '--directory', default='imgs/', help='the path to demo images')
parser.add_argument('-m', '--trained_model', default=None, type=str, help='Trained state_dict file path to open')
parser.add_argument('--video', default=False, type=bool, help='videofile mode')
parser.add_argument('--cam', default=-1, type=int, help='camera device id')
parser.add_argument('--show', action='store_true', help='Whether to display the images')
args = parser.parse_args()

parser.add_argumentの直後に記述されている引数を実行の際に記述することで様々な使い方ができるようです。別途ダウンロードしてきた学習済モデルで動画を判別させるので以下のように実行します。

!python demo.py -c=configs/m2det512_vgg.py -m=weights/m2det512_vgg.pth --video VIDEO #動画用

実行すると動画のディレクトリを指定するように出てくるのでディレクトリを指定します(Step4でimgsフォルダにコピーしたならimgs/(ファイル名))
後は各フレーム毎に物体検出をしてくれるので待ちましょう。
M2Det

Step6.ファイルをダウンロードする

残念ながらmp4をchrome上で再生する術を知らないため、ローカルにダウンロードして再生することにします。以下のように2行で簡単にファイルのダウンロードができます。

from google.colab import files
files.download('imgs/<ファイル名>')

実装例

おわりに

今回M2Detを使用して動画の物体検出を行ってみました。リアルタイムの識別を検討する場合は限られた時間内に一定以上の精度を保証する信頼性がより重要となり、M2Detはこれを達成する一歩になるのでは無いかと思いました。YOLOやSSDについてもまだまだ改良されていくと予想しているので、引き続きリサーチしたいと思います。また、物体検出を利用した異常検知や店の空席率把握などに使えそうなので実装できたらまたブログ書こうと思います。

実装コード

https://colab.research.google.com/drive/1oSPhiGmZC-IeLnyoR2l-UIKIquP1i51g

その他、ドーナツを検知し、無人レジの実現に向けて検証もしており、現在、当社では技術の実用化に向けて様々な検証をしています。


Twitter・Facebookで定期的に情報発信しています!

はじめに

まずは下の動画をご覧ください。
スクリーンショット 2019-12-27 17.18.19.png
スパイダーマン2の主役はトビー・マグワイアですが、この動画ではトム・クルーズがスパイダーマンを演じています。
これは実際にトム・クルーズが演じているのではなく、トム・クルーズの顔画像を用いて合成したもので、機械学習の技術を用いて実現できます。
機械学習は画像に何が写っているか判別したり、株価の予測に使われていましたが、今回ご紹介するGANではdeep learningの技術を用いて「人間を騙す自然なもの」を生成することができます。
「人間を騙す自然なもの」を作るには本物のデータがどのように生成されるのか理解しなければなりません。
例えば、猫の画像を生成するときに耳を3つ書いたりしません。なので猫の耳は2つであるということを学習しなければなりません。


GANによる成果物をもう1つ紹介します。こちらのサイトにアクセスすると人物の顔が出てきます。
ですが出てくる人物はこの世に存在せず、GANが生成した人物です。
ご覧になっていただいてわかるように不自然なところは何もありません。


1. GANとは

GANとはGenerative Adversarial Networksの略で敵対的生成ネットワークとも呼ばれ、2014年に発表されました。
特徴は2つのネットワークを戦わせることにあります。

Gをデータを生成するネットワーク(Generator)、DをデータがGから生成されたものか実際のデータかを判別するネットワーク(Discriminator)とします。
GはDを騙すような画像を生成できるように、DはGから生成されたデータを見抜けるように、競いながら学習をします。
論文では例として、Gを偽札を作る悪人、Dを偽札を見抜く警察としています。


全体のネットワーク構造は以下のようになっています。
deep-learning-for-computer-vision-generative-models-and-adversarial-training-upc-2016-5-638.jpg

Gを図中のGenerator、Dを図中のDiscriminatorとします。
Gは何かしらの確率分布 P_z (一様分布など)から生成されたデータ z (図中 Latent random variable)から、通常のネットワークのように重みをかけて本物と同じサイズの画像を生成します。
Dは入力として画像(図中 sample)を受け取り、本物か偽物かを判定します。

D(・) を「・が本物のデータと判断する確率」としたとき、GAN は以下のような評価関数を最小化するようにGとDのパラメータ θ_g と θ_d を更新していきます。
\begin{eqnarray*} \min _{G} \max _{D} V(D, G)=\mathbb{E}_{\boldsymbol{x} \sim p_{\text {data }}(\boldsymbol{x})}[\log D(\boldsymbol{x})]+\mathbb{E}_{\boldsymbol{z} \sim p_{\boldsymbol{z}}(\boldsymbol{z})}[\log (1-D(G(\boldsymbol{z})))] \end{eqnarray*}

2. アルゴリズム

パラメータθgとθdを更新する手順を以下に示します。
kはハイパーパラメータです。


1. 2~4をイテレーションの数だけ繰り返す
  2. (2-1)~(2-4)をkの数だけ繰り返す
    2-1. pzからm個のサンプル{z1,z2...zm}を取得
    2-2. 実際のデータからm個のサンプル{x1,x2,...xm}を取得
    2-3. θ
dを以下のように更新<

更新式1

  3. pzに従うm個のサンプル{z1,z2...zm}を取得
  4. θ_gを以下のように更新

\begin{eqnarray*} \nabla_{\theta_{d}} \frac{1}{m} \sum_{i=1}^{m}\left[\log D\left(\boldsymbol{x}^{(i)}\right)+\log \left(1-D\left(G\left(\boldsymbol{z}^{(i)}\right)\right)\right)\right] \end{eqnarray*}

更新式2

\begin{eqnarray*} \nabla_{\theta_{g}} \frac{1}{m} \sum_{i=1}^{m} \log \left(1-D\left(G\left(\boldsymbol{z}^{(i)}\right)\right)\right) \end{eqnarray*}


3. おまけ 学習の様子

zがpzに従うときG(z)がpgに従うとします。
概略は以下のようになります。

スクリーンショット 2019-12-26 13.58.30.png


GANの目的はpg = pdataとなることです。
つまり本物のデータが生成される分布pdataとpgが等しいため、理論上は見分けがつかないことです。
これを言い換えるとD(x) = 1/2 が成り立つことだとも言えます。
これは、Dが本物のデータxをを本物だと見分ける確率が1/2で、当てずっぽうで判断していることになります。


論文にはpgがpdataに近づいていく様子が載せられています。
スクリーンショット 2019-12-26 12.28.58-1.png


青のドット線 : D(x)
緑の線 : pg
黒のドット線 : 実際のデータxが従う確率分布p
data

  • a
    学習前の様子です。

  • b Dが上のアルゴリズム(2-3)の手順で更新された後です。

  • c Gを上のアルゴリズム4の手順で更新された後です。
    bの状態と比べて、pgがpdataに近づいた事がわかります。

  • d pg = pdataとなった様子です。

4.まとめ

GANは人を自然な画像を生成するだけでなく、最初に挙げたスパイダーマンのように2つの画像を自然に合成することもできるようです。
買おうか迷っている服を自分の体に合成して、擬似的に試着することもできます。
服の試着だけでなく、化粧品のお試しや、髪型が自分に合うのかなどを事前にわかっていると便利そうです。
今後はGANについて最新の傾向をつかめるようにしていきたいと思います。


Twitter・Facebookで定期的に情報発信しています!

AIの進歩が目覚ましい近年、文章の文脈まで読み取ってくれるAIがあれば嬉しいですよね。
ビジネスの場面でも、クレームとお褒めの言葉を分類したり、議事録のアジェンダと中身が合っているかなど、文脈を判断できればAIの使える場はぐんと広がります。

文脈を機械が読み取るのは難しく、研究者も今まで苦労してきました。
しかし近年、ELMoとBERTという2つのAIが現れたことで、今機械学習は目覚ましい進歩を遂げています。
多くの解説記事は専門家向けに書いてあることが多く、要点だけをまとめたり、仕組みをわかりやすく説明した記事は多くありません。
今回の記事は、そういった「よくわからないけどもっと知りたい」「本質をつかみたい」という要望に答えるべく、最新のAIをできるだけわかりやすく説明することを目的としています。


目次

  • 今までのAI
  • ELMoとBERT ~新星の誕生~
  • 弱点と新たな新星


今までのAI

今までのAIは、単語一つ一つに対して数字を当てはめる方式をとってきました。
有名な例がWord2Vecの
「王」+「男」ー「女」=「女王」
という式です。上手に学習ができれば、このように単語ごとに数字を当てはめても他の単語との相関関係を表す事ができました。

しかし、大きな問題があります。
例えば、次の文を御覧ください。

「昨日から雨が降っている」

この文は、砂漠地域についての文章と熱帯地域の雨季についての文章では意味合いが違ってきます。
他にも、

「コストは2倍だが、利益は3倍だ。」

「利益は3倍だが、コストは2倍だ。」

の2つでは重点が違いますし、順番が違うだけで文章全体の意味合いが変わってきてしまいます。
つまり、単語ごとの数値だけではなく、文脈を考慮した数値を使わないとAIは文章を正確に判断できないのです。

ELMoとBERT ~新星の誕生~

そこで2018年に生まれたのがELMo、そしてその後継者のBERTです。
ELMoは文の単語を1語ずつ読み取り、単語の情報を徐々に蓄積させていく手法を使います。
以下の図を御覧ください。

image1.png

緑の箱はLSTMで、前の単語の情報を引き継いで新しい単語の情報を学習させています。(LSTMについてはこちらを参照
つまり「利益は3倍」というフレーズが、前に「コストは2倍だが」と書いてあるという情報をふまえた上でAIに学習されることになります。
当然、後ろ側が前を参照できるだけでは、前の単語にしっかりと文脈を反映させることはできません。そこで文章の単語を逆向きにして同じような学習をさせます。
image2.png シンプルなように見えますが、ELMoはこの手法を使うことで様々な記録を塗り替え、AIの進歩に大きく貢献しました。

その僅か数カ月後、2018年10月に今度はGoogleがELMoを改良したBERTを発表し、時代はBERTのものになりつつあります。
BERTが改良した点は、主な計算にLSTM(上図で緑の箱)の代わりにTransformer(下図でオレンジの箱)を採用したことです。

image3.png

Transformerとは強そう名前ですが、論文"All you need is Attention"において注目を浴びたAIの学習機で、Attentionと呼ばれるその名の通り「注意度」を考慮に入れた非常に強力なAIのモデルとされています。(Attention モデルについてはこちらを参照
LSTMより強力な部品を使っている分、BERTのほうがELMoよりも性能が高いというわけです。

BERTはコードが公開されているだけでなく、日本語を含めた104ヶ国語に対応しており、まさにGoogleの力を存分に発揮したAIとなっています。

文脈を読み取るとどのような課題に対応できるのでしょうか。
実際にBERTは

  • MNLI:含意関係の分類
  • QQP:質問内容が同じであるかを当てる
  • QNLI:質問と文が与えられ、文が質問の答えになるかを当てる
  • SST-2:映画のレビューに対する感情分析
  • CoLA:文の文法性判断
  • STS-B:2文の類似度を5段階で評価する
  • MRPC:ニュースに含まれる2文の意味が等しいかを当てる
  • RTE:小規模な含意関係の分類


で最高スコアを叩き出しています。


弱点と注意点

ここまで解説したように、BERTは文章の文脈を考慮に入れて文章を分類・生成できる強力なAIで、今後の応用が期待されています。
しかし弱点があるわけではなく、正しく理解しないとお金と時間の無駄になってしまいます。
今回の記事では要点のみを説明したので、細かい部分や数学は省略してあります。
また、BERTは文章をインプットし、文章を出力するAIです。なので実際の個別の課題を解く際はBERTの出力を更に分類したりするモデルが必要になってきます。
しかし言語全体の文脈感を反映させるにあたってやはり強力なAIと言えるでしょう。

また、BERTには弱点があります。
実際の学習では、文章の単語をランダムに隠し、そこを学習させることで精度を向上さています。
しかし実際の文章では文中の単語が隠されていることはなく、また複数の単語が隠されていた場合、それらの間の関係性を学習することができません。

例えば、
[単語]は2倍だが、[単語]は3倍だ。

というふうに隠されていた場合、文が正解として認識される単語の組み合わせは多数あります。なので本来の「コスト」「利益」という単語を学ぶよりも精度が下がってしまいます。
この弱点を克服したのが、今年(2019年)6月に発表されたXLNetなのですが、それはまた次回のブログ記事でご紹介致します。

以上、文脈を読み取ることができる最新のAIについて解説してきました。細かい部分は省略していますが、本質だけを突き詰めると意外とシンプルだという事がわかります。
文脈を読み取ることができれば、社会の様々な場面でAIがますます活躍することになるでしょう。
そのために複雑な数学を理解する必要はありませんが、AIの本質をしっかり把握しておくことで、変化の波に遅れることなく、AI活用の場を見出す事ができるのです。


アクセルユニバースの紹介

私達はビジョンに『社会生活を豊かにさせるサービスを提供する。』ことを掲げ、このビジョンを通して世界を笑顔にしようと機械学習・深層学習を提案しています。
例えば、現在人がおこなっている作業をシステムが代わることで、人はより生産性の高い業務に携わることができます。


当社ではみなさまの課題やお困り事を一緒に解決していきます。
下記にご興味がありましたら、問い合わせ口からご連絡ください。

  • 問い合わせ業務を自動化したい
  • 入力された文章を振り分けたい
  • 機械学習・深層学習は他になにが出来るのか興味ある


Twitter・Facebookで定期的に情報発信しています!

〜普及に向けた課題と解決策〜に続き

 私が前回作成した記事である「海洋エネルギー × 機械学習 〜普及に向けた課題と解決策〜」では、海洋エネルギー発電の長所と課題とその解決策について触れた。今回はそこで取り上げた、

課題①「電力需要量とのバランスが取りにくい」、課題③「無駄な待機運転の時間がある」への解決策

~発電量・電力需要量予測~ ~機械学習を用いた制御~

を実装してみる。

 繰り返しにはなるが、この「発電量・電力需要量予測」と「機械学習を用いた制御」とは、まずカレンダー条件(時間帯、曜日など)と気温や日射強度などの気象条件を説明変数として電力需要量を予測し、電力需要量の送電にかかる時間分先の予測値と同量を発電するように発電装置を制御するという、極めて単純な制御である。前提として、電力は常に需要量と供給量(発電量)が同量でなければ、停電を起こしてしまうのであるが、この制御を行えば、需要と供給のバランスを常に保ちながら発電することができる。また発電装置の無駄な待機時間も削減され、設備稼働率も向上する。このようにして、課題①「電力需要量とのバランスが取りにくい」、課題③「無駄な待機運転の時間がある」を解決するのである。今回は発電方法を波力発電としてこれを実装する。


目次

  1. 今回の実装の設定
  2. 機械学習による電力需要量の予測
  3. 並進動揺型の波力発電装置の運動制御
  4. 考察
  5. 参考文献


1. 今回の実装の設定

1-1. どんな波力発電装置で実装するか

 まず波力発電装置の種類は表1、図1のように分類できる。

表1 波力発電装置の種類

スクリーンショット 2019-12-26 12.44.18.png スクリーンショット 2019-12-26 14.54.18.png

 図1波力発電装置の種類


 今回の実装では、垂直(上下)にのみ変位する並進動揺型の波力発電装置を考える。並進運動型の波力発電装置の特徴は、

  • 波に対する指向性がなく、他の発電装置に対して波向きの影響の点から優位である
  • 構造がシンプルで発電機構(PTO: power take off)も含め大部分が水中に没しているので、浮体本体の構造安全性や信頼性の点で有利である
  • 波浪荷重、発電荷重を下端で支えているため繰り返しの係留力が作用することになり、下部の構造強度に配慮する必要性がある
  • 発電装置が水面より下側にあることから、上部からの浸水に対しては十分注意が必要

 また、現在最も実用化に近いものと見られる波力発電装置は、米国のOcean Power Technologies社が開発した並進動揺型の波力発電装置「PowerBuoy」(図2)である。 スクリーンショット 2019-12-26 14.54.47.png

図2Ocean Power Technologies社の「PowerBuoy」


1-2. 実装の設定

 今回の設定は、1機の上下揺のみする並進動揺型の波力発電装置から、ある海沿いの一軒家に直接電力を供給するという状況を考える。波力発電装置から一軒家への送電時間は10分かかるとする。つまり上記の制御をこの波力発電装置に行うには、現在(時刻\(t\))の発電量\(P(t)\)が10分後(\(\tau=10~[分]\))の電力需要量の予測値\(P_{ML}(t+\tau)\)と等しくなる必要がある。


2. 機械学習による電力需要量の予測

 ある一軒家の電力需要量(電力使用量)を予測する時、本来ならば上記に示したようにカレンダー条件と気象条件を説明変数とするべきだが、カレンダー条件と気象条件、電力消費量のデータセットを手に入れることができなかったので、今回は特徴量を表3のようにしたデータセット(表4)を用いる。これは2016年1月11日17時00分から2016年5月11日16時50分までのデータセットである。

表3 データセットの特徴量

スクリーンショット 2019-12-26 16.11.02.png


表4 データセットの一部

スクリーンショット 2019-12-26 15.03.37.png


 初めに示しておくが、電力需要量は暑い夏や寒い冬など季節によって変動する。であるから正確に予測するためには少なくとも1年以上のデータ数が必要となるが今回はデータ数が1年に満たない。なので当然予測精度は悪くなると考えられるが、今回は実装手順を紹介したいので、精度については目を瞑る。
 予測モデルは時系列データに強いと言われているLSTMを用いた。すると予測結果は図3のようになる。破線はデータ値で、赤線が予測値である。但し値は0から1になるように正規化されているため、正規化された赤線の値を元に戻した値が\(P_{ML}(t+\tau)\)である。図3を見て分かるように電力需要量が多い時の予測値は実際の値とかなりずれているが、それは想定内で、それ以外の時は良く予測されている。この予測値を波力発電装置の制御に用いる。

スクリーンショット 2019-12-26 13.41.48.png

図3 予測結果


3. 並進動揺型の波力発電装置の制御

 まず、波浪中を自由に動揺する浮体(水面に浮く物体の総称)の問題は船舶流体力学の知識から、以下の二つの問題に分別することができる。

表5 波浪中を自由に動揺する浮体の問題の分類

スクリーンショット 2019-12-26 14.49.14.png


 ディフラクション問題において浮体に働く力には以下のようなものがある。但し、重力と浮力は釣り合っているとして除外する。

表6 ディフラクション問題において浮体に働く力

スクリーンショット 2019-12-26 14.49.41.png


 ラディエイション問題において浮体に働く力には以下のようなものがある。

表7 ラディエイション問題において浮体に働く力

スクリーンショット 2019-12-26 14.50.02.png


 当然ではあるが上下揺のみする並進動揺型の波力発電装置も浮体の一つであるから、ここでは浮体と呼ぶことにする。
 まず浮体の動揺を制御していない場合、波浪中の浮体の運動方程式は、浮体の上下方向の運動変位を\(z\)、上記で示したラディエイション力、復原力、波浪強制力の上下方向成分をそれぞれ\(F_{3}^{R}(\ddot{z},\dot{z})\)、\(F_{3}^{S}(\ddot{z},\dot{z})\)、\(F_{3}^{W}(\ddot{z},\dot{z})\)とすると、 \[ m\ddot{z}(t)=F_{3}^{R}(\ddot{z},\dot{z})+F_{3}^{S}(z)+F_{3}^{W}(t)\tag{1} \] と表せる。ここで\(F_{3}^{R}(\ddot{z},\dot{z})\)、\(F_{3}^{S}(z)\)、\(F_{3}^{W}(t)\)は数値計算または計測されていて既知であるとする。
 次に浮体の動揺を制御する場合、浮体の運動方程式は式に制御力\(G_{3}(t)\)を加えて、 \[ m\ddot{z}(t)=F_{3}^{R}(\ddot{z},\dot{z})+F_{3}^{S}(z)+F_{3}^{W}(t)+G_{3}(t)\tag{2} \] と表せる。この制御力\(G_{3}(t)\)を調節することで、望んでいる量を発電するように浮体を動揺させることができる。
 発電量\(P(t)~[W]\)は、発電機構の巻線抵抗\(R_{s}~[\Omega]\)、推力係数\(K_{t}~[N/A]\)を用いると、機械入力\(-G_{3}(t)\dot{z}(t)\)から発電ロス\(\cfrac{ R_{s} }{ K_{t}^{2} }G_{3}^{2}(t)\)の差として表せるので、 \[ P(t)=-G_{3}(t)\dot{z}(t)-\cfrac{ R_{s} }{ K_{t}^{2} }G_{3}^{2}(t)\tag{3} \] となる。
 この発電量\(P(t)\)を、機械学習(machine learning)で予測した時刻\(t+\tau\)の電力需要量\(P_{ML}(t+\tau)\)となるように制御したいので、 \[ P(t)=P_{ML}(t+\tau)\tag{4} \] とする。すると式\((3)\)は\(G_{3}(t)\)の二次方程式 \[ \cfrac{ R_{s} }{ K_{t}^{2} }G_{3}^{2}(t)+\dot{z}(t)G_{3}(t)+P_{ML}(t+\tau)=0\tag{5} \] となる。二次方程式の解の公式より、制御力\(G_{3}(t)\)は \[ G_{3}(t)=G_{3}(\dot{z})=\frac{ -\dot{z}(t)\pm\sqrt{ \dot{z}^2(t)-4\cfrac{ R_{s} }{ K_{t}^{2} }P_{ML}(t+\tau) } }{ 2\cfrac{ R_{s} }{ K_{t}^{2} }}\tag{6} \] と求まり、浮体の上下揺の速度\(\dot{z}\)の関数となる。この制御力\(G_{3}(\dot{z})\)を式\((2)\)に代入することで、浮体の動揺は\(P_{ML}(t+\tau)\)だけ発電するように制御される。


4. 考察

 上記で示したように制御することで、確かに需要と供給のバランスを常に保ちながら発電することができる。ただ、今回は実装の手順を示すことを目的として電力需要量の予測の精度については気にしなかったが、本来はここが極めて重要である。2章でも示したが、今回使用したデータは1年に満たないデータ数である。これでは電力需要量の朝、昼、夜の変化は捉えることができたとしても、季節の変化は捉えることができない。これが予測精度を落としている明らかな理由である。
 また、今回は10分間で送電が完了するという状況を考えたためそう遠くない未来の電力需要量を予測するだけであったが、実際波力発電装置が陸地からかなり遠い沖合に設置された場合、10分で送電できるとは考えられない。つまりもっと先の未来の電力需要量を予測できなければならない。実際に今回のデータセットで1時間先の電力需要量を予測してみたところ、かなり酷い精度になり、これでは波力発電装置の制御には使えないという結果となった。しかしこの原因もデータ数が足りないことであることは分かっている。季節の影響を捉えていないモデルで、未来を上手く予測できないのは当然である。
 上記二つの問題はデータ数を増やすことで改善できるので、長い期間の電力需要量のデータを入手次第、改めてやり直そうと考えている。
 問題はまだある。それは発電量が多い時、精度が悪くなることである。これは今のところ原因が分かっていないので、引き続き研究していく。
 また、今回は求めている発電量を発電できるような制御を行ったが、安全性については全く考慮していない。たとえ電力需要量と同量発電できたとしても、事故を起こしてしまっては元も子もない。なので、今回の制御に安全性の配慮を加えた研究も今後行っていく。


5.参考文献


Twitter・Facebookで定期的に情報発信しています!

アクセルユニバースの栗木です。
今回実務の練習として、同じインターン生の入江君と共に模擬案件に取り組みました。
模擬案件とは、機械学習を用いることで実生活におけるどのような問題を解決できるかを考え、課題設定からデータの準備、前処理、学習、検証までの一連の作業を経験するための問題です。
今回の取り組みの中で僕達が取り組んた内容や理論を各ステップ毎に紹介していきたいと思います。記事は2部構成として、本記事では前半部分を記します。


Step. 1 課題設定

今回は無人レジの実現に挑戦します。
僕は並ぶことが苦手なので、どうにかレジの混雑を解消できないか考えました。
レジ処理は「いらっしゃいませー」「レジに商品を登録」「支払い」「ありがとうございましたー」の流れですよね。ここの「レジに商品を登録」をどうにかして効率化できれば、レジの混雑を解消でき、普及すれば、僕みたいな並びたくない族もたくさん買い物に行くはずです。


今回の目標は画像検出を用いてレジを打つことなくカメラに映すだけで合計金額を計算することとします。
画像を利用する価値を高めるためにタグが付けられない商品(食べ物)で、誰もが大好きなドーナツを対象とします。特にまだ無人化が進んでいないドーナツ屋さんの商品をトレーに載せた状態を想定しました。検出が成功すれば金額だけでなくカロリーなど+αの情報も表示が可能になりますね。


まずはじめにPart1ではひとつひとつのドーナツが何であるかを判定しました。先に結果だけお伝えすると、現時点で正答率が99%を超えていて今後の検討も楽しみです。


実際にシステムを作っていきます。まずは学習データを用意していきましょう。

Step. 2 学習データの準備

早速7種類のドーナツを購入し撮影を行いました。
角度や裏表を変えつつそれぞれ100枚の写真を撮りました。以下に写真の一例を示しておきます。
ちなみに撮影後においしく頂きました。^^

Donuts また、それぞれのドーナツの名前(ラベル)は以下とします。
8枚目の写真は物体検出する際にアノテーションされたデータが必要となるのでそのためのものです。アノテーションに関する詳細は後半の記事で説明したいと思います。


1.エンゼルフレンチ
2.ダブルチョコレート
3.エンゼルクリーム
4.ポン・デ・リング
5.オールドファッションハニー
6.ストロベリーカスタードフレンチ
7.チョコファッション
8.複数ドーナツ(物体検出用)

Step. 3 画像データの読み込み

学習のための画像データが準備出来たので、学習に備えてデータの読み込みとサイズを統一するためリサイズを行います。
今回はGoogle Colabratoryを使用するため、Google Driveに先程の画像をアップロードし以下のようにマウントすることでGoogle Colabratoryからデータにアクセス出来るようにします。
Google DriveにはDonuts Filesディレクトリを作成し、そこに先程の画像のうち複数ドーナツ以外の7種類の画像をディレクトリ毎にまとめてあります。(複数ドーナツの画像は後半で扱います)


from google.colab import drive  
drive.mount('/content/drive')

次にDonuts Files内の画像をディレクトリごとに読み込みリサイズを行います。

#必要なライブラリのimport
import os
import numpy as np
import glob
import cv2
from keras.utils import np_utils

IMAGE_SIZE = 100 #リサイズ後のサイズ指定

# Donuts Filesディレクトリから画像の読み込み及びリサイズ
def get_data_and_label(path):
  X = []
  Y = []
  label = {}
  i = 0
  for dir_name in os.listdir(path):
      label[dir_name] = i
      for file_path in glob.glob(path+'/'+dir_name+'/'+'*'):
        img = cv2.imread(file_path)
        img = cv2.resize(img,(IMAGE_SIZE,IMAGE_SIZE)) #100*100にリサイズ
        X.append(img)
        Y.append(i)
      i += 1
  X = np.array(X)
  Y = np.array(Y)
  X = X.astype('float32')
  X = X / 255.0 #RGBの値を正規化
  Y = np_utils.to_categorical(Y, len(label)) #One-hotベクトルの作成
  return X, Y, label

X, Y, label = get_data_and_label('drive/My Drive/Donuts Files')

これでデータの読み込みとリサイズは完了です。最後に精度の評価用にデータの分割を行います。

from sklearn.model_selection import train_test_split

# 学習データのうち20%を検証データにする
X_train, X_test, y_train, y_test = train_test_split(X, Y, test_size=0.20)

データの準備が出来たので、実際にモデルの学習と精度の検証を行いましょう。
最終目標は物体検出を行うことですが、前半である本記事ではまず1枚の画像に対して1つのラベルの予測を行う画像認識を実装しどの程度分類出来るか確認します。

Step. 4 モデル構築&学習

今回の問題は画像認識なので、モデルはCNN(畳み込みニューラルネットワーク)を使用します。
CNNの詳細については多くの方が記事にされているのでそちらを参考にして下さい。
簡単に説明すると、CNNは畳み込み層、プーリング層、全結合層から成り立っており、まず畳み込み層では複数のフィルタを使用して画像を畳み込むことでエッジやパターンなどの特徴を抽出します。
次にプーリング層でデータサイズを減らしつつ平行移動などの処理に対するロバスト性を獲得し、最後に通常のディープラーニングと同様の全結合層でこれまでに抽出した特徴とラベルの関係性を学習します。
CNNは2012年の画像認識コンペ(ILSVRC)で圧倒的な成績を残して注目されて以来、急速に発展を続けているので是非色々と調べてみて下さい。
それではKerasを用いてモデル構築を行いましょう。KerasはバックエンドでTensorFlowを利用するディープラーニングライブラリラッパーです。

#必要なライブラリのimport
import tensorflow as tf
import keras
from keras.models import Sequential
from keras.layers import Dense, Activation, Flatten
from keras.layers.convolutional import Conv2D
from keras.layers.pooling import MaxPool2D
from keras.optimizers import RMSprop  #最適化手法としてRMSpropを使用

# モデルの定義
model = Sequential()

model.add(Conv2D(32, (3, 3), activation='relu', padding='same', input_shape=(IMAGE_SIZE, IMAGE_SIZE, 3)))
model.add(MaxPool2D((2, 2), padding='same'))
model.add(Conv2D(64, (3, 3), activation='relu', padding='same'))
model.add(MaxPool2D((2, 2), padding='same'))
model.add(Conv2D(128, (3, 3), activation='relu', padding='same'))
model.add(MaxPool2D((2, 2), padding='same'))
model.add(Flatten())
model.add(Dense(64, activation='relu'))
model.add(Dense(len(label), activation='softmax', name='predicton'))

Kerasではモデルの構造をmodel.summary()から以下のように簡単に確認できるので非常に便利ですね。

_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_1 (Conv2D)            (None, 100, 100, 32)      896       
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 50, 50, 32)        0         
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 50, 50, 64)        18496     
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 25, 25, 64)        0         
_________________________________________________________________
conv2d_3 (Conv2D)            (None, 25, 25, 128)       73856     
_________________________________________________________________
max_pooling2d_3 (MaxPooling2 (None, 13, 13, 128)       0         
_________________________________________________________________
flatten_1 (Flatten)          (None, 21632)             0         
_________________________________________________________________
dense_1 (Dense)              (None, 64)                1384512   
_________________________________________________________________
predicton (Dense)            (None, 7)                 455       
=================================================================
Total params: 1,478,215
Trainable params: 1,478,215
Non-trainable params: 0
_________________________________________________________________

それでは最後にモデルの学習と評価を行います。

# モデルのコンパイル
model.compile(loss='categorical_crossentropy',
              optimizer=RMSprop(),
              metrics=['accuracy'])

#パラメータの指定
batch_size = 64
epochs = 20

# 学習
history = model.fit(X_train, y_train, epochs=epochs, batch_size=batch_size)

# 検証データによるモデルの評価
print(model.evaluate(X_test, y_test))

最終的なlossと正解率は以下のようになりました。

loss = 0.02750563397655182
accuracy = 0.9927007299270073

正答率が99%を超えており非常に高いスコアとなりました。
今回はかなり問題設定が単純だったことと、本来は同じ種類であっても多くのドーナツを用意すべきところを1つのドーナツから学習データを作成したことがこのスコアにつながったと考えられます。

終わりに

ドーナツ検出に向けて前半である本記事では、目的を設定し画像を収集するところから単純な画像認識を行うところまでご紹介しました。
最近は画像認識を簡単に実装出来るので、是非身近なものなどの分類に挑戦してみて下さい。


さてドーナツの分類は高精度で行えることが確認できたので、後半はいよいよレジの無人化という課題に向けて検出に挑戦しようと思います。
後半では物体検出のアルゴリズムの概要や検出の様子を紹介したいと考えているので、後半もよろしくお願いします!


アクセルユニバース株式会社(以下当社)では、人が対応している業務を画像認識、音声認識、文字認識等を活用して効率化する機械学習ソリューション開発をおこなっています。
今回挑戦している無人レジだけではなく、人と会話するかのようにシステムに問合せができるチャットボットや、カメラの前の人が誰であるか判別できる認証システムも検討しております。

  • 機械学習でどんな事ができるのか興味がある
  • 課題と感じている業務がある
  • 効率化したい業務がある


上記のような方はぜひ問い合わせページにご連絡ください。


Twitter・Facebookで定期的に情報発信しています!

はじめに

今回はU-netの番外編でskip-conectionについてまとめようと思います。 前回のtensorflowで学習をさせてみた際に、skip-conectionがない場合、学習が遅いだけでなく精度が悪いことがわかりました。 skip-conectionが(おそらく)初めて使われたのが2015年12月に発表されたResNetで、ImageNet2015の分類コンペ(ILSVRC 2015)で1位になりました。


目次

  • ResNet以前
  • skip-conection
  • skip-conectionの効果
  • なぜskip-conectionが効くのか


1.ResNet以前

深層学習において層の数はとても重要な要素であり、ImageNetにおいてResNetが発表される以前は16~30層ほどの比較的深いモデルが主流でした。 そこで「層を深くすればするほど、精度は良くなるのか」という疑問が生まれますが、そう上手くはいきません。 なぜなら層を深くすることにより勾配消失問題が起きてしまうからです。 この勾配消失問題に対してBatch Normalizationが考案され、勾配消失が起きづらくなりました。

しかし、層を深くしていく中である深さで精度が頭打ちになり、更に層を増やすことで精度が著しく悪くなってしまいました。

Alt text

上の図を見ると18層のモデルに層を追加し34層にしても精度がほとんど変わらないどころか、むしろ悪くなっていることがわかります。


2.skip-conection

ResNetの著者は、上の図に現れている劣化現象は勾配消失によるものではなく重みの最適化に問題があると考えました。つまり層を深くしても最適化できず、層を深くした恩恵を受けられないと考えました。 そこで考案されたのがskip-conectionです。

Alt text

xを入力として、xに重みをかける(weighted layerを通る)関数をF(x)とします。 F(x) + xを最終的な出力としています。

skip-conectionを34層のモデルで行った様子は以下のようになります。 左が通常のモデル、右がskip-conectionありのモデル

Alt text


3.skip-conectionの効果

通常のモデルにskip-conectionを用いたものをResNetと呼ぶことにします。 上と同様にImageNetで精度を計算すると以下のようになりました。

Alt text 層が深いモデルの方が精度が良いことがわかります。 さらに重要なことに34層のモデルの学習時の精度(赤の細線)が、18層のモデルの学習時の精度(青の細線)よりもかなり良くなっていることがわかります。 これは劣化現象を上手く解決し、より深く学習できるようになったことを示しています。 (層を深くすることによる恩恵を受けられた。)


4.なぜskip-conectionが効くのか

Alt text xを入力としてH(x)を目標とする関数とします。 xに重みをかける(weighted layerを通る)関数をF(x)とします。 F(x) + xを最終的な出力とし、これを目標の関数H(x)に近づけます。

F(x) + x = H(x) の時(目標の関数に等しくなれた時) F(x) = H(x) - xとなることから、Fは入力xと目標とする関数H(x)の残差(residual)と等しいことがわかります。

つまりFそのものをHに近くなるように学習するのではなく、Hとxの残差を正確に予測できるように学習しています。

このようにするとHが恒等関数に近い時、FそのものをHに近づけるのは難しいですが、F+xを近づけることは簡単になります。 論文の筆者も劣化問題は目標の関数Hが恒等関数の時、重みに対して非線形である関数FでHに近づくことが難しいために起きていると主張しています。


まとめ

U-netでskip-conectionは畳み込みにより失われる物体の位置情報を保持する役目があり、skip-conectionが使われ始めた当初とは少しだけニュアンスが違うように感じました。 残差を予測するという考えは以前からあるようで、代表的なものだとGBDTが挙げられます。 こちらの記事も是非ご覧ください。

GBDTを小学生レベルの数学で直感的に理解する


参考

https://arxiv.org/abs/1512.03385


Twitter・Facebookで定期的に情報発信しています!

概要

DeepArtのようなアーティスティックな画像を作れるサービスをご存知でしょうか?
こういったサービスではディープラーニングが使われており、コンテンツ画像とスタイル画像を元に次のような画像の画風変換を行うことができます。この記事では画風変換の基礎となるGatysらの論文「Image Style Transfer Using Convolutional Neural Networks」[1]の解説と実装を行っていきます。

examples_of_style_transfer

引用元: Gatys et al. (2016)[1]


手法

モデルにはCNN(Convolutional Neural Network) が用いられており、VGG[2]という物体認識のための事前学習済みのモデルをベースとしています。


こちらの図はCNNが各層においてどのようにコンテンツ画像とスタイル画像を表現するか示しています。

image_representaion_on_each_layers_in_CNN

引用元: Gatys et al. (2016)[1]


Content Reconstructions (下段) のa, b, c を見ると入力画像がほぼ完璧に復元されていることがわかります。一方で d, e を見ると詳細な情報は失われているものの、物体認識をする際に重要な情報が抽出されていることがわかります。これは画像からコンテンツが抽出され、画風を表す情報が落とされていると考えられます。よって画風変換のモデルでは画像からコンテンツを捉えるためにCNNの深い層を利用します。
次にStyle Reconstructions (上段) を見てみましょう。a からe はそれまでの各層の特徴マップの相関をもとに復元されています。例えば c は第1層から第3層までの各層における特徴マップの相関から生成されています。こうすることで画像内のコンテンツの配置などによらない画風を抽出することができます[3]。


画風変換のモデルではコンテンツ画像とスタイル画像を入力として受け取り、上記のことを利用してスタイル画像の画風をコンテンツ画像に反映した画像を新たに生成します。


では具体的にモデルの中身を見ていきましょう。


モデルのアーキテクチャ

algorithm_of_style_transfer_model

引用元: Gatys et al. (2016)[1]


まずコンテンツ画像 (\(\vec{p}\)) とスタイル画像 (\(\vec{a}\)) から特徴マップが抽出されます。次にこれらの特徴マップと生成画像 (\(\vec{x}\))の特徴マップとの損失が計算されます。この計算で求められる損失をそれぞれコンテンツ損失、スタイル損失と呼ぶことにします。そしてコンテンツ損失とスタイル損失の合計が最終的な損失となり、これを最小化していきます。
ここで注意しなければならないことが1つあります。通常、ディープラーニングでは重みが最適化の対象になりますが、今回は重みは固定して生成画像のピクセルを最適化します。


コンテンツ損失

コンテンツ損失はコンテンツ画像と生成画像のVGGのある1層から出力された特徴マップの平均二乗誤差によって計算されます。

$$ \mathcal{L}_{content} (\vec{p}, \vec{x}, l) = \frac{1}{2} \sum_{i, j}^{} (F_{ij}^{l} - P_{ij}^{l})^2 $$ ここで\(F_{ij}^{l}\) は生成画像の\(l\)層における\(i\) 番目のフィルターの位置\(j\) でのアクティベーションを表しています。\(P_{ij}^{l}\)についても同様ですが、こちらはコンテンツ画像についてのアクティベーションを表しています。


スタイル損失

スタイル画像から画風を捉えるためにまず各層における特徴マップの相関を計算します。

$$ G_{ij}^{l} = \sum_{k}^{} F_{ik}^{l}F_{jk}^{l} $$ これはグラム行列と呼ばれ、\(l\) 層におけるフィルター間の特徴マップの相関をとっています。このグラム行列を用いることで画風を表現することができます[3]。 そして生成画像のグラム行列とスタイル画像のグラム行列の平均二乗誤差を求めます。 $$ E_{l} = \frac{1}{4N_{l}^{2}M_{l}^{2}} \sum_{i,j}^{} (G_{ij}^{l} - A_{ij}^{l})^2 $$ ここで\(N_{l}\)は特徴マップの数、\(M_{l}\)は特徴マップのサイズを表します。また\(G_{ij}^{l}\)、\(A_{ij}^{l}\) はそれぞれ\(l\) 層における生成画像のグラム行列とスタイル画像のグラム行列を表します。  最後に各層の損失の線形和をとってスタイル損失とします。 $$ \mathcal{L}_{style} (\vec{a}, \vec{x}) = \sum_{l=0}^{L} w_{l}E_{l} $$ このとき\(w_{l}\) は\(l\) 層の損失の重みを表します。


最適化

コンテンツ損失とスタイル損失から合計の損失を求めます。合計損失は\(\alpha\) と\(\beta\) をそれぞれコンテンツ損失とスタイル損失の重みとして $$ \mathcal{L}_{total}(\vec{p}, \vec{a}, \vec{x}) = \alpha\mathcal{L}_{content}(\vec{p}, \vec{x}) + \beta\mathcal{L}_{style}(\vec{a}, \vec{x}) $$ この損失を最小化する形で生成画像の最適化を行っていきます。ですので最適化には\(\frac{\partial L_{totla}}{\partial \vec{x}}\) を用いることになります。 論文中ではL-BFGSで最も良い結果になったと記述されていましたが、今回の実装ではAdam を使って最適化を行いました。 この最適化によって、コンテンツ画像をスタイル画像に合わせて画風変換した新たな画像が生成されます。


実装

実装にはTensorFlowを用いました。また実装に際してTensorFlowのチュートリアル[4]を参考にしました。


まずは必要なライブラリをインストールします。

import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl
%matplotlib inline
mpl.rcParams['figure.figsize'] = (12,12)
mpl.rcParams['axes.grid'] = False
import time
import IPython.display as display
import PIL.Image

import tensorflow as tf
from keras.preprocessing import image


画像を読み込んで配列に変換する関数、テンソルを画像に変換する関数を定義します。

# 画像を読み込み配列に変換し正規化する
def load_image(input_path, size):
    image = tf.keras.preprocessing.image.load_img(input_path, target_size=size)
    image = tf.keras.preprocessing.image.img_to_array(image)
    image = np.expand_dims(image, axis=0)
    image /= 255
    return image


# テンソルを画像に戻す
def tensor_to_image(tensor):
    tensor = tensor.numpy()
    tensor *= 255
    tensor = np.array(tensor, dtype=np.uint8)
    if np.ndim(tensor)>3:
        assert tensor.shape[0] == 1
        tensor = tensor[0]
    return PIL.Image.fromarray(tensor)


このクラスは配列に変換された画像を受け取り、VGG19の各層からの出力を返します。この時点でスタイルの表現に使われる特徴マップはグラム行列に変換されます。

class StyleContentModel():
    def __init__(self):
        # VGG19のどの層の出力を使うか指定する
        self.style_layers = ['block1_conv1', 'block2_conv1', 'block3_conv1', 'block4_conv1', 'block5_conv1']
        self.content_layers = ['block5_conv2']

        self.num_style_layers = len(self.style_layers)        
        self.vgg = self.get_vgg_model()
        self.vgg.trainable = False

    def __call__(self, inputs):
        preprocessed_input = tf.keras.applications.vgg19.preprocess_input(inputs * 255)
        vgg_outputs = self.vgg(preprocessed_input)

        style_outputs, content_outputs = (vgg_outputs[:self.num_style_layers], vgg_outputs[self.num_style_layers:])
        style_outputs = [self.gram_matrix(style_output) for style_output in style_outputs]

        style_dict = {style_name:value for style_name, value in zip(self.style_layers, style_outputs)}
        content_dict = {content_name:value  for content_name, value in zip(self.content_layers, content_outputs)}

        return {'style':style_dict, 'content':content_dict}

    # Keras API を利用してVGG19を取得する  
    def get_vgg_model(self):
        vgg = tf.keras.applications.vgg19.VGG19(include_top=False, weights='imagenet')
        vgg.trainable = False

        outputs = [vgg.get_layer(name).output for name in (self.style_layers + self.content_layers)]
        model = tf.keras.Model(vgg.input, outputs)

        return model

    # グラム行列を計算する
    def gram_matrix(self, input_tensor):
        result = tf.linalg.einsum('bijc,bijd->bcd', input_tensor, input_tensor)
        input_shape = tf.shape(input_tensor)
        num_locations = tf.cast(input_shape[1] * input_shape[2], tf.float32)
        return result / num_locations


そして合計損失を計算する関数、計算した損失から勾配を計算する関数を定義します。

# 合計損失を計算する
def compute_loss(model, base_image, style_targets, content_targets, style_weight, content_weight):
    model_outputs = model(base_image)
    style_outputs = model_outputs['style']
    content_outputs = model_outputs['content']

    style_loss = tf.add_n([tf.reduce_mean((style_outputs[name]-style_targets[name])**2) for name in style_outputs.keys()])
    style_loss *= style_weight / len(style_outputs)

    content_loss = tf.add_n([tf.reduce_mean((content_outputs[name]-content_targets[name])**2) for name in content_outputs.keys()])
    content_loss *= content_weight / len(content_outputs)

    loss = style_loss + content_loss
    return loss, style_loss, content_loss

# 損失を元に勾配を計算する
@tf.function()
def compute_grads(params):
    with tf.GradientTape() as tape:
        all_loss = compute_loss(**params)

    grads = tape.gradient(all_loss[0], params['base_image'])
    return grads, all_loss


最後に画像の生成を行う関数を定義していきます。
生成画像のベースとなる画像にはノイズ画像を指定しています。ベース画像にコンテンツ画像やスタイル画像を指定するとまた違った結果が得られます。

def run_style_transfer(style_path, content_path, num_iteration, style_weight, content_weight, display_interval):
    size = image.load_img(content_path).size[::-1]
    noise_image = np.random.uniform(-20, 20, (1, size[0], size[1], 3)).astype(np.float32) / 255
    content_image = load_image(content_path, size)
    style_image = load_image(style_path, size)

    model = StyleContentModel()
    style_targets = model(style_image)['style']
    content_targets = model(content_image)['content']

    # 生成画像のベースとしてノイズ画像を使う
    # ベースにはコンテンツ画像またはスタイル画像を用いることもできる
    base_image = tf.Variable(noise_image)

    opt = tf.optimizers.Adam(learning_rate=0.02, beta_1=0.99, epsilon=1e-1)

    params = {
        'model': model,
        'base_image': base_image,
        'style_targets': style_targets,
        'content_targets': content_targets,
        'style_weight': style_weight,
        'content_weight': content_weight
    }

    best_loss = float('inf')
    best_image = None

    start = time.time()
    for i in range(num_iteration):
        grads, all_loss = compute_grads(params)
        loss, style_loss, content_loss = all_loss

        opt.apply_gradients([(grads, base_image)])
        clipped_image = tf.clip_by_value(base_image, clip_value_min=0., clip_value_max=255.0)
        base_image.assign(clipped_image)

        # 損失が減らなくなったら最適化を終了する        
        if loss < best_loss:
            best_loss = loss
            best_image = base_image
        elif loss > best_loss:
            tensor_to_image(base_image).save('output_' + str(i+1) + '.jpg')
            break

        if (i + 1) % display_interval == 0:
            display.clear_output(wait=True)
            display.display(tensor_to_image(base_image))
            tensor_to_image(base_image).save('output_' + str(i+1) + '.jpg')
            print(f'Train step: {i+1}')
            print('Total loss: {:.4e}, Style loss: {:.4e}, Content loss: {:.4e}'.format(loss, style_loss, content_loss))

    print('Total time: {:.4f}s'.format(time.time() - start))
    display.clear_output(wait=True)
    display.display(tensor_to_image(base_image))

    return best_image


では実際に画風変換を行ってみましょう。styleweightとcontentweightはそれぞれスタイル損失とコンテンツ損失の重みを表します。

style_path = '../input/neural-image-transfer/StarryNight.jpg'
content_path = '../input/neural-image-transfer/FlindersStStation.jpg'
num_iteration = 5000
style_weight = 1e-2
content_weight = 1e4
display_interval = 100

best_image = run_style_transfer(style_path, content_path, num_iteration, style_weight, content_weight, display_interval)


結果

コンテンツ画像とスタイル画像はこれらの画像を使いました。


コンテンツ画像

Flinders_Street_Station_in_Melbourne

引用元: https://commons.wikimedia.org/wiki/File:Flinders_Street_Station_3.jpg


スタイル画像

The_Starry_Night_art_of_Gogh

引用元: https://commons.wikimedia.org/wiki/File:Van_Gogh_-_Starry_Night_-_Google_Art_Project.jpg


上記のコードを走らせて生成した画像がこちらになります。 スタイル損失とコンテンツ損失の重みを変えて4種類の画像を生成しました。

  • \(\alpha = 10, \beta = 10^{-2}\) generated_image_with_content_loss_weight_1e1_style_loss_weight_1e-2



  • \(\alpha = 10^{2}, \beta = 10^{-2}\) generated_image_with_content_loss_weight_1e2_style_loss_weight_1e-2



  • \(\alpha = 10^{3}, \beta = 10^{-2}\) generated_image_with_content_loss_weight_1e3_style_loss_weight_1e-2



  • \(\alpha = 10^{4}, \beta = 10^{-2}\) generated_image_with_content_loss_weight_1e4_style_loss_weight_1e-2



結果からわかるように \(\frac{\alpha}{\beta}\) が大きくなればなるほどコンテンツ画像がはっきりと生成画像に反映されていることがわかります。これは\(\frac{\alpha}{\beta}\)が大きいとコンテンツ損失に対してモデルが敏感になるからです。 逆に\(\alpha = 10, \beta = 10^{-2}\) の場合はスタイル損失に敏感になりすぎて、生成画像からコンテンツを見つけることができなくなっています。


まとめ

今回は画風変換の基礎となる論文の解説と実装を行いました。ベース画像やパラメーター、VGGのどの層の出力を使うかなどによって違った結果が得られるので色々といじってみるのも面白いかもしれません。またこの論文以降にも画風変換の研究が進められていますので、それらを試す助けになればと思います。


参考文献

[1] Image Style Transfer Using Convolutional Neural Networks
[2] Very Deep Convolutional Networks For Large-scale Image Recognition
[3] Texture Synthesis Using Convolutional Neural Networks
[4] TensorFlow Core: Neural style transfer


Twitter・Facebookで定期的に情報発信しています!

はじめに

前回の「画像セグメンテーションのためのU-net概要紹介」では画像のクラス分類のタスクを、画像のSegmentationのタスクにどう発展させるかを解説し、SegmentationのネットワークであるU-netの理論ついて簡単に解説しました。
今回はTensorFlowのSegmentationのチュートリアルを行いながら、実際にU-netを学習させてみたいと思います。
尚、本記事ではTensorflowの詳しい解説は行いません。
参考 : https://www.tensorflow.org/tutorials/images/segmentation

Segmentationとは

ある物体が画像内に含まれている時、画像のどこにあるのかを推定するタスクのことです。
言い換えると「画像のピクセルがそれぞれ何かを推定する」タスクのことです。
今回はOxford-IIIT Pet Datasetというデータセットを用いて学習を行い、ピクセルごとに以下のようなクラス分けを行います。

  • Class 0 : 動物のピクセル
  • Class 1 : 動物とその他の境界線のピクセル
  • Class 2 : その他のピクセル


目標は以下のような出力を得ることです。(左:入力画像 右:出力画像)
Alt text


ライブラリのインポート

import tensorflow as tf
import sys
from IPython.display import display
from IPython.display import HTML
from PIL import Image
# sys.modules['Image'] = Image 
from __future__ import absolute_import , division, print_function, unicode_literals
from tensorflow_examples.models.pix2pix import pix2pix

import tensorflow_datasets as tfds
tfds.disable_progress_bar()

from IPython.display import clear_output
import matplotlib.pyplot as plt


データの読み込み&可視化

dataset, info = tfds.load('oxford_iiit_pet:3.0.0', with_info=True)

def normalize(input_image,input_mask):
    input_image = tf.cast(input_image, tf.float32) / 255
    input_mask -= 1
    return input_image,input_mask

@tf.function
def load_image_train(datapoint):
    input_image = tf.image.resize(datapoint['image'],(128,128))
    input_mask = tf.image.resize(datapoint['segmentation_mask'],(128,128))

    if tf.random.uniform(()) > 0.5:
        input_image = tf.image.flip_left_right(input_image)
        input_mask = tf.image.flip_left_right(input_mask)
    input_image,input_mask = normalize(input_image, input_mask)
    return input_image, input_mask

def load_image_test(datapoint):
    input_image = tf.image.resize(datapoint['image'], (128, 128))
    input_mask = tf.image.resize(datapoint['segmentation_mask'], (128, 128))
    input_image, input_mask = normalize(input_image, input_mask)
    return input_image, input_mask

TRAIN_LENGTH = info.splits['train'].num_examples
BATCH_SIZE = 64
BUFFER_SIZE = 1000
STEPS_PER_EPOCH = TRAIN_LENGTH // BATCH_SIZE

train = dataset['train'].map(load_image_train, num_parallel_calls=tf.data.experimental.AUTOTUNE)
test = dataset['test'].map(load_image_test)

train_dataset = train.cache().shuffle(BUFFER_SIZE).batch(BATCH_SIZE).repeat()
train_dataset = train_dataset.prefetch(buffer_size=tf.data.experimental.AUTOTUNE)
test_dataset = test.batch(BATCH_SIZE)

def display(display_list):
    plt.figure(figsize=(15, 15))

    title = ['Input Image', 'True Mask', 'Predicted Mask']

    for i in range(len(display_list)):
        plt.subplot(1, len(display_list), i+1)
        plt.title(title[i])
        plt.imshow(tf.keras.preprocessing.image.array_to_img(display_list[i]))
        plt.axis('off')
    plt.show()

for image, mask in train.take(88):
    sample_image, sample_mask = image, mask
display([sample_image, sample_mask])

Alt text


U-netの構築

U-netのencorder部分には今回のデータセットとは別のデータセットを学習したのMovileNetを用います。
MovileNetの重みは学習で更新しないように更新しておきます。
このようにすることU-netの精度を上げることができます。(転移学習)
decorder部分には未学習のpix2pixを用います。
pix2pixの重みは学習により更新されます。

OUTPUT_CHANNELS = 3

# encorder部分には学習済みのMovileNet
base_model = tf.keras.applications.MobileNetV2(input_shape=[128,128,3],include_top=False)
layer_names = [
    'block_1_expand_relu',
    'block_3_expand_relu',
    'block_6_expand_relu',
    'block_13_expand_relu',
    'block_16_project'
]
layers = [base_model.get_layer(name).output for name in layer_names]
down_stack = tf.keras.Model(inputs=base_model.input, outputs=layers)
# MovileNetの重みは固定
down_stack.trainable = False

# decorder部分にはpix2pixを用いる
up_stack = [
    pix2pix.upsample(512, 3),  # 4x4 -> 8x8
    pix2pix.upsample(256, 3),  # 8x8 -> 16x16
    pix2pix.upsample(128, 3),  # 16x16 -> 32x32
    pix2pix.upsample(64, 3),   # 32x32 -> 64x64
]

def unet_model(output_channels):

    # This is the last layer of the model
    last = tf.keras.layers.Conv2DTranspose(
        output_channels, 3, strides=2,
        padding='same', activation='softmax')  #64x64 -> 128x128

    inputs = tf.keras.layers.Input(shape=[128, 128, 3])
    x = inputs

    # Downsampling through the model
    skips = down_stack(x)
    x = skips[-1]
    skips = reversed(skips[:-1])

    # Upsampling and establishing the skip connections
    for up, skip in zip(up_stack, skips):
        x = up(x)
        concat = tf.keras.layers.Concatenate()
        x = concat([x, skip])

    x = last(x)

    return tf.keras.Model(inputs=inputs, outputs=x)


U-netの学習

実際にU-netが学習していく様子を眺めてみましょう。
epochが進むにつれ、正しく予測できているのがわかります。

class DisplayCallback(tf.keras.callbacks.Callback):
    def on_epoch_end(self, epoch,logs=None):
        clear_output(wait=True)
        ims.append([sample_image, sample_mask,
                 create_mask(model.predict(sample_image[tf.newaxis, ...]))])
        print ('\nSample Prediction after epoch {}\n'.format(epoch+1))

def create_mask(pred_mask):
    pred_mask = tf.argmax(pred_mask, axis=-1)
    pred_mask = pred_mask[..., tf.newaxis]
    return pred_mask[0]

def show_predictions(dataset=None, num=1):
    if dataset:
        for image, mask in dataset.take(num):
            pred_mask = model.predict(image)
            display([image[0], mask[0], create_mask(pred_mask)])
            ims.append([image[0], mask[0], create_mask(pred_mask)])
    else:
        display([sample_image, sample_mask,
                 create_mask(model.predict(sample_image[tf.newaxis, ...]))])
        ims.append([sample_image, sample_mask,
                 create_mask(model.predict(sample_image[tf.newaxis, ...]))])

ims = []
EPOCHS = 100
VAL_SUBSPLITS = 5
VALIDATION_STEPS = info.splits['test'].num_examples//BATCH_SIZE//VAL_SUBSPLITS

model = unet_model(OUTPUT_CHANNELS)
model.compile(optimizer='adam', loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])
model_history = model.fit(train_dataset, epochs=EPOCHS,
                          steps_per_epoch=STEPS_PER_EPOCH,
                          validation_steps=VALIDATION_STEPS,
                          validation_data=test_dataset,
                          callbacks=[DisplayCallback()])

import matplotlib.animation as animation

def make_animation(ims):
    %matplotlib nbagg
    fig, (ax0, ax1, ax2) = plt.subplots(1,3,figsize=(14.0, 8.0))
    ax0.axis('off')
    ax1.axis('off')
    ax2.axis('off')
    ax0.set_title('Input Image')
    ax1.set_title('True Mask')
    ax2.set_title('Predicted Mask')

    ims2 = []
    for epoch,im in enumerate(ims):   

        im0, = [ax0.imshow(tf.keras.preprocessing.image.array_to_img(im[0]))]
        im1, = [ax1.imshow(tf.keras.preprocessing.image.array_to_img(im[1]))]
        im2, = [ax2.imshow(tf.keras.preprocessing.image.array_to_img(im[2]))]
        ims2.append([im0,im1,im2])
    ani = animation.ArtistAnimation(fig, ims2, interval=50, repeat_delay=1000)
    return ani

学習の様子

学習の様子を見てみます。epochが進むごとに精度が増していることがわかります。

Alt text

ani1 = make_animation(ims)
HTML(ani1.to_jshtml())


おまけ

U-netにはskip-conectionという手法が使われています。
encorder部分で畳み込みをして失ってしまった画像内の位置情報を保持する役割を持ちます。
U-netにskip-conectionが無い場合も比較してみましょう。

up_stack = [
    pix2pix.upsample(512, 3),  # 4x4 -> 8x8
    pix2pix.upsample(256, 3),  # 8x8 -> 16x16
    pix2pix.upsample(128, 3),  # 16x16 -> 32x32
    pix2pix.upsample(64, 3),   # 32x32 -> 64x64
]

def unet_model_no_sc(output_channels):

    # This is the last layer of the model
    last = tf.keras.layers.Conv2DTranspose(
        output_channels, 3, strides=2,
        padding='same', activation='softmax')  #64x64 -> 128x128

    inputs = tf.keras.layers.Input(shape=[128, 128, 3])
    x = inputs
    x = down_stack(x)
    x = x[-1]
    for up in up_stack:
        x = up(x)
    x = last(x)

    return tf.keras.Model(inputs=inputs, outputs=x)

ims = []
EPOCHS = 100
VAL_SUBSPLITS = 5
VALIDATION_STEPS = info.splits['test'].num_examples//BATCH_SIZE//VAL_SUBSPLITS

model = unet_model_no_sc(OUTPUT_CHANNELS)
model.compile(optimizer='adam', loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])
model_history = model.fit(train_dataset, epochs=EPOCHS,
                          steps_per_epoch=STEPS_PER_EPOCH,
                          validation_steps=VALIDATION_STEPS,
                          validation_data=test_dataset,
                          callbacks=[DisplayCallback()])


学習の様子

こちらも学習の経過を眺めてみます。
skip-conectionがある場合と比べ、学習が遅いばかりか精度が悪いことがわかります。

Alt text


Twitter・Facebookで定期的に情報発信しています!

はじめに

こんにちは、システム部の譚です。
Google Colablatoryで、手書きの数字(0, 1, 2 など)から構成されているMNISTデータセットを使い、分類問題のニューラルネットワークを構築してみました。

目次

はじめに
モデルを構築する
おわりに

モデルを構築する

手順

1.TensorFlowのモデルを構築し訓練するためのハイレベルのAPIである tf.kerasを使用する。

from __future__ import absolute_import, division, print_function, unicode_literals

# TensorFlow and tf.keras
import tensorflow as tf
from tensorflow import keras

# Helper libraries
import numpy as np
import matplotlib.pyplot as plt

print(tf.__version__)

2.訓練データとテストデータをダウンロードする。

fashion_mnist = keras.datasets.fashion_mnist

(train_images, train_labels), (test_images, test_labels) = fashion_mnist.load_data()

3.後ほど画像を出力するときのために、クラス名を保存しておく。

mnist = keras.datasets.mnist

(train_images, train_labels), (test_images, test_labels) = mnist.load_data()

4.label nameをつける。

class_names = ['0', '1', '2', '3', '4','5', '6', '7', '8', '9']

5.データの前処理をする。
最初の画像を調べてみればわかるように、ピクセルの値は0から255の間の数値です。
ニューラルネットワークにデータを投入する前に、これらの値を0から1までの範囲にスケールするので、画素の値を255で割ります。

train_images = train_images / 255.0

test_images = test_images / 255.0

6.訓練用データセットの最初の25枚の画像を、クラス名付きで表示する。

plt.figure(figsize=(10,10))
for i in range(25):
    plt.subplot(5,5,i+1)
    plt.xticks([])
    plt.yticks([])
    plt.grid(False)
    plt.imshow(train_images[i], cmap=plt.cm.binary)
    plt.xlabel(class_names[train_labels[i]])
plt.show()

7.モデルを構築する。 

model = keras.Sequential([
    # データのフォーマット変換
    keras.layers.Flatten(input_shape=(28, 28)),
    # 128個のノードのDense層
    keras.layers.Dense(128, activation=tf.nn.relu),
    # 10ノードのsoftmax層
    keras.layers.Dense(10, activation=tf.nn.softmax)
])

8.モデルのコンパイルをする。

model.compile(
# モデルが見ているデータと、損失関数の値から、どのようにモデルを更新するかを決定
optimizer=tf.keras.optimizers.Adam(),

# 訓練中にモデルがどれくらい正確かを測定します
loss='sparse_categorical_crossentropy',

# 訓練とテストのステップを監視するのに使用します
metrics=['accuracy'])

9.モデルの訓練

モデルは、画像とラベルの対応関係を学習します
model.fit(train_images, train_labels, epochs=5)
Epoch 1/5
60000/60000 [==============================] - 7s 111us/sample - loss: 0.2607 - acc: 0.9261
Epoch 2/5
60000/60000 [==============================] - 6s 105us/sample - loss: 0.1127 - acc: 0.9668
Epoch 3/5
60000/60000 [==============================] - 6s 103us/sample - loss: 0.0763 - acc: 0.9769
Epoch 4/5
60000/60000 [==============================] - 6s 103us/sample - loss: 0.0557 - acc: 0.9832
Epoch 5/5
60000/60000 [==============================] - 6s 104us/sample - loss: 0.0448 - acc: 0.9864

モデルの訓練の進行とともに、損失値と正解率が表示されます。このモデルの場合、訓練用データでは0.98(すなわち98%)の正解率に達します。

10.正解率を評価する。 テスト用データセットに対するモデルの性能を比較します。

test_loss, test_acc = model.evaluate(test_images, test_labels)

print('Test accuracy:', test_acc)
10000/10000 [==============================] - 0s 41us/sample - loss: 0.0717 - acc: 0.9777
Test accuracy: 0.9777

ご覧の通り、テスト用データの正解率は訓練用より少し低い結果でした。これは過学習(over fitting)の一例です。

11.予測する。

predictions = model.predict(test_images)
np.argmax(predictions[0])

今回の予測は合っていることが確認できました。

12.10チャンネル全てをグラフ化する。

def plot_image(i, predictions_array, true_label, img):
  predictions_array, true_label, img = predictions_array[i], true_label[i], img[i]
  plt.grid(False)
  plt.xticks([])
  plt.yticks([])

  plt.imshow(img, cmap=plt.cm.binary)

  predicted_label = np.argmax(predictions_array)
  if predicted_label == true_label:
    color = 'blue'
  else:
    color = 'red'

  plt.xlabel("{} {:2.0f}% ({})".format(class_names[predicted_label],
                                100*np.max(predictions_array),
                                class_names[true_label]),
                                color=color)

def plot_value_array(i, predictions_array, true_label):
  predictions_array, true_label = predictions_array[i], true_label[i]
  plt.grid(False)
  plt.xticks([])
  plt.yticks([])
  thisplot = plt.bar(range(10), predictions_array, color="#777777")
  plt.ylim([0, 1])
  predicted_label = np.argmax(predictions_array)

thisplot[predicted_label].set_color('red')
  thisplot[true_label].set_color('blue')

0番目の画像と、予測、予測配列を見てみましょう。

i = 0
plt.figure(figsize=(6,3))
plt.subplot(1,2,1)
plot_image(i, predictions, test_labels, test_images)
plt.subplot(1,2,2)
plot_value_array(i, predictions,  test_labels)
plt.show()

予測確率は100%なので、すべて青で表示されました。
やはり数字に自信満々ですね。(チュートリアルのほうは、Fashion MNISTを使って若干赤かグレーも出てきました。)

13.予測の中のいくつかの画像を、予測値と共に表示する。

# X個のテスト画像、予測されたラベル、正解ラベルを表示します。
# 正しい予測は青で、間違った予測は赤で表示しています。

num_rows = 5
num_cols = 3
num_images = num_rows*num_cols
plt.figure(figsize=(2*2*num_cols, 2*num_rows))
for i in range(num_images): 
    plt.subplot(num_rows, 2*num_cols, 2*i+1)
    plot_image(i, predictions, test_labels, test_images)
    plt.subplot(num_rows, 2*num_cols, 2*i+2)
    plot_value_array(i, predictions, test_labels)
plt.show()

図のように、三行目の5の予測率が82%だった以外は、100%です。(これは手書きが悪かったですね)

おわりに

今回は試すことが多かったですが、これから学ぶべきことに気づけました。
例えば...

  • モデルのレイヤーとは。
  • モデルをコンパイルする際に、損失関数、オプティマイザ、メトリクスなどはモデルにどのような影響するのか。
  • ヘルパーライブラリnumpy、matplotlib.pyplotなどの具体的な使い方など。
    実際に扱ってみることで、機械学習に対する怖さはなくなり、今後も様々なモデルで実装していきたいと思います。

参考

Google Colablatory link 
TensorFlow はじめてのニューラルネットワーク:分類問題の初歩

アクセルユニバースの根岸です。

突然ですが、お寿司は食べますか?日本に住んでいて食べたことない人はほとんどいないと思います。ということは何を食べているのか判断できるのは当然の教養ですよね。一方で私のようなお寿司に疎い人もいるわけで、なんとなく美味しいで終わってしまうのはもったいない。そんな時にこそ機械学習を使って解決してみましょう。

ということで、本記事ではネットからお寿司の写真を入手して機械学習(画像認識)を用いてお寿司を判別するためのモデル構築を行うまでの一連の流れを記していこうと思います。コードも合わせて載せていくのでpythonの基本文法とAPIについて少し知っておくと理解しやすいかもしれません。

目次

  • 1.お寿司の画像データをネットから収集する
  • 2.画像を振り分ける
  • 3.画像処理と機械学習で判別する
  • 終わりに一連の流れ

1.お寿司の画像データをネットから収集する

まずは機械学習に必要なデータの収集から始めていきます。判別対象はお寿司なのでお寿司の画像データを収集する必要があります。方法としては

  • 画像検索エンジン(googleなど)から一つずつ引っ張る
  • 画像共有サイト(Instagram,フォト蔵など)で公開されているWeb APIから抽出する
  • kaggle等からデータセットを取得する
  • 自分で作る

があります。今回の画像はWebAPIが充実している「フォト蔵」から抽出してくることにしましょう。

そもそもWebAPIとは、あるサイトが備えている機能を外部から利用できるように公開しているものです。WebAPIでは基本的にHTTP通信が利用されており、下の図のようにユーザがAPIを提供しているサーバに対して、任意のHTTPリクエストを送信することでサーバーからXMLやJSON形式のファイルが返ってくるという仕組みになっています。こうしてAPIから任意のデータを取得する行為を俗に「APIを叩く(物理的にでは有りません)」と言います。それぞれの形式の中身もフォーマットが決まっているので、ファイルのどの部分から情報を取ってくればよいか分かりやすくなっています。(下記JSONレスポンス参照)

WebAPI
HTTPリクエストは基本的に提供元が併せて公開してくれているのでそれに従って作りましょう。
それでは早速今回利用する「フォト蔵」のWebAPIを確認していきましょう。フォト蔵の検索APIは以下のようになっています。
http://photozou.jp/basic/apimethodsearch_public
フォト蔵検索API.png

加えて以下のパラメータを指定することで、柔軟な検索が可能になっています。keywordをクエリとして設定することで任意のキーワードに沿ったデータのみ取得が可能になりそうですね。

フォト蔵検索API_パラメータ

試しにkeywordを"中トロ"としてクエリを設定してブラウザのURL欄に入力してみると以下のような表示がされます。
https://api.photozou.jp/rest/search_public.xml?keyword=中トロ

中トロxml
XMLファイルがブラウザ上で表示されました。

中身を詳しく見ていくとoriginal_widthやoriginal_heightがありますよね。これは元画像の解像度なので、写真の投稿者によって異なります。画像認識する上で画像サイズが違うと前処理が大変なので、サムネイルに使用している画像サイズ(120×120)の画像URLであるthumbnail_image_urlを使うことにしましょう。

このURLをファイルの中から全て抜き出してそれぞれダウンロードしてくれば欲しい画像が抜き出せるはずです。これらをすべて入手するのにurllibライブラリを使って画像を収集していきます。

それでは画像取得までの手順を確認してみましょう。

  1. フォト蔵のAPIを利用して(今回は)JSONファイルを取得する。
  2. JSONファイル内のthumbnail_image_urlから画像のURLをすべて取得する。
  3. 取得したURLの画像を一つずつダウンロードして保存する

JSONファイルの構成は以下のようになっています。

JSONレスポンス
{
"info": {
"photo_num": ###,
"photo": [
{
"photo_id": ###,   ←画像のナンバリングとして利用
"user_id": ###,
"album_id": ###,
"photo_title": "タイトル",
"favorite_num": ###,
"comment_num": ###,
"view_num": ###,
"copyright": "normal/creativecommons",
"copyright_commercial": "yes/no"
"copyright_modifications": "yes/no/share"
"original_height": ##,
"original_width": ###,
"geo": {
"latitude": ###,
"longitude": ###
},
"date": "YYYY-MM-DD",
"regist_time": "YYYY-MM-DDThh:mm:ss+09:00",
"url": "URL",
"image_url": "URL",
"original_image_url": "URL",
"thumbnail_image_url": "URL",   ←これを抜き出す
"large_tag": "<a href=\"...\">...</a>",
"medium_tag": "<a href=\"...\">...</a>"
},
...
]
}
}

それでは実際にコードを書いていきます。
まずは必要なモジュールをimportしてきます。

import urllib.request as req 
import urllib.parse as parse
import os, re, time
import json
#APIのメソッドを指定
photozou_api='https://api.photozou.jp/rest/search_public.json'
sushi_dir='./image/sushi'
  • urllib.request:URLの読み込み、ダウンロード
  • urllib.parse:相対URLから絶対URLを取得
  • os, re:ディレクトリー作成
  • time:ソースコードの一時停止(リソースを逼迫させないためにAPI叩く間隔を空ける)
  • json:JSONファイル読み込み

API用のHTTPリクエストとダウンロードした画像を保存する自身のディレクトリーを指定しておきます。
次に画像を検索するための関数search_photoを作っていきます。上記APIのパラメータ表記を参考にkeyword, offset,limitを指定することでAPIのクエリを組み立てて、それに応じた指定のJSONファイルをダウンロードして返す関数になっています。

この時に画像保存のためのフォルダを一緒に作っています。limitの値を変更すれば最大で引っ張ってこれる画像URLの数を調整できます。ここでは上記WebAPIのパラメータに書いてある初期値の100を使います。

def search_photo(keyword, offset=0, limit=100): #画像の検索及びJSONファイルを返す関数
#APIのクエリ組み立て
keyword_encoding=parse.quote_plus(keyword) #HTMLフォーム値の空白をプラス記号に置き換え
query="keyword={0}&offset={1}&limit={2}".format(keyword_encoding,offset,limit)
url=photozou_api+'?'+query
#クエリとキャッシュを保存しておく
if not os.path.exists(sushi_dir):
os.makedirs(sushi_dir)
sushi=sushi_dir+"/"+re.sub(r'[^a-zA-Z0-9\%\#]','_',url)
if os.path.exists(sushi):
return json.load(open(sushi, "r", encoding='utf-8'))
print(url)
req.urlretrieve(url,sushi)
time.sleep(1) #逼迫させないために間隔を空ける
return json.load(open(sushi, "r", encoding='utf-8'))

JSONファイルが取得できたらその中からthumbnail_image_urlの部分を抜き出してダウンロードしてみましょう。中にはthumbnail_image_urlを持たないものが混ざっているので、例外として排除しておきます。ファイル名はphoto_idと'_thumb'を用いてjpgで保存します。画像をダウンロードするときにもリソースの逼迫につながらないように配慮して間隔を空けるようにしておきましょう。

def download_single(info, save_dir): #画像のダウンロード 
#画像保存のためのフォルダを作成
if not os.path.exists(save_dir):
os.makedirs(save_dir)
if info is None:
return
if not 'photo' in info['info']:
return
photo_list=info['info']['photo']
for photo in photo_list:
photo_title=photo['photo_title']
photo_id=photo['photo_id']
url=photo['thumbnail_image_url']
path=save_dir+'/'+str(photo_id)+'_thumb.jpg'
if os.path.exists(path):
continue
try:
print('download',photo_id,photo_title)
req.urlretrieve(url,path)
time.sleep(1)#間隔を空ける
except Exception as e:
print('ダウンロードできませんでした url=',url)

それでは上記2つを実行する関数を作って行きます。

実際に何個の画像を取得できるかわからないのでwhileとmax_photoで上限内でありったけの画像をダウンロードしてくることにします。いくつか条件分岐させていますがニッチなカテゴリーを検索するわけじゃないので、記述しなくても問題ないですが念のため。

def download_all(keyword, save_dir, max_photo=1000):
offset=0
limit=100
while True:
info=search_photo(keyword, offset=offset, limit=limit)
#情報が欠損している場合(例外処理)
if (info is None) or (not 'info' in info) or (not 'photo_num' in info['info']):
print('情報が欠損しています')
return
photo_num=info['info']['photo_num']
if photo_num==0:
print('photo_num=0,offset=',offset)
return
#画像が含まれている場合
print('download offset=', offset)
download_single(info, save_dir)
offset+=limit
if offset >= max_photo:
break

モジュールとして使わずに単独で使用する用に以下を記述しておきます。download_allのkeywordを'中トロ'にしていますがここのkeywordを変えることでそれに応じた検索画像をダウンロードしてきます。

if __name__=='__main__':
download_all('中トロ','./image/sushi')

ということでプログラムが完成した(downloader-sushi.pyで保存)のでターミナルから実行してみましょう。

$ python3 downloader-sushi.py

するとフォト蔵にある中トロの画像を最大で1000件ダウンロードします。
中トロ画像たち.png
中トロ以外にも鉄火巻、こはだについて同じ処理をした後、新しいフォルダ"tyutoro","tekkamaki","kohada"を作成します。

中トロ, 鉄火巻, こはだ
これにて学習に必要な材料は揃いましたが・・・

2.画像を振り分ける

画像を振り分ける

ここからが泥臭い作業になります。

中トロと検索すれば全て中トロのお寿司を1貫を画面いっぱいに載せてくれている画像なら良いのですが、残念ながら中には複数の別のお寿司が写ってる、中トロが写っていない、もはや人しかいない、なんて画像がかなり混在しています。ここで手作業で学習に使えそうな画像を抽出するという泥作業が発生します(画像を部分的にアノテーションすれば使えるものもありますが、サイズの調整が大変なので割愛します)。正にデータサイエンティストの醍醐味ですね!

そうして抽出した画像数は以下の通りです。

  • 中トロ 111点
  • 鉄火巻 135点
  • こはだ 66点

計311点
中トロ、鉄火巻、こはだ

圧倒的こはだ不足です。そもそも全体的にサンプルが足りていないですが、見逃してください...。
これらの画像のファイルは"./image"上にそれぞれ作成しました。後は画像処理を行って画像認識のモデルに突っ込んでいきましょう。

3.画像処理と機械学習で判別する

画像処理と機械学習

ここで行うことは3点です

  • 画像を数値データに変換と水増し(画像処理)
  • 画像認識モデルの構築と学習
  • 画像の判別

3-1 画像を数値データに変換と水増し

画像データを学習させるためには数値データへの変換が必須です。変換後はデータサイエンティスト御用達のNumpyの形式に変換させましょう。まずは必要なモジュールをimportしてきます。

from PIL import Image
import os, glob
import numpy as np
  • PIL:画像処理(エンコードに利用)
  • glob:画像のディレクトリパス一覧を取得

分類対象は中トロ、鉄火巻、こはだの3種類なので下記で指定しておきます。

root_dir='./image/'
categories=['tyutoro','tekkamaki','kohada']
nb_classes=len(categories)
image_size=100

次にフォルダ(カテゴリー)ごとに画像データを読み込んでRGB変換、Numpy形式への変換を行う関数を宣言していきます。

この際に画像の水増しも一緒に行っていきます。画像の角度を変えたり反転させることで画像数を増やしていきます。注意すべきことは水増ししたデータが学習データとテストデータの両方に混在してしまうとリークが発生してしまい、精度が異常に高くなってしまう現象が起きるので、水増しは学習データのみ行うようにしましょう。is_trainの真偽で水増しをするかどうかを設定しています。

#画像処理と水増しを行う
X=[]
Y=[]
def padding(cat, fname, is_train):
img=Image.open(fname)
img=img.convert("RGB") #RGB変換
img=img.resize((image_size,image_size)) #画像サイズ変更(100×100)
data=np.array(img) #numpy形式に変換
X.append(data)
Y.append(cat)
#学習データのみ水増しするので学習データではない場合は以下のfor文を実行しない
if not is_train:
return
for angle in range(-20, 20, 10):
#画像の回転
img2=img.rotate(angle)
data=np.asarray(img2)
X.append(data)
Y.append(cat)
#画像の左右反転
img3=img.transpose(Image.FLIP_LEFT_RIGHT)
data=np.asarray(img3)
X.append(data)
Y.append(cat)

#カテゴリーごとの処理
def make_train(files, is_train):
global X,Y
X=[]
Y=[]
for cat, fname in files:
padding(cat, fname, is_train)
return np.array(X), np.array(Y)

加えてディレクトリーごとに分けられているファイルを収集してallfilesに統合しています。

#ディレクトリーごとにファイルを収集する
files_all=[]
for idx, cat in enumerate(categories):
image_dir=root_dir+'/'+cat
files=glob.glob(image_dir+'/*.jpg')
for f in files:
files_all.append((idx, f))

加えてディレクトリーごとに分けられているファイルを収集してallfilesに統合しています。

最後に学習データとテストデータに分けてモデルに入れるデータが整います。

import random, math
#シャッフル
random.shuffle(files_all)
MATH=math.floor(len(files_all)*0.6)
train=files_all[0:MATH]
test=files_all[MATH:]

後はモデルを作って学習させて完成ですね。

3-2 画像認識モデルの構築と学習

それでは機械学習のフェーズに入っていきましょう。

使用するモデルはCNN(畳み込みニューラルネットワーク)です。ここではTensorFlowとKerasを組み合わせてCNNを組み立てて行きます。モデルの概要は以下のようにしています。
CNN概要
CNNの具体的な中身が以下の通り

_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
conv2d_1 (Conv2D) (None, 100, 100, 64) 1792
_________________________________________________________________
activation_1 (Activation) (None, 100, 100, 64) 0
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 50, 50, 64) 0
_________________________________________________________________
dropout_1 (Dropout) (None, 50, 50, 64) 0
_________________________________________________________________
conv2d_2 (Conv2D) (None, 50, 50, 128) 73856
_________________________________________________________________
activation_2 (Activation) (None, 50, 50, 128) 0
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 25, 25, 128) 0
_________________________________________________________________
dropout_2 (Dropout) (None, 25, 25, 128) 0
_________________________________________________________________
conv2d_3 (Conv2D) (None, 23, 23, 128) 147584
_________________________________________________________________
activation_3 (Activation) (None, 23, 23, 128) 0
_________________________________________________________________
max_pooling2d_3 (MaxPooling2 (None, 11, 11, 128) 0
_________________________________________________________________
dropout_3 (Dropout) (None, 11, 11, 128) 0
_________________________________________________________________
flatten_1 (Flatten) (None, 15488) 0
_________________________________________________________________
dense_1 (Dense) (None, 1028) 15922692
_________________________________________________________________
activation_4 (Activation) (None, 1028) 0
_________________________________________________________________
dropout_4 (Dropout) (None, 1028) 0
_________________________________________________________________
dense_2 (Dense) (None, 3) 3087
_________________________________________________________________
activation_5 (Activation) (None, 3) 0
=================================================================
Total params: 16,149,011
Trainable params: 16,149,011
Non-trainable params: 0
_________________________________________________________________

加えてディレクトリーごとに分けられているファイルを収集してallfilesに統合しています。

それでは上記を元にコードを書いていきます。

# データをロード
def main():
X_train, y_train=make_train(train,True)
X_test, y_test=make_train(test,False)
# データを正規化する
X_train=X_train.astype("float") / 256
X_test=X_test.astype("float") / 256
y_train=np_utils.to_categorical(y_train, nb_classes)
y_test=np_utils.to_categorical(y_test, nb_classes)
# モデルを学習し評価する
model=model_train(X_train, y_train)
model_eval(model, X_test, y_test)
# モデルを構築
def build_model(in_shape):
model=Sequential()
model.add(Convolution2D(64 , 3, 3, border_mode='same',input_shape=in_shape))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))
model.add(Convolution2D(128, 3, 3, border_mode='same'))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))
model.add(Convolution2D(128, 3, 3))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))
model.add(Flatten())
model.add(Dense(1028))
model.add(Activation('relu'))
model.add(Dropout(0.5))
model.add(Dense(nb_classes))
model.add(Activation('softmax'))
model.compile(loss='binary_crossentropy',optimizer='rmsprop',metrics=['accuracy'])
model.summary()
return model
# 学習
def model_train(X, y):
model=build_model(X.shape[1:])
model.fit(X, y, batch_size=30, nb_epoch=30)
return model
# 評価
def model_eval(model, X, y):
score=model.evaluate(X, y)
print('loss=', score[0])
print('accuracy=', score[1])
if __name__=="__main__":
main()

3-3 画像を判別する

ということでプログラムが完成した(sushi_cnn.pyで保存)のでターミナルから実行してみましょう。

$ python3 sushi_cnn.py

計算に結構な時間かかりますが、最終的に以下のような正答率が出てきました。

loss= 1.2733278312683105
accuracy= 0.8826666955947876

およそ88%といったところでしょうか。

他にも画像サイズを変えてみたり、層を変えてみたりしましたが概ね90%前後を漂う感じでした。3種類と比較的少ない種類を判定しましたが、ほどほどに上手くできてるかと思います。

終わりに

本記事では、Web APIによる画像入手に始まり、前処理(データの振り分けや選別)、CNNの学習による画像認識まで行ってみました。もちろん学習させたお寿司の種類が少なかったので実際に使えるわけではないですが、"データが集まって前処理をしっかりすれば"実用的なものになっていくのではないでしょうか。

○環境
OS:Mac Catalina 10.15.1
CPU:3.1 GHz デュアルコアIntel Core i7
RAM:16 GB 1867MHz DDR3
python:3.7.5
各種ライブラリは投稿時点で最新のものを使用しています


Twitter・Facebookで定期的に情報発信しています!

U-netとは

U-netはFCN(fully convolution network)の1つであり、画像のセグメンテーション(物体がどこにあるか)を推定するためのネットワークです。

生物医科学(biomedical)の画像のセグメンテーションを行うために2015年に発表されました。

(論文URL : https://arxiv.org/abs/1505.04597)

この記事では、まずU-netの中で行われている処理についてを1〜4章でまとめ、それらの組み合わせであるU-netをまとめたいと思います。

目次

1.Semantic segmentation

2.fully convolution network(FCN)

3.deconvolution

4.skip-connection

5.U-netの仕組み

1.Semantic segmentation

画像のピクセルそれぞれをクラス分類するタスクです。

(MNISTなど)画像のクラス分類と異なるのは、画像全体をクラス分類しない点です。

ピクセルごとのクラス分類ができると、以下のような出力を得られます。

Alt text

by https://devblogs.nvidia.com/image-segmentation-using-digits-5/

2.fully convolution network(FCN)

一般的なCNNでは畳み込み層と全結合層がありますが、FCNはこの全結合層を畳み込み層に置き換えたものです。

全結合層を畳み込み層に置き換えることで、「物体がなにであるか」という出力から「物体がどこにあるか」という出力になります。

Alt text

by https://arxiv.org/abs/1411.4038

画像上部分が画像全体のクラス分類で、画像下部分はヒートマップで猫がどのあたりにいるかネットワークが把握していることがわかります。

3.deconvolusion

1つ上の画像のヒートマップは入力画像に畳み込み処理を何度も行い、とても小さくなっています。

これに1番上のような出力が得られるような処理を行なっていきます。

このような処理を「up sampling」といい「deconvolution」が有名です。

日本語だと「逆畳み込み」といい、畳み込み処理の逆処理だと思ってください。

CNNで畳み込み層のフィルターの要素を学習していくように、逆畳み込み層のフィルターも同じように学習できます。

4.skip-connection

畳み込み処理を加えていくと、ネットワークが「物体が何であるか」についての特徴を抽出していきますが、poolingの影響で「物体がどこにあるか」についての特徴は失われていきます。

畳み込み処理を行なった後で、逆畳み込みを行なっても物体の位置情報は満足に復元できない場合があります。

それを解決するのがskip-connectionです。

これは畳み込みを行なった後、特徴マップを保持しておいて、後で逆畳み込みをする画像に足し合わせる処理です。

上で提示した車の画像に用いると以下のようになります。

Alt text

by : https://devblogs.nvidia.com/image-segmentation-using-digits-5/

推定する領域(色のついた部分)がシャープになっていることがわかります。

5.U-netの仕組み

U-netには上でまとめた

・fully convolution network(FCN)

・deconvolution

・skip-connection

が使われています。

構造は以下のようになっています。

Alt text

by : https://arxiv.org/abs/1505.04597

左右対象でアルファベットの「U」に似ていることから、「U-net」と呼ばれているそうです。

大雑把に左側の処理では画像の畳み込みを行い、右側では逆畳み込みをskip-connection(中央矢印)からの情報を参考に行なっているイメージです。

まとめ

U-netの内部で行われる処理について、ざっくりと説明しました。

まとめると・・・

・画像全体のクラス分類(MNISTなど)からFCNとdeconvolutionを使い、物体の位置情報を出力できるようになった

・skip-connectionを用いて畳み込みによって失われる位置情報を保持しておくと、より精密な領域を出力できる

・U-netは上記2つを上手く組み合わせている

こちらの記事ではPythonで実装を行い、学習している様子を紹介しています。


定期的にメルマガでも情報配信をしています。
ご希望の方は問い合わせページに「メルマガ登録希望」とご連絡ください。


Twitter・Facebookで定期的に情報発信しています!

はじめに

ニューラルネットワークの学習の目的は、損失関数( loss function )の値をできるだけ小さくするようなパラメータを見つけることに他ならない。これは言い換えれば最適なパラメータを決定するという点で最適化問題に帰着されるが、ニューラルネットワークの最適化はそのパラメータの数の多さから大変複雑な問題として扱われる。

今回はSGD, Momentum, AdaGrad, Adamと呼ばれる4つのパラメータ更新手法を紹介し、最後にMNISTデータセットを用いてこれらの更新手法を比較していく。

※Pythonで用いているコードは『ゼロから作るディープラーニング(オーム社)』を参考にしている。

SGD(Stochastic Gradient Descent)

SGD(確率的勾配降下法)は、勾配方向へある一定距離だけ進むことを繰り返すという単純な方法である。以下はSGDのparameter更新式である。(以下、Lは損失関数)

Deep Learning

但し偏微分は損失関数の勾配を表している。これをPythonのクラスとして実装すると以下のようになる。

Alt text

確かに実装は簡単であるが、関数の形状が等方向でないと非効率な経路で探索してしまうという欠点がある。例えば、

Deep Learning

という関数についてこのSGDを適用してみよう(以下この関数を用いて各手法における最適化問題を解く)。その前にこの関数の形状及び勾配を下記に示す。

SGD SGD


この勾配はy軸方向に大きく、x軸方向には小さいことが分かるだろう。しかし、今この関数の最小値は明らかに(x, y)=(0, 0)であるのに、上で示した勾配は多くの場所で(0, 0)方向を指していない。

実際、この関数にSGDを適用した結果(初期値(x, y)=(-7.0, 2.0))は以下のようなジグザグした動きをし、これはかなり非効率な経路である。

SGD

このSGDの非効率な探索経路の根本的な原因は、勾配の方向が本来の最小値ではない方向を指していることに起因している。 この欠点を改善するために、続いてMomentum, AdaGrad, Adamの3つの手法を紹介する。

Momentum

Momentumとは「運動量」という意味で物理を知っている人なら馴染みの深い言葉だろう。Momentumのparameter更新式は次のよう。

Deep Learning

Momentumは簡単に言う\とSGDに慣性項αvを付与したものである。αはハイパーパラメータであり、前回の更新量にα倍して加算することでparameterの更新をより慣性的なものにする狙いがある。

今回もPythonのクラスとしてMomentumを実装してみる。

Alt text

vは初期化時には何も保持しないが、update()が初めに呼ばれるときに、parameterと同じ構造のデータをディクショナリ変数として保持する。

ではこのMomentumを用いてさっきの関数の最適化問題を解いてみよう。結果は下図。

Momentum

SGDよりもジグザグ度合いが軽減されているのは一目瞭然であるが、一体何故だろうか。x軸、y軸についてそれぞれ考えてみよう。

x軸方向: 受ける力は小さいが、常に同じ方向に力を受けるため、同じ方向へ一定して加速する

y軸方向: 受ける力は大きいが、正と負の方向の力を交互に受け止めるため、それらが互いに打ち消し合い、y軸方向の速度は安定しない。

これよりSGDのときと比べてx軸方向により早く近づくことができジグザグの動きを軽減できる。

AdaGrad

ニューラルネットの学習においてはラーニングレートηの値が重要になってくる。この値が小さすぎると学習に時間がかかりすぎてしまうし、逆に大きすぎても最適解にたどり着かずに発散してしまう。

このラーニングレートに関する有効なテクニックとして、学習係数の減衰( learning rate decay )という手法がある。これは簡潔に言うと、学習が進むにつれてラーニングレートを小さくするというものである。

最初は"大きく"学習し、次第に"小さく"学習する手法で、ニューラルネットの学習ではよく使われる。parameter更新式は以下のよう。

Deep Learning

ここで、変数hにおいて⦿は行列の要素ごとの積を意味し、上式のように経験した勾配の値を2乗和として保持する。そして、更新の際に、hの二乗根で除算することにより学習のスケールを調整する。

これは、パラメータの要素の中でよく動いた(大きく更新された)要素は、ラーニングレートが小さくなることを表している。 実際にpythonのクラスとしてAdaGradを実装してみる。

Alt text

最後の行で1e-7を加算しているのは、self.h[key]の中に0があったときに0で除算してしまうことを防ぐためのものである。 では実際にさっきの関数にAdaGradを適用してみよう。

AdaGrad

これより、さっきのSGD, Momentumよりも効率的に最小値に向かっているのが分かる。y軸方向には勾配が大きいため最初は大きな更新ステップとなっているが、その大きな動きに比例して更新のステップが次第に小さくなるように調整される。

Adam

Adamは2015年に提案された新しい手法であり、直感的にはMomentumとAdaGradを融合したような手法である。この2つの手法のメリットを組み合わせることで、効率的にparameter空間を探索することが期待できる。

また、ハイパーパラメータの「バイアス補正」が行われているのもAdamの特徴である。(詳細はこちらADAM: A METHOD FOR STOCHASTIC OPTIMIZATION)

更新式はやや複雑であるので、今回は割愛するが勾配降下法の最適化アルゴリズムを概観するに掲載してあるので、興味のある方はぜひ見てほしい。

実際にpythonのクラスとしてAdamを実装してみる。

Alt text

Adamを用いて先の関数の最適化問題を解いてみよう。結果は以下のよう。

Adam

ソースコードは以下。

Alt text

この比較実験では、5層のニューラルネット、各隠れ層100個のノードを持つネットワークを対象にした。また活性化関数としてReLUを使用した。

上のグラフより、SGDよりも他の手法のほうが速く学習できていることは見て容易だろう。さらにAdaGradのほうが少しだけ速く学習が行われているようである。

しかし、この実験はハイパーパラメータ(ラーニングレートなど)の設定や、ニューラルネット自体の構造(何層にするか)によって結果は変わってくることに留意せよ。

しかし、一般的にはSGDよりも他の3つの手法の方が速い学習を行い、より高い認識性能を誇ることもある。

以上4つの手法を紹介したが結局どれを用いるのがベストなのか?残念ながら全ての問題で優れた手法は今のところなく、それぞれに特徴があり、得意不得意な問題があるのが現状である。

実際、多くの研究において今でもSGDは用いられているし、他3つの手法も十分に試す価値がある。結局、自分の好きなように色々試して、最良の手法を見つけていく他ないであろう。


定期的にメルマガでも情報配信をしています。
ご希望の方は問い合わせページに「メルマガ登録希望」とご連絡ください。

参考文献

『ゼロから作るディープラーニング(オーム社)』

Optimizer:深層学習における勾配法について

各手法におけるパラメータ更新経路

ニューラルネットワークとは

 ニューラルネットワークとはAI (人工知能)のうちの一つ。また、AIの一つである機械学習のうちの一つでもある。(図1)また、人間の脳内にある神経細胞(ニューロン)とそのつながり、つまり神経回路網を数理モデル化したもの。(図2)

image(図1)

 ニューラルネットワークを構成している最小単位は、パーセプトロン(単純パーセプトロン)という。パーセプトロンとは、複数の入力に対して1つの値を出力する関数のこと。パーセプトロンへの入力値を(X1~Xn)とすると、それぞれの入力に対して重み(W1~Wn)が存在する。また、バイアスW0が存在する。

 f(x)(それぞれの入力値(X1~Xn)に対して重み(W1~Wn)を掛け合わせ、それにW0を足したもの)の値が0より大きい場合は1が出力され、0より小さい場合は0が出力される。(図2)f(x)が0より大きくなり1が出力されることをニューロンの発火という。発火のしやすさはバイアス(W0)によって決まり、バイアス(W0)は発火のしやすさを調整する役割をすると言える。

 図のように1つのパーセプトロンは単純パーセプトロンといい、単純パーセプトロンを複数組み合わせたものを多層パーセプトロンという。単純パーセプトロンを複数組み合わせて多層パーセプトロンにすることで、より複雑な関数近似を行うことができ、出力の精度を高めることができる。   image(図2)

 (図2)において

f(x) < 0 → 1

f(x) = 0 → 0 or 1

f(x) > 0 → 0

のような数式は活性化関数といい、ニューラルネットワークやパーセプトロンで用いられている、モデルを表現するための関数である。活性化関数を用いて変換を行うと様々な値の出力が行え、モデルの表現力を高めることができる。​

 パーセプトロンにおいてはステップ関数、ニューラルネットワークではシグモイド関数が主に使われている。

image(図3)

(シグモイド関数はステップ関数を連続的に表現したもの)

 ここで1つ指摘しておきたいのは、単純パーセプトロンを複数組み合わせてできた多層パーセプトロンはほぼニューラルネットワークと言うが、等しいわけではないということである。多層パーセプトロンはステップ関数を用いているが、ニューラルネットワークはシグモイド関数を用いているという点で違いがある。

ニューラルネットワークの構造

image(図4)

(画像や音声などの情報も数値として入力する)

 ニューラルネットワークは、入力層、中間層、出力層の3つから成り立っている。入力層は人間の脳で言う感覚層、出力層は判別層、中間層は情報の処理を行っている部分に置き換えることができる。

 ニューラルネットワークの入力層と中間層、中間層と出力層の間にはニューロン同士のつながりの強度を示す強度W(重み)というものが存在する。ニューラルネットワークでは、この重みを調節することによって、入力したものに対して望む出力(教師データ)に近づけることができる。

※中間層が2層以上のニューラルネットワークをディープラーニングという​

ニューラルネットワークの種類

 ニューラルネットワークには様々な種類がある。その中でも特によく使われているのが次の3種類である。

  1. RNN(再帰型ニューラルネットワーク)
  2. CNN(畳み込みニューラルネットワーク)
  3. LSTM(Long Short Term Memory)

RNNとLSTMの違い

 (今回は一つ一つを説明することができないため、詳しい内容は別の記事で扱っていく)

 1. RNN(再帰型ニューラルネットワーク)

 再帰型ニューラルネットワーク(Recurrent Neural Network)は主に自然言語処理の分野で使われている。

 それまでのニューラルネットワークでは、入力値は互いに独立したものである必要があった。この場合、画像処理などでは問題ないが言語のように入力値に連続性がある場合は適さない。そこで、RNNでは中間層にループを組み込むことによって前のデータの入力を記憶できるようにした。そのことによって前後のデータが関連付けられるようになり、自然言語処理などの時系列データにも対応できるようになった。RNNは主に、機械翻訳、文章生成、音声認識などに使われている。

image(図5)

 2. CNN(畳み込みニューラルネットワーク)

 畳み込みニューラルネットワーク(Convolutional Neural Network)は一般的なニューラルネットワークと違い、畳み込み層とプーリング層でできている。

 ここでいう畳み込みとは、簡単に言うと画像の特徴を際立たせることである。画像全体から様々な特徴を取り出していき、画像全体をそのまま分析するのではなく、画像より小さなフィルターを画像全体にスライドさせながら、部分部分で分析していく。フィルターを複数枚使って様々な特徴を抽出していく。そして、畳み込み層で抽出した特徴をもとに特徴マップを作成する。

 プーリング層では、特徴マップの要約を行う。特徴マップを小さなウィンドウに区切り、区切ったウィンドウ内の最大値をとっていく。プーリングを行うことによって、特徴の厳密な位置の変化を気にすることなく画像内での特徴を検出することが可能になる。これは、移動普遍性と呼ばれ、画像認識にCNNが向いている大きな理由の一つである。実際のCNNは畳み込み層とプーリング層が何層にも重なってできている。

 プーリング層の後に、多次元のデータを1次元のデータにフラット化していき、ソフトマックス法を用いて分類し、出力していく。​

image(図6)  

 3. LSTM(Long Short Term Memory)

 先ほどのRNNには複雑な構造が故に1つ問題があった。RNNでは入力したデータは全て記憶されてしまい、必要でないデータも記憶してしまうということがあった。そこで、LSTMでは情報を忘れる機能が追加された。それによって、「この情報は必要」と「この情報は必要じゃない」という判断ができるようになった。

 それまでRNNが苦手としていた予測情報と関係情報の距離が長いケースでもLSTMで対処できるようになった。これにより、機械翻訳の精度が飛躍的に向上した。

image(図7)

ニューラルネットワークによって可能になること

 ニューラルネットワークでは、主に以下のことが可能になる。

  1. 画像認識...主にCNNを用いる
  2. 音声認識...主にRNNを用いる
  3. 自然言語処理...主にRNNを用いる
  4. 異常検知...主にCNNを用いる

LSTM(Long Short Term Memory)は音声認識など、様々な場面で使われるモデルなのですが、意外と歴史のあるモデルでもあります。今はattention等に押されている感はありますが、通常のRNNを大きく改善したと呼ばれる、学ぶ価値のあるモデルです。ここでは、RNNとの違い、実際の仕組みを解説していきたいと思います。

1 RNN

LSTMはRNNの一種ですが、通常のRNNが情報をそのまま次に引き継ぐのに対し、LSTMでは中間層を噛ませて次に渡しています。

Alt text

従来のRNNは、一度データを通して得た情報を、次のインプットと一緒に後続に渡す仕組みでした。

Alt text

Long Short-Term Memoryより引用。

しかしこのモデルでは、長期依存性の問題があります。昔の情報を現在まで保持するのが難しいため、文章などのデータを適切に処理できないのです。関連する情報とそれが必要になる場面が離れていれば離れているほど、RNNはこれらを上手く繋げることができないのです。

Alt text

Long Short-Term Memoryより引用。

LSTMはこの問題を解決するために開発されました。

2 LSTMの仕組み

では、LSTMの具体的な仕組みについて解説していきます。以下に二種類の図がありますが、この二つの図が同じものを表していることがわかるでしょうか。これがわかれば、今回の目標は半分達成です。

Alt text

Long Short-Term Memoryより引用。

Alt text

これらの図は、1999年に開発されたバージョンで、chainerのLSTMに実装されているものです。

ではステップごとに見ていきましょう。この順番は便宜上のものであり、計算が前後しても大丈夫な部分もありますが、例えばOutput Gateに必要なインプットはForget GateとInput Gateで計算されるので、そういった部分は順番に注意する必要があります。

2-1 Forget Gate

一つ目の部品は、Forget Gateと呼ばれる、文字どおり「忘れる」ためのゲートです。これは実は1997年のオリジナルのLSTMモデルにはない部分で、インプットが大きく変わる際、一度メモリセルで記憶した内容を一気に「忘れる」ことを学習します。

一つ目の図で言うと、下図のようになります。

Alt text

Long Short-Term Memoryより引用。

二つ目の図では、

Alt text

Long Short-Term Memoryより引用。

という式によって表現されています。(とりあえずp_forの項は無視してください)

2-2 Input Gate

二つ目のステップは、その段階の新しいインプット(xt)を処理するゲートです。古いリカーリングインプット(y{t-1})と新しいインプット(xt)をそれぞれシグモイド関数とtanh関数にかけ、XORすることで、新たな候補値のベクトル(C^~t)を計算します。そして、この新たな候補値を、forget gateに古い候補値(C_{t-1})をかけたものに足します。

一つ目の図で言うと、

Alt text

Alt text

Long Short-Term Memoryより引用。

二つ目の図で言うと、

Alt text

Alt text

Alt text

になります。

少しややこしくなりますが、最後の式を見ると、インプットを処理して得たCに、古いCの中でforget gateが残すべきと判断したものを足し合わせる、という仕組みになっていることがわかります。

2-3 Output Gate

最後のデートは、出力を処理するためのゲートです。他のゲートと同様にインプットをシグモイド処理した後で、セル状態(C_t)をtanh関数で処理したものと掛け合わせる構造になっています。

一つ目の図でいうと、

Alt text

Long Short-Term Memoryより引用。

二つ目の図で言うと、

Alt text

Alt text

になります。

LSTMは、主にこの3種類のゲートで成り立っています。一つ一つのゲートにおける仕組みは、図や式で表現されている通りですが、今一度まとめると、

LSTMの特徴は

・y_{t-1}という古いアウトプットを次の段階でインプットとして使用する、というRNNの構造を保ちつつ

・C_{t-1}という長期記憶を少しずつ変えていく

という2点で、それを

・Forget Gate(古いC_{t-1}のうちどの部分を忘れるか)

・Input Gate(新しいインプットと一つ前のアウトプットを組み込む)

・Output Gate(更新された長期記憶を再度処理してアウトプットを作る)

の3つのゲートで管理しています。

ここで疑問となるのが、

なんで入力と出力にややこしいゲートがあるの?

という点です。必要以上にややこしいというのは、その通りです。しかし、これには理由があります。

2-4 入力ゲートと出力ゲートの意味

実は、このややこしいゲートは重みを上手く調節するために存在しています。一般的なRNNでは、ユニットiからの出力が重みw_{ij}をつけてユニットjに入力されます。しかし、時系列データを使うと従来の方法ではこの重みが相反する2つの作用によって上手く更新されない、そのような事態が起きていました。具体的には

・ユニットを活性化されることによる入力/出力信号の伝達

・ユニットからの無関係な入力/出力によってユニットが消去されることを防ぐを入力/出力信号の保護

の二つの更新が同時に行われる場合があるのです。これを防ぐために、一見ややこしく不必要な入力ゲート・出力ゲートがLSTMに付けられているのです。

2-5 様々なLSTM

LSTMにも様々なバージョンがあり、実際には仕様によって少しづつ違います。その中でも比較的重要なものを一つご紹介したいと思います。

上でも記したように、LSTMは「忘却する・入力する・出力する」という3つのゲートで成り立っています。しかし、制御対象であるメモリセルの内部状態(C_t)それ自体は制御に使用されていませんでした。そこで、peephole connectionと呼ばれる接続を各ゲートに流し込むことで解決を図りました。

Alt text

上の図の青い線がpeephole connectionにあたります。ご覧の通り、各ゲートに青い線でメモリセルの内部状態(C_t)が流れ込んでいるのがわかると思います。上の式で説明されていなかった項の正体は実はこのpeephole connectionです。

(もっと知りたい人向け)

・LSTMのBack Propagation (逆伝播)

・Gradient Clipping

・Constant Error Carousel (CEC)

参考文献:

LSTMネットワークの概要

わかるLSTM ~ 最近の動向と共に

Long Short-Term Memory

1.ニューラルネットワークの定義

ニューラルネットワークとは、人間の脳内にある神経細胞(ニューロン)とそのつながり、 つまり神経回路網を人工ニューロンという数式的なモデルで表現したものである。

2.ニューラルネットワークの歴史

コンピューター科学の父であるアラン・チューリング氏によって様々な論文が出された。 その中の特に「チューリングテスト」によって第一次AIブームが到来し、 ニューラルネットワークの黄金時代を築くようになった。

3.ニューラルネットワークを直感的に理解するための数式

image 生物の脳内にはニューロンという細胞があります。 この細胞には電気物質が通り別の細胞に電気が流れることで脳が活性化します。 以下でこれを数式化したい。脳内では、シナプスという細胞があり、これをxとおく。 シナプスを経由した細胞体(ソマ)に集合する。これをbとおく。細胞体(ソマ)を流れるものがアクソンであり、 これをyに伝達します。これらを通る電気をwとおきます。 これらを数式に置き換えるとf(x1、x2)=b+w1x1+w2x2というわかりやすいものになります。 この数式を重ねれば重ねるほど脳の伝達が複雑化し多様化する。 つまり、ニューラルネットワークの構築には、シナプスに電気物質が流れ他の細胞に行くことが 繰り返されるように情報データを別のデータと集合し重ねる事で、 正確なデータを出すことができるネットワークを作ることができます。

4.ニューラルネットワークの種類

1.ディープニューラルネットワーク
十分なデータがあることで、人間の力なしで機械が自動でデータから特徴を抽出してくれる。
2.畳み込みニューラルネットワーク
人工知能により画像分析を行う手法である。画像認識処理を行うのに使われる。
3.再帰型ネットワーク
時系列データを使用するネットワークである。翻訳機や音声認識に使われている。

5.ニューラルネットワークの目的

1.量を予測するため。
例えば、売上金額を予想したいときに使用する。
2.どこに何が分類されるか予想するため。
例えば、受注か失注かを区別したい時に使用する。 
3.異常なデータの抽出するため。
例えば、機械の故障検知やクレジットカードの不正利用の検知に使用する。
4.構造を発見するため。
例えば、文書検索のためにキーワードを決める目的に使用します。

6.ニューラルネットワークのモデル例

主なモデルを分類すると以下の4つに分ける事ができる。

  1. 回帰モデル
  2. 分類モデル
  3. 異常検知モデル
  4. クラスタリングモデル

これらのモデルを使用して物事を予測したり、分類したりします。

7.私見

現在、ニューラルネットワークは急成長している。私見では、 今後このネットワークは労働不足問題の解決に役立つものである。 ただし、現在のニューラルネットワークを用いて物事を判断する際に 倫理的な判断能力に欠落があり、これは機械学習の大きな課題である。

8.総評

以上のように、ニューラルネットワークは課題はあるものの、 今後の私達の生活の向上に必要不可欠なものである。

このアーカイブについて

このページには、過去に書かれた記事のうちニューラルネットワークカテゴリに属しているものが含まれています。

前のカテゴリは回帰です。

次のカテゴリはクラスタリングです。

最近のコンテンツはインデックスページで見られます。過去に書かれたものはアーカイブのページで見られます。