ほくそ笑む

R言語と統計解析について

Clustered ICE でサンプルごとのクラスタ割付を取得する

最近、『機械学習を解釈する技術』という本を読んでいます。

非常に分かりやすくて良い本なのでおすすめです。

この本には出てこないのですが、著者のブログで紹介されている Clustered ICE という手法がとても便利です。

Clustered ICE は、データサンプルごとに得られる ICE を傾向ごとにクラスタ化してくれる手法です。 この手法により、ICE の傾向が異なるサンプル群があるかどうかを簡単に調べることができます。

しかし、これを実装している ingredients パッケージの cluster_profiles() 関数は、どのサンプルがどのクラスタに属するのかという情報を返してくれません。

つまり、クラスタ化すると傾向の異なるサンプル群が発見できても、どのサンプルがどのクラスタに割り付けられたのかが分からないという状況が起きます。

そこで、この記事では、cluster_profiles() 関数から、サンプルごとのクラスタ割付を取得する方法を説明します。

Clustered ICE

データとモデルは下記のブログ記事の「シミュレーション2」をそのまま使います。

dropout009.hatenablog.com

df |> head()
       Y     X1     X2    X3
1 -5.13  -0.802  0.889     0
2 -2.23  -0.457 -0.394     1
3 -0.843  0.533  0.292     0
4  4.20  -0.551  0.934     1
5 -1.29  -0.595 -0.174     1
6  3.62  -0.393  0.790     1

これを使って explainer を作成します。

library(DALEX)
explainer <- explain(fitted, data = df |> select(-Y), y = df |> pull(Y))

サンプルをランダムに取り出して、特徴量 X2 に対する ICE を作成します。

library(ingredients)
df_ice <- df |> sample_n(100)
ice <- ceteris_paribus(explainer, variables = "X2", new_observation = df_ice) 

この ICE について、 Clustered ICE を適用します。

clustered_ice <- cluster_profiles(ice, k = 2)
plot(clustered_ice)

特徴量 X2 が増加したとき、出力変数が増加するサンプル群と減少するサンプル群の2つのクラスタに分かれることが分かります。

この結果を受けて、それぞれのクラスタに属するサンプルの違いを調べたいのですが、cluster_profiles() 関数は、どのサンプルがどのクラスタに属するのかという情報を返しません。

そこで、次のようにして無理やり取得します。

クラスタ割付の取得

まずは cluster_profiles() 関数の中身を確認します。

> cluster_profiles
function (x, ..., aggregate_function = mean, variable_type = "numerical", 
    center = FALSE, k = 3, variables = NULL) 
{
    (省略)
    clus <- cutree(hclust(as.dist(dist_mat), method = "ward.D2"), 
        k = k)
    (省略)
}

クラスタ割付の情報は関数内の clus 変数に格納されていることが分かります(ついでにクラスタの手法は階層クラスタリング (hclust) であることも分かります)。

この clus 変数を無理やり取得します。それには trace() 関数を使用します。trace() 関数は、任意の関数内で任意のコードを実行できるようにする関数です。

ここでは、cluster_profiles() の終了時点での clus 変数をグローバル環境に代入します。

trace(cluster_profiles, exit = expression(clus <<- clus))
cluster_profiles(ice, k = 2)
untrace(cluster_profiles)

これにより、グローバル環境に clus という変数ができます。あとはこれを df_ice の順番に合うように並び替えて新しいカラムとして加えるだけです。

names(clus) <- str_pad(names(clus), width = 3, pad = "0")
clus <- clus[sort(names(clus))]

d <- df_ice |> cbind(clus = clus)
d |> head()
             Y         X1          X2 X3 clus
001  0.4637253  0.3238136 -0.04910185  0    1
002  1.6953002  0.9427162  0.14749948  1    2
003 -3.1748771 -0.6070075 -0.50735357  1    2
004  0.5291305 -0.8114684  0.26476546  1    2
005 -0.1732673  0.4395402 -0.12539468  1    2
006  0.7308131  0.5727512 -0.04552853  0    1

サンプルごとのクラスタ割付が分かったので、クラスタ間でサンプルがどのような違いを持つのかを調査できます。 試しに、クラスタごとに各特徴量の平均を集計してみます。

d |> group_by(clus) |> summarise_at(vars(X1, X2, X3), mean)
   clus      X1      X2    X3
1     1  0.0807  0.0582     0
2     2 -0.0493 -0.0523     1

特徴量 X3 が 0 と 1 に完全に分かれていることが分かります。 これにより、クラスタ 1 と 2 の違いが、特徴量 X3 による違いだという知見を得ることができました。

model_profile() の場合

さて、Clustered ICE は、DALEX パッケージでは model_profile() 関数に引数 k を指定することで実行できます。この場合についてもサンプルごとのクラスタ割付を取得する方法を書いておきます。

model_profile() 関数は、内部で cluster_profiles() 関数を使っているので、trace() の実行は同じコードになります。異なるのは、explainer を作るときに df_ice を使う必要があるという点です。

library(DALEX)
N <- 100
k <- 2

df_ice <- df |> sample_n(N)

explainer_ice <- explain(fitted, data = df_ice |> select(-Y), y = df_ice |> pull(Y))

trace(cluster_profiles, exit = expression(clus <<- clus))
clustered_ice <- model_profile(explainer_ice, variables = "X2", N = N, k = k)
untrace(cluster_profiles)

names(clus) <- str_pad(names(clus), width = floor(log10(N)+1), pad = "0")
clus <- clus[sort(names(clus))]

d <- df_ice |> cbind(clus = clus)
d |> head()
             Y         X1          X2 X3 clus
001  0.4637253  0.3238136 -0.04910185  0    1
002  1.6953002  0.9427162  0.14749948  1    2
003 -3.1748771 -0.6070075 -0.50735357  1    2
004  0.5291305 -0.8114684  0.26476546  1    2
005 -0.1732673  0.4395402 -0.12539468  1    2
006  0.7308131  0.5727512 -0.04552853  0    1

model_profile() でもサンプルごとのクラスタ割付を取得することができました。

参考

dropout009.hatenablog.com