CUBE SUGAR CONTAINER

技術系のこと書きます。

Python: LightGBM の cv() 関数と SHAP を使ってみる

以前、このブログでは機械学習モデルの解釈可能性を向上させる手法として SHAP を扱った。

blog.amedama.jp

上記のエントリでは、LightGBM の train() 関数と共に、モデルの学習に使ったデータを解釈していた。 今度は cv() 関数を使って、Out-of-Fold なデータを解釈する例を試してみる。 つまり、モデルにとって未知のデータを、どのような根拠で予測をしているのかざっくり把握することが目的になる。

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

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

もくじ

下準備

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

$ pip install -U "lightgbm>3" shap scikit-learn pandas matplotlib

擬似的な二値分類用のデータを作って試してみる

前回のエントリと同じように、scikit-learn の API を使って擬似的な二値分類用のデータを作って試してみよう。 データは 100 次元あるものの、先頭の 5 次元しか分類には寄与する特徴になっていない。

サンプルコードを以下に示す。 基本的には cv() 関数から取り出した CVBooster を使って、Out-of-Fold な Prediction を作る要領で SHAP Value を計算するだけ。 あとは、それを好きなように可視化していく。 以下では、CVBooster を構成している各 Booster について summary_plot() で可視化した。

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

import numpy as np
import pandas as pd
import shap
import lightgbm as lgb
from sklearn.datasets import make_classification
from sklearn.model_selection import StratifiedKFold
from matplotlib import pyplot as plt


def main():
    # 疑似的な教師信号を作るためのパラメータ
    args = {
        # データ点数
        'n_samples': 10_000,
        # 次元数
        'n_features': 100,
        # その中で意味のあるもの
        'n_informative': 5,
        # 重複や繰り返しはなし
        'n_redundant': 0,
        'n_repeated': 0,
        # タスクの難易度
        'class_sep': 0.65,
        # 二値分類問題
        'n_classes': 2,
        # 生成に用いる乱数
        'random_state': 42,
        # 特徴の順序をシャッフルしない (先頭の次元が informative になる)
        'shuffle': False,
    }
    # 擬似的な二値分類用の教師データを作る
    dummy_x, dummy_y = make_classification(**args)
    # 一般的なユースケースを想定して Pandas のデータフレームにしておく
    col_names = [f'feature_{i:02d}'
                 for i in range(dummy_x.shape[1])]
    df = pd.DataFrame(data=dummy_x, columns=col_names)
    df['target'] = pd.Series(data=dummy_y, dtype=bool)
    # 教師データ
    train_x, train_y = df.drop(['target'], axis=1), df.target

    # データの分割方法は Stratified 5-Fold CV
    folds = StratifiedKFold(n_splits=5,
                            shuffle=True,
                            random_state=42,
                            )
    # LightGBM のデータセット形式にする
    lgb_train = lgb.Dataset(train_x, train_y)
    # 学習時のパラメータ
    lgb_params = {
        'objective': 'binary',
        'metric': 'binary_logloss',
        'first_metric_only': True,
        'verbose': -1,
    }
    # 学習する
    cv_result = lgb.cv(params=lgb_params,
                       train_set=lgb_train,
                       folds=folds,
                       num_boost_round=10_000,
                       early_stopping_rounds=100,
                       verbose_eval=10,
                       return_cvbooster=True,
                       )
    # CVBooster を取り出す
    cvbooster = cv_result['cvbooster']

    # Out-of-Fold なデータで SHAP Value を計算する
    split_indices = list(folds.split(train_x, train_y))
    cv_shap_values = np.zeros_like(train_x, dtype=np.float)
    # Booster と学習に使ったデータの対応関係を取る
    booster_split_mappings = list(zip(cvbooster.boosters,
                                      split_indices))
    for booster, (_, val_index) in booster_split_mappings:
        # Booster が学習に使っていないデータ
        booster_val_x = train_x.iloc[val_index]
        # SHAP Value を計算する
        explainer = shap.TreeExplainer(booster,
                                       model_output='probability',
                                       data=booster_val_x)
        train_x_shap_values = explainer.shap_values(booster_val_x)
        cv_shap_values[val_index] = train_x_shap_values

    # 各教師データに対応する SHAP Value さえ計算できれば、後は好きに可視化すれば良い
    # 試しに各 Booster の summary_plot を眺めてみる
    plt.figure(figsize=(12, 16))
    for i, (_, val_index) in enumerate(split_indices):
        # 描画位置
        plt.subplot(3, 2, i + 1)
        # 各 Booster の Explainer が出力した SHAP Value ごとにプロットしていく
        shap.summary_plot(shap_values=cv_shap_values[val_index],
                          features=train_x.iloc[val_index],
                          feature_names=train_x.columns,
                          plot_size=None,
                          show=False)
        plt.title(f'Booster: {i}')
    plt.show()

    # 1 つのグラフにするなら、これだけで良い
    """
    shap.summary_plot(shap_values=cv_shap_values,
                      features=train_x,
                      feature_names=train_x.columns)
    """


