2020年3月アーカイブ

フェイクニュースは珍しいものではありません。
コロナウイルスの情報が凄まじい速さで拡散されていますが、その中にもフェイクニュースは混ざっています。悪意により操作された情報、過大表現された情報、ネガティブに偏って作成された情報は身近にも存在しています。
これらによって、私たちは不必要な不安を感じ、コロナ疲れ・コロナ鬱などという言葉も出現しました。

TwitterやInstagramなどのソーシャルメディアでは嘘みたいな衝撃的なニュースはさらに誇張な表現で拡散され、フェイクニュースは瞬く間に広がります。嘘を見破るのは難しく、見破られないまま多くの人に「正しい情報」として届けられています。
2017年に『Journal of Economic Perspectives』誌に掲載された記事では、アメリカの成人の62%がソーシャルメディア上でニュースを得ており、フェイクニュースサイトへの訪問は41.8%がソーシャルメディアのリンクを経由していることが示されています。

フェイクニュースの無い世界ではじめて、私たちは正しい情報に触れ健全な判断をすることができます。
しかし、膨大な情報が混在するソーシャルメディア上でフェイクニュースを発見する作業は人間が行うにはコストがかかりすぎるため現実的ではありません。

AIはフェイクニュースを発見できないでしょうか。


AIはフェイクニュースを発見できる?

ソーシャルメディアの登録者数が世界的に増えているにも関わらずソーシャルメディアのプラットフォームもこの新興市場に投資していないようで、技術検証に留まっています。

革新的なスタートアップが出現するかどうかは出資の有無にかかっているでしょう。 今後、ソーシャルメディアの世界を牽引するためにフェイクニュース検出への投資が出現するのではないでしょうか。

商品化はされていませんが、技術検証は進んでいます。


FEVERによる情報の正誤判定

マサチューセッツ工科大学では、Facebookが支援している研究者がフェイクニュースを発見するためにFEVER(Fact Extraction and Verification)を利用しています。 ※FEVER:大規模なファクトチェックのためのデータベース

しかし、FEVERでさえもバイアスがあり思うようにフェイクニュースを発見できていません。

FEVERは機械学習の研究者たちによって、ウィキペディアの記事を元に、情報に対して正誤を判定するために利用されてきました。しかし、そのデータセットにバイアスがありそれで学習したモデルが誤った結果を導いてしまったと研究チームは分析しています。

例えば、'did not''yet to'のような否定のフレーズが含まれた文章は「偽」の場合が多いため、FEVERで訓練されたモデルは、これらの文が実際に真実であるかどうかに関係なく、偽と判定してしまう可能性が高いことが課題です。

真実である"Adam Lambert does not not hide publicly his homosexuality "は、AIに与えられたデータから真実と導けるにもかかわらず、否定のフレーズが入っているので偽とされる可能性が高いです。

判定の根拠に主張の言語(どんなフレーズが入っているか)に焦点を当てており、外部の証拠(実際はどうであるか?)を考慮に入れていないことが問題でしょう。

証拠を一切考慮せずに主張を分類することのもう一つの問題点は、全く同じ主張が今日は真であっても、将来的には偽であると考えられる可能性があるということです。 例えば、女優のオリビア・コルマンがオスカーを受賞したことがないというのは2019年までは本当でしたが、現在では違います。情報は常に更新されていくのです。

これらの課題解決のため、研究チームはFEVERのバイアスを解消し一部を修正するデータセットを作成しています。


計算言語学と機械学習のハイブリッド手法

2018年に発表されたミシガン大学とアムステルダム大学の論文は、効果的にフェイクニュースと戦うために、計算言語学とファクトチェック(事実検証)を組み合わせるアイデアを発表しました。

計算言語学では、自動化された方法でフェイクニュース発見を実施できます。 本物と偽物のニュースを区別するために、言語学的特徴に注目してニュース内アイテムの語彙的、構文的、意味的なレベルを見て判断します。

開発されたシステムの性能は、このタスクにおいて人間の性能に近く、最大76%の精度でフェイクニュースを発見することができました。

しかし、考慮するのは言語学的特徴だけではなく、事実検証の要素も含めるべきと指摘されています。 例えば、記事へのリンク数やコメントなどのメタ特徴や、ニュースページの見栄えなどのモダリティ的な特徴のことです。これらは機械学習でアプローチすることができます。

これらを踏まえて今後の研究では、計算言語学や事実検証(AI・機械学習)のアプローチをハイブリッドにした予測と意思決定が必要だと考えられます。


実現に向けた課題

これらの例を見ただけでも課題があることがわかります。
イノベーターたちは様々な角度から問題を考えてきましたが、思うような改善には至っていません。

どちらの例でも言及されていない課題があります。
フェイクニュースがどのように作られているかご存知ですか。現在はAIがかなり精巧に作成していますが、問題は人間の作るフェイクニュースです。AIはルールに従ってフェイクニュースを作成するため、私たちも対抗することができますが、人間が作成したニュースを見破るためには難易度が一気に上がります。


たとえば、Twitter。


多くの情報を拡散したい時にTwitterは効果的なツールです。
Twitterでフェイクニュースを拡散したい時、複数のBotで自動投稿するでしょう。Botは単に「いいね!」や「フォロー」数を増やしたり、リンクを拡散したり、トレンドに入るために多くの投稿をしたり、反対派を荒らしたりするために構築されます。このように自動化されたアカウントの大部分は非常に単純なもので発見しやすく現在の技術でも対応できるでしょう。

これだけの対策でも、拡散されるフェイクニュースの効果を弱めることができます。 悪意のある情報の拡散は私たちの気持ちを落ち込ませるだけでなく、健全な判断をも奪います。

スクリーンショット 2020-03-26 16.52.10.png


フェイクニュース発見に尽力すべきは誰?

このようなソーシャルメディア上のフェイクニュースの発見は誰が行うべきでしょうか。

FacebookやGoogleなどは暴力やテロリストグループの情報を含むコンテンツの発見には投資していますが、フェイクニュースの発見にはそれほど熱心ではないようです。理由は先程のようにオンライン上には様々な表現、文脈で存在し、AIが独自に発見するは難しいためでしょう。 しかし、今後さらに大量のコンテンツが溢れ、有益な情報のみ吟味するためにはフェイクニュースを発見することは必須ではないでしょうか。


市場のチャンス?

今後、ソーシャルメディアのプラットフォームを牽引する企業は出現するでしょう。
利用ユーザー数を増やし、市場を獲得しするためにはユーザーの安全が確保されることが大切です。子供には使わせられないソーシャルメディアは大人も使いたくないですよね?

ソーシャルメディアの登録者数が世界的に増えているにも関わらず、どのソーシャルメディアのプラットフォームもフェイクニュースの発見に積極投資はしていないようで、技術検証に留まっています。

しかし、より安全で健全な世界のためにこの技術は凄まじい進歩を遂げていくのではないでしょうか。


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

ディープラーニングを使って、人の顔の画像を入力すると 年齢・性別・人種 を判別するモデルを作ります。
身近な機械学習では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で定期的に情報発信しています!

Googleが発表したBERTは記憶にも新しく、その高度な性能はTransformerを使ったことで実現されました。
TransformerとBERTが発表される以前の自然言語処理モデルでは、時系列データを処理するRNNとその発展形であるLSTMが使われてきました。このLSTMには、構造が複雑になってしまうという欠点がありました。こうしたなか、2017年6月に発表された論文「Attention is all you need」で論じられた言語モデルTransformerとAttentionと呼ばれる手法は、LSTMのような複雑な構造を使わずに高性能を実現したのでその後の言語モデル開発に大きな影響を与えました。 今回は転移学習モデルTransformerを用いた新しいモデルのText-to-Text Transfer Transformerを紹介します。


Text-to-Text Transfer Transformer(T5)とは

 近年、自然言語処理の分野では、事前学習モデルを利用しfine tuningをする転移学習(transfer learning)が強力な技術として様々なタスクで少ないデータセットでも精度向上をもたらしています。特に2018年に発表されたBERT以降、研究が盛んに行われており、ベンチマーク(GLUEなど)のSoTAも頻繁に更新されています。この論文に「多様なアプローチ、手法、実践がなされている」と書かれているように、それぞれの研究によってアプローチのしかたも異なります。  その中で、この論文で紹介されているモデルT5は「Text-to-Text Transfer Transformer」の略で、Text-to-Textとある通り、入力と出力の両方をテキストのフォーマットに統一して、転移学習を行うモデルです。
Figure1.png

 上図にあるように、翻訳、質疑応答、分類、要約などすべてのタスクで入力をテキストで受けて、出力もテキストの形で返しています。また、このモデルの特徴は一つのモデルで上で挙げたようなタスクをすべてこなせることです。実際にタスクを行わせる際には上の図にあるように"translate English to German:"とPrefixを与えてどのタスクかを知らせています。T5のモデル構造のベースはTransformerとなっていて、論文中でも特別新しい手法を紹介しているわけではありません。転移学習における研究が盛んに行われ、異なる手法の比較が難しい中で、どんなモデル構造やデータセットなどを用いれば良いのかを、T5を用いて比較していくことがこの論文の中心となっています。


