[Python]機械学習を用いてはんだクラック判別するアプリを作成する！



<h1>はじめに</h1>

<p>はじめまして。エンジニア転職を目指して日々プログラミング学習をしています回路設計エンジニアです。<br>
   初めて作成したアプリを学習のアウトプットとして投稿します。Qita初投稿、初学者なので至らぬ点があると思いますが、暖かくコメントやご指摘をいただけると幸いです。

<h1>目的</h1>
<p>はじめてアプリを作成するにあたり何をしたいのか考えたときに現職の業務に機械学習を活かせないかと思いました。<br>
そこで「人(評価者)によって判断に差がある業務」、「学習用のデータが多数保存されている」2点を軸に考え、その結果、基板のはんだクラックの有無を判別するアプリを作成し、人(評価者)に判断に左右されず平等な判別を実現する。

<h1>はんだクラックとは？</h1>
<p>普段使用しているあらゆる電気製品の内部にある基板は、はんだという金属で基板と電子部品が接合されています。<br>
その基板と部品を接合しているはんだにクラック(ひび)が発生することをはんだクラックと呼んでいます。<br>
製造時にはんだにクラック(ひび)が発生している状態だと経年劣化や使用時にクラックが拡大していき基板破損や動作不良に至ります。<br>
製品品質を担保するために基板にはんだクラックがないか確認をしています。<br>

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2591474/2e798f25-d09f-2b0d-ec84-b00ba564444e.jpeg" width=50% height=50%>

ただし、この確認は人が顕微鏡を用いて一つ一つ目視確認をしている状況です。<br>そのため、確認者(ベテランから新人まで)によって判定に差異が発生しています。<br>判定に悩んだ時は、知見者含め何名かで再度確認し、クラックの有無を判別決定しています。<br>そこで、機械学習を用いた判別するアプリを作成し人によるクラック有り無しの判定のばらつきと再確認の工数を削減する。

<h1>アプリ作成で行ったこと</h1>

1.学習用の写真データの収集
2.収集した写真の水増し
3.機械学習(ディープラーニング)を用いて判別するモデルの作成
4.Flaskを使用してwebアプリケーションを作成

開発環境はVisual Studio Codeで、開発言語はpythonで作成しました。

<h1>学習用の写真データの収集</h1>
<p>はんだクラック有り無しの写真を集め、はんだクラックの有無を分類しました。<br>
はんだクラックの写真は自身で評価していた写真を使用した。<br>
はんだクラック有りとはんだクラック無し写真をそれぞれ100枚用意した。<br>

左の写真が「クラックあり」とし、右の写真が「クラックなし」の写真例です。<br>
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2591474/2e798f25-d09f-2b0d-ec84-b00ba564444e.jpeg" width=50% height=50%><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2591474/a4eab3fb-b0f1-f96a-1016-32a397ffcf01.jpeg" width=50% height=50%>

<h1>収集した写真の水増し</h1>
<p>学習用の写真がはんだクラック有り無し合わせて200枚しかないので、画像の水増しを行い学習データ数を増やします。<br>
画像の水増しには、KerasのImageDataGeneratorを用いました。<br>
今回は、画像回転、水平移動、垂直移動、水平反転、垂直反転、拡大縮小を行いました。
他にも引数を設定で白色化や標準化なども行えます。

<details><summary>コードを表示</summary>

```python
from keras.preprocessing.image import ImageDataGenerator
from keras.preprocessing.image import image, load_img, img_to_array
import matplotlib.pyplot as plt
import numpy as np
import os
import cv2

#水増ししたい画像の格納場所
input_data_path = "./handadata/"

#カテゴリーの作成
categorys = ["crack","no_crack"]

#水増しした画像の保存先
output_data_path = "./handadata/inflatdata/"

#水増しした画像の保管リスト
inflated_datas = []

# ImageDataGeneratorの生成
datagen = ImageDataGenerator(
rotation_range=90, # ランダムに回転させる範囲
width_shift_range=0.2, # ランダムに幅をシフトさせる範囲
height_shift_range=0.2, # ランダムに高さをシフトさせる範囲
#shear_range = 0.4,       # せん断の度合い
channel_shift_range = 50, # RGBそれぞれにランダムな値を加算する 
zoom_range=0.1,        # ランダムにズームさせる範囲
horizontal_flip=True, # ランダムに水平方向に反転させる
vertical_flip=True, # ランダムに垂直方向に反転させる
)

#水増し回数
N_img = 10

#水増し処理の関数
#関数でしたいこと
#1.まず、各フォルダ内の画像データを参照する
#2.フォルダ内のデータを取り出す
#3.取り出したデータのサイズをそろえる
#4.ImageDataGeneratorを用いて水増しをする
#5.データとカテゴリーを保存する

def inflated_data():
  for i, category in enumerate(categorys):
    #1.まず、各フォルダ内の画像データを参照する
    data_path = os.path.join(input_data_path, category)

    for image in os.listdir(data_path):
        try:
            #2.フォルダ内のデータを取り出す
            img = cv2.imread(os.path.join(data_path, image))
            #3.取り出したデータのサイズをそろえる
            resize_image = cv2.resize(img, (100,100))
            img_array = np.expand_dims(resize_image, axis=0)
            count = 0
            
            #4.ImageDataGeneratorを用いて水増しをする
            for inf_data in datagen.flow(img_array,save_to_dir = output_data_path,save_prefix = category,
                                    save_format = 'jpeg'):
                count += 1
                inf_data = np.resize(inf_data, (inf_data.shape[1],inf_data.shape[2],inf_data.shape[3]))
                #ImageDataGeneratorを用いて水増ししたデータとラベルを格納する。
                inflated_datas.append([inf_data, i])
                if count == N_img:
                    break
        except Exception as e:
            pass


inflated_data()

#5.データとカテゴリーを保存する
x = []
y = []
for image_array, label in inflated_datas:
    x.append(image_array)
    y.append(label)

x = np.array(x)
y = np.array(y)

np.savez("./handadata/exsam/data.npz", x,y)
```

