Pythonで特異値分解(SVD)を理解する
以前の投稿( 論文メモ: Item2Vec: Neural Item Embedding for Collaborative Filtering - け日記 )で比較対象になっていた特異値分解(SVD)についてまとめ、Pythonで実装してみます。
SVDとは
特異値分解(singular value decomposition: SVD)は、数学的にはM×N(M行N列)の行列を分解する方法の一つです。
コンピュータサイエンスでは、行列で表現される特徴(情報検索の分野では、例えば文書毎の単語の出現頻度の行列など)を可能な限り損ねること無く次元を圧縮するために利用され、多次元の特徴を扱う画像処理、自然言語処理、検索や推薦に応用されています。
他の次元圧縮手法としては、主成分分析(PCA)や非負値行列因子分解(NMF)などがあります。
SVDで何ができるのか
任意のM×Nの行列Cは、以下の形に分解することができます。
- 左特異行列Uは、M×Mの正方行列で、各列はと直交する固有ベクトル
- 右特異行列Vは、N×Nの正方行列で、各列はと直交する固有ベクトル
- ΣはN×M行列で、対角行列成分が (ただし、 )で、それ以外が0
- は、行列Cの特異値とよばれる
- rは行列Cのランク(一次独立な列または行ベクトルの数)
具体的な例を挙げます。
以下の3つの文章から単語(ここでは名詞と動詞に限定します)を抽出して、出現頻度行列Cを作ります。
I drive my car. I read this document. I read this paper.
行列Cは、行は単語(上から"I", "drive", "car", "read", "document", "paper")、列は文書で、値は単語の出現頻度にそれぞれ対応しています。
$$ C=\left( \begin{matrix} 1 & 1 & 1 \\ 1 & 0 & 0 \\ 1 & 0 & 0 \\ 0 & 1 & 1 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \end{matrix} \right) $$
これを特異値分解( )するとこうなります。
- UのN列目以降、ΣがN行目以降は削除してます(ΣのN行目以降は全て0のため)
$$ C=\left( \begin{matrix} 0.717 & -0.158 & 0 \\ 0.192 & -0.59 & 0 \\ 0.192 & -0.59 & 0 \\ 0.525 & 0.432 & 0 \\ 0.262 & 0.216 & -0.707 \\ 0.262 & 0.216 & 0.707 \end{matrix} \right) \left( \begin{matrix} 2.394 & 0 & 0 \\ 0 & 1.506 & 0 \\ 0 & 0 & 1 \end{matrix} \right) \left( \begin{matrix} 0.46 & 0.628 & 0.628 \\ -0.888 & 0.325 & 0.325 \\ 0 & -0.707 & 0.707 \end{matrix} \right) $$
実践では、データ量が減らせることもあって、との2つの行列で持つことが多いとのことです。
$$ C=\left( \begin{matrix} 1.716 & -0.238 & 0 \\ 0.46 & -0.888 & 0 \\ 0.46 & -0.888 & 0 \\ 1.256 & 0.65 & 0 \\ 0.628 & 0.325 & -0.707 \\ 0.628 & 0.325 & 0.707 \end{matrix} \right) \left( \begin{matrix} 0.46 & 0.628 & 0.628 \\ -0.888 & 0.325 & 0.325 \\ 0 & -0.707 & 0.707 \end{matrix} \right) $$
numpyでSVDを実装するとこんな感じになりますが、numpy.linarg.svdがありますので、実践ではそちらを使いましょう。
import numpy as np import pandas as pd np.set_printoptions(precision=3, suppress=True) # 分解する行列 c = np.array([[1,1,1],[1,0,0,],[1,0,0],[0,1,1],[0,1,0],[0,0,1]]) # 特異値行列Σ(sigma) → 右特異行列V(v) → 左特異行列U(u) の順に求める # C^TCの固有値と固有ベクトルの計算 ctc = np.dot(c.T, c) eigen_values, eigen_vectors = np.linalg.eig(ctc) # 特異値の計算 singular_values = np.sqrt(eigen_values) singular_index = np.argsort(singular_values)[::-1] # 特異値行列の計算 sigma = np.diag(singular_values[singular_index]) # 右特異行列の計算 v = eigen_vectors[:,singular_index] # 左特異行列の計算 u = np.array([np.dot(c, v[:,i]) / sigma.diagonal()[i] for i in range(len(sigma.diagonal()))]).T
SVDを使うとどうして次元圧縮できるのか
においてΣは対角行列のため、対角要素σが大きければ、Cに与える影響も大きくなります(逆に言いますと、σが小さくなれば影響も小さくなります)。
そこで、上位k(<M)個のσを残し、それ以外は0としたで近似することで、次元を削減します。
低ランク近似と呼ばれており、小さな特異値から削減することで、の各要素の2乗誤差が最小になることが知られています。
上の例でk=2としてを求めると、以下の形になり、概ね行列Cとの値が近いことがわかります。
またこの近似によって、の5行目("document")と6行目("paper")の値はいずれも(0, 0.5, 0.5)となっています。
同一の文書に現れるわけではない"document"と"paper"が、SVDによって類似した単語と判定できるようになった、と言うことができます。
SVDを使ってレコメンドなど欠測値が多いデータの補完に用いられているケースもあります。
$$ C_k=\left( \begin{matrix} 0.717 & -0.158 \\ 0.192 & -0.59 \\ 0.192 & -0.59 \\ 0.525 & 0.432 \\ 0.262 & 0.216 \\ 0.262 & 0.216 \end{matrix} \right) \left( \begin{matrix} 2.394 & 0 \\ 0 & 1.506 \end{matrix} \right) \left( \begin{matrix} 0.46 & 0.628 & 0.628 \\ -0.888 & 0.325 & 0.325 \end{matrix} \right)=\left( \begin{matrix} 1 & 1 & 1 \\ 1 & 0 & 0 \\ 1 & 0 & 0 \\ 0 & 1 & 1 \\ 0 & 0.5 & 0.5 \\ 0 & 0.5 & 0.5 \end{matrix} \right) $$
Pythonの実装は以下のとおりです。
# U_kの計算(k列目以降を削除) u_k = np.delete(u, range(k, u.shape[1]), axis=1) # Σ_kの計算(k×k行列にする) sigma_k = np.delete(sigma, range(k, sigma.shape[0]), axis=0) sigma_k = np.delete(sigma_k, range(k, sigma.shape[1]), axis=1) # V_kの計算(k列目以降を削除 v_k = np.delete(v, range(k, v.shape[1]), axis=1) c_k = u_k.dot(sigma_k.dot(v_k.T))
参考文献
理解・実装にあたり、こちら2冊が参考になりました。
- 作者: 岩波データサイエンス刊行委員会
- 出版社/メーカー: 岩波書店
- 発売日: 2017/02/16
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (3件) を見る
- 作者: Christopher D.Manning,Prabhakar Raghavan,Hinrich Schutze,岩野和生,黒川利明,濱田誠司,村上明子
- 出版社/メーカー: 共立出版
- 発売日: 2012/06/23
- メディア: 単行本
- 購入: 2人 クリック: 69回
- この商品を含むブログ (5件) を見る