PCA – 主成分分析

概要

主成分分析(principal component analysis: PCA)は教師なし学習の手法の一つでもあり、その考え方は、特徴量の線形組み合わせの中で、最もデータの情報を多く含む組み合わせを発見し、それを主成分として分析を行うものである。

具体的には、特徴量空間の中でベクトルを考え、そのベクトル沿いのデータの分散が最も大きくなるようにベクトルを定める。その考え方と定式化については、主成分分析の定式化にまとめた。

クラス分類データへの適用

Irisデータセット

Irisデータセットにscikit-learnのPCAを適用した例。

PCA – Irisデータセット

教師なし学習として、クラス分類のターゲットデータを用いることなく、特徴量の分析だけでクラスがうまく分離できるような主成分が得られる。

Breast cancerデータセット

Breast cancerデータセットにscikit-learnのPCAを適用した例。

PCA – Breast cancerデータセット

Irisデータの場合と同じく、教師なし学習として、クラス分類のターゲットデータを用いることなく、特徴量の分析だけでクラスがうまく分離できるような主成分が得られる。

ここでは、主成分を構成する各特徴量の寄与をヒートマップによって視覚化している。

LFW peopleデータセット

著名人の顔画像データを集めたLFW peopleデータセットにscikit-learnのPCAを適用した例。

PCA – LFWデータセット

主成分の可視化、次元圧縮後の画像の再現など、様々な角度でPCAの特性を見ている。

回帰データへの適用

Boston house pricesデータセット

Boston house pricesデータセットにPCAを適用して、ターゲットが連続量で与えられた回帰系の問題への適用性を確認する。

PCA – Boston house pricesデータセット

このデータセットに関する限り、PCAで明確な関係は見いだせなかった。

なお、このデータセットには属性データが含まれ、前処理としてone-hot encodingを行っている。

 

Ruby – dateライブラリー

導入

require("date")でライブラリーを読み込むとDateクラスが使えるようになる。

Dateオブジェクトの生成

Dateクラスのコンストラクターの引数で年、月、日を指定する。

parseクラスメソッドに様々な形の日付文字列を与えてDateオブジェクトを生成。

todayクラスメソッドは今日の日付でDateオブジェクトを生成。

値の取り出し

年月日や曜日番号を個別に取り出し。

日付の書式付表示

strftimeメソッドの引数で書式を設定。

主な書式文字

%C 世紀
%Y 西暦年
%y 西暦年の下2桁
%m 月(01-12)
%d 日(01-31)
%w 曜日番号(日曜:0~土曜:6)
%u 曜日番号(月曜:1~日曜:7)
%A 曜日の名称(Monday, Tuesday, …)
%a 曜日の略称(Mon, Tue, …)

 

Ruby – ハッシュ

ハッシュの定義

ハッシュは{}で囲み、key => valueをカンマで連ねて定義する。

空のハッシュは次のいずれかで。

ハッシュのサイズ

ハッシュのサイズはsizelengthで取得。

ハッシュが空かどうかはempty?で確認できる。

キーと値のリスト

keysvaluesでキーや値のリストが得られる。

キーと値の検索

has_key?、has_value?でキーや値の存在を確認。

値の参照・追加・変更

キーを指定して値を参照。存在しない場合はnil

新たなキーを指定してアイテムを追加。

キーを指定してアイテムを削除。

キーを指定して値を変更。

キーと値の順次取り出し

eachメソッドでキーと値のセットを順次取り出す。

シンボルによるキーの設定

:key => valueの形で、キーをシンボルで設定できる。

シンボルで設定する場合、key: valueの形でも設定できる。ただし参照する場合は:keyの形で。

キーのリストもシンボルで表示される。

 

Ruby – chomp/chomp!~末尾の改行文字の削除

chompは末尾の改行文字を削除する。ただし"\n\r"の場合は"\n"が削除されずに残る。

"\n\r"を削除したいときは引数に"\n\r"を指定するか、chompを2回実行する(chompは改行文字列だけを削除する)。

末尾以外にある改行文字は削除されない。

chopは非破壊的であり、元の文字列は変更されない。

chop!にすると破壊的メソッドになり、戻り値も変更後の文字列。

 

Ruby – chop/chop!~末尾文字の削除

chopは文字列の末尾1文字を削除する。

末尾に改行文字がある場合は、それらが削除される。ただし"\n\r"の場合だけは"\n"が削除されずに残る。

chopは非破壊的であり、元の文字列は変更されない。

chop!にすると破壊的メソッドになり、戻り値も変更後の文字列。

 

Ruby – 配列の演算

概要

Rubyの配列同士の演算はPythonのようにブロードキャストされない。加算/減算はそれぞれ和集合/差集合のように扱われる。

配列同士の加算

+~加算は和集合

配列同士をで加算すると双方の和集合となる。

必ずしも重複が削除されるわけではない。

~減算は差集合

減算の場合、元の配列から重なる要素だけが削除される。下の例ではabに共通な[3, 4]が削除され、[5, 6]はもともとaに含まれていないので無視される。

積集合の求め方

加算と減算を組み合わせて、2つの配列の席集合に相当する配列が得られる。

