Persistence Codebook を使った機械学習¶

このチュートリアルは Persistence Image を使った機械学習の一通りを済ませたことを仮定して進めます。 重複した内容は説明しません。

In [1]:
import os  # for makedirs
import homcloud.interface as hc  # HomCloud 
import homcloud.interface.codebook  # HomCloud's implementation of persistence codebook
import numpy as np  # Numerical array library
from tqdm.notebook import tqdm  # For progressbar
import matplotlib.pyplot as plt  # Plotting
import sklearn.linear_model as lm  # Machine learning
from sklearn.decomposition import PCA  # for PCA
from sklearn.model_selection import train_test_split

ラベルを読み込んで表示します。

In [2]:
labels = np.loadtxt("pc/label.txt")
labels
Out[2]:
array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
       0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
       0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
       0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
       0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
       0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 1.,
       1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
       1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
       1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
       1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
       1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
       1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.])

ポイントクラウドデータの読み込み¶

In [3]:
pointclouds = [np.loadtxt("pc/{:04d}.txt".format(i)) for i in tqdm(range(200))]
  0%|          | 0/200 [00:00<?, ?it/s]

パーシステンス図の計算¶

それでは、HomCloud を使ってパーシステンス図を計算しましょう。
計算されたファイルは pd ディレクトリに保存しておきます。

In [4]:
os.makedirs("pd", exist_ok=True)
for i in tqdm(range(200)):
    hc.PDList.from_alpha_filtration(pointclouds[i], save_boundary_map=True, 
                                    save_to="pd/{:04d}.pdgm".format(i))
  0%|          | 0/200 [00:00<?, ?it/s]

このチュートリアルでは、2 次のパーシステンス図を使用します。つまり、空隙構造(ボイド)に注目して解析を行います。

In [5]:
pds = [hc.PDList("pd/{:04d}.pdgm".format(i)).dth_diagram(2) for i in tqdm(range(200))]
  0%|          | 0/200 [00:00<?, ?it/s]

0番目(ラベル0)と100番目(ラベル1)のパーシステンス図を表示します。

In [6]:
pds[0].histogram(x_range=(0, 0.03)).plot(colorbar={"type": "log"})
pds[100].histogram(x_range=(0, 0.03)).plot(colorbar={"type": "log"})
No description has been provided for this image
No description has been provided for this image

Persistence Codebookによるベクトル化¶

Persistence Codebook については、以下の文献を参考にしてください:

  • https://link.springer.com/article/10.1007/s10462-020-09897-4

この論文では多くのベクトル化法が提案されていますが、HomCloud では PBoW, Stable PBoW, PFV の3つのみ実装されています。 ここでは PBoW を使いましょう。 このチュートリアルの作者 (大林) の経験では、PBoWが一番手軽に利用できます。 パラメーター調整を適当にやってもそれなりに良い結果が得られます。

Persistence codebook では、ベクトル化の対象となるPDのbirth-death pairをすべて混ぜ合わせたものからランダムにサンプリングし、そこからpairのクラスターを計算します。 クラスタリングによってPDのpairの分布を低次元化することを目指します。 基本的にサンプリングはクラスタリングのコストを下げるために利用しますが、それだけでなく各pairを重み付けてサンプリングすることでpairの重要さを反映させる効果があります。

次のコードで、サンプリングするpairの個数(100000個)、クラスタ数 (20個)、重み関数 ($\arctan(0.01 (d - b)^3)$)を指定します。

In [7]:
def weight(b, d):
    return np.atan(0.01 * (d - b) ** 3)

vectorize_spec = hc.codebook.PBoWSpec(100000, 20, weight)

次のセルでクラスタを計算します。

In [8]:
vectorize_spec.fit(pds)

ではパーシステント図をベクトル化しましょう。

In [9]:
pdvects = np.vstack([vectorize_spec.vectorize(pd) for pd in pds])

主成分分析(PCA)¶

まずは PCA(主成分分析)を用いてデータを解析します。
PCA は、高次元のデータをできるだけ情報を失わずに低次元に圧縮するための手法です。
解析結果が大きく破綻することが少ないため、パーシステンス図と機械学習の組み合わせでは第一に試す価値があります。

