Python初心者がCNN画像認識で洗濯タグ識別をやってみる

# 目次
1.はじめに
2.実行環境
3.ソースコードの解説
　3.1 画像収集と処理
　3.2 学習用データと検証用データの作成
　3.3 モデルの定義と学習
　3.4 モデルのトレーニングと結果の可視化、及びモデルの保存
4.製作したアプリ
5.苦労した点
6.おわりに

# 1.はじめに
はじめまして、IT未経験で転職を目指す20代半ばの男性です。
今回、教育訓練給付制度を使用してAidemyさんのAIアプリ開発講座（3ヶ月）を受講しました。
このブログは、その最終成果物としての課題です。

### アプリ開発の経緯
日常生活でおしゃれな服を洗濯する際に洗濯タグに書かれた記号や指示が理解しづらいことが多く
どの洗濯方法が最適なのかが分からない瞬間が何度もあり、画像から洗濯タグの内容を判別できたら
便利だな～と思ったのが経緯となります。

しかし、洗濯タグの種類は現在で41種類にもなります。![column_04-kv-1024x427.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3560226/920d78d2-7c57-ad1f-bcbd-7602e67ca43f.jpeg)
今の私の技術では全てのタグを種類を識別させることは難しいので
まずは、「洗濯不可」と「液温40度を限度として洗濯可能」の2種類に絞ってアプリ開発をしました。

  　　   　　　　<font color="Blue">液温40度を限度として洗濯可能</font>　　　　　<font color="Crimson">洗濯不可</font>
 　　　　　　　　　　　　![wash40 (18).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3560226/69befc23-c89a-712c-e356-3dcbb89dccb4.jpeg)　　　　　　　　　　　![colum1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3560226/8cfb8581-5f5e-091e-08a4-3c7fd50e91af.jpeg)

※液温40度を限度として洗濯可能を以下、洗濯可能と省略します。


# 2.実行環境
・Visual Studio Code
・Google Colaboratory
・Python 3.11.1


# 3.ソースコードの解説
## 3.1 画像収集と処理
まずは、洗濯可能の画像と洗濯不可の画像をWEBサイト上から手動で拾ってきます。
スクレイピングを使用して集めようかとも思いましたが、関係ない画像が多かったので
手動で各50枚ずつ集めトリミングを行い、googleドライブに保存します。

`トリミング前`
![1221215754.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3560226/19d85ce8-0de2-86ea-2200-7fe9af769c91.jpeg)
`トリミング後`
この時実際の使用を想定して周りにあえて余計な線などを残します。
![1221215744.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3560226/b4f3cc0f-6b9e-e0ad-d2f9-8d5ad3df1911.jpeg)

### 画像の水増し
OpenCVを用いて画像を15度と-15度のものを加え、各画像を約150枚まで水増しを行う。
```python:qiita.rb
import os
import cv2

# 入力フォルダと出力フォルダのパスを指定
input_folder = "/content/drive/MyDrive/dataset/not_washable_resize"
output_folder = "/content/drive/MyDrive/dataset/kaiten_not_wash"

# 回転角度（度）を指定
angles = [-15, 15]  # -15度と15度の回転

# 出力フォルダを作成
os.makedirs(output_folder, exist_ok=True)

# 入力フォルダ内の画像ファイルを処理
for filename in os.listdir(input_folder):
    if filename.endswith((".jpg", ".jpeg", ".png")):
        # 画像読み込み
        img_path = os.path.join(input_folder, filename)
        img = cv2.imread(img_path)

        # 画像の中心座標を計算
        height, width = img.shape[:2]
        center = (width // 2, height // 2)

        # 回転行列を作成
        rotation_matrix = cv2.getRotationMatrix2D(center, angle_degrees, 1.0)

        # 画像を指定した角度で回転
        rotated_image = cv2.warpAffine(img, rotation_matrix, (width, height))

         # 新しいファイル名を生成
        base_name, file_extension = os.path.splitext(filename)
        new_filename = f"{base_name}_rotated{angle_degrees}{file_extension}"

        # 回転後の画像を保存
        output_path = os.path.join(output_folder, filename)
        cv2.imwrite(output_path, rotated_image)


print("画像の回転が完了しました。")
```
OpenCVライブラリを使って画像をリサイズし、googleドライブフォルダに保存します。
```python:qiita.rb
import os
import cv2

img_size = 50  # 画像サイズ：50×50
base_dir = "/content/drive/MyDrive/data_test/"

# wash画像を処理
wash_folder = "40_moto/"
path_40_wash = os.listdir(os.path.join(base_dir, wash_folder))
img_40 = []

# 保存先フォルダを作成
output_folder = "resized_wash/"
output_dir = os.path.join(base_dir, output_folder)
os.makedirs(output_dir, exist_ok=True)

for filename in path_40_wash:
    img_path = os.path.join(base_dir, wash_folder, filename)
    img = cv2.imread(img_path)
    b, g, r = cv2.split(img)
    img = cv2.merge([r, g, b])
    img = cv2.resize(img, (img_size, img_size))

    # 保存先のファイルパスを生成
    output_path = os.path.join(output_dir, filename)

    # リサイズされた画像を保存
    cv2.imwrite(output_path, img)

# not_wash画像を処理
not_wash_folder = "not_wash_moto/"
path_not_wash = os.listdir(os.path.join(base_dir, not_wash_folder))
img_40 = []

# 保存先フォルダを作成
output_folder = "resized_not_wash/"
output_dir = os.path.join(base_dir, output_folder)
os.makedirs(output_dir, exist_ok=True)

for filename in path_not_wash:
    img_path = os.path.join(base_dir, not_wash_folder, filename)
    img = cv2.imread(img_path)
    b, g, r = cv2.split(img)
    img = cv2.merge([r, g, b])
    img = cv2.resize(img, (img_size, img_size))

    # 保存先のファイルパスを生成
    output_path = os.path.join(output_dir, filename)

    # リサイズされた画像を保存
    cv2.imwrite(output_path, img)
```

