こしあん
2019-05-20

統計学や機械学習で使われる分散共分散行列、相関行列とグラム行列の関係

Pocket
LINEで送る
Delicious にシェア

8.8k{icon} {views}



TensorFlowなど分散共分散行列の計算関数が用意されていない場合は、分散共分散行列や相関行列を計算する際に自分で関数を定義しなければいけません。そこでグラム行列から、分散共分散行列、相関行列と派生させて計算する方法を理論を中心に見ていきます。

きっかけは主成分分析を使ったPCA Color Augmentation

なぜ自分がTensorFlowで分散共分散行列の計算が必要になったかということですが、Data Augmentation用に「PCA Color Augmentation(Qiitaで書きました)」が必要になったからです。「Data AugmentationならNumpyで書けばよくね?」と思われるかもしれませんが、実はNumpyで書くと分散共分散行列の計算がボトルネックになって訓練速度が遅く(1.5倍~2倍レベル)なってしまいました。これはPCA Color Augmentationの記事ではないので詳細は別の機会にしますが、ちょっとCPU(Numpy)でやらせるにはハードな行列計算をGPUでブーストできるTensorFlowのレイヤーで定義しようというのがきっかけです。

TensorFlowにはcov関数がない

分散共分散行列の計算は、Numpyではnp.cov()という関数を使えば一発で計算してくれます。例えばIrisデータセットのカラム間の共分散を計算するコードは次のとおりです。

from sklearn.datasets import load_iris
import numpy as np

(iris_X, iris_y) = load_iris(return_X_y=True)
covmat = np.cov(iris_X, rowvar=False)
print(covmat)

#[[ 0.68569351 -0.03926846  1.27368233  0.5169038 ]
# [-0.03926846  0.18800403 -0.32171275 -0.11798121]
# [ 1.27368233 -0.32171275  3.11317942  1.29638747]
# [ 0.5169038  -0.11798121  1.29638747  0.58241432]]

今回はカラム(Irisだったらガクの長さや花弁の幅といった列の方向)間で集計しましたが、Numpyでは列の方向と行の方向の両方で集計できるようになっています。引数の「rowvar=False」は「行方向ではない、つまり列方向での分散を計算するよ」という意味です。

さて、TensorFlowでは2018年10月時点でnp.covに相当する、分散共分散行列の計算関数がありません。先程のPCA Color Augmentationだけじゃなくて、レイヤーの途中で主成分分析やカーネル法で多分使うと思うんですがなぜないんでしょうね。Back Propの定義の都合上なのかな(適当な想像)。

グラム行列と分散共分散行列の関係

分散共分散行列を数式で見てみましょう。分散共分散行列にはいくつか公式のような重要な性質があって、

$$Cov(X)=E[X^TX] – \mu^T\mu \tag{1}$$

で表されます。ここでCov(X)は分散共分散行列を表します。$E[\cdot]$は期待値を表す関数。$\mu$は平均のベクトルです。サイトによっては(Wikipediaもそうですが)、

$$Cov(X)=E[XX^T] – \mu\mu^T \tag{2}$$

とかける順序を逆に書いているサイトも多いです。もちろんどっちが正しくてどっちが間違いということはなくて、Xの定義によりけりなんですよね。例えば今回のIrisの場合、

print(iris_X.shape)
# (150, 4)

となり、Xは150×4の行列であることがわかります。(2)式の計算方法で計算すると、期待値の中身は150×4の行列と4×150の行列の積になるので、150×150の行列になってしまいます。(1)式の計算方法で計算すると、4×150と150×4の行列の積なので、期待された4×4の行列ができます。数学のページではないので、Xの行列の定義とかは厳密にしませんが(ぶっちゃけ自分がそういうのできるほど知識がない)、状況に応じて変わるよということを言いたかっただけです。ちょうど、np.covのrowvarをTrueとするか、Falseとするかのような違いです。(2)の式も間違いではないですが、今回の記事では(1)の式を使います。

さて、(1)の式ですが、もしXが平均で引かれていたらどうでしょうか?ここで注意したいのが、平均で引く操作は 標準化や正規化(どちらも英語ではnormalization)と言われる操作とは異なることです。後で説明しますが、データを標準化してグラム行列を計算すると相関行列となります。分散共分散行列を出したいときは標準偏差で割るのではなく平均だけ引けばいいです。