</details>

<h1>機械学習(ディープラーニング)を用いて判別するモデルの作成</h1>
<p>機械学習には、転移学習を用いました。<br>
転移学習とは、学習され公開されている学習済みのモデルを使って新たなモデルの学習を行うことを転移学習といいます。<br>
今回は、2014年のILSVRCという大規模な画像認識のコンペティションで2位になった、オックスフォード大学VGG(Visual Geometry Group)チームが作成したネットワークモデルであるVGG16(16層の畳み込みニューラルネットワーク)を使用しました。<br>

<details><summary>コードを表示</summary>


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

npz = np.load("./handadata/exsam/data.npz")

npz_x = npz['x']
npz_y = npz['y']

X_train = npz_x[:int(len(npz_x)*0.8)]
y_train = to_categorical(npz_y[:int(len(npz_y)*0.8)])
X_test = npz_x[int(len(npz_x)*0.8):]
y_test = to_categorical(npz_y[int(len(npz_y)*0.8):])

input_tensor = Input(shape=(100,100,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(512, activation = 'relu'))
top_model.add(BatchNormalization())              #追加箇所
top_model.add(Dense(128, activation = 'relu'))   #追加箇所
top_model.add(BatchNormalization())              #追加箇所
top_model.add(Dense(32, activation = 'relu'))    #追加箇所
top_model.add(BatchNormalization())              #追加箇所
top_model.add(Dropout(0.5))                      #追加箇所
top_model.add(Dense(2,activation = 'softmax'))

history = model = Model(inputs = vgg16.input, outputs = top_model(vgg16.output))

for layer in model.layers[:19]:
    layer.trainable = False

model.compile(loss= 'categorical_crossentropy',optimizer=optimizers.SGD(lr=0.001 , momentum=0.9),metrics=['accuracy'])

model.fit(X_train,y_train,validation_data=(X_test,y_test),batch_size= 32, epochs=10)

model.save('model1.h5')

scores = model.evaluate(X_test,y_test,verbose=1)
print('Test loss:', scores[0])
print('Test accuracy:', scores[1])

model.summary()

#学習結果をmatplotlibでグラフの描画を行う
acc = history.history['accuracy']
val_acc = history.history['val_accuracy']
loss = history.history['loss']
val_loss = history.history['val_loss']

epochs = range(len(acc))

plt.plot(epochs, acc, 'r', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title('Training and validation accuracy')
plt.legend()

plt.figure()

plt.plot(epochs, loss, 'r', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.legend()
plt.show()
```

</details>

1回目はVGG16の転移学習あとに512のユニットを持ち活性化関数relu関数の層を後に出力層で学習を行いました。
epoch(学習回数)は100回で学習したグラフになります。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2591474/901d8079-36ca-8528-11c7-8bf32ce3eeec.png" width=50% height=50%><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2591474/282fdbbd-296a-f768-3c05-4e92c4e1aab2.png" width=50% height=50%>
Test loss: 0.12134046852588654
Test accuracy: 0.9558303952217102

正答率、損失ともに学習回数30回ほどで横ばいになり、訓練データではほぼ正答率100%、損失0.0に近くなったが、テストデータでは、正答率95%、損失0.2ほどでとどまり、過学習とまではいかないがその傾向があり訓練データとの差異が見られました。

精度を上げるためにバッチ正則化と128のユニットを持ち活性化関数relu関数の層、ドロップアウトの層を追加して再度学習を行いました。<br>追加箇所は上記コード内にコメントアウトで「#追加箇所」と記載している部分です。<br>追加したバッチ正則化とドロップアウトの役割は次の通りです。

<h4>バッチ正則化</h4>
レイヤごとにインプットを正規化を行い、その際にミニバッチごとの統計量(平均・分散)を使ってを正規化する。見込まれる効果としては、<br>
・学習が安定する。<br>
・パラメータのスケールや初期値の影響が小さくなる。それにより、学習スピードが速くなる。<br>
・ドロップアウトの必要性を減らすことができる。
<h4>ドロップアウト</h4>
  モデルの精度をあげるための手法の一つ<br>レイヤーの出力を学習時にランダムで0に落とすことで、一部のデータが欠損していても正しく認識ができるようにします。<br>
  これにより、訓練データに対する過学習を抑制する効果があります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2591474/444987c5-1360-62fc-7d3c-39f735994e13.png" width=50% height=50%><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2591474/19a14aa3-1243-ae14-84c1-8f771b02ea3d.png" width=50% height=50%>
Test loss: 0.07055503875017166
Test accuracy: 0.9734982252120972

学習回数は1回目の結果から30回程度で大丈夫と判断できるが、上記結果と比較するために100回実施した。
1回目と比較して少ない学習回数で訓練データとテストデータの正答率及び損失が近づくことがわかった。学習回数は30回程度で頭打ちすることには変わりはないが、訓練データとテストデータの正答率と損失の差異が小さくなり、正答率は0.955から0.973損失は0.121から0.073と学習モデルの精度の向上も確認できました。

<h1>Flaskを使用してwebアプリケーションを作成</h1>
Flask（フラスク）は小規模向けの簡単なWebアプリケーションを作るのに適しているPythonのWebアプリケーションフレームワークです。

先ほど、作成した学習モデルを用いてFlaskでアプリ作成を行いました。
Flaskアプリにはウェブページ側としてHTML、ウェブページのデザインのCSS、アプリの動作を担うFlaskのメイン部分(今回はpythonで作成)が必要になります。

今回のアプリは、はんだ画像を送信してクラックの有り無しを判別するメインページと、はんだクラックとはなにか説明を記載した説明ページの構成で作成しました。
メインページと説明ページは互いにリンクで行き来できる構成です。

<details><summary>Flaskのメイン部分コードを表示</summary>

```python
from fileinput import filename
import os
from unicodedata import name
from flask import Flask,request,redirect,render_template,flash
from werkzeug.utils import secure_filename
from tensorflow.keras.models import Sequential, load_model
from tensorflow.keras.preprocessing import image
import numpy as np

classes = ["クラックあり","クラックなし"]
image_size = 100

UPLOAD_FOLDER = "uploads"
ALLOWED_EXTENSIONS = set(['png', 'jpg', 'jpeg', 'gif'])

app = Flask(__name__, static_folder='./statics')

def allowed_file(filename):
    return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

model = load_model('./model1.h5')#学習済みモデルをロード

#はんだクラック説明ページ
@app.route('/crack', methods=['GET', 'POST'])
def crack():
    name= "はんだクラック"
    messa = "はんだクラックとは"
    return render_template("handa_crack.html",message=messa,name=name)

@app.route('/', methods=['GET', 'POST'])
#ルートページ
def upload_file():
    if request.method == 'POST':
        if 'file' not in request.files:
            flash('ファイルがありません')
            return redirect(request.url)
        file = request.files['file']
        if file.filename == '':
            flash('ファイルがありません')
            return redirect(request.url)
        if file and allowed_file(file.filename):
            filename = secure_filename(file.filename)
            file.save(os.path.join(UPLOAD_FOLDER, filename))
            filepath = os.path.join(UPLOAD_FOLDER, filename)

            #受け取った画像を読み込み、np形式に変換
            img = image.load_img(filepath, target_size=(image_size,image_size))
            img = image.img_to_array(img)
            data = np.array([img])
            #変換したデータをモデルに渡して予測する
            result = model.predict(data)[0]
            predicted = result.argmax()
            pred_answer = "送信したはんだ写真は " + classes[predicted] + " です"

            return render_template("index.html",answer=pred_answer)

    return render_template("index.html",answer="")

if __name__ == "__main__":
    port = int(os.environ.get('PORT',8080))
    app.run(host = '0.0.0.0', port = port)
```

</details>

<details><summary>メインページのHTMLを表示</summary>

 ```html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>はんだクラック判別</title>
    <link rel="stylesheet" href="../statics/css/stylesheet.css">
</head>
<body>
    <header>   
        <a class="header-logo" href="#">Solder crack discriminator</a>
    </header>

    <div class="main"> 
        <img class="kiban_img" src="../statics/image/image.png" alt="kiban">
        <div class="right">
            <h2> はんだ画像からクラックの有無を判別します</h2>
            <p>画像を送信してください</p>
            <form method="POST" enctype="multipart/form-data">
                <input class="file_choose" type="file" name="file">
                <input class="btn" value="submit!" type="submit">
            </form>
            <div class="answer">{{answer}}</div>
        </div>
    </div>

    <footer>
        <a class="footer_logo" href="{{url_for('crack')}}">   はんだクラックとは？</a> 
    </footer>
</body>
</html>
```

</details>

<details><summary>説明ページのHTMLを表示</summary>

```html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>はんだクラック判別</title>
    <link rel="stylesheet" href="../statics/css/stylesheet.css">
</head>

<body>
    <header>
        <h3 class="header-logo">{{message}}</p>
    </header>
    <div class="crackmain">
        <h3>はんだクラックとは製造行程ではんだ付けしたときにはんだが綺麗に付かずひび割れができること</h3>
        <h4>はんだクラックがある状態だと製品を使用し続けるとクラックが進行し製品が動かなくなる動作不良に至る</h4>
        <div class="crack">
            <img class="crack_img" src="../statics/image/nocrack.JPG" alt="kiban">
            <p>正常だと左の写真のようにはんだ部分にひびはなく綺麗にはんだがついている</p>
        </div>
        <div class="crack">
            <img class="crack_img" src="../statics/image/crack.JPG" alt="kiban">
            <p>クラックが発生すると左の赤丸部分ようにはんだ部分にひび(クラック)が発生している。<br>
            このまま製品として使用し続けるとひびがだんだん大きくなり製品が故障してしまう。</p>
            
        </div>

        <h3>現状は作業者が目視確認でチェックしており、作業者毎に判別にバラつきが発生しているため本アプリを作成した</h3>       
    </div>
</body>
<footer>
    <a class="footer_logo" href="{{url_for('upload_file')}}">   トップページにもどる</a>
</footer>
</html>
```

</details>

<details><summary>CSSを表示</summary>

```css
header {
    background-color: #76B55B;
    height: 60px;
    margin: -8px;
    display: flex;
    flex-direction: row-reverse;
    justify-content: space-between;
}

.header-logo {
    color: #fff;
    font-size: 25px;
    margin: 15px 25px;
}

.header_img {
    height: 25px;
    margin: 15px 25px;
}

.main {
    height: 550px;
    display: flex;
}
.kiban_img{
    height: 500px;
    width: 500px;
    margin: 15px 15px;
}

h2 {
    color: #1fc0a5;
    margin: 100px 10px;
    text-align: center;
}

p {
    color: #444444;
    margin: 50px 0px 50px 0px;
    text-align: center;
}

.answer {
    color: #dd1414;
    margin: 70px 0px 30px 0px;
    text-align: center;
}

form {
    text-align: center;
}

footer {
    background-color: #F7F7F7;
    height: 50px;
    margin: -8px;
    position: relative;
}

.footer_logo {
    margin: 15px 25px;
    position: absolute;
    left: 0;
}

.crack_img{
    height: 300px;
    width: 300px;
    margin: 15px 15px;
}

.crack{
    display: flex;
}
```

</details>

実際に作成したwebアプリは以下になります。

[はんだクラック判定アプリ](https://soldercrackdiscriminator.herokuapp.com/)

はんだクラックデータを持っている人は少ないと思いますのでアプリ動作のデモ動画になります。
![Videotogif (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2591474/43b2fb2d-3e79-9325-3d7b-6563a9432907.gif)

当初の課題であった「確認者(ベテランから新人まで)によって判定に差異が発生してしまう」問題がこのアプリを用いれば、確認者のレベルに関係なく、はんだクラックの有り無し判別できるようになりました。

<h1>まとめ</h1>

今回は、はんだクラックの写真があったのでデータ集めにそこまで苦労しませんでした。しかし、フリーのはんだ写真のデータセットは探した限り見つからなかったので手持ちのデータがなければこのアプリはできなかったです。機械学習はデータ集めや前処理が大切であることを学習を通して実感できたと思います。また、手持ちデータはありましたがデータ数が少なかったため、クラックの有り無しを判別するモデルを作成しましたが、今後はもっとデータ数を集めてクラックの大きさで分類する数を増やしたいと思います。

転移学習を用いてモデル作成を行ったため、精度の高いモデルの作成ができましたが、知識を身に着けるという意味では畳み込み部分も一度、自分で設計してみたいと思います。また、転移学習に用いられるモデルもさまざまあり、vgg19(深さが19層の畳み込みニューラルネットワーク)やResNet-50(深さが50層の畳み込みニューラルネットワーク)などもあるみたいなので試してみて精度に違いがあるのか比較もしてみたいと考えています。

初投稿ということもあり長文ですが最後までお付き合いいただきありがとうございました。