EfficientNetによるRaspberry Piを用いたリアルタイム画像分類



# Raspberry Piを使ったセルフレジを製作
機械学習実務未経験者が勉強目的でセルフレジを製作してみました。
学習済みの5種類のペットボトルを正しく分類し価格を表示し、学習していないものに対してはエラーを返すことを目標とする設計にしました。
また、撮影から値段の表示までの時間が五秒以内に収まることを条件にします。

### セルフレジの外観
白いボックス内に撮影するペットボトルを設置する。
カメラで商品を撮影し、PC上に商品名、価格などを表示する。
![スクリーンショット 2020-02-05 18.12.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/568664/f0a97342-901e-4232-4473-f469e090bb28.png)


### データの収集
trainデータ計300枚(各5種類のペットボトルをサンプル60枚ずつ、360度回転させて写真を撮影)
![IMG_0388.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/568664/b3ef84b8-9771-0325-4708-9a440fbf9785.jpeg)



# EfficientNetとは
2019年に発表されたモデルで、グラフに示すように少ないパラメータ数で高い精度を出すことができる。
モデルのパラメータ数に応じてEfficientNet-B0からEfficientNet-B7まで選択できる。
今回は撮影から値段の表示まで五秒以内という目標から、速度と精度を加味し、EfficientNet-B3を使用し学習を行った。
![x1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/568664/7e680d0e-f8c2-b909-a92d-83725839eac2.png)


↓論文
https://arxiv.org/abs/1905.11946
↓EfficientNetを詳しくまとめてくれた方がいらっしゃるので共有します。
https://qiita.com/omiita/items/83643f78baabfa210ab1


# ArcFace
畳み込みニューラルネットワークは、識別機能を学習する能力が高いため、近年、顔認識のパフォーマンスを大幅に向上させました。
その中でもArcFaceは、動画や大量の画像など10種もの顔認識データセットを適用し、従来モデルと比較した結果、最高精度の結果を記録。
今回は形状の似たペットボトルを認識するということでArcFaceを応用出来ないかと思い、ArcFaceを使用しました。
![スクリーンショット 2020-02-05 16.47.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/568664/9802d87d-b7d3-c3b3-0a87-56f3c390cc04.png)

↓論文
https://arxiv.org/abs/1801.07698
↓詳しくはこちらのQiita参照
https://qiita.com/yu4u/items/078054dfb5592cbb80cc

# 実装
こちらの記事にはモデルの実装、学習の課程のコードのみ掲載します。
Raspberry Pi周りの実装は以下のgitを参照してください。
https://github.com/teamozawa/group_work

### 学習データの水増し
ArcFaceに対応させるためのクラスを作成しました。

```py
from keras.preprocessing.image import ImageDataGenerator

class Generator_xandy(object):
    def __init__(self):
        train_dir="train_data"
        test_dir="test_data"
        batch_size=32
        datagen = ImageDataGenerator(zoom_range=[0.95, 1.0],width_shift_range =0.1,height_shift_range=0.1, brightness_range=[0.95, 1.05], rescale=1/255.)

        train_gen = datagen.flow_from_directory(
            train_dir,
            target_size=(224,224),
            batch_size=batch_size,
            class_mode='categorical',
            shuffle=True,
            subset = "training" 
        )
        
        self.gene = train_gen
        
    def __iter__(self):
    
        return self
    
    def __next__(self): 
        X, Y = self.gene.next()
        #元はreturn X, Y
        return [X,Y], Y
    
train_generator=Generator_xandy()
val_generator=Generator_xandy()
```

### モデルの実装→学習
```py
from keras import backend as K
from keras.engine.topology import Layer
import numpy as np
import tensorflow as tf

#arcface layerの実装
class Arcfacelayer(Layer):
    def __init__(self, output_dim, s=30, m=0.50, easy_magin=False):
        self.output_dim = output_dim
        self.s = s
        self.m = m
        self.easy_magin = easy_magin
        super(Arcfacelayer, self).__init__()
        
    def build(self, input_shape):
        # Create a trainable weight variable for this layer.
        self.kernel = self.add_weight(name='kernel',
                                      shape=(input_shape[0][1], self.output_dim),
                                      initializer='uniform',
                                      trainable=True)
        super(Arcfacelayer, self).build(input_shape)
        
    def call(self, x):
        y = x[1]
        x_normalize = tf.math.l2_normalize(x[0]) #|x = x'/ ||x'||2
        k_normalize = tf.math.l2_normalize(self.kernel) # Wj = Wj' / ||Wj'||2
        
        cos_m = K.cos(self.m)
        sin_m = K.sin(self.m)
        th = K.cos(np.pi - self.m)
        mm = K.sin(np.pi - self.m) * self.m
        
        cosine = K.dot(x_normalize, k_normalize) # W.Txの内積
        
        sine = K.sqrt(1.0 - K.square(cosine))
        phi = cosine * cos_m - sine * sin_m
        
        if self.easy_magin:
            phi = tf.where(cosine > 0, phi, cosine) 
            
        else:
            phi = tf.where(cosine > th, phi, cosine - mm) 
        
        output = (y * phi) + ((1.0 - y) * cosine) # true cos(θ+m), False cos(θ)
        output *= self.s
        
        return output
    
    def compute_output_shape(self, input_shape):
        
        return (input_shape[0][0], self.output_dim)
        
```

