ココンの情報をいつでも、どこでも。ココントコ。

エンジニア 2018.08.01

少数の例外値を発見する手法について〜例外画像の検出ニューラルネットワーク〜

AI戦略室の坂本です。
AIはその応用範囲の広さから、様々な分野に利用されうるのですが、現状でビジネススキームにマッチングの良い応用分野といえば、ビッグデータ分析・データマイニングなどの分野が挙げられるのではないでしょうか。
機械学習のために用意されたデータセットは、多くの場合ノイズの少ない「きれいなデータ」ですが、実際に利用できるデータには色々なノイズ・例外値が含まれており、それらの検出が重要だったりします。
なので今回は、「学習用データセットに含まれていない」、「少数の例外値を検出する」手法について書きたいと思います。

目次

背景

機械学習を使用してビッグデータの分析を行う際に、「データに含まれているかもしれない少数の例外値」は、なかなか厄介な問題だったりします。
ニューラルネットワークのような手法を使えば、少数の例外値は無視するように学習してくれるのでは?と思う方もいらっしゃるかもしれませんが、なかなかどうして、その例外値に学習が引きづられてしまったり、テストデータで作成したモデルと実際の学習結果で性能が食い違ってしまう原因になったりと、無視できない効果をもたらす場合があるのです。
また無視してくれるなら無視してくれるで困る場合もあり、統計的な解析に使用するのであれば問題なくとも、例外値に対するAIの動作が予測できないものになるなど、ビッグデータの利用目的によってはこの問題はクリティカルなものになり得ます。

ここで問題となるのは、少数の例外値はそのデータが少数であるが故に、学習用のデータとして利用しにくいという点です。上の図のように、大規模なデータから学習用のサブセットを作成する場合など、例外値が含まれないデータで学習を行うこともありますし、リアルタイム処理系で学習できなかったパターンが入力される場合もあります。
また、例外値の具体的な値がどのような範囲になるかも解らない事が多く、全ての生データを検証せずに学習させるのは、セキュリティ的に見ても問題があったりします。

例外値を含んだデータセットの用意

誰が見ても解りやすい例外値として、ここでは画像データを使用します。
使用するデータは、機械学習ではおなじみのMNISTの手書き数字画像に、少数の例外画像をドープしたものを用意しました。
https://github.com/cocon-ai-group/MNIST-with-anomaly-datasets

上記のGitHubからダウンロード出来るデータには、以下のファイルがあります。

・train.zip – ラベル付けされたMNISTの手書き数字画像21,038枚
・test.zip – ラベル付けされたMNISTの手書き数字画像20,962枚
・anomaly.zip – ラベル付けされていない画像28,000枚で、内MNISTの手書き数字画像が27,990枚、例外画像10枚

ここで、train.zip、test.zip、anomaly.zip内のデータは、全て異なる画像ファイルですが、train.zip、test.zipには0から9までのラベルが振られた手書き数字画像のみが含まれています。
そしてanomaly.zip内には、MNISTの手書き数字画像が27,990枚と、例外となる画像ファイルが10枚含まれています。当然、例外画像は手書き数字画像と同じ28×28ピクセル1チャンネルのモノクロ画像で、解像度の判定で例外画像を見つけることは出来ません。

分散表現と例外検出

ここで、今回のお題は、「train.zip内に含まれる画像のみを学習データとして、anomaly.zip内に含まれている例外画像を発見するモデルを作成する事」となります。
このような場合、どのような機械学習モデルが使用できるでしょうか?
まず考えられるのは、以前も少し書いた多様体学習による次元削減です。
多様体学習によってデータの次元数を削減してゆくと、データ内の特徴量に基づいて次元内にデータが分散してゆくので、その分散表現を可視化すれば、データ内に含まれている傾向が見て取れるはずです。
多様体学習で作成されたモデルに例外値を含むデータを入力すれば、他のデータと明らかに異なるデータは、他のデータとは異なる場所にマッピングされるのではないか?という訳です。
根拠は何やら怪しげですが、アイデア自体は上手くゆきそうに思えてきます。
それでは実際に、オートエンコーダーというニューラルネットワークを使用して入力データを2次元に次元削減した結果を見てみましょう。


まずは、train.zip内に含まれる画像に対してオートエンコーダーによる教師なし学習を行い、完成したモデルを使用してanomaly.zip内に含まれている画像全てを次元削減してみました。
そして、画像内の対応する座標にファイル名をプロットして散布図にしたものが上の図になります。さて、上の図の中から、例外画像10枚を発見できるでしょうか?

