sonoshouのまじめなブログ

情報系大学生からのウェブ見習い人生の記録

非負値行列因子分解

このエントリーは、集合知プログラミング第8章を参照にしています。

非負値行列因子分解は、データマイニングの手法の一つである。
データの重要な特徴を抽出するために用いられる。
非負値行列因子分解は、non-negative matrix factorizationの日本語訳であり、
よくNMFと省略されるので、こちらの省略形も覚えておきたい。

非負値行列因子分解の基本的なアイディア

非負値行列因子分解は、その名の通り、行列を正の数(非負値)で因子分解することで、
特徴の抽出を行う。
因子分解とは、掛け合わせることで再び分解前の行列を構築できるような
2つの小さな行列を探し出すということである。

非負値行列因子分解の例

以下、具体例を交えながら解説する。

文書の記事記事内に存在する単語との対応付けがあるとき、
これらに対してNMFで特徴を抽出する。

対応付けの表は以下のようになっているとする。

apple lemon google tv gree book doraemon
computer of history 5 0 6 2 2 9 0
a lot of fruits 7 5 0 1 0 6 0
dessert in japan 4 3 0 0 0 5 0
new machine 1 0 1 8 1 8 1
what is a smartphone 2 0 4 3 6 6 0

行に記事の名前(カテゴリ名ではない)、列に単語のカウントがあったとする。
例えば、「a lot of fruits」という記事には、
「lemon」という単語が5回出てきたことを意味する。

早速これを非負値行列因子分解にかけてみる。
使用したプログラムは以下である。

# -*- coding: utf-8 -*-
from numpy import *

def difcost(a, b):
	dif = 0
	# 行列の全ての行と列をループする。
	for i in range(shape(a)[0]):
		for j in range(shape(a)[1]):
			# 差を足し合わせる
			dif += pow(a[i,j]-b[i,j],2)
	return dif

def factorize(v, pc=10, iter=50):
	ic=shape(v)[0]	# shape関数は行列の行と列の数を返す。
	fc=shape(v)[1]

	# 重みと特徴の行列をランダムな値で初期化
	w = matrix([[random.random() for j in range(pc)] for i in range(ic)])
	h = matrix([[random.random() for i in range(fc)] for i in range(pc)])

	# 最大でiterの回数だけ操作を繰り返す:
	for i in range(iter):
		wh = w * h

		# 現在の差を計算
		cost = difcost(v, wh)

		# 行列が完全に因子分解されたら終了
		if cost == 0:
			break

		# 特徴の行列を更新
		hn = (transpose(w) * v)
		hd = (transpose(w) * w * h)
		
		h = matrix(array(h)*array(hn)/array(hd))

		# 重みの行列を更新
		wn = (v * transpose(h))
		wd = (w * h * transpose(h))

		w = matrix(array(w)*array(wn)/array(wd))
	
	return w,h

if __name__ == '__main__':
	mat=matrix([[5,0,6,2,2,9,0],
				[7,5,0,1,0,6,0],
				[4,3,0,0,0,5,0],
				[1,0,1,8,1,8,1],
				[2,0,4,3,6,6,0]])

	w,h= factorize(mat,pc=4,iter=100)

	# 分解された行列を出力
	print 'w='
	for i in range(shape(w)[0]):
		for j in range(shape(w)[1]):
			print ('%3f' % w[i,j]),
		print
	print
	print 'h='
	for i in range(shape(h)[0]):
		for j in range(shape(h)[1]):
			print ('%3f' % h[i,j]),
		print
	print
	# 元々の行列を出力
	print 'answer='
	print mat
	print
	# 分解された行列2つを掛け合わせた答えを出力
	print 'w*h='
	wh = w * h
	for i in range(shape(wh)[0]):
		for j in range(shape(wh)[1]):
			print ('%3f' % wh[i,j]),
		print

非負値行列因子分解の初期値はランダムのため、
毎回分解される行列が異なる。
今回は一つの出力結果を例としてあげて考察する。
尚、今回は特徴を4つと定義した。
これを変更するためには、main内のw,h= factorize(mat,pc=4,iter=100)のpc=4
変更すれば良い。

w=
0.115763 1.394507 1.002702 0.002616
0.101676 0.608862 0.000098 1.524762
0.056321 0.589217 0.003223 0.837026
1.748477 0.097005 0.302305 0.008632
0.192223 0.000058 1.808071 0.001300

h=
0.211139 0.000000 0.001217 4.357839 0.075848 3.833696 0.560386
2.861827 0.110886 1.678718 0.044343 0.000000 4.220322 0.000000
1.058467 0.000000 2.590953 1.249636 2.991750 2.879790 0.000000
3.220199 3.315222 0.000000 0.199414 0.000000 2.062877 0.000000

answer=
[[5 0 6 2 2 9 0]
 [7 5 0 1 0 6 0]
 [4 3 0 0 0 5 0]
 [1 0 1 8 1 8 1]
 [2 0 4 3 6 6 0]]

w*h=
5.085030 0.163304 4.939077 1.819846 3.008614 9.222034 0.064872
6.674068 5.122439 1.022486 0.774269 0.008006 6.105072 0.056978
4.396929 2.840262 0.997549 0.442506 0.013915 4.438564 0.031561
0.994560 0.039372 0.948232 8.003378 1.037040 8.000905 0.979823
1.958723 0.004318 4.684958 3.097369 5.423875 5.946716 0.107719

wの行列は、
行に記事名、列に特徴の値として定義される。
hの行列は、
行に特徴、列に単語名として定義される。

answerは元々の行列であるが、
これがwの行列とhの行列に分解され、
さらにこれらの行列を掛け合わせると、
ほとんどanswer(元の行列)と近くなっていることが確認できる。

さて、この表の見方には少しコツがいる。

まずはwの行列について説明する。
(1,4)の要素(左から1番目、上から4番目)を見てみると、
同列内の他の要素と比べて、1.748477と大きい。
この行は「new machine」なので、
「特徴1」は「new machine」の特徴をよく表していると言える。

また、wの行列について、
(4,2)と(4,3)の要素を見てみると、
こちらも同列内の他の要素と比べると、やや大きい。
この行である「a lot of fruits」と「dessert in japan」は
「特徴4」によく表れているといえ、またこれらの記事は
カテゴリとして近いことが言える。

次に、hの行列について説明する。
(2,4)の要素を見てみると、
同列内の他の要素と比べて、3.315222とかなり大きい。
この列は「lemon」なので、
「特徴4」は「lemon」という単語の重みが大きいことを示している。

以上のことから、「特徴4」は果物系やデザート系をまとめたカテゴリであり、
「lemon」という単語がキーワードとなっていることがわかる。

また、hの行列の7行目はどれも値が変わらない。
7列目は「doraemon」であるが、
この単語は特徴としてはあまり意味をなしていないことがわかる。

このような予想を繰り返すと、
何となくではあるが、このように特徴を捉えると、
うまくwとhの行列を説明できるように思える。

特徴1:機械
特徴2:アップル
特徴3:スマートフォン
特徴4:果物、デザート系

このように行列を分解することによって、
様々な特徴を捉えられることがわかった。