if __name__ == '__main__':
    main()

上記を保存したら、実行してみよう。 パフォーマンス的には、SHAP Value の計算部分を並列化すると、もうちょっと速くできそう。

$ python lgbcvshap.py
[10]    cv_agg's binary_logloss: 0.417799 + 0.00797668
[20]    cv_agg's binary_logloss: 0.333266 + 0.0108777
[30]    cv_agg's binary_logloss: 0.30215 + 0.0116352
...(省略)...
[220]   cv_agg's binary_logloss: 0.282142 + 0.0185382
[230]   cv_agg's binary_logloss: 0.283128 + 0.0188667
[240]   cv_agg's binary_logloss: 0.284197 + 0.0195125
 98%|===================| 1951/2000 [00:29<00:00]

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

f:id:momijiame:20210108185439p:plain
各 Booster の SHAP Value を可視化したチャート

基本的には、どの Booster の結果も、先頭 5 次元の SHAP Value が大きくなっていることが見て取れる。

いじょう。

Ubuntu 20.04 LTS に後から GUI (X Window System) を追加する

Ubuntu 20.04 LTS のサーバ版をインストールした場合には、デフォルトでは GUI 環境が用意されない。 しかし、後から必要になる場合もある。 今回は、そんなときどうするかについて。 なお、必要な操作は Ubuntu 18.04 LTS の場合と変わらなかった。

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

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.1 LTS
Release:    20.04
Codename:   focal
$ uname -r
5.4.0-59-generic

もくじ

下準備

あらかじめ、リポジトリの情報を更新しておく。

$ sudo apt-get update

デスクトップ環境が必要なとき

デスクトップ環境が必要なときは ubuntu-desktop パッケージをインストールする。

$ sudo apt-get -y install ubuntu-desktop

かなり色々と入るので気長に待つ必要がある。

インストールが終わったら再起動する。

$ sudo shutdown -r now

うまくいけば自動的にデスクトップ環境が有効な状態で起動してくる。

f:id:momijiame:20210106073254p:plain
Ubuntu 20.04 LTS のデスクトップ環境

X Window System だけで良いとき

デスクトップ環境は必要なく、単純に X Windows System だけで良いときは xserver-xorg パッケージをインストールする。 これは、たとえば X11 Forwarding を使って、リモートで起動したアプリケーションの画面をローカルに出したいときに多い。

$ sudo apt-get -y install xserver-xorg

X Windows System を使うアプリケーションとして xeyes などを起動して動作確認する。

$ sudo apt-get -y install x11-apps
$ xeyes

f:id:momijiame:20210106072626p:plain
xeyes

いじょう。

普段使ってるオーラルケア用品について

このブログでは、年に何回か技術系じゃないエントリも書いているんだけど、今回もそれ。 普段使っているオーラルケア用品で、これは良いなと思っているものを理由と一緒に書いていく。 なお、完全なる主観に過ぎないことをあらかじめ断っておきます。

マウスウォッシュ

はじめにマウスウォッシュから。 ジャンルとしては液体ハミガキ粉に分類されるのかな。 歯磨きをする前に口に含んで、ゆすいでから吐き出すやつ。 普段はリステリンの紫 (トータルケア) を使っている。

初めて使うときは以下の低刺激タイプから入るのが良さそう。

以下は口に含むと口の中がブワッてなるので最初は驚くかも。