精度比較

 それでは論文中で行われている実証研究について見ていきます。


Baseline

 まずベースラインについて見ていきます。

 事前学習どの程度行ったなどの詳細はここでは述べませんが、基本的なモデルの構造としては一般的なTransformerを用いて、特徴的なのはすべてのタスクがtext-to-textの形式にされていることです。また事前学習にはC4(Colosal Clean Crawled Corpus)というデータセットを使っています。後にデータセットでの精度比較があるのでC4の詳細はそこで述べます。
Figure2.png

教師なし学習の目的関数ついては、BERTで用いられたMasked Language Modelingなどに倣った"denoiding objective"を使用しています。上の図を見るとイメージしやすいと思います。
Table1.png

 ベースラインの性能を簡単に表で見ていきます。一番上がベースラインの結果の平均、2番目が標準偏差、最後が事前学習を行っていないモデルの結果となっています。この結果の中で、GLUEとSQuADに関してはBERTのモデルと非常に近いスコアを出しているそうです。またEnFr(英語からフランス語への翻訳)に関しては、十分に大きな訓練データがあるために、事前学習で得られる情報が重要ではなくなっているため、ベースラインのスコアと事前学習をしなかったモデルのスコアがあまり変わらないという結果が出ています。

それではここからモデル構造などを変えての比較を見ていきます。


モデル構造

 モデルのベースは上でも述べたようにTransformerで、今回の比較は次の三つについて行っています。

  • Encoder-Decoder:基本的なTransformerで採用されているモデル  Encoderはfully-visible attention mask(全ての入力を出力の予測に用いるマスクになっている)で、 Decoderはcasual attention mask(予測する単語を見えないようにしている)を採用したモデル       
  • Language Model:Encoder-DecoderでのDecoder部分のみを使ったようなモデル
  • Prefix LM:Language Modelをベースに、Prefix部分はマスクしないという変更を与えたモデル

以下の図がそれぞれのモデル構造のイメージ図になっています。
Figure4.png

 また、attention maskのイメージ図は下のようになっています。
Figure3.png

Transformerの詳細については、【論文】"Attention is all you need"の解説をご覧ください。検証結果は以下のようになります。
Table2.png

ここで教師なし学習の目的関数として、ベースラインと同様のdenoisingだけでなく、LM(通常の言語モデルで予測)も採用しています。また、モデルの構造で上の三つの他に"Enc-dec,shared"と"Enc-dec,6 layers"があります。Enc-dec,sharedはencoderとdecoderでパラメータを共有したもので、Enc-dec,6 layersはencoderとdecoderの層をそれぞれ12から6にして、パラメータを半分にしたもので、計算コストも半分になっています。それぞれのイメージ図は以下のようになります。

  • Enc-dec,shared
    enc-dec_share.png

  • Enc-dec,6 layers
    enc_dec_6layer.png

 結果を見てみると、まず目的関数については全体を見てもDenoisingのほうが良い結果を得ています。モデル構造については、Encoder-decoderが一番良い精度を出しています。Enc-dec,sharedはほとんど変わらず、パラメータを共有するしないは精度にあまり影響を及ぼさないことが分かり、Enc-dec,6 layersはその2つと比べて性能が劣り、層の数はある程度影響を与えています。また、Language ModelとPrefix LMもEncoder-decoderよる精度はわるくなっていて、2つを比べるとPrefix LMのほうが良い結果になっていてます。


Objectives

 次に教師なし学習の目的関数について比較を行っていきます。
Figure5.png

ここでは、上の図の左から順にそれぞれ検証していきます。下のような目的関数の例も記載されているので、逐次見ていきます。この例の元の文は"Thank you for inviting me to your party last week."です。
Table3.png

High-Level Approaches
 ここで比較するのは次の三つの目的関数です。
Table3_1.png

  • Prefix language modeling :オリジナルの文章の途中までを入れて、その続きの文章を予測
  • BERT-style:一部の単語がマスクトークンに置き換えられた文章から、オリジナルの文章を予測
  • Deshuffling:単語がランダムにシャッフルされた文章から、オリジナルの文章を予測

Table4.png  結果をみると、BERT-styleが、翻訳タスクではPrefix language modelingと大差ないものの、全体で見ると一番優れていることが分かります。

Corruption Strategies
 上の結果から、BERT-styleが優れていることが分かりました。ここでは、そのBERT-styleをベースとして、下の例のような三種類のトークンの入れ方を比較します。
Table3_2.png

  • mask tokens:一部の単語がマスクトークンになっている文章から、全文を予測
  • replase tokens:一部の単語がマスクトークンになっている文章から、マスクされた部分のみを予測
  • drop tokens:一部分が抜かれた文章から、抜かれたところのみを予測

 また、下の表中のMASS-styleは、BERT-styleに似た方式のものだそうです。詳細はMASS: Masked Sequence to Sequence Pre-training for Language Generationをご覧ください。
Table5.png  結果を見てみると全体的に似たようなスコアを出していて、特別優れているものはありません。ただ、BERT-styleとMASS-styleは全文を予測する目的関数なのに対して、replace tokenとdrop tokenはマスクされた部分のみを予測するので、より短い時間で訓練可能となり、その点でいうと優れていると言えます。また単純に一番良いスコアを出した数でいうと、drop tokenを使用したものになりますが、SuperGLUEでは、replace tokenのほうが良い結果を出していて、この2つのことからreplace tokenを用いるのが一番良い手法だと結論付けています。

Corruption Rate
 次にマスクする割合を10%,15%,25%,50%と変えて比較します。
Table6.png  結果を見てみると、それぞれであまり大きな精度の違いは見られず、マスクする割合はモデルの性能に大きな影響を与えないということが分かります。ただし、50%に関しては特にGLUEとSGLUEで精度が悪くなっており、マスクする割合が大きすぎると、モデルの性能に悪影響を与えてしまうことが分かります。このことから、従来の研究に沿ってBERTでも採用されている15%が適切としています。

Corrupted Span Length
 次に、マスクするトークンの平均の長さを変えて比較します。
Table7.png

 結果を見ると、10の場合はあまり良くなっていないので、長すぎると性能が下がっていくことが分かります。ただ、それ以外は、あまり性能に差はなく、ベースラインのものをそのまま採用することにしています。

結論
 ここまで、目的関数に関する4つの項目について比較を見ました。結果的に上で示した図のフローになります。   この検証で一番モデルの性能に影響を与えたのは、最初に見た3つ目的関数の選択でした。性能だけを見ると他の要素はあまり重要でないように感じますが、マスクトークンの取り方で、予測する文・単語は短い方が学習にかかる時間が短く、計算コストも抑えられることも大切なポイントになります。


Dataset

 次に、使うデータセットを変えることでの性能の違いを見ていきます。まず、使用するデータセットについて、T5で重要となるC4について説明したいと思います。

C4(Colosal Clean Crawled Corpus)
C4はColosal Clean Crawled Corpusの略で、Webから取得できるCommon Crawlを元に以下の前処理したものです。

  • ピリオド、クエスチョンマーク、ビックリマーク、コーテーションマークで終わる文のみを使用する。
  • 汚い言葉、不適切な語を含むページは取り除く。
  • javascriptの単語を含む行は取り除く。
  • ダミーテキストがあるページは取り除く。
  • ソースコードなどは取り除く。
  • 重複文は取り除く。

C4の一番の特徴は、データのサイズがかなり大きいことです(データが多様)。Common Crawlが1か月に20TBになるようなデータで、ここでののC4は前処理をすることで、750GBにしたデータセットです。これは結果の表にも書いてありますが、他のデータセットに比べてかなり大きなデータとなっており、ここでの比較により、データサイズによりどんな違いが出るのかを見ることができます。

比較の結果は以下のようになります。
Table8.png

 まず、C4とC4,unfiltered(前処理をしていないC4データセット)を比べると、当たり前ですがC4のほうが精度が良く、前処理が重要であることが分かります。  次に、C4と他のデータセットを比べると、タスクによって精度の良いデータセットが違っているように感じます。まず分かることは、データのサイズが単純に大きいだけでは、必ずしも精度は上がらないことです。また、このような結果になった要因として、使ったデータセットがあるタスクに適応しやすいデータだったということが挙げられます。このことから、それぞれのタスクに対して適したデータセット、ドメイン固有のデータセットを使ったほうが精度が高くなることも確認できます。  また一般的にドメイン固有のデータセットのサイズは小さくなりますが、そういったサイズの小さいデータを繰り返し学習させるのと、より大きな多様性のあるデータを繰り返さずに学習させるのでは、どちらが良いのかを検証したのが、次の表とグラフです。
Table9.png

Figure6.png

 結果を見ると、データの大きさを小さくして、リピートの回数を増やすほど、Training lossの収束が速くなるものの、精度が低くなっていることが分かります。どんどん過学習になりやすくなっているということです。このことから、可能であればより大きなデータセットを使って、データに多様性を持たせたほうが良いことが確認できます。