要素の追加

<<演算子は左辺の配列に右辺の要素を追加する。連続して複数の要素も追加可能。push()と同じ動作。

 

Ruby – 配列メソッド

概要

配列オブジェクトのメソッドのには破壊系と非破壊系があり、注意を要する。

パラメーター系

size/length~要素数の取得

sizeメソッド、lengthメソッドとも配列の要素数を返すエイリアス。

sum~合計値の取得

sumメソッドは配列要素の合計値を返す。

要素抽出系

first/last~先頭要素/末尾要素の取得

firstメソッドは配列の先頭の要素、lastメソッドは末尾の要素を返す。

sample~ランダムな要素取得

sampleメソッドは配列からランダムに1つ要素を返す。引数を指定すると重複なしでその個数分のサンプル配列を返すが、引数が要素数を超えた場合は全要素がランダムに並べられた配列が返される。

破壊的メソッド

push/unshift~要素の追加

pushメソッドは配列の末尾に要素を追加し、unshiftメソッドは配列の先頭に要素を追加する。いずれも元の配列を変更する(要素の追加は<<演算子でもできる)。

pop/shift~要素の取出し

popメソッドは配列の末尾から要素を取り出し、shiftメソッドは配列の先頭から要素を取り出す。いずれも元の配列が変更され、取り出された要素が戻り値となる。

非破壊的メソッド

reverse~要素の順番の反転

reverseメソッドは、元の配列の要素の順番を反転した配列を新たに生成して返す。

sort~昇順ソート

sortメソッドは、元の配列を昇順でソートした配列を生成して返す。文字列オブジェクトの場合は辞書順で、大文字→小文字の順。

sort.reverse~降順ソート

sortメソッドとreverseメソッドの組み合わせで、降順にソートされた配列が生成されて返される。

uniq~重複要素の削除

uniqは配列中の重複した要素を削除して1つにし、重複のない配列とする。元の配列は変更されず、新たな配列が生成される。

shuffle~要素のシャッフル

shuffleメソッドは、元の配列の要素をランダムに並べ替えた配列を生成して返す。

文字列化・配列化

split~文字列の配列への分解

splitメソッドは、指定した文字列で元の文字列を区切って、それぞれが要素となる配列を生成する。

join~配列要素の文字列への結合

joinメソッドは、配列の各要素を指定した文字列でつないだ文字列を生成する。

 

Ruby – 文字列メソッド

length~文字列の長さ

半角も全角も1文字。

結合系

+~文字列の連結

+演算子は文字列同士を結合する。半角と全角の連結もok。

join~配列要素の文字列化

配列のメソッドだが、joinメソッドは配列要素を指定した文字列で結合して文字列にする。

分割系

split~文字列の配列への分割

引数で指定した文字列で元の文字列を分解して配列化。

2つ目の例から、引数で指定した文字列"--"が見つかるたびに、そこまでの文字列を要素として配列に加えていることがわかる。

削除系

delete~特定の文字の削除

引数で指定した文字列を削除。

strip~前後のスペースの削除

半角スペースを削除。全角スペースは削除されない

chop/chop!~末尾文字の削除

chop/chop!は末尾文字を削除する。

chomp/chomp!~末尾の改行文字の削除

chomp/chomp!は末尾文字を削除する。

変換系

upcase/downcase~大文字化/小文字化

アルファベットの大文字化/小文字化。

 

PCA – LFWデータセット

概要

Scikit-learnで提供されているLFW peopleデータセットを、主成分分析を使って分析する。

データの読み込みと確認

LFWデータセットは世界の著名人の顔画像を、その名前とそれに対応するクラスデータとともに格納したものである。

書籍”Pythonではじめる機械学習”に沿って、画像サイズを0.7にし、20枚以上の画像がある人物を抽出する。

画像の人物は書籍と同じだが顔画像は異なっている。書籍執筆後画像データが追加/変更されたものと思われる。

画像の枚数の絞り込み

元のコード

LFW peopleの画像データは、人物によって枚数にばらつきがある(特にGeorge Bushだけ500枚を超えている)。画像データの多寡によるばらつきを抑えるため、書籍では画像の数を50枚までとし、それ以上の画像は切り落としている。

このコードがちょっとわかり難かったので、別にこちらで整理している

k-近傍法との組み合わせによる精度の確認

書籍では、画像を50枚以下に制限したデータについて、k-近傍法(knn)を適用したときのスコア、元データを主成分分析によって変換した場合のknnのスコアを確認している。

その過程をトレースしてみた

  • 画像データを最近傍データ1つで判定する1-nnの実行結果は、スコアは0.23と低い
  • 元の画像データを100個の主成分で変換したデータに対しては、1-nnのスコアは0.31と若干向上
  • PCAインスタンス生成時にwhiten=Trueを指定しない場合、PCA変換後もスコアは向上しなかった

主成分の可視化

PCA.fit()を実行すると、PCA.components_に主成分が格納される。components_は2次元配列で、[主成分の数, 元の特徴量数]という形になっている。たとえば今回のデータの場合、主成分の数はn_componentsで指定した100、特徴量の数は画像のピクセル数87×65=5655となり、components_は100×5655の2次元配列になっている。