パーシステンス図との組み合わせでは、各図を低次元の点として表現できるため、図同士の関係性を視覚的に理解しやすくなります。

ここでは、可視化しやすいように 2 次元に次元削減を行います。

In [10]:
pca = PCA(n_components=2)
pca.fit(pdvects)
Out[10]:
PCA(n_components=2)
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Parameters
n_components n_components: int, float or 'mle', default=None

Number of components to keep.
if n_components is not set all components are kept::

n_components == min(n_samples, n_features)

If ``n_components == 'mle'`` and ``svd_solver == 'full'``, Minka's
MLE is used to guess the dimension. Use of ``n_components == 'mle'``
will interpret ``svd_solver == 'auto'`` as ``svd_solver == 'full'``.

If ``0 < n_components < 1`` and ``svd_solver == 'full'``, select the
number of components such that the amount of variance that needs to be
explained is greater than the percentage specified by n_components.

If ``svd_solver == 'arpack'``, the number of components must be
strictly less than the minimum of n_features and n_samples.

Hence, the None case results in::

n_components == min(n_samples, n_features) - 1
2
copy copy: bool, default=True

If False, data passed to fit are overwritten and running
fit(X).transform(X) will not yield the expected results,
use fit_transform(X) instead.
True
whiten whiten: bool, default=False

When True (False by default) the `components_` vectors are multiplied
by the square root of n_samples and then divided by the singular values
to ensure uncorrelated outputs with unit component-wise variances.

Whitening will remove some information from the transformed signal
(the relative variance scales of the components) but can sometime
improve the predictive accuracy of the downstream estimators by
making their data respect some hard-wired assumptions.
False
svd_solver svd_solver: {'auto', 'full', 'covariance_eigh', 'arpack', 'randomized'}, default='auto'

"auto" :
The solver is selected by a default 'auto' policy is based on `X.shape` and
`n_components`: if the input data has fewer than 1000 features and
more than 10 times as many samples, then the "covariance_eigh"
solver is used. Otherwise, if the input data is larger than 500x500
and the number of components to extract is lower than 80% of the
smallest dimension of the data, then the more efficient
"randomized" method is selected. Otherwise the exact "full" SVD is
computed and optionally truncated afterwards.
"full" :
Run exact full SVD calling the standard LAPACK solver via
`scipy.linalg.svd` and select the components by postprocessing
"covariance_eigh" :
Precompute the covariance matrix (on centered data), run a
classical eigenvalue decomposition on the covariance matrix
typically using LAPACK and select the components by postprocessing.
This solver is very efficient for n_samples >> n_features and small
n_features. It is, however, not tractable otherwise for large
n_features (large memory footprint required to materialize the
covariance matrix). Also note that compared to the "full" solver,
this solver effectively doubles the condition number and is
therefore less numerical stable (e.g. on input data with a large
range of singular values).
"arpack" :
Run SVD truncated to `n_components` calling ARPACK solver via
`scipy.sparse.linalg.svds`. It requires strictly
`0 < n_components < min(X.shape)`
"randomized" :
Run randomized SVD by the method of Halko et al.

.. versionadded:: 0.18.0

.. versionchanged:: 1.5
Added the 'covariance_eigh' solver.
'auto'
tol tol: float, default=0.0

Tolerance for singular values computed by svd_solver == 'arpack'.
Must be of range [0.0, infinity).

.. versionadded:: 0.18.0
0.0
iterated_power iterated_power: int or 'auto', default='auto'

Number of iterations for the power method computed by
svd_solver == 'randomized'.
Must be of range [0, infinity).

.. versionadded:: 0.18.0
'auto'
n_oversamples n_oversamples: int, default=10

This parameter is only relevant when `svd_solver="randomized"`.
It corresponds to the additional number of random vectors to sample the
range of `X` so as to ensure proper conditioning. See
:func:`~sklearn.utils.extmath.randomized_svd` for more details.

.. versionadded:: 1.1
10
power_iteration_normalizer power_iteration_normalizer: {'auto', 'QR', 'LU', 'none'}, default='auto'