Fine-tuning

 次に、fine-tuningの仕方について以下の3つを比較しています。

  • All parameters:モデルのすべてのパラメータをfine-tuningして調節する
  • Adapter layers:事前学習のモデルを壊さないで保持するために、Dense + ReLU + Denseの層からなるAdapter layerを各層の間に入れてfine-tuningする
  • Gradual unfreezing:徐々にチューニングするパラメータを増やして学習範囲を広げていく   結果は、下のようになり従来の手法の通り、全てのパラメータをfine-tuningしたほうが精度が高くなっています。
    Table10.png


Multi-task

 次に、Multi-taskという名の通り、まとめて一度に様々なタスクを学習させることにより、精度がどのように変化するかを検証していきます。つまり、普通はあるタスクに対してひとつの教師なし学習を行わせるのに対して、タスクをミックスさせることにより、全てのタスクで同じパラメータを共有するということになります。  ここで比較するのは、

  • Equal:全てのタスクを同じ割合で学習させる方法
  • Examples-proportional:各タスクのデータのサイズに上限Kを与えて、その中でランダム得たデータを学習させる方法
  • Tempereture-scaled:一番でデータの少ないタスクと一番データの多いタスクの差に制限をかけたデータで学習させる方法

Table11.png

 結果を見ると、タスクごとに教師なし学習により事前学習+fine-tuningしたのに比べて、全体的に性能は劣っていて、特にEqualのときが一番悪くなっていて、GLUE、SGLUEのタスクに関してはすべて良い性能が出ていません。ただし、Examples-proportionalとTempereture-scaledについては、パラメータをうまく調節できた場合に限って、CNNDM、SQuADなどで精度が良くなっていることが分かります。  次に、ここで分かった性能の差を埋めることができないかということで、multi-taskとfine-tuningの様々な組み合わせ方について検証していきます。 ここで比較する手法は、通常の事前学習+fine-tuningとMulti-task含めて5つあります。

  • Unsupervised pretrining + fine-tuning
    multi1.png

  • Multi-task
    multi2.png

  • Multi-task pre-training + fine-tuning:すべてのタスクを事前学習させる方法
    multi3.png

  • Leave-one-out multi-task training:1つのデータを除いたもので事前学習を行い除いたタスクでの性能を図る方法
    multi4.png

  • Supervised multi-task pre-training:教師あり学習のみを事前学習させる方法
    multi5.png

Table12.png  結果をみると、Multi-task pre-training + fine-tuningによって性能の差を埋めることは出来ました。また、Leave-one-out multi-task training性能がそこまで下がっていないことから、様々なタスクに対して事前学習+fine-tuningをすると新しいタスクに対して、ある程度対応できる可能性を示しています。そして、Supervised multi-task pre-trainingの結果を見ると、翻訳のタスクのみ良い性能を示していることから、教師なし学習による事前学習は、それ以外のタスクに重要な影響を与えていることが分かります。


Scaling

 最後に、学習ステップ数・バッチサイズ・モデルの大きさ・アンサンブルについて比較を行っています。
Table13.png  結果を見ると、ベースラインと比較して、それぞれ大きくすることで精度が良くなっていることが分かります。予想できた結果ですが、より大きなモデルを使って、学習もそれなりに行ったほうが良いことが確認できます。


実験のまとめ

 最後に、以上の実験から得た結果を反映したモデルの性能を、様々なベンチマークで検証した結果を見てみます。
Table14.png それぞれのモデルのパラメータ数はこちらです。

  • T5-Base : 220million
  • T5-small : 60million
  • T5-Large : 770million
  • T5-3B : 2.8billion
  • T5-11B : 11billion

 今までの検証からも予想できるように、パラメータ数の多いもののほうが性能が良くなっています。また、様々なベンチマークでSoTAを獲得できていることから、検証して得た結果は正しかったことが分かります。ただし、翻訳タスクに関しては、事前学習に英語のみのデータセットを使ったために、これまでのSoTAには届いていないようです。  それぞれのベンチマークによる考察は長くなるので省きますが、興味があれば是非論文を読んでみてください。


まとめ

 今回はT5の論文について紹介しました。T5モデルのポイントはText-to-Textの形式を採用したこととC4というかなり大きなデータセットを使ったことですが、NLP分野は研究が盛んで次々に新たなモデルが発表されているので、整理するという意味でも、こののように検証を行っていくことも今後重要になってくると思います。


参考文献

最近はGoogleを始めとする翻訳サービスにも機械学習が取り入れられ、翻訳精度が向上しています。

しかし、完璧な翻訳を求めるには精度が足りず、確認作業に時間がかかったり、翻訳されたものが正しいのか見極めるスキルが必要なケースがほとんどです。

このような課題がある中、高精度な翻訳ができる「DeepL」が、日本語と中国語の翻訳に新しく対応したので、日本語での翻訳機能を試してみました。


DeepLとは

DeepLはドイツのケルンで開発された深層学習(ディープラーニング)を用いた人工知能(AI)による翻訳システムです。以前から英語・オランダ語・フランス語・ドイツ語・イタリア語・ポーランド語・ポルトガル語・ロシア語・スペイン語などの言語に対応していましたが、2020年3月19日から日本語と中国語にも対応したと発表されました。

DeepL翻訳が日本語と中国語を習得
https://www.deepl.com/blog/20200319.html

2020年初めにDeepL翻訳はニューラルネットワークの大幅な改善を行い、日本語と中国語の翻訳精度が向上しました。日本語と中国語の翻訳精度を他翻訳と比較したグラフです。
DeepL


Google翻訳と性能比較

DeepLではどの程度の翻訳ができるのでしょうか。
AIができるコロナウイルス対策について紹介している文章の冒頭を翻訳しました。
Fighting the Spread of Coronavirus with Artificial Intelligence
https://tractica.omdia.com/artificial-intelligence/fighting-the-spread-of-coronavirus-with-artificial-intelligence/

DeepL

ほとんど完璧な翻訳です。Google翻訳と比較してみましょう。
前半はほぼ同様の訳のため割愛し、後半のHowever以下を掲載します。少し苦戦しているようです。
DeepL

Google翻訳では訳しきれていない部分があります。その他にも理解はできますが、やや首を傾げる箇所もあり、この文章ではDeepLの方がスムーズな翻訳ができていることが分かります。


機械学習、深層学習は学習のために翻訳前後をセットにして学習させ、精度を向上していきます。 今後も更に学習を進め、精度向上していくでしょう。


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

NCAAコンペ概要

 全米大学体育協会バスケットボールトーナメントの試合の勝敗を予測するコンペでした。男女別にコンペが開かれました。リーグ戦の試合結果の詳細とトーナメントの試合結果のデータが年ごとに与えられ、今年のトーナメントの試合結果を予測します。評価指標はLoglossでした。


結果

 新型コロナウイルスの影響で、大会自体がキャンセルになってしまいました。リークなしのLBの最も良いスコアは0.52586です。


取り組み内容

コンペの内容を理解してから

 LBの順位が参考にならないということを一番意識しなければならないと感じました。
というのも、LBは2015~2019年の予測のスコアなのですが、その答えはすでに与えられたデータに入っているため簡単にスコアを0にすることができ、リークに気を付けないと簡単に誤ったスコアが出てしまいます。
どのコンペでもCV(Cross Validation)は重要ですが、今回は特に必要でした。公開されているノートブックにもリークをしたものが多かったため、もしコンペが中止にならなければかなりのshakeが起きていたと思います。しかし、リークを防ぐことに関するディスカッションもいくつかあり、そこに注意できている参加者もいました。
また、過去数年間は毎年NCAAコンペは開かれているため、それらの上位成績者の解法を読み参考にしました。しかし、一貫した特徴はなく様々な解法であった為、自分なりの方法を考える必要がありました。


前処理 • 特徴量エンジニアリング

 基本的には、リーグ戦の合計得点などの試合の詳細データをチームごとに集計し、それを特徴量としました。
また、与えられているデータにチームのランキングなどもあったため、欠損値を補完しながら特徴量に加えました。Kenpom.comという、リーグとトーナメントのデータを集計しているサイトがあり、その独自の集計方法が特徴量として使えそうであったためスクレイピングをしたのですが、トーナメントのデータをリークしているためそのまま使うことはできず、自分でコーディングして作り直しました。
このうちのピタゴラス勝率やポゼッション率は比較的重要な特徴量となりました。また、各チームのプレースタイルの特徴を掴む為、チームごとのリーグ戦の試合内容に対してPCA分析をしてその結果を加えるといったこともしました。特徴量選択はまだしていませんでした。


CV(Cross Validation)

 交差検証の仕方についてですが、2014年までのデータ12年分を用い12FoldのGroupKFoldで行いました。去年、一昨年などの戦績を反映した特徴量を作っていないため、リークは起きていないと思います。年によってValidationのスコアが全然違うことから、やはりスポーツの予測は難しいなということを感じました。ジャイアントキリングの多く起こった年のスコアはかなり悪くなってしまいます。


