CUBE SUGAR CONTAINER

技術系のこと書きます。

Python: TensorFlow/Keras で Entity Embedding を試してみる

ニューラルネットワークでカテゴリ変数を扱う方法としては One-Hot エンコーディングがある。 しかし、One-Hot エンコーディングでは特徴量のカーディナリティが高いと扱う次元数が大きくなる。 そこで、今回紹介する Entity Embedding を使うと、ラベルエンコードしたカテゴリ変数をニューラルネットワークで扱うことができるようになる。 Entity Embedding では、Embedding 層を使うことでカテゴリ変数ごとにパラメータの重み (分散表現) を学習する。 今回は TensorFlow/Keras で Entity Embedding を試してみる。

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

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

下準備

あらかじめ、使用するパッケージをインストールしておく。

$ pip install tensorflow scikit-learn sklearn-pandas seaborn

Embedding 層について

Entity Embedding を理解する上では、まず Embedding 層について理解する必要がある。 Keras では Embedding 層の実装として tensorflow.keras.layers.Embedding が使える。

keras.io

上記を実装するには tensorflow.nn.embedding_lookup() という関数が使われている。

www.tensorflow.org

そこで、まずはこの関数の振る舞いについて見ていくことにしよう。 はじめに Python のインタプリタを起動する。

$ python

TensorFlow をインポートしたら、例として次のような 5 行 2 列の Tensor オブジェクトを用意する。

>>> import tensorflow as tf
>>> tensor = tf.constant([
...     [1, 2],
...     [3, 4],
...     [5, 6],
...     [7, 8],
...     [9, 10],
... ])
>>> tensor
<tf.Tensor: shape=(5, 2), dtype=int32, numpy=
array([[ 1,  2],
       [ 3,  4],
       [ 5,  6],
       [ 7,  8],
       [ 9, 10]], dtype=int32)>

上記の Tensor オブジェクトに対してtf.nn.embedding_lookup() 関数を使ってみよう。 この関数を使ってできることは単純で、行のインデックス番号を指定すると、その行を取り出すことができる。 例えば、次のように 0, 2, 3 をリストで渡すと、その行に対応した要素を取り出すことができる。

>>> tf.nn.embedding_lookup(tensor, [0, 2, 3])
<tf.Tensor: shape=(3, 2), dtype=int32, numpy=
array([[1, 2],
       [5, 6],
       [7, 8]], dtype=int32)>

先ほど作った Tensor オブジェクトから 0, 2, 3 行目が取り出されていることがわかる。

できることがわかったところで、これの何が嬉しいのか考えてみよう。 先ほど作った Tensor オブジェクトで、行がカテゴリ変数の各カテゴリ、列がそのカテゴリの重み (分散表現) に対応すると捉えたらどうだろうか。 カテゴリ変数を扱う上で、One-Hot にしなくても、ラベルエンコードしたカテゴリ変数についてインデックス番号ごとに分散表現を管理できることがわかる。

念の為、tensorflow.keras.layers.Embedding の挙動についても確認しておこう。 先ほど作った Tensor オブジェクトと同じ重みを持った Embedding オブジェクトを次のように作る。

>>> initializer = tf.keras.initializers.Constant(
    value=[
        [1, 2],
        [3, 4],
        [5, 6],
        [7, 8],
        [9, 10],
    ]
)
>>> embedding_layer = tf.keras.layers.Embedding(5, 2, embeddings_initializer=initializer)

あとは入力として、先ほど tf.nn.embedding_lookup() 関数の引数に使ったのと同じインデックス番号を渡してみよう。

>>> embedding_layer(inputs=tf.constant([0, 2, 3]))
<tf.Tensor: shape=(3, 2), dtype=float32, numpy=
array([[1., 2.],
       [5., 6.],
       [7., 8.]], dtype=float32)>

すると、ちゃんとインデックス番号に対応した重みが出力として得られることがわかる。

カテゴリ変数を Entity Embedding で扱う MLP を書いてみる

続いては、実際にカテゴリ変数を Entity Embedding で扱うサンプルコードを見てみる。 以下では、Diamonds データセットを使って、3 層の MLP (Multi Layer Perceptron) で価格 (price) を推定する回帰問題を学習している。 Diamonds データセットには、カテゴリ変数として cutcolorclarity という特徴量がある。 今回は、それらのカテゴリ変数をラベルエンコードした数値として受け取って、2 次元の分散表現として学習させた。 モデルの評価は 3-Fold CV で、Out-of-Fold で推定した値を RMSLE で最後に出力している。

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

