強化学習:Introduction

はじめに

今回は、唐突ですが強化学習について整理します. これまで(といってもたったの数回ですが)私が記事で扱ってきたのは、教師あり学習でした.前回クラスの勉強をしている際、ふと強化学習に興味を持ってしまいましたので、理解を深めるため記事にまとめてアウトプットしていきたいと思います.

強化学習とは?

強化学習は、教師あり学習教師なし学習と同じく機械学習の一分野です.ただし、後者の2つがデータを必要とするのに対して、強化学習では学習を行うエージェント・制御器は自分で行動をしてデータを集めます.データには、自分が取った行動、その行動がよかったのか悪かったのかという評価が含まれます.また、その評価は行動後すぐ得られるものばかりではなく、多くの場合いくつかの行動を行った後に与えれることになります.

目標(ゴール)に対して、自分で試行錯誤しながら行動して学習する点がより人間らしいと考えることもできます.

強化学習の基本的なフレームワーク

f:id:shirakonotempura:20190122035140p:plain
Reinforcement Learning: An Introductionに加筆

強化学習の基本的な枠組みを示します.学習を行うエージェント(制御器などとも言いますが、以下、エージェントとします)は、環境(Environment)から、前の行動に対する報酬(Reward)と現在の状況(State)を獲得し、次の行動(Action)を計算します.

エージェントが環境から状態を獲得する方法は、問題の状況によりますが、例えばカメラによる周囲の画像などが状態となります.また後述の囲碁のケースでは、盤面の棋譜(を撮影した画像)が状態となります.報酬は良いと考えられる行動に対しては正の値で、悪い行動には負の値で与えることができます.

教師あり学習のおさらい

教師あり学習では、行動によって得られる経験の代わりに、正解ラベル付きデータセット[x _ {1}, x _ {2}, x _ {3}, \dots x _ {n}, y]で与えられます. 教師あり学習では、入力[tex:x}から出力[tex:yを予測する関数を学習します.

E.g.)顔の検出や識別

f:id:shirakonotempura:20190122042015p:plain

教師なし学習のおさらい

一方、教師なし学習では、ラベルyは与えられません.教師なし学習では、入力データのみから、データ間の深い関連性などを学習します.イメージがつかみにくい分、教師あり学習に比べて、少々難しい気がします.

E.g.)クラスタリング、次元削減 f:id:shirakonotempura:20190122042954p:plain

強化学習の学習手順

f:id:shirakonotempura:20190122035140p:plain

強化学習における学習手順について、上の図(再掲)を用いて説明します.

  • 時間ステップtごとに、エージェントは現在の状態を獲得.
  • 獲得した結果に基づき、次とるべき行動を選択.
  • 選択した行動は、エージェントに報酬(値)を受け取る.

この手順を繰り返すことで、長期的な収益(の期待)値を最大化する行動を選択する方法(ポリシー、\pi)を見つけることが強化学習の目的となります.

強化学習の主な特徴

強化学習の主な特徴を以下にまとめます.

  • エージェントは、どの行動を取るべきかは教えられません.その代わり、試行錯誤でどの行動をとることがよい結果につながったかを学習していきます.
    E.g. 例えばゲームを学習する場合、『ある状態においてはAボタンを押すべき』というような教師データは与えられません.何千回というシミュレーションで、この状態ならAを押すことが一番良い結果につながったということを自分で学習します.そのため、強化学習の学習初期は非常に一見意味のない誤った行動を何度も繰り返すことがよくあります.
  • (ほとんどの場合)環境は、確率的(stochastic)です.
  • (ほとんどの場合)報酬は遅れて与えられます.短期・即時の報酬だけで行動を決めていては、より大きな長期的な報酬を獲得できなくなることもあります.
  • エージェントは、探索と利用(Explore and Exploit)のバランスをとる必要があります.探索とは、これまでの学習結果によらない行動をとること、利用とはこれまでの学習で得られた知識を使うこと、つまり現在の状況で最もよいと思われる行動をとることを指します.

補足:探索と利用について

探索と利用の問題は、強化学習における重要な問題の1つであると言えます.

以下のRPG風シチュエーションで、利用と探索の問題について考えてみます.ちょっと無理やりですが勇者は、モンスターを倒すことが目標で、ひたすらそれを繰り返し、ある一定の時間に倒したモンスターが多いほど良いとでも仮定してください.(一回モンスターを倒すことを一試行とします)

f:id:shirakonotempura:20190122053639p:plain

もし、上の道を選んだ場合、素手でモンスターと戦うことになります.真ん中の道を選んだ場合、剣を獲得してモンスターに挑むことができます.下の道を選んだ場合、魔法使いを仲間にしてかつ剣を獲得してモンスターに挑むことができます.

もし、たまたま最初の試行でに下の道を選んだのであれば、その行動を繰り返すこと(つまり利用すること)が一番効率がいいのですが、もし最初にたまたま上や真ん中の道を選んだ場合、その行動が最もよいわけではないのですが、モンスターを倒すことで得られる報酬を獲得することになり、今選んだ道がベストな道だと勘違いしてしまうことになります. その勘違いを防ぐため、探索として、たまにちょっと違う行動をとらせることで、実は他にもっといい道があったんだということを自分で学習させることができます.

しかし、探索ばっかりしていては、学習した結果を使わないことになりますのでそれはそれで効率が悪くなります.ですので、  ε-greedy法のように、ある程度は学習結果を利用しつつ、たまには学習結果と無関係の行動をとる、といった工夫を行う必要があります.(  ε-greedy法については、次回バンディット問題の説明で詳しく説明します)

今回は、あえて報酬の数値を入れませんでしたが、実際は選んだ行動に対して、剣を獲得+10、魔法使いを仲間にした+20といったように、報酬が数値で与えられることになります.そのあたりは、次回以降で、もう少し詳しく説明していきます.

強化学習の成功例

強化学習が成功した例を紹介します.

補足:AlphaGoは教師あり学習と自己対戦による強化学習で学習を行っていたため、人間の対局データを学習データとして使用していました.
しかし、既にAlphaGo Zeroという後継システムでは、人間の棋譜データを使わずAI同士が数百万回対戦を行い、40日の学習で旧バージョンの能力を超えたとされています.

まとめ

強化学習の導入部分を整理しました. 次回は、強化学習の基本である、バンディット問題について整理しています.次回からはコーディングを入れていく予定です.

最後に、ちゃんとした参考書籍、参考記事を以下にまとめておきます.

抑えておくべき書籍:

とりあえず強化学習の勉強を始めるならコレという本(だと思う).

Reinforcement learning: An introduction(Second Edition)
by Richard S. Sutton and Andrew G. Barto
→けっこう前から改定のドラフトが公開されていたが、とうとう2nd Editionが出版されました.

Algorithms for Reinforcement Learning
by Csaba Szepesvari
→個人的には上の本より難しい気がするので、他の本で全体像を把握した上で読む方がいいかも.

上記2冊は、ハードカバーで買うこともできますし、PDFでよければ無料で見ることもできます.他にも強化学習の本はたくさんありますが、上の2タイトルを知っておいて損はないと思います.有料ですが日本語にも翻訳されています.ただ、Suttonの方は、初版の翻訳になりますので少し古いです.

素早く全体像を知ることができる記事:

強化学習に関する技術ブログは多々ありますが、とりあえず以下の記事あたりが鉄板でしょうか.

ゼロからDeepまで学ぶ強化学習 - Qiita

これから強化学習を勉強する人のための「強化学習アルゴリズム・マップ」と、実装例まとめ - Qiita

深層強化学習アルゴリズムまとめ - Qiita

初心者がPythonのクラスについて理解しようとする話

f:id:shirakonotempura:20190120050031j:plain

はじめに

これまで、私が公開したいくつかのコードでは、写経部分ではたまに使っているかもしれませんが、自分のコーディング部分にはこれでもかというくらいにclassを使っていません.