Power iteration normalizer for randomized SVD solver.
Not used by ARPACK. See :func:`~sklearn.utils.extmath.randomized_svd`
for more details.

.. versionadded:: 1.1
'auto'
random_state random_state: int, RandomState instance or None, default=None

Used when the 'arpack' or 'randomized' solvers are used. Pass an int
for reproducible results across multiple function calls.
See :term:`Glossary `.

.. versionadded:: 0.18.0
None

どのように2次元上に表現されているか可視化します。 赤がラベル0、青がラベル1を意味します。

In [11]:
reduced = pca.transform(pdvects)  # すべてのデータを2次元に落とす
plt.gca().set_aspect('equal')  # 縦横のアスペクト比を揃える
plt.scatter(reduced[labels == 0, 0], reduced[labels == 0, 1], c="r")  # ラベル0のデータを赤で描画
plt.scatter(reduced[labels == 1, 0], reduced[labels == 1, 1], c="b")  # ラベル1のデータを青で描画
Out[11]:
<matplotlib.collections.PathCollection at 0x7901e5904cb0>
No description has been provided for this image

2 次元平面上でX軸方向で赤と青の点がある程度分離されていることから、PBoWによって得られたベクトルは2つのグループの違いをうまく表現できていると考えられます。

では、この図の X 軸と Y 軸は何を表しているのでしょうか?

X 軸と Y 軸に対応するベクトルをそれぞれ $v_1$, $v_2$ とすると、PBoWによってベクトル $w_i$($i$ はデータのインデックス)は次のように近似されます:

$$ w_i \approx \mu_{i1} v_1 + \mu_{i2} v_2 + v_0 $$

ここで $v_0$ は $w_1, w_2, \ldots$ の平均ベクトルで,$(\mu_{i1}, \mu_{i2})$が上の図の各点の座標です(このあたりの理論については PCA の基本を学んでください)。
つまり、$v_1$ と $v_2$ がどのようなベクトルであるかがわかれば、この図が何を意味しているかをより深く理解できます。

なお、scikit-learn の PCA では、$v_0$ は PCA.mean_、$v_1$, $v_2$ は PCA.components_ に格納されています。

In [12]:
pca.mean_
Out[12]:
array([1382.53977497, 1113.12157663, 2316.20804098,  544.58056669,
        921.26733574, 1706.82253307, 1050.23883135, 1234.84060285,
        944.74688089, 2418.6258216 , 1530.33987473,  246.66255163,
        740.14725272,  349.92313533,  272.04492509,  355.24084419,
       1411.41960292, 1562.27499827,   62.13692049, 1643.1220043 ])
In [13]:
pca.components_
Out[13]:
array([[ 0.39326077, -0.24055517,  0.21960124, -0.07153537, -0.20310608,
         0.48443186, -0.00377261, -0.06989213, -0.1630181 ,  0.18469233,
        -0.19420942, -0.05430333, -0.0748403 ,  0.00398742, -0.04671522,
        -0.04850072,  0.12773821,  0.09995429, -0.01214863,  0.56156237],
       [ 0.10362294,  0.23225009,  0.14514279,  0.09262878, -0.07614634,
        -0.41993717,  0.11772431,  0.16389753, -0.19611693,  0.21991956,
        -0.31901628, -0.00838165,  0.12423459,  0.09569389, -0.02517399,
         0.0405355 ,  0.176744  ,  0.65894105, -0.00131564, -0.04276406]])

各ベクトルの次元は20で、これはクラスター数と同じです。 Persistence codebook では、これらの係数はクラスタごとの重みとして解釈されます。 つまりクラスタリング結果を理解するためにはクラスタリング結果を調べるのが肝心です。

そこで、クラスタリングの中心をプロットしてみましょう。vectorize_spec.cluster_centers で各クラスターの中心座標が得られます。

