CUBE SUGAR CONTAINER

技術系のこと書きます。

Python: 主成分分析を重み付き和への分解と解釈した場合の可視化

読んでいる本の中に、主成分分析 (Principal Component Analysis; PCA) はデータを重み付き和に分解していると解釈することもできる、という記述があった。 なるほどーと思ったので、今回はそれについて試してみた。

使った環境は次のとおり。

$ sw_vers 
ProductName:    Mac OS X
ProductVersion: 10.15.7
BuildVersion:   19H15
$ python -V
Python 3.8.6

下準備

下準備として、あらかじめ必要なパッケージをインストールしておく。

$ pip install scikit-learn matplotlib

使うデータセットを確認する

今回は Labeled Faces in the Wild データセットを用いる。 これは、著名人の顔画像を切り抜いたデータセットになっている。

以下のサンプルコードでは、データセットの先頭 10 件の画像をプロットしている。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from sklearn.datasets import fetch_lfw_people
from matplotlib import pyplot as plt


def main():
    # Labeled Faces in the Wild データセット
    people = fetch_lfw_people(min_faces_per_person=20)

    # データセットの情報
    print(f'dataset shape: {people.images.shape}')
    print(f'number of classes: {len(people.target_names)}')

    # 先頭の 10 件をグレイスケールで表示してみる
    fig, axes = plt.subplots(2, 5,
                             figsize=(12, 6),
                             subplot_kw={'xticks': (),
                                         'yticks': ()})
    mappings = zip(people.target, people.images, axes.ravel())
    for target, image, ax in mappings:
        ax.imshow(image, cmap='gray')
        ax.set_title(people.target_names[target])

    plt.show()


if __name__ == '__main__':
    main()

上記を実行してみよう。

$ python lfw.py

すると、次のようなプロットが得られる。

f:id:momijiame:20201207001809p:plain
Labeled Faces in the Wild データセットの画像例

主成分分析したときの主軸 (Principal Axes) を可視化する

一般的に、主成分分析では得られた主成分得点 (Principal Component Score) に着目することが多い。 一方で、データを主成分得点に変換するときに用いる主軸 (Principal Axes) の情報も得られる。 試しに、主軸を主成分ごとに可視化してみよう。

以下のサンプルコードでは、先頭の 10 主成分について主軸をプロットしている。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from sklearn.datasets import fetch_lfw_people
from sklearn.decomposition import PCA
from matplotlib import pyplot as plt


def main():
    people = fetch_lfw_people(min_faces_per_person=20)

    print(f'dataset shape: {people.images.shape}')
    print(f'number of classes: {len(people.target_names)}')

    x, y = people.data, people.target
    image_shape = people.images.shape[1:]

    # PCA で先頭の 100 成分を取り出す
    pca = PCA(n_components=100,
              random_state=42)
    x_pca = pca.fit_transform(x)

    print(f'transformed shape: {x_pca.shape}')
    print(f'principal axes shape: {pca.components_.shape}')

    # 主軸 (Principal Axes) を可視化する
    fig, axes = plt.subplots(2, 5,
                             figsize=(12, 6),
                             subplot_kw={'xticks': (),
                                         'yticks': ()})
    mappings = zip(pca.components_, axes.ravel())
    for i, (component, ax) in enumerate(mappings):
        ax.imshow(component.reshape(image_shape))
        ax.set_title(f'components: {i + 1}')

    plt.show()


if __name__ == '__main__':
    main()

上記を実行してみよう。

$ python plotax.py  
dataset shape: (3023, 62, 47)
number of classes: 62
transformed shape: (3023, 100)
principal axes shape: (100, 2914)

すると、以下のようなプロットが得られる。

f:id:momijiame:20201207225939p:plain
主軸 (Principal Axes) の可視化

主成分得点と主軸から画像を再構成する

元のデータは、先ほど得られた主軸と主成分得点という二つの要素から再構成できる。 ここから、主成分分析はデータを主軸と主成分得点に分解していると解釈できる。