というのは、全然使い方が理解できていないからです.それに関数defがあれば十分なのではと考えていました.

しかし、例えばKerasのドキュメントなんかにいくと

ImageDataGeneratorクラス

とか当然のようにclassが使われています.チュートリアルやサンプルが充実しているので、見よう見まねで使えてしまうのですが、少しは理解しておかないと痛い目見るだろうと思い、ようやく調べてみることにしました.


注記)記事中に、classとクラスが混在してしていますが、基本的に同じ意味です.ややこしくて申し訳ありません.

classに対するイメージ

現時点で、私がclassに対して持っているイメージです.

  • 複雑そう.
  • 使えたら便利そうだけど、defでがんばれば、使えなくても大丈夫そう
  • よくselfってみるけどなんや・・.

こんなレベルですので、コーディングの前に少し言葉の意味などを調べるところから始めてみます.

class(クラス)とは

まず、classっていったい何なのか、その定義を調べようとしたのですが、 - classがあれば同じコードを何度も書く必要がなくなります.

とか、 - classを理解するには、オブジェクト指向をまず理解・・.

のようになります.定義を調べたいのですが、コード中のclassの定義の仕方や、使い方の説明ばかり出てきます.

こんな質問まで見つかりました(笑)

Python - クラスって存在する意味あるんですか?|teratail

おそらく、classそのものの意味を定義することは難しくて、なるほどこういったことをできる機能をclassというのだと後で理解できるように なるのだと期待しておきます.

ですので一旦、定義のことは忘れてclassで何ができるかを調べていきたいと思います.


なお、私は当然のようにオブジェクト指向を理解していませんでしたので、それについても少し調べました.

こちらもclassに負けず劣らず理解の難しい得たいの知れないやつなんですが、オブジェクト指向に関しては全く分からない方にとっては以下の記事が参考になるかと思います.あくまで概念の話ではあるのですが、一番分かりやすかったです.こういう記事を書けるようになりたいです.

satoshi.blogs.com

classでできること

classについてのこちらの記事では、のように書いています.

classを使用すると、関数を定義するときのように複数の処理や値をまとめておくことができる.

classからは、インスタンスというものが作成でき、インスタンスにはデータを保持させておくことができたり(属性)、関数のように処理をまとめておくことができたり(メソッド)します.このように『1つのもの(オブジェクト)に情報や処理をひも付けて管理できる』のがclassです.

出典:具体例で学ぶ、Pythonのclassの使い方

見慣れない言葉がたくさん出てきて心が折れそうになりますが、私なりの理解も加えながら1つずつ見ていきます.

  • インスタンス:クラス(設計図)をもとに具体化したもの.量産される何か
  • 属性インスタンスごとに持つ個別のデータ.
  • メソッド:クラスの中で呼ぶ関数のこと.のちほど出てくるコンストラクinit() もメソッドの一つ.defで定義する関数はクラスの中ではメソッドと呼ばれているんだな、と捉えて多分大丈夫です.
  • オブジェクト: 数値、文字列、関数などすべてを指す.これもあまり深く考えなくてよさそう.というか、ガチ勢でなければたぶん深く考えてはいけないオブジェクト指向と呼ばれるプログラム言語で扱うモノは全てオブジェクトくらいの勢いでOK.


なんとなく掴めてきたイメージは、クラスっていうベースになる設計図があって、それがあればベースの機能(関数とか)が使えるインスタンスと呼ばれるオブジェクトが量産できるってこと.でも、まだdefで定義する関数との違いが良く分かりません.

関数を集めたファイル:モジュール

いわゆる関数だけを集めたまとまりとしてモジュールがあります.モジュールというのは、tools.pyとかのように名前が付けられたPythonファイルのことで、通常そのファイルの中にはたくさんの関数が書かれます.(ファイルから読み込む機能のことをモジュール機能と言うっていう解釈の方が正しいかもしれません)

例えば、tools.pyファイルの中に、runrun, hithitという名前の関数を定義している場合、以下のようにすれば、関数runrunおよび関数hithitを使用することができるようになります.

from tools import runrun
from tools import hithit

複数行書くのが面倒な場合、from tools import *でも取り込めますが、この場合ファイル内の全ての関数を取り込んでしまいますので、余りスマートではありません.

ライブラリやパッケージとは

モジュールと同じように、複数の関数を使ったりするライブラリやパッケージというものがあります.これらもモジュールと同じように使えます.

詳しく理解できていないので、あくまでイメージとして捉えてほしいですが、乱暴に書けば、モジュールが複数の関数が書かれたファイルだとすると、ライブラリは複数のモジュールが集まったものパッケージは複数のライブラリおよびモジュールが集まったものです.

さらに乱暴に言ってしまえば

パッケージ >ライブラリ > モジュール.でも使い方は同じ

というイメージで問題ないかと思います.

クラスの呼び出し方

話をクラスに戻しますが、クラスもモジュールと同じような書き方で使うことができます.クラス

例えば、my_school.pyというファイルの中に、Studentというクラスを持っているとします.同じファイルの中に他のクラスが入っていても問題ないです.また、クラスを別ファイルで作成しなければいけないわけでもありません.

from my_school import Student

呼び出し方に違いがないことが分かってどうするって思いますよね.私もです.

具体的にコーディングしてみる

結局、ここまでしてきた話では(少なくとも私は)、やはりまだクラスの恩恵を感じていません. こうなったら恩恵が得られることを信じて、具体的にクラスを使ったコードを書いていきたいと思います.

以下のような設定となるclassを作っていきます.

例1 class Student

  • studentクラスから作られる個別の学生インスタンスは、属性として名前、年齢、身長、体重を持つ.
  • 自己紹介を行うメソッドと、身長と体重からBMIとベスト体重を計算して発表するメソッドを持たせる.

classの定義:

classは、関数とほぼ関数と同じように書きます.ここでは、Studentというクラスを作成します. クラス名の最初の1文字目は大文字とするのが慣例のよう.2単語以上つなげる場合は、単語ごとに最初の文字を大文字にしましょう.

class Student:
    pass # ただ定義するだけなので、このクラスは何の機能もありません.

これで、学生インスタンスを作り出す設計図(量産工場)ができました.

インスタンスの作成:

では、各学生に相当する具体的なインスタンス(オブジェクト)を作ってみます. 以下のように、作成したclass(ここではStudent)を変数に代入すれば作成できます.

student1 = Student()

ここのstudent1というのが、各プレイヤーごとに与えるインスタンス名になります。これでstudent1という中身が空っぽの学生インスタンスが現れたことになります.

属性を与える:

学生にの名前・身長・体重という情報を、属性として与えます.student1に与える場合、次のように書けます.

student1.name = "matsuzaka"
student1.height = 183
student1.weight = 93

同様に以下のようにすればstudent2を作成できます.

student2 = Student()
player2.name     = "mori"
player2.height   = 170
player2.weight   = 80

まだ、恩恵は何も感じていないと思いますが、もう少しお付き合いください.

class内の関数であるメソッドを使う

上の例では、nameやheightを一行ずつ与えていますが、少し面倒ですしスマートではありません.class内の関数であるメソッドを使えば、もう少し省略することができます.

メソッドは関数と同様にdef(): という形式で書くことができますが、第1引数にselfというオブジェクトを指定する必要があります.

selfもなかなか理解が複雑な曲者なんですが、self:自身という名前が示すとおり、メソッドを呼び出したインスタンス自身のことを示します.具体的なイメージを持つため、実際にselfを使って、classにメソッドを追加してみます.

  • set_data:nameやheightを設定するメソッド
  • say_my_data:自己紹介するメソッド
  • my_BMI:自分のBMIと適正体重を言うメソッド
