CNNを用いた画像分類


##はじめに
私はエンジニア転職を目指して勉強中の数学担当塾講師です
学習の一環として本記事を投稿します
今回は初心者の方の参考になればと
自分が間違えたところも記述しています
至らぬ点も多いですが最後までお付き合いください
（コードは完成したものを載せています）

####Aidemyで学習したこと
・Python入門
・Numpy基礎
・Pandas基礎
・Matplotlib基礎
・データクレンジング
・機械学習概論
・教師あり学習（分類・回帰）
・教師なし学習
・スクレイピング
・ディープラーニング基礎
・CNNを用いた画像認識
・男女識別
・Flask入門の為のHTML&CSS
・Flask入門
・文字認識アプリの作成
・コマンドライン入門
・Git入門
・Herokuへのデプロイ方法
・自然言語処理基礎
・ネガポジ分析
・確率論情報理論
・画像認識アプリの作成
・pythonによるexcelの自動化
・ビジネスIoT入門
・推薦システム実装入門
・ブロックチェーン入門
・強化学習を用いた三目並べゲームの開発


スキルがまだまだ足りない初心者なのでカリキュラム外もいくつか学習しました
これからも時間を見つけて取り組みたいと思います
高校数学と関連しているところは理解しやすくて楽しいと感じました
教える側として質問する人としない人では伸び率が違うと感じるので何かを学んでいる人は質問をたくさんしてください
疑問点+αで質問することを私も意識しました


##目的
今回は和柄（檜垣文様・七宝文様・鱗文様）の画像分類をします

テーマ決めに難航しておりましたがキャラクター分類をしている方を見て柄の分類ならいつか自分も使うかもしれないと思い『よく見るのに覚えられない・どう検索するか迷う』和柄を分類しようと考えました。
今回は実行時間の問題から3種類にしましたが実用性を考え将来的には種類を増やしたいです

##開発環境
・GoogleColaboratory
・VSCode 1.59.0
・Python 3.8.8
・tensorflow 2.5.0
##画像収集

画像の調達はスクレイピングを用いりました

参考文献
https://www.youtube.com/watch?v=hRB104ik6pQ

ですが思っていた程収集出来なかったので和柄のフリー素材を検索してそこからいくつか作成したりしました


##画像の水増し

画像の数が少ないので水増しします
このコードで1枚につき32枚水増しされます
リサイズで既に多少画像が潰れてしまっているので細かい柄を残す為ノイズ除去は今回しませんでした

```python:
import os
import numpy as np
import matplotlib.pyplot as plt
import cv2
import glob

 
def scratch_image(img, flip=True, cvt=True, cvt2=True, bit=True, warp=True):
    # 水増しの手法を配列にまとめる
    methods = [flip, cvt, cvt2, bit, warp]

    # flip は画像の左右反転
    # cvt  は色調変換
    # cvt2 は色調変換
    # bit  は色反転
    # warp は90度回転

    images = [img]

    #画像のトリミング
    center = (img.shape[0]//2, img.shape[1]//2)
    img = img[center[0] - np.min(center) : center[0] + np.min(center), center[1] - np.min(center) : center[1] + np.min(center)]

    # 手法に用いる関数
    scratch = np.array([
       
        #画像の左右反転のlambda関数
        lambda x: cv2.flip(x,1),
        
        #色調変換のlambda関数
        lambda x: cv2.cvtColor(x, cv2.COLOR_BGR2RGB),
        
        #色調変換2のlambda関数
        lambda x: cv2.cvtColor(x, cv2.COLOR_BGR2YCrCb),
        
        #色反転のlambda関数
        lambda x: cv2.bitwise_not(x),

        #90度回転するlambda関数
        lambda x: cv2.warpAffine(x, cv2.getRotationMatrix2D
        (tuple(np.array([img.shape[1], img.shape[0]]) / 2), 90, 1.0), img.shape[::-1][1:3])
              
    ])

    
    # 関数と画像を引数に、加工した画像を元と合わせて水増しする関数
    doubling_images = lambda f, imag: (imag + [f(i) for i in imag])
    
    # doubling_imagesを用いてmethodsがTrueの関数で水増ししてください
    for func in scratch[methods]:
        images = doubling_images(func,images)
        
    return images

# 画像ディレクトリのパス
root_dir = "/content/drive/MyDrive/wagara.app/"
# 画像ディレクトリ名
pattern = ["higaki", "sippou", "uroko"]

for dir in pattern:
    file_dir = root_dir + dir
    img_file = glob.glob(file_dir + "/*")
    for j in img_file:
     # 画像の読み込み
       wagara_img = cv2.imread(j)
    #画像のトリミング
       center = (wagara_img.shape[0]//2, wagara_img.shape[1]//2)
       wagara_img = wagara_img[center[0] - np.min(center) : center[0] + np.min(center), center[1] - np.min(center) : center[1] + np.min(center)]
     #画像のリサイズ
       wagara_img = cv2.resize(wagara_img, (200,200))        
     # 画像の水増し
       scratch_wagara_images = scratch_image(wagara_img)
     # 拡張子なしファイル名を取得
       name = os.path.splitext(os.path.basename(j))[0]

       for num, im in enumerate(scratch_wagara_images):
           # まず保存先のディレクトリを指定、番号を付けて保存
           cv2.imwrite(file_dir + "_c" + name + "_" + str(num) + ".jpg" ,im)       
 
```




