機械学習を用いた電車識別アプリを作成

**[目次]**
1.背景
2.アプリ概要及び背景
3.実行環境
4.準備
5.学習及び結果分析
6.今後の対応

**1.　背景**
子どもが電車好きで、よく電車を見に行ったり、本を見ていました。
2歳の息子は山手線と京浜東北線の区別を確り名前を言い当てて上手に識別してましたが、その本を見るとぬり絵でした。白黒の状態にも拘らず、言い当てていました。

私自身、緑が山手線、水色が京浜東北線と色でしか判別していないなと気付き、AIによる画像識別をしてみたいという発想に至りました。

**2.　アプリ概要**
電車の画像識別アプリです。山手線と京浜東北線の画像識別を行います。
まずはカラー画像を取り扱い分類を行います。加えて、グレースケールの場合で上手く識別出来るかやってみました。
勉強途上のため、まずはカラー画像で2つの分類を行いますが、今後は更に拡張して行きたいと考えています。

**3.　実行環境**
Google Colab


**4.　準備**
（1）データの収集
日本の電車を対象とした画像のデータセットは中々見当たらなかったため、画像を収集してファイルに保存して分類することにしました。山手線、京浜東北線の画像をそれぞれ約100枚準備して、trainデータとtestデータを8:2程度に分けて対応しました。
ファイルの構成は以下です。

```python
MyDrive/dataset/
├ train/
│　　├ yamanote/
│　　├ keihintouhoku/
├ test/
│　　├ yamanote/
│　　├ keihintouhoku/
```

（2）Googleドライブのマウント
収集したファイルのデータを読み込むためにマウントし、
必要なモジュールをインポートした上でディレクトリを取得します。
```python
Googleドライブのファイルをマウントする。
from google.colab import drive
drive.mount('/content/drive')
マウントしたファイルを確認する。
import glob
files = glob.glob("/content/drive/MyDrive/dataset")
for file in files:
    print(file)
#必要なモジュールをインポートする。
import os
import cv2
import numpy as np
import matplotlib.pyplot as plt
from tensorflow.keras.utils import to_categorical
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
# ディレクトリを取得する。フォルダ構成はtrainとtestフォルダに分け、それぞれに山手線と京浜東北線の写真を格納。
train_path_yamanote = os.listdir('/content/drive/MyDrive/dataset/train/yamanote')
train_path_keihintouhoku = os.listdir('/content/drive/MyDrive/dataset/train/keihintouhoku')

test_path_yamanote = os.listdir('/content/drive/MyDrive/dataset/test/yamanote')
test_path_keihintouhoku = os.listdir('/content/drive/MyDrive/dataset/test/keihintouhoku')
```

（3）画像の前処理
教師データとテストデータそれぞれで空のリストを準備します。
カラー画像を1枚ずつリストに数値化して、100×100のサイズに変換します。
```python
#画像の前処理を行う。
#空のリストを準備する。
train_img_yamanote = []
train_img_keihintouhoku = []

test_img_yamanote = []
test_img_keihintouhoku = []

#画像の数だけ抽出して出力する。
#cv2のカラー画像をRGBの順に変換し、サイズを100×100に変換する。
for i in range(len(train_path_yamanote)):
    print(train_path_yamanote[i])
    img = cv2.imread('/content/drive/MyDrive/dataset/train/yamanote/' + train_path_yamanote[i])
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    img = cv2.resize(img, (100,100))
    train_img_yamanote.append(img)

for i in range(len(test_path_yamanote)):
    print(test_path_yamanote[i])
    img = cv2.imread('/content/drive/MyDrive/dataset/test/yamanote/' + test_path_yamanote[i])
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    img = cv2.resize(img, (100,100))
    test_img_yamanote.append(img)

#山手線と同様に京浜東北線のファイルも同様の処理を行う。
for i in range(len(train_path_keihintouhoku)):
    print(train_path_keihintouhoku[i])
    img = cv2.imread('/content/drive/MyDrive/dataset/train/keihintouhoku/' + train_path_keihintouhoku[i])
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    img = cv2.resize(img, (100,100))
    train_img_keihintouhoku.append(img)

for i in range(len(test_path_keihintouhoku)):
    print(train_path_keihintouhoku[i])
    img = cv2.imread('/content/drive/MyDrive/dataset/test/keihintouhoku/' + test_path_keihintouhoku[i])
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    img = cv2.resize(img, (100,100))
    test_img_keihintouhoku.append(img)
```