In [14]:
vectorize_spec.cluster_centers
Out[14]:
array([[0.01011662, 0.01670536],
       [0.00829549, 0.01951828],
       [0.0069341 , 0.0104699 ],
       [0.01272092, 0.02197064],
       [0.00880535, 0.02163759],
       [0.00746409, 0.01755715],
       [0.0148032 , 0.01946074],
       [0.01019561, 0.01915189],
       [0.00729412, 0.02075994],
       [0.00610172, 0.00811547],
       [0.00704536, 0.01882478],
       [0.00747822, 0.02268904],
       [0.01070353, 0.020874  ],
       [0.01520447, 0.02324038],
       [0.01036722, 0.0244698 ],
       [0.01825379, 0.0231114 ],
       [0.01239476, 0.01853661],
       [0.00885005, 0.01819233],
       [0.0079025 , 0.02517447],
       [0.00730205, 0.01608447]])

散布図でクラスター中心をプロットします。PDと比較しやすいよう対角線も描画します。

In [15]:
import matplotlib.lines

fig, ax = plt.subplots()
ax.scatter(vectorize_spec.cluster_centers[:, 0], vectorize_spec.cluster_centers[:, 1])
ax.set_aspect("equal")
ax.add_artist(matplotlib.lines.Line2D([0, 0.03], [0, 0.03], color="k"))
ax.set_xlim(0, 0.03)
ax.set_ylim(0, 0.03)
Out[15]:
(0.0, 0.03)
No description has been provided for this image

death > 0.015 の領域に大半のクラスタ中心が分布していますが、death < 0.015の領域にも3箇所あります。

PDを重ね合わせたものをクラスタリングする際には対角線を割り引いてサンプリングしているので、対角線のそばには多くのbirth-death pairが分布しているにもかかわらずクラスタ中心が現われません。 この重みの調整はPBoWの運用において肝心な点です。

次にPCAの結果を可視化します。

PCAの結果で計算されるベクトル$v_i$の次元はクラスター数と同じで、このベクトルの各要素がクラスター毎の「重み」と見なせます。 詳しくはCodebookの論文を読んでください。 そこで、$v_i$の各要素をクラスター中心の点の色で表現しましょう。 その値が正なら青、負なら赤、0に近ければ白、となるように色付けを調整します。

In [16]:
fig, axes = plt.subplots(1, 2, figsize=(12, 4))

for k in [0, 1]:
    ax = axes[k]
    sc = ax.scatter(
        vectorize_spec.cluster_centers[:, 0], 
        vectorize_spec.cluster_centers[:, 1],
        c=pca.components_[k, :],
        vmax=0.7,
        vmin=-0.7,
        cmap='RdBu'
    )
    
    cbar = fig.colorbar(sc, ax=ax)
    ax.grid(True)
    ax.set_aspect("equal")
    ax.add_artist(matplotlib.lines.Line2D([0, 0.03], [0, 0.03], color="k"))
    ax.set_xlim(0, 0.03)
    ax.set_ylim(0, 0.03)

# グラフの表示
plt.show()
No description has been provided for this image

この色の分布を見てPCAの結果を解釈します。 解釈法はロジスティック回帰と似ているので、そちらの説明を見てください。

ロジスティック回帰¶

次はロジスティック回帰です。ロジスティック回帰は、PCA に続いてパーシステントホモロジーとの組み合わせが比較的容易な手法です。
特に2クラス分類が解釈しやすく、扱いやすいという利点があります。

まずは学習を始める前にデータをテスト用(test set)と学習用(training set)に分割します。

In [17]:
pdvects_train, pdvects_test, labels_train, labels_test = train_test_split(pdvects, labels, test_size=0.25)

学習用データを使ってモデルを学習します。
ここでは、sklearn の LogisticRegression クラスを使用します。
正則化パラメータ C は、今回はあらかじめ適当な値を設定していますが、LogisticRegressionCV クラスを使えば、交差検証によって最適なパラメータを自動的に選ぶことも可能です。

In [18]:
model = lm.LogisticRegression(C=0.01, solver="lbfgs")
In [19]:
model.fit(pdvects_train, labels_train, )
Out[19]:
LogisticRegression(C=0.01)
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Parameters
penalty penalty: {'l1', 'l2', 'elasticnet', None}, default='l2'

Specify the norm of the penalty:

- `None`: no penalty is added;
- `'l2'`: add a L2 penalty term and it is the default choice;
- `'l1'`: add a L1 penalty term;
- `'elasticnet'`: both L1 and L2 penalty terms are added.

.. warning::
Some penalties may not work with some solvers. See the parameter
`solver` below, to know the compatibility between the penalty and
solver.

.. versionadded:: 0.19
l1 penalty with SAGA solver (allowing 'multinomial' + L1)

.. deprecated:: 1.8
`penalty` was deprecated in version 1.8 and will be removed in 1.10.
Use `l1_ratio` instead. `l1_ratio=0` for `penalty='l2'`, `l1_ratio=1` for
`penalty='l1'` and `l1_ratio` set to any float between 0 and 1 for
`'penalty='elasticnet'`.
'deprecated'
C C: float, default=1.0

Inverse of regularization strength; must be a positive float.
Like in support vector machines, smaller values specify stronger
regularization. `C=np.inf` results in unpenalized logistic regression.
For a visual example on the effect of tuning the `C` parameter
with an L1 penalty, see:
:ref:`sphx_glr_auto_examples_linear_model_plot_logistic_path.py`.
0.01
l1_ratio l1_ratio: float, default=0.0

The Elastic-Net mixing parameter, with `0 <= l1_ratio <= 1`. Setting
`l1_ratio=1` gives a pure L1-penalty, setting `l1_ratio=0` a pure L2-penalty.
Any value between 0 and 1 gives an Elastic-Net penalty of the form
`l1_ratio * L1 + (1 - l1_ratio) * L2`.

.. warning::
Certain values of `l1_ratio`, i.e. some penalties, may not work with some
solvers. See the parameter `solver` below, to know the compatibility between
the penalty and solver.

.. versionchanged:: 1.8
Default value changed from None to 0.0.

.. deprecated:: 1.8
`None` is deprecated and will be removed in version 1.10. Always use
`l1_ratio` to specify the penalty type.
0.0
dual dual: bool, default=False

Dual (constrained) or primal (regularized, see also
:ref:`this equation `) formulation. Dual formulation
is only implemented for l2 penalty with liblinear solver. Prefer `dual=False`
when n_samples > n_features.
False
tol tol: float, default=1e-4

Tolerance for stopping criteria.
0.0001
fit_intercept fit_intercept: bool, default=True

Specifies if a constant (a.k.a. bias or intercept) should be
added to the decision function.
True
intercept_scaling intercept_scaling: float, default=1

Useful only when the solver `liblinear` is used
and `self.fit_intercept` is set to `True`. In this case, `x` becomes
`[x, self.intercept_scaling]`,
i.e. a "synthetic" feature with constant value equal to
`intercept_scaling` is appended to the instance vector.
The intercept becomes
``intercept_scaling * synthetic_feature_weight``.

.. note::
The synthetic feature weight is subject to L1 or L2
regularization as all other features.
To lessen the effect of regularization on synthetic feature weight
(and therefore on the intercept) `intercept_scaling` has to be increased.
1
class_weight class_weight: dict or 'balanced', default=None

Weights associated with classes in the form ``{class_label: weight}``.
If not given, all classes are supposed to have weight one.

The "balanced" mode uses the values of y to automatically adjust
weights inversely proportional to class frequencies in the input data
as ``n_samples / (n_classes * np.bincount(y))``.

Note that these weights will be multiplied with sample_weight (passed
through the fit method) if sample_weight is specified.

.. versionadded:: 0.17
*class_weight='balanced'*
None
random_state random_state: int, RandomState instance, default=None

Used when ``solver`` == 'sag', 'saga' or 'liblinear' to shuffle the
data. See :term:`Glossary ` for details.
None
solver solver: {'lbfgs', 'liblinear', 'newton-cg', 'newton-cholesky', 'sag', 'saga'}, default='lbfgs'

Algorithm to use in the optimization problem. Default is 'lbfgs'.
To choose a solver, you might want to consider the following aspects:

- 'lbfgs' is a good default solver because it works reasonably well for a wide
class of problems.
- For :term:`multiclass` problems (`n_classes >= 3`), all solvers except
'liblinear' minimize the full multinomial loss, 'liblinear' will raise an
error.
- 'newton-cholesky' is a good choice for
`n_samples` >> `n_features * n_classes`, especially with one-hot encoded
categorical features with rare categories. Be aware that the memory usage
of this solver has a quadratic dependency on `n_features * n_classes`
because it explicitly computes the full Hessian matrix.
- For small datasets, 'liblinear' is a good choice, whereas 'sag'
and 'saga' are faster for large ones;
- 'liblinear' can only handle binary classification by default. To apply a
one-versus-rest scheme for the multiclass setting one can wrap it with the
:class:`~sklearn.multiclass.OneVsRestClassifier`.

.. warning::
The choice of the algorithm depends on the penalty chosen (`l1_ratio=0`
for L2-penalty, `l1_ratio=1` for L1-penalty and `0 < l1_ratio < 1` for
Elastic-Net) and on (multinomial) multiclass support:

================= ======================== ======================
solver l1_ratio multinomial multiclass
================= ======================== ======================
'lbfgs' l1_ratio=0 yes
'liblinear' l1_ratio=1 or l1_ratio=0 no
'newton-cg' l1_ratio=0 yes
'newton-cholesky' l1_ratio=0 yes
'sag' l1_ratio=0 yes
'saga' 0<=l1_ratio<=1 yes
================= ======================== ======================

.. note::
'sag' and 'saga' fast convergence is only guaranteed on features
with approximately the same scale. You can preprocess the data with
a scaler from :mod:`sklearn.preprocessing`.

.. seealso::
Refer to the :ref:`User Guide ` for more
information regarding :class:`LogisticRegression` and more specifically the
:ref:`Table `
summarizing solver/penalty supports.

.. versionadded:: 0.17
Stochastic Average Gradient (SAG) descent solver. Multinomial support in
version 0.18.
.. versionadded:: 0.19
SAGA solver.
.. versionchanged:: 0.22
The default solver changed from 'liblinear' to 'lbfgs' in 0.22.
.. versionadded:: 1.2
newton-cholesky solver. Multinomial support in version 1.6.
'lbfgs'
max_iter max_iter: int, default=100

Maximum number of iterations taken for the solvers to converge.
100
verbose verbose: int, default=0

For the liblinear and lbfgs solvers set verbose to any positive
number for verbosity.
0
warm_start warm_start: bool, default=False

When set to True, reuse the solution of the previous call to fit as
initialization, otherwise, just erase the previous solution.
Useless for liblinear solver. See :term:`the Glossary `.

.. versionadded:: 0.17
*warm_start* to support *lbfgs*, *newton-cg*, *sag*, *saga* solvers.
False
n_jobs n_jobs: int, default=None

Does not have any effect.

.. deprecated:: 1.8
`n_jobs` is deprecated in version 1.8 and will be removed in 1.10.
None

まずテスト用データを使って性能を確認します。

In [20]:
model.score(pdvects_test, labels_test)
Out[20]:
0.98

高い精度です。 次にロジスティック回帰の係数を以下で可視化します。

まずは係数の数値を見てみましょう。

In [21]:
model.coef_
Out[21]:
array([[-0.01049108,  0.00159178, -0.00790289,  0.0028876 ,  0.00148073,
        -0.00490653,  0.00829817, -0.00032661,  0.00164977,  0.00560674,
         0.00312246,  0.00093899,  0.00170018,  0.00687491, -0.00083505,
         0.00625443,  0.00148933, -0.00139073,  0.00244847, -0.00490393]])
In [22]:
model.coef_.max(), model.coef_.min()
Out[22]:
(np.float64(0.008298170615342543), np.float64(-0.010491077669949847))
In [23]:
fig, ax = plt.subplots()

sc = ax.scatter(
    vectorize_spec.cluster_centers[:, 0], 
    vectorize_spec.cluster_centers[:, 1],
    c=model.coef_,
    vmax=0.01,
    vmin=-0.01,
    cmap='RdBu'
)