モデリング

 LightGBM、CatBoost、DNNのアンサンブルです。シングルモデルのスコアにそれほどの違いはなかったのですが、出力の分布は大きく違い、アンサンブルをすることでスコアを向上することができました。僕の場合は、LightGBM、CatBoost、DNNの配分を、3:2:5にした場合が最もスコアが良くなりました。
XGBoostは他のモデルに比べスコアが良くなかったため使いませんでした。ハイパーパラメータチューニングはまだしていなかったため、これをするとさらに良くなったかもしれません。


まとめ

 コンペ自体がキャンセルになってしまい非常に残念です。しかし、今までしっかりできていなかった、シングルモデル作成→複数モデル作ってアンサンブル の流れに慣れてきたという点や、LBの順位が全く信用できない中でCVだけを頼りに取り組むという点においては学びが多く、良かったと感じています。来年はコロナウイルスの影響も収まりコンペがまた開かれると思うので、その時に今回の取り組みの成果を出したいです。

 


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

サービスへのお客様の評価はその場では気付きにくく、特にネガティブなものは直接伝えてはくれません。

しかしお客様の声を知ることはより満足してもらうためには必須です。
今回は口コミサイトに投稿されたレビューを分析し、お客様の本当の声を知るためのサービスを紹介します。

0.サービス紹介_口コミ分析

Web上で集めた口コミ(レビュー)をAIが精査し、ネガティブな口コミはネガティブな原因を特定します。

数が少ないうちは手作業で評価の精査が出来ますが、数が多くなってくるとそういう訳にもいきません。
大量の口コミを一から全て見ていくとなると大量の人手、時間がかかります。

そのコスト削減に向けて口コミ文章をAIに評価させネガティブなものだけを選別し、改善のための時間を創出しましょう。

前回のブログでosetiというライブラリを用いて口コミをネガポジ分析しましたが、これには問題点がありました。

  1. 日本語評価極性辞書に載っていない単語が出現した際、それらを定量的に評価できない。
  2. 極性辞書の単語の評価には作成者の感情が入っているので、客観的な評価が完璧にできない。
  3. 1文ごとにネガポジ判定を行うので、文章全体で見たときに文脈を読み取れない。


特に3.について、1つの口コミ全体で見た時にそれがかなりポジティブなものであっても、トータルで見た時に「かなり」の部分をAIは読み取れませんでした。 また、全体的にポジティブなレビュー(=口コミ) でも、一部ネガティブな内容が含まれているケースもあります。

このような "ポジティブ寄りのレビュー" 中に含まれるネガティブな内容について、このように仮定しています。
ポジティブ寄りのレビューにもネガティブな内容を含んでいるものがあり、「自店のネガティブなイメージを発見したい」という目標を達成するためには、これらも発見しなければなりません。 そこで、図のような関係があると仮定し、今回はネガティブ寄りのレビューを発見していくことにします。

ブログbert1.png


1.口コミの抽出

ランダムに抽出した東京都内にあるラーメン店で、約800件のレビューをGoogle口コミから抽出しました。一部です。
ブログbert2.png

このように約800件のレビューを抽出し、ポジティブは「1」ネガティブは「0」とラベリングを行いました。


2.そもそもBERTとは?

ではBERTにこれらのデータを学習させ、評価をしていきましょう。BERTについて簡単に説明をします。

BERT

BERT ( Bidirectional Encoder Representations from Transformers )は2018年にGoogleが発表したNLPモデルであり、その翌年には検索エンジンにも導入されました。この導入に対しては「過去最大のアップデート」と呼ばれ、メディアにも大きく取り上げられました。

このBERTの特徴はなんといっても「(文章における) 文脈を理解できる 」ことでしょう。

BERTが発表される以前のNLPモデルでは、「魚介じゃないラーメン」と「魚介 ラーメン」がほぼ同じように認識されていました。 これまでのモデルは「魚介」「じゃない」「ラーメン」と個別に理解できることは可能でしたが、「じゃない」という単語が「魚介」にかかっていることを認識できずに、結果文章の中で意味が明確な「魚介」「ラーメン」の二語が強調されてしまうことが原因でした。

しかしこのBERTでは、「魚介じゃないラーメン」のうち、「じゃない」という単語が「魚介」にかかるという文法上の構造を理解することができるのです。 このようなBERTの処理能力の高さは、今回例にした「魚介じゃないラーメン」のような短いワード以上に、「銀座駅で10分以内に魚介じゃないラーメンを食べたい」などの文章となった場合に、より力を発揮すると推測されます。
Googleが導入したBERTとは?誰でもわかるBERTの特徴を解説より引用

このBERTについての詳しいアルゴリズムについては、本記事のレベルを逸脱するため割愛します。(7.参考文献を参照)


3.口コミ分析の実装

BERTについては、こちらのサイトにあるBERT日本語Pretrainedモデルを用い、コーディングはBERTを用いたネガポジ分類機の作成を参考に実装しました。

この日本語Pretrainedモデルは、日本語全てのWikipediaの文章約1,800万文のテキストから、32,000語の語彙を事前学習したモデルです。

未知語抽出によるデータの前処理

この事前学習済みモデルはWikipediaの文章から単語を学習しているため、「おいしかった」「不快だった」などの感情を表す客観的な言葉が未知語として認識されています。そのため、未知語を抽出しvocab2というBERT語録辞書に入っている意味の似た単語と未知語を交換します。
例えば、「めっちゃ」を「とても」に、「不快だった」を「悪かった」などといったように変換していきます。

import re
replaced_pos = []
replaced_neg = []

for i in range(len(pos)):
  replaced_pos.append(re.sub(r'おいしい', 'うまい', pos[i]))

for i in range(len(neg)):
  replaced_neg.append(re.sub(r'不味い', 'ひどい', neg[i]))

for i in range(len(neg)):
  replaced_neg[i] = re.sub(r'おいしい', 'うまい', replaced_neg[i])

for i in range(len(pos)):
  replaced_pos[i] = re.sub(r'らーめん', 'ラーメン', replaced_pos[i])

for i in range(len(neg)):
  replaced_neg[i] = re.sub(r'らーめん', 'ラーメン', replaced_neg[i])

. . .

レビューの評価に影響するであろう「味」「接客態度」「店内の雰囲気(衛生面など)」の3つに注目し変換しました。

サンプルの文章による推論とAttentionの可視化

レビューの評価にあたりAIが注目した箇所を色付けします。
前述したPretrainedモデルの utils/predict.py に学習済みモデルのビルド( buildbertmodel )と推論( predict )のメソッドを定義してあるので、 これらを利用してサンプルの文章をインプットし、予測値(Positive or Negative)とAttentionを可視化します。

from utils2.config import *
from utils2.predict import predict, create_vocab_text, build_bert_model
from IPython.display import HTML, display

input_text = "サンプルの文章を入力"
net_trained = build_bert_model()
html_output = predict(input_text, net_trained)
print("======================推論結果の表示======================")
print(input_text)
display(HTML(html_output))

結果は以下のようになります。(例としてPositiveとNegativeを1つずつ)

attention.png ブログbert3.png

ここで[UNK]は未知語を、[CLS]は文の開始を表しています。また、Attentionの重み値が大きいほど背景色が濃い赤で表示されています。Attentionによって、AIは判定に必要であろう単語に"注目"していることが分かります。
Attentionについては参考文献でも紹介しています。


4.結果及び考察

最後にテストデータで実際に判定をします。 今回は全てのデータを「訓練データ:テストデータ=7:3」に分け、ハイパーパラメータ(バッチサイズ、エポック)は適宜変更して学習させていきます。

バッチサイズを8、16、32としそれぞれエポックを1~100まで設定した結果は以下のようになりました。

  • 正解率 : 全サンプルのうち、正解したサンプルの割合
  • 適合率(精度) : positiveと予測された中で、実際にpositiveであった確率
  • 再現率 : 実際にpositiveであるデータに対して、positiveと予測された確率
  • F値 : 適合率と再現率の調和平均

バッチサイズ8(1).png バッチサイズ16(1).png バッチサイズ32(1).png

全体として適合率は90%前後と高い結果です。かなりの高精度でネガティブなレビューを抽出できています。(ネガティブが抽出できるということはポジティブも自ずと抽出できています。)

バッチサイズが8の時は再現率に波があり、エポック数を増やしても再現率が40%を下回るときがありました。そこでバッチサイズを16に変更したところ、エポック数の増加に伴い再現率もかなり向上しました。 次にバッチサイズを32として学習を進めると、エポック数45までは再現率70%前後でその後急低下、再び上昇し最終的には90%近くまで上がるという興味深い結果が得られました。 一見するとバッチサイズ16の時が一番学習が上手くいくように見えますが、3つのバッチサイズでは全体的に最終的なF値は90%前後で頭打ちとなる結果となりました。