## 3.2 学習用データと検証用データの作成
必要なライブラリをインポートしたら、Googleドライブから洗濯可能と洗濯不可の画像を読み込み
画像サイズを50×50に変換しリストに格納します。
この時、水増し前のデータも別でリストに格納します。
```python:qiita.rb
import os#osモジュール（os機能がpythonで扱えるようにする）
import cv2#画像や動画を処理するオープンライブラリ
import numpy as np#python拡張モジュール
import matplotlib.pyplot as plt#グラフ可視化
from tensorflow.keras.utils import to_categorical#正解ラベルをone-hotベクトルで求める
from tensorflow.keras.layers import Dense, Dropout, Flatten, Input#全結合層、過学習予防、平滑化、インプット
from tensorflow.keras.applications.vgg16 import VGG16#学習済モデル
from tensorflow.keras.models import Model, Sequential#線形モデル
from tensorflow.keras import optimizers#最適化関数

#洗濯タグ画像の格納
drive_wash = "/content/drive/MyDrive/dataset/not_washable_resize/"
drive_not_wash = "/content/drive/MyDrive/dataset/40_resize/"
drive_before_wash = "/content/drive/MyDrive/data_test/resized_wash/"#水増し前の画像データ
drive_before_not_wash = "/content/drive/MyDrive/data_test/resized_not_wash/"#水増し前の画像データ
image_size = 50#50x50のサイズに指定

#os.listdir() で指定したファイルを取得
path_wash = [filename for filename in os.listdir(drive_wash) if not filename.startswith('.')]
path_not_wash = [filename for filename in os.listdir(drive_not_wash) if not filename.startswith('.')]
path_before_wash = [filename for filename in os.listdir(drive_before_wash) if not filename.startswith('.')]
path_before_not_wash = [filename for filename in os.listdir(drive_before_not_wash) if not filename.startswith('.')]

 #洗濯タグ画像を格納するリスト作成
img_wash = []
img_not_wash = []
img_before_wash = []
img_before_not_wash = []

for i in range(len(path_wash)):
    img = cv2.imread(drive_wash + path_wash[i])  # 画像を読み込む
    img_wash.append(img)  # 画像配列に画像を加える
for i in range(len(path_not_wash)):
    img = cv2.imread(drive_not_wash + path_not_wash[i])
    img_not_wash.append(img)
for i in range(len(path_before_wash)):
    img = cv2.imread(drive_before_wash + path_before_wash[i])  # 画像を読み込む
    img_before_wash.append(img)  # 画像配列に画像を加える
for i in range(len(path_before_not_wash)):
    img = cv2.imread(drive_not_wash + path_not_wash[i])
    img_before_not_wash.append(img)
```
　
2種類の画像を集約してXに学習画像、yに正解ラベルを代入します。
3種類の時は[2],4種類の時は[3]とラベルを増やしていきます。
```python:qiita.rb
#np.arrayでXに学習画像、ｙに正解ラベルを代入
X = np.array(img_wash + img_not_wash)
#正解ラベルの作成
y =  np.array([0]*len(img_wash) + [1]*len(img_not_wash))
label_num = list(set(y))
#水増し前のデータの代入
X_before = np.array(img_before_wash + img_before_not_wash)
#正解ラベル（水増し前）
y_before =  np.array([0]*len(img_before_wash) + [1]*len(img_before_not_wash))
```

