20210915のTensorFlowに関する記事は1件です。

[Tensorflow] VGG16をファインチューニングして画像分類する手順 (画像データ拡張~画像分類)

概要 入力画像数の拡張から、VGG16のファインチューニングモデルを用いた画像の二値分類までの手順を記載します。 画像拡張やVGG16モデルを用いた画像分類を行う方法は様々かと思いますが、一つの方法として参考にしていただけたらと思います。 実行環境 Windows 10 Pro(64bit) Anaconda 4.10.3 JupyterNotebook 6.1.5 Python 3.7.9 tensorflow 2.3.0 VGG16モデルについて 2014年 ILSVRC (大規模画像認識の競技会) で準優勝をしたディープラーニングのモデルで、16層からなる畳み込みニューラルネットワーク。 特徴 小さいカーネルを持つ畳み込み層を2〜4つ連続して重ね、 それをプーリング層でサイズを半分にするというのを繰り返し行う構造が特徴です。 大きいカーネルで画像を一気に畳み込むよりも、小さいカーネルを用いて何個も畳み込み層を深くする方が特徴をより良く抽出できると言われています。 また、シンプルなため使いやすいとも言われています。 事前学習済みのVGG16ネットワークを読み込みできるので、 このネットワークを活用して画像分類を行うコードを記載していきます。 STEP1.画像の水増し 概要 2クラスそれぞれのフォルダに入った画像を、Data Augumentationにより100枚ずつに拡張します。 前提 分類の対象となる画像が各クラスのフォルダに20~30枚程度入っているとします。 今回は二値問題のため、2つのフォルダに各画像(jpg画像)が入っている状態になります。 コード ①必要なコードをインポートします。 import import os import re import numpy as np import glob from tensorflow.keras.preprocessing.image import load_img, img_to_array, ImageDataGenerator from distutils.dir_util import copy_tree ②拡張した画像を出力する場所へ移動します。 ディレクトリ移動 os.chdir(“移動先のパス”) ③元画像のフォルダ名を指定します。 元画像フォルダ input_dir = "Normal" ④ 出力フォルダ指定します。 出力先フォルダ out_dir = input_dir + "_augumented" ⑤ 出力ファイル数を指定します。 出力ファイル数 # フォルダ内に作りたい合計ファイル数 out_file_count = 100 ⑥ フォルダのファイル数を確認する関数は以下のとおりです。 (⑦の画像拡張の関数と連携することで、指定枚数まで画像を増やすことができます。) ファイル数カウント def count_file(dir): # ファイル数を初期化 file_count = 0 # 指定フォルダ内のファイル・フォルダ数回繰り返し for file_name in os.listdir(dir): # 指定フォルダ内のファイルのパスを取得 file_path = os.path.join(dir,file_name) # 指定フォルダ内にファイル有無を判定 if os.path.isfile(file_path): # ファイル数をカウント file_count += 1 # ファイル数を返す return file_count ⑦ ImageDataGeneratorクラスのflowメソッドを使用し、画像を水増しして保存する関数は以下のとおりです。 拡張関数 # 100枚に達するまで入力フォルダ内の画像を拡張する関数 def data_augumentation(input_dir, out_dir, out_file_count): # 入力フォルダ内のファイル名を取得 files = glob.glob(input_dir + '/*.jpg') # 入力ファイル数確認 (拡張数の計算に使用) in_file_count = count_file(input_dir) # 出力フォルダがない場合は作成する if not os.path.exists(out_dir): os.mkdir(out_dir) # 入力フォルダの画像を出力フォルダにコピー copy_tree(input_dir,out_dir) # 画像を1枚ずつ処理 for i, file in enumerate(files): # RGB画像(PIL形式)を読み込む img = load_img(file) x = img_to_array(img) # numpy配列に変換する x = x[np.newaxis] # 入力用に3次元→4次元に変換 # ImageDataGeneratorで画像を増やす params = { ‘rotation_range’: 80, # 回転角度(degree) ‘width_shift_range’: 0.4, # 水平移動(rate) ‘height_shift_range’: 0.4, # 垂直移動(rate) ‘horizontal_flip’: True, # 水平フリップ ‘vertical_flip’: True, # 垂直フリップ ‘zoom_range’:0.2, # 拡大(rate) ‘shear_range’:0.2, # シアー(rate)[せん断,平行四辺形] ‘fill_mode’:“nearest” # 変形により空いたスペース(境界周り)のピクセルをどう埋めるか } # インスタンス作成、パラメータは上記paramsから読み込む # paramsは辞書形式のため、読み込む際に"*"を2つ付ける。 datagen = ImageDataGenerator(**params) # flowメソッドで拡張画像を出力するイテレータをgenに入れる # 出力ファイル名の接頭辞に、入力画像フォルダ名の上部(アルファベット)を付加し、jpg形式で出力するよう設定 gen = datagen.flow(x, batch_size=1, save_to_dir=out_dir, save_prefix=re.search("[a-zA-Z]+",input_dir).group(), save_format='jpg') # 繰り返し回数 = (出力したいファイル数 / 現在の入力フォルダ内のファイル数 -1[元画像の分を差し引く]) for j in range(out_file_count // in_file_count -1): # nextメソッドでイテレータから1枚ずつ出力 batch = gen.next() # 繰り返し回数が入力画像の枚数に達してから、不足分を水増しする if i+1 == in_file_count: # 出力ファイル数確認 cur_file_count = count_file(out_dir) # 出力したい枚数に達していない場合、最後の入力画像から不足分を水増しする。 if out_file_count > cur_file_count: # 出力枚数が計算より1~5枚少ないことがあるため、余分に20枚見ておく。 for k in range((out_file_count % in_file_count)+20): batch = gen.next() cur_file_count = count_file(out_dir) # 出力したい枚数に達したらforループから抜けるようにする。 if cur_file_count >= out_file_count: break # 上記の関数を実行して、画像を拡張する。 data_augumentation(input_dir, out_dir, out_file_count) ※上記の関数は、指定した単一フォルダの画像を拡張しますので、各フォルダについて実行する必要があります。 (二値問題の場合は、入力・出力フォルダを指定し2回実行) ImageDataGeneratorのパラメータについて ImageDataGeneratorのパラメータには以下のようなものがあります。 rotation_range: 画像を回転させる角度の範囲 (度) width_shift_range: 画像を水平移動させる範囲 height_shift_range: 画像を垂直移動させる範囲 horizontal_flip: 画像を水平フリップさせるかどうか vertical_flip: 画像を垂直フリップさせるかどうか zoom_range: 拡大・縮小させる範囲 shear_range: シアーの強さ[せん断,平行四辺形]の範囲 (度)、変形する角度を指定する fill_mode: 変形により空いたスペース(境界周り)のピクセルをどう埋めるかを指定する。 デフォルトは"nearest" fill_modeの各オプションについて constant 空いたスペースを一色で埋める(デフォルトは黒) nearest 隣接したピクセルと同じ色で埋める reflect 鏡のように反射した画像で埋める/dd> wrap 同じ画像を複数繋いだような繰り返しで埋める/dd> ※ 上記以外にもパラメータはありますので、気になった方はこちらの公式HPをご参照下さい。 Flow メソッドの引数について x : 入力画像(4次元のnumpy array) batch_size : データのバッチサイズ(1枚) save_to_dir : 保存先フォルダ save_prefix : 出力ファイル名の接頭辞 save_format : 出力ファイル形式 出力ファイル名そのものは自動で決められますが、接頭辞とファイル形式は指定できます。 ※ 引数は他にもありますので、気になった方はこちらの公式HPをご参照下さい。 STEP2.データセット作成 概要 各フォルダの各画像にVGG16に合わせた前処理を実施し、One-Hotエンコーディング形式のラベルを付加します。 コード ① 必要なコードをインポートします。 import import matplotlib.pyplot as plt import os import cv2 import random import numpy as np from tensorflow.keras.utils import to_categorical from tensorflow.keras.applications.vgg16 import preprocess_input ② 入力元の画像フォルダがある場所へ移動します。 ディレクトリ移動 os.chdir(“移動先のパス”) ③ 入力元の画像フォルダNormal,Tumorをリスト形式で指定します。 クラス指定リスト categories = [“Tumor","Normal"] ④ 入力元フォルダのリストの要素数を指定します。ここでは「2」が入ることになります。 クラス数 category_size = len(categories) ⑤ データセット作成用の関数は以下のとおりです。 データセット作成 def create_data_set(): data_set = [] # データセット用の配列を用意 for class_num, category in enumerate(categories): # categoriesの要素数だけ繰り返し path = os.path.join(PATH, category) # 入力元フォルダのパス for image_name in os.listdir(path): # フォルダ内の画像枚数だけ繰り返し img_pil = load_img(os.path.join(path, image_name)) # 画像読み込み(PIL形式,RGB) img_array = img_to_array(img_pil) # NumPy配列に変換 img_preprocessed = preprocess_input(img_array) #vgg16用に前処理 data_set.append([img_preprocessed, to_categorical(class_num, category_size)]) # 画像、ラベル追加 rng = np.random.default_rng() # 乱数生成の為のインスタンス作成 # Xをランダムに入れ替える。axis=0で行をランダムに入れ替える。 data_set = rng.permutation(data_set, axis=0)   # 画像データ X = [] # ラベル情報 y = [] # X,yデータセット作成 for feature, label in data_set: X.append(feature) y.append(label) # X,yをnumpy配列に変換 X = np.array(X) y = np.array(y) # 作成したX,yを返す return X,y # 定義したcreate_data_set関数を用いX_train_val_testに画像部分、y_train_val_testにラベル部分を出力 X_train_val_test, y_train_val_test = create_data_set() preprocess_inputについて tensorflow.keras.applications.vgg16.preprocess_inputを用いて、 入力画像を、VGG16が学習を行った際に用いたフレームワーク(caffe)に合わせて前処理しています。 [主な処理内容] ・RGB → BGR変換 ・入力画像からimagenet画像の平均値として[103.939, 116.779, 123.68]を差引く 前処理実施、非実施を比較の記事をweb上で見かけますが、おおよそ前処理をした方が良いと言われているようです。 to_categoricalについて 正解ラベルをone-hotエンコーディングの形に変換するために使用しています。 正解ラベルが0や1の数値(labelエンコーディング形式)の場合、数字の大小が学習に影響するため、one-hotエンコーディングの形式が望ましいと考えられます。 なお、今回の説明ではホールドアウト法によりデータを学習データ、検証データに分割していますが、StratifiedKFold等で交差検証を行いたい場合は、ここではなく学習の直前にto_categoricalを実行する必要があります。 (one-hotエンコーディング形式の入力には対応していないため) to_categoricalによる変換イメージ: <二値分類> ラベル one-hot 0 [1 0] 1 [0 1] <多値分類(3個)> ラベル one-hot 0 [1 0 0] 1 [0 1 0] 2 [0 0 1] データセットの形式について Step2では、画像と正解ラベルを繋げ、以下のような形式に変換し、その後、Xとyに分割しています。 STEP3.学習データ、検証データ、テストデータ分割 概要 train_test_splitを用いて段階に分けてデータを3分割します。 このように学習データと検証データをシンプルに分ける方法をホールドアウト法と言います。 コード 必要なコードをインポートし、データを分割します。 学習データ、検証データ、テストデータの分割 from sklearn.model_selection import train_test_split # 学習データ+検証データとテストデータに分割 X_train_val, X_test, y_train_val, y_test = train_test_split(X_train_val_test, y_train_val_test, test_size=0.25, stratify = y_train_val_test) # 学習データと検証データに分割 X_train, X_val, y_train, y_val = train_test_split(X_train_val, y_train_val, test_size=0.35, stratify = y_train_val) X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.35, stratify=y)について [出力] X_train, X_val, y_train, y_val: 学習用X, 検証用X, 学習用y, 検証用yの4つが出力されます。 [引数] X : 画像データです。 y: ラベルデータです。 test_size : testデータの割合(rate)になります。 stratify: 層化抽出の指定部分です。 指定した変数の要素が均等に分かれるように分割されますが、 ここではyを指定しているので、各クラスの画像が均等になるように振り分けられます。 学習データと検証データ等を分割する際にクラスに偏りがあるとモデルの精度に影響が出ますので、stratifyを指定するとより良いと思われます。 ※ 引数は他にもありますので、気になった方はこちらの公式HPをご参照下さい。 STEP4.VGG16モデル作成 概要 VGG16モデルを読み込み、自作のレイヤーと結合して、14層までの重みをVGG16モデルのものに固定します。 コード ① 必要なコードをインポートします。 import from tensorflow.keras.models import Sequential from tensorflow.keras.models import Model from tensorflow.keras.layers import Input, Dense, Activation, Flatten from tensorflow.keras.layers import Dropout from tensorflow.keras.layers import BatchNormalization from tensorflow.keras import initializers from tensorflow.keras import regularizers import tensorflow as tf ② モデルを作成します。 モデル作成 # VGG16モデルを読み込む base_model_avg = tf.keras.applications.vgg16.VGG16(include_top=False, pooling="avg", input_shape=(X_train.shape[1],X_train.shape[2],X_train.shape[3])) x = base_model_avg.output x = Dense(256)(x) x = BatchNormalization(axis = 1)(x) x = Activation('relu')(x) x = Dropout(0.35)(x) x = Dense(128)(x) x = BatchNormalization(axis = 1)(x) x = Activation('relu')(x) x = Dropout(0.35)(x) # 二値分類のためsigmoid関数を採用 x = Dense(category_size, activation="sigmoid")(x) # モデルを用意 model_functional = Model(inputs = base_model_avg.input, outputs = x) # モデルの重みを14層目まで凍結する。(後でモデルのコンパイルが必要) for layer in model_functional.layers[:15]: layer.trainable = False 上記モデルについて、VGG16モデルに追加した自作の層を含む部分は以下のとおりです。 モデル自作部分 x = Dense(256)(x) x = BatchNormalization(axis = 1)(x) x = Activation('relu')(x) x = Dropout(0.35)(x) x = Dense(128)(x) x = BatchNormalization(axis = 1)(x) x = Activation('relu')(x) x = Dropout(0.35)(x) # 二値分類のためsigmoid関数を採用 x = Dense(category_size, activation="sigmoid")(x) VGG16を読み込む箇所について base_model_avg = tf.keras.applications.vgg16.VGG16(include_top=False, pooling="avg", input_shape=(X_train.shape[1],X_train.shape[2],X_train.shape[3])) [引数] Include_top: 全結合層~出力層について、VGG16モデルをそのまま使うかどうかを指定します。 VGG16モデルは多値分類(1000クラス)ですが、今回は二値分類用のモデルを作成しますので、Falseとして自作の層をつなげます。 pooling: グローバルプーリング層を追加します。後で説明しています。 (指定する場合は”アベレージ”か”MAX”か選択します。) Input_shape: 入力する際の形状を指定します。 Input_tensor: 入力層側に新たな層を追加します。 (今回は特に指定していません。) ※ 引数は他にもありますので、気になった方はこちらの公式HPをご参照下さい。 本モデルの構成 今回は14層目までの重みを固定しており、残りの部分で学習する形となっています。 重みをどこまで固定するかは決まっていませんので、状況に応じて最適な方法を決めるのが良いと思います。 グローバルアベレージプーリングについて flatten等で平坦化して全結合層に繋げる場合、各チャンネルの各データから全結合するため、パラメータ数は膨大になります。 (パラメータ数:3x3x512 x 512 = 2,359,296) そこでグローバルアベレージプーリングを用いることで、各チャンネルで平均した値を用いるためパラメータ数が減り、計算コストが小さくて済みます。 (パラメータ数:512) なお、pooling='max'の場合は各チャンネルの最大値が用いられます。 下記のようにイメージしていただければ良いと思います。 計算量が減ると精度に影響が出ないか心配になりますが、CIFAR-10(6万枚程度の10クラスの画像データセット)と、同様のモデルを用いて全結合層、全結合層+ドロップアウト、グローバルアベレージプーリングを比較している以下の論文では、グローバルアベレージプーリングを用いたモデルが最も精度が高いという結果になっています。 Min Lin, Qiang Chen, Shuicheng Yan (2014). Network In Network. arXiv https://arxiv.org/pdf/1312.4400.pdf 上記の論文では、グローバルアベレージプーリングはこの層での過剰適合を抑制する構造的な"レギュラライザー"(正則化の役割を持つもの)と言うことができると説明されています。 なお、全結合層のみでは正則化の役割を持つ"レギュラライザー"が無いため精度が伸びなかったことも指摘されています。 モデルを構築する手法について モデルは大きく分けて以下の3つの方法で構築できますが、今回は②のFunctional APIを用いています。 ①Sequential API ・シンプルに1直線のモデルを作成します。 Sequential_API例 model = Sequential(name=“model_1”) model.add(Conv2D(32, (3, 3), activation='relu', input_shape=(100, 100, 3))) model.add(Conv2D(32, (3, 3), activation='relu')) … ②Functional API ・入力出力が複数あるモデル等も作成可能です。 Functional_API例 x_input = Input(shape=[100,100,3]) x = Conv2D(256,3,3)(x_input) x = BatchNormalization(axis=1)(x) x = Activation('relu')(x) … ③SubclassingAPI ・define by run(実行中にネットワーク構築)の形式になります。 ・最も柔軟な方法と言われています。 Functional APIでのモデルのインスタンス化について 自作モデルを用いる例 以下のようにInputから繋ぎ、Modelクラスでinputsとoutputsを指定します。 (x_input ⇒ x ⇒ x ⇒ x ⇒ x ⇒ x ⇒ x_output) 自作層の例 x_input = Input(shape=[100,100,3]) x = Conv2D(256,3,3)(x_input) x = BatchNormalization(axis=1)(x) x = Activation('relu')(x) x = GlobalAveragePooling2D()(x) x = Dense(16,activation="relu")(x) x_output = Dense(1, activation="sigmoid")(x) model_functional_s = Model(inputs = x_input, outputs = x_output) VGG16を読み込む例 以下のように追加部分をVGG16のoutputから繋ぎ、Modelクラスでinputsとoutputsを指定します。 (base_model.input ⇒ … ⇒ base_model.output ⇒ x ⇒ … ⇒ x) VGG16読み込み例 base_model = tf.keras.applications.vgg16.VGG16(include_top=False, pooling="avg", input_shape=100,100,3) x = base_model.output x = Dense(256)(x) x = BatchNormalization(axis = 1)(x) x = Activation('relu')(x) x = Dropout(0.35)(x) x = Dense(2, activation="sigmoid")(x) model_functional = Model(inputs = base_model.input, outputs = x) 自作箇所を構成する層、関数等について Activation: 活性化関数を表しています。活性化関数は各層で入力信号の総和を出力信号に変換する関数です。             Relu: 活性化関数の1つです。 入力が0以下の場合0、0を超えたらそのまま出力します。 Sigmoid: 活性化関数の1つです。 0~1の値をとります。 ラベルが1であるかどうかの確率を0~1の値で表現できるので、 二値化に適していると言われています。 Dense: 全結合層です。 Dropout: ドロップアウト層です。 ニューロンを指定した割合で不活性化することで、繰り返しの学習の中でニューロンの形状が異なるような表現ができ、過学習の抑制に用いられます。 BatchNormalization: バッチ正規化、伝わってきたデータに対し平均0分散1になるように行う正規化です。 データ分布を強制的に調整するので、過学習や勾配消失を抑制し学習の進行を早めます。 STEP5.モデルのコンパイルと学習、予測 概要 学習時の設定を追加するためにモデルをコンパイルし、学習を実施します。 その後、学習したモデルを用いてテストデータの正解を予測します。 コード ② モデルのコンパイルを実施します。 モデルコンパイル model_functional.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy’]) コンパイルについて コンパイルでは、学習を始める前に処理の設定を行います。 ③ モデルを用いて学習、検証、モデルの保存を行います。 コールバック関数設定、モデル学習・検証 #コールバック(early stopping、重みを保存するチェックポイント) es_cb = callbacks.EarlyStopping(monitor='val_loss', patience=20, verbose=1, mode='auto’) ch_pt = callbacks.ModelCheckpoint(filepath='my_model_Epoch{epoch}_loss{loss:.4f}_valloss{val_loss:.4f}.h5',monitor='val_loss',save_best_only=True,save_weights_only=True) # 作成モデルを用いて、学習、検証を行う。 result = model_functional.fit(x=X_train, y=y_train, epochs=100, batch_size=64, validation_data=(X_val, y_val),callbacks=[es_cb,ch_pt]) # モデルを保存する model_functional.save('vgg16_trained\\my_model') コールバック関数について 学習中に、コールバック関数毎の条件を満たした時点で呼び出される関数です。 ch_pt = callbacks.ModelCheckpoint(filepath='my_model_Epoch{epoch}_loss{loss:.4f}_valloss{val_loss:.4f}.h5',monitor='val_loss',save_best_only=True,save_weights_only=True) [引数] filepath: 保存ファイル名(epoch, loss等含む) monitor: どの値を監視するかを指定します。 verbose : 表示モードです。 0=(保存の)ログ出力なし, 1=出力 mode: {auto,min,max}で指定します。 監視しているのが”val_loss”の場合は最小値”min”になります。 save_best_only: Trueにすると、(ファイル名が固定の場合)最新の最良モデルが上書きされません。 Falseにするとperiodで指定した間隔で上書き保存されます。 ただ、今回の例のように出力ファイル名がepoch毎に変わる場合は、ファイルが上書きされないため、指定による違いはあまりないと思われます。 save_weights_only: モデル全体を保存するか重みだけ保存するかを指定します。 period: 何エポック毎に保存するかどうかを指定します。 es_cb = callbacks.EarlyStopping(monitor=“val_loss”, patience=20, verbose=1, mode=“auto”) [引数] monitor : 監視する値を指定します。 patience: monitorで指定した値について、指定したエポック数の間に改善が無ければ学習が停止します。 verbose : 表示モードを指定します。 0=(停止の)ログ出力なし, 1=出力 mode: {auto,min,max}で指定します。 minでは監視する値の減少が停止したら学習が終了します。 maxでは監視する値の増加が停止したら学習が終了します。 autoでは監視する値から自動推定します。 min_delta: 監視する値が改善していると判定される最小変化量を指定します。 fit関数について ④ 学習したモデルを用いて、テストデータを予測、評価します。 テストデータ予測・検証 # 学習したモデルでテストデータを予測する。 y_pred = model_functional.predict(X_test) # 予測値を丸める y_pred = np.round(y_pred) # F値を計算 f1 = f1_score(y_test, y_pred, average="micro") f1_2 = f1_score(y_test, y_pred, average="macro") y_predはsigmoid関数を通じて出力された小数の予測値なので、ラベルと比較してF値を求めるために整数に直す必要があります。 以下のようなイメージです。 F値について F値は再現率と適合率の調和平均です。 再現率や適合率については以下に説明を記載しましたのでご参照下さい。 補足: 交差検証について KFold法 KFold法では以下のように処理します。 ① データをK個に分割する ② K個の内、1つを検証データに割り当て、残りを学習データに割り当てて学習する ③ ②とは別の部分を検証データに割り当て、残りを学習データに割り当てて学習する ④ 上記を合計K回繰り返す 上記の方法により、データの偶然の偏りに対応することが可能です。 以下のようなイメージになります。 また、以下のようにKFold交差検証にはいくつか種類があります。 正解ラベルが偏らないように、検証データと学習データで正解ラベルを均等に分けたい場合は、Stratified KFoldが良いと思われます。 評価方法としては、各分割データでの検証データの正解率の平均を取る方法や、各分割データの検証データの予測結果を足し合わせて、学習・検証データ全体の正解ラベルと比較し正解率を求める方法があるようです。 以下のようなイメージになります。 気付いた注意点としては、以下のようなものがあります。 ①ラベルデータ(y_train_val)はK-Foldの分割処理~学習前でOne-Hotエンコーディング形式 ([1 0]形式) にする 今回のコードではデータセット作成時にラベルデータをOne-Hotエンコーティングの形式に変更していますが、K-Foldはこの形式の処理に対応していないため、上記のような形で行う必要があります。 ②K-Foldの中でモデルを用意・コンパイルする K回それぞれ重みをリセットして学習を繰り返すために、現時点ではK-Foldで分割を行った後の処理の中でモデルを準備しコンパイルした方が良いと思われます。 本記事は、これまで実際にコーディングしテストを行う中で気付いた点を中心に記載していますが、書かれている内容の全てが正しいとは限りません。 本記事のコードは参考程度にしていただけたらと思います。 今後誤りが見つかった場合は修正したいと思います。 (作成者:平尾)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む