参考までに訓練データに対する損失と正解率も見てみましょう。

  • trainloss : 訓練データの損失(学習の時点でほぼ0)
  • trainacc : 訓練データの正解率(学習の時点でほぼ1)
  • valloss : バリデーションデータの損失(低い方が良い)
  • valacc : バリデーションデータの正解率(高い方が良い)

バッチサイズ8(2).png バッチサイズ16(4).png バッチサイズ32(2).png

trainlossとtrainaccについては、訓練データで学習を行っているので自明の結果といって良いでしょう。 バリデーションデータの損失の山になっている(急上昇している)ところでは、それに対応して再現率は下がっていることが分かります。学習がうまく出来ていないということです。

5.【参考】口コミのグルーピング

今回の目的であったネガティブな口コミの選定は達成できることがわかりました。次にネガティブなレビューの原因を特定しましょう。
「clerk(店員の態度)」「shop(店内の雰囲気)」「taste(味)」「noodle(麺)」「soup(スープ)」「ingredients(具材)」の6つ(重複あり)に分けて棒グラフにします。

ネガ口コミ.png

それぞれが原因のレビューの数です。味はもちろん、店員の態度も重要な要因なことが分かります。

EmbeddingProjectorを用いた可視化

EmbeddingProjectorを用いて可視化します。
知りたいカテゴリーを絞り込んでレビューを確認できるので、改善点をより発見しやすくなり、口コミを活かした業務改善が効率よく行えるでしょう。

EmbeddingProjectorは口コミの文章をword2vecによってベクトル化し、その後PCAによる次元削減を行い2or3次元空間上にマッピングすることによって可視化できます。 今回は「店員」「味」の2つを3次元空間に落とし込んでみます。まずはデータをロードし、下の画面右側のSearchのところで「店員」と検索します。

clerk3.png

「店員」と含まれている口コミが計88件あり、3次元空間上にきちんとマッピングできています。 その中で基準となる口コミを一つ選択し、下の画面右側のneighbor(近傍)を10などと設定すると、基準となる口コミから距離の近い(意味の近い)口コミが10個マーキングされます。

clerk.png

同じように「味」についてもみていきましょう。

taste.png

棒グラフでみた店員と味に絞って「何が悪いか」を更に深堀りしました。


6.総評

今回はBERTを用いて口コミ分析をしました。

前回の「単語」のみに着目した評価では、辞書に載っていない単語の評価が出来なかったり、文章全体の文脈を理解出来ていないという問題点がありましたが、BERTを用いることで、人間に近い判断基準をモデルに学習させることが出来、大幅に精度が向上しました。 口コミの中でネガティブな評価を選定し、グルーピングをすることで今まで発見できていなかった課題点も見つけやすくなります。

最後に、これから更に改善できる点です。

  • BERTのpretrainedモデルは日本語のwikipediaを対象に事前学習していたため、口コミ特有のワードへの対応が不十分であった
  • データの前処理の方法はさらに検討できる
  • 対象となるターゲットを広げたときに口コミの文章や書き方にバラツキがでるため、良い精度を出すためにさらに検討が必要


これらの課題を解決するにはSNSの事前学習モデルを使ったり、様々な工夫を考えて分類器を作成することが重要となってくるでしょう。 BERTでもかなりの精度を出せると思いますが、他の新しいNLPモデルも次々と発表されているので、これらを用いてまだ企業が利活用出来ていない口コミやSNSの書き込みなどを分析することで、顧客満足度向上に繋げられると思います。


7.参考文献

BERTとは何か?Googleが誇る最先端技術の仕組みを解説!
【論文】"Attention is all you need"の解説
BERTを用いたネガポジ分類機の作成
BERTを理解しながら自分のツイートを可視化してみるハンズオン


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

はじめに

このブログは前回、ドーナツの無人レジ化に向け機械学習をどのように用いるかを紹介しました。今回は、その中で出てきたドーナツ検出器の中身について紹介します。


目次

  • はじめに
  • 検出器を作るために必要なもの
  • どのような流れで作るか
  • 実際に作る
  • まとめ


必要なもの

ドーナツ検出器を作るために、ドーナツの画像データを訓練とテストを用意します。
今回は、「6種類のドーナツを検出し、合計金額を出す」ことが目標として、6種類のドーナツそれぞれの写真を50枚ずつ撮影しました。また、ドーナツが裏返っていても判断できるよう、裏返ったドーナツも同様に撮影しています(訓練データ)。テストデータは複数のドーナツが写る写真を撮りました。このドーナツの中には裏返ったものや写真からきれてしまっているドーナツも含んでいます。

スクリーンショット 2020-03-12 16.04.54.png


作成手順

ドーナツ検出器をどのような流れで作るかを紹介します。物体検出のためにYOLO3を用います。

 大まかな流れとして、このような作業をしています。

  1. 学習用の画像を整える、画像の水増し
  2. YOLO3でテスト用データのドーナツ一つ一つを検出する
  3. 学習モデルを用いてドーナツ名を予測する
  4. 合計金額などを表示する


1.ではYOLO3を用いて学習用の画像データからドーナツの部分だけを検出します。検出後、画像のアスペクト比(縦横比)を変えずにリサイズをします。画像は一つのドーナツに対して50枚しかないので回転や反転を用いて画像を水増しましょう。一つのドーナツに対して1500枚まで水増ししました。
3.では2.で検出したドーナツ画像から学習モデルを作成します。VGG16を用いて作成しました。


実際に作る

訓練データからドーナツの部分だけ検出する

スクリーンショット 2020-03-12 17.19.50.png

必要なライブラリをインポートします。

import logging
logging.disable(logging.WARNING)
import os
import glob

import numpy as np
from keras import backend as K
from keras.layers import Input

from PIL import Image

from yolo import YOLO
from PIL import Image
from yolo3.utils import letterbox_image

from matplotlib import pyplot as plt

keras-yolo3のYOLOクラスを継承してdetect_imageメソッドを修正します。

class CustomYOLO(YOLO):
  _defaults = {
        "model_path": 'model_data/yolo.h5',
        "anchors_path": 'model_data/yolo_anchors.txt',
        "classes_path": 'model_data/coco_classes.txt',
        "model_image_size" : (640, 640),
        "gpu_num" : 1,
    }

  def __init__(self, score=0.3, iou=0.45):
    self.__dict__.update(self._defaults)
    self.score = score
    self.iou = iou
    self.class_names = self._get_class()
    self.anchors = self._get_anchors()
    self.sess = K.get_session()
    self.boxes, self.scores, self.classes = self.generate()

  def detect_image(self, image):
        if self.model_image_size != (None, None):
            assert self.model_image_size[0]%32 == 0, 'Multiples of 32 required'
            assert self.model_image_size[1]%32 == 0, 'Multiples of 32 required'
            boxed_image = letterbox_image(image, tuple(reversed(self.model_image_size)))
        else:
            new_image_size = (image.width - (image.width % 32),
                              image.height - (image.height % 32))
            boxed_image = letterbox_image(image, new_image_size)
        image_data = np.array(boxed_image, dtype='float32')

        image_data /= 255.
        image_data = np.expand_dims(image_data, 0)  # Add batch dimension.

        out_boxes, out_scores, out_classes = self.sess.run(
            [self.boxes, self.scores, self.classes],
            feed_dict={
                self.yolo_model.input: image_data,
                self.input_image_shape: [image.size[1], image.size[0]],
                K.learning_phase(): 0
            })

        donut_out_boxes = []

        for i, c in reversed(list(enumerate(out_classes))):
          if self.class_names[c] == 'donut':
            box = out_boxes[i]
            top, left, bottom, right = box
            top = max(0, np.floor(top + 0.5).astype('int32') - 100)
            left = max(0, np.floor(left + 0.5).astype('int32') - 100)
            bottom = min(image.size[1], np.floor(bottom + 0.5).astype('int32') + 100)
            right = min(image.size[0], np.floor(right + 0.5).astype('int32') + 100)
            donut_out_boxes.append([left, top, right, bottom])

        return donut_out_boxes

YOLOで訓練画像からドーナツ部分を切り取って画像を保存します。

def crop_donuts(front_or_back):
  for dir_name in label_dict.keys():
    print(dir_name)
    output_path = '../data/cropped_donut_images/' + front_or_back + '/' + dir_name

    if not os.path.exists(output_path):
      os.makedirs(output_path)

    for file_path in glob.glob('../data/donut_images/' + front_or_back + '/' + dir_name + '/*.jpg'):
      file_name = file_path[-12:-4] 
      image = Image.open(file_path)
      out_boxes = yolo.detect_image(image.copy())

      if len(out_boxes) == 1:
        cropped_img = image.crop(out_boxes[0])
        cropped_img.save(output_path + '/' + str(file_name) + '.jpg')

訓練画像内にはドーナツが1つしかないはずなので、2つ以上ドーナツを検出した場合は保存せずその画像はスキップします。保存した画像を確認し、検出したがうまく切り取れていない画像はフォルダから削除してください。

YOLOでの検出がうまくいかなかった画像、検出したが切り出しがうまくいかなかった画像を抽出します。