Xが系列間の平均で引かれていた、つまり平均補正済みとしましょう。このときデータの系列間の平均は0になるので、

$$\mu^T\mu=0$$

となります。つまり、Xが平均補正済みだとしたら分散共分散行列は、

$$Cov(X)=E[X^TX]$$

という簡単な式になります。さて、期待値の中身は前回の記事で見たようにグラム行列なので、こういうが言えます。「XがXの系列間の平均で引かれていたら、分散共分散行列の導出はグラム行列の期待値を取ればよい」

本当にそうなるかNumpyで確認していきましょう。

平均で引かれていたら、分散共分散行列=グラム行列の期待値

まずは前回のおさらい。グラム行列とは行列$X$に対して次のような行列積計算を行って得られた行列のことです。

$$X^T X$$

Numpyで計算するときはランク2のテンソルである行列の場合は、np.dot(X.T, X)で計算できます。

import numpy as np

matrix = np.arange(4).reshape(2,2)
gram = np.dot(matrix.T, matrix)
print("gram : ")
print(gram)
#gram :
#[[ 4  6]
# [ 6 10]]

例としてはIrisデータセットをそのまま使います。Iris_Xを平均で引きグラム行列の期待値を取ったら、本当にnp.cov()で計算できた分散共分散行列と等しくなるか確認します。

from sklearn.datasets import load_iris
import numpy as np

(iris_X, iris_y) = load_iris(return_X_y=True)
# サンプル数(後で期待値の計算に使う)
n_sample = iris_X.shape[0]
print(n_sample) # 150
# 系列の平均
column_mean = np.mean(iris_X, axis=0, keepdims=True)
print(column_mean.shape) # (1, 4)

# 平均で引く
iris_X_meanadj = iris_X - column_mean
print(iris_X_meanadj.shape) # (150, 4)←変わっていない

# 平均で引いたグラム行列
gram_meanadj = np.dot(iris_X_meanadj.T, iris_X_meanadj)
# グラム行列の期待値
gram_meanadj_exp = gram_meanadj / n_sample
print("平均で引く→グラム行列の期待値")
print(gram_meanadj_exp)

# np.covによる分散共分散行列
print("分散共分散行列")
print(np.cov(iris_X, rowvar=False))

出力は次の通りです。小数第3位ぐらいになってくると誤差で若干変わっていますが、だいたい同じ結果を返しているのが確認できます。

追記:誤差ではなくNumpyのcovが不偏分散で計算しているからだそうです。N-1と計算するとあいます。

平均で引く→グラム行列の期待値
[[ 0.68112222 -0.03900667  1.26519111  0.51345778]
 [-0.03900667  0.18675067 -0.319568   -0.11719467]
 [ 1.26519111 -0.319568    3.09242489  1.28774489]
 [ 0.51345778 -0.11719467  1.28774489  0.57853156]]
分散共分散行列
[[ 0.68569351 -0.03926846  1.27368233  0.5169038 ]
 [-0.03926846  0.18800403 -0.32171275 -0.11798121]
 [ 1.27368233 -0.32171275  3.11317942  1.29638747]
 [ 0.5169038  -0.11798121  1.29638747  0.58241432]]

分散共分散行列と相関行列の関係、グラム行列と相関行列の関係

さきほど少し触れた平均で引くだけでなく標準偏差で割られていた場合、つまりデータが標準化されていた場合はどうなるか見てましょう。結論からいうと、グラム行列の期待値は相関行列になります

相関行列というと統計やっていた方ならあーと思い浮かぶと思います。系列間のパラメーターの相関を見るときにとても便利な行列です。例えばIrisの相関行列は次のようになります。

[[ 1.         -0.10936925  0.87175416  0.81795363]
 [-0.10936925  1.         -0.4205161  -0.35654409]
 [ 0.87175416 -0.4205161   1.          0.9627571 ]
 [ 0.81795363 -0.35654409  0.9627571   1.        ]]

対角成分は同一系列の相関を取ったものなので必ず1になります。データの傾向見る際は便利なのでぜひ活用してみてください。

相関行列と分散共分散行列は別物ではなく、相関行列が分散共分散行列の特殊な場合と考えるほうが自然です。相関行列の定義は、これは相関係数の定義ですが、系列$X_i$と$X_j$の相関係数$\rho_{ij}$は、