# メソッドを使用したクラス
class Student():
  def set_data(self, name, height, weight)
    self.name = name
    self.height = height
    self.weight = weight
  
  # 自己紹介するメソッド
  def say_my_data(self):
    print(print("I am {}.
                My height is {}cm.
                My weight is {}kg."
         .format(self.name, self.height, self.weight)))


  # BMIと適正体重を言うメソッド
  def my_BMI(self):
    bmi = round(self.weight /(self.height/100.)**2)
    best_weight = round((self.height/100.)**2 * 22)
    print("My BMI is {}.
                  My BEST weight is {} kg."
         .format(bmi, best_weight))

メソッドを実行する(引数を与える)側ではselfは引数には指定してはいけません.メソッド側の第2引数から指定してください.

  • メソッドを使えるように書き換えた後
student1 = Student() # インスタンスを作成
student1.set_data("matsuzaka", 183, 93) # 属性を一気に設定
  • 参考:メソッドを使う前はこれ(再掲):
student1 = Player()
student1.name = "matsuzaka"
student1.height = 183
student1.weight = 93

以下を実行すると、

player1.say_my_data()
player1.my_BMI()

以下のように出力されます

I am matsuzaka. My height is 183cm. My weight is 93 kg.
My BMI is 19. My BEST weight is 79 kg.

インスタンスを作成する際に呼び出すメソッド:コンストラク

これで、学生の量産が比較的簡単にできるようになりました.ですが、量産部分のコードを見て思われるかもしれませんが、インスタンスを設定→属性を設定という作業が地味に面倒です.

そこで、コンストラクというものが登場します.コンストラクインスタンス作成と同時に自動的に呼び出されるメソッドのことを指します.

コンストラクタを使うことで、先の例でset_dataメソッドを呼んでいたstudent1.set_data(引数)の一行を省略することが可能になります.

コンストラク__ini__()を使う

コンストラクタは、メソッド名が決まっており__init__()と定義する必要があります.

# コンストラクタを使ったPlayerクラスの定義
class Student:
  def __init__(self, name, height, weight):
    self.name = name
    self.height = height
    self.weight = weight

これで、大分スマートにインスタンス(学生オブジェクト)を量産することができます.今は属性の情報が非常に単純なんでこれが一体何になるのかいまいちピンとこないかもしれません.

例えば、対戦ゲームなんかで、取れる行動の種類などは同じだけど、置かれている状況は違う.といった場合なんかに、それぞれのインタンスの管理がしやすいんじゃないかと思います.たぶんマルチエージェントになるとクラスを使わないと無理なんじゃないかと思えてきました.

すみません.語彙力がなさすぎてうまく説明できない・・・.

クラスの継承

気を撮り直して説明を続けます.classの特徴の1つがここで説明する継承機能と呼ばれるものです.

この機能を利用することで、既に作成したclassをベースに、機能の追加を容易に行うことができます.

先ほど作成したクラス(Student)をベースにして、Universityクラスを作ってみます.

classの継承は以下のように、新しく定義するclass名に続く( )内に、継承したいclass名を指定すればOKです.

class University(Student):
    pass

もし、上のようにその他のコードを何も触らなかった場合、UnivesityクラスとStudentoクラスは全く同じになります.

student1 = University("matsuzaka", 183,  93)
  • メソッドを実行
student1.say_my_data()
student1.my_BMI()
  • これまでと全く同じ出力をするだけです.

    I'm matsuzaka. My height is 183cm. My weight is 93 kg.
    My BMI is 28. My BEST weight is 74 kg.

Universityクラスのコンストラクタを拡張し、大学名と出身県を紹介するセリフを追加してみます.

class University(Student):
  def __init__(self, name, height, weight, univ_name, pref):
    super().__init__(name, height, weight)
    self.univ_name = univ_name
    self.pref = pref
  
  def say_my_univ(self):
    print("I study at {}, I'm from {} prefucture"
          .format(self.univ_name, self.pref))

インスタントstudent3を作成します.コンストラクタの引数を増やしたので、大学名と出身県を追加しています.

student3 = University("uchikawa", 185, 93, "Fukuoka Univerisity", "Ohita")

say_my_dataメソッド、my_BMIメソッドは継承されているので、これまでと同じように呼び出すことができます.

student3.say_my_data()
student3.my_BMI()
student3.say_my_univ()

この場合、出力は以下のようになります.

I'm uchikawa. My height is 185cm. My weight is 93 kg.
My BMI is 27. My BEST weight is 75 kg.
I study at Fukuoka Univerisity, I'm from Ohita prefucture

継承の継承もできる

なお、以下のように継承したクラスの継承も可能です.

class Doctor(University):
  def __init__(self, name, height, weight, univ_name, pref, study):
    super().__init__(name, height, weight, univ_name, pref)
    self.study = study
    
  def my_study(self):
    print("I study about {} at {} as posdoc"
         .format(self.study, self.univ_name))

以下を実行すると

student4 = Doctor("yamada", 180, 76, "Osaka Univerisity", "Hyogo", "Yakult")
student4.say_my_data()
student4.my_BMI()
student4.say_my_univ()
student4.my_study()

こう出力されます.

I'm yamada. My height is 180cm. My weight is 76 kg.
My BMI is 23. My BEST weight is 71 kg.
I study at Osaka Univerisity, I'm from Hyogo prefecture.
I study about Yakult at Osaka Univerisity as posdoc

例2 class Countman

ただ、指定した範囲の偶数と奇数をリスト化するだけのクラスを作ってみます.

  • Countman:コンストラクタは開始値と終了値
  • count_even:指定範囲にある偶数のみをリスト化するメソッド
  • count_odd:指定範囲にある奇数のみをリスト化するメソッド
class Countman():
  def __init__(self,start, finish):
    self.start = start
    self.finish = finish
    
  def count_even(self):
    list = []
    for i in range(self.start, self.finish+1):
      if i % 2 == 0:
        list.append(i)
    return list

  def count_odd(self):
    list = []
    for i in range(self.start, self.finish+1):
      if i % 2 == 1:
        list.append(i)
    return list

インスタンスを作成

指定範囲の異なる2つのインスタンスを作成

countman1 = Countman(20, 30)
countman2 = Countman(1220, 1230)

結果の確認

それぞれのインタンスに対して、メソッドを実施

print(countman1.count_even())
print(countman1.count_odd())
print(countman2.count_even())
print(countman2.count_odd())

出力は以下.大丈夫そうです.

[20, 22, 24, 26, 28, 30]
[21, 23, 25, 27, 29]
[1220, 1222, 1224, 1226, 1228, 1230]
[1221, 1223, 1225, 1227, 1229]

まとめ

クラスについて、少し勉強しました.

今回書いたコード程度では、その恩恵を感じられたわけではないのですが、なんか便利そうだということは少し理解できました.

例えば、マルチエージェントのように同じような操作を並列に行う場合などの管理には必須と言えそうです.

使い方を体に覚えさせるため、積極的に使っていきたいと思います.

以下の記事を参考にしました.ありがとうございました.

具体例で学ぶ、Pythonのclassの使い方 | 株式会社キャパ CAPA,Inc.

【入門者向け】Pythonのクラスを習得する方法を詳しく解説! | CodeCampus

Pythonでclass(クラス)を使う方法【初心者向け】 | TechAcademyマガジン

【Python入門】クラスの使い方やオブジェクト指向の概念を理解しよう | 侍エンジニア塾ブログ | プログラミング入門者向け学習情報サイト

「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典:インスタンス

fine tuningで芸術作品の識別をやってみた

はじめに

こんにちは、前々回に引き続きfine tuningを使ってまた別の識別問題に挑戦したいと思います.なお、前回は2クラスの識別問題として俳優の福士蒼汰さんと中川大志さんを識別するAIを作ってみました.

shirakonotempura.hatenablog.com

今回やること:芸術作品(主に絵画)の識別問題

今回は、タイトルにあるとおり絵画の識別をやってみたいと思います. つまり、作品を見てその作者(画家)を推定するAIを作ってみたいと思います.