cbar = fig.colorbar(sc, ax=ax)
ax.grid(True)
ax.set_aspect("equal")
ax.add_artist(matplotlib.lines.Line2D([0, 0.03], [0, 0.03], color="k"))
ax.set_xlim(0, 0.03)
ax.set_ylim(0, 0.03)

# グラフの表示
plt.show()
No description has been provided for this image

この図の見方ですが、濃い赤の領域に多くの birth-death ペアが存在するデータは、ラベル 0 である可能性が高く、 一方で、濃い青の領域に多くの birth-death ペアが存在するデータは、ラベル 1 である可能性が高いことを示しています。

この図ではdeathが0.018あたりに青と赤の境界があるように見えます。つまりdeath=0.018のラインより下にpairが多ければラベル0のデータ、上にpairが多ければラベル1のデータであることがわかります。

PDをベクトル化して得られたベクトルとこのベクトルのアダマール積を取ると、どのクラスターがどの程度予測に貢献しているかもわかります。

そこで、ラベル0とラベル1のデータを1つづつ(1番目と101番目)取ってきて計算し、それを上のように可視化しましょう。

In [24]:
pdvects[1, :] * model.coef_
Out[24]:
array([[-3.21191502e+01,  0.00000000e+00, -1.68964610e+01,
         7.24862167e+00,  0.00000000e+00, -1.94181397e+01,
         3.86970391e+00, -1.30557095e+00,  0.00000000e+00,
         1.14009952e+01,  1.52847312e+01,  0.00000000e+00,
         0.00000000e+00,  0.00000000e+00, -0.00000000e+00,
         1.57046035e-06,  1.69002750e+00, -0.00000000e+00,
         0.00000000e+00, -1.59528157e+01]])
In [25]:
pdvects[101, :] * model.coef_
Out[25]:
array([[-2.83204956e-01,  5.97278013e+00, -8.49660910e+00,
         0.00000000e+00,  3.28045154e+00, -8.53542659e+00,
         4.65867559e+00, -0.00000000e+00,  0.00000000e+00,
         5.79111333e+00,  6.55395527e+00,  0.00000000e+00,
         2.94293212e+00,  0.00000000e+00, -0.00000000e+00,
         1.35214975e-03,  7.65964220e-04, -3.05768193e+00,
         0.00000000e+00, -0.00000000e+00]])
In [26]:
fig, axes = plt.subplots(1, 2, figsize=(14, 6))

sc = axes[0].scatter(
    vectorize_spec.cluster_centers[:, 0], 
    vectorize_spec.cluster_centers[:, 1],
    c=pdvects[1, :] * model.coef_,
    vmax=30,
    vmin=-30,
    cmap='RdBu'
)

cbar = fig.colorbar(sc, ax=axes[0])
axes[0].grid(True)
axes[0].set_aspect("equal")
axes[0].add_artist(matplotlib.lines.Line2D([0, 0.03], [0, 0.03], color="k"))
axes[0].set_xlim(0, 0.03)
axes[0].set_ylim(0, 0.03)
pds[1].histogram((0, 0.03), 128).plot(colorbar={"type": "log"}, ax=axes[1])
fig.tight_layout()
# グラフの表示
plt.show()
No description has been provided for this image
In [27]:
fig, axes = plt.subplots(1, 2, figsize=(14, 6))

sc = axes[0].scatter(
    vectorize_spec.cluster_centers[:, 0], 
    vectorize_spec.cluster_centers[:, 1],
    c=pdvects[101, :] * model.coef_,
    vmax=10,
    vmin=-10,
    cmap='RdBu'
)

cbar = fig.colorbar(sc, ax=axes[0])
axes[0].grid(True)
axes[0].set_aspect("equal")
axes[0].add_artist(matplotlib.lines.Line2D([0, 0.03], [0, 0.03], color="k"))
axes[0].set_xlim(0, 0.03)
axes[0].set_ylim(0, 0.03)
pds[101].histogram((0, 0.03), 128).plot(colorbar={"type": "log"}, ax=axes[1])
fig.tight_layout()
# グラフの表示
plt.show()
No description has been provided for this image

左右を見比べると、どのあたりのpairが予測に効いているかがわかります。