```py
from keras.models import Model
from keras.models import Sequential
from keras.layers import Activation 
from keras.layers import Input, Dense, GlobalAveragePooling2D
import efficientnet.keras as efn

n_categories=5
#B3の部分をB0~B7と変えるだけでモデルを変更可能
base_model = efn.EfficientNetB3(input_shape=(224,224,3), weights="imagenet",include_top=False)
    
#ArcFaceLayerをくっつける
x = base_model.output
yinput = Input(shape=(n_categories,))
hidden = GlobalAveragePooling2D()(x)
x = Arcfacelayer(5, 40, 0.05)([hidden,yinput])
prediction = Activation('softmax')(x)
efn_model = Model(inputs=[base_model.input,yinput],outputs=prediction)

efn_model.compile(optimizer='rmsprop',
              loss='categorical_crossentropy',
              metrics=['accuracy'])
#学習
efn_model.fit_generator(train_generator, steps_per_epoch=5, epochs=300, verbose=1,callbacks=[modelCheckpoint], 
                         validation_data=val_generator, validation_steps=5, class_weight=None, max_queue_size=10, workers=1, 
                        use_multiprocessing=False, 
                        shuffle=True, initial_epoch=0)
```
学習した5種類のペットボトルの特徴ベクトルと、商品の特徴ベクトルを比較し、コサイン類似度を算出することによって未学習の商品を判別する。

```py
def get_hold_vector(model, classes=[1,2,3,4,5]):
    """
    classes: クラス名のリスト　イメージの名前はこのリスト名にしてください
    hold_dir: str イメージが入ったフォルダpath
    """
    
    hold_vector=np.zeros((5,1280))
    train_path="/content/train_data"

    for clas in classes:
        img_array = np.empty((0, 224,224,3))
        for i in range(60):
            imagepath=glob.glob("/content/train_data/{}/*".format(clas))[i]
            
            img = load_img(imagepath, target_size=(224,224))
            array = img_to_array(img).reshape(1, 224, 224, 3)
            img_array = np.vstack((img_array, array))
       
            
        img_array = img_array/255.0
        vector = model.predict(img_array)
        vector=vector.mean(axis=0)
        hold_vector[clas-1, :]=vector
    return hold_vector
#予測用のモデル
predict_model=Model(efn_model.get_layer(index=0).input, efn_model.get_layer(index=-4).output)
predict_model.save("models.h5")

#学習データ（5種類のペットボトル）のベクトル
hold_vector=get_hold_vector(model=predict_model)
np.save(
    "hold_vector.npy", # データを保存するファイル名
    hold_vector,  # 配列型オブジェクト（listやnp.array)
)

```
# 結果
学習した5種類のペットボトルを正確に分類し、学習していない未知データ5種類に対してはエラーを返すことに成功しました。
EfficientNet使用することで、学習した5種類のペットボトルに対してはコサイン類似度が0.999..と高い数値が算出される為、閾値を0.993と高い値に設定することができました。これにより、未知データはかなり似たデザインのペットボトルでも判別ができました。
また、撮影から値段の表示までが2秒前後だった為速度の目標も達成できました。
結論として、EfficientNetを使用することで高速かつ高い精度を得られ、ArcFaceはペットボトルの分類にも応用できることがわかりました。

（上が学習した商品。こちらはしっかり５値分類できた。下は未学習のサンプル。学習した商品と非常に似ているがエラーを返すことができた。）
![IMG_0388.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/568664/f1fc18a3-35b4-c8c1-167f-a3b4e1cf69d2.jpeg)
![IMG_0389.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/568664/f201cd3e-c7c5-093f-a350-0b65a13b4472.jpeg)





# まとめ、感想
・未学習データを判別するのにArcFaceは有効
・EfficientNetは軽くて精度が良い