画像認識の最近の記事

ディープラーニングを使って、人の顔の画像を入力すると 年齢・性別・人種 を判別するモデルを作ります。
身近な機械学習では1つのデータ(画像)に対して1つの予測を出力するタスクが一般的ですが、今回は1つのデータ(画像)で複数の予測(年齢・性別・人種)を予測します。


実装方法

学習用データ

まず、学習用に大量の顔画像が必要になりますが、ありがたいことに既に公開されているデータセットがあります。 UTKFace というもので、20万枚の顔画像が含まれています。また、年齢(0-116歳)、人種(白人、黒人、 アジア系、インド系、その他)、性別はもちろんですが、表情や画像の明るさ・解像度も多種多様なものがそろっています。
utkface.png

モデル

次に予測モデルについてですが、Efficient Net という2019/5月に Googleが発表したモデルを使います。このモデルは従来よりかなり少ないパラメータ数ながら、高い精度を誇る優れたモデルです。Kaggleのようなコンテストでも既に多用されていて、上位の人たちの多くが使っています。(参考:このコンテストでは上位陣の多くがEfficient Netを取り入れていました 詳しくは元論文や、その解説記事を参考にしてください。

efficientnet.png

加えて、今回のタスクは1つの入力(顔画像)から3つの出力(年齢・性別・人種)を返す、いわゆる複数出力型(Multi Output)にする必要があることにも注意します。


実装例

プログラミング環境 いくらEfficient Netの計算コストが小さいとはいっても、学習データの数も多く普通のCPUでは時間がかかりすぎてしまいます。専用GPU付PCがあればそれで良いのですが、私はないのでGoogle Colaboratoryのような外部のGPUを使う必要があります。実はKaggleにもGPU提供の機能があり、しかもKaggleの場合、あらかじめデータセットがNotebookに読み込まれている場合があります。つまり、配布元のサイトからデータをダウンロードしてくる必要がありません。今回使うUTKFaceも既に用意されているので、ありがたくこれを使っていきます。

まず、ここに行き、右にあるNew Notebookをクリックします。

how1.png

今回はPythonで実装するので、そのまま createを選択します。しばらくするとデータが読み込まれた状態のNotebookが使えるようになります。これで環境構築は完了です。

この時、図のように、【Settings】の項目のうち、 Internet を On に、Accelerator を GPU に設定 しておきましょう。

how2.png


コード全文

それではいよいよ実装していきます。ここでは、最初にコード全文を載せ、後で詳細に解説していきたいと思います。

# This Python 3 environment comes with many helpful analytics libraries installed
# It is defined by the kaggle/python docker image: https://github.com/kaggle/docker-python
# For example, here's several helpful packages to load in 

import numpy as np # linear algebra
import pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)

# Input data files are available in the "../input/" directory.
# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory

import os
for dirname, _, filenames in os.walk('/kaggle/input'):
    for filename in filenames:
        print(os.path.join(dirname, filename))

# Any results you write to the current directory are saved as output.


###### EfficientNetをインストール
! pip install -U efficientnet


###### 必要なライブラリを読み込む
import glob
import matplotlib.pyplot as plt
import seaborn as sns
from PIL import Image

from efficientnet.keras import EfficientNetB4  # Bの後の数字を変えれば別のスケールのモデルを使える
from sklearn.model_selection import train_test_split
from keras.utils import to_categorical

from keras.layers import Input, Dense
from keras.models import Model
from keras.callbacks import ModelCheckpoint


###### 定数を定義
DATA_DIR = '../input/utkface-new/UTKFace'
IM_WIDTH = IM_HEIGHT = 198
TRAIN_TEST_SPLIT = 0.01  # 全体の8割を訓練データ、残り2割をテストデータにする
TRAIN_VALID_SPLIT = 0.7  # 訓練データのうち3割はバリデーションデータとして使う
ID_GENDER_MAP = {0: 'male', 1: 'female'}  # IDから性別へ変換するマップ
GENDER_ID_MAP = dict((g, i) for i, g in ID_GENDER_MAP.items())  # IDと性別の逆引き辞書
ID_RACE_MAP = {0: 'white', 1: 'black', 2: 'asian', 3: 'indian', 4: 'others'}  # IDから人種へ変換するマップ
RACE_ID_MAP = dict((r, i) for i, r in ID_RACE_MAP.items())  # IDと人種の逆引き辞書


###### ファイル名から正解ラベルを取り出す関数
def parse_filepath(filepath):
    # 年齢(int)、性別(str)、人種(str) を返す
    try:
        path, filename = os.path.split(filepath)  # 相対パスからファイル名を取り出す
        filename, ext = os.path.splitext(filename)  # 拡張子を除く
        age, gender, race, _ = filename.split("_")  # _は無名変数
        return int(age), ID_GENDER_MAP[int(gender)], ID_RACE_MAP[int(race)]

    except Exception as e:  # いくつか欠損値があるので例外処理をしておく
        print(filepath)
        return None, None, None


###### 年齢、性別、人種、ファイル名からなるDataFrameを作成
files = glob.glob(os.path.join(DATA_DIR, "*.jpg"))  # 全ての画像ファイル名をfilesという変数にまとめる
attributes = list(map(parse_filepath, files))  # 上で作成した関数にファイル名を一つずつ入力

df = pd.DataFrame(attributes)
df['file'] = files
df.columns = ['age', 'gender', 'race', 'file']
df = df.dropna()  # 欠損値は3つ
df['gender_id'] = df['gender'].map(lambda gender: GENDER_ID_MAP[gender])
df['race_id'] = df['race'].map(lambda race: RACE_ID_MAP[race])

# 10歳以下、65歳以上の人の画像は比較的少ないので使わないことにする
df = df[(df['age'] > 10) & (df['age'] < 65)]
# その中での最高年齢
max_age = df['age'].max()

###### train, test, validationデータの分割
p = np.random.permutation(len(df))  # 並び替え
train_up_to = int(len(df) * TRAIN_TEST_SPLIT)
train_idx = p[:train_up_to]
test_idx = p[train_up_to:]

# split train_idx further into training and validation set
train_up_to = int(train_up_to * TRAIN_VALID_SPLIT)
train_idx, valid_idx = train_idx[:train_up_to], train_idx[train_up_to:]


###### データの前処理を行う関数
def get_data_generator(df, indices, for_training, batch_size=32):
    # 処理した画像、年齢、人種、性別をbatch_sizeずつ返す

    images, ages, races, genders = [], [], [], []
    while True:
        for i in indices:
            r = df.iloc[i]
            file, age, race, gender = r['file'], r['age'], r['race_id'], r['gender_id']
            im = Image.open(file)
            im = im.resize((IM_WIDTH, IM_HEIGHT))
            im = np.array(im) / 255.0  # 規格化
            images.append(im)
            ages.append(age / max_age)  # 最大年齢で規格化
            races.append(to_categorical(race, len(RACE_ID_MAP)))  # kerasの仕様に合わせ、one-hot表現に
            genders.append(to_categorical(gender, 2))  # kerasの仕様に合わせ、one-hot表現に
            if len(images) >= batch_size:  # メモリを考慮して少しずつ結果を返す
                yield np.array(images), [np.array(ages), np.array(races), np.array(genders)]
                images, ages, races, genders = [], [], [], []
        if not for_training:
            break


###### モデルの作成
input_layer = Input(shape=(IM_HEIGHT, IM_WIDTH, 3))  # 最初の層

efficient_net = EfficientNetB4(
    weights='noisy-student',  # imagenetでもよい 
    include_top=False,  # 全結合層は自分で作成するので要らない
    input_tensor = input_layer,  # 入力
    pooling='max')

for layer in efficient_net.layers:  # 転移学習はしない
    layer.trainable = True

# 複数出力にする必要があるので、efficientnetの最終層から全結合層3つを枝分かれさせる
bottleneck=efficient_net.output 

# 年齢の予測
_ = Dense(units=128, activation='relu')(bottleneck)
age_output = Dense(units=1, activation='sigmoid', name='age_output')(_)

# 人種の予測
_ = Dense(units=128, activation='relu')(bottleneck)
race_output = Dense(units=len(RACE_ID_MAP), activation='softmax', name='race_output')(_)

# 性別の予測
_ = Dense(units=128, activation='relu')(bottleneck)
gender_output = Dense(units=len(GENDER_ID_MAP), activation='softmax', name='gender_output')(_)

# efficientnetと全結合層を結合する
model = Model(inputs=input_layer, outputs=[age_output, race_output, gender_output])

# 最適化手法・損失関数・評価関数を定義してコンパイル
model.compile(optimizer='rmsprop', 
              loss={'age_output': 'mse', 'race_output': 'categorical_crossentropy', 'gender_output': 'categorical_crossentropy'},
              loss_weights={'age_output': 2., 'race_output': 1.5, 'gender_output': 1.},
              metrics={'age_output': 'mae', 'race_output': 'accuracy', 'gender_output': 'accuracy'})

# バッチサイズを定義
batch_size = 32
valid_batch_size = 32

train_gen = get_data_generator(df, train_idx, for_training=True, batch_size=batch_size)
valid_gen = get_data_generator(df, valid_idx, for_training=True, batch_size=valid_batch_size)

# 検証誤差が最も低い状態のモデルを保存しておく
callbacks = [
    ModelCheckpoint('./model_checkpoint', monitor='val_loss', verbose=1, save_best_only=True, mode='min')
]