正解となる例外画像の位置は、上のようになります。
この例では、例外画像をそのほかのMNISTデータから分離できていません。
ここで使用したモデルは、train.zip内に含まれる画像で学習したものなので、anomaly.zipの側にのみ含まれている例外画像は学習には使用されていません。そうすると、例外画像もそのほかの画像とほぼ同じ座標へとマッピングされてしまうことが解ります。

Metric Learningによる例外検出

ではどのようにするかというと、ここではMetric Learningという手法を利用し、MNISTのデータと、意味消失したデータとの距離学習を行います。
意味消失したデータとは、画像の場合は単色で塗りつぶした画像や、ランダムな数値のピクセルからなる画像などで、ここでは真っ白と真っ黒の2種類の塗りつぶし画像を使用しました。
Metric Learningとは、Similarity Learningの一種で、複数のデータ間における距離を作成する機械学習の手法です(ちなみに、この手の技術用語は適切な訳語がなかなか出来ないので困ります – Similarity Learningには類似学習という訳語を見つけましたが、この記事を書いている時点でMetric Learningには良い訳語が見つかりませんでした。変な訳を勝手に作って後から別の訳語が一般化すると嫌なので、英語のまま使うことにします)。
ニューラルネットワークでMetric Learningを行うには、Triplet lossという損失関数がよく使われます。

ニューラルネットワークの出力は2次元ベクトルとし、train.zip内に含まれる画像で学習したモデルの、anomaly.zipのデータに対する出力をそのまま散布図にしたものが、以下の図となります。

一見していくらかの例外値が、主たる系統から分離してマッピングされていることが解ります。
上の図の中で、正解となる例外画像の位置は、下の赤い部分になります。

今度は正しく例外画像を分離できています。また、ここまできれいに例外値が分離していると、クラスタリングを使用して機械的に例外を判断することも出来そうです。

実際のプログラムコード