$$\rho_{ij}=\frac{\sigma_{X_iX_j}}{\sigma_{X_i}\sigma_{X_j}} $$

で表されます。ここで分子は$X_i$と$X_j$の共分散、分母はそれぞれの分散を表します。これを行列表記にしてみましょう。今$\Sigma$を分散共分散行列とし、$P$を相関行列とします。系列数がN個あるものとし(Irisの例ではN=4です)、$\Sigma$と$P$ともにN×N行列で表されます。また、分散共分散行列の対角成分を$\sigma$で取り出します。本来はこの対角成分はベクトルですが、以下のような1×Nの列行列で定義します。

$$\sigma = [\sigma_{X_1}, \cdots, \sigma_{X_i}, \cdots, \sigma_{X_N}]$$

括弧の中身は要素ごとの分散ですね。で、これは本来の数学の計算ではないのですが、Pythonではできるブロードキャスティングを使った計算で、$\sigma$をsigmaという変数に入れ、次のコードで求められる行列を$S$とします。

S = np.sqrt(sigma.T * sigma)

これは相関係数の定義にある分母の値を行列で書き直したものにすぎません。この$S$という行列が定義できれば、相関行列$P$は分散共分散行列$\Sigma$を用いて次のように表されます。

$$P=\frac{\Sigma}{S} $$

ここでの演算は要素間の割り算です(もう少し厳密に言うならアダマール積の割り算版)。つまり、このSの行列の成分が全て1になれば$P=\Sigma$という式が成立します。ちなみにもっと厳密な数学の定義は、高校数学の美しい物語にある「相関行列の定義と分散共分散行列との関係」をご覧ください。なるほど、こう書くのかと思いました。

行列Sの成分が全て1になるということは、Xの系列間の分散が全て1になるということなので、「系列間の標準偏差で割る」という補正をしていた場合は、分散共分散行列=相関行列となります。さらに分散共分散行列については、「系列間の平均で引く」という補正をしていた場合は、分散共分散行列=グラム行列の期待値となります。

つまり三段論法的な発想により、「データを標準化していたら」グラム行列の期待値は相関行列と等しくなる。という結論が出ます。これを実際のデータで確認してみます。

標準化していたら、相関行列=分散共分散行列=グラム行列の期待値

Irisデータで確かめる

標準化済みのグラム行列の期待値と、分散共分散行列と、標準化していないときの相関行列が等しくなるのを確認します。

from sklearn.datasets import load_iris
import numpy as np

(iris_X, iris_y) = load_iris(return_X_y=True)

# サンプル数(後で期待値の計算に使う)
n_sample = iris_X.shape[0]
print(n_sample) # 150
# 系列の平均
column_mean = np.mean(iris_X, axis=0, keepdims=True)
print(column_mean.shape) # (1, 4)
# 系列の標準偏差
column_sd = np.std(iris_X, axis=0, keepdims=True)
print(column_sd.shape) # (1, 4)

# データの標準化
iris_X_normalized = (iris_X - column_mean) / column_sd

# グラム行列の期待値
gram_exp = np.dot(iris_X_normalized.T, iris_X_normalized) / n_sample
print("標準化済みグラム行列の期待値")
print(gram_exp)

# 標準化済みのときの分散共分散行列
cov = np.cov(iris_X_normalized, rowvar=False)
print("標準化済みの分散共分散行列")
print(cov)

# 実際の相関行列
corr = np.corrcoef(iris_X, rowvar=False)
print("実際の相関行列")
print(corr)

結果は以下の通りです。

標準化済みグラム行列の期待値
[[ 1.         -0.10936925  0.87175416  0.81795363]
 [-0.10936925  1.         -0.4205161  -0.35654409]
 [ 0.87175416 -0.4205161   1.          0.9627571 ]
 [ 0.81795363 -0.35654409  0.9627571   1.        ]]
標準化済みの分散共分散行列
[[ 1.00671141 -0.11010327  0.87760486  0.82344326]
 [-0.11010327  1.00671141 -0.42333835 -0.358937  ]
 [ 0.87760486 -0.42333835  1.00671141  0.96921855]
 [ 0.82344326 -0.358937    0.96921855  1.00671141]]