（4）画像の正規化
特徴量がもつ値の重みを平等にするために、特徴量を0〜1に変換する正規化を行います。
カラーの画像データであれば3原色である赤、緑、青の色の強さををそれぞれ0~255までの離散整数値で表すことで、画像として表示されています。
従って、255で割ることで正規化を行います。
山手線か京浜東北線か判別するため、one-hotエンコーディングを行います。
```python
#画像を正規化し、trainデータとtestデータをNumpy配列にする。
X_train = np.array(train_img_yamanote + train_img_keihintouhoku)/255
y_train =  np.array([0]*len(train_img_yamanote) + [1]*len(train_img_keihintouhoku))

rand_index = np.random.permutation(np.arange(len(X_train)))
X_train = X_train[rand_index]
y_train = y_train[rand_index]

X_test = np.array(test_img_yamanote + test_img_keihintouhoku)/255
y_test =  np.array([0]*len(test_img_yamanote) + [1]*len(test_img_keihintouhoku))

# 正解ラベルをone-hotの形にします
y_train = to_categorical(y_train)
y_test = to_categorical(y_test)

```

（5）データの水増し
画像データを100枚ずつしか準備出来ていないため、精度にばらつきが出ると考え、データの水増しを行いました。同様に水増しデータも正規化を行います。
```python
#学習データに対してデータの水増しを行う。
from tensorflow.keras.preprocessing.image import ImageDataGenerator
X_datagen= ImageDataGenerator(rescale=1./255, # 255で割ることで正規化
                                   zoom_range=0.2, # ランダムにズーム
                                   horizontal_flip=True, # 水平反転
                                   rotation_range=40, # ランダムに回転
                                   vertical_flip=True) # 垂直反転
#テストデータに対しては水増しせずに正規化のみ行う。
Y_datagen = ImageDataGenerator(rescale=1./255)

#generatorに対して、flow_from_directoryを使用して、画像データを読み取らせます。
X_generator = X_datagen.flow_from_directory("/content/drive/MyDrive/dataset/train/", target_size=(100, 100),
batch_size=100, class_mode='categorical', shuffle=True)

#ラベル付けを確認
X_generator.class_indices

#水増し画像の確認
X_generate = []
y_generate = []
for _ in range(4):
    temp_X,temp_y= next(iter(X_generator))
    X_generate += list(temp_X)
    y_generate += list(temp_y)
    print(len(X_generate))
plt.figure(figsize=(12,12))
for i, image in enumerate(X_generate[:100], 1):
    plt.subplot(10,10,i)
    plt.imshow(image)
    plt.axis('off')
X_generate=np.array(X_generate)
y_generate=np.array(y_generate)

```
4回水増しを行いましたが、trainデータが159枚、batch-size100のため、100-159-259-318と100枚、59枚の水増しが繰り返し行われました。
※以下画像は一部切り抜きしていますが、上記コードでは100枚出てきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3500071/777c3bb8-d3d4-6ef6-3f68-764fb7550b79.png)


**5.学習及び結果分析**
vgg16を用いて学習を行いました。vgg16とは、2014年に発表され、「ImageNet」と呼ばれる大規模画像データセットで学習された16層からなるCNNモデルのことです。

（1）水増しデータがない場合
過学習を抑制するためにDropoutを行いましたが精度が落ちたため、用いませんでした。また、ReLU関数よりもsigmoid関数の方が精度が高かったです。
```python
# モデルにvggを使います
input_tensor = Input(shape=(100, 100, 3))
vgg16 = VGG16(include_top=False, weights='imagenet', input_tensor=input_tensor)

# vggのoutputを受け取り、2クラス分類する層を定義します
# その際中間層を下のようにいくつか入れると精度が上がります
top_model = Sequential()
top_model.add(Flatten(input_shape=vgg16.output_shape[1:]))
top_model.add(Dense(256, activation='sigmoid'))
top_model.add(Dense(128, activation='sigmoid'))
#top_model.add(Dropout(0.5))
top_model.add(Dense(2, activation='softmax'))

# vggと、top_modelを連結します
model = Model(vgg16.inputs, top_model(vgg16.output))

# vggの層の重みを変更不能にします
for layer in model.layers[:19]:
    layer.trainable = False

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

# 学習を行います
history = model.fit(X_train, y_train, batch_size=100, epochs=30, validation_data=(X_test, y_test))

#正解率と損失率を可視化して結果を見る
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()
```
`loss: 0.4908 - accuracy: 0.8167`
`Test loss: 0.4908309280872345`
`Test accuracy: 0.8166666626930237`

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3500071/69d2b1d2-43c5-fde3-727f-da834a635a8c.png)