history = model.fit_generator(train_gen,
                    steps_per_epoch=len(train_idx)//batch_size,
                    epochs=10,
                    callbacks=callbacks,
                    validation_data=valid_gen,
                    validation_steps=len(valid_idx)//valid_batch_size)


###### 損失関数、評価関数の値をプロットする関数
def  plot_train_history(history):
    fig, axes = plt.subplots(1, 4, figsize=(20, 5))
    axes[0].plot(history.history['race_output_accuracy'], label='Race Train accuracy')
    axes[0].plot(history.history['val_race_output_accuracy'], label='Race Val accuracy')
    axes[0].set_xlabel('Epochs')
    axes[0].legend()

    axes[1].plot(history.history['gender_output_accuracy'], label='Gender Train accuracy')
    axes[1].plot(history.history['val_gender_output_accuracy'], label='Gener Val accuracy')
    axes[1].set_xlabel('Epochs')
    axes[1].legend()

    axes[2].plot(history.history['age_output_mae'], label='Age Train MAE')
    axes[2].plot(history.history['val_age_output_mae'], label='Age Val MAE')
    axes[2].set_xlabel('Epochs')
    axes[2].legend()  

    axes[3].plot(history.history['loss'], label='Training loss')
    axes[3].plot(history.history['val_loss'], label='Validation loss')
    axes[3].set_xlabel('Epochs')
    axes[3].legend()

plot_train_history(history)


test_gen = get_data_generator(df, test_idx, for_training=False, batch_size=128)
dict(zip(model.metrics_names, model.evaluate_generator(test_gen, steps=len(test_idx)//128)))

test_gen = get_data_generator(df, test_idx, for_training=False, batch_size=128)
x_test, (age_true, race_true, gender_true)= next(test_gen)
age_pred, race_pred, gender_pred = model.predict_on_batch(x_test)

race_true, gender_true = race_true.argmax(axis=-1), gender_true.argmax(axis=-1)
race_pred, gender_pred = race_pred.argmax(axis=-1), gender_pred.argmax(axis=-1)
age_true = age_true * max_age
age_pred = age_pred * max_age

from sklearn.metrics import classification_report
print("Classification report for race")
print(classification_report(race_true, race_pred))

print("\nClassification report for gender")
print(classification_report(gender_true, gender_pred))

import math
n = 30
random_indices = np.random.permutation(n)
n_cols = 5
n_rows = math.ceil(n / n_cols)
fig, axes = plt.subplots(n_rows, n_cols, figsize=(15, 20))
for i, img_idx in enumerate(random_indices):
    ax = axes.flat[i]
    ax.imshow(x_test[img_idx])
    ax.set_title('a:{}, g:{}, r:{}'.format(int(age_pred[img_idx]), ID_GENDER_MAP[gender_pred[img_idx]], ID_RACE_MAP[race_pred[img_idx]]))
    ax.set_xlabel('a:{}, g:{}, r:{}'.format(int(age_true[img_idx]), ID_GENDER_MAP[gender_true[img_idx]], ID_RACE_MAP[race_true[img_idx]]))
    ax.set_xticks([])
    ax.set_yticks([])


###### 最後にモデルを保存する
model.save('my_model.h5')

以上がデータ整形から、モデル構築、学習、予測までの一連のプログラムです。 ただ上記のコードは、データの可視化や、データの分布の分析といった試行錯誤の過程を含めていません。 以降ではこれらも含めて解説していきます。

コード解説

最初から行きましょう。

# This Python 3 environment comes with many helpful analytics libraries installed
# It is defined by the kaggle/python docker image: https://github.com/kaggle/docker-python
# For example, here's several helpful packages to load in 

import numpy as np # linear algebra
import pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)

# Input data files are available in the "../input/" directory.
# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory

import os
for dirname, _, filenames in os.walk('/kaggle/input'):
    for filename in filenames:
        print(os.path.join(dirname, filename))

# Any results you write to the current directory are saved as output.

この部分は Kaggle のNotebookの最初に必ず書いてあるコードで、単純に numpy, pandas を読み込んだ後、input ディレクトリ内にある、データファイルをすべて書き出すという処理です。

続いて、efficientnet をインストールしてライブラリを読み込みます。

###### 定数を定義
DATA_DIR = '../input/utkface-new/UTKFace'
IM_WIDTH = IM_HEIGHT = 198
TRAIN_TEST_SPLIT = 0.8  # 全体の8割を訓練データ、残り2割をテストデータにする
TRAIN_VALID_SPLIT = 0.7  # 訓練データのうち3割はバリデーションデータとして使う
ID_GENDER_MAP = {0: 'male', 1: 'female'}  # IDから性別へ変換するマップ
GENDER_ID_MAP = dict((g, i) for i, g in ID_GENDER_MAP.items())  # IDと性別の逆引き辞書
ID_RACE_MAP = {0: 'white', 1: 'black', 2: 'asian', 3: 'indian', 4: 'others'}  # IDから人種へ変換するマップ
RACE_ID_MAP = dict((r, i) for i, r in ID_RACE_MAP.items())  # IDと人種の逆引き辞書

ここでは各種定数を定義しています。DATADIR は画像ファイルが入っているディレクトリを指定します。画像は後で前処理をして、IMWIDTH, IM_HEIGHT のサイズにします。 この値は本来注意深く選ぶべきです(EfficientNetの強みが生きるパラメータです)が、今回はとりあえず予測まで実装することが先決なので、適当(テキトー)な値に設定してしまいます。 精度を上げたい場合には見直さなければいけないでしょう。

###### ファイル名から正解ラベルを取り出す関数
def parse_filepath(filepath):
    # 年齢(int)、性別(str)、人種(str) を返す
    try:
        path, filename = os.path.split(filepath)  # 相対パスからファイル名を取り出す
        filename, ext = os.path.splitext(filename)  # 拡張子を除く
        age, gender, race, _ = filename.split("_")  # _は無名変数
        return int(age), ID_GENDER_MAP[int(gender)], ID_RACE_MAP[int(race)]

    except Exception as e:  # いくつか欠損値があるので例外処理をしておく
        print(filepath)
        return None, None, None

データの配布元のwebページを見ればわかるのですが、それぞれの画像ファイルの名前は、 [age][gender][race]_[date&time].jpg となっており、[age] はそのまま 0 ~ 116 までの整数、[gender] は 0 (男性) か 1 (女性)、[race] は 0 (白人) か 1 (黒人) か 2 (アジア系) か 3 (インド系) か 4 (その他--ヒスパニックやラテン系等) となっています。 従って画像ファイル名から、その画像に映っている人の情報を取り出す処理が必要で、それをしているのが上記の部分です。

###### 年齢、性別、人種、ファイル名からなるDataFrameを作成
files = glob.glob(os.path.join(DATA_DIR, "*.jpg"))  # 全ての画像ファイル名をfilesという変数にまとめる
attributes = list(map(parse_filepath, files))  # 上で作成した関数にファイル名を一つずつ入力

df = pd.DataFrame(attributes)
df['file'] = files
df.columns = ['age', 'gender', 'race', 'file']
df = df.dropna()  # 欠損値は3つ
df['gender_id'] = df['gender'].map(lambda gender: GENDER_ID_MAP[gender])
df['race_id'] = df['race'].map(lambda race: RACE_ID_MAP[race])

ここでは取り出した正解ラベルから、分析しやすいようにDataFrame を作成しています。 欠損値については

df.isnull().sum()

で調べることができ、結果は3でした。全体2万枚のうちで欠損値は3枚だけなので今回は考慮しません。

ここで、年齢について考えてみると、高齢者の写真は他の年代に比べて少ないのではないかと推測されます。もしそうであれば学習させるデータに偏りが生じることになり、予測精度が落ちてしまうでしょう。 画像の枚数が各年代で均一になるように画像の水増しをしても良いですが、ここでは簡単のためそういったマイナーな分は捨象することにします。

まず、次のようにして性別・人種ごとの年齢分布を調べます。

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 6))
_ = sns.boxplot(data=df, x='gender', y='age', ax=ax1)
_ = sns.boxplot(data=df, x='race', y='age', ax=ax2)

how04.png

すると、図のような結果になり、多くが10歳から60歳くらいであるとわかります。

年齢だけの分布を見るには

fig = plt.figure()
ax = fig.add_subplot(111)

ax.hist(df['age'], bins=50)
fig.show()

とすればよく、次のグラフが得られます。

how4.png

以上の分析をもとに、10歳以上65歳以下だけ考えることにします(ちゃんと精度を上げたいなら、データの水増しの方が有効でしょう)。それが次の部分です。

# 10歳以下、65歳以上の人の画像は比較的少ないので使わないことにする
df = df[(df['age'] > 10) & (df['age'] < 65)]

次に行きます。

###### train, test, validationデータの分割
p = np.random.permutation(len(df))  # 並び替え
train_up_to = int(len(df) * TRAIN_TEST_SPLIT)
train_idx = p[:train_up_to]
test_idx = p[train_up_to:]

# split train_idx further into training and validation set
train_up_to = int(train_up_to * TRAIN_VALID_SPLIT)
train_idx, valid_idx = train_idx[:train_up_to], train_idx[train_up_to:]

この部分は実際に traintestsplit のように分割を行っているわけではなく、index を振りなおしているだけです。

###### データの前処理を行う関数
def get_data_generator(df, indices, for_training, batch_size=32):
    # 処理した画像、年齢、人種、性別をbatch_sizeずつ返す

    images, ages, races, genders = [], [], [], []
    while True:
        for i in indices:
            r = df.iloc[i]
            file, age, race, gender = r['file'], r['age'], r['race_id'], r['gender_id']
            im = Image.open(file)
            im = im.resize((IM_WIDTH, IM_HEIGHT))
            im = np.array(im) / 255.0  # 規格化
            images.append(im)
            ages.append(age / max_age)  # 最大年齢で規格化
            races.append(to_categorical(race, len(RACE_ID_MAP)))  # kerasの仕様に合わせ、one-hot表現に
            genders.append(to_categorical(gender, 2))  # kerasの仕様に合わせ、one-hot表現に
            if len(images) >= batch_size:  # メモリを考慮して少しずつ結果を返す
                yield np.array(images), [np.array(ages), np.array(races), np.array(genders)]
                images, ages, races, genders = [], [], [], []
        if not for_training:
            break

このこの部分はデータの前処理を行っています。前処理といっても大したことはしておらず、やっていることは、規格化とラベルの表現をone-hotに直すことだけです。 yield を使っているのは、メモリの上限が割と厳しいので、少しずつ渡さないとパンクしてしまうからです。

また、while True: の無限ループは学習実行時は epochs=10 のように同じ処理を繰り返す必要があるため、学習実行時のみ必要です。

続いて、いよいよモデルを構築していきます。 最終的に作りたいモデルは以下の図です。

how5.png

下の枝分かれしている部分(全結合層)は自分で作成し、上の畳み込み部分( EfficientNet )と結合させます。 Kerasには大きく二つの書き方があり、Sequentialモデルと、Functional API と呼ばれています。 Sequentialモデルの方は、

model = Sequential()
model.add(Dense(64, input_dim=100))
model.add(Activation('relu'))

のように、.add()メソッドを使って層を積み重ねていくようにモデルを構築できるので直感的でわかりやすい反面、柔軟性にやや劣り、複数入出力や分岐などを含む、複雑なモデルを構築するのには向いていません。

もうひとつのFunctional API は

inputs = Input(shape=(784,))
x = Dense(64, activation='relu')(inputs)
x = Dense(64, activation='relu')(x)
predictions = Dense(10, activation='softmax')(x)

のような書き方で、層一枚一枚の入出力を指定できる分、自由度の高いモデル構築が可能です。

今回作るものは、分岐が一か所入るだけの、比較的単純なものなのでどちらの書き方でも可能です。コードではFunctional APIで記述しています。

###### モデルの作成
input_layer = Input(shape=(IM_HEIGHT, IM_WIDTH, 3))  # 最初の層

efficient_net = EfficientNetB4(
    weights='noisy-student',  # imagenetでもよい 
    include_top=False,  # 全結合層は自分で作成するので要らない
    input_tensor = input_layer,  # 入力
    pooling='max')

for layer in efficient_net.layers:  # 転移学習はしない
    layer.trainable = True

efficient_net の引数について説明します。 weights は初期状態でのネットワークのパラメータのことです。これはランダムでもよいですが、imagenet という画像データ群で学習させた値を使ったり、noisy-studentという学習法で学習させた値を使ったほうが、一般的に計算時間は短くなります。

include_top は畳み込み層だけでなく、全結合層もefficientnetの物を使うかどうかを決める引数です。今回は分岐という特殊な場合なので自分で作成する必要があります。

また、layer.trainable は転移学習するかどうかを決めます。した方がずっと計算時間は短くなりますが、試したところ精度が悪かったので、今回はすべてのパラメータを学習させます。

# 複数出力にする必要があるので、efficientnetの最終層から全結合層3つを枝分かれさせる
bottleneck=efficient_net.output 

# 年齢の予測
_ = Dense(units=128, activation='relu')(bottleneck)
age_output = Dense(units=1, activation='sigmoid', name='age_output')(_)

# 人種の予測
_ = Dense(units=128, activation='relu')(bottleneck)
race_output = Dense(units=len(RACE_ID_MAP), activation='softmax', name='race_output')(_)

# 性別の予測
_ = Dense(units=128, activation='relu')(bottleneck)
gender_output = Dense(units=len(GENDER_ID_MAP), activation='softmax', name='gender_output')(_)

# efficientnetと全結合層を結合する
model = Model(inputs=input_layer, outputs=[age_output, race_output, gender_output])
この部分は functional API の書き方で全結合層を作成し、efficientnetと結合させています。

注意すべきは次の部分です。

# 最適化手法・損失関数・評価関数を定義してコンパイル
model.compile(optimizer='rmsprop', 
              loss={'age_output': 'mse', 'race_output': 'categorical_crossentropy', 'gender_output': 'categorical_crossentropy'},
              loss_weights={'age_output': 2., 'race_output': 1.5, 'gender_output': 1.},
              metrics={'age_output': 'mae', 'race_output': 'accuracy', 'gender_output': 'accuracy'})

全結合層が3つある分、損失関数や評価関数も3つずつ定義する必要があります。 年齢の予測は回帰問題なので、平均二乗誤差、性別と人種は分類問題なので categorical cross-entropy を損失関数に使えばよいでしょう。

続く部分はバッチサイズを定義し、データを作成しています。 バッチサイズを大きくすると収束性が良くなりますが、やりすぎるとメモリがパンクするので注意しましょう。 batch_size = 64 だとうまくいかないと思います。

次です。

# 検証誤差が最も低い状態のモデルを保存しておく
callbacks = [
    ModelCheckpoint('./model_checkpoint', monitor='val_loss', verbose=1, save_best_only=True, mode='min')
]

callbacks という便利な機能を使います。これは validation data の損失関数を監視し、それが最小であったepochでのモデルを保存しておいてくれる機能です。 これにより過学習を防ぐことができます。

そして次の部分で学習を実行します。

history = model.fit_generator(train_gen,
                    steps_per_epoch=len(train_idx)//batch_size,
                    epochs=10,
                    callbacks=callbacks,
                    validation_data=valid_gen,
                    validation_steps=len(valid_idx)//valid_batch_size)

私の場合は40分ほどかかりました。気長に待ちましょう。

学習が済んだら損失関数と評価関数の値をグラフに表して学習がうまくいったかどうか確認します。

def  plot_train_history(history):
    fig, axes = plt.subplots(1, 4, figsize=(20, 5))
    axes[0].plot(history.history['race_output_accuracy'], label='Race Train accuracy')
    axes[0].plot(history.history['val_race_output_accuracy'], label='Race Val accuracy')
    axes[0].set_xlabel('Epochs')
    axes[0].legend()

    axes[1].plot(history.history['gender_output_accuracy'], label='Gender Train accuracy')
    axes[1].plot(history.history['val_gender_output_accuracy'], label='Gener Val accuracy')
    axes[1].set_xlabel('Epochs')
    axes[1].legend()

    axes[2].plot(history.history['age_output_mae'], label='Age Train MAE')
    axes[2].plot(history.history['val_age_output_mae'], label='Age Val MAE')
    axes[2].set_xlabel('Epochs')
    axes[2].legend()  

    axes[3].plot(history.history['loss'], label='Training loss')
    axes[3].plot(history.history['val_loss'], label='Validation loss')
    axes[3].set_xlabel('Epochs')
    axes[3].legend()

plot_train_history(history)

loss1.png

epoch2.png

epoch 8 で何やら起こっていますが他の部分でも変動が激しいことからも確率的に起こりうることなのかもしれません。 また、特に性別と人種の分類において、かなり過学習が起こっていることが見て取れます。

最後に、各種精度に関係する値を出力し、テストデータについても予測します。

test_gen = get_data_generator(df, test_idx, for_training=False, batch_size=128)
dict(zip(model.metrics_names, model.evaluate_generator(test_gen, steps=len(test_idx)//128)))

test_gen = get_data_generator(df, test_idx, for_training=False, batch_size=128)
x_test, (age_true, race_true, gender_true)= next(test_gen)
age_pred, race_pred, gender_pred = model.predict_on_batch(x_test)

race_true, gender_true = race_true.argmax(axis=-1), gender_true.argmax(axis=-1)
race_pred, gender_pred = race_pred.argmax(axis=-1), gender_pred.argmax(axis=-1)
age_true = age_true * max_age
age_pred = age_pred * max_age

from sklearn.metrics import classification_report
print("Classification report for race")
print(classification_report(race_true, race_pred))

print("\nClassification report for gender")
print(classification_report(gender_true, gender_pred))

import math
n = 30
random_indices = np.random.permutation(n)
n_cols = 5
n_rows = math.ceil(n / n_cols)
fig, axes = plt.subplots(n_rows, n_cols, figsize=(15, 20))
for i, img_idx in enumerate(random_indices):
    ax = axes.flat[i]
    ax.imshow(x_test[img_idx])
    ax.set_title('a:{}, g:{}, r:{}'.format(int(age_pred[img_idx]), ID_GENDER_MAP[gender_pred[img_idx]], ID_RACE_MAP[race_pred[img_idx]]))
    ax.set_xlabel('a:{}, g:{}, r:{}'.format(int(age_true[img_idx]), ID_GENDER_MAP[gender_true[img_idx]], ID_RACE_MAP[race_true[img_idx]]))
    ax.set_xticks([])
    ax.set_yticks([])

err1.png

err2.png

f1- score が大まかな指標になります。人種については80%の確率、性別については97%の確率で正解していることが分かります。また、学習時と比べると、過学習の傾向が強いこともうかがえます。 人種に関しては、ラベル4、すなわち 「その他の人種」についての予測が壊滅的にできていないことが分かります。

続いてテストデータのサンプルです。上が予測値、下が実際の値です。

face1.png face2.png

チューニングの余地がある割にはある程度予測できています。

何人か知っている人についても予測してみました。

sample1.png sample2.png

あれ.........

まとめ

今回は顔の画像から、年齢・性別・人種を同時に推定するモデルをEfficient Net を使って作りました。 今回の例のようなマルチタスク処理は、シングルタスクの精度を向上させる際にも使われることもあり、有用なので是非利用してほしいと思います。 ただ、本来EfficientNetは画像の解像度を畳み込み層の深さ・大きさと共に調節すべきものであり、今回の処理ではそれを省いているので制度は出にくい状態です。 また、画像認識の大変なところはパラメータを調節して精度を上げていくところにこそあるので、次回はこのモデルをチューニングし、過学習を抑えたりしてより精度を上げていこうと思います。

参考となるリンク先


Twitter・Facebookで定期的に情報発信しています!

概要

自分に似合う色、引き立たせてくれる色を知る手法として「パーソナルカラー診断」が最近流行しています。

パーソナルカラーとは、個人の生まれ持った素材(髪、瞳、肌など)と雰囲気が合う色のことです。人によって似合う色はそれぞれ異なります。 パーソナルカラー診断では、個人を大きく2タイプ(イエローベース、ブルーベース)、さらに4タイプ(スプリング、サマー、オータム、ウィンター)に分別し、それぞれのタイプに合った色を知ることができます。


パーソナルカラーを知るメリット

  • 自分をより好印象に見せることができる
  • 自分に合うものを知って、買い物の無駄を減らせる
  • 本来の魅力を発見できる


現在、「パーソナルカラー診断」と検索すると、膨大な量のページが見つかります。しかしそれらのほとんどは設問に自分で答える自己申告タイプでした。

ほんとにこれで合ってるの?と疑問に感じることもあるでしょう。
専門家に見てもらうにはお金も時間もかかるし正直面倒臭い。写真や動画で、自宅で気軽に判断できたらいいのに...なんて思いませんか?


今回は、「パーソナルカラー診断サービス」を機械学習を用いて製作しました!


どうやるの? 機械学習のしくみ

今回の作業の流れです。
①画像を収集→②パーソナルカラー(4パターン)の正解を付け→③モデルを作成→④モデルに②の画像を学習
→⑤学習させたモデルに新たな画像を投入(テスト)→⑥画像に対してパーソナルカラーを判定→⑦精度向上のための考察


この作業で大まかには正解のパーソナルカラーを予測できるシステムが作成できました。
技術を詳しくご紹介します。


訓練データの確保

[芸能人の名前 パーソナルカラー] で調べると、その人のパーソナルカラーが分かります。それを参考にして判定用のモデルを作成しています。
学習をイチからするためには相当量のデータが必要なので、今回は予め学習されたモデルの一部を基にして層を追加する転移学習という手法でモデルを作成しました。

実装はkerasを用いて行います。kerasでは様々な事前学習済みモデルが用意されています。

ImageNet検証データでのスコアがそれぞれ出ていますが、どのモデルが今回のケースに一番うまくマッチしてくれるかは試すまでわからないのでひとまずVGG16と、ここでのベンチマークで一番精度の高いInceptionResNetV2を選びました。
InceptionV3という別のモデルもありますが、所々 InceptionResNetV2をInceptionモデルと略すところがあります。


Applications.jpg (引用)https://keras.io/applications/


├─personal-color
│ ├─data
│ ├─images
│ │ ├─autumn
│ │ │ └─original
│ │ ├─spring
│ │ │ └─original
│ │ ├─summer
│ │ │ └─original
│ │ └─winter
│ │ │ └─original
│ ├─scripts

というフォルダ構造で季節ごとに階層を分け、収集してきた画像をoriginalに格納しています。この時、画像の名前は、通し番号のみで管理しています。
この後学習させるにあたってパーソナルカラーを決める上で重要な要素となる顔の形や目だけにトリミングして別画像として保存することを考えたため、originalという形で保存しています。

収集した画像から顔を切り取るのに加えて、目だけ、口だけを切り取るために、dlibのランドマーク検出を使います。
ランドマーク検出とは、顔から要所となる点を検出するものです。


facereg.jpg (引用)https://docs.opencv.org/master/d2/d42/tutorialfacelandmarkdetectioninanimage.html


import logging
import os
import shutil

import cv2
import dlib
import numpy as np
from PIL import Image, ImageFilter
import keras
from keras.preprocessing.image import ImageDataGenerator
from keras.models import Sequential, Model
from keras.layers import Input, Dense, Dropout, Activation, Flatten
from keras import optimizers
from keras.applications.vgg16 import VGG16
from sklearn.model_selection import train_test_split


LABELS = ['spring', 'summer', 'autumn', 'winter']
PARTS = ['face', 'eyes', 'mouth']

for season in LABELS:
    pathList = ['../images', season, 'original']
    rootPath = os.path.join(*pathList)
    files = os.listdir(rootPath)
    files.remove('.DS_Store')

    for file in files:
        filePath = os.path.join(rootPath, file)

        img = cv2.imread(filePath)
        rects = detector(img, 1)
        PREDICTOR_PATH = ('../data/shape_predictor_68_face_landmarks.dat')
        logging.debug('--- fetch each coordinates from settled index ---')
        for rect in rects:
            face = rect
            predicted = predictor(img, rect).parts()
            left_eye = predicted[36:42]
            right_eye = predicted[42:48]
            mouth = predicted[48:]
            partsList = [left_eye, right_eye, mouth]
    logging.debug('--- trimming and save images ---')
    savePart()  # 上で求めた領域をそれぞれトリミングする。
一部略


モデルの作成

画像を入力用に再度加工し、モデルを作っていきます。
訓練用データは少し水増しを加え、テストデータはそのまま使います。前述の通り今回はVGG16とInceptionResNetV2を使っています。


def imagePreprocess(self):
    trainDataGen = ImageDataGenerator(rescale=1 / 255,
                                      shear_range=0.2,
                                      zoom_range=0.2,
                                      rotation_range=60,
                                      brightness_range=[0.8, 1.0],
                                      horizontal_flip=True,
                                      vertical_flip=True)
    self.trainGenerator = trainDataGen.flow_from_directory(directory=self.trainFolder,
                                                           target_size=(
                                                               self.IMG_HEIGHT, self.IMG_WIDTH),
                                                           color_mode='rgb',
                                                           classes=LABELS,
                                                           class_mode='categorical',
                                                           batch_size=self.BATCHSIZE,
                                                           shuffle=True)

    testDataGen = ImageDataGenerator(rescale=1 / 255)
    self.testGenerator = testDataGen.flow_from_directory(directory=self.testFolder,
                                                         target_size=(
                                                             self.IMG_HEIGHT, self.IMG_WIDTH),
                                                         color_mode='rgb',
                                                         classes=LABELS,
                                                         class_mode='categorical',
                                                         batch_size=self.BATCHSIZE,
                                                         shuffle=True)


def vgg16Model(self, summary=False, name='model', verbose=1, lr=1e-4, epochs=25, freezefrom=0, optimizer='RMSprop'):
    input_tensor = Input(shape=(self.IMG_HEIGHT, self.IMG_WIDTH, 3))
    vgg16 = VGG16(include_top=False, weights='imagenet',
                  input_tensor=input_tensor)
    if summary:
        vgg16.summary()
    top_model = Sequential()
    top_model.add(Flatten(input_shape=vgg16.output_shape[1:]))
    top_model.add(Dense(256, activation='relu'))
    top_model.add(Dropout(0.5))
    top_model.add(Dense(len(LABELS), activation='softmax'))
    self.model = Model(inputs=vgg16.input, outputs=top_model(vgg16.output))
    if summary:
        self.model.summary()

    vgg16.trainable = True

    if freezefrom == 0:
        for layer in vgg16.layers:
            if layer.name == 'block5_conv1':
                layer.trainable = True
            else:
                layer.trainable = False
    else:
        for layer in vgg16.layers[:freezefrom]:
            layer.trainable = False

    if optimizer == 'RMSprop':
        self.model.compile(loss='categorical_crossentropy',
                           optimizer=optimizers.RMSprop(lr=lr), metrics=['acc'])
    if optimizer == 'SGD':
        self.model.compile(loss='categorical_crossentropy',
                           optimizer=optimizers.SGD(lr=lr), metrics=['acc'])

    history = self.model.fit_generator(self.trainGenerator, steps_per_epoch=25, epochs=epochs, validation_data=self.testGenerator,
                                       validation_steps=10, verbose=verbose)
    self.history.append(history)
    self.model.save(os.path.join(
        *[self.baseDir, 'data', name + '.h5']), include_optimizer=False)


def inception_resnet(self, name='model', lr=1e-4, epochs=25, trainFrom=0):

    resnet_v2 = InceptionResNetV2(include_top=False, weights='imagenet',
                                  input_tensor=Input(shape=(self.IMG_HEIGHT, self.IMG_WIDTH, 3)))
    model = Sequential()
    model.add(Flatten(input_shape=resnet_v2.output_shape[1:]))
    model.add(Dense(256, activation='relu'))
    model.add(Dropout(0.5))
    model.add(Dense(len(LABELS), activation='softmax'))

    if trainFrom == 0:
        resnet_v2.trainable = False
    else:
        for layer in resnet_v2.layers[:trainFrom]:
            layer.trainable = False

    self.inception_model = Model(
        input=resnet_v2.input, output=model(resnet_v2.output))
    self.inception_model.compile(
        loss='categorical_crossentropy', optimizer=optimizers.RMSprop(lr=1e-4), metrics=['acc'])

    hist = self.inception_model.fit_generator(
        self.trainGenerator, epochs=epochs, validation_data=self.testGenerator, steps_per_epoch=25)
    self.history.append(hist)
    self.inception_model.save(os.path.join(
        *[self.baseDir, 'data', name + '.h5']), include_optimizer=False)


結果は?

目と口のみで学習させたモデルの結果はval accが0.25に非常に近いものでした。4クラス問題なので、これでは全く意味がありません。

一方顔全体での結果は以下の通りでした。


Inception
Epoch 25/25
25/25 [==============================] - 18s 709ms/step - loss: 0.7033 - acc: 0.7200 - val_loss: 2.7571 - val_acc: 0.4781
VGG16
Epoch 25/25
25/25 [==============================] - 11s 458ms/step - loss: 1.0617 - acc: 0.5225 - val_loss: 1.2682 - val_acc: 0.4230

学習時間はColab Tesla T4でInceptionが約10分、VGGが5分ほどでした。


決して非常に高機能なモデルとは言えませんが、目のモデルと比べると顔全体を使用したモデルが良さそうです。何より、私自身がパーソナルカラー診断をしたら確実に0.25を下回るくらい難しい分類だったので満足の行く結果に見えます。過学習していなければですが。
ということで過学習に震えながら別のテストをすると、内実は惨憺たるものでした。

VGGモデルに関しては、少し夏と冬が多くあとはほぼランダムな出力でした。上振れでテストスコアが上下するような感じです。
一方Inceptionモデルは、夏と冬のテストに関しては約8割ほどの正解率でした。しかし、春と秋はほとんどが夏、冬と回答されていて、春か秋と答えられたのは、100件中2件ほどでした。オリジナルデータ数はこの時すべて同じ枚数で設定していたのでこのような偏りはすごい不思議なものでした。


どうしてこうなったのか原因を考えていきます。


精度向上のための原因考察

仮説その1 パラメータに問題がある

ハイパーパラメータに問題があって過学習している可能性を考え、learning rateを1e-2 - 1e-5まで変化させてテストをしました。また、ファインチューニングという新しいモデルを追加するだけではなく、以前のモデルの一部を学習時に用いるという手法も試しました。

結果として、概してval accの結果が良いのは1e-4のときでした。

1e-5では、25epochsだと少したりないような印象を受けました。上述のスコアも1e-4にしたときのスコアです。しかし、スコアの微小な上下はあれど、本質的な偏りの修正には至りませんでした。


仮説その2 モデルがそもそもあっていない

人間の顔をその雰囲気から分類するというタスクがこれらのモデルにあっていないのではないか。しかし、これを言ってしまうと本末転倒です。
もともと訓練データを集めづらい分野において、予め学習してあるモデルを転用するというコンセプトが転移学習ですし、一旦考えなかったことにします。


仮説その3 訓練画像の質が悪い

画像の質と言っても、様々な要素があります。

画質、水増しでゴミができていないか、正しいクラスに分類されているかetc...
今回訓練に使ったのは芸能人の画像でした。もともと、化粧や照明の影響で印象が変わることは危惧していましたが、とりあえずモデルに投入していました。 今一度初心に帰り、これらを見直すことにしました。

基準が私では判断ができないので、当社腕利きのパーソナルカラー診断士が人の目で答えがわかっているテスト画像を再度精査をしました。

すると驚くことに、VGGモデルで、学習機の導き出していた答えが人の目の意見と一致するものが多々ありました。
人の目でも即座に判断できるものは正解しており、判断が難しいものに関しては、イエローベース、ブルーベースの2択までは絞り込めるという感覚でした。

つまり、テストデータとして与えていたラベルが間違っていただけで学習機は頑張って学習していたのに、画像を収集する際にこの人はこの季節、とその写真のときの状態を見ずに決めてしまっていたため引き起こされてしまったのです。

この結果を受けて、再度すべての画像を再分類することにしました。
VGGモデルでは偏りが減り、正解率も微小ながら上がりました。しかし、Inceptionモデルでは逆に夏と冬しか結果として出力されないようになってしまいました。
このモデルは(他もそうですが特に)非常に複雑なモデルなので、難しい学習の果てに夏と冬だけに焦点をあてた方が精度が上がると判断されてしまったのかもしれません。データ数に不均衡はないので今回は仮説その2として切り上げます。

(参考)https://qiita.com/koshian2/items/20af1548125c5c32dda9


まとめ

一番の原因がデータ処理が甘かったというなんとも情けのないものでした。

色々なところで色々な人が機械学習の8割は前処理といったことを口を酸っぱく言っている意味を再度認識しました。
また、とんでもなく巨大なBiTという新モデルが2019年の12月に発表されたので、次回はこれも試してみたいです。

(参考)https://qiita.com/omiita/items/90abe0799cf3efe8d93d


AI・機械学習を活用したシステムが少しずつ普及しています。今回のように画像を用いた判定・予想は人の手をシステムに代替するために便利な手法です。
例えば、人がおこなっている不良品チェックを機械学習でおこなったり、複数の散乱している物をなにであるか判定し金額を表示することで無人レジを作成したり...。
リモートワークや自由な働き方が進んでいる中、人間とシステムが上手に共生する社会がすぐそこまで迫ってきています。

当社ではAI・機械学習を活用したソリューションを提案しています。


アクセルユニバースの紹介

私達はビジョンに『社会生活を豊かにさせるサービスを提供する。』ことを掲げ、このビジョンを通して世界を笑顔にしようと機械学習・深層学習を提案しています。

  • ミッション(存在意義)
    私達は、情報通信技術を使って万物(全ての社会、生物)の暮らしをよりよくすることに貢献し、 それを加速させることを使命とします。

  • ビジョン(目標とする姿)
    社会生活を豊かにさせるサービスを提供する。

  • バリュー(行動規範)

    1. 変化を求め、変化を好み、変化する
    2. 自分の高みを目指してどんどん挑戦する
    3. お客様と一蓮托生でプロジェクトを進める


会社概要はこちら


Twitter・Facebookで定期的に情報発信しています!

はじめに

この記事では物体検出に興味がある初学者向けに、最新技術をデモンストレーションを通して体感的に知ってもらうことを目的としています。今回紹介するのはAAAI19というカンファレンスにて精度と速度を高水準で叩き出した「M2Det」です。one-stage手法の中では最強モデル候補の一つとなっており、以下の図を見ても分かるようにYOLO,SSD,Refine-Net等と比較しても同程度の速度を保ちつつ、精度が上がっていることがわかります。
M2Det

https://arxiv.org/pdf/1811.04533.pdfより引用

物体検出デモ

それではM2Detでの物体検出をしていきたいのですがひとつ問題が
著者がgithubに公開しているソースコードはCUDAを使用する前提のため、NVIDIAのGPUが搭載していない私のPCではすぐに動かす事ができません。
そのため今回はGPUなしでも動かせる環境を提供してくれるGoogle先生の力をお借りします!
ということでGoogle Colaboratoryを使ってM2Detを動かしていきます。
例のごとくqijiezhao/M2Detの説明を参考に進めていきます。


Step1.前準備

Google Colaboratoryを開いたら先ずハードウェアのアクセラレータをGPUに設定しましょう。メニューバーの「ランタイム」→「ランタイムのタイプを変更」をクリック。「ハードウェア アクセラレータ」のプルダウンからGPUを選択して「保存」します。これで設定は完了です。
M2Det
それではコードを書いていきます。必要なモジュールをインストールし、上記githubからクローンを作成します。M2Detファイルに移動したらシェルを以下のように実行します。

!pip install torch torchvision
!pip install opencv-python tqdm addict
!git clone https://github.com/qijiezhao/M2Det.git
%cd M2Det
!sh make.sh

Step2.学習済モデルをGoogle Driveからダウンロードする(引用)

次に学習済モデルを入手します(とてつもなく学習に時間がかかるので出来合いのものを使用させていただきます)。githubの説明にも書いてあるようにbackbornはVGG-16とし、指定のGoogle Driveからダウンロードしてきます。ダウンロードが簡単にできる便利なもの(nsadawi/Download-Large-File-From-Google-Drive-Using-Python)を見つけたので引用させていただきます。

#引用開始
import requests
def download_file_from_google_drive(id, destination):
  URL = "https://docs.google.com/uc?export=download"
  session = requests.Session()
  response = session.get(URL, params = { 'id' : id }, stream = True)
  token = get_confirm_token(response)
  if token:
    params = { 'id' : id, 'confirm' : token }
    response = session.get(URL, params = params, stream = True)
  save_response_content(response, destination)
def get_confirm_token(response):
  for key, value in response.cookies.items():
    if key.startswith('download_warning'):
      return value
    return None
def save_response_content(response, destination):
  CHUNK_SIZE = 32768
  with open(destination, "wb") as f:
    for chunk in response.iter_content(CHUNK_SIZE):
      if chunk: # filter out keep-alive new chunks
        f.write(chunk)
#引用終了
%mkdir weights
directory = '1NM1UDdZnwHwiNDxhcP-nndaWj24m-90L'
adress = './weights/m2det512_vgg.pth'
download_file_from_google_drive(directory, adress)

これでM2Detフォルダの中に学習済モデルm2det512_vgg.pthがダウンロードできました。


Step3.判別の閾値を設定する(任意)

ここで一旦パラメータの調整を挟みます。YOLOでは判別の閾値が0.5以上の時にアノテーションすることにしていたのでM2Detでも同じ値にします。demo.pyの63行目を確認すると

def draw_detection(im, bboxes, scores, cls_inds, fps, thr=0.2):

thrの値がデフォルトで0.2になっているのでこの値を0.5に以下のように書き換えます。

!sed -i -e "63c def draw_detection(im, bboxes, scores, cls_inds, fps, thr=0.5):" demo.py

Step4.Google Driveにアップロードした動画ファイルをM2Detフォルダの指定ディレクトリにコピーする

後は判別させたい画像または動画をGoogle Driveからimgsフォルダに移動させてきましょう。なのでご自分で予めGoogle Driveに画像または動画をアップロードしておきましょう。今回は動画を使用しているので以下のようにマウントした後、任意の動画をコピーします。実行の際にオースコード(Auth code)が要求されるのでURLをクリックして表示されるコードを貼り付けます。

from google.colab import drive
drive.mount('/content/drive')
!cp /content/drive/My\ Drive/*.mp4 ./imgs #動画用

M2Det
これで必要なものは全て揃いました。

Step5.デモ

ここでdemo.pyの引数について確認してみます。demo.pyの17行目から24行目を確認すると以下のように記述されています。

parser = argparse.ArgumentParser(description='M2Det Testing')
parser.add_argument('-c', '--config', default='configs/m2det320_vgg.py', type=str)
parser.add_argument('-f', '--directory', default='imgs/', help='the path to demo images')
parser.add_argument('-m', '--trained_model', default=None, type=str, help='Trained state_dict file path to open')
parser.add_argument('--video', default=False, type=bool, help='videofile mode')
parser.add_argument('--cam', default=-1, type=int, help='camera device id')
parser.add_argument('--show', action='store_true', help='Whether to display the images')
args = parser.parse_args()

parser.add_argumentの直後に記述されている引数を実行の際に記述することで様々な使い方ができるようです。別途ダウンロードしてきた学習済モデルで動画を判別させるので以下のように実行します。

!python demo.py -c=configs/m2det512_vgg.py -m=weights/m2det512_vgg.pth --video VIDEO #動画用

実行すると動画のディレクトリを指定するように出てくるのでディレクトリを指定します(Step4でimgsフォルダにコピーしたならimgs/(ファイル名))
後は各フレーム毎に物体検出をしてくれるので待ちましょう。
M2Det

Step6.ファイルをダウンロードする

残念ながらmp4をchrome上で再生する術を知らないため、ローカルにダウンロードして再生することにします。以下のように2行で簡単にファイルのダウンロードができます。

from google.colab import files
files.download('imgs/<ファイル名>')

実装例

おわりに

今回M2Detを使用して動画の物体検出を行ってみました。リアルタイムの識別を検討する場合は限られた時間内に一定以上の精度を保証する信頼性がより重要となり、M2Detはこれを達成する一歩になるのでは無いかと思いました。YOLOやSSDについてもまだまだ改良されていくと予想しているので、引き続きリサーチしたいと思います。また、物体検出を利用した異常検知や店の空席率把握などに使えそうなので実装できたらまたブログ書こうと思います。

実装コード

https://colab.research.google.com/drive/1oSPhiGmZC-IeLnyoR2l-UIKIquP1i51g

その他、ドーナツを検知し、無人レジの実現に向けて検証もしており、現在、当社では技術の実用化に向けて様々な検証をしています。


Twitter・Facebookで定期的に情報発信しています!

はじめに

まずは下の動画をご覧ください。
スクリーンショット 2019-12-27 17.18.19.png
スパイダーマン2の主役はトビー・マグワイアですが、この動画ではトム・クルーズがスパイダーマンを演じています。
これは実際にトム・クルーズが演じているのではなく、トム・クルーズの顔画像を用いて合成したもので、機械学習の技術を用いて実現できます。
機械学習は画像に何が写っているか判別したり、株価の予測に使われていましたが、今回ご紹介するGANではdeep learningの技術を用いて「人間を騙す自然なもの」を生成することができます。
「人間を騙す自然なもの」を作るには本物のデータがどのように生成されるのか理解しなければなりません。
例えば、猫の画像を生成するときに耳を3つ書いたりしません。なので猫の耳は2つであるということを学習しなければなりません。


GANによる成果物をもう1つ紹介します。こちらのサイトにアクセスすると人物の顔が出てきます。
ですが出てくる人物はこの世に存在せず、GANが生成した人物です。
ご覧になっていただいてわかるように不自然なところは何もありません。


1. GANとは

GANとはGenerative Adversarial Networksの略で敵対的生成ネットワークとも呼ばれ、2014年に発表されました。
特徴は2つのネットワークを戦わせることにあります。

Gをデータを生成するネットワーク(Generator)、DをデータがGから生成されたものか実際のデータかを判別するネットワーク(Discriminator)とします。
GはDを騙すような画像を生成できるように、DはGから生成されたデータを見抜けるように、競いながら学習をします。
論文では例として、Gを偽札を作る悪人、Dを偽札を見抜く警察としています。


全体のネットワーク構造は以下のようになっています。
deep-learning-for-computer-vision-generative-models-and-adversarial-training-upc-2016-5-638.jpg

Gを図中のGenerator、Dを図中のDiscriminatorとします。
Gは何かしらの確率分布 P_z (一様分布など)から生成されたデータ z (図中 Latent random variable)から、通常のネットワークのように重みをかけて本物と同じサイズの画像を生成します。
Dは入力として画像(図中 sample)を受け取り、本物か偽物かを判定します。

D(・) を「・が本物のデータと判断する確率」としたとき、GAN は以下のような評価関数を最小化するようにGとDのパラメータ θ_g と θ_d を更新していきます。
\begin{eqnarray*} \min _{G} \max _{D} V(D, G)=\mathbb{E}_{\boldsymbol{x} \sim p_{\text {data }}(\boldsymbol{x})}[\log D(\boldsymbol{x})]+\mathbb{E}_{\boldsymbol{z} \sim p_{\boldsymbol{z}}(\boldsymbol{z})}[\log (1-D(G(\boldsymbol{z})))] \end{eqnarray*}

2. アルゴリズム

パラメータθgとθdを更新する手順を以下に示します。
kはハイパーパラメータです。


1. 2~4をイテレーションの数だけ繰り返す
  2. (2-1)~(2-4)をkの数だけ繰り返す
    2-1. pzからm個のサンプル{z1,z2...zm}を取得
    2-2. 実際のデータからm個のサンプル{x1,x2,...xm}を取得
    2-3. θ
dを以下のように更新<

更新式1

  3. pzに従うm個のサンプル{z1,z2...zm}を取得
  4. θ_gを以下のように更新

\begin{eqnarray*} \nabla_{\theta_{d}} \frac{1}{m} \sum_{i=1}^{m}\left[\log D\left(\boldsymbol{x}^{(i)}\right)+\log \left(1-D\left(G\left(\boldsymbol{z}^{(i)}\right)\right)\right)\right] \end{eqnarray*}

更新式2

\begin{eqnarray*} \nabla_{\theta_{g}} \frac{1}{m} \sum_{i=1}^{m} \log \left(1-D\left(G\left(\boldsymbol{z}^{(i)}\right)\right)\right) \end{eqnarray*}


3. おまけ 学習の様子

zがpzに従うときG(z)がpgに従うとします。
概略は以下のようになります。

スクリーンショット 2019-12-26 13.58.30.png


GANの目的はpg = pdataとなることです。
つまり本物のデータが生成される分布pdataとpgが等しいため、理論上は見分けがつかないことです。
これを言い換えるとD(x) = 1/2 が成り立つことだとも言えます。
これは、Dが本物のデータxをを本物だと見分ける確率が1/2で、当てずっぽうで判断していることになります。


論文にはpgがpdataに近づいていく様子が載せられています。
スクリーンショット 2019-12-26 12.28.58-1.png


青のドット線 : D(x)
緑の線 : pg
黒のドット線 : 実際のデータxが従う確率分布p
data

  • a
    学習前の様子です。

  • b Dが上のアルゴリズム(2-3)の手順で更新された後です。

  • c Gを上のアルゴリズム4の手順で更新された後です。
    bの状態と比べて、pgがpdataに近づいた事がわかります。

  • d pg = pdataとなった様子です。

4.まとめ

GANは人を自然な画像を生成するだけでなく、最初に挙げたスパイダーマンのように2つの画像を自然に合成することもできるようです。
買おうか迷っている服を自分の体に合成して、擬似的に試着することもできます。
服の試着だけでなく、化粧品のお試しや、髪型が自分に合うのかなどを事前にわかっていると便利そうです。
今後はGANについて最新の傾向をつかめるようにしていきたいと思います。


Twitter・Facebookで定期的に情報発信しています!

概要

DeepArtのようなアーティスティックな画像を作れるサービスをご存知でしょうか?
こういったサービスではディープラーニングが使われており、コンテンツ画像とスタイル画像を元に次のような画像の画風変換を行うことができます。この記事では画風変換の基礎となるGatysらの論文「Image Style Transfer Using Convolutional Neural Networks」[1]の解説と実装を行っていきます。

examples_of_style_transfer

引用元: Gatys et al. (2016)[1]


手法

モデルにはCNN(Convolutional Neural Network) が用いられており、VGG[2]という物体認識のための事前学習済みのモデルをベースとしています。


こちらの図はCNNが各層においてどのようにコンテンツ画像とスタイル画像を表現するか示しています。

image_representaion_on_each_layers_in_CNN

引用元: Gatys et al. (2016)[1]


Content Reconstructions (下段) のa, b, c を見ると入力画像がほぼ完璧に復元されていることがわかります。一方で d, e を見ると詳細な情報は失われているものの、物体認識をする際に重要な情報が抽出されていることがわかります。これは画像からコンテンツが抽出され、画風を表す情報が落とされていると考えられます。よって画風変換のモデルでは画像からコンテンツを捉えるためにCNNの深い層を利用します。
次にStyle Reconstructions (上段) を見てみましょう。a からe はそれまでの各層の特徴マップの相関をもとに復元されています。例えば c は第1層から第3層までの各層における特徴マップの相関から生成されています。こうすることで画像内のコンテンツの配置などによらない画風を抽出することができます[3]。


画風変換のモデルではコンテンツ画像とスタイル画像を入力として受け取り、上記のことを利用してスタイル画像の画風をコンテンツ画像に反映した画像を新たに生成します。


では具体的にモデルの中身を見ていきましょう。


モデルのアーキテクチャ

algorithm_of_style_transfer_model

引用元: Gatys et al. (2016)[1]


まずコンテンツ画像 (\(\vec{p}\)) とスタイル画像 (\(\vec{a}\)) から特徴マップが抽出されます。次にこれらの特徴マップと生成画像 (\(\vec{x}\))の特徴マップとの損失が計算されます。この計算で求められる損失をそれぞれコンテンツ損失、スタイル損失と呼ぶことにします。そしてコンテンツ損失とスタイル損失の合計が最終的な損失となり、これを最小化していきます。
ここで注意しなければならないことが1つあります。通常、ディープラーニングでは重みが最適化の対象になりますが、今回は重みは固定して生成画像のピクセルを最適化します。


コンテンツ損失

コンテンツ損失はコンテンツ画像と生成画像のVGGのある1層から出力された特徴マップの平均二乗誤差によって計算されます。

$$ \mathcal{L}_{content} (\vec{p}, \vec{x}, l) = \frac{1}{2} \sum_{i, j}^{} (F_{ij}^{l} - P_{ij}^{l})^2 $$ ここで\(F_{ij}^{l}\) は生成画像の\(l\)層における\(i\) 番目のフィルターの位置\(j\) でのアクティベーションを表しています。\(P_{ij}^{l}\)についても同様ですが、こちらはコンテンツ画像についてのアクティベーションを表しています。


スタイル損失

スタイル画像から画風を捉えるためにまず各層における特徴マップの相関を計算します。

$$ G_{ij}^{l} = \sum_{k}^{} F_{ik}^{l}F_{jk}^{l} $$ これはグラム行列と呼ばれ、\(l\) 層におけるフィルター間の特徴マップの相関をとっています。このグラム行列を用いることで画風を表現することができます[3]。 そして生成画像のグラム行列とスタイル画像のグラム行列の平均二乗誤差を求めます。 $$ E_{l} = \frac{1}{4N_{l}^{2}M_{l}^{2}} \sum_{i,j}^{} (G_{ij}^{l} - A_{ij}^{l})^2 $$ ここで\(N_{l}\)は特徴マップの数、\(M_{l}\)は特徴マップのサイズを表します。また\(G_{ij}^{l}\)、\(A_{ij}^{l}\) はそれぞれ\(l\) 層における生成画像のグラム行列とスタイル画像のグラム行列を表します。  最後に各層の損失の線形和をとってスタイル損失とします。 $$ \mathcal{L}_{style} (\vec{a}, \vec{x}) = \sum_{l=0}^{L} w_{l}E_{l} $$ このとき\(w_{l}\) は\(l\) 層の損失の重みを表します。


最適化

コンテンツ損失とスタイル損失から合計の損失を求めます。合計損失は\(\alpha\) と\(\beta\) をそれぞれコンテンツ損失とスタイル損失の重みとして $$ \mathcal{L}_{total}(\vec{p}, \vec{a}, \vec{x}) = \alpha\mathcal{L}_{content}(\vec{p}, \vec{x}) + \beta\mathcal{L}_{style}(\vec{a}, \vec{x}) $$ この損失を最小化する形で生成画像の最適化を行っていきます。ですので最適化には\(\frac{\partial L_{totla}}{\partial \vec{x}}\) を用いることになります。 論文中ではL-BFGSで最も良い結果になったと記述されていましたが、今回の実装ではAdam を使って最適化を行いました。 この最適化によって、コンテンツ画像をスタイル画像に合わせて画風変換した新たな画像が生成されます。


実装

実装にはTensorFlowを用いました。また実装に際してTensorFlowのチュートリアル[4]を参考にしました。


まずは必要なライブラリをインストールします。

import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl
%matplotlib inline
mpl.rcParams['figure.figsize'] = (12,12)
mpl.rcParams['axes.grid'] = False
import time
import IPython.display as display
import PIL.Image

import tensorflow as tf
from keras.preprocessing import image


画像を読み込んで配列に変換する関数、テンソルを画像に変換する関数を定義します。

# 画像を読み込み配列に変換し正規化する
def load_image(input_path, size):
    image = tf.keras.preprocessing.image.load_img(input_path, target_size=size)
    image = tf.keras.preprocessing.image.img_to_array(image)
    image = np.expand_dims(image, axis=0)
    image /= 255
    return image


# テンソルを画像に戻す
def tensor_to_image(tensor):
    tensor = tensor.numpy()
    tensor *= 255
    tensor = np.array(tensor, dtype=np.uint8)
    if np.ndim(tensor)>3:
        assert tensor.shape[0] == 1
        tensor = tensor[0]
    return PIL.Image.fromarray(tensor)


このクラスは配列に変換された画像を受け取り、VGG19の各層からの出力を返します。この時点でスタイルの表現に使われる特徴マップはグラム行列に変換されます。

class StyleContentModel():
    def __init__(self):
        # VGG19のどの層の出力を使うか指定する
        self.style_layers = ['block1_conv1', 'block2_conv1', 'block3_conv1', 'block4_conv1', 'block5_conv1']
        self.content_layers = ['block5_conv2']

        self.num_style_layers = len(self.style_layers)        
        self.vgg = self.get_vgg_model()
        self.vgg.trainable = False

    def __call__(self, inputs):
        preprocessed_input = tf.keras.applications.vgg19.preprocess_input(inputs * 255)
        vgg_outputs = self.vgg(preprocessed_input)

        style_outputs, content_outputs = (vgg_outputs[:self.num_style_layers], vgg_outputs[self.num_style_layers:])
        style_outputs = [self.gram_matrix(style_output) for style_output in style_outputs]

        style_dict = {style_name:value for style_name, value in zip(self.style_layers, style_outputs)}
        content_dict = {content_name:value  for content_name, value in zip(self.content_layers, content_outputs)}

        return {'style':style_dict, 'content':content_dict}

    # Keras API を利用してVGG19を取得する  
    def get_vgg_model(self):
        vgg = tf.keras.applications.vgg19.VGG19(include_top=False, weights='imagenet')
        vgg.trainable = False

        outputs = [vgg.get_layer(name).output for name in (self.style_layers + self.content_layers)]
        model = tf.keras.Model(vgg.input, outputs)

        return model

    # グラム行列を計算する
    def gram_matrix(self, input_tensor):
        result = tf.linalg.einsum('bijc,bijd->bcd', input_tensor, input_tensor)
        input_shape = tf.shape(input_tensor)
        num_locations = tf.cast(input_shape[1] * input_shape[2], tf.float32)
        return result / num_locations


そして合計損失を計算する関数、計算した損失から勾配を計算する関数を定義します。

# 合計損失を計算する
def compute_loss(model, base_image, style_targets, content_targets, style_weight, content_weight):
    model_outputs = model(base_image)
    style_outputs = model_outputs['style']
    content_outputs = model_outputs['content']

    style_loss = tf.add_n([tf.reduce_mean((style_outputs[name]-style_targets[name])**2) for name in style_outputs.keys()])
    style_loss *= style_weight / len(style_outputs)

    content_loss = tf.add_n([tf.reduce_mean((content_outputs[name]-content_targets[name])**2) for name in content_outputs.keys()])
    content_loss *= content_weight / len(content_outputs)

    loss = style_loss + content_loss
    return loss, style_loss, content_loss

# 損失を元に勾配を計算する
@tf.function()
def compute_grads(params):
    with tf.GradientTape() as tape:
        all_loss = compute_loss(**params)

    grads = tape.gradient(all_loss[0], params['base_image'])
    return grads, all_loss


最後に画像の生成を行う関数を定義していきます。
生成画像のベースとなる画像にはノイズ画像を指定しています。ベース画像にコンテンツ画像やスタイル画像を指定するとまた違った結果が得られます。

def run_style_transfer(style_path, content_path, num_iteration, style_weight, content_weight, display_interval):
    size = image.load_img(content_path).size[::-1]
    noise_image = np.random.uniform(-20, 20, (1, size[0], size[1], 3)).astype(np.float32) / 255
    content_image = load_image(content_path, size)
    style_image = load_image(style_path, size)

    model = StyleContentModel()
    style_targets = model(style_image)['style']
    content_targets = model(content_image)['content']

    # 生成画像のベースとしてノイズ画像を使う
    # ベースにはコンテンツ画像またはスタイル画像を用いることもできる
    base_image = tf.Variable(noise_image)

    opt = tf.optimizers.Adam(learning_rate=0.02, beta_1=0.99, epsilon=1e-1)

    params = {
        'model': model,
        'base_image': base_image,
        'style_targets': style_targets,
        'content_targets': content_targets,
        'style_weight': style_weight,
        'content_weight': content_weight
    }

    best_loss = float('inf')
    best_image = None

    start = time.time()
    for i in range(num_iteration):
        grads, all_loss = compute_grads(params)
        loss, style_loss, content_loss = all_loss

        opt.apply_gradients([(grads, base_image)])
        clipped_image = tf.clip_by_value(base_image, clip_value_min=0., clip_value_max=255.0)
        base_image.assign(clipped_image)

        # 損失が減らなくなったら最適化を終了する        
        if loss < best_loss:
            best_loss = loss
            best_image = base_image
        elif loss > best_loss:
            tensor_to_image(base_image).save('output_' + str(i+1) + '.jpg')
            break

        if (i + 1) % display_interval == 0:
            display.clear_output(wait=True)
            display.display(tensor_to_image(base_image))
            tensor_to_image(base_image).save('output_' + str(i+1) + '.jpg')
            print(f'Train step: {i+1}')
            print('Total loss: {:.4e}, Style loss: {:.4e}, Content loss: {:.4e}'.format(loss, style_loss, content_loss))

    print('Total time: {:.4f}s'.format(time.time() - start))
    display.clear_output(wait=True)
    display.display(tensor_to_image(base_image))

    return best_image


では実際に画風変換を行ってみましょう。styleweightとcontentweightはそれぞれスタイル損失とコンテンツ損失の重みを表します。

style_path = '../input/neural-image-transfer/StarryNight.jpg'
content_path = '../input/neural-image-transfer/FlindersStStation.jpg'
num_iteration = 5000
style_weight = 1e-2
content_weight = 1e4
display_interval = 100

best_image = run_style_transfer(style_path, content_path, num_iteration, style_weight, content_weight, display_interval)


結果

コンテンツ画像とスタイル画像はこれらの画像を使いました。


コンテンツ画像

Flinders_Street_Station_in_Melbourne

引用元: https://commons.wikimedia.org/wiki/File:Flinders_Street_Station_3.jpg


スタイル画像

The_Starry_Night_art_of_Gogh

引用元: https://commons.wikimedia.org/wiki/File:Van_Gogh_-_Starry_Night_-_Google_Art_Project.jpg


上記のコードを走らせて生成した画像がこちらになります。 スタイル損失とコンテンツ損失の重みを変えて4種類の画像を生成しました。

  • \(\alpha = 10, \beta = 10^{-2}\) generated_image_with_content_loss_weight_1e1_style_loss_weight_1e-2



  • \(\alpha = 10^{2}, \beta = 10^{-2}\) generated_image_with_content_loss_weight_1e2_style_loss_weight_1e-2



  • \(\alpha = 10^{3}, \beta = 10^{-2}\) generated_image_with_content_loss_weight_1e3_style_loss_weight_1e-2



  • \(\alpha = 10^{4}, \beta = 10^{-2}\) generated_image_with_content_loss_weight_1e4_style_loss_weight_1e-2



結果からわかるように \(\frac{\alpha}{\beta}\) が大きくなればなるほどコンテンツ画像がはっきりと生成画像に反映されていることがわかります。これは\(\frac{\alpha}{\beta}\)が大きいとコンテンツ損失に対してモデルが敏感になるからです。 逆に\(\alpha = 10, \beta = 10^{-2}\) の場合はスタイル損失に敏感になりすぎて、生成画像からコンテンツを見つけることができなくなっています。


まとめ

今回は画風変換の基礎となる論文の解説と実装を行いました。ベース画像やパラメーター、VGGのどの層の出力を使うかなどによって違った結果が得られるので色々といじってみるのも面白いかもしれません。またこの論文以降にも画風変換の研究が進められていますので、それらを試す助けになればと思います。


参考文献

[1] Image Style Transfer Using Convolutional Neural Networks
[2] Very Deep Convolutional Networks For Large-scale Image Recognition
[3] Texture Synthesis Using Convolutional Neural Networks
[4] TensorFlow Core: Neural style transfer


Twitter・Facebookで定期的に情報発信しています!

はじめに

こんにちは、システム部の譚です。
Google Colablatoryで、手書きの数字(0, 1, 2 など)から構成されているMNISTデータセットを使い、分類問題のニューラルネットワークを構築してみました。

目次

はじめに
モデルを構築する
おわりに

モデルを構築する

手順

1.TensorFlowのモデルを構築し訓練するためのハイレベルのAPIである tf.kerasを使用する。

from __future__ import absolute_import, division, print_function, unicode_literals

# TensorFlow and tf.keras
import tensorflow as tf
from tensorflow import keras

# Helper libraries
import numpy as np
import matplotlib.pyplot as plt

print(tf.__version__)

2.訓練データとテストデータをダウンロードする。

fashion_mnist = keras.datasets.fashion_mnist

(train_images, train_labels), (test_images, test_labels) = fashion_mnist.load_data()

3.後ほど画像を出力するときのために、クラス名を保存しておく。

mnist = keras.datasets.mnist

(train_images, train_labels), (test_images, test_labels) = mnist.load_data()

4.label nameをつける。

class_names = ['0', '1', '2', '3', '4','5', '6', '7', '8', '9']

5.データの前処理をする。
最初の画像を調べてみればわかるように、ピクセルの値は0から255の間の数値です。
ニューラルネットワークにデータを投入する前に、これらの値を0から1までの範囲にスケールするので、画素の値を255で割ります。

train_images = train_images / 255.0

test_images = test_images / 255.0

6.訓練用データセットの最初の25枚の画像を、クラス名付きで表示する。

plt.figure(figsize=(10,10))
for i in range(25):
    plt.subplot(5,5,i+1)
    plt.xticks([])
    plt.yticks([])
    plt.grid(False)
    plt.imshow(train_images[i], cmap=plt.cm.binary)
    plt.xlabel(class_names[train_labels[i]])
plt.show()

7.モデルを構築する。 

model = keras.Sequential([
    # データのフォーマット変換
    keras.layers.Flatten(input_shape=(28, 28)),
    # 128個のノードのDense層
    keras.layers.Dense(128, activation=tf.nn.relu),
    # 10ノードのsoftmax層
    keras.layers.Dense(10, activation=tf.nn.softmax)
])

8.モデルのコンパイルをする。

model.compile(
# モデルが見ているデータと、損失関数の値から、どのようにモデルを更新するかを決定
optimizer=tf.keras.optimizers.Adam(),

# 訓練中にモデルがどれくらい正確かを測定します
loss='sparse_categorical_crossentropy',

# 訓練とテストのステップを監視するのに使用します
metrics=['accuracy'])

9.モデルの訓練

モデルは、画像とラベルの対応関係を学習します
model.fit(train_images, train_labels, epochs=5)
Epoch 1/5
60000/60000 [==============================] - 7s 111us/sample - loss: 0.2607 - acc: 0.9261
Epoch 2/5
60000/60000 [==============================] - 6s 105us/sample - loss: 0.1127 - acc: 0.9668
Epoch 3/5
60000/60000 [==============================] - 6s 103us/sample - loss: 0.0763 - acc: 0.9769
Epoch 4/5
60000/60000 [==============================] - 6s 103us/sample - loss: 0.0557 - acc: 0.9832
Epoch 5/5
60000/60000 [==============================] - 6s 104us/sample - loss: 0.0448 - acc: 0.9864

モデルの訓練の進行とともに、損失値と正解率が表示されます。このモデルの場合、訓練用データでは0.98(すなわち98%)の正解率に達します。

10.正解率を評価する。 テスト用データセットに対するモデルの性能を比較します。

test_loss, test_acc = model.evaluate(test_images, test_labels)

print('Test accuracy:', test_acc)
10000/10000 [==============================] - 0s 41us/sample - loss: 0.0717 - acc: 0.9777
Test accuracy: 0.9777

ご覧の通り、テスト用データの正解率は訓練用より少し低い結果でした。これは過学習(over fitting)の一例です。

11.予測する。

predictions = model.predict(test_images)
np.argmax(predictions[0])

今回の予測は合っていることが確認できました。

12.10チャンネル全てをグラフ化する。

def plot_image(i, predictions_array, true_label, img):
  predictions_array, true_label, img = predictions_array[i], true_label[i], img[i]
  plt.grid(False)
  plt.xticks([])
  plt.yticks([])

  plt.imshow(img, cmap=plt.cm.binary)

  predicted_label = np.argmax(predictions_array)
  if predicted_label == true_label:
    color = 'blue'
  else:
    color = 'red'

  plt.xlabel("{} {:2.0f}% ({})".format(class_names[predicted_label],
                                100*np.max(predictions_array),
                                class_names[true_label]),
                                color=color)

def plot_value_array(i, predictions_array, true_label):
  predictions_array, true_label = predictions_array[i], true_label[i]
  plt.grid(False)
  plt.xticks([])
  plt.yticks([])
  thisplot = plt.bar(range(10), predictions_array, color="#777777")
  plt.ylim([0, 1])
  predicted_label = np.argmax(predictions_array)

thisplot[predicted_label].set_color('red')
  thisplot[true_label].set_color('blue')

0番目の画像と、予測、予測配列を見てみましょう。

i = 0
plt.figure(figsize=(6,3))
plt.subplot(1,2,1)
plot_image(i, predictions, test_labels, test_images)
plt.subplot(1,2,2)
plot_value_array(i, predictions,  test_labels)
plt.show()

予測確率は100%なので、すべて青で表示されました。
やはり数字に自信満々ですね。(チュートリアルのほうは、Fashion MNISTを使って若干赤かグレーも出てきました。)

13.予測の中のいくつかの画像を、予測値と共に表示する。

# X個のテスト画像、予測されたラベル、正解ラベルを表示します。
# 正しい予測は青で、間違った予測は赤で表示しています。

num_rows = 5
num_cols = 3
num_images = num_rows*num_cols
plt.figure(figsize=(2*2*num_cols, 2*num_rows))
for i in range(num_images): 
    plt.subplot(num_rows, 2*num_cols, 2*i+1)
    plot_image(i, predictions, test_labels, test_images)
    plt.subplot(num_rows, 2*num_cols, 2*i+2)
    plot_value_array(i, predictions, test_labels)
plt.show()

図のように、三行目の5の予測率が82%だった以外は、100%です。(これは手書きが悪かったですね)

おわりに

今回は試すことが多かったですが、これから学ぶべきことに気づけました。
例えば...

  • モデルのレイヤーとは。
  • モデルをコンパイルする際に、損失関数、オプティマイザ、メトリクスなどはモデルにどのような影響するのか。
  • ヘルパーライブラリnumpy、matplotlib.pyplotなどの具体的な使い方など。
    実際に扱ってみることで、機械学習に対する怖さはなくなり、今後も様々なモデルで実装していきたいと思います。

参考

Google Colablatory link 
TensorFlow はじめてのニューラルネットワーク:分類問題の初歩

はじめに

iOSネイティブアプリと機械学習を組み合わせたいと思い、Appleが提供してくれているフレームワークCore MLとVision、事前に訓練したInception v3モデルを使用し、ドラマのSilicon Valleyに出たthis is not hot dogアプリを作ってみました。 
this is not hot dog とは、撮影された画像がホットドッグかどうかを認識するアプリです。YouTube動画がありますのでご覧ください。
YouTubeより Silicon Valley: Season 4 Episode 4: Not Hotdog (HBO)

目次

はじめに
作成手順
まとめ
ソースコード


環境

  • プロセッサ Intel Core i7-5557U 3.1 GHz
  • メモリ 16GB 1867 MHz DDR3
  • Apple Swift version 4.2.1
  • Xcode Version 10.1 (10B61)


Core MLとは

Core ML is the foundation for domain-specific frameworks and functionality
日本語に訳すと学習モデル等をiOS / macOS上で利用する際に、開発者が専門的な知識を必要とせずに扱えるように補助するフレームワークです。

主に下記の2つ機能があります。
1.Load a Pre-Trained Model
 事前学習済みModelをロードでする
2.Make Predictions
 予測する

Visionとは

コンピュータビジョンの機械学習機能を容易にAppに組み込むことができます。
フェイストラッキング、顔検出、ランドマーク、テキスト検出、矩形検出、バーコード検出、オブジェクトトラッキング、画像レジストレーションなどの機能に対応しています。

Inception v3はCore MLモデルのひとつです。 
木、動物、食べ物、乗り物、人々など、1000のカテゴリセットから画像に存在する主なオブジェクトを検出することができます。

作成手順

1.Xcodeにてプロジェクトを作成する
2.下の画像のようなUIをつくる
Xcode作成画面 3.こちらのリンク からInception v3をダウンロード
4.ダウンロードしたものをプロジェクトに入れる
5.モデルの作成

guard let model = try? VNCoreMLModel(for: Inceptionv3().model) else {
  fatalError("Loading Core ML Model Failed.")
}

6.リクエスト(VNCoreMLRequest)の生成とハンドラ処理

 let request = VNCoreMLRequest(model: model){( request,error) in
             guard let results = request.results as? [VNClassificationObservation] else{
                 fatalError("Model failrd to process image")
             }

7.受け取ったrequestのfirstでhotdogかどうかを判断し、結果はnavigationItemに渡す(一個目のrequestは精度が一番高いため、下記のように94%ぐらいで判断された) 

6A29C419-EB06-4321-BC25-8A7220368DC8, revision 1, 0.942064 "hotdog, hot dog, red hot",
if let firstResult = results.first {
   if firstResult.identifier.contains("hotdog") {
       self.navigationItem.title = "Hotdog!"
   } else {
       self.navigationItem.title = "not Hotdog!"
   }
}

8.CIImageへの変換

guard let ciimage = CIImage(image: userPickedImage) else {
    fatalError("Could not convert UIImage into CIImage")
}

9.ハンドラの生成と実行

let handler = VNImageRequestHandler(ciImage: image)
do {
   try! handler.perform([request])
}
catch{
   print(error)
}

10.画像のようなアプリが完成。
hotdogだと...。 Core MLとVision、Inception v3モデルを使ったiOSアプリ

画面上にhotdogの判定があります! 

hotdog以外だと...・ Core MLとVision、Inception v3モデルを使ったiOSアプリ

きちんとNot hotdogの判定がされました。

まとめ

いかがでしょうか?
数行のソースコードでこのように作成できて楽しかったです。
Appleさんは他のモデルも用意してくれているので、ぜひ使ってみてください!

アクセルユニバース株式会社 (以下当社)では、人が対応している業務を画像認識、音声認識、文字認識等を活用して効率化する機械学習ソリューション開発をおこなっています。

当社のインターン生は機械学習論文解説を書いたり、実際に実装したりしています。
随時紹介していくので是非ご覧ください!

定期的にメルマガでも情報配信予定なので、問い合わせページに「メルマガ登録希望」とご連絡ください。


ソースコード

class ViewController: UIViewController, UIImagePickerControllerDelegate,
UINavigationControllerDelegate {


    @IBOutlet weak var imageView: UIImageView!
    let imagePicker = UIImagePickerController()

    var classificationResults : [VNClassificationObservation] = []

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.

        imagePicker.delegate = self
        imagePicker.sourceType = .camera
        imagePicker.allowsEditing = true

    }

    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {

        if let userPickedImage = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
            imageView.image = userPickedImage

            guard let ciimage = CIImage(image: userPickedImage) else {
                fatalError("Could not convert UIImage into CIImage")

            }
            detect(image: ciimage)
        }

         imagePicker.dismiss(animated: true, completion: nil)
    }

    func detect(image: CIImage){

        guard let model = try? VNCoreMLModel(for: Inceptionv3().model) else {
            fatalError("Loading Core ML Model Failed.")
        }

        let request = VNCoreMLRequest(model: model){( request,error) in
            guard let results = request.results as? [VNClassificationObservation] else{
                fatalError("Model failed to process image")
            }

            print(results)

            if let firstResult = results.first {
                if firstResult.identifier.contains("hotdog") {
                    self.navigationItem.title = "Hotdog!"
                } else {
                    self.navigationItem.title = "not Hotdog!"
                }
            }
        }

        let handler = VNImageRequestHandler(ciImage: image)

        do {
            try! handler.perform([request])
        }
        catch{
            print(error)
        }
    }

    @IBAction func cameraTapped(_ sender: UIBarButtonItem) {
        imagePicker.sourceType = .camera
        imagePicker.allowsEditing = false

        present(imagePicker, animated: true, completion: nil)
    }
}

アクセルユニバースの根岸です。

突然ですが、お寿司は食べますか?日本に住んでいて食べたことない人はほとんどいないと思います。ということは何を食べているのか判断できるのは当然の教養ですよね。一方で私のようなお寿司に疎い人もいるわけで、なんとなく美味しいで終わってしまうのはもったいない。そんな時にこそ機械学習を使って解決してみましょう。

ということで、本記事ではネットからお寿司の写真を入手して機械学習(画像認識)を用いてお寿司を判別するためのモデル構築を行うまでの一連の流れを記していこうと思います。コードも合わせて載せていくのでpythonの基本文法とAPIについて少し知っておくと理解しやすいかもしれません。

目次

  • 1.お寿司の画像データをネットから収集する
  • 2.画像を振り分ける
  • 3.画像処理と機械学習で判別する
  • 終わりに一連の流れ

1.お寿司の画像データをネットから収集する

まずは機械学習に必要なデータの収集から始めていきます。判別対象はお寿司なのでお寿司の画像データを収集する必要があります。方法としては

  • 画像検索エンジン(googleなど)から一つずつ引っ張る
  • 画像共有サイト(Instagram,フォト蔵など)で公開されているWeb APIから抽出する
  • kaggle等からデータセットを取得する
  • 自分で作る

があります。今回の画像はWebAPIが充実している「フォト蔵」から抽出してくることにしましょう。

そもそもWebAPIとは、あるサイトが備えている機能を外部から利用できるように公開しているものです。WebAPIでは基本的にHTTP通信が利用されており、下の図のようにユーザがAPIを提供しているサーバに対して、任意のHTTPリクエストを送信することでサーバーからXMLやJSON形式のファイルが返ってくるという仕組みになっています。こうしてAPIから任意のデータを取得する行為を俗に「APIを叩く(物理的にでは有りません)」と言います。それぞれの形式の中身もフォーマットが決まっているので、ファイルのどの部分から情報を取ってくればよいか分かりやすくなっています。(下記JSONレスポンス参照)

WebAPI
HTTPリクエストは基本的に提供元が併せて公開してくれているのでそれに従って作りましょう。
それでは早速今回利用する「フォト蔵」のWebAPIを確認していきましょう。フォト蔵の検索APIは以下のようになっています。
http://photozou.jp/basic/apimethodsearch_public
フォト蔵検索API.png

加えて以下のパラメータを指定することで、柔軟な検索が可能になっています。keywordをクエリとして設定することで任意のキーワードに沿ったデータのみ取得が可能になりそうですね。

フォト蔵検索API_パラメータ

試しにkeywordを"中トロ"としてクエリを設定してブラウザのURL欄に入力してみると以下のような表示がされます。
https://api.photozou.jp/rest/search_public.xml?keyword=中トロ

中トロxml
XMLファイルがブラウザ上で表示されました。

中身を詳しく見ていくとoriginal_widthやoriginal_heightがありますよね。これは元画像の解像度なので、写真の投稿者によって異なります。画像認識する上で画像サイズが違うと前処理が大変なので、サムネイルに使用している画像サイズ(120×120)の画像URLであるthumbnail_image_urlを使うことにしましょう。

このURLをファイルの中から全て抜き出してそれぞれダウンロードしてくれば欲しい画像が抜き出せるはずです。これらをすべて入手するのにurllibライブラリを使って画像を収集していきます。

それでは画像取得までの手順を確認してみましょう。

  1. フォト蔵のAPIを利用して(今回は)JSONファイルを取得する。
  2. JSONファイル内のthumbnail_image_urlから画像のURLをすべて取得する。
  3. 取得したURLの画像を一つずつダウンロードして保存する

JSONファイルの構成は以下のようになっています。

JSONレスポンス
{
"info": {
"photo_num": ###,
"photo": [
{
"photo_id": ###,   ←画像のナンバリングとして利用
"user_id": ###,
"album_id": ###,
"photo_title": "タイトル",
"favorite_num": ###,
"comment_num": ###,
"view_num": ###,
"copyright": "normal/creativecommons",
"copyright_commercial": "yes/no"
"copyright_modifications": "yes/no/share"
"original_height": ##,
"original_width": ###,
"geo": {
"latitude": ###,
"longitude": ###
},
"date": "YYYY-MM-DD",
"regist_time": "YYYY-MM-DDThh:mm:ss+09:00",
"url": "URL",
"image_url": "URL",
"original_image_url": "URL",
"thumbnail_image_url": "URL",   ←これを抜き出す
"large_tag": "<a href=\"...\">...</a>",
"medium_tag": "<a href=\"...\">...</a>"
},
...
]
}
}

それでは実際にコードを書いていきます。
まずは必要なモジュールをimportしてきます。

import urllib.request as req 
import urllib.parse as parse
import os, re, time
import json
#APIのメソッドを指定
photozou_api='https://api.photozou.jp/rest/search_public.json'
sushi_dir='./image/sushi'
  • urllib.request:URLの読み込み、ダウンロード
  • urllib.parse:相対URLから絶対URLを取得
  • os, re:ディレクトリー作成
  • time:ソースコードの一時停止(リソースを逼迫させないためにAPI叩く間隔を空ける)
  • json:JSONファイル読み込み

API用のHTTPリクエストとダウンロードした画像を保存する自身のディレクトリーを指定しておきます。
次に画像を検索するための関数search_photoを作っていきます。上記APIのパラメータ表記を参考にkeyword, offset,limitを指定することでAPIのクエリを組み立てて、それに応じた指定のJSONファイルをダウンロードして返す関数になっています。

この時に画像保存のためのフォルダを一緒に作っています。limitの値を変更すれば最大で引っ張ってこれる画像URLの数を調整できます。ここでは上記WebAPIのパラメータに書いてある初期値の100を使います。

def search_photo(keyword, offset=0, limit=100): #画像の検索及びJSONファイルを返す関数
#APIのクエリ組み立て
keyword_encoding=parse.quote_plus(keyword) #HTMLフォーム値の空白をプラス記号に置き換え
query="keyword={0}&offset={1}&limit={2}".format(keyword_encoding,offset,limit)
url=photozou_api+'?'+query
#クエリとキャッシュを保存しておく
if not os.path.exists(sushi_dir):
os.makedirs(sushi_dir)
sushi=sushi_dir+"/"+re.sub(r'[^a-zA-Z0-9\%\#]','_',url)
if os.path.exists(sushi):
return json.load(open(sushi, "r", encoding='utf-8'))
print(url)
req.urlretrieve(url,sushi)
time.sleep(1) #逼迫させないために間隔を空ける
return json.load(open(sushi, "r", encoding='utf-8'))

JSONファイルが取得できたらその中からthumbnail_image_urlの部分を抜き出してダウンロードしてみましょう。中にはthumbnail_image_urlを持たないものが混ざっているので、例外として排除しておきます。ファイル名はphoto_idと'_thumb'を用いてjpgで保存します。画像をダウンロードするときにもリソースの逼迫につながらないように配慮して間隔を空けるようにしておきましょう。

def download_single(info, save_dir): #画像のダウンロード 
#画像保存のためのフォルダを作成
if not os.path.exists(save_dir):
os.makedirs(save_dir)
if info is None:
return
if not 'photo' in info['info']:
return
photo_list=info['info']['photo']
for photo in photo_list:
photo_title=photo['photo_title']
photo_id=photo['photo_id']
url=photo['thumbnail_image_url']
path=save_dir+'/'+str(photo_id)+'_thumb.jpg'
if os.path.exists(path):
continue
try:
print('download',photo_id,photo_title)
req.urlretrieve(url,path)
time.sleep(1)#間隔を空ける
except Exception as e:
print('ダウンロードできませんでした url=',url)

それでは上記2つを実行する関数を作って行きます。

実際に何個の画像を取得できるかわからないのでwhileとmax_photoで上限内でありったけの画像をダウンロードしてくることにします。いくつか条件分岐させていますがニッチなカテゴリーを検索するわけじゃないので、記述しなくても問題ないですが念のため。

def download_all(keyword, save_dir, max_photo=1000):
offset=0
limit=100
while True:
info=search_photo(keyword, offset=offset, limit=limit)
#情報が欠損している場合(例外処理)
if (info is None) or (not 'info' in info) or (not 'photo_num' in info['info']):
print('情報が欠損しています')
return
photo_num=info['info']['photo_num']
if photo_num==0:
print('photo_num=0,offset=',offset)
return
#画像が含まれている場合
print('download offset=', offset)
download_single(info, save_dir)
offset+=limit
if offset >= max_photo:
break

モジュールとして使わずに単独で使用する用に以下を記述しておきます。download_allのkeywordを'中トロ'にしていますがここのkeywordを変えることでそれに応じた検索画像をダウンロードしてきます。

if __name__=='__main__':
download_all('中トロ','./image/sushi')

ということでプログラムが完成した(downloader-sushi.pyで保存)のでターミナルから実行してみましょう。

$ python3 downloader-sushi.py

するとフォト蔵にある中トロの画像を最大で1000件ダウンロードします。
中トロ画像たち.png
中トロ以外にも鉄火巻、こはだについて同じ処理をした後、新しいフォルダ"tyutoro","tekkamaki","kohada"を作成します。

中トロ, 鉄火巻, こはだ
これにて学習に必要な材料は揃いましたが・・・

2.画像を振り分ける

画像を振り分ける

ここからが泥臭い作業になります。

中トロと検索すれば全て中トロのお寿司を1貫を画面いっぱいに載せてくれている画像なら良いのですが、残念ながら中には複数の別のお寿司が写ってる、中トロが写っていない、もはや人しかいない、なんて画像がかなり混在しています。ここで手作業で学習に使えそうな画像を抽出するという泥作業が発生します(画像を部分的にアノテーションすれば使えるものもありますが、サイズの調整が大変なので割愛します)。正にデータサイエンティストの醍醐味ですね!

そうして抽出した画像数は以下の通りです。

  • 中トロ 111点
  • 鉄火巻 135点
  • こはだ 66点

計311点
中トロ、鉄火巻、こはだ

圧倒的こはだ不足です。そもそも全体的にサンプルが足りていないですが、見逃してください...。
これらの画像のファイルは"./image"上にそれぞれ作成しました。後は画像処理を行って画像認識のモデルに突っ込んでいきましょう。

3.画像処理と機械学習で判別する

画像処理と機械学習

ここで行うことは3点です

  • 画像を数値データに変換と水増し(画像処理)
  • 画像認識モデルの構築と学習
  • 画像の判別

3-1 画像を数値データに変換と水増し

画像データを学習させるためには数値データへの変換が必須です。変換後はデータサイエンティスト御用達のNumpyの形式に変換させましょう。まずは必要なモジュールをimportしてきます。

from PIL import Image
import os, glob
import numpy as np
  • PIL:画像処理(エンコードに利用)
  • glob:画像のディレクトリパス一覧を取得

分類対象は中トロ、鉄火巻、こはだの3種類なので下記で指定しておきます。

root_dir='./image/'
categories=['tyutoro','tekkamaki','kohada']
nb_classes=len(categories)
image_size=100

次にフォルダ(カテゴリー)ごとに画像データを読み込んでRGB変換、Numpy形式への変換を行う関数を宣言していきます。

この際に画像の水増しも一緒に行っていきます。画像の角度を変えたり反転させることで画像数を増やしていきます。注意すべきことは水増ししたデータが学習データとテストデータの両方に混在してしまうとリークが発生してしまい、精度が異常に高くなってしまう現象が起きるので、水増しは学習データのみ行うようにしましょう。is_trainの真偽で水増しをするかどうかを設定しています。

#画像処理と水増しを行う
X=[]
Y=[]
def padding(cat, fname, is_train):
img=Image.open(fname)
img=img.convert("RGB") #RGB変換
img=img.resize((image_size,image_size)) #画像サイズ変更(100×100)
data=np.array(img) #numpy形式に変換
X.append(data)
Y.append(cat)
#学習データのみ水増しするので学習データではない場合は以下のfor文を実行しない
if not is_train:
return
for angle in range(-20, 20, 10):
#画像の回転
img2=img.rotate(angle)
data=np.asarray(img2)
X.append(data)
Y.append(cat)
#画像の左右反転
img3=img.transpose(Image.FLIP_LEFT_RIGHT)
data=np.asarray(img3)
X.append(data)
Y.append(cat)

#カテゴリーごとの処理
def make_train(files, is_train):
global X,Y
X=[]
Y=[]
for cat, fname in files:
padding(cat, fname, is_train)
return np.array(X), np.array(Y)

加えてディレクトリーごとに分けられているファイルを収集してallfilesに統合しています。

#ディレクトリーごとにファイルを収集する
files_all=[]
for idx, cat in enumerate(categories):
image_dir=root_dir+'/'+cat
files=glob.glob(image_dir+'/*.jpg')
for f in files:
files_all.append((idx, f))

加えてディレクトリーごとに分けられているファイルを収集してallfilesに統合しています。

最後に学習データとテストデータに分けてモデルに入れるデータが整います。

import random, math
#シャッフル
random.shuffle(files_all)
MATH=math.floor(len(files_all)*0.6)
train=files_all[0:MATH]
test=files_all[MATH:]

後はモデルを作って学習させて完成ですね。

3-2 画像認識モデルの構築と学習

それでは機械学習のフェーズに入っていきましょう。

使用するモデルはCNN(畳み込みニューラルネットワーク)です。ここではTensorFlowとKerasを組み合わせてCNNを組み立てて行きます。モデルの概要は以下のようにしています。
CNN概要
CNNの具体的な中身が以下の通り

_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
conv2d_1 (Conv2D) (None, 100, 100, 64) 1792
_________________________________________________________________
activation_1 (Activation) (None, 100, 100, 64) 0
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 50, 50, 64) 0
_________________________________________________________________
dropout_1 (Dropout) (None, 50, 50, 64) 0
_________________________________________________________________
conv2d_2 (Conv2D) (None, 50, 50, 128) 73856
_________________________________________________________________
activation_2 (Activation) (None, 50, 50, 128) 0
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 25, 25, 128) 0
_________________________________________________________________
dropout_2 (Dropout) (None, 25, 25, 128) 0
_________________________________________________________________
conv2d_3 (Conv2D) (None, 23, 23, 128) 147584
_________________________________________________________________
activation_3 (Activation) (None, 23, 23, 128) 0
_________________________________________________________________
max_pooling2d_3 (MaxPooling2 (None, 11, 11, 128) 0
_________________________________________________________________
dropout_3 (Dropout) (None, 11, 11, 128) 0
_________________________________________________________________
flatten_1 (Flatten) (None, 15488) 0
_________________________________________________________________
dense_1 (Dense) (None, 1028) 15922692
_________________________________________________________________
activation_4 (Activation) (None, 1028) 0
_________________________________________________________________
dropout_4 (Dropout) (None, 1028) 0
_________________________________________________________________
dense_2 (Dense) (None, 3) 3087
_________________________________________________________________
activation_5 (Activation) (None, 3) 0
=================================================================
Total params: 16,149,011
Trainable params: 16,149,011
Non-trainable params: 0
_________________________________________________________________

加えてディレクトリーごとに分けられているファイルを収集してallfilesに統合しています。

それでは上記を元にコードを書いていきます。

# データをロード
def main():
X_train, y_train=make_train(train,True)
X_test, y_test=make_train(test,False)
# データを正規化する
X_train=X_train.astype("float") / 256
X_test=X_test.astype("float") / 256
y_train=np_utils.to_categorical(y_train, nb_classes)
y_test=np_utils.to_categorical(y_test, nb_classes)
# モデルを学習し評価する
model=model_train(X_train, y_train)
model_eval(model, X_test, y_test)
# モデルを構築
def build_model(in_shape):
model=Sequential()
model.add(Convolution2D(64 , 3, 3, border_mode='same',input_shape=in_shape))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))
model.add(Convolution2D(128, 3, 3, border_mode='same'))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))
model.add(Convolution2D(128, 3, 3))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))
model.add(Flatten())
model.add(Dense(1028))
model.add(Activation('relu'))
model.add(Dropout(0.5))
model.add(Dense(nb_classes))
model.add(Activation('softmax'))
model.compile(loss='binary_crossentropy',optimizer='rmsprop',metrics=['accuracy'])
model.summary()
return model
# 学習
def model_train(X, y):
model=build_model(X.shape[1:])
model.fit(X, y, batch_size=30, nb_epoch=30)
return model
# 評価
def model_eval(model, X, y):
score=model.evaluate(X, y)
print('loss=', score[0])
print('accuracy=', score[1])
if __name__=="__main__":
main()

3-3 画像を判別する

ということでプログラムが完成した(sushi_cnn.pyで保存)のでターミナルから実行してみましょう。

$ python3 sushi_cnn.py

計算に結構な時間かかりますが、最終的に以下のような正答率が出てきました。

loss= 1.2733278312683105
accuracy= 0.8826666955947876

およそ88%といったところでしょうか。

他にも画像サイズを変えてみたり、層を変えてみたりしましたが概ね90%前後を漂う感じでした。3種類と比較的少ない種類を判定しましたが、ほどほどに上手くできてるかと思います。

終わりに

本記事では、Web APIによる画像入手に始まり、前処理(データの振り分けや選別)、CNNの学習による画像認識まで行ってみました。もちろん学習させたお寿司の種類が少なかったので実際に使えるわけではないですが、"データが集まって前処理をしっかりすれば"実用的なものになっていくのではないでしょうか。

○環境
OS:Mac Catalina 10.15.1
CPU:3.1 GHz デュアルコアIntel Core i7
RAM:16 GB 1867MHz DDR3
python:3.7.5
各種ライブラリは投稿時点で最新のものを使用しています


Twitter・Facebookで定期的に情報発信しています!

U-netとは

U-netはFCN(fully convolution network)の1つであり、画像のセグメンテーション(物体がどこにあるか)を推定するためのネットワークです。

生物医科学(biomedical)の画像のセグメンテーションを行うために2015年に発表されました。

(論文URL : https://arxiv.org/abs/1505.04597)

この記事では、まずU-netの中で行われている処理についてを1〜4章でまとめ、それらの組み合わせであるU-netをまとめたいと思います。

目次

1.Semantic segmentation

2.fully convolution network(FCN)

3.deconvolution

4.skip-connection

5.U-netの仕組み

1.Semantic segmentation

画像のピクセルそれぞれをクラス分類するタスクです。

(MNISTなど)画像のクラス分類と異なるのは、画像全体をクラス分類しない点です。

ピクセルごとのクラス分類ができると、以下のような出力を得られます。

Alt text

by https://devblogs.nvidia.com/image-segmentation-using-digits-5/

2.fully convolution network(FCN)

一般的なCNNでは畳み込み層と全結合層がありますが、FCNはこの全結合層を畳み込み層に置き換えたものです。

全結合層を畳み込み層に置き換えることで、「物体がなにであるか」という出力から「物体がどこにあるか」という出力になります。

Alt text

by https://arxiv.org/abs/1411.4038

画像上部分が画像全体のクラス分類で、画像下部分はヒートマップで猫がどのあたりにいるかネットワークが把握していることがわかります。

3.deconvolusion

1つ上の画像のヒートマップは入力画像に畳み込み処理を何度も行い、とても小さくなっています。

これに1番上のような出力が得られるような処理を行なっていきます。

このような処理を「up sampling」といい「deconvolution」が有名です。

日本語だと「逆畳み込み」といい、畳み込み処理の逆処理だと思ってください。

CNNで畳み込み層のフィルターの要素を学習していくように、逆畳み込み層のフィルターも同じように学習できます。

4.skip-connection

畳み込み処理を加えていくと、ネットワークが「物体が何であるか」についての特徴を抽出していきますが、poolingの影響で「物体がどこにあるか」についての特徴は失われていきます。

畳み込み処理を行なった後で、逆畳み込みを行なっても物体の位置情報は満足に復元できない場合があります。

それを解決するのがskip-connectionです。

これは畳み込みを行なった後、特徴マップを保持しておいて、後で逆畳み込みをする画像に足し合わせる処理です。

上で提示した車の画像に用いると以下のようになります。

Alt text

by : https://devblogs.nvidia.com/image-segmentation-using-digits-5/

推定する領域(色のついた部分)がシャープになっていることがわかります。

5.U-netの仕組み

U-netには上でまとめた

・fully convolution network(FCN)

・deconvolution

・skip-connection

が使われています。

構造は以下のようになっています。

Alt text

by : https://arxiv.org/abs/1505.04597

左右対象でアルファベットの「U」に似ていることから、「U-net」と呼ばれているそうです。

大雑把に左側の処理では画像の畳み込みを行い、右側では逆畳み込みをskip-connection(中央矢印)からの情報を参考に行なっているイメージです。

まとめ

U-netの内部で行われる処理について、ざっくりと説明しました。

まとめると・・・

・画像全体のクラス分類(MNISTなど)からFCNとdeconvolutionを使い、物体の位置情報を出力できるようになった

・skip-connectionを用いて畳み込みによって失われる位置情報を保持しておくと、より精密な領域を出力できる

・U-netは上記2つを上手く組み合わせている

こちらの記事ではPythonで実装を行い、学習している様子を紹介しています。


定期的にメルマガでも情報配信をしています。
ご希望の方は問い合わせページに「メルマガ登録希望」とご連絡ください。


Twitter・Facebookで定期的に情報発信しています!

現在、多くの業界でAI・機械学習の重要性が増加しています。

背景のひとつに労働人口の減少による人手不足があります。帝国データバンクの調査によると、「正社員不足」状態の企業は全体の50.3%を占めています。
人手不足も解消のためには、業務自体を削減すること、または業務を人から代替する必要があります。

ここでは人間の目の代わりとなり、判断をおこなう機械学習である画像認識技術を紹介します。

スタンフォード大の研究者らがAI関連の研究開発や経済、教育、各国の動向など多様なデータをまとめた「AI Index」の2019年版では、ここ1年半でクラウドインフラで大規模な画像分類システムを学習させるのに必要な時間が、2017年10月の約3時間から2019年7月の約88秒に短縮され、同様に学習させるコストも下がったと報告されています。 時間・マシンスペックの制限が徐々になくなることで機械学習の画像認識分野のビジネス活用はますます注目されていきます。


目次


機械学習での画像認識とは

画像からパターンを認識し、何が写っているのか判断することが画像認識です。光景の知覚・認識をコンピュータがおこなうことで、人間がおこなうはずであった画像の判別や物体の検出などを自動化することができます。

例えば、動物が写っている写真をその動物ごとに分類することもできますし、製造業でのシーンでは正解とは違うもの(不良品)を見つけることもできます。本記事の最後でも紹介しますが、画像認識は様々な分野でも応用できることと、ディープラーニングの登場で精度が向上したこと、高性能なモデルの開発で学習(構築)時間を削減したことから急速にビジネス活用が進みました。

元々はロボットにおいて外界にある物体検出の手段として考えられた画像認識技術でしたが、1970年代における人間の知識をプログラムとして直接書く方式から、画像の形、色、動きなどの幾何学的な特徴を認識する方法にシフトして大きな進歩を遂げています。

技術の発展により、画像認識はAI(人工知能)が活用される一分野として現在も急速に発展しています。それでは画像から物体を検出しそれが何であるか判定する仕組みをご紹介します。


画像認識の仕組み

一般的な画像認識のプロセスをご紹介します。

Image recognition


画像認識のモデルの構築

画像認識をおこなう際、「画像のどこに何が写っているのか」を認識するためのモデルが必要です。機械学習を用いて学習データからこのような検出・判別するモデルを構築します。モデルをあらかじめ構築しておいて、認識する画像に対して判別結果を出力する仕組みを作ります。


特徴抽出

モデルで実際に画像を判別する際には、画像の中から 特徴抽出する必要があります。画像の局所的な特徴の抽出や画像に含まれるノイズの除去などを、画像処理の手法や統計的な手法によっておこないます。判別に必要のない情報を除いた上で、判別結果を計算することができます。


プーリング

抽出した特徴はいくつかの一定の大きさの領域にまとめた後、プーリング と呼ばれる処理でまとめられた領域を1つの値で代表します。画像の空間的な情報を削除し、判別に必要な情報のみを残します。


分類

特徴抽出・プーリングの実施後には、事前に用意していたモデルによって画像を判別します。

また、ディープラーニングに代表される学習手法は特徴抽出・プーリングの処理をひとまとめにして、それらの手順をさらに繰り返すことで認識精度を高めています。


画像認識技術の4つの活用事例

文字認識

画像認識の代表的な活用事例です。文字の書かれた画像から文字の領域を抽出し、どの文字か推測します。この技術で文章読み取りが自動になり、転記作業の負担を軽減します。

Image recognition

手書き書類・メモを文字起こししていませんか?社内は紙の書類で溢れていませんか?

業務を効率化し生産性を向上するためには、紙の情報ををデータ化することが不可欠です。顧客データから自社製品が強い顧客像を割り出したり、売上データから来期の売上予測をしたりと、利益につながるデータの二次活用は勿論、紙の保管場所を削減します。

画像に写る物体を理解し、それが何かというラベル(正解)を割り当てる過程を 物体認識 と呼びます。


医療画像認識による診断支援

医療分野においても画像認識は活用されています。医師が医療画像を見ることで病変を判断する過程を、コンピュータである程度自動化します。最近では医師でさえ見抜けない腫瘍の変異をコンピュータが画像の中の領域を抽出し、判別する事例もあります。これにより、病変時は発見までの時間短縮、診断ミスを低減します。

医療画像での変異発見は 物体検出 と呼ばれ、画像の中の変異の領域を抽出することを セマンティックセグメンテーションと呼びます。


インターネット上の画像検索

インターネット上で画像認識を身近に感じるのは、この 画像検索 だと思います。入力された文字列あるいは画像と類似する画像を検索する技術であり、データベースに保存された画像から似たような画像を検索します。

Image recognition

画像検索は具体的な判別を行う インスタンス認識 によってラベル付けをします。あらかじめラベル付けしておくことで瞬時に内容や見た目の近い画像を見つける類似画像検索が可能です。


自動車の自動運転

今後、さらに発展が予測される画像認識の活用分野のひとつとして、自動車の自動運転が挙げられます。自動車に搭載されたカメラによって外部の状況を判断し、自動車に運転をさせる技術です。カメラから受け取った画像を認識させることで、人間が難しい操作をすることなく搭載されたコンピュータが運転することが可能になります。

自動運転では静止した画像だけでなく、その時系列情報も必要です。この時系列情報を利用した画像認識を 動画像認識と呼びます。


まとめ

今回はこれらのことをご紹介しました。

  • 画像認識とは何か
  • 画像認識技術の活用事例
  • 画像認識の仕組み


画像認識は2020年東京オリンピックで採用されたりと、今後浸透していく機械学習の技術です。応用方法の豊富さと進歩の速さがあいまって、「 AI搭載家電」など、急激に導入検討が進んでいます。


当社はAI・機械学習を活用したソリューションをご提案しています。

  • 自社にあるデータを2次活用したい
  • 画像データを活用したAIを作成したい
  • 既にAIシステムを利用しているが思うような効果が得られていない
  • 機械学習ではなにが出来るのか興味がある


このようなご相談は画面右上の[お問い合わせ]までご相談ください。


アクセルユニバースの紹介

私達はビジョンに『社会生活を豊かにさせるサービスを提供する。』ことを掲げ、このビジョンを通して世界を笑顔にしようと機械学習・深層学習を提案しています。

  • ミッション(存在意義)
    私達は、情報通信技術を使って万物(全ての社会、生物)の暮らしをよりよくすることに貢献し、 それを加速させることを使命とします。

  • ビジョン(目標とする姿)
    社会生活を豊かにさせるサービスを提供する。

  • バリュー(行動規範)

    1. 変化を求め、変化を好み、変化する
    2. 自分の高みを目指してどんどん挑戦する
    3. お客様と一蓮托生でプロジェクトを進める


acceluniverse


参考文献

原田達也(2018) 『画像認識』 講談社

平井有三(2018) 『はじめてのパターン認識』 森北出版


Twitter・Facebookで定期的に情報発信しています!

このアーカイブについて

このページには、過去に書かれた記事のうち画像認識カテゴリに属しているものが含まれています。

前のカテゴリは自然言語処理です。

次のカテゴリは論文解説です。

最近のコンテンツはインデックスページで見られます。過去に書かれたものはアーカイブのページで見られます。