絵画等芸術作品のブログ等への転載についてはついて詳しく調べる時間がなかったので、前回以上にモザイク多めです.ご了承ください.

集めたデータについて

今回対象とした作家は、セザンヌゴーギャンゴッホピカソルノワールの5名としました.作品画像は、作家1人につき約50枚程度収集し、以下のようにデータを分けました. テストデータに各クラス5枚を確保し、残りを訓練と検証用に8:2で分けています.

class train valid test
cezanne 35 11 5
gaugain 35 11 5
gogh 34 11 5
picasso 34 11 5
renoir 33 11 5

f:id:shirakonotempura:20190118074934p:plain
学習データの一部

かなりデータの数が少ないのですが、fine tuningの力を信じでやってみます.

実装

実装部分は、ほとんどこれまでと変わらないのでさくっと行きます.

  • modelの最終層の活性化関数はactivation = "softmax"
  • モデルのコンパイルで定義するloss関数はloss = "categorical_crossentropy"
  • ジェネレーターで定義するclass_modeはclass_mode = "categorical"

モデルの定義およびコンパイル

#---------Prameter for Modeling-------------------------
# 今回は6カテゴリに分類する
n_categories=5
batch_size=32
img_size = 120

# 画像データを保存しているディレクトリの定義(trainとvalidは同じデータ)
train_dir="images/train"
validation_dir="images/valid"
test_dir="images/test"
#-------------------------------------------------------

# display_dirの中身はテストデータと同じもの.ただし、クラスごとのフォルダ分けはしていない
display_dir='images/display'
file_name='art_finetuning_vgg16'

# VGG16の既存の全結合層は、1000クラス分類用なので使えない→Falseで削除
vgg16_model=VGG16(weights='imagenet',include_top=False,
                 input_tensor=Input(shape=(img_size,img_size,3)))

# 全結合層(FC層)を構築
x=vgg16_model.output
x=GlobalAveragePooling2D()(x)
x=Dense(1024,activation='relu')(x)
prediction=Dense(n_categories, activation='softmax')(x)

# vgg16と全結合層をつなぐ
model=Model(inputs=vgg16_model.input, outputs=prediction)

# 最後のCONV層の直前までの層を更新しない(freeze)
for layer in vgg16_model.layers[:15]:
    layer.trainable=False

# fine-tuningにおいては、optimizerはSGDが多い.
model.compile(optimizer=SGD(lr=0.0001,momentum=0.9),
              loss='categorical_crossentropy', # 2クラスの場合はbinary_crossentropy
              metrics=['accuracy'])

model.summary()

#save model
json_string=model.to_json()
open(file_name+'.json','w').write(json_string)

fit_generatorでデータの水増しおよび学習

def gen_and_fit(model, file_name):
    train_datagen=ImageDataGenerator(
        rescale=1.0/255,
        rotation_range = 30,
        shear_range=0.3,
        zoom_range=0.3,
        width_shift_range = 0.2,
        height_shift_range = 0.2,
        horizontal_flip=True)

    validation_datagen=ImageDataGenerator(
        rescale=1.0/255
)

    train_generator=train_datagen.flow_from_directory(
        train_dir,
        target_size=(img_size,img_size),
        color_mode = "rgb",
        batch_size=batch_size,
        class_mode='categorical', # categoricalからbinaryに変更
        shuffle=True
)
  # 検証用データは一度に検証用データの枚数だけ作成(正規化するだけ)
    validation_generator=validation_datagen.flow_from_directory(
        validation_dir,
        target_size=(img_size,img_size),
        color_mode = "rgb",
        batch_size= num_validation,
        class_mode='categorical',
        shuffle=True
)

    hist=model.fit_generator(train_generator,
                         steps_per_epoch = num_training//batch_size,
                         epochs=epochs,
                         verbose=1,
                         validation_data=validation_generator,
                         validation_steps = 1, #num_validation//batch_size,
                         callbacks=[CSVLogger(file_name+'.csv')])
    
    #save weights
    model.save(file_name+'.h5')
    
    return hist
# 実行して描画
log2graph(gen_and_fit(model, file_name), "art")

学習履歴のグラフを以下に示します.10エポック目くらいから検証データの精度が上がっていませんね.本当はここでモデルの見直しをする必要があるのですが、このままテストデータを使った評価に移ります.最終エポックの検証データに対する精度と同じく80%いかないくらいの精度になるのでしょう.

f:id:shirakonotempura:20190118073506p:plain
f:id:shirakonotempura:20190118073509p:plain

テストデータを使って評価

テストディレクトリに保存した計25枚のテストデータを使って、学習済モデルの評価を行います.

# テストデータによる評価
def testeval(model, file_name):

#load model and weights
    json_string=open(file_name+'.json').read()
    model=model_from_json(json_string)
    model.load_weights(file_name+'.h5')

    model.compile(optimizer=SGD(lr=0.0001,momentum=0.9),
              loss='categorical_crossentropy',
              metrics=['accuracy'])

#data generate
    test_datagen=ImageDataGenerator(rescale=1.0/255)
    test_generator=test_datagen.flow_from_directory(
#        validation_dir,
        test_dir,
        target_size=(img_size,img_size),
        batch_size= num_of_test,
#        batch_size = num_validation,
        class_mode="categorical",
        shuffle=True
)

    #evaluate model
    score=model.evaluate_generator(test_generator, steps = 1) 
    print('\n test loss:',score[0])
    print('\n test_acc:',score[1])
    
    return score

出力結果

Found 25 images belonging to 5 classes.

test loss: 1.1050524711608887

test_acc: 0.7200000286102295

テストデータに対する精度は72%となりました.テストデータは25枚でしたので、7枚間違ったことになります.

テストデータの予測結果例

テストデータを25枚しか用意していないので、すべて表示して確認します.

# 以下は図で表示するためのスクリプト
nb_of_disp = 25
nb_of_row = 5 #np.sqrt(nb_of_disp)
nb_of_col = 5 #np.sqrt(nb_of_disp)

files=os.listdir(display_dir)
img=random.sample(files,nb_of_disp)

plt.figure(figsize=(10,10))
for i in range(nb_of_disp):
    temp_img=load_img(os.path.join(display_dir,img[i]),target_size=(img_size,img_size))
    plt.subplot(nb_of_row, nb_of_col,i+1)
    plt.imshow(temp_img)
    #Images normalization
    temp_img_array=img_to_array(temp_img)
    temp_img_array=temp_img_array.astype('float32')/255.0
    temp_img_array=temp_img_array.reshape((1,img_size,img_size,3))
    #predict image
    img_pred=model.predict(temp_img_array)
    plt.title(label[np.argmax(img_pred)])
    plt.xticks([]),plt.yticks([])


plt.show()
f:id:shirakonotempura:20190118075543p:plain
テストデータに対する予測結果

もはやモザイクで良く分からないですね.今後、正解しているのかどうかが分かるような表示方法に修正していくようにします.

まとめ

今回は、芸術作品を見て作家を推定するAIを作成しました.ちょっと絵画を知っている人くらいの精度は出たんでしょうか.まあ、AIとしては全然ダメな精度ですね.

ただ、ここまでデータが少ない場合、どうすればいいんでしょうか.水増しするにしても限界があるというか.詳しい人の意見が欲しい・・.

Colaboratory上のノートはコチラ

今回参考にさせていただいたすばらしい記事たちです.ありがとうございます.

VGG16を転移学習させて「まどか☆マギカ」のキャラを見分ける - Qiita

GPUを使ってVGG16をFine Tuningして、顔認識AIを作って見た - Qiita

画像の前処理 - Keras Documentation

教師あり学習における訓練、検証、テストデータについて(図解なし)

今回は、少し教師あり学習におけるデータの扱いに考えたので整理します.訓練とか検証とかテストの話です.

私なりのかみくだいた解釈であり、言葉などは素人用語を多用しています.間違っていたら叱咤なりコメントなりください.

データセットとは