効果としては、使うと口臭が抑えられる感じがある。

電動歯ブラシ

歯医者さんの検診の歯磨き指導で毎回磨き残しがあると言われ続けて、ついには磨く時間が 5 分を越えたときに「これ以上は手でやるのムリ」と判断して購入したのが電動歯ブラシ。 導入後の指導では「まあ良いでしょう」というコメントをもらえた。

今は、もう廃版になっているフィリップスのモデルを買ってから使い続けている。 他のメーカーは使ったことがないのでわからない。 ただ、選定する上では先端の交換用ブラシのアタッチメントの互換性とか、値段は考慮した方が良いかも。 ランニングコストに関わってくるので。 とりあえず、フィリップスに関しては 10 年くらいアタッチメントの互換性は保たれている。 交換用のブラシは一本あたり千円くらいはする。

フロス

歯ブラシだけだと歯の間の掃除はできない、みたいなことはよく聞くけど本当にそうだと思う。 丁寧に磨いた後でも、フロスを使うと口をゆすいだときに食べかすが出てくる。

リーチデンタルフロス ワックス 18M

リーチデンタルフロス ワックス 18M

  • メディア: ヘルスケア&ケア用品

あと、最初使い始めた頃はフロスが当たった歯肉から血が出てきて驚いた。 結局のところ、それは歯肉炎になっているのが原因だったのだと思う。 使い続けると血は出なくなるし、歯肉が引き締まる感じがある。 使ってない人には是非使ってもらいたい。

歯みがき粉

歯みがき粉はマウスウォッシュとは別に、目的に合わせて 2 種類を使い分けている。

ひとつは以下のビリリアントモアっていうやつで、これはホワイトニング効果の高いもの。 リモートワークが始まってコーヒーと紅茶の消費量が増えて、歯のステインに悩んでいたんだけど、これのおかげで解消した。 ステインが気になるところを、これを付けた電動歯ブラシで重点的に磨くと本当にキレイになる。 最初は連続して使っていたけど、今は週に一回くらいのペースで使っている。

もうひとつは知覚過敏を防ぐ効果のあるシュミテクトを使っている。 冷たいものを食べるとしみるのが気になって使い始めたんだけど、最近はそれなりにマシになっている気がしないでもない。 いや、もしかすると気のせいかも。 ブリリアントモアを使わない日はこちらを使っている。

キシリトールタブレット

これはちょっと番外編かもしれないけど、テーブルにキシリトールタブレットを置いといて、気が向いたときにポリポリと食べてる。 きっかけは子どもの歯みがきトレーニングでご褒美に使えるかなと考えて、大人が味見がてら買ってから続いている。 ほのかな甘さでなかなか美味しい。

ハキラ ブルーベリー 45粒

ハキラ ブルーベリー 45粒

  • メディア: ヘルスケア&ケア用品

子どもにも奥歯が生えたら、歯みがきのご褒美にあげようと思う。

まとめ

とりあえず今フロスを使っていないなら是非使ってもらいたい。 面倒くさがりで時短したいなら電動ハブラシはおすすめ。 口臭ケアが気になってる人はリステリン良いと思う。 歯磨き粉は用途に合わせて。

そんなかんじで。

Docker でコンテナにマウントできるボリュームについて

Docker では、ボリュームという機能を使うことで、ホストや外部のストレージをコンテナにマウントできる。 今回は、それらについてざっと使い方を見ていく。 紹介するボリュームの種類は次のとおり。

  • bind mount
  • volume mount
  • tempfs mount

使った環境は以下のとおり。

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 18.04.5 LTS
Release:    18.04
Codename:   bionic
$ uname -r
4.15.0-124-generic
$ docker version
Client: Docker Engine - Community
 Version:           20.10.1
 API version:       1.41
 Go version:        go1.13.15
 Git commit:        831ebea
 Built:             Tue Dec 15 04:34:59 2020
 OS/Arch:           linux/amd64
 Context:           default
 Experimental:      true

Server: Docker Engine - Community
 Engine:
  Version:          20.10.1
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.13.15
  Git commit:       f001486
  Built:            Tue Dec 15 04:32:40 2020
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.4.3
  GitCommit:        269548fa27e0089a8b8278fc4fc781d7f65a939b
 runc:
  Version:          1.0.0-rc92
  GitCommit:        ff819c7e9184c13b7c2607fe6c30ae19403a7aff
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0

