やること(概要)
-
- 画像データの収集
-
- データセットの作成(画像データの変換)
-
- モデルの作成 & 学習
-
- 実行(コマンドライン)
動作環境
- macOS Catalina 10.15 beta
- Python 3.6.8
- flickapi 2.4
- pillow 6.0.0
- scikit-learn 0.20.3
- google colaboratory
実施手順
1. 画像データの収集
・3種類(りんご、トマト、いちご)の画像分類を実施するため、画像ファイルをflickrから取得
・flickrによる画像ファイルの取得方法は前回記事で書いたこちら
・それぞれ300枚の画像ファイルを取得
・検索キーワードは、「apple」、「tomato」、「strawberry」を指定
・flickrからダウンロードした不要なデータ(検索キーワードと関係ない画像ファイル)は目で見て除外しておく
download.py
from flickrapi import FlickrAPI
from urllib.request import urlretrieve
import os, time, sys
# Set your own API Key and Secret Key
key = "XXXXXXXXXX"
secret = "XXXXXXXXXX"
wait_time = 0.5
keyword = sys.argv[1]
savedir = "./data/" + keyword
flickr = FlickrAPI(key, secret, format='parsed-json')
result = flickr.photos.search(
text = keyword,
per_page = 300,
media = 'photos',
sort = 'relevance',
safe_search = 1,
extras = 'url_q, license'
)
photos = result['photos']
for i, photo in enumerate(photos['photo']):
url_q = photo['url_q']
filepath = savedir + '/' + photo['id'] + '.jpg'
if os.path.exists(filepath): continue
urlretrieve(url_q,filepath)
time.sleep(wait_time)
2. データセットの作成(画像データの変換)
・取得した画像ファイルをnumpy形式(バイナリファイル -> .npy)で保存
・VGG16のデフォルトサイズの224にresize
generate_data.py
from PIL import Image
import os, glob
import numpy as np
from sklearn import model_selection
classes = ['apple', 'tomato', 'strawberry']
num_classes = len(classes)
IMAGE_SIZE = 224 # Specified size of VGG16 Default input size in VGG16
X = [] # image file
Y = [] # correct label
for index, classlabel in enumerate(classes):
photo_dir = './data/' + classlabel
files = glob.glob(photo_dir + '/*.jpg')
for i, file in enumerate(files):
image = Image.open(file)
# standardize to 'RGB'
image = image.convert('RGB')
# to make image file all the same size
image = image.resize((IMAGE_SIZE, IMAGE_SIZE))
data = np.asarray(image)
X.append(data)
Y.append(index)
X = np.array(X)
Y = np.array(Y)
X_train, X_test, y_train, y_test = model_selection.train_test_split(X, Y)
xy = (X_train, X_test, y_train, y_test)
np.save('./image_files.npy', xy)
3. モデルの作成 & 学習
1). Google Colaboratoryの利用
- トレーニング処理に時間がかかるため、GPUが無料で利用可能なGoogle Colaboratoryを使用(環境構築不要・無料で使えるブラウザ上のPython実行環境)
- 今回はGoogle Driveに「2.」で作成した「image_files.npy」をGoogle Driveへ格納し、ファイルをGoogle Colabからの読み込み
- 読み込みするためにGoogle Driveのマウントが必要であるが、方法は下記の通り
(Google Colabの詳しい使い方はこちらを参考にした)
マウント方法
from google.colab import drive
drive.mount('/content/gdrive')
# image_files.npyの格納先(My Drive直下に'hoge'フォルダを作成し、そこに格納)
PATH = '/content/gdrive/My Drive/hoge/'
2). データの読み込み & データ変換
- google driveに格納した「image_files.npy」を読み込み、訓練データとテストデータに分割
- 正解ラベルをone-hotベクトルへ変換(Ex:0 -> [1,0,0], 1 -> [0,1,0]のようなイメージ)
- データを標準化(画像データを0~1の範囲に変換。RGB形式なので、(0,0,0)~(255,255,255)の範囲であるため、255で割る)
X_train, X_test, y_train, y_test = np.load(PATH + 'image_files.npy', allow_pickle=True)
# convert one-hot vector
y_train = np_utils.to_categorical(y_train, num_classes)
y_test = np_utils.to_categorical(y_test, num_classes)
# normalization
X_train = X_train.astype('float') / 255.0
X_test = X_test.astype('float') / 255.0
3). モデルの作成
- VGG16を利用
- 3つのパラメータは下記の通り。
-
include_top
: ネットワークの出力層側にある3つの全結合層(Fully Connected層)を含むかどうか。今回はFC層を独自に計算するため、Falseを指定。 -
weights
: VGG16の重みの種類を指定する。None(ランダム初期化)か'imagenet' (ImageNetで学習した重み)のどちらか一方 -
input_shape
: オプショナルなshapeのタプル。include_topがFalseの場合のみ指定可能 (そうでないときは入力のshapeは(224, 224, 3)。正確に3つの入力チャンネルをもつ必要があり、width とheightは48以上にする必要がある
-
モデルの作成
vgg16_model = VGG16(
weights='imagenet',
include_top=False,
input_shape=(IMAGE_SIZE, IMAGE_SIZE, 3)
)
- FC層を構築
- input_shapeには上記modelのoutputの形状で、1番目以降を指定(0番目は個数が入っている)
FC層の構築
top_model = Sequential()
top_model.add(Flatten(input_shape=vgg16_model.output_shape[1:]))
top_model.add(Dense(256, activation='relu'))
top_model.add(Dropout(0.5))
top_model.add(Dense(num_classes, activation='softmax'))
- vgg16_modelとtop_modelを結合してモデルを作成
モデルの結合
# combine models
model = Model(
inputs=vgg16_model.input,
outputs=top_model(vgg16_model.output)
)
model.summary()
model.summaryの出力結果
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_1 (InputLayer) (None, 224, 224, 3) 0
_________________________________________________________________
block1_conv1 (Conv2D) (None, 224, 224, 64) 1792
_________________________________________________________________
block1_conv2 (Conv2D) (None, 224, 224, 64) 36928
_________________________________________________________________
block1_pool (MaxPooling2D) (None, 112, 112, 64) 0
_________________________________________________________________
block2_conv1 (Conv2D) (None, 112, 112, 128) 73856
_________________________________________________________________
block2_conv2 (Conv2D) (None, 112, 112, 128) 147584
_________________________________________________________________
block2_pool (MaxPooling2D) (None, 56, 56, 128) 0
_________________________________________________________________
block3_conv1 (Conv2D) (None, 56, 56, 256) 295168
_________________________________________________________________
block3_conv2 (Conv2D) (None, 56, 56, 256) 590080
_________________________________________________________________
block3_conv3 (Conv2D) (None, 56, 56, 256) 590080
_________________________________________________________________
block3_pool (MaxPooling2D) (None, 28, 28, 256) 0
_________________________________________________________________
block4_conv1 (Conv2D) (None, 28, 28, 512) 1180160
_________________________________________________________________
block4_conv2 (Conv2D) (None, 28, 28, 512) 2359808
_________________________________________________________________
block4_conv3 (Conv2D) (None, 28, 28, 512) 2359808
_________________________________________________________________
block4_pool (MaxPooling2D) (None, 14, 14, 512) 0
_________________________________________________________________
block5_conv1 (Conv2D) (None, 14, 14, 512) 2359808
_________________________________________________________________
block5_conv2 (Conv2D) (None, 14, 14, 512) 2359808
_________________________________________________________________
block5_conv3 (Conv2D) (None, 14, 14, 512) 2359808
_________________________________________________________________
block5_pool (MaxPooling2D) (None, 7, 7, 512) 0
_________________________________________________________________
sequential_1 (Sequential) (None, 2) 6423298
=================================================================
Total params: 21,137,986
Trainable params: 21,137,986
Non-trainable params: 0
_________________________________________________________________
4). 重みの固定
- 上記で作成したモデルは下記2つを結合したもの
- vgg16_model:FC層を除いたVGG16
- top_model:多層パーセプトロン
- この内、vgg16_modelの'block4_pool'(model.summary参照)までの重みを固定(VGG16の高い特徴量抽出を継承するため)
重みの固定
for layer in model.layers[:15]:
layer.trainable = False
5).モデルの学習
- optimizerはSGDを指定
- 多クラス分類を指定
モデルの学習
opt = SGD(lr=1e-4, momentum=0.9)
model.compile(loss='categorical_crossentropy', optimizer=opt, metrics=['accuracy'])
model.fit(X_train, y_train, batch_size=32, epochs=10)
6).テストデータでの評価
テストデータでの評価
score = model.evaluate(X_test, y_test, batch_size=32)
print('loss: {0} - acc: {1}'.format(score[0], score[1]))
7).モデルの保存
モデルの保存
model.save(PATH + 'vgg16_transfer.h5')
4. 実行(コマンドライン)
- 作成したモデル(vgg16_transfer.h5)を使って、画像ファイルの推定を行う
predict.py
import numpy as np
from tensorflow import keras
from tensorflow.keras.models import Sequential, Model, load_model
from PIL import Image
import sys
classes = ['apple', 'tomato', 'strawberry']
num_classes = len(classes)
IMAGE_SIZE = 224
# convert data by specifying file from terminal
image = Image.open(sys.argv[1])
image = image.convert('RGB')
image = image.resize((IMAGE_SIZE, IMAGE_SIZE))
data = np.asarray(image)
X = []
X.append(data)
X = np.array(X)
# load model
model = load_model('./vgg16_transfer.h5')
# estimated result of the first data (multiple scores will be returned)
result = model.predict([X])[0]
predicted = result.argmax()
percentage = int(result[predicted] * 100)
print(classes[predicted], percentage)
- 実行は下記の通り(引数に推定する画像ファイル名を指定)
実行
$ python predict.py XXXX.jpeg
結果例
strawberry 100
ソースコード
https://github.com/hiraku00/vgg16_transfer
('image_files.npy'と'vgg16_transfer.h5'は100MB超過のため除外)