それでは実際のプログラムコードを見てゆきましょう。
なお、このコードは先ほどのGitHub(https://github.com/cocon-ai-group/MNIST-with-anomaly-datasets)にて公開しているので、そちらも参照してください。
まずは学習用データを読み込み、Tripletを作成する関数です。

# 全てのpngファイルを読み込む
train = []
train_fn = []
for fn in glob.glob('train/*/*.png'):
	img = Image.open(fn).convert('L')
	x = np.array(img, dtype=np.float32)
	x = x.reshape((1, 28, 28))  ## 畳み込みニューラルネットワークの場合
	train.append(x)
	train_fn.append(fn.split('/')[1])
train = np.array(train)

# Metric学習のデータを取得する関数
triplet_pos = 0
# 一枚画像を取得
def get_one():
	global triplet_pos
	data = train[triplet_pos]
	triplet_pos = triplet_pos+1
	if triplet_pos >= len(train):
		triplet_pos = 0
	return data

# 1トリプレットを取得
def get_one_triple():
	if random.random() < 0.5:
		c = get_one()
		d = np.zeros(c.shape, dtype=np.float32)
		e = np.zeros(c.shape, dtype=np.float32) + 255
	else:
		d = get_one()
		e = get_one()
		c = np.zeros(d.shape, dtype=np.float32)
		if random.random() < 0.5:
			c = c + 255
	return (c,d,e)

特に難しい点はありません。get_one_triple関数はTripletとして、MNISTのデータか、意味消失したデータとして白(255)または黒(0)単色で塗りつぶした画像かを選択し、Anchor、Positive、Negativeの順番でタプルを返します。

# ニューラルネットワークのモデル
class NMIST_Triplet_NN(chainer.Chain):

	def __init__(self):
		super(NMIST_Triplet_NN, self).__init__()
		with self.init_scope():
			self.layer1 = L.Linear(28*28, 50)
			self.layer2 = L.Linear(50, 50)
			self.layer3 = L.Linear(50, 50)
			self.layer4 = L.Linear(50, 2)

	def __call__(self, x):
		# ニューラルネットワークによるMetric認識
		x = F.tanh(self.layer1(x))
		x = F.tanh(self.layer2(x))
		x = F.tanh(self.layer3(x))
		return self.layer4(x)

使用したニューラルネットワークは上のようになります。
画像データなのだから本来は畳み込みニューラルネットワークを使えば良いのですが、28×28のMNISTデータなので、全結合層だけで済ますことにしました。因みにこの構造は、前述のオートエンコーダーにおける前半部分と同じです。なので、オートエンコーダーとMetric Learningの差を評価するという意味合いもありました。

# カスタムUpdaterのクラス
class TripletUpdater(training.StandardUpdater):

	def __init__(self, optimizer, device):
		self.loss_val = []
		super(TripletUpdater, self).__init__(
			None,
			optimizer,
			device=device
		)

	# イテレーターがNoneなのでエラーが出ないようにオーバライドする
	@property
	def epoch(self):
		return 0

	@property
	def epoch_detail(self):
		return 0.0

	@property
	def previous_epoch_detail(self):
		return 0.0

	@property
	def is_new_epoch(self):
		return False
		
	def finalize(self):
		pass
	
	def update_core(self):
		batch_size = 1000
		# Optimizerを取得
		optimizer = self.get_optimizer('main')
		# Tripletを取得
		anchor = []
		positive = []
		negative = []
		for i in range(batch_size):
			in_data = get_one_triple()
			anchor.append(in_data[0])
			positive.append(in_data[1])
			negative.append(in_data[2])
		anchor = np.array(anchor)
		positive = np.array(positive)
		negative = np.array(negative)
		# ニューラルネットワークを3回実行
		model = optimizer.target
		anchor_r = model(anchor)
		positive_r = model(positive)
		negative_r = model(negative)
		# Triplet Lossで学習
		optimizer.update(F.triplet, anchor_r, positive_r, negative_r)

ニューラルネットワークの学習は上のようにカスタムUpdaterを作成し、ミニバッチ分のTripletを作成した後、ニューラルネットワークを3回実行してその結果をTriplet lossで逆伝播させます。
学習するための実行結果はTripletの分だけ必要ですが、Tripletのデータをミニバッチ方向に積み上げれば、ニューラルネットワークの実行自体は1回にすることも出来ます。

# クラスタリング
from sklearn import cluster
clf = cluster.AgglomerativeClustering(n_clusters=2, linkage='average', affinity='l2')
clz = clf.fit_predict(result.data)
# クラスタ番号が0か1なので、0以外の数を数えて、クラスタの大きさを比較
count1 = np.count_nonzero(clz)
count2 = len(clz) - count1
# 小さい方のクラスタを取得
clzidx = 1 if count1 < count2 else 0
# 小さい方のクラスタに属しているインデックスを取得
idx = np.argwhere(clz==clzidx)[:,0]

# 色分けして保存
im2 = Image.new('RGB', (1000,1000), (0xff,0xff,0xff))
draw = ImageDraw.Draw(im2)
for i in range(len(result.data)):
	l = test_fn[i]
	c = (0xff,0,0) if clz[i] == clzidx else (0x80,0x80,0x80)
	x = int((result.data[i][0]-xmin) / (xmax-xmin) * 900 + 50)
	y = int((result.data[i][1]-ymin) / (ymax-ymin) * 900 + 50)
	draw.text((x, y), l, c)
im2.save('clusters.png', 'PNG')

最後に、結果をクラスタリングして、散布図にして保存します。
クラスタリングのアルゴリズムとしては、KMeans法などが有名ですが、分散表現化したデータを分離するためには、AgglomerativeClusteringにlinkage=’average’を指定して使用するのが、だいたいの場合良い結果になるようです。

まとめ

今回は、「学習用データセットに含まれていない」「少数の例外となる」データを検出する手法について紹介しました。
機械学習による分析では、ビッグデータを学習用データとすることが多々あるのですが、そのデータが何の検証も受けていない、ウェブから入手したデータだったりすると、セキュリティ的にも問題があるので、機械学習の前にデータの内容を確認することは重要だったりします。
しかしなにぶん、ビッグデータはデータのサイズが本来的にビッグなので、その全てを人が確認するのは事実上不可能です。そうすると、検証すべきデータの’あたり’をつけて、少数のデータのみ検証することで、データセット全体が想定したとおりのデータから成り立っているかを確認しなければなりません。
そういった場合、例外値を見つけ出す手法というのは意外と実用的だったりします。今回紹介した手法が使える場合も多いので、何かの役に立てれば幸いです。

イベントインタビュー広報 2020.01.31

「OpenID Summit Tokyo 2020」参加レポート

広報の馬場です。4年に一度のOpenIDの祭典「OpenID Summit Tokyo 2020」が渋谷ストリーム・ホールにて1月2…

イベントインタビュー広報 2019.11.11

世界のハッカーが競う「DEF CON CTF Finals 2019」参加者インタビュー

広報の馬場です。 DEF CONの人気コンテンツの一つである「DEF CON CTF」は、熾烈な予選を勝ち抜いたチーム…

エンジニア 2019.08.23

危険すぎると話題の文章生成モデル「GPT-2」で会社紹介文を生成してみました

ココングループのAI TOKYO LABの細川です。AI、特にディープラーニング系のホットな技術を実際に動かすと…