もくじ

bind mount

はじめに、従来からある bind mount について。 このやり方では、コンテナの起動時に Docker ホストのディレクトリパスを指定すると、そのディレクトリをコンテナにマウントできる。

たとえば、カレントディレクトリに確認用のテキストファイルを書き込んでおこう。

$ echo "Hello, World" > greet.txt

bind mount するときは、一般的には -v または --volume オプションを使うことが多い。 このオプションでは、左辺にマウントしたい Docker ホストのディレクトリを絶対パスで指定する。 そして、コロンを挟んだ右辺にコンテナでマウント先のディレクトリを指定する。

以下では、カレントディレクトリをコンテナの /mnt にマウントしている。

$ docker container run \
    --rm \
    -v $(pwd):/mnt \
    -it ubuntu:18.04 \
    bash

起動したコンテナの中で /mnt 以下を確認すると、先ほど作ったテキストファイルが確認できる。

root@4829d9a83fe9:/# cat /mnt/greet.txt 
Hello, World

ただし、最近では --mount というオプションも使える。 このオプションでは、マウントの種類とマウント元、マウント先を指定することで、より明示的な指示になっている。

$ docker container run \
    --rm \
    --mount type=bind,source=$(pwd),destination=/mnt \
    -it ubuntu:18.04 \
    bash

結果は先ほどと変わらないので割愛する。

volume mount

続いては volume mount というやり方を扱う。 bind mount ではマウント元のディレクトリを指定していた。 これは直感的な反面、マウントするディレクトリの管理が煩雑になるデメリットがある。 一方、volume mount では、Docker が管理している領域にマウント用のディレクトリが作られて、それを名前で指定できる。

たとえば、以下のようにしてボリュームを作成しておく。 ここでは example-volume という名前でボリュームを作った。

$ docker volume create example-volume

ボリュームの一覧を見ると、ちゃんとボリュームが作成されている。 ここで着目すべき点として DRIVER というカラムがある。 先ほど作ったボリュームでは、値が local になっている。 これは、ドライバを変更することで Docker ホスト以外の場所もボリュームとして管理できることを表している。 公式のドキュメントでも SSHFS や NFS を使ってコンテナ間でストレージを共有する方法が示されている。

$ docker volume ls
DRIVER    VOLUME NAME
local     example-volume

volume mount するときは、bind mount と同じように -v オプションが使える。 先ほどは左辺に Docker ホストのディレクトリを指定したのに対し、今回はボリュームの名前を指定する。

$ docker container run \
    --rm \
    -v example-volume:/mnt \
    -it ubuntu:18.04 \
    bash

あるいは、--mount オプションを使っても良い。 こちらの場合は typebind ではなく volume を指定した上で、source にボリュームの名前を指定する。

$ docker container run \
    --rm \
    --mount type=volume,source=example-volume,destination=/mnt \
    -it ubuntu:18.04 \
    bash

初期状態では、マウントされたディレクトリにファイルが何もない。 そこで、一旦は確認用のファイルを書き込んでコンテナを終了しておこう。

root@10cc21f927a7:/# ls /mnt
root@10cc21f927a7:/# echo "Hello, World" > /mnt/greet.txt
root@10cc21f927a7:/# exit
exit

そして、改めて同じボリュームをマウントするコンテナを起動する。

$ docker container run \
    --rm \
    --mount type=volume,source=example-volume,destination=/mnt \
    -it ubuntu:18.04 \
    bash

すると、次のように先ほど作成したファイルが存在することが確認できる。

root@324a3d594723:/# ls /mnt
greet.txt
root@324a3d594723:/# cat /mnt/greet.txt 
Hello, World

ちなみに、ボリュームの場所は次のようにして調べることができる。 まずはコンテナの識別子か名前を確認しておく。

$ docker ps
CONTAINER ID   IMAGE          COMMAND   CREATED          STATUS          PORTS     NAMES
324a3d594723   ubuntu:18.04   "bash"    59 seconds ago   Up 58 seconds             reverent_golick

