scikit-learnで機械学習をして好きな画像を自動で分類することに挑戦します。
例えば、Twitterで検索して表示される画像から、ターゲットが写っている画像とそうでない画像とを分類するときに使えます。
1.訓練用の画像データの読み込み(訓練データの用意)
何からの手段で訓練用の画像を入手し、ターゲットが写っている画像とそうでない画像とでフォルダ分けします。ここではそれぞれpositive、negativeという名前のフォルダにしてプログラムを実行しているファイルと同じ階層に入れています。
import os
from skimage import io, color
from skimage.transform import resize
#定数の設定
POS_DIRECTORY = './positive/'
NEG_DIRECTORY = './negative/'
#データを格納するディクショナリを作成
materials = {'data': [], 'target': []}
#画像の読み込みとリサイズをする関数の作成
def read_and_resize_images(directory, target_value):
list_files = os.listdir(directory)
for filename in list_files:
image = io.imread(directory + filename)
if image.ndim == 2:
continue
if image.shape[2] == 4:
image = color.rgba2rgb(image)
resized_image = resize(image, (64, 64))
materials['data'].append(resized_image)
materials['target'].append(target_value)
#イメージの読み込み
read_and_resize_images(POS_DIRECTORY, 1)
read_and_resize_images(NEG_DIRECTORY, 0)
#データ数の確認
print(len(materials['data']))
時間をかけて画像ファイルを読み込んだ後、そのファイルの数が表示されます。
読み込んだ画像の一部を確認してみましょう。
import matplotlib.pyplot as plt
#描画領域の確保
fig, axes = plt.subplots(10, 10, figsize=(15, 15), subplot_kw={'xticks':[], 'yticks':[]})
#確保した描画領域に読み込んだ画像の最初の50枚と最後の50枚を表示
for i, ax in enumerate(axes.flat):
if i < 50:
ax.imshow(materials['data'][i])
else:
ax.imshow(materials['data'][-i+49])
これで最初の50枚(ターゲットが写っている画像)と最後の50枚(ターゲットが写っていない画像)が表示されるはずです。
2.次元削減の程度の決定
上で読み込んだ画像の特徴量の数は64×64×3=12288です。PCAで次元削減をしましょう。
import numpy as np
from sklearn.decomposition import PCA
#データの整形
reshaped_data = np.array(materials['data']).reshape(len(materials['data']), -1)
print (reshaped_data.shape)
#PCAによって削減される特徴量数と失われるデータの分散との関係を表示
pca = PCA().fit(reshaped_data)
plt.plot(np.cumsum(pca.explained_variance_ratio_))
plt.xlabel('number of components')
plt.ylabel('cumulative explained variance')
plt.show()
まず、データを整形することにより、サンプル数×特徴量数(12288)の2次元配列が得られます。
次に、特徴量数と説明できる分散の割合のグラフが表示されます。このグラフを参考にして、分散の値を決めます。
今度はその分散の値でPCAを実行し、特徴量数を確かめます。分散の値を0.95にするなら次のようなコードです。
#PCAで次元削減
pca = PCA(0.95).fit(reshaped_data)
print('pca.n_components_:', pca.n_components_)
これで特徴量数が表示されます。私が試してみたデータでは306まで特徴量数を削減できました。
次元削減してから復元した画像を目で見て確かめてみます。
#次元を削減してから元に戻し、0~1の範囲にクリップする
reduced_data = pca.transform(reshaped_data)
restored_data = pca.inverse_transform(reduced_data)
clipped_data = np.clip(restored_data, 0, 1)
#描画領域の確保
fig, axes = plt.subplots(10, 10, figsize=(15, 15), subplot_kw={'xticks':[], 'yticks':[]})
#確保した描画領域に読み込んだ画像の最初の50枚と最後の50枚を表示
for i, ax in enumerate(axes.flat):
if i < 50:
ax.imshow(clipped_data[i].reshape(64, 64, 3), vmin=0, vmax=1)
else:
ax.imshow(clipped_data[-i+49].reshape(64, 64, 3), vmin=0, vmax=1)
クリップしないとClipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers)という警告メッセージが表示されますが、画像は表示されます。ここではそのエラーを防ぐために数値が0〜1の範囲に収まるようにクリップしました。
3.しきい値の調節
いくらか余計な画像が混じってもいいからターゲット画像を見逃したくないということが多いでしょう。
そこでしきい値を調節して再現率(recall)を上げます。
まずは普通に学習した結果の情報を確認します。
SVCをデフォルトパラメータで使うとします。
import matplotlib.pyplot as plt
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.decomposition import PCA
from sklearn.pipeline import Pipeline
from sklearn.svm import SVC
from sklearn.metrics import accuracy_score
from sklearn.metrics import plot_confusion_matrix
from sklearn.metrics import classification_report
from sklearn.metrics import plot_precision_recall_curve
#訓練データとテストデータに分割
reshaped_data = np.array(materials['data']).reshape(len(materials['data']), -1)
X_train, X_test, y_train, y_test = train_test_split(reshaped_data, materials['target'])
#パイプラインの構築
steps = [('pca', PCA()), ('model', SVC())]
pipe = Pipeline(steps)
#作成したモデルで学習
pipe.fit(X_train, y_train)
#学習結果のテスト
y_test_pred = pipe.predict(X_test)
print('accuracy_score:', accuracy_score(y_test, y_test_pred))
#テストの混同行列
plot_confusion_matrix(pipe, X_test, y_test)
plt.show()
#訓練の混同行列
plot_confusion_matrix(pipe, X_train, y_train)
plt.show()
#適合率(precision)・再現率(recall)・F1値の一括計算
y_train_pred = pipe.predict(X_train)
print(classification_report(y_train, y_train_pred))
#適合率―再現率曲線
plot_precision_recall_curve(pipe, X_train, y_train)
plt.show()
その結果から、目指すべき再現率(recall)を決めます。そして、その再現率に合うように、訓練データの中で見逃してもよい個数を算出します。
from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay
#見逃す個数の設定
NUM_TO_MISS = 3
#訓練データの中からfalse_negativeだけをリスト内包表記で抽出
false_negative = [X_train[i] for i in range(len(X_train)) if (y_train_pred[i] == False and y_train[i] == True)]
#false_negativeに決定関数を適用した値を取得後、ソートして表示
false_negative_value = pipe.decision_function(false_negative)
sorted_false_negative_value = np.sort(false_negative_value)
print(sorted_false_negative_value, '\n\n')
#決定関数のしきい値を見逃す個数に応じて設定
y_train_pred_lower_threshold = (pipe.decision_function(X_train) >= sorted_false_negative_value[NUM_TO_MISS - 1])
#新しく設定したしきい値での適合率(precision)・再現率(recall)・F1値の一括計算
print(classification_report(y_train, y_train_pred_lower_threshold))
#新しく設定したしきい値での混同行列
disp = ConfusionMatrixDisplay(confusion_matrix(y_train, y_train_pred_lower_threshold))
disp.plot()
plt.show()
#新しく設定したしきい値での学習結果のテスト
y_test_pred_lower_threshold = (pipe.decision_function(X_test) >= sorted_false_negative_value[NUM_TO_MISS - 1])
accuracy_score(y_test, y_test_pred_lower_threshold)
#新しく設定したしきい値で学習したテストデータの混同行列
disp = ConfusionMatrixDisplay(confusion_matrix(y_test, y_test_pred_lower_threshold))
disp.plot()
plt.show()
望む結果が得られたら学習後のモデルとしきい値を保存しておきましょう。
import pickle
pipe_and_threshold = {'pipe':pipe, 'threshold':sorted_false_negative_value[NUM_TO_MISS - 1]}
filename = 'finalized_pipe_and_threshold.sav'
pickle.dump(pipe_and_threshold, open(filename, 'wb'))
4.未知の画像への適用
これでやりたいことができます。未知の画像に先ほど学習したモデルを適用します。
import pickle
import os
from skimage import io, color
from skimage.transform import resize
import numpy as np
import matplotlib.pyplot as plt
#定数の設定
APPLY_DIRECTORY = './apply/'
#画像データを保存するリストを作成
data = []
# 保存したパイプラインとしきい値をロードする
filename = 'finalized_pipe_and_threshold.sav'
loaded = pickle.load(open(filename, 'rb'))
#適用する画像ファイルの読み込み
list_files = os.listdir(APPLY_DIRECTORY)
for filename in list_files:
image = io.imread(APPLY_DIRECTORY + filename)
if image.ndim == 2:
continue
if image.shape[2] == 4:
image = color.rgba2rgb(image)
resized_image = resize(image, (64, 64))
data.append(resized_image)
#画像データを変形して適用
X = np.array(data).reshape(len(data), -1)
y = loaded['pipe'].decision_function(X) >= loaded['threshold']
#Trueと判定されたデータだけを取得
true_data = [data[i] for i in range(len(data)) if y[i] == True]
#描画領域の確保
fig, axes = plt.subplots(10, 10, figsize=(15, 15), subplot_kw={'xticks':[], 'yticks':[]})
#Trueと判定された画像の表示
for i, ax in enumerate(axes.flat):
ax.imshow(true_data[i])
if i+1 >= len(true_data): break
できました。