import numpy as np
import tensorflow as tf
from sklearn.metrics import mean_squared_log_error
from sklearn_pandas import DataFrameMapper
from sklearn.model_selection import KFold
from sklearn.preprocessing import LabelEncoder
from sklearn.preprocessing import MinMaxScaler
import seaborn as sns
from matplotlib import pyplot as plt


class MultiLayerPerceptronWithEntityEmbedding(tf.keras.Model):
    """カテゴリ変数を Entity Embedding で扱う MLP"""

    def __init__(self):
        super().__init__()

        # cut を Entity Embedding する層
        self.cut_embedding = tf.keras.Sequential([
            tf.keras.layers.Input(shape=(1,)),
            tf.keras.layers.Embedding(5, 2),
            tf.keras.layers.Flatten(),
        ], name='cut_embedding')

        # color を Entity Embedding する層
        self.color_embedding = tf.keras.Sequential([
            tf.keras.layers.Input(shape=(1,)),
            tf.keras.layers.Embedding(7, 2),
            tf.keras.layers.Flatten(),
        ], name='color_embedding')

        # clarity を Entity Embedding する層
        self.clarity_embedding = tf.keras.Sequential([
            tf.keras.layers.Input(shape=(1,)),
            tf.keras.layers.Embedding(8, 2),
            tf.keras.layers.Flatten(),
        ], name='clarity_embedding')

        # MLP 層
        self.mlp = tf.keras.Sequential([
            tf.keras.layers.Dense(64,
                                  activation=tf.keras.activations.relu),
            tf.keras.layers.Dropout(0.25),
            tf.keras.layers.Dense(32,
                                  activation=tf.keras.activations.relu),
            tf.keras.layers.Dropout(0.25),
            tf.keras.layers.Dense(1),
        ], name='mlp')

    def call(self, inputs):
        # cut の Embedding
        cut = inputs[:, 0]
        cut_embed = self.cut_embedding(cut)

        # color の Embedding
        color = inputs[:, 1]
        color_embed = self.color_embedding(color)

        # clarity の Embedding
        clarity = inputs[:, 2]
        clarity_embed = self.clarity_embedding(clarity)

        # 残りの特徴量と連結する
        concat_layers = [
            cut_embed,
            color_embed,
            clarity_embed,
            inputs[:, 3:],
        ]
        x = tf.keras.layers.Concatenate()(concat_layers)

        # MLP 層に入れる
        x = self.mlp(x)

        return x


def main():
    # Diamonds データセットを読み込む
    df = sns.load_dataset('diamonds')
    x, y = df.drop(['price'], axis=1), df['price']

    mapper = DataFrameMapper([
        # カテゴリ変数はラベルエンコードする
        ('cut', LabelEncoder()),
        ('color', LabelEncoder()),
        ('clarity', LabelEncoder()),
        # 連続変数は Min-Max スケーリングで標準化する
        (['carat', 'depth', 'table', 'x', 'y', 'z'], MinMaxScaler()),
    ], df_out=True)

    # エンコードする
    x_transformed = mapper.fit_transform(x)
    print(x_transformed.head())

    models = []
    folds = KFold(n_splits=3, shuffle=True, random_state=42)
    for train_index, val_index in folds.split(x_transformed, y):
        train_x = x_transformed.iloc[train_index]
        train_y = y.iloc[train_index]
        valid_x = x_transformed.iloc[val_index]
        valid_y = y.iloc[val_index]

        # タスクを定義する
        model = MultiLayerPerceptronWithEntityEmbedding()
        optimizer = tf.keras.optimizers.Adam(learning_rate=1e-2)
        loss_function = tf.keras.losses.MeanSquaredLogarithmicError()
        model.compile(optimizer=optimizer,
                      loss=loss_function)
        model.build((None,) + train_x.shape[1:])

        # 学習に使うコールバック
        checkpoint_filepath = '/tmp/keras-checkpoint'
        callbacks = [
            # 検証データに対する損失が一定のエポック数の間に改善しないときは学習を打ち切る
            tf.keras.callbacks.EarlyStopping(monitor='val_loss',
                                             patience=50,
                                             verbose=1,
                                             mode='min'),
            # モデルを記録する
            tf.keras.callbacks.ModelCheckpoint(monitor='val_loss',
                                               filepath=checkpoint_filepath,
                                               save_best_only=True,
                                               verbose=1,
                                               mode='min'),
        ]

        # 学習する
        model.fit(train_x, train_y,
                  batch_size=1024,
                  epochs=1_000,
                  verbose=1,
                  validation_data=(valid_x, valid_y),
                  callbacks=callbacks,
                  )

        # 最良のモデルを読み込む
        model.load_weights(checkpoint_filepath)
        models.append(model)

    y_preds = np.zeros_like(y, dtype=np.float64)
    model_fold_mappings = zip(models, folds.split(x_transformed, y))
    for model, (_, val_index) in model_fold_mappings:
        # Out-of-Fold で予測を作る
        valid_x = x_transformed.iloc[val_index]
        y_pred = model.predict(valid_x)
        y_preds[val_index] = y_pred.ravel()

    rmsle = np.sqrt(mean_squared_log_error(y, y_preds))
    print(f'Root Mean Squared Log Error: {rmsle}')

    # predicted vs actual plot
    plt.scatter(y_preds, y)
    y_max = max(max(y_preds), max(y))
    plt.plot([0, y_max], [0, y_max], color='r')
    plt.xlabel('Predicted')
    plt.ylabel('Actual')
    plt.show()