そして、docker inspect を使えばボリュームの情報が確認できる。 これで、"/var/lib/docker/volumes/example-volume/_data" にマウント元のディレクトリがあることがわかった。

$ docker inspect 324a3d594723 | grep example-volume
                "example-volume:/mnt"
                "Name": "example-volume",
                "Source": "/var/lib/docker/volumes/example-volume/_data",

Docker ホスト側でディレクトリの内容を確認しておこう。 ちゃんとファイルが存在することがわかる。

$ sudo ls /var/lib/docker/volumes/example-volume/_data
greet.txt
$ sudo cat /var/lib/docker/volumes/example-volume/_data/greet.txt
Hello, World

tmpfs mount

続いては、これまでとちょっと毛色が異なる tmpfs mount を紹介する。 tmpfs は Linux のファイルシステムの一種で、主記憶装置を使った (つまりオンメモリの) ファイルシステムになっている。 もちろん、オンメモリということはデータは永続化されず揮発する。

tmpfs mount をするときは、専用のオプションとして --tmpfs が使える。 指定するのはコンテナのマウント先だけ。

$ docker container run \
    --rm \
    --tmpfs /mnt \
    -it ubuntu:18.04 \
    bash

あるいは、--mount オプションで typetmpfs を指定しても良い。

$ docker container run \
    --rm \
    --mount type=tmpfs,destination=/mnt \
    -it ubuntu:18.04 \
    bash

起動したコンテナを確認すると、/mnt ディレクトリに tmpfs でマウントされている。

root@71b7c20cbcb8:/# df -h | grep /mnt
tmpfs           7.9G     0  7.9G   0% /mnt

dd(1) を使ってちょっと大きめのファイルを書くこんでみると、別の場所に比べてスループットが高いことが確認できる。

root@71b7c20cbcb8:/# dd if=/dev/zero of=/mnt/zeros bs=1MB count=2000
2000+0 records in
2000+0 records out
2000000000 bytes (2.0 GB, 1.9 GiB) copied, 0.651633 s, 3.1 GB/s
root@71b7c20cbcb8:/# dd if=/dev/zero of=/zeros bs=1MB count=2000
2000+0 records in
2000+0 records out
2000000000 bytes (2.0 GB, 1.9 GiB) copied, 1.93574 s, 1.0 GB/s

いじょう。

参考

docs.docker.com

Python: GPU を使う主要なパッケージで CUDA が有効か確かめる方法について

CUDA を利用する Python のパッケージは、多くの場合それ自体のバージョンだけでなく対応している CUDA Toolkit や cuDNN のバージョンまで気にする必要がある。 なんだか環境やコンテナイメージを作るたびに確認する方法や互換性について調べている気がするので、以下のパッケージについてまとめておく。

  • PyTorch
  • TensorFlow (2.x)
  • CuPy
  • CuDF

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

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 18.04.5 LTS
Release:    18.04
Codename:   bionic
$ uname -r
4.15.0-124-generic
$ python -V       
Python 3.8.6
$ pip list | egrep -ie "^(torch|tensorflow|cupy|cudf) "
cudf                      0.17.0             
cupy                      8.1.0              
tensorflow                2.4.0              
torch                     1.7.1

もくじ

下準備

あらかじめ各種パッケージをインストールして Python の REPL を起動しておく。

$ python

PyTorch

PyTorch では、次のようにする。

>>> import torch
>>> torch.cuda.is_available()
True

CUDA との互換性を調べる方法

最新バージョンについては以下のページを参照する。

pytorch.org

以前のバージョンを調べたいときは以下。

pytorch.org

TensorFlow (2.x)

TensorFlow では以下のようにする。

>>> import tensorflow as tf
>>> len(tf.config.list_physical_devices('GPU')) > 0
True

CUDA との互換性を調べる方法

以下のページを参照する。 日本語のページは翻訳の関係で新しいバージョンが表示されないことがあるため注意する。

www.tensorflow.org

CuPy

CuPy はインポートでエラーにならなければ使える。

>>> import cupy as cp

あえて関数にするとしたら、こんな感じ?

def cupy_is_available():
    try:
        import cupy as cp
        return True
    except ImportError:
        return False

CUDA との互換性を調べる方法