def copy_undetected_images(front_or_back):
  for dir_name in label_dict.keys():
    print(dir_name)
    output_path = '../data/not_detected_images/' + front_or_back + '/' + dir_name

    if not os.path.exists(output_path):
      os.makedirs(output_path)

    for file_path in glob.glob('../data/donut_images/' + front_or_back + '/' + dir_name + '/*.jpg'):
      file_name = file_path[-12:-4] 
      path = '../data/cropped_donut_images/' + front_or_back + '/' + dir_name + '/' + file_name + '.jpg'

      if not os.path.exists(path):
        image = Image.open(file_path)
        image.save(output_path + '/' + str(file_name) + '.jpg')

ここで、抽出されたものは、手動でドーナツ部分を切り取りました。

1つ前の関数で抽出した画像をcroppeddonutimagesフォルダ下の適切なフォルダにコピーします。

def copy_images_cropped_by_hands(front_or_back):
  for dir_name in label_dict.keys():
    print(dir_name)
    output_path = '../data/cropped_donut_images/' + front_or_back + '/' + dir_name

    if not os.path.exists(output_path):
      os.makedirs(output_path)

    for file_path in glob.glob('../data/images_cropped_by_hands/' + front_or_back + '/' + dir_name + '/*.jpg'):
      file_name = file_path[-12:-4] 
      path = output_path + '/' + file_name + '.jpg'

      if not os.path.exists(path):
        image = Image.open(file_path)
        image.save(path)

ドーナツの画像分類のモデル作成

スクリーンショット 2020-03-12 17.25.30.png

アスペクト比を変えずに設定した大きさに画像をリサイズします。

def resize(img, base_w, base_h):
  base_ratio = base_w / base_h
  img_h, img_w = img.shape[:2]
  img_ratio = img_w / img_h

  white_img = np.zeros((base_h, base_w, 3), np.uint8)
  white_img[:, :] = [220, 220, 220]

  if img_ratio > base_ratio:
    h = int(base_w / img_ratio)
    w = base_w
    resize_img = cv2.resize(img, (w, h))
  else:
    h = base_h
    w = int(base_h * img_ratio)
    resize_img = cv2.resize(img, (w, h))

  white_img[int(base_h/2-h/2):int(base_h/2+h/2),int(base_w/2-w/2):int(base_w/2+w/2)] = resize_img
  resize_img = white_img

  return resize_img

正解ラベルのエンコード用に辞書を定義します。

label_dict = {
    'エンゼルクリーム': 0,
    'エンゼルフレンチ': 1,
    'オールドファッションハニー': 2,
    'ダブルチョコレート': 3,
    'チョコファッション': 4,
    'ポンデリング': 5,
}

データセットを作成します。

front_path = './drive/My Drive/donuts_detection/data/cropped_donut_images/front/'
back_path = './drive/My Drive/donuts_detection/data/cropped_donut_images/back/'

IMG_SIZE = 256
X = []
y = []

print("== DONUTS FRONT ==")
for dir_name in label_dict.keys():
  print(dir_name)
  label = label_dict[str(dir_name)]

  X_tmp = []
  y_tmp = []

  for file_path in glob.glob(front_path + dir_name + '/*.jpg'):
    img = image.load_img(file_path)
    # img = rotate(img)
    img = image.img_to_array(img)
    img = resize(img, IMG_SIZE, IMG_SIZE)

    X_tmp.append(img)
    y_tmp.append(label)

  for x in datagen.flow(np.array(X_tmp), batch_size=1):
    new_image = x[0]

    X_tmp.append(new_image)
    y_tmp.append(label)

    if len(X_tmp) % 1000 == 0:
      break

  X.extend(X_tmp)
  y.extend(y_tmp)

del X_tmp, y_tmp

print("== DONUTS BACK ==")
for dir_name in label_dict.keys():
  print(dir_name)
  label = label_dict[str(dir_name)]

  X_tmp = []
  y_tmp = []

  for file_path in glob.glob(back_path + dir_name + '/*.jpg'):
    img = image.load_img(file_path)
    # img = rotate(img)
    img = image.img_to_array(img)
    img = resize(img, IMG_SIZE, IMG_SIZE)

    X_tmp.append(img)
    y_tmp.append(label)

  for x in datagen.flow(np.array(X_tmp), batch_size=1):
    new_image = x[0]

    X_tmp.append(new_image)
    y_tmp.append(label)

    if len(X_tmp) % 1000 == 0:
      break

  X.extend(X_tmp)
  y.extend(y_tmp)

X = np.array(X, dtype='float32')
X /= 255.0
y = np.array(y)
y = np_utils.to_categorical(y, len(label_dict))

データの水増しを行いながらデータセットを作成します。 ドーナツの表面だけ使用するなら、それぞれのラベルにつき1500枚までメモリ不足にならずに水増しできました。表面・裏面とも使用する場合はメモリ容量の関係で各ラベル1000枚まで水増ししました。

モデルを作成します。

def create_model():
  base_model=VGG16(weights='imagenet',include_top=False, input_tensor=Input(shape=(IMG_SIZE, IMG_SIZE, 3)))
  x = base_model.output
  x = GlobalAveragePooling2D()(x)
  x = Dense(1024, activation='relu')(x)
  prediction = Dense(len(label_dict), activation='softmax')(x)

  model=Model(inputs=base_model.input, outputs=prediction)

  for layer in base_model.layers:
      layer.trainable = False

  model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['acc'])

  return model

VGG16の事前学習モデルを利用しました。モデルにVGG16の全結合層は含まず、分類用に独自に全結合層を追加しています。また、VGG16の層は学習しないようにしています。

モデルを学習して保存します。

EPOCHS = 20
BATCH_SIZE = 64
MODEL_PATH = './drive/My Drive/donuts_detection/models/donuts_front_and_back_cropped_model_2.h5'

X_train, X_valid, y_train, y_valid = train_test_split(X, y, test_size=0.30)

model = create_model()

history = model.fit(
    X_train, y_train,
    validation_data=[X_valid, y_valid],
    epochs=EPOCHS,
    batch_size=BATCH_SIZE
)

model.save(MODEL_PATH)

エポック数とバッチサイズは適切なものを選んで下さい。

画像内のドーナツをすべて検出し分類する

スクリーンショット 2020-03-12 17.30.46.png

必要な辞書を定義します。

label_dict = {

    'エンゼルクリーム': 0,
    'エンゼルフレンチ': 1,
    'オールドファッションハニー': 2,
    'ダブルチョコレート': 3,
    'チョコファッション': 4,
    'ポンデリング': 5,
}

inverse_dict = dict([(v, k) for k, v in label_dict.items()])

'''
{
   商品名: [値段, 熱量(kcal), たんぱく質(g), 脂質(g), 炭水化物(g), 食塩相当量(g)]
}
'''
menu_dict = {
    'エンゼルクリーム': [120, 219, 1.3, 12.0, 26.1, 0.6],
    'エンゼルフレンチ': [130, 200, 2.0, 15.5, 12.6, 0.3],
    'オールドファッションハニー': [110, 355, 3.4, 18.0, 44.4, 0.8],
    'ダブルチョコレート': [130, 265, 3.6, 16.8, 24.3, 0.5],
    'チョコファッション': [130, 330, 3.7, 20.8, 31.3, 0.8],
    'ポンデリング': [110, 219, 1.3, 12.0, 26.1, 0.6],
}

YOLOで取得したバウンディングボックスを元に画像からドーナツ部分をトリミングして分類します。

image = Image.open("../data/donut_images/multiple_front_and_back/IMG_2367.jpg")
r_image, out_boxes = yolo.detect_image(image.copy())

plt.figure(figsize=(6, 8))
plt.imshow(r_image)

X = []
for box in out_boxes:
  cropped_img = image.crop(box)
  cropped_img = np.asarray(cropped_img)
  cropped_img = resize(cropped_img, IMG_SIZE, IMG_SIZE)
  X.append(cropped_img)

X = np.array(X, dtype='float32')
X /= 255.0

preds = model.predict(X)

pred_labels = [inverse_dict[pred] for pred in np.argmax(preds, axis=1)]
result = np.zeros(6)

for label in pred_labels:
  result += np.array(menu_dict[label])

print()
print(pred_labels)

print()
print(f"値段:         {int(result[0])}")
print(f"熱量(kcal):   {result[1]:.1f}")
print(f"たんぱく質(g): {result[2]:.1f}")
print(f"脂質(g):      {result[3]:.1f}")
print(f"炭水化物(g):   {result[4]:.1f}")
print(f"食塩相当量(g): {result[5]:.1f}")
print()
image

まとめ

最後にどれだけ正確に判定できているか、分類のパフォーマンスを計算してみましょう。

def extract_labels(row):
  labels = []
  for key in label_dict.keys():
    if row[key] == '1':
      labels.append(key)
  return labels

df = pd.read_csv('../multiple_front_and_back.csv')
df['labels'] = df.apply(extract_labels, axis=1)