どの関数を使用するかは1枚だけのお試し用のコードを書いて選択しました
試す際には参考文献にあったファイルごと削除するコードを使うと早かったです
試した結果あまり多くても意味がなく、柄によっては3つくらいがベストかもしれません
1度関数が多い状態でまとめて処理してしまった為容量オーバーになり画像の削除に時間が掛かったので容量には注意してください
データはまとめて処理できると思っていましたが何事も試してから進めることが大事だと反省いたしました


##画像処理
処理コードは〈画像の水増し〉のコードに含まれているのでそちらをご覧ください
今回私は元の画像が入っているフォルダに水増し画像保存したのですが
元の画像はサイズが違うので新しいフォルダを作り保存先にするべきでした
あとから元の画像だけ移動してもいいのですが
画像やフォルダの数によっては手間がかかるので新しいフォルダを作る方が楽だと思いました
載せているコードは新しく元の画像のフォルダ名に_cを加えた名前のフォルダ(例：higaki_c)を作った場合のコードです
そして画像によっては反転や回転しても元の画像と変わらないものがあるので手作業で削除しました

##画像の分類
ここでは転移学習を用いて画像の分類をしています
読み込み時間を考慮して読む込む枚数を制限しています
水増し画像が多いのでランダムに読み込むようにしています
#####転移学習とは
学習済みのモデルを使って新たなモデルの学習を行うことで少ないデータから高精度なモデルを作成したり短時間で学習することが可能になります
今回はみ込み13層と全結合層3層からなるVGG16を利用しています
モデル構造はこのようになっています
![モデル構造](http://drive.google.com/uc?export=view&id=1pxbanRVh7D8X8Tbe0iBpc0WnF_v9Ht6o)


```python:
import numpy as np
from tensorflow.keras import optimizers
from tensorflow.keras.applications.vgg16 import VGG16
from tensorflow.keras.layers import Dense, Dropout, Flatten, Input
from tensorflow.keras.models import Model, Sequential
from tensorflow.keras.utils import to_categorical
import matplotlib.pyplot as plt
import cv2
import glob

# 画像ディレクトリのパス
root_dir = "/content/drive/MyDrive/wagara.app/"
# 画像ディレクトリ名
pattern = ["higaki_c", "sippou_c", "uroko_c"]

X = []  # 画像の2次元データを格納するlist
y = []  # ラベル(正解)の情報を格納するlist

for dir in pattern:
    file_dir = root_dir + dir
    img_file = glob.glob(file_dir + "/*")
    #読み込み時間を少なくするため枚数を限定してランダムに読み込む
    img_file = np.random.choice(img_file, 250, replace=False)#すべて読み込む際はこのコードを削除
    y.append(len(img_file))
    for i in img_file:
        img = cv2.imread(i)
        X.append(img)


X = np.array(X, dtype=np.int64)
y = np.array([0]*y[0] + [1]*y[1] + [2]*y[2] )

#X,Yリストをシャッフルする
rand_index = np.random.permutation(np.arange(len(X)))
X = X[rand_index]
y = y[rand_index]

# データの分割
X_train = X[:int(len(X)*0.8)]
y_train = to_categorical(y[:int(len(y)*0.8)])
X_test = X[int(len(X)*0.8):]
y_test = to_categorical(y[int(len(y)*0.8):])


#input_tensorの定義をして、vggのImageNetによる学習済みモデルを作成
input_tensor = Input(shape=(200,200,3))
vgg16 = VGG16(include_top=False,weights="imagenet",input_tensor=input_tensor)

# 特徴量抽出部分のモデルを作成
top_model = Sequential()
top_model.add(Flatten(input_shape=vgg16.output_shape[1:]))
top_model.add(Dense(256, activation='sigmoid'))
top_model.add(Dropout(0.5))
top_model.add(Dense(3, activation='softmax'))

# vgg16とtop_modelを連結
model = Model(inputs=vgg16.input,outputs=top_model(vgg16.output))

# 15層目までの重みを固定
for layer in model.layers[:15]:
    layer.trainable = False

#　モデル構造を確認
model.summary()

# コンパイルをしています
model.compile(loss='categorical_crossentropy',
              optimizer=optimizers.SGD(lr=1e-4, momentum=0.9),
              metrics=['accuracy'])


# 学習を行う
model.fit(X_train, y_train, validation_data=(X_test, y_test), batch_size=32, epochs=3)


# 画像を一枚受け取り、画像の種類を判定して返す関数
pred_obj = []
def pred_object(img):
  pred = np.argmax(model.predict(np.array([img])))
  if pred == 0:
     pred_obj.append("檜垣文様")

  elif pred == 1:
    pred_obj.append("七宝文様")  

  else:
    pred_obj.append("鱗文様")  
 

# 精度の評価をしています
scores = model.evaluate(X_test, y_test, verbose=1)
print('Test loss:', scores[0])
print('Test accuracy:', scores[1])

#pred_object関数に写真を渡して画像の分類予測
#データの可視化（テストデータの先頭10枚）
for i in range(10):
  img = X_test[i]
  plt.subplot(2, 5, i+1)
  plt.imshow(X_test[i])
  pred_object(img)
plt.suptitle("10 images of test data",fontsize=16)
plt.show()
print(np.reshape(pred_obj,(2,5)))
```


ここでは先程少し述べたサイズの違う元の画像があったことで色々なエラーが起きました
たくさんの方に質問しましたが原因が分からず
色んな情報を出力したり制限することでやっと気づけました
今回は画像処理の方でリサイズなどを行ったためこちらには書かなかったことも影響したので念のために書いておくと良かったかも知れません
ですが重複など余分な情報は人に伝える際にはない方が良いと考えここは直していません

##結果
分類結果はこのようになりました
![出力結果](http://drive.google.com/uc?export=view&id=1F38vuNjdk4dv09kpkzUEbTaYOVKi_erT)

正解率は98.6％でした
水増しで同じような画像が多かった為か少ない学習で高い正解率が出ています
別に用意した写真で確認したところ正解は5つ中3つでした
写真だったのでしわや写り方で少し正解率が下がっています

##最後に
今まではデータセットを利用して画像の保存場所など考えていなかったので今後はそのような細かいところにも気を配って進めようと思います
今回失敗が多く時間はかかりましたが学べたことも多く、なによりきちんと実行できたことでプログラミングが楽しいと思えました
次からはコードの書き方や正解率の上げ方をもっと工夫したいと思います

この記事が少しでも初心者の参考になれば幸いです
最後まで読んでいただきありがとうございました