以下のサンプルコードでは使う主成分の数を変化させながら主成分得点と主軸を使って画像を再構成している。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from sklearn.datasets import fetch_lfw_people
from sklearn.decomposition import PCA
from matplotlib import pyplot as plt


def main():
    people = fetch_lfw_people(min_faces_per_person=20)

    print(f'dataset shape: {people.images.shape}')
    print(f'number of classes: {len(people.target_names)}')

    x, y = people.data, people.target
    image_shape = people.images.shape[1:]

    fig, axes = plt.subplots(5, 5,
                             figsize=(12, 6),
                             subplot_kw={'xticks': (),
                                         'yticks': ()})

    # 元画像をプロットする
    axes[0, 0].set_title('original image')
    for i in range(5):
        axes[i, 0].imshow(people.images[i],
                          cmap='gray')

    # 利用する主成分の次元ごとに処理する
    for i, n_components in enumerate([10, 50, 100, 500],
                                     start=1):
        # 主成分得点に変換する
        pca = PCA(n_components=n_components,
                  random_state=42)
        x_pca = pca.fit_transform(x)
        # 主成分得点を元の画像に逆変換する
        x_pca_reversed = pca.inverse_transform(x_pca)

        # 逆変換した画像をプロットする
        axes[0, i].set_title(f'{n_components} components')
        for j in range(5):
            ax = axes[j, i]
            ax.imshow(x_pca_reversed[j].reshape(image_shape),
                      cmap='gray')

    plt.show()


if __name__ == '__main__':
    main()

上記を実行してみよう。

$ python pcarev.py 
dataset shape: (3023, 62, 47)
number of classes: 62

すると、以下のようなプロットが得られる。

f:id:momijiame:20201207230507p:plain
主成分得点と主軸から再構成した画像

再構成に使う主成分の数が増えるほど、だんだんと鮮明な画像が得られていることがわかる。 ちなみに、今回使ったデータセットでは 500 主成分を使った場合に累積寄与率が 99% を越えていた。

とはいえ、再構成するのに scikit-learn の API を使うだけだと面白くない。 なので、以下のサンプルコードでは scikit-learn の API を使わずに画像を再構成している。 具体的には主成分得点と主軸のドット積を取った上で、元データの平均を足せば良い。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import numpy as np
from sklearn.datasets import fetch_lfw_people
from sklearn.decomposition import PCA
from matplotlib import pyplot as plt


def main():
    people = fetch_lfw_people(min_faces_per_person=20)

    print(f'dataset shape: {people.images.shape}')
    print(f'number of classes: {len(people.target_names)}')

    x, y = people.data, people.target
    image_shape = people.images.shape[1:]

    fig, axes = plt.subplots(5, 5,
                             figsize=(12, 6),
                             subplot_kw={'xticks': (),
                                         'yticks': ()})

    axes[0, 0].set_title('original image')
    for i in range(5):
        axes[i, 0].imshow(people.images[i],
                          cmap='gray')

    for i, n_components in enumerate([10, 50, 100, 500],
                                     start=1):
        pca = PCA(n_components=n_components,
                  random_state=42)
        x_pca = pca.fit_transform(x)
        # inverse_transform() を使わずに逆変換してみる
        x_pca_reversed = np.dot(x_pca, pca.components_) + pca.mean_

        # 逆変換した画像をプロットする
        axes[0, i].set_title(f'{n_components} components')
        for j in range(5):
            ax = axes[j, i]
            ax.imshow(x_pca_reversed[j].reshape(image_shape),
                      cmap='gray')

    plt.show()


if __name__ == '__main__':
    main()

上記を実行してみよう。

$ python pcarev2.py 
dataset shape: (3023, 62, 47)
number of classes: 62

先ほどと同様のプロットが得られる。

f:id:momijiame:20201207230752p:plain
主成分得点と主軸から再構成した画像 (scikit-learn API を使わない場合)

いじょう。