転移学習による画像分類 – 最先端のCNNモデルを作成する

新しいモデルが頻繁に発表され、コミュニティが認めたデータセットに対してベンチマークが行われているため、すべてのモデルに対応することは難しくなっています。

これらのモデルのほとんどはオープンソースであり、自分で実装することも可能です。

つまり、平均的な愛好家は、自分の家で、ごく普通のマシンで、最先端のモデルをロードして遊ぶことができ、技術に対する深い理解と評価を得ることができるだけでなく、科学的な議論に貢献し、改良がなされた場合には自分自身で発表することができるのです。

このレッスンでは、画像分類のために事前に学習された最先端のDeep Learningモデルを使用し、あなた自身の特定のアプリケーションのためにそれらを再利用する方法を学びます。

この方法では、高性能で独創的なアーキテクチャと他人のトレーニング時間を活用し、これらのモデルを自分のドメインに適用することができます。

コンピュータビジョンと畳み込みニューラルネットワークのための転送学習

知識および知識表現は非常に普遍的である。

あるデータセットで学習したコンピュータビジョンモデルは、他の多くのデータセットで非常に一般的である可能性のあるパターンを認識することを学習する。

特に、Bharath Ramsundar、Peter Eastman、Patrick Walters、Vijay Pandeによる「Deep Learning for the Life Sciences」では、以下のように記されています。

「分子結合予測に使用するための推薦システムアルゴリズムの使用について調べた研究が複数ある。

ある分野で使用される機械学習アーキテクチャは、他の分野にも引き継がれる傾向があるため、革新的な作業に必要な柔軟性を保持することが重要である。

例えば、直線や曲線は、一般的にCNNの下位階層で学習されるものですが、実質的にすべてのデータセットに存在するはずです。

また、ハチとアリを区別するような高レベルの特徴は、より高い階層で表現され、学習されることになる。

これらの間の “微妙なライン “が、再利用可能なものです データセットとモデルが事前に学習したデータセットの類似度によって、データセットの一部または大部分を再利用できる可能性があります。

ということです。

人造物を分類するモデル(Places365などのデータセットで学習)と動物を分類するモデル(ImageNetなどのデータセットで学習)は、多くはないですが、いくつかの共有パターンを持っているはずです。

例えば、自動運転車のビジョンシステムのために、バスと車を区別するモデルを訓練したいと思うかもしれません。

また、あなたと似たようなデータセットでうまく動作することが証明されている、非常にパフォーマンスの高いアーキテクチャを使用することを合理的に選択することもできます。

そして、長い学習プロセスが始まり、最終的には自分自身の高性能なモデルを手に入れることができるのです。

しかし、他のモデルがより低い抽象度や高い抽象度で同様の表現を持っている可能性がある場合、ゼロからモデルを再トレーニングする必要はない。

すでに訓練された重みのいくつかを使うことにしてもよいでしょう。

それは、オリジナルのアーキテクチャの作成者が適用したのと同じように、あなた自身のモデルの適用にも適用できます。

これは既存のモデルから新しいモデルに知識の一部を移すことになり、移転学習と呼ばれます。

私が思うに、転移学習の重要性と汎用性は控えめです。

それはしばしば脇役にされたり、授業や講義の最後に簡単に触れられたり、CNNについて学ぶときに最後に取り上げられる概念であることが多いのです。

コンピュータビジョンの特定の問題への応用について読んでいるときはいつでも、可能性としては、背景には伝達学習があるのです。

私のように午後を費やして様々な分野の研究論文を読んでいると(私はほとんど知識がないのですが)、転送学習がその名前で言及されていない場合でも、いかに一般的に使われているかに気づくでしょう。

あまりに一般的なので、事実上、転移学習が使われていると思われているのです。