おそらくデータセットという言葉に関しては、データのまとまりであるとか、関連データの集まりというような掘り下げだすとかなり深そうな雰囲気がするのですが、教師あり学習に関して言えば、入力データとそれが何を表すラベルがセットになったデータが集まったものという理解で十分かと思います.入力データの部分はテキストであったり、画像や動画、音声や音楽なども含まれます.


公開されているデータセットの例
MNISTデータセット:手書き数字の画像データと、その画像が0~9のどの数字を表すかがセットになっている
Kineticsデータセット:約30万件のYouTube動画に、400種に分類された人間のアクションがラベリングされている.DeepMindによる

こんな感じで公開されているものをまとめている記事もありました.ありがたい.
【24個掲載】機械学習で使えるデータセット一挙勢揃い!

画像を扱う機械学習のためのデータセットまとめ - Qiita

教師あり学習

今更ですが、ある入力に対してそれがどういった出力を出すかの関係(複雑な関数、大体ブラックボックス)を、教師データを使って学習するのが教師あり学習です.教師データは、その名の通り教師ですから、その入力データが表しているものはこれですよという正解ラベルが付いています.そのため、教師データを使えば、推定したラベルが正解ラベルと合っているかを判断することができます.

現実のデータにラベルは付いていない

ただし、現実世界のデータは入力でしかなく正解ラベルは付いていません.そんなものなくても私たち人間は目の前の人が女の人でおそらく30代、アジアの人だろうな、というようなことを瞬時に推定できるわけです.
機械にもそういった推定をさせるためには、教師データで学習した入出力の関係性がラベルを持たない未知の入力データにも使えるような関係性である必要があります.このように未知のデータにも対応できる性質を汎用性と言い、汎用性を持たせることを汎化とか汎化性能を高めるとか言います.

では、どのように高い推定精度を持ちかつ汎用性の高い関係性を見つけるのかいうのが次の問題になりますが、ここでいったん言葉の整理をしておきます.正式な定義じゃないです.

  • モデル:入力データと出力(ラベル)の関係を内部に持つ識別器.どんなニューラルネットを使うのか、あるいはサポートベクターマシンを使うのかなど枠組み(識別手法)の部分は人間が決める必要があります.学習率などのハイパーパラメータも人間が与える必要があります.
  • ハイパーパラメータ:モデルの構造・構成に関わる値.ニューラルネットであれば何層にするか、学習率はいくつにするかなどの数値のこと.この数値で決められたモデルの構造に従って学習は進められます.この値は人間が与える必要があります.
  • 重み:モデルが内部に持っている、入力と出力の関係性を示す関数の係数にあたる部分.学習はこの重みをコンピュータが試行錯誤して決めていく作業になります.選択したモデルによっては理論的に試行錯誤なしで求まったりしますが、今は忘れましょう.

どうやって学習させるか

モデルに汎用性を持たせるためによく用いる方法として、クロスバリデーション(Cross Validation、交差検証)と呼ばれる手法があります.学習用データセットを訓練用と検証用に分けて、学習は訓練データのみで行い、その汎用性の検証を検証データで行うという方法です.

学習はあくまで訓練用データを使って行いますので、学習で得られる重みは訓練用データを推定するのに適した重みとなります.しかし、それでは訓練用データに入っていないデータに対して推定ができない(汎用性がない)ことになりますので、それを確認するために検証用データを使います.

訓練用データで学習したにも関わらず、ちゃんと検証用データに対しても高精度の推定ができるのであれば、それは汎用性が高いと言えます.

検証用データとは別にテスト用データが必要

しかし、検証データで検証すれば汎用性は十分かと言うとそうとは言えません.もし検証の結果、いい精度が出せなかった場合、検証データに対しても精度が上がるようにモデルの設計やハイパーパラメータを見直すことになります.つまり、検証用データがいい結果になるようにチューニングを行っていることになります.別にそのつもりはないとしても、そうなってしまっているのです.

そのため、最後の最後には、検証用データとは別のテスト用データ(訓練にも検証にも出てこなかったデータ)で、モデルの最終評価、まさにテストを行う必要があります.それで結果がダメなら、そのモデルはまだ不十分だと判断しなければなりません.

以上のことから、手元にあるデータセット訓練用、検証用、テスト(最終評価)用に分ける必要があると言えます.

  • 訓練用データ:重みの学習に使用します.
  • 検証用データ:学習済モデルが汎用性があるのかどうかを検証するために使用します.
  • テスト用データ:テストに使用します.最後の最後まで使いませんので、訓練・検証が終わるまでは存在を忘れてもいいレベル.

データの数が十分でない場合、K-foldクロスバリデーションという方法で、訓練用データと、検証用データを入れ替えながら検証を行う方法もありますが、ここで説明は省きます.このときもテスト用データは使いません.


クロスバリデーションに関しては以下の記事が図解付きで参考になりました.
モデルの汎化性を評価する「交差検証」について、Pythonで学んでみた

最終的な学習済モデルはどれを選べばよいのか.

順を追って勉強していれば、おそらくこのような疑問は持たなかったのかもしれませんが、これは私がずっと疑問に思っていた内容です.

例えば、5-Fold クロスバリデーションで検証を行った場合、5つの学習済モデルが出来てしまいます.じゃあ、テストに使う最終的なモデルってどれ・・?.一番スコアが良かったやつ?などと悩んでいたのですが、いろいろ記事を読んで得られた結論は以下です.

ハイパーパラメータも決まって、クロスバリデーションで精度も確認できた.
では次にやることは

訓練用データ、検証用データをまとめた学習用データでもう一度学習

です.


は?と思われるかもしれませんが、モデルをうまく学習させる方法(NNの設計とか、ハイパーパラメータの値)が決まったのであれば、訓練データ・検証データの役割はひとまず終わりです.クロスバリデーション中に学習した様々な重みなども忘れてください.

今使えるデータ(訓練用+検証用)を最大限に使ってモデルを学習させましょう.テスト用データは別にしといてください.

学習後にこれまで触らずにとっておいたテスト用データを使って学習済モデルの確認を行います.ここで精度が悪ければ、あきらめてモデルを見直しましょう.(そのときは、テスト用データの存在はまた忘れてください)


同じ疑問を持った人はいたみたいで、以下の記事も参考になります.

machinelearningmastery.com

まとめ

今回は、訓練データ、検証データ、テストデータについて、自分の記録として残しました.

正直まだ少しモヤっとはしているところもあります.

検証もOK、最終テストもOKとなった場合、実務で最終的にリリースする場合はテストデータ含め持っているデータ全て使ちゃってもいいんでしょうか. 何かを実務でリリースするなんてことは考える必要もない話なんですが、ちょっと気になります.

以下の記事も参考にしました

機械学習のテストデータは2種類用意すべき - へたれプログラマーのメモ

scikit-learn を用いた交差検証(Cross-validation)とハイパーパラメータのチューニング(grid search) - Qiita

交差検定(クロスバリデーション)など機械学習の評価方法まとめ : 新規事業のつくり方

fine tuningで顔識別(福士蒼汰vs中川大志)をやってみた

はじめに

前回、fine tuningで味を占めたので、ちょっと同じようなパターンで変わり種の識別をいくつかやっていきたいと思います.

shirakonotempura.hatenablog.com

今回やること:『福士蒼汰』と『中川大志』の2クラス分類

早速ですが、今回は顔認識に挑戦したいと思います.対象は俳優福士蒼汰氏と中川大志氏.よく見たらもちろん違う顔なのですが、パッと見、見間違いかねないこの2人の顔識別を前回と同じくfine tuningを用いてやってみたいと思います.

福士蒼汰と中川大志の見分け方 - NAVER まとめ

顔データの収集にあたっては、以下記事を参考にして顔部分の切り出しを行いました.ありがとうございます.

qiita.com