donuts_num = 0
detected_donuts_num = 0
corrected_classified_donuts_num = 0

print('== INCORRECTLY DETECTED ==')

for file_path in glob.glob('../data/donut_images/multiple_front_and_back/*.jpg'):
  file_name = file_path[-12:-4]
  correct_labels = df[df['file_name'] == file_name]['labels'].values[0]
  donuts_num += len(correct_labels)

  image = Image.open(file_path)
  r_image, out_boxes = yolo.detect_image(image.copy())

  X = []
  for box in out_boxes:
    cropped_img = image.crop(box)
    cropped_img = np.asarray(cropped_img)
    cropped_img = resize(cropped_img, IMG_SIZE, IMG_SIZE)
    X.append(cropped_img)

  X = np.array(X, dtype='float32')
  X /= 255.0

  preds = model.predict(X)
  pred_labels = [inverse_dict[pred] for pred in np.argmax(preds, axis=1)]

  if len(pred_labels) != len(correct_labels):
    print(file_name)

  for label in pred_labels:
    if label in correct_labels:
      corrected_classified_donuts_num += 1
      correct_labels.remove(label)

  detected_donuts_num += len(pred_labels)

print('==========================')

print()
print('The number of all donuts: {:d}'.format(donuts_num))
print('The number of donuts detected by YOLO: {:d}'.format(detected_donuts_num))
print('Percentage of donuts detected by YOLO: {:.2f}'.format(detected_donuts_num / donuts_num))
print('Percentage of donuts classified correctly out of all dontuts: {:.2f}'.format(corrected_classified_donuts_num / donuts_num))
print('Percentage of donuts classified correctly out of detected dontuts: {:.2f}'.format(corrected_classified_donuts_num / detected_donuts_num))

出力結果は以下のようになりました。

== INCORRECTLY DETECTED ==
IMG_2331
IMG_2344
IMG_2357
IMG_2356
==========================

The number of all donuts: 195
The number of donuts detected by YOLO: 195
Percentage of donuts detected by YOLO: 1.00
Percentage of donuts classified correctly out of all dontuts: 0.96
Percentage of donuts classified correctly out of detected dontuts: 0.96

つまり、ドーナツはYOLOによって100%検出され、すべてのドーナツ(検出されたドーナツ)を96%の割合で正しく分類できています。
このシステムにより、今まで手動で行っていたドーナツのレジ計算を自動で行うことができます。

もちろん、ドーナツだけではなく、お弁当詰めにミスがないかチェックしたり、工場の生産ラインで不良品を弾くこともできます。


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

世界の人口推移と起こりうる問題

日本だけでは人口減少と言われていますが、世界を見ると人工は増加すると予測されています。 2019年に国連が発表した、世界人口推計2019年版 データブックレットによると2050年に97億人、2100年には110億人まで増加すると言われています。
5902c9be50a0cb27b5b66f0daaecf766.png

人口増加分の8割ほどをアフリカが占めています。
各国はアフリカが巨大なマーケットになると予測して、国を挙げて進出を加速しています。中国はこれまでに6900億ドル以上をアフリカに投資しており、日本では2016年8月にケニアで開催された第6回アフリカ開発会議(TICAD)にて、アフリカに300億ドル単位の投資を行うと発表しています。

アフリカを始めとした各国で人口増加すると問題に挙がるのが「食糧問題」です。 日本以外にも北米の各国など、人口減少すると予測されている国もあり、今まで以上に効率的な食料供給が求められます。


各国の農業

2012年の農作物輸出額は1,449億ドルと世界トップのアメリカは、広大な土地を活かし、大型の農業機械で少ない種類の作物を最適な環境で作る大規模農業を採用し、高い生産性を保っています。

スクリーンショット 2020-03-12 14.29.23.png

2位のオランダは、決して広くはない農地で効率の良い生産で高い輸出高を保っています。 最新の情報通信技術(ICT)や環境制御技術を駆使した「スマート・アグリ」を導入し世界トップレベルの土地生産性を保っています。


日本の農業

日本はかねてから集約農業を採用していますが、コストがかかり農作物単価は高くなるため世界市場では戦いにくい状態です。近年の海外における日本食ブームや 2013年に「和食」がユネスコ無形文化遺産に登録されたことを受けて、コストを下げた大量生産をおこなうために日本でも大規模農業を進める動きがあります。

また、日本では農業に従事する人が増えています。 個人数は減少していますが、法人化して農業を行う経営体が急増(2000年:5,272経営体→2017年:21,800経営体)しており、"サラリーマン"として農業が出来る働き先が増えています。

2009年の農地法改正で農業参入が全面自由化になり、企業の農業参入も増加(2000年:5,272経営体→2017年:21,800経営体)したことで、増加に拍車をかけています。

農家数が減少はしたものの若い農業従事者が増え、企業での大規模農業の採用...。
日本でもデータに基づく効率的な農業をおこなうことでより高い生産性を実現できそうです。


機械学習を農業に活用するサービス

農場をデジタル管理する Climate社 FieldView

最高の生産性を実現するために、天候情報、窒素量、畑の健康状態などを監視してダッシュボード化し、収量目標に合わせた作付けプランを提供します。 広大な農地でも、現在の状況がダッシュボード化されるので管理しやすく、意思決定を助けます。農業でSFAを活用しているかのようです。

スクリーンショット 2020-03-11 16.54.12.png

実際の画面です。
左から、マップを並べて収穫量等を比較している画面、現状の良し悪しを判断し、適切な意思決定をするための収集分析、更に詳細な分析のために領域ごとに土地の価値を判定する画面、です。

その他にも機械学習を活用してトウモロコシの病害診断をおこなったり、それぞれの農地で何が成功して何が失敗したかを科学的に分析するための収益分析をおこないます。


航空映像から生育マップを作成する Mavrx社 Taranis

ドローン等で撮影した空中映像を、葉に乗っているカブトムシを数えられるくらいの高解像度の画像処理をして衛生画像と組み合わせた生育マップを作成します。

jared-brashier-duNHkmSkW6M-unsplash (1).jpg

撮影された写真は植物の健康と品質を評価するディープラーニングエンジンによって分析されます。ディープラーニングエンジンは、最先端の数学モデルとクラウド上のハードウェアプラットフォームを使用し、60人以上の専門の農学者によって訓練され、1,000,000を超える作物の健康問題の例を提供しています。

例えば、雑草を早期発見し適切な除草剤の作成を支援したり、作物の生育不良を特定して対処方法をユーザーに知らせるなど、適切な対処のための指南をしてくれます。


今後の方針

上記事例のように、従来の集約農業から大規模農業に変化するための仕組みが提供されはじめました。 また、農業へのノウハウがなくとも高品質な生産ができるようにデータの可視化だけではなく、対処法まで指南してくれるようになり始めています。 2050年までの人口増加へ準備が必要な段階になっています。日本では労働人口が減っていますが、生産やサービスの需要は減りません。いかに効率よく生産できるか、生産性を上げられるかがこれからの農業のカギになるでしょう。

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


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

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

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

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

  • バリュー(行動規範)

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


会社概要はこちら


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


参考

世界人口推計2019年版 データブックレット

農林水産省「平成29年一般企業の農業への参入状況」

概要

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

パーソナルカラーとは、個人の生まれ持った素材(髪、瞳、肌など)と雰囲気が合う色のことです。人によって似合う色はそれぞれ異なります。 パーソナルカラー診断では、個人を大きく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で定期的に情報発信しています!

「思っていたよりいい!」
購入した商品を使ったとき、こう思えると嬉しいですね。

重要なことは『思っていたより』という点で、私たち提供者は常にお客様の期待を超えていかねばなりません。
驚きと喜びが両立して初めて感動が生まれ、リピーター、そしてファンになってもらえるのだと思います。

そんな『いい!』と思ってもらうための重要な要素である品質。
今回はAI・機械学習を活用して品質を向上し、高品質を保つ事例をご紹介します。


スクリーンショット 2020-03-05 17.07.21.png

品​​質向上のための機械学習活用

機械学習が、機器を適切に動作させ、材料または工程順序の変更が製造内容に影響しないようにすることで、製造者がより良い製造工程と品質を保つことを手助けします。

まず、製造機器に取り付けられたセンサーで、信号を収集、監視し、機械の動作データと温度、相対湿度、その他の環境データなどの周囲のセンサーデータの両方を追跡できます。
次に、機械学習を使用して、この過去データとリアルタイムプロセスデータを比較します。品質に影響する条件が検出された場合、製造完了前に修正が可能です。

1日あたり5,000万回以上の溶接をする、ある自動車メーカーでは、低品質な溶接に気付き一定の品質を保つためにシステムが活用されています。
品質の低い溶接を発見した際には、溶接をやり直し、部品を完全に廃棄し、部品をキューに戻す時間のロスと金額のコストを削減しており、
各溶接ロボットからは、トルク、電流、モーターの1分あたりの回転数、アーク時間、および供給されたワイヤの太さを測定する信号を含む15の信号が収集されています。
同社は既知の高品質な溶接の信号をロボットからリアルタイムで取得するデータ信号と比較することで、溶接品質の良し悪しをリアルタイムで検出できます。