if __name__ == '__main__':
    main()

上記を実行してみよう。 なお、データもネットワークも小さいので CPU で十分処理できる。

$ python entityembed.py
...
Root Mean Squared Log Error: 0.11266712415049261

回帰問題は評価指標だけだとイマイチイメージがつきにくいので真の価格と推定した価格の散布図も示す。

f:id:momijiame:20210115015343p:plain
真の価格と Out-of-Fold で推定した価格のプロット

とりあえず、カテゴリ変数を One-Hot しなくても学習できていることがわかった。 これができると、特徴量を扱う処理でもニューラルネットワーク用とそれ以外の差分が小さくなる余地ができてありがたい。

補足

今回使った Diamonds データセットについては、以下の理由からあえて Entity Embedding する必要はないかなという話もある。

  • カテゴリ変数のカーディナリティが小さいので One-Hot エンコーディングでも十分に扱える
  • カテゴリ変数が名義尺度ではなく順序尺度なので、順序にもとづいて Ordinal エンコーディングすることもできる

なお、軽く検証した限りでは、今回のデータとネットワークと評価指標だと性能は One-Hot Encoding > Entity Embedding > Ordinal Encoding という感じだった。 Ordinal Encoding がイマイチ効かないのは、順序尺度毎の寄与がすべて一定という仮定を置くことになって、それが誤りだからなのかな。

コード的な差分は、One-Hot エンコードに関しては前処理を次のようにする。

    mapper = DataFrameMapper([
        # カテゴリ変数は One-Hot エンコードする
        ('cut', LabelBinarizer()),
        ('color', LabelBinarizer()),
        ('clarity', LabelBinarizer()),
        (['carat', 'depth', 'table', 'x', 'y', 'z'], MinMaxScaler()),
    ], df_out=True)

Ordinal エンコードであれば、こう。

    mapper = DataFrameMapper([
        # カテゴリ変数を Ordinal エンコードした後に標準化する
        (['cut', 'color', 'clarity'], [
            OrdinalEncoder(categories=[
                ['Ideal', 'Premium', 'Very Good', 'Good', 'Fair'],
                ['D', 'E', 'F', 'G', 'H', 'I', 'J'],
                ['IF', 'VVS1', 'VVS2', 'VS1', 'VS2', 'SI1', 'SI2', 'I1'],
            ]),
            MinMaxScaler(),
        ]),
        (['carat', 'depth', 'table', 'x', 'y', 'z'], MinMaxScaler()),
    ], df_out=True)

そして、モデルは MLP 層だけにして上記の出力をそのまま突っ込んでやれば良い。

参考文献

元ネタの論文では、Entity Embedding のメリットとして扱う次元数の減少や学習の高速化以外についても触れられている。

arxiv.org

いじょう。

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

いじょう。