私は、毎回google.colab.files.upload()を使って、ファイルのアップロードをしているのですが、Google Driveをマウント(使えるように認識させるくらいの認識でOKです)しておくとGoogle Drive上のファイルを参照できるようになります.最初にいくつか設定が必要ですが、この方法の方が手間と時間を短縮できるようです.

用意した画像

集めた画像を吟味して、ちゃんと本人の顔だけを選択しました.結局手元に残ったのは以下の枚数.これを水増しして学習用データとしたいと思います.(もちろんテストデータは最後の確認用なので水増ししません)

福士さん(fukushi):訓練用133枚、検証用20枚、テスト用30枚
中川さん(nakagawa):訓練用107枚、検証用20枚、テスト用30枚

こんな感じのデータをたくさん作成します.(念のため、モザイクかけておきます.モザイク越しでもイケメンぶりが伝わってきますね・・) f:id:shirakonotempura:20190116032657j:plain

上記のような画像が準備できましたら、以降はほぼ前回と同じことの繰り返しです.ただし、今回は2クラス分類である点が前回(4クラス分類)と異なりますので、該当する部分は若干変更する必要があります.

fine tuningによる画像識別

  • フォルダ構成について
    • 扱う問題は2クラス問題とする[fukushi, nakagawa]
    • images_dirの下にtrain, valid, testディレクトリがあり、それぞれにはfukushi, nakagawaのディレクトリを作成し、写真を保存.
    • 同じくimages直下に作成するdisplayディレクトリにはtestディレクトリと同じデータをfukushi, nakagawaに分けずに保存
    • 学習に適さないデータは削除しておく


* 検討の方針: * VGG16の学習済み結果をベースに、一部重みを変更するFine Tuningを行う. * 次に、fit_generator()を利用して、水増ししながら学習を行う


不必要なデータの削除は手作業となります.ちゃんとチェックしておかないと,たまに誰?みたいなおばあさんが保存されたりします.これは顔のトリミングはあくまで顔検出で行っているので,最初にダウンロードした写真が例えば舞台挨拶の写真だったり、テレビのキャプチャだったりした場合、下図のように写っている顔全てを切り出してしまいます.あっ、千眼さん・・.

f:id:shirakonotempura:20190116033752j:plain
フォーゼは顔として認識せず

VGG16モデルの構築およびコンパイル

主な手順としては、
* VGG16モデルと学習済みの重みをロード
* 今回の分類用の全結合層を構築
* VGG16とFC層を接続
* 学習させない層を定義
* モデルのコンパイル

今回は2クラス分類なので、model.compile()で定義するロス関数はloss = binary_crossentropyとして2値識別用の評価関数を定義しています(多クラスの場合はcategorical_crossentropy).
また、最終層の活性化関数は2値分類で使用するシグモイド関数とし、Dense(1, activation = "sigmoid")としています.(最初の引数は1です.カテゴリー数の2とは違いますので注意).

# 必要なライブラリのインポート
from keras.models import Model
from keras.layers import Dense, GlobalAveragePooling2D,Input
from keras.applications.vgg16 import VGG16
from keras.preprocessing.image import ImageDataGenerator
from keras.optimizers import SGD
from keras.callbacks import CSVLogger

# 今回は2カテゴリに分類する
n_categories=2
batch_size=32
img_size = 200

# 画像データを保存しているディレクトリの定義
train_dir="images/train"
validation_dir="images/valid"
test_dir="images/test"

# display_dirの中身はテストデータと同じもの.ただし、クラスごとのフォルダ分けはしていない
display_dir='images/display'

file_name='face_finetuning_vgg16'

# VGG16の既存の全結合層は、1000クラス分類用なので使えない→Falseで削除
vgg16_model=VGG16(weights='imagenet',include_top=False,
                 input_tensor=Input(shape=(img_size,img_size,3)))

# 全結合層(FC層)を構築
x=vgg16_model.output
x=GlobalAveragePooling2D()(x)
x=Dense(1024,activation='relu')(x)
prediction=Dense(1, activation='sigmoid')(x)

# vgg16と全結合層をつなぐ
model=Model(inputs=vgg16_model.input, outputs=prediction)

# 最後のCONV層の直前までの層を更新しない(freeze)
for layer in vgg16_model.layers[:15]:
    layer.trainable=False

# fine-tuningにおいては、optimizerはSGDがいいらしい.
model.compile(optimizer=SGD(lr=0.0001,momentum=0.9),
              loss='binary_crossentropy', # 2クラスの場合はbinary_crossentropy
              metrics=['accuracy'])

model.summary()

#save model
json_string=model.to_json()
open(file_name+'.json','w').write(json_string)

必要パラメータの定義

epochs はエポックの数 num_trainingおよびnum_validation:generatorで繰り返し作成する画像の枚数.

epochs = 50
num_training = 1600 
num_validation = 400
num_of_test = 60
label = ["fukushi","nakagawa"] 

fit_genratorを使って学習