機械学習は、新しい異常を検出するためにも使用されます。異常はシステムにフィードバックされ、アルゴリズムを改良および改善します。
最終結果として、不良溶接の検出率は98%になり、再溶接が必要な溶接数の5倍減少しました。手動ではできなかった検知・分析によりコストと時間の両方が大幅に節約されています。


機械学習による品質基準チェック

品質基準を満たしていることを保証するためにもAI・機械学習は活用されています。

通常、製品の目視検査は、生産中または完成後に行われます。以前は人間が検査していましたがやはり疲労や速度に限界がありました。
また、人間には健康状態(アレルギー、病気、ストレスによる視力低下、白内障、黄斑変性、円錐角膜などの検出されない状態など)の悪い時があり、それが目視検査に影響を与える可能性があります。
マシンビジョンと機械学習を活用することで、機械と人間が適切に役割分担をし、製品検査工程の効率、精度、再現性の向上が実現します。

Landing.AIは、Google Brainの開発を率いた著名な機械学習エキスパートであるAndrew Ngによって設立された新しい会社です。
マシンビジョン、オブジェクト識別、および機械学習を組み込み、AIを業務の工程に統合できるようにしています。
以前の目視検査システムは、全ての欠陥を確実に認識するために約100万枚の画像データでトレーニングする必要がありました。新たなLanding.AIのシステムは5つの製品画像のみをレビューした後に欠陥のパターンを認識することができます。Landing.AIのディープラーニングは部品の検査に0.5秒で済み、多くの場合人間よりも正確だと言われています。

機械学習での画像認識に必要なデータ量と時間を削減するもう1つの例に富士通研究所の事例があります。
今までは、製造ラインや部品を変更する場合、検査システムも改訂して、画像データのデータベースが最新であることを確認する必要がありましたが、同社の既存の画像認識システムプログラムの目標と目的は事前に設定されていましたが、新しい製造ラインを開始する際にに使用できる画像データの量は限られていました。
富士通は、システムをテンプレート化して画像認識アプリケーションを構成する、学習プロセスを高速化する専門の遺伝的プログラミングに注目しました。たとえば、テンプレートを使用すると、学習と認識のプロセスを3つのプロセス(画像処理、しきい値処理、バイナリ画像処理)に絞り込むことができます。プログラムは正常な部品と欠陥のある部品の画像からトレーニングデータを準備して合格/不合格の判断を下すことにより、自動的に学習し、進化します。
このシステムを部品の組立ラインでテストすると、検査用のコードを自動的に生成しほぼ100%の認識率を達成しました。

その結果、プログラムを開発する時間が80%短縮され、調整が行われても97%以上の認識率を維持しながら、同社の部品組立機が再学習できるようになりました。


スクリーンショット 2020-03-05 17.07.02.png

機械学習でファン化を進める?

お客様は多くの情報を持ち、膨大な選択肢から購入の意思決定をます。また、一度購入したから安心できるわけでありません。買い替え(リプレイス)の決断はとても早く、容赦なくやってきます。

しかし、ファンになってしまえばどうでしょうか?
一度ファンになると簡単には離れません。それどころか、他の製品も好きになってもらえるでしょう。

品質はそんなファンを作る大きな要因です。

まだまだ機械学習の活用は発展段階ですが、品質向上、維持のためのAI活用は順調に進んでおり、2025年までに4億1,820万ドルの年間収益を生み出し、アジア太平洋(1億3,380万ドル)と北米(1億1,710万ドル)が市場を牽引すると予測されています。

品質向上、維持のために当社でもAI・機械学習を活用したソリューションをご提案します。


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

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

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

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

  • バリュー(行動規範)

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


会社概要はこちら


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

エネルギー分野における課題

エネルギーの供給と消費は金融および社会環境コストの観点から企業・国民のリスクの1つであるため、現在は再生可能エネルギーへの関心が高まっています。 ブルームバーグ・ニュー・エナジー・ファイナンスのニューヨークに拠点を置くアナリスト、カイル・ハリソンは以下のように話しています。

「100%再生可能エネルギーを使用すると宣言している企業もある。そのような企業の増加により、政府の補助金に関係なく、再生可能エネルギーの活用検討が促進される。」


再生可能エネルギー分野におけるAI・機械学習活用可能性は、気象予測の改善だけではなくエネルギー消費要因全般にも関連しています。 今回は海外事例から再生可能エネルギー分野のさらなる可能性を紹介します。


スクリーンショット 2020-03-05 14.50.37.png

機械学習を活用した事例 5選

再生可能エネルギー需要量予測

特定の日においてエネルギー需要がどれだけ発生するか予測します。 過去の時間ごとのエネルギー消費量データを機械学習に学習させて予測モデルを作成します。 正確なエネルギー需要と消費を予測することで、施設や建物の管理者、エネルギー会社、公益事業会社が省エネポリシーを展開するために使用できます。企業では予測データを活用し、運用とエネルギー貯蔵システムを最適化する方法を計画立てることができます。 また、顧客ごとに必要エネルギー量を予測し、最適な契約容量を提案することで、エネルギー費用のコストを最小限に抑えることができます。


再生可能エネルギーの供給量予測

ドイツでは既に再生可能エネルギー分野において機械学習が活用されています。 たとえば、エネルギー量の不足を事前に察知するために、国内の風力タービンとソーラーパネルのリアルタイムデータを分析して用意できるエネルギー量を予測します。2日間分のエネルギーが用意できるかどうかを基準として不足可能性があるとアラートで知らせます。

また、IBMでは再生可能エネルギーのために天気パターンを予測支援できる機械学習のシステムを独自に開発しました。 Self-Learning Weather ModelおよびRenewable Energy Forecasting Technology(SMT)と名付けられ、1,600の気象観測所、太陽光発電所、風力発電所、および気象衛星から取得したデータを分析します。SMTの天気予報は、National Weather Serviceがまとめたものよりも最大30%正確です。

上記の2つの事例は再生可能エネルギーの収集量を予測するために役立っています。


再生可能エネルギーの最適価格決定

需要と供給が予測できるようになったことで、エネルギー価格の最適値を予測することができます。従来の価格設定システムに比べていくつかの利点があります。

  • ニューラルネットワークは人では管理できない膨大な量のデータを分析できる
  • エネルギーの供給と需要の間の非線形相関を学習して、直感に反してデータに基づいた設定価格を推奨できる
  • ビッグデータ主導の価格戦略は可視化できる
  • 価格管理者の業務時間を短縮し、高レベルの意思決定のみが業務として残すことができる


スクリーンショット 2020-03-05 14.49.59.png

風速変動予測による風力発電地の検討

マサチューセッツ工科大学(MIT)の研究者は、一定期間の風速の変動をより迅速に予測できるシステムを開発しました。 このシステムにより、公益事業会社や再生可能エネルギーの新興企業は、風力発電所の候補地をより早く見つけられます。 候補地発見のためのデータ収集は電力会社がおこなうと、最大で12か月程かかりますが、MITが開発したような機械学習を活用したシステムでは、わずか3か月のデータに基づいてモデルを作成します。しかも従来のデータ収集よりも正確に予測ができます。


エネルギー消費量を利用した機器異常の検出

機械学習でエネルギー消費を常に監視、分析し、異常を検出します。 電力が消費されている中でどこでどのくらい電気が使用されているかを確認するのは難しく、機器の誤動作を検出することも難しいです。システムに障害が発生したり、構成が誤っていると、火災などの悪影響が生じる可能性があり、このシステムは大きな経済的損失を防ぎます。

人工知能アルゴリズムの開発により、スーパーマーケット、高校などを自動的に定義し、エネルギー消費に基づいて異常をリアルタイムで検出および分類できます。このようなソリューションは、リアルタイムでエネルギー消費の異常を自動的に検出し、非常に迅速に意思決定できるユーザーに通知します。大きな経済的損失を避けるのに役立ちます。


今後のエネルギーの移り変わり

最近まで再生可能エネルギーは予測不能であったこともあり、現時点では世界のエネルギー需要を満たすために化石燃料は必要不可欠です。 AIも化石燃料の探査・生産コストを削減し、配送を合理化することで化石燃料の供給を支援しています。

AIは、再生可能エネルギーと同じくらい化石燃料を強化できますが、化石燃料の持続不可能性が考慮すると、長期的に見た理想的なシナリオではありません。
最終的には、再生可能エネルギーが化石燃料に取って代わると予想され、AI・機械学習の活用は再生可能エネルギーの普及を助けるために役立っていくでしょう。


当社でもAI・機械学習を活用した課題解決をご提案しております。


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

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

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

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

  • バリュー(行動規範)

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


会社概要はこちら


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

このアーカイブについて

このページには、2020年3月に書かれた記事が新しい順に公開されています。

前のアーカイブは2020年2月です。

次のアーカイブは2020年4月です。

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