（2）水増しデータを用いた場合
単純に水増しを行えば精度が向上すると思いましたが、精度は逆に落ちてしまいました。
epoch数やReLU関数の変更、モデルの切り替えなども試しましたが、中々向上しませんでした。
`loss: 1.8474 - accuracy: 0.4667`
`Test loss: 1.8473849296569824`
`Test accuracy: 0.46666666865348816`
```python
history = model.fit(X_generate, y_generate, batch_size=100, epochs=50, validation_data=(X_test, y_test))
```

（3）グレースケールの場合
カラー画像と同等だが、以下の部分を変更した。vgg16ではカラー画像で取り扱うことが一般的のようなので実装する際はご注意下さい。
```python
#画像の数だけ抽出して出力する。
#cv2のカラー画像をGrayScaleで白黒に変換し、サイズを100×100に変換する。
for i in range(len(train_path_yamanote)):
    print(train_path_yamanote[i])
    img = cv2.imread('/content/drive/MyDrive/dataset/train/yamanote/' + train_path_yamanote[i],cv2.IMREAD_GRAYSCALE)
    img = cv2.cvtColor(img, cv2.COLOR_GRAY2RGB)
#GrayScaleのため、RGBの入れ替えは無し。
#    b,g,r = cv2.split(img)
#    img = cv2.merge([r,g,b])
    img = cv2.resize(img, (100,100))
    train_img_yamanote.append(img)

for i in range(len(test_path_yamanote)):
    print(test_path_yamanote[i])
    img = cv2.imread('/content/drive/MyDrive/dataset/test/yamanote/' + test_path_yamanote[i],cv2.IMREAD_GRAYSCALE)
    img = cv2.cvtColor(img, cv2.COLOR_GRAY2RGB)
#GrayScaleのため、RGBの入れ替えは無し。
#    b,g,r = cv2.split(img)
#    img = cv2.merge([r,g,b])
    img = cv2.resize(img, (100,100))
    test_img_yamanote.append(img)
```
```python
#学習データに対して白黒でも対応可能なものに変換する。
X_generate = np.array(list(map(lambda x:cv2.cvtColor(x,cv2.COLOR_GRAY2RGB),X_generate)))
```
`loss: 0.7055 - accuracy: 0.5000`
`Test loss: 0.705535888671875`
`Test accuracy: 0.5`

＜考察＞
100枚ずつの画像識別で、画像水増しすれば精度向上すると考えていたが、単純に増やしただけでは上手く行かず、より多くのモデル構造や関数で比較してみないと簡単には精度向上しない。

また、グレースケールではカラーに比べて識別が難しくなると考えていたが、大きな精度減少は無かった。これは、カラーでの識別精度が高くないことによるものと考えられるのでもう少し改善していきたい。
```python
# pred_gender関数に写真を渡して電車の種類を予測します
img = cv2.imread('/content/drive/MyDrive/dataset/test/yamanote/' + test_path_yamanote[0])
b,g,r = cv2.split(img)
img1 = cv2.merge([r,g,b])
plt.imshow(img1)
plt.show()
print(rail(img))
```

（1）カラー画像の場合
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3500071/5fbd160f-5d07-7048-034d-d8f2a05f8919.png)

（2）グレースケールの場合
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3500071/ab791ecc-2372-0532-0c15-d4072f1de7ef.png)


**6.今後の対応**
今後、2種類の分類だけでなく、より多くの分類が出来るようにしていきたい。
また、別モデルの適用による精度改善、スクレイピングによる手法の理解などにも取り組んでみたい。
更に、画像だけでなくYOLO等を用いた動画にもチャレンジしてみたい。


E資格へのチャレンジするため、一旦ここまでの考察とし、自己研鑽しながら引き続き改善していきたい。


＜参考＞
1．ImageDataGenerator
https://qiita.com/okateru/items/1465215d12257493dd7b