実際の相関行列
[[ 1.         -0.10936925  0.87175416  0.81795363]
 [-0.10936925  1.         -0.4205161  -0.35654409]
 [ 0.87175416 -0.4205161   1.          0.9627571 ]
 [ 0.81795363 -0.35654409  0.9627571   1.        ]]

分散共分散行列がちょっと誤差ありますが、グラム行列の期待値と実際の相関行列は完全に同じ値を返していますね。きっとnp.corrcoefの実装がこんなのなんでしょう。

一般の分散共分散行列から相関行列を求める方法

さて、補足ですが、標準化していない一般の分散共分散行列から相関行列を求める方法をコードで確認しておきましょう。

from sklearn.datasets import load_iris
import numpy as np

(iris_X, iris_y) = load_iris(return_X_y=True)

# 分散共分散行列
cov = np.cov(iris_X, rowvar=False)
# 分散共分散行列の対角成分
cov_diag = np.expand_dims(np.diag(cov), axis=0)
# 対角成分とその転置のelementwise積
S = np.sqrt(cov_diag.T * cov_diag)
# 手動で求めた相関行列
corr_manual = cov / S
print("分散共分散行列から求めた相関行列")
print(corr_manual)

# 実際の相関行列
corr = np.corrcoef(iris_X, rowvar=False)
print("実際の相関行列")
print(corr)

やっていることは先程述べたこととをコードに書き換えたものです。

分散共分散行列から求めた相関行列
[[ 1.         -0.10936925  0.87175416  0.81795363]
 [-0.10936925  1.         -0.4205161  -0.35654409]
 [ 0.87175416 -0.4205161   1.          0.9627571 ]
 [ 0.81795363 -0.35654409  0.9627571   1.        ]]
実際の相関行列
[[ 1.         -0.10936925  0.87175416  0.81795363]
 [-0.10936925  1.         -0.4205161  -0.35654409]
 [ 0.87175416 -0.4205161   1.          0.9627571 ]
 [ 0.81795363 -0.35654409  0.9627571   1.        ]]

一致しているのを確認できましたね。これで分散共分散行列、相関行列をマスターできました。

グラム行列、分散共分散行列、相関行列の関係のまとめ

今までの議論をまとめると次の図になります。これを見るともう少しすっきり理解できるのではないでしょうか。

今、実数の行列の話のみ考えるとしましょう。グラム行列というのはとても広い考え方で、どんな行列を取ってこようがその結果はみんなグラム行列と呼びます。分散共分散行列、相関行列はおおよその行列で計算できますが、スケールだったり対角要素だったりだんだん縛りが多くなり、より狭い概念となっていきます。

分散共分散行列はグラム行列より狭い概念で、「グラム行列計算前に平均で引いていたら」そのグラム行列の期待値が、分散共分散行列と等しくなります。相関行列はもっと狭い概念で、「グラム行列計算前に標準化していたら」、グラム行列の期待値が相関行列と等しくなります。分散共分散行列と相関行列の関係は、先程見たような分散の調整を行ったものですが、やはり相関行列のほうが狭い概念です。

期待値操作や、スケール調整が入るため、必ずしもこのような包含図が正しいとは限りませんが、漠然とした理解ではこのような図になります。実際に実装するときはこれだけわかってれば大丈夫です。

TensorFlowへの実装へのまとめ

これまで、グラム行列、分散共分散行列、相関行列への関係を見てきましたが、理解できたでしょうか?

分散共分散行列、相関行列の関数がないときは、前回みたような方法でグラム行列を計算すればよいことがわかりました。あとはデータの標準化次第で、分散共分散行列になるか、相関行列になるかが決まるというわけです。次回は実際にTensorFlowで実装してみます。

次回:TensorFlow/Kerasでの分散共分散行列・相関行列、テンソル主成分分析の実装
https://blog.shikoan.com/tensorflow-varcov-pca/



Shikoan's ML Blogの中の人が運営しているサークル「じゅ~しぃ~すくりぷと」の本のご案内

技術書コーナー

【新刊】インフィニティNumPy――配列の初期化から、ゲームの戦闘、静止画や動画作成までの221問

