kivantium活動日記

プログラムを使っていろいろやります

二次元画像判別器に対するActive Learning導入の検討

前回の記事では、Twitter上の画像から二次元画像を選ぼうとすると二次元とも三次元とも言い難い画像が入ってくる問題があることを見ました。今回は、Active Learningという方法を使って境界領域の画像をうまく扱う方法を適用したいと思います。

Active Learningについて

Active Learningという言葉は教育業界と機械学習業界の両方で使われているので混乱がありますが、ここでは機械学習でのActive learningを指します。通常の機械学習の問題設定では学習データは既に与えられたものとして扱うことが多いですが、Active Leaningではどのデータを学習するかを選ぶことができるという設定のもとで学習を行います。これにより、少ないデータ数で学習が行えるようになることが期待できます。

f:id:kivantium:20200418181517p:plain:w600
Active Learningでは、境界に近いデータを能動的に選ぶことで効率的に学習を行うことを目指す。
ICML 2019のActive Learningチュートリアルのスライドより。)

以下、Active Learning Literature Surveyの内容に沿って話を進めます。

Active Learningの主なシナリオには3つあります。

  • Membership Query Synthesis: 学習器が入力空間中の任意のラベルなしインスタンスについてラベル付けを要求できる(新しく生成したインスタンスでも良い)
  • Stream-Based Selective Sampling: 1つずつ流れてくるデータそれぞれについてラベルを要求するか破棄するかを決める
  • Pool-Based Sampling: ラベル付きデータとラベルなしデータが与えられ、ラベルなしデータの中からどのデータにラベル付けを要求するか決める

どのデータに対してラベルを要求するかを決定する基準として最もよく使われているのがUncertainty Samplingという方式で、主なものが3種類あります。

  • least confident: 一番確信度が低いものを選ぶ。数式で書くと、 1 − P(\hat{y}|x) が最大のものを選ぶ(\hat{y} = \mathrm{arg max}_y P(y|x))。
  • margin sampling: 一番可能性が高いクラスと二番目に可能性が高いクラスの分類確率の差が一番小さいものを選ぶ。数式で書くと、 P(\hat{y}_1|x) − P(\hat{y}_2|x) が最小のものを選ぶ。
  • entropy: エントロピーが最大のものを選ぶ。数式で書くと、 -\sum_{i} P(y_i|x) \log{P(y_i|x)}が最大のものを選ぶ。

二次元画像判別に対する応用

今回は、ラベル付けを行った画像とラベルがついていない画像が与えられているのでPool-Based Samplingのシナリオになります。とりあえず一番簡単そうなmargin samplingを使って、昨日ラベル付けをサボったデータに対してActive Learningをやってみようと思ったのですが、1個ずつラベル付けするのは面倒なので、分類確率の差が0.3より小さいデータがどんな感じのデータになるのかを見てみることにします。

import os
import shutil

import numpy as np
from PIL import Image
import more_itertools
from sklearn.svm import SVC
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from tqdm import tqdm

import i2v

illust2vec = i2v.make_i2v_with_onnx("illust2vec_ver200.onnx")

# 学習データの準備
X = []
y = []
batch_size = 4

negative_path = '0'
negative_list = os.listdir(negative_path)
for batch in tqdm(list(more_itertools.chunked(negative_list, batch_size))):
    img_list = [Image.open(os.path.join(negative_path, f)) for f in batch]
    features = illust2vec.extract_feature(img_list)
    X.extend(features)
    y.extend([0] * len(batch))

positive_path = '1'
positive_list = os.listdir(positive_path)
for batch in tqdm(list(more_itertools.chunked(positive_list, batch_size))):
    img_list = [Image.open(os.path.join(positive_path, f)) for f in batch]
    features = illust2vec.extract_feature(img_list)
    X.extend(features)
    y.extend([1] * len(batch))

# Random Forestの学習
clf = RandomForestClassifier(max_depth=2, random_state=0)
clf.fit(X, y)

# Unlabeled データをフォルダ分けする
pool_path = 'unlabeled'
pool_list = os.listdir(pool_path)
for filename in pool_list:
    filename = os.path.join(pool_path, filename)
    img = Image.open(filename)
    feature = illust2vec.extract_feature([img])
    prob = clf.predict_proba(feature)[0]
    # 確率値の差が0.3以下ならラベル付けを要求する
    if np.abs(prob[0]-prob[1]) < 0.3:
        shutil.move(filename, 'uncertain')
    elif prob[0] > prob[1]:
        shutil.move(filename, 'negative')
    else:
        shutil.move(filename, 'positive')

Unlabeledデータ2021枚のうち、uncertainに分類されたものが193枚、negativeに分類されたものが1783枚、positiveに分類されたものが52枚でした。

f:id:kivantium:20200418191154p:plain:w600
紛らわしいと判定された画像

uncertainに分類された画像をさらに詳しく見てみました。

前回の記事で述べた紛らわしい種類の画像がきちんとuncertainに分類されており、Random Forestによる分類確率が紛らわしさをきちんと捉えていることが確認できました。

positiveに分類された画像はアニメのスクリーンショット1枚を除いて全てイラストでした。

f:id:kivantium:20200418190731p:plain:w600
二次元イラストだと判定された画像

negativeに分類された画像のうちイラストは34枚でした。これらの画像は、コントラストが薄めである・人間がたくさん書かれているなどの理由から漫画と間違えられた可能性が高いと思っています。(今回のラベリングではコマ割りがあるまたは白黒の画像は全て二次元イラストではないとしています)

f:id:kivantium:20200418190429p:plain:w600
二次元イラストではないと間違えて判定されたイラスト

以上の結果から、margin samplingは二次元画像分類の境界ケースをきちんと集めることができそうだという感触を得ました。これを学習データに加えたら精度が上がったという実験結果を出せればよかったのですが、ランダムサンプリングでも95%くらいの精度が出ていたのでActive Learningで有意差を出すことが難しそうでした。Active Learningをするというよりは、棄却オプションをつけて不確かな画像は人手で分類するようにするのが良さそうです。

次回はこの結果を使って二次元画像だけのタイムラインを表示するアプリを作ろうと思います。

広告コーナー