ImageDataGeneratorで学習データの水増しを行います.train_generatorおよびvalidation_generatorで定義しているclass_modeをclass_mode = "binary"としています.(多クラスの場合は"categorical"

train_datagen=ImageDataGenerator(
    rescale=1.0/255,
    shear_range=0.2,
    zoom_range=0.2,
    horizontal_flip=True)

validation_datagen=ImageDataGenerator(
    rescale=1.0/255
)

train_generator=train_datagen.flow_from_directory(
    train_dir,
    target_size=(img_size,img_size),
    color_mode = "rgb",
    batch_size=batch_size,
    class_mode='binary', # categoricalからbinaryに変更
    shuffle=True
)

validation_generator=validation_datagen.flow_from_directory(
    validation_dir,
    target_size=(img_size,img_size),
    color_mode = "rgb",
    batch_size=batch_size,
    class_mode='binary',
    shuffle=True
)

hist=model.fit_generator(train_generator,
                         steps_per_epoch = num_training//batch_size,
                         epochs=epochs,
                         verbose=1,
                         validation_data=validation_generator,
                         validation_steps = num_validation//batch_size,
                         callbacks=[CSVLogger(file_name+'.csv')])

#save weights
model.save(file_name+'.h5')

テストデータによる結果

学習が終われば、テストディレクトリに入っているデータ(合計60枚)を使って精度の確認を行います.

# テストデータによる評価
from keras.models import model_from_json
import matplotlib.pyplot as plt
import numpy as np
import os,random
from keras.preprocessing.image import img_to_array, load_img


#load model and weights
json_string=open(file_name+'.json').read()
model=model_from_json(json_string)
model.load_weights(file_name+'.h5')

model.compile(optimizer=SGD(lr=0.0001,momentum=0.9),
              loss='binary_crossentropy',
              metrics=['accuracy'])

#data generate
test_datagen=ImageDataGenerator(rescale=1.0/255)

test_generator=test_datagen.flow_from_directory(
    test_dir,
    target_size=(img_size,img_size),
    batch_size= num_of_test,
    class_mode='binary',
    shuffle=True
)

#evaluate model
score=model.evaluate_generator(test_generator, steps = 1) 
print('\n test loss:',score[0])
print('\n test_acc:',score[1])

# 以下は図で表示するためのスクリプト
#predict model and display images
nb_of_disp = 16
nb_of_row = np.sqrt(nb_of_disp)
nb_of_col = np.sqrt(nb_of_disp)
files=os.listdir(display_dir)
img=random.sample(files,nb_of_disp)

plt.figure(figsize=(10,10))
for i in range(nb_of_disp):
    temp_img=load_img(os.path.join(display_dir,img[i]),target_size=(img_size,img_size))
    plt.subplot(nb_of_row, nb_of_col,i+1)
    plt.imshow(temp_img)
    #Images normalization
    temp_img_array=img_to_array(temp_img)
    temp_img_array=temp_img_array.astype('float32')/255.0
    temp_img_array=temp_img_array.reshape((1,img_size,img_size,3))
    #predict image
    img_pred=model.predict(temp_img_array)
    #img_pred が0.5以下ならfukushi, 0.5以上ならnakagawa
    if img_pred <= 0.5:
      plt.title(label[0])
    else:
      plt.title(label[1])
    #eliminate xticks,yticks
    plt.xticks([]),plt.yticks([])


plt.show()


気になる精度ですが、テストデータに対して識別精度87%となりました.2値識別なので決して高い制度ではないですが,得られたデータが少ないことを考えるとぼちぼちといったところでしょうか.おそらく30代・40代男性の程度の識別率には達したはずです.そもそも2人を知らないなんて人は新入社員と会話できるくらいには覚えておきましょう.

Found 60 images belonging to 2 classes.

 test loss: 0.34022757411003113

 test_acc: 0.8666666746139526

f:id:shirakonotempura:20190116034828p:plain
テストデータに対する予測結果の一部(学術研究ということで著作権、ご理解ください!)

まとめ

fine tuningを使って、福士蒼汰氏と中川大志氏の顔認識を行いました.
2クラス分類としては精度87%といまいちでしたが、個人的には満足です.おそらく私より精度いいですし.
参考とさせていただいたこちらの記事でやられている、ぼかし処理や閾値処理などをすればもう少し上がるかもしれませんが、とりあえずここまで.

Colaboratoryのノートはコチラにおいておきます.

ネコ科の猛獣識別にfine tuningを適用したら結構うまくいきました

はじめに

ここ数日、画像識別に行き詰っていたのですがようやくネコ科の猛獣判別でいい精度が出せましたので、内容をまとめておきます.

画像識別で迷走した記録 - 他力本願で生き抜く(本気)

いろいろと迷走してきたのですが、今回はタイトルにあるように既存の学習済モデルの一部を利用するfine tuningというテクニックを使って、一気に識別精度を上げたいと思います.


手順概要

  • VGG16のモデルと学習済の重みをダウンロードする
  • モデルの一部のみ学習する(重みを修正する)ようにモデルの変更を行う(fine tuning)
  • fit_generator()を利用して、データの水増しと学習を同時に行う


学習用(画像)データについて

  • 今回扱う問題は4クラス問題(ネコ、ライオン、トラ、リンクス)
  • スクリプトを実行するディレクトリの下にimagesディレクトリを作成.さらに、その直下に訓練用、検証用、テスト画像用フォルダを作成.
  • データの中身を確認し、学習に不適なデータが入らないよう精査しておく(ここ大事です)
  • 今回、訓練用データは各150枚、検証用データは各50枚、テスト用データは各40枚準備しました

f:id:shirakonotempura:20190113005403p:plain
分類する4クラス(左からライオン、トラ、リンクス、ネコ)

f:id:shirakonotempura:20190113013137p:plain
フォルダ構成

訓練データ、検証データ、テストデータの分け方にはいろいろな方法、考え方があると思うのですが、訓練データと検証データで過学習していないことを確認した後、最後に全く学習・検証に関わっていないテストデータで精度のチェックを行えるよう、完全に分けています.


転移学習(Transfer Lerning)とfine tuningについて

転移学習とFine Tuningを完全に混同していましたが、厳密には2つは微妙に違うようです.既存の学習済モデル(出力層以外の部分)を、重みデータは変更せずに特徴量抽出機として利用するとことを転移学習、学習済モデルの重みの一部を再学習して特徴量抽出機として利用することをFine Tuningと呼ぶようです.
私が今回やることは、重みの一部を再学習させているので、おそらく後者のFine Tuningに該当します.

参考:
What is the difference between transfer learning and fine tuning? - Quora


違いがあることは分かったのですが、既存の学習済みモデルを使うというのが転移学習の概念なのであれば、一部を再学習さえてしまおうというFine Tuningは転移学習の派生版ですよね.くくりとしては、転移学習の方が大きいような気もします(完全に私見です.)


転移学習・fine tuningのメリット

これまで何度か試してきたCNNの実装においては、一から学習を行う必要があるため、大量の学習データが必要となります.また、その分学習時間も多くかかります.そこで、既存の学習済モデルを利用することで、少ない画像で学習効率を上げるのが転移学習およびfine tuningになります. 今回は、ImageNETの約120万枚の画像を1000クラスに分類したVGG16というモデルを利用します.VGG16のネットワーク構造を以下に示します.

f:id:shirakonotempura:20190113020724p:plain

今回は、上図において、fine-tuningと書かれた15層目以降のみ学習を行い、14層目までの学習済みの重みは学習済の重みをそのまま使用します.

なぜそんなことが可能、疑問だったのですが、CNNによる学習においては浅い層ではざっくりとした特徴の抽出が行われ、深い層では画像特融の特徴を抽出するということが分かっています.車における特徴抽出のイメージ図を以下に示します.

f:id:shirakonotempura:20190113015136p:plain

(申し訳ありません.よくブログ記事で拝見する画像なのですが、出典を理解できておりません)


実装

実装にあたっては、以下の記事を参考にしています.
絶対VGG16のクラスにないであろうアニメキャラの画像識別を行っています.あらかじめ訓練・検証・テスト用に学習データを分離して学習および最終評価をしており、その検討手順が理解しやすかったです.

qiita.com


VGG16モデルの構築およびコンパイル

主な手順は以下です.

  • VGG16モデルと学習済みの重みをロード
  • 今回の分類用の全結合層を構築
  • VGG16とFC層を接続
  • 学習させない層を定義
  • モデルのコンパイル
# 必要なライブラリのインポート
from keras.models import Model
from keras.layers import Dense, GlobalAveragePooling2D,Input
from keras.applications.vgg16 import VGG16
from keras.preprocessing.image import ImageDataGenerator
from keras.optimizers import SGD
from keras.callbacks import CSVLogger

# 今回はネコ、ライオン、トラ、リンクスの4カテゴリに分類する
n_categories=4
batch_size=32
img_size = 100

# 画像データを保存しているディレクトリの定義
train_dir="images/train"
validation_dir="images/valid"
test_dir="images/test"

# display_dirの中身はテストデータと同じもの.ただし、クラスごとのフォルダ分けはしていない
display_dir='images/display'

file_name='cats_fintuning_vgg16'

# VGG16の既存の全結合層は、1000クラス分類用なので使えない→Falseで削除
vgg16_model=VGG16(weights='imagenet',include_top=False,
                 input_tensor=Input(shape=(img_size,img_size,3)))

# 全結合層(FC層)を構築
x=vgg16_model.output
x=GlobalAveragePooling2D()(x)
x=Dense(1024,activation='relu')(x)
prediction=Dense(n_categories,activation='softmax')(x)

# vgg16と全結合層をつなぐ
model=Model(inputs=vgg16_model.input, outputs=prediction)

# 最後のCONV層の直前までの層を更新しない(freeze)
for layer in vgg16_model.layers[:15]:
    layer.trainable=False

# fine-tuningにおいては、optimizerはSGDが多い.
model.compile(optimizer=SGD(lr=0.0001,momentum=0.9),
              loss='categorical_crossentropy',
              metrics=['accuracy'])

model.summary()

# 結果の描画用(勉強不足のため理解が追い付かず)
#save model
json_string=model.to_json()
open(file_name+'.json','w').write(json_string)


必要パラメータの定義

  • epochs はエポックの数
  • num_trainingおよびnum_validation:generatorで繰り返し作成する画像の枚数. -1回のエポックでnum_training枚およびnum_validation枚の画像を作成して学習あるいは検証を行う.
  • num_of_test:テストフォルダに入っている画像の総数(今回は40枚×4クラスで160枚)
  • label:テストデータで推定した結果を表示する際に使用します.(alphabetにしておかないと、ダメっぽい?(要精査))
epochs = 50
num_training = 1600 
num_validation = 400
num_of_test = 160
label = ["cat","lion", "lynx", "tiger"]


fit_genratorを使って学習

ImageDataGeneratorで学習データの水増しを行います. 今回は、これまでのようにデータを一度作成して保存しておくわけではなくfit_generator()により、学習データの水増しと、それを使った学習を同時に行っています.flow_from_directoryで指定したフォルダ名がそのままラベルに使われるので非常に楽です.

train_datagen=ImageDataGenerator(
    rescale=1.0/255,
    shear_range=0.2,
    zoom_range=0.2,
    horizontal_flip=True)

validation_datagen=ImageDataGenerator(rescale=1.0/255)

train_generator=train_datagen.flow_from_directory(
    train_dir,
    target_size=(img_size,img_size),
    color_mode = "rgb",
    batch_size=batch_size,
    class_mode='categorical',
    shuffle=True
)

validation_generator=validation_datagen.flow_from_directory(
    validation_dir,
    target_size=(img_size,img_size),
    color_mode = "rgb",
    batch_size=batch_size,
    class_mode='categorical',
    shuffle=True
)

hist=model.fit_generator(train_generator,
                         steps_per_epoch = num_training//batch_size,
                         epochs=epochs,
                         verbose=1,
                         validation_data=validation_generator,
                         validation_steps = num_validation//batch_size,
                         callbacks=[CSVLogger(file_name+'.csv')])

#save weights
model.save(file_name+'.h5')


学習は順調に進み、最終的には検証データの精度90.6%となりました.工夫をすればもう少し良くなるかもしれません.

(一部)省略
Epoch 50/50
50/50 [===] - 7s 134ms/step - acc: 0.9975  - val_acc: 0.9056


テストデータによる結果

最後に、訓練にも検証にも使用していない、テスト用の画像データを使ってモデルの評価を行います. まず、精度の値.

Found 160 images belonging to 4 classes.
 test loss: 0.2039002388715744
 test_acc: 0.91875


テストデータによる精度は、検証精度とほぼ同等の91.8%!今までテストデータの精度だけ悪かったので、これは素直にうれしいです.fine tuning おそるべし・・.

次にdisplayディレクトリからランダムに選んだ画像に対してラベルを予測した結果を示します.
ちゃんと予想できており、一安心です.
完全にGod_KonaBananaさんのやり方を利用させてもらっています.

f:id:shirakonotempura:20190113022435p:plain

補足

補足1:
実は最初img_sizeを50x50で実行したのですが、その際の精度は80%程度でした.次に100x100にして今回の結果となりましたが元の画像サイズが75x75なので、75より大きくしても余り意味はないのかもしれません.

補足2:
水増しの影響も見るため、用意した600枚の訓練データ、200枚の検証データのみを使った検討も行いました.テストデータに対して90%近い精度を出してしまいました.水増しの効果も結構あると思っていたので、あまり素直に喜べない・・.水増しなしのアプローチは最初に行うべきなのかと思います.あとはfine tuningが効果的すぎるということなんでしょう.

補足3:
これも検討の前に調べてとけっていう話なんですが、今回用意した4クラスはいずれもVGG16の1000クラスの中に名前が挙がっていました.lynxはないだろうと思っていたのですが見事に存在しておりました.ですので、少し悲しいのですが、かなり有利な問題設定になってしまっていると思います.


まとめ

fine tuningを使って、ネコ科の猛獣4クラス分類問題を再度行いました.
結果としては、まだ改善の余地ありという結果ですがこれまでよりはかなり高精度で分類することができました.ただし、用意した4クラスがVGG16の中に含まれているため、高精度は当然の結果とも言えます.今後、全く関係ないクラスでの識別などもやっていきたいと思います.

Google Colaboratory上のノートはコチラ


今回も多くの記事を参考にさせていただきました.ありがとうございました

実装の手順が、細かく書かれています

fit_generatorのstep数などの理解に役立ちました.

非常に丁寧に説明がされています.顔認識に挑戦したくなりました.

もちろん公式ドキュメントも見ましょう.

画像識別で迷走した記録

はじめに

前回は、自分で作成したデータセットを水増しして4クラスの画像識別に挑戦しました.データを水増しすることで、汎化性能が上がりテストデータに対する精度が上がるかと思ったのですが、結果は惨敗.あらかじめ断っておきまさすが、今回の記事は迷走の記録です.技術的に有用な情報は皆無です.

悲しい結果に終わった記事は以下. shirakonotempura.hatenablog.com

具体的には、検証データに対する精度は上がったのですが、肝心のテストデータに対する精度が水増し前よりも下がるという状況. この後、いろいろと記事を読んで、原因は以下のようなところではないかと推測したわけです.

  • 水増しの仕方が悪い
  • ネットワークの設計が悪い
  • そもそもの画像データの質が悪い

この後、何も計画もなしに水増しの方法をパラスタしはじめたことが迷走の始まりでした.


迷走1:datagenのパラスタ

この後、画像変換における角度・スライド幅の範囲を変えてみたり、最適化手法の方法を変更してみたり、かなり迷走しました.
しかし、どれも検証データの結果はそこそこの結果になるのですが、テストデータに対する評価はよくて65%程度とまり. 水増し方法を変更するだけでは、にっちもさっちもいかないので水増し方法を変えるのはあきらめました.

私は、rangeの値は0.1でやってましたが、以下のような設定が多いようです.

train_datagen=ImageDataGenerator(
    rescale=1.0/255,
    shear_range=0.2,
    zoom_range=0.2,
    horizontal_flip=True)

迷走2:optimizerのパラスタ

では、optimizerを違うのを使えば良くなるのではとAdadelta()としていたoptimizerRMSprop()、Adagrad()、Adam()、Nadam()として比較.それぞれのパラメータはデフォルトのまま利用しています.
検証データは90%近く精度は出ているのですが、結局テストデータに対する精度は相変わらず60%とかでした.強いて言えば、今回のケースではRMSprop()の収束があまりよくなかったのですが、これは問題によりけりでしょう.
載せていませんが、転移学習で使うSGD()も試してはいます.安定はしていましたが結果は同じでした.

f:id:shirakonotempura:20190112074748p:plain

迷走3:データセットを変えてみる

これが一番ダメな迷走でした.選んだ題材が悪かったのかとネコ科の猛獣をやめて果物の識別もやってみたりもしました.
猛獣よりは識別しやすいようで、テストデータに対する精度は80%近くは出ました.わーい.でも、検証データの精度は100%近く出ている上でのこのテスト精度なわけで、根本的解決にはならず.

f:id:shirakonotempura:20190112074755p:plain

この辺で、何をやっているのかわからなくなってきましたので、心を落ち着かせるためにデータを見つめなおすことにしました.

データの見直しをしてみる

結局はネコ科の猛獣識別でいい結果を出したいので、今更なのですが持っているデータをあらためて確認してみました.
そしたら、いろいろと問題の画像が出てきました.
サバンナの草原の中に、ポツンといるライオンやトラの画像.リンクスには、ヘリコプターの画像も混ざっていました.
いわゆる質が悪いデータセットを使っていたようです.
ですので、多少なりとも改善の期待を込めて画像の選別をしていこうと思います.基本的には画像内に1匹、かつ人の目で顔が認識できる写真のみを選別.選別後、150枚ずつくらいはデータが残ればいいのですが.ちなみにライオンはメスとオスが混ざっているのですが、これは両方ともライオンと識別できると期待をこめて残しておきます.

つべこべ言わずに転移学習を入れてみることにする

転移学習については、通常のCNNである程度結果が出てからと考えていたのですが、結局後で適用するのであればもう導入してしまおうと思います.

まとめ

まとめるも何もほぼ何も成果がないのですが・・.
今回いろいろと試してみて思ったのは、MNISTやIrisを使ったチュートリアルは簡単に精度が出せるけど、実際のデータでは難しいんだなあということ.
まあ、そんなことは多くの先人が数年前からたびたび指摘してくれているのですが、身をもって痛感しました.でも、自分で色々試しているうちにコードの書き方にも慣れてきましたので結果オーライと考えてまた頑張ります.