事前にロードされたモデルと移転された知識があれば、ほとんど誰でも深層学習の力を利用して、ある分野をさらに発展させることができます。

  • 医師はコンピュータビジョンモデルを使って画像診断ができます(X線、組織学、網膜鏡検査など)。
  • 都市では、コンピュータ・ビジョンを使って、道路の歩行者や車を検知し、交通の流れを最適化するために信号機を適応させることができます。
  • モールで駐車場の混雑状況を把握することができます。
  • 海洋生物学者は、コンピュータビジョンを使って、絶滅の危機に瀕したサンゴ礁を検出することができます(例
  • 製造業では、コンピュータビジョンを使って生産ラインの欠陥を検出することができます(薬の錠剤の紛失など)。
  • 報道機関がコンピュータビジョンを使って古い新聞をデジタル化する。
  • 農作物の収穫量や健康状態(虫や害虫の有無など)をコンピュータビジョンで検出

ワークフローや投資の最適化から人命救助まで、コンピュータ・ビジョンは非常に応用範囲が広いのです。

しかし、もう一度このリストを読み返してみてください。

これらのテクノロジーを使っているのは誰でしょう?医師、生物学者、農家、都市計画家などです。

彼らはコンピュータやデータサイエンスに精通しているわけでも、大規模なネットワークを構築するのに必要な強力なハードウェアを持っているわけでもありません。

民主化されたモデルを通じて、彼らはデータサイエンスのバックグラウンドを必要としません。

無料または安価なクラウドトレーニングプロバイダーと事前学習されたネットワークにより、強力なハードウェアは必要ありません。

ということです。

また、「employee.net」は、「employee.net」の略で、「employee.net」は、「employee.net」の略で、「employee.net」は、「employee.net」の略で、「employee.net」は、「employee.net」の略です。

トランスファー・ラーニングの利点は、トレーニングを短縮することにとどまりません。

もしデータが少なければ、ネットワークは初期の段階でいくつかの区別を学習することができません。

あるデータセットで広範囲に学習させた後、別のデータセットに転送すると、多くの表現が既に学習されていることになります。

画像分類の定番と最先端モデル

多くのモデルが存在し、よく知られたデータセットでは、オンラインリポジトリや論文で公開されている何百ものよくできたモデルを見つけることができるでしょう。

ImageNetデータセットで学習したモデルの全体像は、PapersWithCodeで見ることができます。

その後、多くのDeep Learningフレームワークに移植された、よく知られた公開アーキテクチャをいくつか紹介します。

  • EfficientNet
  • SENet
  • InceptionとXception
  • ResNet
  • VGGNet

注:よく知られているからといって、そのアーキテクチャが最先端の性能を発揮するとは限りません。

例えば、VGGNetを転移学習に使うことはおそらくないでしょう。

なぜなら、より新しく、より堅牢で、より効率的なアーキテクチャが移植され、事前に学習されているからです。

PapersWithCodeのモデル一覧は常に更新されており、そこでの位置づけにこだわるべきではありません。

上位を占める新しいモデルの多くは、実は上記のリストに概説されているものをベースにしているのです。

残念ながら、最新モデルのいくつかはTensorflowやPyTorchなどのフレームワーク内に事前学習済みモデルとして移植されていませんが、チームは事前学習済みの重みで移植することにかなり精力的に取り組んでいるようです。

しかし、TensorflowやPyTorchのようなフレームワークでは、最新のモデルはプレトレーニングモデルとして移植されません。

Kerasによる転送学習 – 既存のモデルの適応

Kerasでは、事前に学習されたモデルが tensorflow.keras.applications モジュールの下で利用可能です。

各モデルは独自のサブモジュールとクラスを持っています。

モデルを読み込む際に、いくつかのオプション引数を設定することで、モデルの読み込み方法を制御することができます。

となります。

注意:Keras.ioで移植されたモデルを見つけることができますが、このリストには最新のモデルや実験的なモデルは含まれていません。

最新のリストはTensorFlowのDocsを参照してください。

例えば、weights 引数がある場合、どの事前学習された重みを使用するかを定義する。

省略した場合は、アーキテクチャ(未訓練のネットワーク)のみが読み込まれる。

データセット名を指定すると、そのデータセットに対して事前に学習されたネットワークが返される。

また、読み込みたい重みのファイルへのパスを指定することもできます(全く同じアーキテクチャであることが条件です)。

さらに、転送学習では最上位層を削除することが多いので、 include_top 引数を用いて最上位層を表示するかどうかを定義します。

import tensorflow.keras.applications as models


# 98 MB
resnet = models.resnet50.ResNet50(weights='imagenet', include_top=False)
# 528MB
vgg16 = models.vgg16.VGG16(weights='imagenet', include_top=False)
# 23MB
nnm = models.NASNetMobile(weights='imagenet', include_top=False)
# etc...


注意:事前に学習されたモデルを読み込んだことがない場合、インターネット接続を介してダウンロードされます。

これは、インターネットの速度やモデルのサイズによりますが、数秒から数分かかる場合があります。

学習済みモデルのサイズは、14MB(通常、モバイルモデルより小さい)から549MBまであります。

EfficientNetは、パフォーマンスとスケーラビリティに優れ、効率的なネットワークです。

学習可能なパラメータを減らすことを念頭に置いて作られているため、学習するためのパラメータは4Mしかありません。

4Mはまだ大きな数字ですが、例えばVGG19が139Mであることを考えると、これはとても大きな数字です。

EfficientNetファミリーの1つであるEfficientNetB0をロードしてみましょう。

effnet = keras.applications.EfficientNetB0(weights='imagenet', include_top=False)
effnet.summary()


この結果は

Model: "efficientnetb0"
__________________________________________________________________________________________________
 Layer (type)                   Output Shape         Param #     Connected to                     
==================================================================================================
 input_2 (InputLayer)           [(None, None, None,  0           []                               
                                 3)]                                                              

 rescaling_1 (Rescaling)        (None, None, None,   0           ['input_2[0][0]']                
                                3)                    

...
...


block7a_project_bn (BatchNorma  (None, None, None,   1280       ['block7a_project_conv[0][0]']   
 lization)                      320)                                                              

 top_conv (Conv2D)              (None, None, None,   409600      ['block7a_project_bn[0][0]']     
                                1280)                                                             

 top_bn (BatchNormalization)    (None, None, None,   5120        ['top_conv[0][0]']               
                                1280)                                                             

 top_activation (Activation)    (None, None, None,   0           ['top_bn[0][0]']                 
                                1280)                                                             

==================================================================================================
Total params: 4,049,571
Trainable params: 4,007,548
Non-trainable params: 42,023
__________________________________________________________________________________________________


一方、EfficientNetB0を先頭から読み込むと、末尾にImageNetのデータを分類するために学習された新しい層がいくつか追加されます。

これは、我々のアプリケーションのために、我々自身が学習するモデルのトップとなります。

effnet = keras.applications.EfficientNetB0(weights='imagenet', include_top=True)
effnet.summary()


これは最後のトップレイヤーで、最後にDense分類器を含んでいます。

Model: "efficientnetb0"
__________________________________________________________________________________________________
 Layer (type)                   Output Shape         Param #     Connected to                     
==================================================================================================
 input_1 (InputLayer)           [(None, 224, 224, 3  0           []                               
                                )]                                                                

 rescaling (Rescaling)          (None, 224, 224, 3)  0           ['input_1[0][0]']      

...
...


block7a_project_bn (BatchNorma  (None, 7, 7, 320)   1280        ['block7a_project_conv[0][0]']   
 lization)                                                                                        

 top_conv (Conv2D)              (None, 7, 7, 1280)   409600      ['block7a_project_bn[0][0]']     

 top_bn (BatchNormalization)    (None, 7, 7, 1280)   5120        ['top_conv[0][0]']               

 top_activation (Activation)    (None, 7, 7, 1280)   0           ['top_bn[0][0]']                 

 avg_pool (GlobalAveragePooling  (None, 1280)        0           ['top_activation[0][0]']         
 2D)                                                                                              

 top_dropout (Dropout)          (None, 1280)         0           ['avg_pool[0][0]']               

 predictions (Dense)            (None, 1000)         1281000     ['top_dropout[0][0]']            

==================================================================================================
Total params: 5,330,571
Trainable params: 5,288,548
Non-trainable params: 42,023
__________________________________________________________________________________________________


これらの名前は top_ で始まっており、最上位の分類器に属するというアノテーションがつけられています。

注:この構造は時代とともに変化する可能性があります。

Keras の以前のバージョンでは、 include_top 引数を False に設定すると top_conv, top_bn, top_activation がロードされませんでしたが、最新バージョンではロードされます(名前もまだ top_ というプレフィックスが付いているので少し紛らわしいですね。

オリジナルの実装に触発されたかどうかに関わらず、独自のモデルを定義する前に、常にモデルの「top」が何であるかを確認してください。

我々はEfficientNetモデルに独自のトップを追加し、トップに追加したものだけを再トレーニングするため、トップレイヤーを使用しません(畳み込みベースの微調整を行う前に)。

しかし、このアーキテクチャがもともと最上層に何を使っているかは注目すべき点です!彼らは、「Global Gene」を使っているようです。

最終的なDense分類層の前にGlobalAveragePooling2DDropoutを使用しているようです。

このアプローチに厳密に従う必要はないが(他のデータセットでは他のアプローチの方が良いと証明されるかもしれない)、オリジナルのトップがどのように見えたかを覚えておくことは合理的である。

事前学習済みモデルの前処理入力

注意: データの前処理はモデルの学習において重要な役割を果たし、ほとんどのモデルは異なる前処理パイプラインを持っています。

ここで推測を行う必要はありません。

該当する場合、モデルには独自の preprocess_input() 関数が付属しています。

この preprocess_input() 関数は、学習時に適用したのと同じ前処理を入力に適用します。

モデルが独自のモジュールに存在する場合、そのモデルのそれぞれのモジュールからこの関数をインポートすることができます。

例えば、ResNets は独自の preprocess_input 関数を持っています。

from keras.applications.resnet50 import preprocess_input


つまり、Kerasでモデルをロードし、入力の前処理を行い、結果を予測することは、以下のように簡単にできます。

import tensorflow.keras.applications as models
from keras.applications.resnet50 import preprocess_input


resnet50 = models.ResNet50(weights='imagenet', include_top=True)


img = # get data
img = preprocess_input(img)
pred = resnet50.predict(img)


となります。

注意:全てのモデルが専用の preprocess_input() 関数を持っているわけではありません、なぜなら前処理はモデル自身の中で行われるからです。

例えば、今回使用するEfficientNetは、モデル内の前処理レイヤーが処理を行うため、専用の前処理関数を持ちません。

以上です。

さて、配列 pred には人間が読めるようなデータは含まれていないので、モジュールから preprocess_input() 関数と一緒に decode_predictions() 関数をインポートすることも可能です。

あるいは、専用のモジュールを持たないモデルにも適用できる汎用的な decode_predictions() 関数をインポートすることもできます。

from keras.applications.model_name import preprocess_input, decode_predictions
# OR
from keras.applications.imagenet_utils import decode_predictions
# ...
print(decode_predictions(pred))


ここでは、urllib でツキノワグマの画像を取得し、EfficientNet で利用可能なサイズに保存し(入力層は (batch_size, 224, 224, 3) の形を想定)、学習済みのモデルで分類してみましょう。

from tensorflow import keras
from keras.applications.family_name import preprocess_input, decode_predictions
from tensorflow.keras.preprocessing import image


import urllib.request
import matplotlib.pyplot as plt
import numpy as np


# Public domain image
url = 'https://upload.wikimedia.org/wikipedia/commons/0/02/Black_bear_large.jpg'
urllib.request.urlretrieve(url, 'bear.jpg')


# Load image and resize (doesn't keep aspect ratio)
img = image.load_img('bear.jpg', target_size=(224, 224))
# Turn to array of shape (224, 224, 3)
img = image.img_to_array(img)
# Expand array into (1, 224, 224, 3)
img = np.expand_dims(img, 0)
# Preprocess for models that have specific preprocess_input() function
# img_preprocessed = preprocess_input(img)


# Load model and run prediction
effnet = keras.applications.EfficientNetB0(weights='imagenet', include_top=True)
pred = effnet.predict(img)
print(decode_predictions(pred))


しかし、我々は画像をURLから取得しました。

モバイルデバイス、REST APIコール、または他のソースから画像を取得し、それを分類することができます。

事前学習済みの分類器を使えば、それをインポートして画像を送り込み、結果をデコードするだけと、とても簡単です。

わずか数行のコードで、エンドユーザにコンピュータビジョンモデルを提供することができます。

[[
('n02133161', 'American_black_bear', 0.6024658),
('n02132136', 'brown_bear', 0.1457715),
('n02134418', 'sloth_bear', 0.09819221),
('n02510455', 'giant_panda', 0.0069221947),
('n02509815', 'lesser_panda', 0.005077324)
]]


この画像はアメリカクロクマの画像であることがかなり確実で、これは正しいです 前処理関数で前処理をすると、画像が大きく変化することがあります。

例えば、ResNetの前処理機能では、熊の毛色が変わってしまうのです。

かなり茶色くなりましたね。

この画像をEfficientNetに渡すと、ヒグマと認識されますね。

[[
('n02132136', 'brown_bear', 0.7152758), 
('n02133161', 'American_black_bear', 0.15667434), 
('n02134418', 'sloth_bear', 0.012813852), 
('n02134084', 'ice_bear', 0.0067828503), ('n02117135', 'hyena', 0.0050422684)
]]


重要なのは、モデル間で前処理機能を混在させないことです。

例えばResNetは、前処理で色が変わってしまったので、茶色と見えるものを黒と呼ぶことを学習し、「茶色」と呼ばれるものに「黒」というラベルを付けたものしか見たことがないのです。

しかし、クロクマとヒグマを分類するように訓練されており、色は間違いなく混ざっています。

これは良いことなのでしょうか、悪いことなのでしょうか?

誰に聞くかによる 史上最も影響力のある哲学者の一人であるジョン・ロックは、物の性質を一次的性質と二次的性質に分類し、両者を明確に区別しています。

一次的性質とは、観察者とは無関係に存在するものである。

私がどう見ようと、本は本であり、大きさがある。

それが一次品質だ。

二次的品質とは、観察者に依存するもの(色、味、匂い)などで、これはかなり主観的なものです。

幼い頃から、「私の黄色」と「あなたの黄色」は同じなのか、と自問自答してきた人は多いでしょう。

違う色に見えても、それを「黄色」と呼ぶように教えられていたかもしれません。

だからといって、黄色い本が本であることに変わりはありません。

本当かどうかは別にして、私たちは皆、世界を少しずつ違った見方で見ているということは考えられる。

特に、主観的な経験の源を説明するために、数値的なユビキタス値を割り当てることができる以上、それが私たちが世界を伝え、構築し、理解することを止める明確な理由はない。

これは「黄色」ではなく、約600nmの波長を持つ電磁波です。

目の中の赤と緑の受容体がそれに反応して、「黄色が見える」のです。

今は、色のような二次的な性質も、議論の余地のない性質として表現できるようになりました。

確かに、前処理を考える必要がない分、別の機能を持たせるよりも、生画像をモデルに与えて、モデルに前処理をさせる方が簡単です(EfficientNetがそうしているように)。

ただし、ResNetが「色を混ぜる」ことが客観的に良いとか悪いとかいうことではありません。

むしろ、知識の多様性が、美しいビジュアライゼーションにつながることもあるのです。

それがどのようなものかは、DeepDreamアルゴリズムを取り上げる別のレッスンで見てみましょう。

すごい モデルが動作しています。

では、新しいトップを追加して、ImageNetセット以外の分類を行うようにトップを再トレーニングしてみましょう。

事前学習済みモデルへの新しいTopの追加

転移学習を行う際、一般的にはtopのないモデルを読み込むか、手動でtopを削除することになります。

# Load without top
# When adding new layers, we also need to define the input_shape
effnet_base = keras.applications.EfficientNetB0(weights='imagenet', 
                                          include_top=False, 
                                          input_shape=((224, 224, 3)))


# Or load the full model
full_effnet = keras.applications.EfficientNetB0(weights='imagenet', 
                                            include_top=True, 
                                            input_shape=((224, 224, 3)))

# And then remove X layers from the top
trimmed_effnet = keras.Model(inputs=full_effnet.input, outputs=full_effnet.layers[-3].output)


ここでは、より便利な前者の方法をとります。

畳み込みブロックを微調整するかしないかによって、凍結するかしないかを決めます。

例えば、事前に学習した特徴マップを使用し、レイヤーを凍結して、一番上の新しい分類レイヤーだけを再学習させるとします。

effnet_base.trainable = False


モデルを繰り返し実行して、各レイヤーを trainable にするかしないかを設定する必要はありません。

もし、最初の n レイヤーをオフにして、より高いレベルの特徴マップを微調整できるようにしたいが、より低いレベルの特徴マップはそのままにしておきたい場合は、このようにすることができます。

for layer in effnet_base.layers[:-2]:
    layer.trainable = False


ここでは、ベースモデルの最後の2層を除いて、すべての層を学習不可能に設定しました。

このモデルを確認すると、学習可能なパラメータは2.5K程度になっています。

effnet_base.summary()


# ...                
=========================================================================================
Total params: 4,049,571
Trainable params: 2,560
Non-trainable params: 4,047,011
_________________________________________________________________________________________


次に、この effnet_base の上に載せる Sequential というモデルを定義します。

幸いなことに、Keras ではモデルを連結することは、新しいモデルを作って別のモデルの上に置くのと同じくらい簡単です。

Functional APIを活用して、モデルの上に新しいレイヤーをいくつか連鎖させるだけでいい。

ここでは、GlobalAveragePooling2Dレイヤー、Dropoutレイヤー、そしてDense Classificationレイヤーを追加してみましょう。

gap = keras.layers.GlobalAveragePooling2D()(effnet_base.output, training=False)
do = keras.layers.Dropout(0.2)(gap)
output = keras.layers.Dense(100, activation='softmax')(do)


new_model = keras.Model(inputs=effnet_base.input, outputs=output)


注意:EfficientNetの層を追加する際に、trainingFalseに設定します。

これはネットワークを学習モードではなく推論モードにするもので、先ほど False に設定した trainable とは別のパラメータとなります。

これは後でレイヤーのフリーズを解除する場合に重要なステップです。

BatchNormalizationは移動統計量を計算します。

凍結を解除すると、パラメータの更新を開始し、微調整の前に行われた学習を「元に戻す」ことになります。

TF 2.0 以降では、モデルの trainableFalse にすると、trainingFalse になりますが、これは BatchNormalization 層のみなので、TF 2.0 以降ではこのステップは不要ですが、念には念を入れておいたほうがよいでしょう。

また、Sequential APIを使用して、 add() メソッドを複数回呼び出すか、レイヤーのリストで渡すことも可能です。

new_model = keras.Sequential([
    effnet_base,
    keras.layers.GlobalAveragePooling2D(),
    keras.layers.Dropout(0.2),
    keras.layers.Dense(100, activation='softmax')
])


new_model.layers[0].trainable = False


これはモデル全体をレイヤーとして追加するので、1つのエンティティとして扱われます。

Layer: 0, Trainable: False # Entire EfficientNet model
Layer: 1, Trainable: True
Layer: 2, Trainable: True
...


モデルがシーケンシャルであれば、単純に次のように追加できます。

new_model = keras.Sequential()
new_model.add(base_network.output) # Add unwrapped layers
new_model.add(...layer)
...


しかし、これは非シーケンシャルなモデルに対しては失敗します。

Sequential APIは必要な柔軟性を提供しませんし、すべてのモデルがシーケンシャルであるとは限らないので、このような用途にはFunctional APIを使うことをお勧めします(実のところ、TF 2.4.0以降、事前学習済みのモデルはすべてFunctionalになっているのです)。

さらに、ベースネットワークを簡単に推論モードにすることができません – training 引数がありません。

EfficientNetのモデル全体がブラックボックス層であるという事実は、それを使って簡単に作業することを助けません。

したがって、Sequential APIのちょっとした便利さは、私たちにはあまりメリットがなく、いくつかの短所があります。

Moedelに戻ります。

CIFAR100クラスの出力ニューロンが100個あり、softmaxで活性化されています。

このネットワークで学習可能な層を見てみましょう。

for index, layer in enumerate(new_model.layers):
    print("Layer: {}, Trainable: {}".format(index, layer.trainable))


この結果は

Layer: 0, Trainable: False
Layer: 1, Trainable: False
Layer: 2, Trainable: False
...
Layer: 235, Trainable: False
Layer: 236, Trainable: False
Layer: 237, Trainable: True
Layer: 238, Trainable: True
Layer: 239, Trainable: True
Layer: 240, Trainable: True
Layer: 241, Trainable: True


すごい データセットをロードし、前処理を行い、分類レイヤーを再トレーニングしてみましょう。

前回のレッスンと同じCIFAR100データセットを使います。

これはCNNを訓練するのが難しいことがわかったからです。

データ不足とデータ増強の制限により、強力な分類器を作成することが困難でした。

TensorFlowデータセット

今回もCIFAR100データセットを扱います。

ただし、今回はKerasから素のNumPy配列としてロードするのではありません。

TensorFlowのデータセットを使います。

Kerasの datasets モジュールにはいくつかのデータセットが含まれていますが、これらは主にベンチマークや学習のためのもので、それ以上にはあまり役に立ちません。

tensorflow_datasets` を使えば、より多くのデータセットにアクセスすることができます。

さらに、このモジュールのデータセットはすべて標準化されているので、モデルをテストするデータセットごとに異なる前処理をする必要はない。

多くのモデルを学習させる場合、オーバーヘッド作業にかかる時間は煩わしいことこの上ない。

このライブラリは、MNISTからGoogle Open Images (11MB – 565GB)まで、いくつかのカテゴリにまたがるデータセットへのアクセスを提供します。

  • オーディオ
  • D4rl
  • グラフ
  • 画像
  • 画像分類
  • 物体検出
  • 質問応答
  • ランキング
  • Rlds
  • ロボミミック
  • ロボット工学
  • テキスト
  • 時系列
  • テキストの簡略化
  • 視覚言語
  • ビデオ
  • 翻訳
  • etc…

そして、リストはどんどん増えています! 2022年現在、278のデータセットが利用可能で、その名前は tfds.list_builders() で取得することができます。

さらに、TensorFlow Datasetsはコミュニティデータセットをサポートしており、700以上のHuggingfaceデータセットとKubricデータセットジェネレータがある。

一般的な知的システムを構築するのであれば、そこに公開データセットがある可能性は非常に高い。

それ以外の目的であれば、公開データセットをダウンロードし、カスタムの前処理ステップを経て作業することができます。

Kaggle、Huggingface、そしてアカデミックレポジトリがよく使われる選択肢です。

さらに、同様の取り組みとして、TensorFlowは素晴らしいGUIツール – Know Your Dataをリリースしました。

これはまだベータ版(執筆時点)で、データの破損(壊れた画像、悪いラベルなど)、データの感度(データに機密情報が含まれているか)、データのギャップ(明らかなサンプル不足)、データバランスなどに関する重要な質問に答えることを目的としています。

これらの多くは、偏りやデータの歪みを避けるのに役立ちます。

他の人間に影響を与える可能性のあるプロジェクトに取り組む場合、間違いなく最も重要なことの1つです。

もう一つの驚くべき機能は、TensorFlow Datasetsから来るデータセットが最適化されているということです。

データセットは tf.data.Dataset オブジェクトに格納され、事前フェッチ、自動最適化(TensorFlowの背後で)、データセット全体に対する簡単な変換などを通じて、ネットワークのパフォーマンスを最大化することができる。

注意:もし、Datasetのような独自のクラスが好きでないなら – フレームワークに依存しないように、単純なNumPyの配列に戻すことができます。

このモジュールは、以下の方法でインストールすることができます。

$ pip install tensorflow_datasets


一度インストールすると、利用可能なデータセットのリストにアクセスできるようになります。

print(tfds.list_builders())
print(f'Number of Datasets: {len(tfds.list_builders())}')


['abstract_reasoning', 'accentdb', 'aeslc', 'aflw2k3d', ...]
Number of Datasets: 278


ただし、このリストよりも、TensorFlow Datasets Web サイトの関連ページで、より詳細な情報やサンプル画像などを確認した方が良いだろう。

データセットを読み込むには、load()関数を使用することができる。

dataset, info = tfds.load("cifar100", as_supervised=True, with_info=True)
class_names = info.features["label"].names
n_classes = info.features["label"].num_classes
print('Class names:', class_names)
print('Num of classes:', n_classes)


データセットは教師なし、教師あり、またラベル名やクラス数などの追加情報の有無に関わらずインポートすることができます。

上のコードでは、"cifar100" を教師ありデータセット(ラベル付き)として読み込み、情報を与えています。

Class names: ['apple', 'aquarium_fish', 'baby', ...]
Num of classes: 100


このとき、info.features["label"].namesというリストが役に立ちます。

これは、データセットに含まれる数値ラベルに対応する、人間が読めるラベルのリストです。

TensorFlowデータセットによる訓練、テスト、検証の分割

load()関数に渡すことのできるオプション引数の一つにsplit引数があります。

新しい Split API では、データセットのどの分割を行うかを定義することができます。

デフォルトでは、このデータセットでは’train’‘test’のみの分割をサポートしています。

これがこのデータセットの「公式」な分割です。

Valid という分割はありません。

注:各データセットには「公式」の分割がある.train」だけの分割もあれば、「train」と「test」の分割、さらには「validation」の分割もある。

これは意図した分割であり、データセットが分割をサポートしている場合にのみ、その分割の文字列エイリアスを使用することができます。

もしデータセットが ‘train’ のみを含む場合は、問題なく学習データを train/test/valid に分割することができます。

これらは tfds.Split.TRAINtfds.Split.TEST 、そして tfds.Split.VALIDATION 列挙型に対応しており、以前のバージョンでは API を通じて公開されていました。

データセットを任意の数のセットにスライスすることができますが、通常は train_settest_setvalid_set の3つのセットを使用します。

(test_set, valid_set, train_set), info = tfds.load("cifar100", 
                                           split=["test", "train[0%:20%]", "train[20%:]"],
                                           as_supervised=True, with_info=True)


class_names = info.features["label"].names
n_classes = info.features["label"].num_classes
print(f'Class names: {class_names[:10]}...', ) # ['apple', 'aquarium_fish', 'baby', 'bear', 'beaver', 'bed', 'bee', 'beetle', 'bicycle', 'bottle']...
print('Num of classes:', n_classes) # Num of classes: 100


print("Train set size:", len(train_set)) # Train set size: 40000
print("Test set size:", len(test_set)) # Test set size: 10000
print("Valid set size:", len(valid_set)) # Valid set size: 10000


ここでは、'test' を分割して test_set に取り込みました。

train’split の 0 % から 20% までのスライスをvalid_setに、25 % 以降をtrain_set` に割り当てる。

これはセット自体のサイズによっても検証される。

パーセンテージの代わりに、絶対値やパーセンテージと絶対値の混在を使用することもできます。

# Absolute value split
test_set, valid_set, train_set = tfds.load("cifar100", 
                                           split=["test", "train[0:10000]", "train[10000:]"],
                                           as_supervised=True)


print("Train set size:", len(train_set)) # Train set size: 40000
print("Test set size:", len(test_set)) # Test set size: 10000
print("Valid set size:", len(valid_set)) # Valid set size: 10000


# Mixed notation split
# 5000 - 50% (25000) left unassigned
test_set, valid_set, train_set = tfds.load("cifar100", 
                                           split=["test[:2500]", # First 2500 of 'test' are assigned to `test_set`
                                           "train[0:10000]",    # 0-10000 of 'train' are assigned to `valid_set`
                                           "train[50%:]"],        # 50% - 100% of 'train' (25000) assigned to `train_set`
                                           as_supervised=True)


これはあまり一般的ではありませんが、集合がインターリーブされているためです。

train_and_test, half_of_train_and_test = tfds.load("cifar100", 
                                split=['train+test', 'train[:50%]+test'],
                                as_supervised=True)

print("Train+test: ", len(train_and_test))               # Train+test:  60000
print("Train[:50%]+test: ", len(half_of_train_and_test)) # Train[:50%]+test:  35000


これらの2つの集合は、現在、大きくインターリーブされています。

N個の集合に対する偶数分割

繰り返しますが、分割リストに分割を追加するだけで、任意の数の分割を作成することができます。

split=["train[:10%]", "train[10%:20%]", "train[20%:30%]", "train[30%:40%]", ...]


しかし、多くの分割を行う場合、特に偶数分割の場合は、 渡される文字列が非常に予測しやすくなります。

これは、文字列のリストを作成することで自動化することができます。

その際、文字列には等間隔 (たとえば 10%)を指定します。

まさにこの目的のために、 tfds.even_splits() 関数は、プレフィックス文字列と希望する分割数を与えて、文字列のリストを生成します。

import tensorflow_datasets as tfds


s1, s2, s3, s4, s5 = tfds.even_splits('train', n=5)
# Each of these elements is just a string
split_list = [s1, s2, s3, s4, s5]
print(f"Type: {type(s1)}, contents: '{s1}'")
# Type: <class 'str'="", contents: 'train[0%:20%]'


for split in split_list:
    test_set = tfds.load("cifar100", 
                                split=split,
                                as_supervised=True)
    print(f"Test set length for Split {split}: ", len(test_set))


この結果は

Test set length for Split train[0%:20%]: 10000
Test set length for Split train[20%:40%]: 10000
Test set length for Split train[40%:60%]: 10000
Test set length for Split train[60%:80%]: 10000
Test set length for Split train[80%:100%]: 10000


また、 split_list 全体を split 引数として渡すことで、ループの外側で複数の分割データセットを構築することもできます。

ts1, ts2, ts3, ts4, ts5 = tfds.load("cifar100", 
                                split=split_list,
                                as_supervised=True)


CIFAR100のロードとデータ補強

tfds`を理解した上で、CIFAR100のデータセットをロードしてみましょう。

import tensorflow_datasets as tfds
import tensorflow as tf


(test_set, valid_set, train_set), info = tfds.load("cifar100", 
                                           split=["test", "train[0%:20%]", "train[20%:]"],
                                           as_supervised=True, with_info=True)


class_names = info.features["label"].names
n_classes = info.features["label"].num_classes
print(f'Class names: {class_names[:10]}...', ) # ['apple', 'aquarium_fish', 'baby', 'bear', 'beaver', 'bed', 'bee', 'beetle', 'bicycle', 'bottle']...
print('Num of classes:', n_classes) # Num of classes: 100


print("Train set size:", len(train_set)) # Train set size: 40000
print("Test set size:", len(test_set)) # Test set size: 10000
print("Valid set size:", len(valid_set)) # Valid set size: 10000


コンフィグ辞書にある、いくつかの関連する変数をメモしておきましょう。

config = {
    'TRAIN_SIZE' : len(train_set),
    'BATCH_SIZE' : 32
}


さて、CIFAR100の画像はImageNetの画像と大きく異なっています。

つまり、CIFAR100の画像は32×32であるのに対し、EfficientNetのモデルは224×224の画像を想定しているため、CIFAR100の画像のサイズを変更する必要があります。

いずれにせよ、画像のリサイズは必要でしょう。

また、データセットに十分なサンプルがないため、クラスごとのサンプルサイズを人為的に拡大するために、重複する画像にいくつかの変換関数を適用したいと思うかもしれません。

ImageDataGeneratorでは、拡張に関して非常に緩やかな自由度を持っており、その処理は高度に自動化されていることがわかりました。

TensorFlow Datasetsを扱う場合、それらが提供する最適化を少しでも利用するために、一般的にはpreprocess_image()関数で画像の変換や回転などのためにtf.image` 操作を使用することになるでしょう。

専用の preprocess_image() 関数の代わりに、ラムダ関数で複数の map() 呼び出しを連結することもできますが、この方法は可読性が著しく低下するため、より多くの処理を行う場合には推奨されません。

欠点は – tf.image がかなり初歩的であることです。

Kerasの豊富な演算とは異なり、ランダム拡張に使えるものは驚くほど少なく、自由度も低いです。

この記事を書いている時点では、まだ初期の段階なので、データ拡張の理想的なアプローチとは言えませんが、何もしないよりはましです。

代わりに生のNumPyシーケンスで ImageDataGenerator を使うこともできますが、これはそれらを変換する必要があり、いくつかの最適化を失ってしまうことを意味します。

注意: モデルをより前処理に依存しないものにするために、keras.lays.RandomFlip()keras.layers.RandomRotation(0.2) のような前処理レイヤーをモデルに組み込むことも良い方法でしょう。

各画像とそれに付随するラベルに対して、前処理関数を定義してみましょう。

def preprocess_image(image, label):
    resized_image = tf.image.resize(image, [224, 224])
    img = tf.image.random_flip_left_right(resized_image)
    img = tf.image.random_brightness(img, 0.4)
    # Preprocess image with model-specific function if it has one
    # processed_image = preprocess_input(resized_image)
    return img, label


さらに、検証用とテスト用のセットにはランダムな変換を施したくないので、これらには別の関数を定義しましょう。

def preprocess_test_valid(image, label):
    resized_image = tf.image.resize(image, [224, 224])
    # Preprocess image with model-specific function if it has one
    # processed_image = preprocess_input(resized_image)
    return resized_image, label


そして最後に、この関数をセット内の各画像に適用したいと思います。

これは map() 関数で簡単に行うことができます。

ネットワークへの入力はバッチ((224, 224, 3)の代わりに (batch_size, 224, 224, 3))も期待できるので、マッピング後にデータセットを batch() もします。

train_set = train_set.map(preprocess_image).batch(32).repeat().prefetch(tf.data.AUTOTUNE)
test_set = test_set.map(preprocess_test_valid).batch(32).prefetch(tf.data.AUTOTUNE)
valid_set = valid_set.map(preprocess_test_valid).batch(32).prefetch(tf.data.AUTOTUNE)


この例では、データパイプラインを作成し、その利用を最適化するための組み込みモジュールである tf.data を使っています。

このモジュールは tfds と混同しないように注意してください。

このモジュールはデータセットを取得するための単なるライブラリであり、tf.data はハードウェア上で重い処理を行うものです。

プリフェッチ()関数はオプションですが、効率化に役立ちます。

また、tf.data.AUTOTUNEの呼び出しにより、TensorFlowがプリフェッチの実行方法を最適化することができます。

モデルは1つのバッチで学習するので、prefetch()関数は次のバッチをプリフェッチし、学習ステップが終了したときに待たされることがないようにします。

同様に、cache()interleave()といった関数を使って、IOやデータ抽出をさらに最適化することができますが、これらはやみくもに使うべきではありません。

しかし、これらはやみくもに使ってはいけません。

不適切な場所やタイミングで使うと、パイプラインを遅くしてしまう可能性があるからです。

データパイプラインの最適化については、後日、レッスンを行う予定です。

今のところ、prefetch()`だけにしておきます。

他のセットにはない train_setrepeat() コールがあります。

これは ImageDataGenerator クラスに類似しており、ランダムな変換を施したトレーニングサンプルを無限に生成することができます。

各リクエストで、私たちが書いた preprocess_image() 関数は入力された画像をランダムに変換するので、わずかに変化したデータの新鮮で安定したストリームを持つことができます。

テストセットと検証セットでは、画像を同じサイズにすることと、共通の前処理がある場合はそれを適用すること以外は行いません(EfficientNetB0は外部の前処理関数を持ちません)。

注:テストタイム・オーグメンテーションもある。

それについては後で説明します。

早速、いずれかのセットから画像を見てみましょう。

fig = plt.figure(figsize=(10, 10))


i = 1
for entry in test_set.take(25):


sample_image = np.squeeze(entry[0].numpy()[0])
    sample_label = class_names[entry[1].numpy()[0]]
    ax = fig.add_subplot(5, 5, i)

    ax.imshow(np.array(sample_image, np.int32))
    ax.set_title(f"Class: {sample_label}")
    ax.axis('off')
    i = i+1


plt.tight_layout()
plt.show()


転送学習を用いたモデルの学習

データがロードされ、前処理が行われ、適切なセットに分割されたので、いよいよモデルを学習させることができます。

スパース分類を行うので、sparse_categorical_crossentropy損失がうまく機能するはずです。

また、Adamオプティマイザは妥当なデフォルトのオプティマイザです。

モデルをコンパイルし、いくつかのエポックについて学習してみましょう。

このとき、ネットワークのほとんどの層が凍結されていることを思い出してください。

我々は、抽出された特徴マップの上で新しい分類器を学習しているに過ぎないのです。

トップレイヤーを学習した後、特徴抽出レイヤーのフリーズを解除し、もう少し微調整を行うことができます。

このステップは任意ですが、通常、モデルのベストを引き出すことができます。

しかし、ネットワーク全体を学習するのに多くの時間がかかるので、それらがかなり大きい場合は注意が必要です。

経験則としては、データセットを比較して、どのレベルの階層を再トレーニングせずに再利用できるかを推測し、再トレーニングが冗長になる可能性のあるレベルの再トレーニングを避けるのがよいでしょう。

もし本当に違っていたら、おそらく間違ったデータセットで事前に訓練されたネットワークを選んだのだろう。

動物の分類にPlaces365(人工物)の特徴抽出を使うのは効率的ではないでしょう。

しかし、ImageNet(様々な物体、動物、植物、人間を含む)で学習したネットワークを、CIFAR100のような比較的似たカテゴリを持つ別のデータセットに使用するのは理にかなっていると思います。

実際、ImageNetの重みが、たとえドメインとの遠隔のつながりがないように見えるデータセットであっても、ほとんどのデータセットにうまく転送されるのは驚くべきことです。

特に、組織画像から乳がんを分類する次のガイド付きプロジェクトで、このことが分かると思います。

注意:使用しているアーキテクチャによっては、すべてのレイヤーをアンフリーズすると、メモリ不足の例外が発生することがあります。

可能であれば、特徴抽出器を全て変更しなくても済むような、十分に類似したデータセットで事前にトレーニングされたアーキテクチャを見つけるようにしましょう。

では、ネットワークをコンパイルして、その構造をチェックしてみましょう。

checkpoint = keras.callbacks.ModelCheckpoint(filepath='effnet_transfer_learning.h5', save_best_only=True)


new_model.compile(loss="sparse_categorical_crossentropy", 
                  optimizer=keras.optimizers.Adam(), 
                  metrics=["accuracy", keras.metrics.SparseTopKCategoricalAccuracy(k=3)])


new_model.summary()


これはレイヤーを正しくフリーズさせたかどうかを検証する絶好の機会である。

...
==================================================================================================
Total params: 4,177,671
Trainable params: 128,100
Non-trainable params: 4,049,571
__________________________________________________________________________________________________


学習可能なパラメータはたったの128k! データのロード,オーグメンテーション,すべてのフィルタへの通過,畳み込みの実行など,より多くのことが行われるため,当然ながら128kのMLPよりも学習時間がかかりますが,ネットワーク全体を学習するよりは大幅に短縮されます.それでは,新しいネットワーク(実際には上部のみ)を10エポック学習させてみましょう。

history = new_model.fit(train_set, 
                        epochs=10,
                        steps_per_epoch = config['TRAIN_SIZE']/config['BATCH_SIZE'],
                        callbacks=[checkpoint],
                        validation_data=valid_set)


train_setは無限なので、steps_per_epoch` を定義する必要があります。

これは時間がかかるので、GPU で行うのが理想的です。

モデルの大きさや、投入されるデータセットにもよりますが。

GPUを利用できない場合は、Google CollabやKaggle Notebooksなど、無料のGPUにアクセスできるクラウドプロバイダーでこのコードを実行することをお勧めします。

各エポックは、強力なGPUでは60秒から、弱いGPUでは10分かかることがあります。

この時点で、腰を落ち着けてコーヒー(または紅茶)を飲みに行くことになります。

10エポック後、学習と検証の精度は良い感じです。

Epoch 1/10
1250/1250[==============================] - 97s 76ms/step - loss: 1.9179 - accuracy: 0.5196 - sparse_top_k_categorical_accuracy: 0.7216 - val_loss: 1.3436 - val_accuracy: 0.6324 - val_sparse_top_k_categorical_accuracy: 0.8225
...
Epoch 10/10
1250/1250[==============================] - 86s 74ms/step - loss: 0.8610 - accuracy: 0.7481 - sparse_top_k_categorical_accuracy: 0.9015 - val_loss: 1.0820 - val_accuracy: 0.6935 - val_sparse_top_k_categorical_accuracy: 0.8651


検証精度は69%、トップ3検証精度は86%です。

しかし、これらはネットワークの潜在能力からは程遠いものです。

分類トップはおそらく、現状の特徴抽出器でできる限りのことをしたのでしょう。

では、学習曲線を見てみましょう。

微調整の前の評価

すべてのレイヤーの凍結を解除する前に、まずこのモデルをテストしてみましょう。

メトリクス、学習曲線、混乱マトリックスなど、基本的な評価を行います。

まずはメトリクスから。

new_model.evaluate(test_set)
# 157/157 [==============================] - 10s 65ms/step - loss: 1.0806 - accuracy: 0.6884 - sparse_top_k_categorical_accuracy: 0.8718


テストセットで ~69% 、検証セットでほぼ同じ精度を達成しました。

トップ3精度は87%とかなり優秀です。

我々のモデルはうまく汎化しているように見えますが、まだ改善の余地があります。

学習曲線を見てみましょう。

学習曲線は予想通りです。

10エポックしか学習していないのでかなり短いですが、すぐにプラトー化しているので、おそらくもっとエポックを増やしてもそれほど良いパフォーマンスは得られなかったと思います。

検証の損失と精度は学習の損失と精度と一緒に踊るので、このままさらに学習させると、モデルをオーバーフィットさせてしまうでしょう。

エポック11では振動が起こり、精度が上がる可能性がありますが、その可能性はあまり高くないので、そのチャンスを逃すことにします。

テスト集合を予測し、その集合からラベルを抽出して、分類レポートと混同行列を作成してみましょう。

y_pred = new_model.predict(test_set)
labels = tf.concat([y for x, y in test_set], axis=0)


クラスが100個あるので、分類レポートと混同行列は非常に大きくなり、ほとんど読めなくなります。

from sklearn import metrics
print(metrics.classification_report(labels, np.argmax(y_pred, axis=1)))


      precision    recall  f1-score   support


0       0.89      0.89      0.89        55
   1       0.76      0.78      0.77        49
   2       0.45      0.64      0.53        45
   3       0.45      0.58      0.50        52
...


テストセットには1クラスあたり50枚程度の画像しかありませんが、それ以上の画像を得ることはできません。

あるクラスが他のクラスよりもよく学習されていることは明らかで、例えば0はクラス3よりも有意に高いリコールと精度を有しています。

クラス 0apple で、クラス 3bear なので、これは実は驚くべきことなのです!ImageNet には熊の画像があります。

ImageNetには熊の画像があり、さらに異なる種類の熊を分類しているので、ネットワークがImageNetから知識を伝達して熊にうまく汎化することが期待されます。

どちらかというと、画像が小さいので、このネットワークがいかに効果的に「処方箋」を持っているかを物語っています。

混同行列をプロットしてみよう。

from sklearn.metrics import confusion_matrix
import seaborn as sns


matrix = confusion_matrix(labels, y_pred.argmax(axis=1))


# Plot on heatmap
fig, ax = plt.subplots(figsize=(15, 15))
sns.heatmap(matrix, ax=ax, fmt='g')


# Stylize heatmap
ax.set_xlabel('Predicted labels')
ax.set_ylabel('True labels')
ax.set_title('Confusion Matrix')


# Set ticks
ax.xaxis.set_ticks(np.arange(0, 100, 1))
ax.yaxis.set_ticks(np.arange(0, 100, 1))
ax.xaxis.set_ticklabels(class_names, rotation=90, fontsize=8)
ax.yaxis.set_ticklabels(class_names, rotation=0, fontsize=8)


この結果、以下のようになる。

繰り返しになりますが、100クラスあるので、混同行列はかなり大きくなります。

しかし、ほとんどの場合、理想的ではないものの、実際にはうまくクラスに対して汎化しているように見えます。

このネットワークをさらに微調整することは可能でしょうか?特徴マップの分類に関わるトップレイヤーを入れ替えて再トレーニングしていますが、特徴マップ自体が理想的でないかもしれませんね かなり良いのですが、この画像はImageNetとは単純に異なるので、特徴抽出層も時間をかけて更新する価値があります。

畳み込み層の凍結を解除し、微調整も行ってみよう。

層が凍らないようにする – 移転学習で学習させたネットワークの微調整

トップレイヤーの再トレーニングが終わったら、契約を終了し、自分のモデルに満足することができます。

例えば、95%の精度が得られたとしましょう – これ以上は必要ありません。

精度をさらに1%上げることができれば、大したことはないと思うかもしれませんが、その逆のケースも考えてみてください。

あなたのモデルが100サンプルで95%の精度を持つ場合、5サンプルを誤って分類しています。

それを96%の精度に上げると、4つのサンプルを誤分類してしまいます。

精度が1%上がると、誤判定が25%減ることになります。

モデルからさらに絞り出すことができるものは何でも、実際には誤った分類の数に大きな違いをもたらすことができます。

繰り返しになりますが、CIFAR100の画像はImageNetの画像よりもずっと小さく、まるで視力の良い人が突然大きな処方箋を得て、ぼやけた目でしか世界を見れなくなったようなものです。

そして、読み込んだモデルの凍結を解除し、微調整を行い、元のモデルの重みを誤って台無しにしてしまわないようにしましょう。

new_model.save('effnet_transfer_learning.h5')
loaded_model = keras.models.load_model('effnet_transfer_learning.h5')


これで、new_modelに影響を与えることなく loaded_model を変更できるようになりました。

まず始めに、loaded_model を推論モードから学習モードに戻します。

つまり、レイヤーのフリーズを解除して、再び学習可能にします。

注意: ネットワークが BatchNormalization を使用している場合(ほとんどの場合そうです)、ネットワークの微調整を行う際にレイヤーを凍結したままにしておくとよいでしょう。

ベースとなるネットワーク全体をフリーズさせることはもうしないので、代わりに BatchNormalization のレイヤーだけをフリーズさせて、他のレイヤーは変更できるようにしましょう。

BatchNormalization` 層をオフにして、トレーニングが無駄にならないようにしましょう。

for layer in loaded_model.layers:
    if isinstance(layer, keras.layers.BatchNormalization):
        layer.trainable = False
    else:
        layer.trainable = True


for index, layer in enumerate(loaded_model.layers):
    print("Layer: {}, Trainable: {}".format(index, layer.trainable))


うまくいったかどうか確認してみましょう。

Layer: 0, Trainable: True
Layer: 1, Trainable: True
Layer: 2, Trainable: True
Layer: 3, Trainable: True
Layer: 4, Trainable: True
Layer: 5, Trainable: False
Layer: 6, Trainable: True
Layer: 7, Trainable: True
Layer: 8, Trainable: False
...


すごい このモデルで何かをする前に、学習能力を「強化」するために、モデルを再コンパイルする必要があります。

今回は、ネットワークを学習させるのではなく、すでにあるものを微調整するだけなので、learning_rateを小さくします。

checkpoint = keras.callbacks.ModelCheckpoint(filepath='effnet_transfer_learning_finetuned.h5', save_best_only=True)


# Recompile after turning to trainable
loaded_model.compile(loss="sparse_categorical_crossentropy", 
                  optimizer=keras.optimizers.Adam(learning_rate=3e-6, decay=(1e-6)), 
                  metrics=["accuracy", keras.metrics.SparseTopKCategoricalAccuracy(k=3)])


history = loaded_model.fit(train_set, 
                        epochs=15,
                        steps_per_epoch = config['TRAIN_SIZE']/config['BATCH_SIZE'],
                        callbacks=[checkpoint],
                        validation_data=valid_set)


繰り返しますが、この作業には時間がかかります。

この作業をバックグラウンドで行っている間、好きな飲み物を飲んでください。

微調整にかかる時間は、選択したアーキテクチャに大きく依存しますが、ほとんどの最先端のアーキテクチャは、家庭用セットアップでいくつかの時間がかかります。

一旦終了すると、検証セットで約80%の精度と約93%のトップ3精度に達するはずです。

Epoch 1/15
1250/1250[==============================] - 384s 322ms/step - loss: 0.6567 - accuracy: 0.8024 - sparse_top_k_categorical_accuracy: 0.9356 - val_loss: 0.8687 - val_accuracy: 0.7520 - val_sparse_top_k_categorical_accuracy: 0.9069
...
Epoch 15/15
1250/1250[==============================] - 377s 322ms/step - loss: 0.3858 - accuracy: 0.8790 - sparse_top_k_categorical_accuracy: 0.9715 - val_loss: 0.7071 - val_accuracy: 0.7971 - val_sparse_top_k_categorical_accuracy: 0.9331


また、学習曲線を見てみると、プラトーしていないように見えるので、もっと長く学習させれば、さらにモデルの性能を上げることができたかもしれません。

注:おそらく、さらに訓練を重ねれば、さらなる性能向上が見込めたと思われます。

なお、長時間のトレーニングには当然ながら時間がかかる。

他の多くのアーキテクチャやデータセットと比較すると比較的少ないですが、このデータセットで100エポックを学習する場合、家庭用GPUで10時間以上かかりました。

これだけの時間を待つことに不安を覚えるのは理解できますが、残念ながら、10時間はネットワークの学習を待つには長すぎる時間ですらありません。

それでは、評価と予測値の可視化をしてみましょう。

loaded_model.evaluate(test_set)
# 157/157 [==============================] - 10s 61ms/step - loss: 0.7041 - accuracy: 0.7920 - sparse_top_k_categorical_accuracy: 0.9336


fig = plt.figure(figsize=(10, 10))


i = 1
for entry in test_set.take(25):
    # Predict, get the raw Numpy prediction probabilities
    # Reshape entry to the model's expected input shape
    pred = np.argmax(loaded_model.predict(entry[0].numpy()[0].reshape(1, 224, 224, 3)))


# Get sample image as numpy array
    sample_image = entry[0].numpy()[0]
    # Get associated label
    sample_label = class_names[entry[1].numpy()[0]]
    # Get human label based on the prediction
    prediction_label = class_names[pred]
    ax = fig.add_subplot(5, 5, i)
    # Plot image and sample_label alongside prediction_label
    ax.imshow(np.array(sample_image, np.int32))
    ax.set_title(f"Actual: {sample_label}
Pred: {prediction_label}")
    ax.axis('off')
    i = i+1


plt.tight_layout()
plt.show()


80%の精度のモデルに期待されるように、いくつかの誤分類があります。

アライグマはトガリネズミに分類されましたが、これはモグラに似た動物です(真実からそれほど離れてはいません)。

チンパンジーはランプに分類された(私ならビール瓶に分類する)。

バスはピックアップトラックに分類された。

バスの青いストライプがピックアップトラックのように見えるからです。

これは、モデルが青いストライプをピックアップトラックの荷台の端と理解し、グレーの幌をバスの一部と認識しなかったようです。

最後に、クモはマスに分類されましたが、これは全く違うクラスですが、画像がぼやけて小さくなっているので、全く理解できます。

このデータセットで構築され、訓練された我々の以前のカスタムモデルは、トップ1の精度が66%で、エラーレートが39%減少したことになります(100画像あたり33枚から20枚に減少)。

もし精度のために時間的なパフォーマンスを犠牲にし、より多くのデータ増強オプションを提供する(モデルをより堅牢にする) ImageDataGenerator クラスを使用していたら、このモデルでより良い精度が見られた可能性が非常に高いでしょう。

もしTop-K予測(最も確率の高いものだけでなく)を得たい場合は、argmax()を使う代わりにTensorFlowのtop_kメソッドを利用することができます。

pred = loaded_model.predict(np.expand_dims(img, 0))
top_probas, top_indices = tf.nn.top_k(pred, k=k)


print(top_probas)  # tf.Tensor([[0.900319   0.07157221 0.00889194]], shape=(1, 3), dtype=float32)
print(top_indices) # tf.Tensor([[66 88 21]], shape=(1, 3), dtype=int32)


この情報を入力と予測の横に表示したい場合は、入力画像の横にネットワークの信頼度を表す棒グラフをプロットすることができます。

for entry in test_set.take(1):
    img = entry[0][0].numpy().astype('int')
    label = entry[1][1]

    # Predict and get top-k classes
    pred = loaded_model.predict(np.expand_dims(img, 0))
    top_probas, top_indices = tf.nn.top_k(pred, k=3)
    # Convert to NumPy, squeeze and convert to list for ease of plotting
    top_probas = top_probas.numpy().squeeze().tolist()
    # Turn indices into classes
    pred_classes = [] 
    for index in top_indices.numpy().squeeze():
        pred_classes.append(class_names[index])

    fig, ax = plt.subplots(1, 2, figsize=(16, 4))
    ax[0].imshow(img)
    ax[0].axis('off')
    ax[1].bar(pred_classes, top_probas)

plt.tight_layout()


ここでは、ネットワークはこの画像がアライグマの画像であることをかなり確信している。

虎やチンパンジーの画像も少しありますが、確率は本当に低いです。

なんと

結論

転移学習とは、あるモデルから別のモデルへ、既に学習した知識表現を適用可能な場合に転移させるプロセスである。

以上で、KerasとTensorflowを使った画像分類のための転移学習に関するレッスンを終了します。

まず、転移学習とは何か、そしてモデルやアーキテクチャ間でどのように知識表現を共有することができるかを見てきました。

そして、公開されている画像分類のための最も人気のある最先端のモデルをいくつか見て、そのうちの1つである EfficientNet を使って、私たち自身のデータの分類に役立てることにしました。

このレッスンでは、学習済みのモデルをロードして調べる方法、レイヤーを使って作業する方法、レイヤーを使って予測する方法、結果をデコードする方法、そして独自のレイヤーを定義して既存のアーキテクチャと絡める方法について見てきました。

このレッスンでは、TensorFlow Datasets、このモジュールを使用する利点、およびそれを使用した作業の基本を紹介しました。

最後に、データセットをロードして前処理を行い、その上で新しい分類トップレイヤーをトレーニングし、レイヤーの凍結を解除して、さらにいくつかの追加エポックを通じて微調整を行いました。

</class

タイトルとURLをコピーしました