CuPy は CUDA のバージョンごとにバイナリが用意されているので、それを使えば良い。

github.com

CuDF

CuDF も、インポートでエラーにならなければ使えるはず。

>>> import cudf

少なくとも現行の安定バージョン (0.17) では、ソースコードを確認するとインポートするタイミングでチェックが走っているようなので。

github.com

CUDA との互換性を調べる方法

互換性を調べるときは以下のインストールページを確認するのが良いのかな。

rapids.ai

いじょう。

MSI GeForce RTX 3090 VENTUS 3X 24G OC グラフィックスボード VD7357

MSI GeForce RTX 3090 VENTUS 3X 24G OC グラフィックスボード VD7357

  • 発売日: 2020/10/09
  • メディア: Personal Computers

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 を使わない場合)

いじょう。

動作中の Docker コンテナからイメージを作る

Docker コンテナを使って技術検証をしているときに、色々と試行錯誤している場面では、ある手順から作業をやり直したくなることがある。 すべての作業がすぐに終わるなら特に問題にはならないものの、時間がかかる場合には初めからやり直したときに大きなロスが生じる。 そんなときは、動作中の Docker コンテナから Docker イメージを作ることで時間の節約ができる。 具体的には docker commit サブコマンドを使うことで実現できる。

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

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.15.7
BuildVersion:   19H15
$ docker version                     
Client: Docker Engine - Community
 Cloud integration: 1.0.2
 Version:           19.03.13
 API version:       1.40
 Go version:        go1.13.15
 Git commit:        4484c46d9d
 Built:             Wed Sep 16 16:58:31 2020
 OS/Arch:           darwin/amd64
 Experimental:      false

Server: Docker Engine - Community
 Engine:
  Version:          19.03.13
  API version:      1.40 (minimum version 1.12)
  Go version:       go1.13.15
  Git commit:       4484c46d9d
  Built:            Wed Sep 16 17:07:04 2020
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          v1.3.7
  GitCommit:        8fba4e9a7d01810a393d5d25a3621dc101981175
 runc:
  Version:          1.0.0-rc10
  GitCommit:        dc9208a3303feef5b3839f4323d9beb36df0a9dd
 docker-init:
  Version:          0.18.0
  GitCommit:        fec3683

もくじ

作業中を模した Docker コンテナを用意する

とりあえず、何でも構わないのでコンテナを立ち上げておく。

$ docker container run \
  --rm \
  -it ubuntu:18.04 \
  bash

立ち上げた以下のコンテナを、何らかの検証で作業中のコンテナに見立てる。

root@f499b0f5b72b:/#

たとえば、次のようにファイルを書き出しておこう。 このファイルがあることで、作業が途中まで完了していることの目印とする。

root@f499b0f5b72b:/# echo "Hello, World" > /var/tmp/greet.txt

動作中のコンテナからイメージを作る

Docker ホスト側で、お目当てのコンテナが動作していることを確認する。 別にコンテナが終了していても構わないんだけど、今回は --rm オプションをつけているのでコンテナは終了すると消えてしまう。

$ docker container ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
f499b0f5b72b        ubuntu:18.04        "bash"              31 seconds ago      Up 29 seconds                           eloquent_keller

それでは、docker commit コマンドを使って、コンテナからイメージを作ろう。 最初の引数にお目当てとなるコンテナの ID ないし名前を指定して、次の引数にコンテナを元ネタにしたイメージの名前を指定する。

$ docker commit f499b0f5b72b example
sha256:0bbb09b0e9a30bfd0ba7a342eddb7bbcc6380b0542a1379c7c8e54587e24392a

イメージの一覧を確認すると、ちゃんと指定した名前でイメージができている。

$ docker image list | grep example
example                              latest                                           0bbb09b0e9a3        14 seconds ago      64.2MB

作ったイメージからコンテナを起動する

上記のイメージを使ってコンテナを立ち上げてみよう。

$ docker container run \
  --rm \
  -it example \
  bash

イメージの元ネタになったコンテナにあったはずのファイルを確認すると、次のようにちゃんとある。 つまり、作業の途中からやり直すことができている。

root@a05ba64a868b:/# cat /var/tmp/greet.txt 
Hello, World

めでたしめでたし。

参考

docs.docker.com