「本当の実装力を身につける」ための221本ノック――
機械学習(ML)で避けて通れない数値計算ライブラリ・NumPyを、自在に活用できるようになろう。「できる」ための体系的な理解を目指します。基礎から丁寧に解説し、ディープラーニング(DL)の難しいモデルで遭遇する、NumPyの黒魔術もカバー。初心者から経験者・上級者まで楽しめる一冊です。問題を解き終わったとき、MLやDLなどの発展分野にスムーズに入っていけるでしょう。

本書の大きな特徴として、Pythonの本でありがちな「NumPyとML・DLの結合を外した」点があります。NumPyを理解するのに、MLまで理解するのは負担が大きいです。本書ではあえてこれらの内容を書いていません。行列やテンソルの理解に役立つ「従来の画像処理」をNumPyベースで深く解説・実装していきます。

しかし、問題の多くは、DLの実装で頻出の関数・処理を重点的に取り上げています。経験者なら思わず「あー」となるでしょう。関数丸暗記では自分で実装できません。「覚える関数は最小限、できる内容は無限大」の世界をぜひ体験してみてください。画像編集ソフトの処理をNumPyベースで実装する楽しさがわかるでしょう。※紙の本は電子版の特典つき

モザイク除去から学ぶ 最先端のディープラーニング

「誰もが夢見るモザイク除去」を起点として、機械学習・ディープラーニングの基本をはじめ、GAN(敵対的生成ネットワーク)の基本や発展型、ICCV, CVPR, ECCVといった国際学会の最新論文をカバーしていく本です。
ディープラーニングの研究は発展が目覚ましく、特にGANの発展型は市販の本でほとんどカバーされていない内容です。英語の原著論文を著者がコードに落とし込み、実装を踏まえながら丁寧に解説していきます。
また、本コードは全てTensorFlow2.0(Keras)に対応し、Googleの開発した新しい機械学習向け計算デバイス・TPU(Tensor Processing Unit)をフル活用しています。Google Colaboratoryを用いた環境構築不要の演習問題もあるため、読者自ら手を動かしながら理解を深めていくことができます。

AI、機械学習、ディープラーニングの最新事情、奥深いGANの世界を知りたい方にとってぜひ手にとっていただきたい一冊となっています。持ち運びに便利な電子書籍のDLコードが付属しています。

「おもしろ同人誌バザールオンライン」で紹介されました!(14:03~) https://youtu.be/gaXkTj7T79Y?t=843

まとめURL:https://github.com/koshian2/MosaicDeeplearningBook
A4 全195ページ、カラー12ページ / 2020年3月発行

Shikoan's ML Blog -Vol.1/2-

累計100万PV超の人気ブログが待望の電子化! このブログが電子書籍になって読みやすくなりました!

・1章完結のオムニバス形式
・機械学習の基本からマニアックなネタまで
・どこから読んでもOK
・何巻から読んでもOK

・短いものは2ページ、長いものは20ページ超のものも…
・通勤・通学の短い時間でもすぐ読める!
・読むのに便利な「しおり」機能つき

・全巻はA5サイズでたっぷりの「200ページオーバー」
・1冊にたっぷり30本収録。1本あたり18.3円の圧倒的コストパフォーマンス!
・文庫本感覚でお楽しみください

Vol.1 電子550円
Vol.2 電子550円

北海道の駅巡りコーナー

日高本線 車なし全駅巡り

ローカル線や秘境駅、マニアックな駅に興味のある方におすすめ! 2021年に大半区間が廃線になる、北海道の日高本線の全区間・全29駅(苫小牧~様似)を記録した本です。マイカーを使わずに、公共交通機関(バス)と徒歩のみで全駅訪問を行いました。日高本線が延伸する計画のあった、襟裳岬まで様似から足を伸ばしています。代行バスと路線バスの織り成す極限の時刻表ゲームと、絶海の太平洋と馬に囲まれた日高路、日高の隠れたグルメを是非たっぷり堪能してください。A4・フルカラー・192ページのたっぷりのボリュームで、あなたも旅行気分を漫喫できること待ったなし!

見どころ:日高本線被災区間(大狩部、慶能舞川橋梁、清畠~豊郷) / 牧場に囲まれた絵笛駅 / 窓口のあっただるま駅・荻伏駅 / 汐見の戦争遺跡のトーチカ / 新冠温泉、三石温泉 / 襟裳岬

A4 全192ページフルカラー / 2020年11月発行


Pocket
LINEで送る
Delicious にシェア

Add a Comment

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です