(1)    \begin{equation*} \tt{components_} = \left[ \begin{array}{ccc} (p_{0, 0} & \cdots & p_{0, 5654} ) \\ & \vdots &\\ (p_{99, 0} & \cdots & p_{99, 5654}) \end{array} \right] = \left[ \begin{array}{c} \boldsymbol{p}_0 \\ \vdots \\ \boldsymbol{p}_{99} \end{array} \right] \end{equation*}

components_に収められた主成分はそれぞれが画像データと同じサイズの配列なので、これらを画像として表示させてみる。

たとえばComponent-0は顔と背景のコントラスト、Component-2は顔の左右の明るさの差をコーディングしているように見える、と書籍では解説している。その他にも、Component-5は目の下の出っ張った部分、Component-11は鼻筋のあたりを表現しているかもしれないといった想像はできる。

上の画像は以下のコードで表示させたが、要点は以下の通り。

  • 最低20枚の画像を持つ人物のみ読み込んでいる
  • 画像の最大数を50枚以下に制限している
  • 訓練データとテストデータに分割し、訓練データを主成分分析にかけている
  • components_プロパティーの主成分配列のうち、15行分を取り出して表示させている
  • 表示にあたって、リニアな5655の要素を画像の形(87, 65)に変形している
  • components_の形状が、100行×5655の2次元配列であることを確認

次元圧縮された主成分からの復元

概要

主成分の意味の一つとして、元のデータは主成分の線形和で表せるという解釈がある。

(2)    \begin{equation*} \boldsymbol{x} = (x_0, ..., x_n) = a_0 \boldsymbol{p}_0 + a_1 \boldsymbol{p}_1 + a_2 \boldsymbol{p}_2 + \cdots \end{equation*}

LFWの顔画像データで考えると、components_に収められた主成分の重みによって、元のそれぞれの人物の画像を再現しようとすることになる。

そこで、限られた主成分だけを用いて元の顔画像を再現してみる。

顔画像の選定

まず、特に有名な人物の顔画像をいくつか表示させてみた。選んだ人物は、Arnold Schwarzenegger, Tiger Woods, Vladimir Putinの3人。

これらの画像から、一旦次元削減して復元する画像を選ぶ。Shwalzzeneggerは正面少し左向きの31番、Tiger Woodsは少し右側から撮った歯を出している683番、Putinは左を向いた顔をほぼ正面から撮った372番を選んだ。

次元削減後の逆変換

そして次元数を変化させながらPCAモデルに全データを学習させ、それらのモデルで3枚の画像を変形し、逆変換する。

10個の主成分では、3人とも似たような顔になっているが、30個になると顔の方向や葉を出しているかどうかといった特徴が表れ始めている。

70個から100個にかけて、ShwaltzeneggerとWoodsはかなり元の顔に近いが、Putinはあまり判然としない。前者2人が「濃い」顔立ちなのに比べると、Putinの顔立ちは平板だということだろうか。

この画像は、以下の手順で作成した。

  1. 20枚以上の画像を持つ人物を選び、画像の枚数を50枚以下に制限
  2. 3人の顔画像について、次元数を10、30、70、100と変化させて以下を実行
    1. 設定された次元数で全データを学習
    2. 学習済みモデルで各顔画像を変換(ここで次元が削減される)
    3. 設定された次元数で元の顔画像に逆変換

同一人物の画像

さらに、3人について1人ずつ、3枚の顔画像について同様のことを行った結果が以下の通り。

Shwalzeneggerの後半2枚は向きが逆だが口元などがよく似ていて、目元と口元の特徴が強調されている。1枚目の画像はこの2枚と特徴が違うが、主成分30個あたりではよく似た感じともいえる。

Tiger Woodsも、主成分30個のところで173と683の画像が似ている。だが、535については一貫して他の2つと異なっているように見える。個人の特徴よりも顔の表情に大きく引きずられているようだ。

Putinは60と372の画像が割に似ているが、239の画像はかなり異なり、コントラストが強調されているようだ。60や372では、そもそも顔画像が平板なせいなのか、主成分を増やしても明確な画像が得られていない(他の人物との区別も難しいのではないだろうか)。

第2主成分までによるクラスの分布

第1主成分と第2主成分だけを使って、各クラスの分布をみてみる。62人の人物の各画像データが1つの点に対応している。2つの主成分だけでは人物が明確なクラスターとしては認識し難い(というよりもクラスが多すぎて識別も難しい)。

試しに表示するクラスを5つに限定してみる。やはり2つの主成分では明確なクラスターは確認できない。先ほどの変換・逆変換の結果でも、主成分10個でも個々の顔の識別は困難だったので、2つの主成分では難しいのは自明だが。

以上の可視化のコードは以下の通り。

 

Ruby – コメント

1行コメント~#

文頭に'#'があると、以降行末までコメントになる。

複数行コメント~=begin/=end

=begin=endの間の行は埋め込みドキュメントとして実行時には無視される。=begin=endはそれぞれ行頭になければならない。

__END__の応用

__END__はコードの終端を示し、それ以降の記述は実行対象にならない。