集約した学習用データをランダムに並び替えます。 <font color="Red">※データの偏りを無くすため</font>
データセットの画像データは学習用に全ての画像を、検証用として水増し前の画像を使ってデータリストを
作成しました。
初め実行した際に学習用
```python:qiita.rb
#配列のラベルをシャッフルする
rand_index = np.random.permutation(np.arange(len(X)))
X = X[rand_index]
y = y[rand_index]

#学習データと検証データを用意
X_train = X
y_train = y
X_test = X_before
y_test = y_before
```

正解ラベルをone-hotベクトルに変換する。
one-hotエンコーディングについてわかりやすく説明しているサイト(https://pyming.info/2021/12/26/machine_learning_onehot/) がありました。エンジニア初心者の方がいれば参考にしてみて下さい。
```python:qiita.rb
#正解ラベルをone-hotベクトルで求める
y_train = to_categorical(y_train)
y_test = to_categorical(y_test)
```

## 3.3 モデルの定義と学習
Kerasモジュールを使用して学習済みモデルVGG16を読み込んで転移学習を行います。
VGG16モデルから出力を受け取り、Sequentialモデルを用いて追加の層を定義します。


```python:qiita.rb
#モデルの入力画像として用いるためのテンソールのオプション
input_tensor = Input(shape=(image_size,image_size, 3))

#転移学習のモデルとしてVGG16を使用
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(64, activation='sigmoid'))
top_model.add(Dropout(0.5))
top_model.add(Dense(32, activation='sigmoid'))
top_model.add(Dropout(0.5))
top_model.add(Dense(2, activation='softmax'))

#vgg16による特徴抽出部分の重みを15層までに固定（以降に新しい層（top_model)が追加）
for layer in model.layers[:15]:
   layer.trainable = False


```
自作モデルとvggモデルを連結させ新たなモデルに代入します。
モデルの学習プロセスを設定し、指定された損失関数を最小化するように最適化アルゴリズムを設定します。
```python:qiita.rb
#vggと自作のtop_modelを連結
model = Model(inputs=vgg16.input, outputs=top_model(vgg16.output))

# 損失関数と最適化関数の設定
model.compile(loss='categorical_crossentropy',
             optimizer=optimizers.SGD(lr=1e-4, momentum=0.9),
             metrics=['accuracy'])
```
　
## 3.4 モデルのトレーニングと結果の可視化及びモデルの保存
evaluateメソッドで、損失と精度のスコアを算出しその後コンパイルを行います。
```python:qiita.rb
#グラフ（可視化）用コード
history = model.fit(X_train, y_train, batch_size=32, epochs=50, verbose=1, validation_data=(X_test, y_test))
score = model.evaluate(X_test, y_test, batch_size=32, verbose=0)
print('validation loss:{0[0]}\nvalidation accuracy:{0[1]}'.format(score))
#acc, val_accのプロット
plt.plot(history.history["accuracy"], label="accuracy", ls="-", marker="o")
plt.plot(history.history["val_accuracy"], label="val_accuracy", ls="-", marker="x")
plt.ylabel("accuracy")
plt.xlabel("epoch")
plt.legend(loc="best")
plt.show()
#モデルを保存
model.save("my_model.h5")
```
以下が出力された検証データの損失と精度のグラフです

![水増し後.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3560226/6227c916-d9b4-cedc-1fc6-5ae780269ec3.png)


epoch数は50で訓練し、評価は
`validation loss:[0.015072443522512913]`
`validation accuracy:[1.0]`
val_accuracyが1.0になっています。
はじめは、検証用データに水増しした画像含む全データを入れていて、その所為で
精度が異常に高くなっていると思い、検証用データに水増し前の画像のみを入れてみたものの
精度はあまり変わりませんでした。
実際にアプリ上で試してみた結果、トリミングしてある画像に対しては高い正解率が出ましたが
やはりタグ全体の写った画像の正解率は低かったです。
通常、val_accuuracyが1.0になることは無いので、画像データが少なすぎる事で過学習をおこして
いるものと思います。
もう少し画像データを集める事ができれば、過学習を起こさずに正しい精度に落ち着くと推測できます。



# 4.製作したアプリ
完成したアプリ
https://wash24.onrender.com/

　
# 5.おわりに
成果物作成にかけれる時間が少なく当初予定していた機能よりも簡易なものになってしまいましたが受講終了後には、物体検出を使ってより精度の高い複数の認識できるアプリの制作を行いたいと思います。
これから色々実装したい機能などもあるので、これから実用的になるところまでやってみようと思います。
最後に、つまずく点は多々ありましたがそれを解決できる力も付いたと思います。
この経験を生かして次の就職先を見つけれるよう頑張っていきます。