AREKORE

daikikatsuragawaのアレコレ

複数の反実仮想説明に基づく複数の意思決定の促進を目的としたひとつの施策の設計を支援する手法の提案

​​以前、筆者(id:daikikatsuragawa)は反実仮想説明を生成するDiCEというPythonで実装されているライブラリに興味を持ちました。DiCEが生成する反実仮想説明は、現在の状態を望んだ状態に変えるために、必要となる具体的な特徴の変化例を算出するという点で、重要業績評価指標(Key Performance Indicator:KPI)の設定に貢献し、意思決定の促進に有効であると考えられます。しかし、複数の意思決定の促進を目的とした施策を設計する場合、考慮すべき課題が挙げられました。そこで、複数の意思決定の促進を目的とした施策の設計を支援することを動機として、複数の反実仮想説明に基づく手法を提案します。

意思決定を促進する反実仮想説明

機械学習を活用したサービスでの目的として「予測」が挙げられます。例えばユーザの特徴に基づき、ユーザの状態を“望ましい状態”か否かの二値で分類します。このようなサービスの中には以下の動機へ対応する目的で提供されているものが存在していると考えられます。

  • ユーザ自身が状態を把握したい
    • “望ましい状態”ではないと予測された場合はそれを覆したい
  • サービス提供者がユーザの状態を把握したい
    • サービス提供者にとって“望ましい状態”のユーザの数を増やしたい

しかし、上述した目的のサービスにおいて、以下の課題が挙げられます。

  • ユーザもしくサービス提供者は状態を覆すための具体的な行動指針がわからない
    • 何をどのように行動(改善)したら良いのかがわからない
    • そもそも決断に至る行動を設定できない
  • 有識者でない人によって考案された行動指針が効果的かわからない
    • 行動指針の信頼性が低いことにより行動の決断に至る可能性が低い
    • 仮に行動指針に従ったところで望ましい状態にならない可能性が高い

以上の理由で、例えば予測を実施する機械学習を活用したサービスでは予測に加えて上記の課題を解決した、意思決定の促進を目的とした情報の提供が必要です。本記事における「意思決定」の定義を「行動を決断すること」、および「行動すること」、その結果として「特徴が変化すること」までとします。また「意思決定者」を行動指針に基づき、「意思決定」を実施するか否かといった判断が可能である人と定義します。ここで挙げる「行動指針」とは意思決定に必要なもので、「自身で判断が可能なもの」、「有識者によって提供されるもの」があると定義します。具体的には何をどれだけ変化させたら状態が覆るのかを表現したものであるとします。

課題の解決に対して、近年、議論や研究がなされている機械学習の解釈手法が有効であると考えられます。これらは予測の結果に対して、それからどういった解釈ができるのかをより詳細にユーザに伝えることを目的としています。その中でも「未来のためにどうしたら良いのか(どんな意思決定をしたら良いのか)」の指針を示すことを目的とした解釈手法があります。この手法により上述した課題の解決が期待されます。

上述した目的の機械学習の解釈手法のひとつとして反実仮想を応用した「反実仮想説明(Counterfactual Explanations)」が提案されています。反実仮想とは以下を意味します。

文法で、事実と反対のことを想定すること。「もし〜だったら…だろうに」のような言い方。

引用元:反実仮想(ハンジツカソウ)とは? 意味や使い方 - コトバンク

このような反実仮想を応用した反実仮想説明は、機械学習の予測に対して、状態が覆る、具体的な特徴の変化例を提案します。「もし〜だったら…だろうに」という伝え方が可能であるため、提案を受けた人は具体的な行動指針の想像が可能です。具体的には、「もし特徴Aがa、特徴Bがbだけ変化した場合、状態が覆る」と言ったことを伝えることが可能です。それゆえ、意思決定の促進に対する有効性が期待されます。

反実仮想説明を生成するDiCE*1というライブラリが開発されています。Pythonで実装されておりPypi*2で公開されています。筆者は個人的にDiCEに興味を持ち、以前、反実仮想説明をサービスとして活用するという観点で考察をしました。

daikikatsuragawa.hatenablog.com

その過程で、複数の意思決定の促進を目的とする場合、特に目的を満たすひとつの施策を設計する場合、工夫が必要であるという課題を挙げました。

複数の意思決定の促進を目的とした場合の課題

改めて課題を把握するために、反実仮想説明が必要となるサービスについて、ユースケースを以下の表にて整理します。2つの軸を設定し、それらに基づき比較します。

意思決定者が少ない/BtoB 意思決定者が多い/BtoC
有識者による解釈が不要(もしくは有識者≒意思決定者) ①意思決定者が反実仮想説明を確認し「行動指針」を理解し自ら行動(組織向け) ②意思決定者が反実仮想説明を確認し「行動指針」を理解し自ら行動(個人向け)
有識者による解釈が必要 有識者が反実仮想説明を解釈して行動指針(施策)を意思決定者に提案 有識者が複数の反実仮想説明を解釈し複数の意思決定を促進する施策を提案

例えば「①意思決定者が反実仮想説明を確認し「行動指針」を理解し自ら行動(組織向け)」は法人向け(Business to Business:BtoB)な形式のサービスに当てはまります。その中でも有識者による解釈が不要、もしくは意思決定者自身が有識者であるため、反実仮想説明を参考にした行動指針の考案、意思決定の実現が可能です。組織を“望ましい状態”にするための分析、提案を提供するサービスがこれに当てはまります。それに対して「②意思決定者が反実仮想説明を確認し「行動指針」を理解し自ら行動(個人向け)」は消費者向け(Business to Consumer:BtoC)な形式のサービスに当てはまります。その中でも有識者による解釈が不要、もしくは意思決定者自身が有識者である例です。個人を“望ましい状態”にするための分析、提案を提供するサービスがこれに当てはまります。例えば、金融業界におけるローン許諾判定システムや、教育における受験の合格判定システムです。ユーザは具体的に何をしたら良いのか、判断できるはずです。次に「③有識者が反実仮想説明を解釈して行動指針(施策)を意思決定者に提案」はBtoBな形式のサービスで、出力される反実仮想説明に有識者による解釈が必要な例です。意思決定者にとって理解が難しい特徴が扱われている場合です。サービスの提供に加えて有識者であるコンサルタントによるサポートが前提となる状況です。

最後に「④有識者が複数の反実仮想説明を解釈し複数の意思決定を促進する施策を提案」です。結論から述べると、この状況への対応に課題が挙げられます。これはBtoCな形式のサービスかつ有識者が必要な例です。この時、BはCの意思決定を促進させたいと思うものの、対象となる意思決定者が複数、つまり対象となる反実仮想説明が複数であることから、施策の設計が困難です。またCに対して個別に施策を設計することも考えられます。しかし、その量の多さから現実的ではありません。それゆえ、この課題を解決するために、複数の反実仮想説明を理解、解釈し、要約したうえで、ひとつ、つまり、一体多の施策を設計する必要があります。これを実現する手法(処理)が必要であると考えられます。

本記事における「施策」の定義を、設定したKPIを達成することを目的としたアクションとします。そのための具体的なアクションのひとつに「複数の意思決定の促進」が属しています。また、それに対して、意思決定の促進を目的とした有識者による一対一のアクションのことを「診断」と定義します。

複数の意思決定の促進を目的としたひとつの施策の設計を支援する手法の提案

課題の解決のために以下の手法を提案します。以前、Minimum Viable Product*3に基づき実用最小限の手法を考えました。この手法を具体化してフレームワークとして提案します。提案手法の概念図を以下に示します。

複数の反実仮想説明に基づく複数の意思決定の促進を目的とした施策の設計の概念図

前提として、いくつかの反実仮想、つまり“望ましい状態”へ状態が覆る特徴の変化例が生成されている状況であるとします。例として、n件の反実仮想が生成されているとします。はじめの処理として、n件の反実仮想をクラスタリングによって、k件のクラスタに分類します。それらをクラスタごとに要約します。本手法における、要約とは、ドメインによって異なるとは思いますが、大きく以下の2点を想定しています。

次の処理として、反実仮想の要約に基づき、有識者が施策を設計します。例えば、クラスタ毎の各特徴の変化例の基本統計量より、特定の特徴の変化例の中央値を確認し、これを元にKPIを設定します。そして、そのKPIを達成させるための施策を設計します。施策の数はクラスタの数と一致すると想定しています。

要約の手法としてクラスタリングを採用した理由を説明します。反実仮想説明において特徴の変化例の正負は重要です。それゆえ、要約の過程で、絶対値をとることや、単純に平均することは回避したいと考えます。そこで、反実仮想説明をクラスタリングします。これにより、要約のために基本統計量、特に平均値などを確認したとしても、反実仮想における特徴の正負が考慮されることになります。

このフレームワークにより、例えば生成したn件の反実仮想説明に対し、そのk件の反実仮想説明の要約が実現されます。そして、それに基づいて、施策の設計をすると、クラスタに特化した施策になります。これにより、クラスタに属する複数の人の意思決定の促進が期待されます。

提案手法の実装

提案手法の実装を紹介します。上述したフレームワークの要約までを対象とします。以下は提案手法をPythonの関数(summarize_cf)として実装した例です。入出力は以下のとおりとします。

  • 入力:DiCEにより生成される反実仮想説明群
  • 出力:反実仮想の要約群(入力に対してクラスタ列が付与)

また、DiCEで出力されるクラス(CounterfactualExplanations)を特徴の変化例で構成されるDataFrameに変換する関数(convert_to_diff_df)も用意します。

from sklearn.cluster import AgglomerativeClustering

def convert_to_diff_df(target_df, dice_exp):
  """
  CounterfactualExplanationsをDataFrameに変換する。
  """
  diff_dfs = []
  for i in range(len(dice_exp.cf_examples_list)):
    final_cfs_df = dice_exp.cf_examples_list[i].final_cfs_df
    test_instance_df = dice_exp.cf_examples_list[i].test_instance_df
    diff_df = final_cfs_df - test_instance_df
    diff_dfs.append(diff_df)
  diff_df = pd.concat(diff_dfs)
  diff_df.index = target_df.index.to_list()

  return diff_df

def summarize_cf(diff_df, n_clusters):
  """
  複数の反実仮想を要約するためにクラスタリングし、クラスタ列を追加する。
  """
  cluster_df = diff_df.copy()
  agglomerative_clustering = AgglomerativeClustering(n_clusters=n_clusters)
  labels = agglomerative_clustering.fit_predict(cluster_df)
  cluster_df["cluster"] = labels

  return cluster_df

上記のスクリプトを含め、本記事で紹介するスクリプトおよび関連するスクリプトのリンクを最後に載せています。

提案手法の有用性の検証

想定するシナリオ

検証のために活用例として想定するシナリオを仮定します。補足としての概念図を以下に掲載します。

提案手法が想定するシナリオの概念図

とある意思決定者群がいたとします。その群では“望ましい状態”になる傾向が高いか否かの2値で分類されるとします。それに加え、傾向は0〜1の範囲で確率としても表現が可能です。しかし、“望ましい状態”になる傾向が高いか否か、どうしたら“望ましい状態”になるのか、行動指針について当人たちは知りません。ただし、有識者に診断してもらうことで“望ましい状態”になるための助言を得たり、サポートを受けて、“望ましい状態”になる傾向を高めることができます。有識者のリソース(コスト)は限られており、診断の対象者が多くなるほど、双方にデメリットが生じます。そこで、提案手法を使うことにします。提案手法により、意思決定者群内の全員の状態を予測し、“望ましい状態”ではない意思決定者の反実仮想説明を生成し、それを要約します。その要約に対して、有識者と相談することで複数の意思決定の促進を目的としたひとつの施策の設計をします。また、シナリオの理解を容易にする目的で、有識者による診断コストと施策の設計コストを同一とします。診断コストと施策の設計コストはそれぞれ1とします。例えば、提案手法によりk件の施策を設計する場合、その施策の設計コストはk(1×k)となります。つまりk人の有識者が必要になるということになります。このシナリオは教育、医療、マーケティングなど様々な分野に当てはめることが可能です。是非、身近なドメインで読み替えてください。

つまり、以下の可能性が確認された場合、提案手法の導入が有用であったと判断できるとします。

  • 意思決定の促進数に対するコストの削減
  • 限られたコストの中での意思決定の促進数の増加

このシナリオに基づき、以下のリサーチ・クエスチョン(RQ)を設定し、これらの検証を通して有用性を確認します。

  • RQ1:クラスタの数を増やすことによりひとつの施策において参考になる特徴の数は厳選されるか?
  • RQ2:設計した施策は複数の意思決定を促進する可能性があるか?

提案手法の実施例

続いて、提案手法の実施例を紹介します。実行環境はGoogle Colaboratory*4プログラミング言語としてPythonを利用します。まずはDiCEをインストールします。

!pip install dice_ml

DiCEで反実仮想説明を生成するために、以下2点が必要です。

  • 学習データ/テストデータ
  • 学習済みモデル

まず、学習データ/テストデータを用意します。今回はsklearn.datasets.make_classificationにより、擬似データを生成します。説明変数は、feature_0、feature_1、feature_2…feature_19の20件、目的変数はlabel(1/0)とします。データ数は1,000件とします。

import numpy as np
import pandas as pd
from sklearn.datasets import make_classification
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split

def generate_sample_df(feature_column_names, label_column_name, n_samples, n_classes,
                       n_informative=10, n_redundant=5, n_clusters_per_class=5, random_state=123):
  """
  サンプルデータを生成する。
  """
  n_features = len(feature_column_names)
  sample_classification = make_classification(
      n_samples=n_samples,
      n_features=n_features,
      n_informative=n_informative,
      n_redundant=n_redundant,
      n_clusters_per_class=n_clusters_per_class, 
      n_classes=n_classes,
      random_state=random_state
      )
  
  sample_df = pd.DataFrame(sample_classification[0], columns = feature_column_names)
  sample_df[label_column_name] = sample_classification[1]
  
  return sample_df

feature_column_names = [
  'feature_0', 'feature_1', 'feature_2', 'feature_3', 'feature_4',
  'feature_5','feature_6', 'feature_7', 'feature_8', 'feature_9', 
  'feature_10', 'feature_11', 'feature_12', 'feature_13', 'feature_14',
  'feature_15', 'feature_16', 'feature_17', 'feature_18', 'feature_19'
]
label_column_name = "label"
n_samples = 1000
n_classes = 2

sample_df = generate_sample_df(feature_column_names, label_column_name, n_samples, n_classes)
sample_df.head()
feature_0 feature_1 feature_2 feature_3 feature_4 feature_5 feature_6 feature_7 feature_8 feature_9 feature_10 feature_11 feature_12 feature_13 feature_14 feature_15 feature_16 feature_17 feature_18 feature_19 label
0 -6.1069 1.50221 -0.920145 3.11483 0.517572 0.0664958 0.819485 8.26875 -0.172542 -1.63748 0.67169 0.1841 -1.60933 -1.30478 1.82454 0.272891 2.42674 -1.60545 3.65572 -2.57617 0
1 2.90556 -2.39181 -1.98608 -0.291323 1.94349 0.609876 -0.70765 -0.493859 3.88505 -3.80109 -0.237092 -11.1492 -3.62402 4.17303 1.4179 1.71246 1.39182 -1.27753 -2.2001 2.04083 0
2 1.38131 -2.23358 0.193688 -2.10218 0.218239 1.67453 -0.932348 -4.75815 2.12282 0.527165 0.931531 -1.12455 -0.0708599 -0.475601 1.79064 0.0427529 -0.201005 -1.14527 -2.13361 0.40052 0
3 -5.76885 1.44654 -0.0165823 1.06346 -0.348609 -1.76193 -0.236788 -3.22219 0.408844 2.89314 0.830156 4.39721 -0.584138 -0.453076 -0.296942 -1.32787 -0.0741044 -3.36125 -0.484488 -1.93662 0
4 2.10656 -0.528755 -0.662955 0.217316 0.126619 -0.501721 -0.381227 3.60504 -4.00343 -1.6433 -0.833339 -0.451738 -1.32528 -1.19022 -0.282072 0.902531 1.10312 1.17115 -1.32348 -0.249233 1

参考までに、生成した擬似データの基本統計量は以下のとおりです。

sample_df.describe()
feature_0 feature_1 feature_2 feature_3 feature_4 feature_5 feature_6 feature_7 feature_8 feature_9 feature_10 feature_11 feature_12 feature_13 feature_14 feature_15 feature_16 feature_17 feature_18 feature_19 label
count 1000 1000 1000 1000 1000 1000 1000 1000 1000 1000 1000 1000 1000 1000 1000 1000 1000 1000 1000 1000 1000
mean -0.22679 -0.356719 -0.0218113 0.441077 -0.0120808 0.0240415 0.00697184 0.692381 -0.0892546 -0.37842 -0.0255278 -1.12954 0.0680271 0.182109 0.865859 -0.0673906 -0.011046 0.329891 -0.333647 -0.555261 0.501
std 4.65845 2.04645 2.09725 2.04333 0.98827 0.966302 1.02004 4.36257 2.0632 1.99052 1.02409 3.89122 2.2191 2.0807 1.85447 2.08003 1.0157 3.34635 2.24569 2.65902 0.500249
min -14.6509 -8.42079 -8.04211 -7.31673 -3.70133 -3.04058 -3.42982 -12.5212 -8.91695 -7.27698 -3.51781 -12.8647 -7.272 -6.03672 -5.4566 -6.25308 -2.97349 -10.4826 -8.35191 -9.11558 0
25% -3.10476 -1.65953 -1.45208 -0.883971 -0.696745 -0.578649 -0.662017 -2.13695 -1.35213 -1.74197 -0.657473 -3.71965 -1.56645 -1.16425 -0.364591 -1.45298 -0.715649 -2.01443 -1.8739 -2.40565 0
50% -0.334114 -0.450487 -0.00115219 0.54274 -0.015385 0.0260954 0.0292148 0.553981 -0.0622885 -0.392014 -0.0357425 -1.21894 0.0365768 0.328775 0.950246 -0.226534 0.007382 0.239435 -0.334767 -0.533561 1
75% 2.47047 0.925326 1.37644 1.82188 0.663336 0.64216 0.669061 3.55 1.15315 0.904647 0.66061 1.12373 1.64833 1.66251 2.02817 1.33499 0.673261 2.34194 1.19304 1.09123 1
max 18.888 7.00507 6.92699 7.08935 3.34537 3.34539 4.52377 14.9244 6.84834 5.7327 3.20739 11.6939 6.81684 6.88055 6.63172 6.97343 3.26358 16.5605 8.1032 8.86344 1

続いて、学習済みモデルを用意します。ここまでで用意した擬似データを学習データとテストデータに分割します。そして、学習データをモデルに学習させます。採用したアルゴリズムはロジスティック回帰です。scikit-learn*5により実装されているものを利用します。

X = sample_df.drop(columns="label")
y = sample_df["label"]

train_x, test_x, train_y, test_y = train_test_split(X, y, test_size=0.2, random_state=123)

model = LogisticRegression(random_state=123)
model.fit(train_x, train_y)

ここまでで、DiCEで反実仮想説明を生成するために必要な「学習データ(train_xtrain_y)/テストデータ(test_xtest_y)」と「学習済みモデル(model)」の用意ができました。

念のため、学習済みモデルの精度を確認します。ある程度の精度が担保されているモデルでない場合、生成される反実仮想に対する信頼性が失われてしまいます。精度の検証は、あらかじめ一つのデータを学習データ/テストデータに分割したホールドアウト検証とします。評価値としてROC曲線、AUCを算出します。

import matplotlib.pyplot as plt
import japanize_matplotlib
from sklearn.metrics import roc_curve

y_predict_proba = model.predict_proba(test_x)[:,1]
fpr, tpr, thresholds = roc_curve(test_y, y_predict_proba)

plt.plot(fpr, tpr)
plt.plot([0, 1], [0, 1], 'k')

plt.xlabel('偽陽性率')
plt.ylabel('真陽性率')
plt.title('ROC曲線')
plt.legend(["学習済みモデル", "基準"] , bbox_to_anchor=(1.05, 1), loc="upper left")
plt.grid()

# plt.show()

学習済みモデルのROC曲線

ROC曲線を確認した限り、おおよそ問題のない結果であると判断します。

from sklearn.metrics import roc_auc_score

auc = roc_auc_score(test_y, y_predict_proba)
auc
0.8513649136892814

AUCは約0.85と算出されました。こちらもおおよそ問題のない結果であると判断します。ROC曲線、AUCより、モデルは信頼できるものだと判断します。本来はドメインに応じて評価することが望ましいです。もし、問題が考えられる結果だった場合、問題が解消されるまでやり直すことが望ましいです。これ以降、テストデータにおけるラベルは未知の結果であるため、使用しません。

今回は“望ましい状態”の人を増やしたいという動機で反実仮想説明を生成します。つまり、“望ましい状態”でないと予測される人を対象とします。

import dice_ml
from numpy.random import seed


seed(123)

target_df = test_x.copy()
y_predict = model.predict(test_x)
target_df["label"] = y_predict
pre_counter = target_df.query('label == 0')
pre_counter = pre_counter.drop(columns="label")

続いて、DiCEにより生成される反実仮想説明群を生成します。

d = dice_ml.Data(dataframe = pd.concat([test_x, test_y], axis=1),
                 continuous_features=[], 
                 outcome_name = "label",
                 random_seed=123
                 )

m = dice_ml.Model(model=model, 
                  backend="sklearn")

exp = dice_ml.Dice(d, m, method='random')

dice_exp = exp.generate_counterfactuals(
    pre_counter,
    total_CFs= 1,
    features_to_vary=pre_counter.columns.to_list(),
    desired_class = 1,
    random_seed=123
)

diff_df = convert_to_diff_df(pre_counter, dice_exp)
diff_df = diff_df.drop(columns="label")

反実仮想の要約群(入力に対してクラスタ列が付与)を生成します。

n_clusters = 10
summarized_cf = summarize_cf(diff_df, n_clusters)
summarized_cf.head()
feature_0 feature_1 feature_2 feature_3 feature_4 feature_5 feature_6 feature_7 feature_8 feature_9 feature_10 feature_11 feature_12 feature_13 feature_14 feature_15 feature_16 feature_17 feature_18 feature_19 cluster
203 20.4505 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 5
632 0 0 0 0 0 0 0 0 0 0 0 0 -5.71025 0 0 0 0 0 0 0 6
461 0 0 -3.75703 0 0 0 0 0 0 0 0 0 0 0 -0.720825 0 0 0 0 0 1
924 0 -4.99364 0 0 0 0 0 0 0 -2.8706 0 0 -6.7881 0 0 0 0 0 0 0 6
195 0 0 0 0 0 0 0 0 -5.20829 0 0 0 -5.71097 0 0 0 0 0 0 0 6

例えば、クラスタ1の要約(基本統計量)は以下になります。

target_cluster = 1
summarized_cf[summarized_cf["cluster"] == target_cluster].describe()
feature_0 feature_1 feature_2 feature_3 feature_4 feature_5 feature_6 feature_7 feature_8 feature_9 feature_10 feature_11 feature_12 feature_13 feature_14 feature_15 feature_16 feature_17 feature_18 feature_19 cluster
count 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6
mean 0 0 0 -0.225622 0 0 0 0 0 0 0 0 -0.929857 0 0 -0.456884 0 -9.41295 0 -0.78332 1
std 0 0 0 0.552659 0 0 0 0 0 0 0 0 2.27768 0 0 1.11913 0 1.68538 0 1.91873 0
min 0 0 0 -1.35373 0 0 0 0 0 0 0 0 -5.57914 0 0 -2.74131 0 -11.8436 0 -4.69992 1
25% 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -9.95383 0 0 1
50% 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -9.60744 0 0 1
75% 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -8.8791 0 0 1
max 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -6.71359 0 0 1

この要約に対して、例えば、中央値が0ではないcolumn_7はこのクラスタにおいて参考になる特徴であるという判断が可能になります。この判断からKPIとして意思決定者群のcolumn_7を7.721859(中央値)だけ変化させるなどという考案が可能です。

以上により、反実仮想の要約群が生成されます。続けて、予め設定した2つのRQに答える形式で、提案手法の有用性の検証をします。

RQ1:クラスタの数を増やすことによりひとつの施策において参考になる特徴の数は厳選されるか?

一般的に、参考になる指標の種類が少ないほど、比較的低いコストで、比較的高い精度での施策の設計が期待されます。提案手法では反実仮想をクラスタリングしてその要約を施策の設計の参考にします。この時、クラスタごとの要約で参考になる特徴が変化することが考えられます。つまり、クラスタリングによりひとつの施策において参考になる特徴が厳選されることが期待されます。またそのクラスタの数を増やすことによりより厳選されていくことが期待されます。これに対して、本当に厳選されているのかを確認します。ここでは、参考になる特徴の基準を広くします。クラスタにおいて、少しでも差が発生している特徴を指すこととします。逆に全く差が生じていない、基本統計量において、最小値と最大値が0の場合は参考になる特徴ではないとします。

クラスタの数の変化に伴う参考になる特徴の数の変化

縦軸が「参考になる特徴の数」、横軸が「クラスタの数」です。上記のグラフより、大まかにクラスタの数の増加に伴って、各クラスタにおける参考になる特徴の数が減少傾向にあることがわかります。つまり、クラスタリングによりひとつの施策において参考になる特徴の数は厳選されることがわかります。そしてクラスタの数を増やすことで、参考になる特徴の数はより厳選されることがわかります。各クラスタにおいて対応すべき問題が単純になっていると考えられます。つまり、比較的低いコストで、比較的高い精度での施策の設計が期待されます。

この結果が見られた原因として、クラスタリングにより各クラスタにおけるレコードの数の減少が予想されます。参考までに「クラスタの数の変化に伴う各クラスタに属するレコードの数の変化」を確認します。

クラスタの数の変化に伴う各クラスタに属するレコードの数の変化

予想通り、クラスタの数が増えるに伴い、レコードの数が減少しています。

これにより、提案手法により、比較的低い労力で、比較的高い精度の施策の設計が期待されます。

RQ2:設計した施策は複数の意思決定を促進するか?

次に設計した施策が複数の意思決定を促進するかを確認します。現在の手法では施策の質については有識者の力量次第であり不確実な情報です。それゆえ、シミュレーションを設計し、これに基づき検証をします。シミュレーションにおける施策の設計方法を以下のように定義します。

  • 施策の設計方法
    • 反実仮想の要約より参考になる特徴の条件は「中央値が0ではない」と設定
    • 複数に絞り込まれた参考になる特徴からt件をランダムに選択
    • 施策による特徴の変化量は反実仮想の要約より中央値と変化率cを掛け合わせた結果と設定
    • 各意思決定者に対して成功率pで施策が成功(特徴が変化)

上述した条件に従い、シミュレーションを実施します。シミュレーションでは施策の質を変化させて結果を確認します。シミュレーションにおける施策の質を表現する要素(パラメータ)とそのデフォルトとする基準値は以下の通りです。

  • 参考になる特徴の数(基準値:1)
  • 施策の成功率(基準値:0.5)
  • 施策の特徴の変化率(基準値:0.5)

シミュレーションの理解を容易にするために、施策と比較するための有識者による診断の定義を「確実に“望ましい状態”に変化させること」と設定します。施策に対する診断の質の高さを表現しています。

はじめに、参考になる特徴の数の変化に伴う意思決定が成功した件数の変化を確認します。以下に可視化した結果を示します。

施策における参考にする特徴の数の変化に伴う意思決定が成功した件数の変化

縦軸が「意思決定が成功した数」、横軸が「クラスタの数(有識者・施策の数)」です。参考になる特徴の数を変化させて描画させています。また比較対象として「有識者による診断(基準)」をも描画します。参考になる特徴の数を1から3まで変化させたものの、結果は全て変わらず、重なりました。クラスタの数の変化により意思決定が成功した件数が上昇していますが、今回の条件では有用になる場合がないことがわかります。

続いて、施策の成功率の変化に伴う意思決定が成功した件数の変化を確認します。以下に可視化した結果を示します。

施策における成功率の変化に伴う意思決定が成功した件数の変化

以下の条件で基準以上になっています。

これは、今回の条件で、有用になる場合があることがわかります。

最後に、施策の特徴の変化率の変化に伴う意思決定が成功した件数の変化を確認します。 以下に可視化した結果を示します。

施策における特徴の変化率の変化に伴う意思決定が成功した件数の変化

以下の条件で基準以上になっています。

これは、今回の条件で、有用になる場合があることを意味します。

以上より、条件によっては基準、つまり診断以上の効果が見られるという結果となり、提案手法が有用である可能性を確認しました。具体的には同一コストで意思決定の促進が成功した件数が1〜16件(特徴の変化率が1.0/クラスタの数が8の場合)だけ増加しました。これは、言い換えると、同じ数の意思決定の促進を成功させるために必要なコストが1〜16件(特徴の変化率が1.0/クラスタの数が8の場合)だけ減少したことを意味します。つまり、「設計した施策は複数の意思決定を促進するか?」の回答として、「是」である可能性が示唆されました。

以上より、提案手法の有用性が確認されました。

今後の課題と展望

今後の課題と展望について紹介します。最も課題だと感じている、今後の実施が望ましいこととしてより詳細なシミュレーションの設計、もしくは実証実験が挙げられます。有用性の検証では、シミュレーションを実施しましたが、本来の問題はより複雑で、不確実な点があります。また、施策の質を表現する数値も仮に設定したものです。これらは実社会との差があると考えられます。それゆえ、より詳細なシミュレーションの設計が必要になります。もしくは、提案手法を社会に実装した際、それが本当に有用なのかを検証する必要があります。特に提案手法における反実仮想の要約を有識者に伝える際、それが使い物になるのかの検証は大事です。ドメインによって新たな課題も浮かび挙げられます。以上より、実証実験の実施をしたいのですが、これ以上の個人での実施は難しいため、諸々考える、工夫する必要があります。他にも反実仮想の要約から施策を設計する段階についての議論も必要です。そして、設計する施策の評価についての議論も必要です。

本記事のまとめ

本記事では、複数の反実仮想説明に基づく複数の意思決定の促進を目的としたひとつの施策の設計を支援する手法を提案しました。そして、仮に設定したシナリオに基づくシミュレーションにより有用性を検証しました。その結果、提案手法が対象とする特定の条件において、「複数の意思決定の促進を目的としたひとつの施策の設計を支援する」という点での貢献が期待できます。

本記事は追記、修正する可能性があります。ご了承ください。もし意見、質問、指摘などがあれば、以下に記載されている連絡先に連絡をいただけるととても嬉しいです。

docs.google.com

本記事に関するスクリプトは以下で公開しています。

gist.github.com

機械学習における反実仮想説明(Counterfactual Explanations)を生成するライブラリ“DiCE”を活用したサービス開発についての考察

個人的に、機械学習における反実仮想説明(Counterfactual Explanations)*1を生成するPythonのライブラリ“DiCE”に興味を持ちました。そして、DiCEを活用したサービス開発について考察をしました。本記事はその考察についてのメモです。

機械学習における意思決定を促進する説明

機械学習を活用した予測において予測対象者の行動を促す、つまり「意思決定の促進」を目的とする場合があります。このとき、意思決定者(予測対象者)に予測の根拠についての説明を提供することが重要です。そのゆえ、機械学習の文脈で説明についての研究、議論が進められています。

例えば、機械学習モデルの出力の解釈性を扱った手法の例としてSHAP(SHapley Additive exPlanations)というものがあります。

proceedings.neurips.cc

これは各予測の根拠として、各特徴量の貢献度(寄与度)を算出します。これを提供することで、予測に対して納得したり妥当だと感じることにつながります。このとき、その予測への信頼が高まると考えられるため「意思決定の促進」が期待されます。

反実仮想説明を生成するDiCE

DiCEとは?

上記で紹介した手法では、予測を実施したタイミングにおける、機械学習モデルの出力の解釈性として、各特徴量の貢献度などの出力が可能です。ただし、それらはあくまで事実における結果自体の根拠の説明です。もし、予測を覆したいシチュエーションだった場合、意思決定者は具体的にどの特徴量をどれだけ変動させたら良いのかが明らかではありません。意思決定者へ行動を促すためには、どれだけでもわかりやすく説明することが望ましいです。

それに対して「未来の状態を提示する」というアプローチで説明を提供するDiCEを紹介します。DiCEとは、目的とする出力を得るための反実仮想サンプルを生成するアルゴリズムおよびそれが実装されているライブラリです。DiCEはMicrosoft Researchにより開発されています。

www.microsoft.com

反実仮想、つまり「事実と反対のことを想定すること」という概念に基づき、予測結果が覆る(反対になる)特徴量の変動例を提示する説明を反事実的説明(Counterfactual Explanations)と呼びます。DiCEは反事実的説明を生成します。具体的には、DiCEは予測結果に対して、異なる結果が得られたであろう「入力の特徴量を変動させたサンプル」を生成することにより、説明を提供します。この説明を受け取った人は「どの特徴量をどれだけ変動させたら良いのか」といった指針を得ます。これにより「何にどれだけ取り組めば良いのか」といった具体的な行動を促すことが期待されます。「サンプルを生成することによる直接的な材料提供」という点において、他の類似するアルゴリズムとは異なります。

具体的なDiCEの活用シーンについて、README.rstなどでも紹介されている「金融会社におけるローン審査モデル」にDiCEを適用する例を説明します。

github.com

前提として、ある金融会社では、機械学習モデルの分類アルゴリズムを活用したローンの審査をサービスとして提供しているとします。これにDiCEを組み合わせてみると、DiCEは「ローンを申請したが拒否された人」に対して、特徴量を変動させてローンの申請が承認されるサンプルを提示します。例えばサービスは「もしあなたの収入が今より10,000ドル高かった場合、ローンを認めます。」と説明します。この説明を受けた人は、 「どの特徴量をどれだけ変動させたら良いのか」といった指針を得ることが可能です。指針を得ることにより「具体的に何をしたら良いのか」を想像できます。このように、DiCEは 予測に貢献した重要な特徴だけを提供するのではなく、反実仮想を考慮し、意思決定の対象者が望ましい結果を得るために次に何をすべきかを決定するのに役立ちます 。

また、DiCEは実現可能性やドメインの制約を考慮できます。事実サンプルと反実仮想サンプルのベクトルとしての距離を考慮し、実現可能性が高い反実仮想サンプルを提供します。そして、ドメインの制約を考慮して、特定の特徴量を変動の対象外にしたり、特徴量の値の上限と下限を設定することなども可能です。

DiCEの使用方法と動作の紹介

プログラムよってDiCEの使用方法と動作を紹介します。DiCEはPythonで実装されており、OSSオープンソースソフトウェア)としてGitHubで公開されています。

github.com

そして、PythonサードパーティーソフトウェアリポジトリであるPyPIに登録されています。PyPIにおけるDiCEの識別子はdice_mlです。

pypi.org

題材は「タイタニック号の生存者予測」とします。これは以下の点でDiCEの例を紹介するために適していると考えられます。

  • 生存か否かという2クラス分類であること
  • 予測対象者が一方のクラス(生存)に予測されることを望むであろうこと
  • 機械学習チュートリアルとして一般的に知られている例であること

シチュエーションとしては、「生存か否かを判別を事前に判別するサービス」があったとしてその開発を考えます。このシチュエーションは多少無理がある気もしますが、ローンなど良さそうなサンプルデータを見つけられなかったこともあり、あくまでDiCEの例を紹介するためなので流してもらえると幸いです。

実行環境はGoogle Colaboratoryとします。まずはDiCEをインストールします。

!pip install dice_ml

DiCEで反実仮想説明を生成するために、以下2点が必要です。

  • 学習データ/テストデータ
  • 学習済みモデル

はじめにこれらを準備します。「タイタニック号の生存者予測」のデータはseabornから取得します。最低限の前処理を実施します。ホールドアウト検証として、学習データとテストデータに分割します。モデルはロジスティック回帰を選定しました。scikit-learnで実装されているものを利用します。余談ですが、個人的にロジスティック回帰のような予測確率を算出できる手法はDiCEとの相性が良いと考えています。理由は組み合わせることによって説明に有用な情報を増すことができる点です。

import numpy as np
import pandas as pd
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
import seaborn as sns

titanic_df = sns.load_dataset("titanic")

titanic_df = titanic_df.drop(columns="alive")
titanic_df["age"] = titanic_df["age"].fillna(titanic_df["age"].median())
titanic_df["sex"], _ = pd.factorize(titanic_df["sex"], sort=True)
titanic_df["embarked"], _ = pd.factorize(titanic_df["embarked"], sort=True)
titanic_df["class"], _ = pd.factorize(titanic_df["class"], sort=True)
titanic_df["who"], _ = pd.factorize(titanic_df["who"], sort=True)
titanic_df["adult_male"], _ = pd.factorize(titanic_df["adult_male"], sort=True)
titanic_df["deck"], _ = pd.factorize(titanic_df["deck"], sort=True)
titanic_df["embark_town"], _ = pd.factorize(titanic_df["embark_town"], sort=True)
titanic_df["alone"], _ = pd.factorize(titanic_df["alone"], sort=True)

X = titanic_df.drop(columns="survived")
y = titanic_df["survived"]

train_x, test_x, train_y, test_y = train_test_split(X, y, test_size=0.2, random_state=123)

model = LogisticRegression()
model.fit(train_x, train_y_bin)

ここまでで以下の用意が完了しました。

  • 学習データ(train_xtrain_y)/テストデータ(test_xtest_y
  • 学習済みモデル(model

ここからはDiCEによる準備です。dice_ml.Dataでテストデータについての設定をし、dice_ml.Modelで学習済みモデルについての設定をします。

import dice_ml

d = dice_ml.Data(dataframe = pd.concat([test_x, test_y], axis=1),
                 continuous_features=['age', 'fare'], 
                 outcome_name = "survived")

m = dice_ml.Model(model=model, 
                  backend="sklearn")

exp = dice_ml.Dice(d, m)

それでは、試しにテストデータ1件(index == 524)の反実仮想説明を生成します。パラメータについては生成する反実仮想の数をtotal_CFsに、変動対象の特徴をfeatures_to_varyに、変数のとりうる値の上限と下限をpermitted_rangeに設定します。そして、望むクラス(0)をdesired_classに設定します。

pre_counter = test_x.query('index == 524')

dice_exp = exp.generate_counterfactuals(
    pre_counter,
    total_CFs=5,
    features_to_vary=["pclass", "sibsp", "parch", "fare", "embarked", "class", "deck"],
    permitted_range = {"fare":[0, 300]},
    desired_class = 1, 
)

dice_exp.visualize_as_dataframe(show_only_changes=True)

上記のような出力がなされます。もともと、生存予測の結果は否(0)でしたが、例えば、sibspを2、fareを約8.03、classを0に変動させると生存予測の結果が変わるというサンプルが生成されたということが示されています。また、このようなサンプルがtotal_CFsに基づき、5件生成されています。またこれらは、features_to_varypermitted_rangeで設定した制約を満たすものであり、より実現性の高いものが提示されています。

本記事では概要の理解までを目的としているためここまでとします。詳しくはドキュメントをご覧ください。

interpret.ml

以上より、DiCEを活用して反実仮想説明を生成することが可能です。

DiCEを活用したサービスの例

DiCEを活用したサービスの例について考察します。ユーザの意思決定の促進を目的としたサービスにおいてDiCEは有用であると考えられます。以下の例が考えられます。

  • 金融機関における機械学習に基づくローンの審査
  • 医療における機械学習に基づく病気の判定
  • 模擬試験の結果の通知とアドバイス
  • マーケティングにおけるファンクラブ加入者の分析と増加のための施策選定

“DiCEとは?”でも説明しましたが、ローンの審査結果として融資の可否を提示するとき、DiCEは有用です。ローンの審査結果として、拒否となってしまった人がいるとします。その人はローンを受けたいため、現状の課題を解決して再び挑もうとするはずです。そのとき、何をどれだけ改善すれば良いのかという情報が得られたら、それを参考に現状の課題の改善に取り組むことができます。少し先の未来で、その方がローンの審査を通過した場合、サービス提供元も利益が得られます。このように単にユーザのためだけでなく、サービス提供元も利益が得られるといった点で、投資する価値のあるサービスであると考えられます。このようにサービス提供元、提供先のともどもに利益が考えられます。

他にも医療において、機械学習を活用して病気の判定を実施するとき、DiCEは有用です。例えば、特定の病気である、もしくはその可能性が高いと判断された人がいるとします。その人はその病気に打ち勝つために行動を起こすはずです。また、その判定が医療の現場で行われる場合であっても、医師がその病気を治療するはずです。そのとき、何をどれだけ改善すれば良いのかという情報が得られたら、それを参考に現状の課題の改善に取り組むことができます。

予備校や通信教育など学習を提供する企業による模擬試験の結果の通知とアドバイスにもDiCEが有用であると考えられます。例えば大学入試、単純な点数の和ではなく、場合によっては必要十分条件などもあるかと思われます。それらも加味した上で、ある教科の点数をこれだけ上げて、その他の教科の点数をこれだけ上げると合格可能性が高いクラスに変動するということを説明できます。今後の学びの指針を明らかにすることで、現状の課題の改善に取り組むことができます。

上記3点は主にユーザによる、ユーザのための反実仮想説明の提示、つまり主な目的をUX(ユーザーエクスペリエンス)向上として掲げている場合です。サービス提供元も利益が得られる場合もありますが、ユーザが主なトリガーです。。それに対して、サービス提供者がユーザを動かすための施策を設計する参考として反実仮想説明を扱う場合もあり得ます。例えば、ファンクラブ加入者の分析と増加のための施策の設計はそれに当たります。企業はファンクラブ加入者を増やしたいと考えているとします。そのために、ファンクラブ加入者とそうでない人の特徴を分析し、比較してどうしたら、そうでない人をファンクラブ加入者にできるかということを考えます。DiCEを活用すると、各ユーザの何をどれだけ変動させることができたらファンクラブ加入者になる、もしくはファンクラブ加入者に近づけることができるということを理解できます。これは施策の設計におけるターゲットの選定、施策内容の決定、具体的な数値目標の設定の参考として活用が可能であることが考えられます。

上記のようにさまざまなサービスにおいてDiCEは有用です。特に、ユーザの現状に基づいた反実仮想サンプルを生成するため、行動を促すハードルは低くなるはずです。また、時間や費用と相談して、最適な意思決定につなげることはできるはずです。

DiCEを活用したサービス開発における課題と解決策

DiCEを活用したサービス開発について考えたとき、いくつかの課題が考えられました。その解決策として以下の点が考えられました。

  • 設計の工夫による現実に起こりうる反実仮想説明の生成
  • 複数の反実仮想説明に基づく施策の設計

これらについて説明、考察します。

設計の工夫による現実に起こりうる反実仮想説明の生成

DiCEは、各説明変数に対して「変動対象か否か」や「変動量の幅」の設定が可能です。これをサンプルのフィルタリングに用いることで、ドメインを考慮した実現可能性の高く、存在しうるサンプルを提供します。しかし、この時、以下が課題として挙げられます。

  • モデルは特徴に潜在的に存在するルールの解釈が難しいという点
  • モデルは特徴量間の関係を解釈が難しいという点
  • フィルタリングを厳しくした場合、モデルが解釈できない珍しく重要なサンプルを見逃してしまう可能性もあるという点

このような機械に解釈させるのが難しいドメインの制約については、DiCEに限らず機械学習を活用したサービス開発全体に発生しうる課題です。それゆえ、機械学習を活用したサービス開発において、機械学習ドメインとの付き合い方がとても重要であると考えられます。また、それは機械学習やDiCEの進展に任せ切ってしまう話ではなく、サービス全体で対応すべき課題であると考えています。DiCEを活用するサービス開発において、そもそも扱っているデータが何かの誤りで存在しないものだったり、DiCEでも設定しきれないドメインにおける制約を見逃してしまい、存在しないサンプルを提示しまうことも考えられます。以下ではそんなDiCEを活用したサービス開発について記述します。特に、ドメインを考慮した反実仮想クラスの設計による対応について紹介します。DiCEを活用したサービス開発において、ドメインを考慮した反実仮想サンプルの生成の方法を提案します。主な要素は以下の2つです。

  • 特徴間の関係を加味したクラスの設計
  • 特徴の扱い方としての値オブジェクトの採用

特徴間の関係を加味したクラスの設計

ある特徴量の値が、他の特徴量の値に影響を及ぼす場合があります。例えば、人によって、値の上限と下限が違う場合です。その場合、存在し得ない状態を提示してしまう可能性があります。そんな存在し得ない状態はドメインを考慮したクラスの設計により検知できます。

具体的なプログラムの例を紹介します。先に紹介した「タイタニック号の生存者予測」の例より、費用(fare)、年齢(age)のみを抜粋します。

例えば、以下のようなクラス(Sample)を用意しておきます。これは費用と年齢に制約を与えているため、生成されたインスタンスドメインにおける制約を満たすことになります。

import dataclasses

@dataclasses.dataclass(frozen=True)
class Sample:
    fare: float
    age: int

    def __post_init__(self):
      if self.fare <= 0 or 300 <= self.fare:
        raise Exception()
      if self.age <= 0 or 100 <= self.age:
        raise Exception()
      if self.age < 20 and 150 <= self.fare:
        raise Exception()

例えば、DiCEが以下のようなドメインにおける制約を満たすサンプルを生成したとし、これを表現するインスタンスを生成してみます。

sample = Sample(150, 20)
sample 

すると問題なく生成できます。これは反実仮想説明として提示しても良いでしょう。

Sample(fare=150, age=20)

逆に、DiCEが以下のようなドメインにおける制約を満たさないサンプルを生成したとし、これを表現するインスタンスを生成してみます。

sample = Sample(150, 19)

するとこのインスタンスの生成は失敗します。これによりサービスはあり得ないサンプルを説明として提示することを回避します。

---------------------------------------------------------------------------
Exception                                 Traceback (most recent call last)
<ipython-input-20-81b073a3d0d2> in <module>()
----> 1 sample = Sample(150, 19)

<string> in __init__(self, fare, age)

<ipython-input-17-cb29eedcdebf> in __post_init__(self)
     12         raise Exception()
     13       if self.age < 20 and 150 <= self.fare:
---> 14         raise Exception()

Exception: 

このように、特徴間の関係を加味したクラスの設計により、DiCEが生成した反実仮想説明に対して、現実に存在しうる反実仮想説明のみを抽出が可能であると考えられます。

特徴の扱い方としての値オブジェクトの採用

DiCEでは特徴に対して、変動の有無や変動の幅を設定できます。ただし、その設定もドメインに忠実に適応できない場合があると考えられます。それゆえ、設計でカバーすることも望ましいと考えられます。DiCEが生成した反実仮想サンプルに対して、現実に存在しうる反実仮想サンプルのみを抽出する方法として、値オブジェクトが考えられます。値オブジェクトとは、ドメイン駆動設計の要素のひとつです。簡単に説明すると、プログラムの中で、その値の振る舞いをドメインに基づくのもに制限するという設計です。上述したクラスの設計の例と同様に、これによりよりドメインに忠実に、異常な状態を検知できます。

具体的なプログラムの例を紹介します。

import dataclasses

@dataclasses.dataclass(frozen=True)
class fare:
    value: float
    def __post_init__(self):
      if self.value <= 0 or 300 <= self.value:
        raise Exception()

@dataclasses.dataclass(frozen=True)
class age:
    value: int
    def __post_init__(self):
      if self.value <= 0 or 100 <= self.value:
        raise Exception()

例えば、DiCEが以下のような全ての特徴量がドメインにおける制約を満たす値であるサンプルを生成したとし、それぞれを値オブジェクトとしてインスタンスを生成してみます。

fare = Fare(150)
fare

age = Age(20)
age

すると問題なく生成できます。これは実現可能性の高い説明として提示しても良いでしょう。

Fare(value=150)
Age(value=20)

逆に、DiCEが以下のような特徴量にドメインにおける制約を満たさない値を持つサンプルを生成したとし、それぞれを値オブジェクトとしてインスタンスを生成してみます。

fare = Fare(1000)
fare

するとこのインスタンスの生成は失敗します。これによりサービスはあり得ないサンプルを説明として提示することを回避します。

---------------------------------------------------------------------------
Exception                                 Traceback (most recent call last)
<ipython-input-33-4dbe6b6b540a> in <module>()
----> 1 fare = Fare(1000)
      2 fare

<string> in __init__(self, value)

<ipython-input-27-2319445039c4> in __post_init__(self)
      6     def __post_init__(self):
      7       if self.value <= 0 or 300 <= self.value:
----> 8         raise Exception()
      9 
     10 @dataclasses.dataclass(frozen=True)

Exception: 

このように、値オブジェクトを使うことで、DiCEが生成したサンプルに対して、現実に存在しうるサンプルのみをサービスとして扱うことが可能であると考えられます。

また、値オブジェクトは上述したクラスの設計と組み合わせて、クラスが持つプロパティ全てを値オブジェクトにすると効果的であると考えられます。例えば以下のような記述が考えられます。

@dataclasses.dataclass(frozen=True)
class Sample:
  fare: Fare = fare
  age: Age = age
  
  def __post_init__(self):
      if self.age.value < 20 and 150 <= self.fare.value:
        raise Exception()
fare = Fare(150)
age = Age(20)
sample = Sample(fare, age)
sample
Sample(fare=Fare(value=150), age=Age(value=20))

これらを実施することで、機械学習を活用したサービス開発における安全性・信頼性といった品質保証の担保にも貢献が可能です。

複数の反実仮想説明に基づく施策の設計

マーケティングにおけるファンクラブ加入者の分析と増加のための施策選定」のように多くの人を動かすこと、つまり複数の意思決定の促進を目的とする場合があります。前提として、人を動かすために、施策を設計します。施策の設計のために、反実仮想説明が有用であると考えられます。この時、DiCEが生成する反実仮想説明は一人を対象とした説明です。もちろん一人の反実仮想説明を確認して、特定の説明変数の値を変化させるためにどうすれば良いのかを考えることができると思います。しかし、複数人を動かそうとした場合は反実仮想説明を生成した後、施策を設計するまでにハードルがあることが考えられます。

まずは、各反実仮想説明を理解、解釈し、要約したうえで、施策を設計する必要があります。これを実現する手法(処理)が必要です。その理由はマーケティングにおいて、複数の意思決定を促進させるための各反実仮想説明への対応が現実的ではないことです。

上述した手法が確立されたとして、次は「その施策によって意思決定を促す人の数を最大化させる要望」も生まれます。例えば、ファンクラブ加入者をX人にすることをKGI(経営目標達成指標)とした時、それに向けて設定する施策が中間数値指標としてのKPI(重要業績評価指標)となります。このKPIを設定する際、最も効果がある施策は何なのかを設定することが難しいと考えられます。

それゆえ、「複数の反実仮想説明に基づく施策の設計」に関する議論が重要になってくると考えています。以下、「複数の反実仮想説明に基づく施策の設計」に関して私が考えていることを紹介します。まずは概念図を以下に示します。

複数の反実仮想説明に基づく施策の設計の概念図

予測対象1件に対して、m件の反実仮想説明を生成し、その合計をn件とします。これは施策の設計の参考になる情報であり、これらを包括して理解、解釈することで複数の反実仮想の実現につなげる施策を設計する際のヒントとなります。反実仮想×nの生成はDiCEにより実現が可能です。議論すべきは「反実仮想×nから施策を設計する処理」です。まずは反実仮想×nを要約する必要があります。

Minimum Viable Productに基づき実用最小限の手法を考えました。処理の流れを以下に示します。

  1. 反実仮想をクラスタリング(例:k平均法)
  2. クラスタをサマライズ(例:中央値)
  3. クラスタごとにサマライズされた情報を参考にしてマーケティング担当者が施策を設計

人が介入する必要はありますが、これにより「反実仮想×nから施策を設計する処理」がなんとか実現できるのではと考えています。これをベースラインにして、これらの方法については思案・調査中です。もし有識者の方から意見をいただけるととてもありがたいです。

反実仮想説明の関連研究

事実から反実仮想まで変動させるための状態遷移の考慮

事実から反実仮想を目指す場合、そのベクトルが直線でない場合も考えられます。具体的には「暗黙的に存在している状態」を経由する必要がある場合です。これを正しく理解しないと、事実を反実仮想まで変動させるのに想定以上の苦労がかかったり、そもそも不可能だったりする可能性が考えられます。それゆえ、事実から反実仮想まで変動させるための状態遷移を考慮した手法やサービスの設計が必要になります。このような背景から、事実から反実仮想まで変動させるための状態遷移を考慮した手法を提案する研究もなされています。

富士通研究所北海道大学は、実現の可能性と順序を考慮した適切な変化方法を見いだすことが課題であると捉え、反実仮想説明の考え方に基づき、事実を反実仮想に変動させる手順として、行動自体とその実現の順序を提示する方法を提案した。

pr.fujitsu.com

今回、両者による共同研究において開発した新技術は、反実仮想説明(注5)という考えに基づき、属性変更におけるアクションとその実施順序を手順として提示します。過去の事例の分析を通して非現実的な変更を避けつつ、属性値の変更がほかの属性値に与える因果関係などの影響をAIが推定し、それに基づいて実際に利用者が変更しなければならない量を計算することで、適切な順序、かつ一番少ない労力で最適な結果が得られるアクションの提示を可能としました。

上記の研究では「順序」を考慮しているという点により、実現可能性が高い反実仮想説明の生成されます。これにより、意思決定の促進が期待できます。

本記事のまとめ

本記事では、機械学習における反実仮想説明を生成するライブラリ“DiCE”を紹介しました。また、DiCEのサービスへの活用例を紹介しました。そして、DiCEを活用したサービス開発における課題と解決策について、以下2点を紹介しました。

  • 設計の工夫による現実に起こりうる反実仮想説明の生成
  • 複数の反実仮想説明に基づく施策の設計

関連研究でも紹介しましたが、ドメインへの対応が重要だと考えられます。設計で対応できることもあるので、ぜひ検討をしてください。そして、特定のシチュエーションにおいて施策の設計が必要とされると考えられます。今後、考えていこうと思います。

本記事は追記、修正する可能性があります。ご了承ください。もし意見、質問、指摘などがあれば、以下に記載されている連絡先に連絡をいただけるととても嬉しいです。

https://daikikatsuragawa.github.io/

*1:本記事ではCounterfactual Explanationsを反実仮想説明と和訳します。他にも反事実的説明、反実仮想的説明、反実仮想的な説明として和訳されている場合があります。

アソシエーションルールマイニングに基づく推薦を民主化するライブラリ「AutoARM」をリリースするまでの記録

はじめに

先日、アソシエーションルールマイニングに基づく推薦を民主化するライブラリ「AutoARM」をリリースしました。AutoARMはPythonで実装されており、PyPIで公開されています。

pypi.org

またオープンソースソフトウェア(OSS)としてGitHubで公開されています。

github.com

ゼロからアイデアを生み出したことからリリースするところまででさまざまな経験をしました。そこで、さまざまな学びがありました。本記事はAutoARMをリリースするまでの記録です。

開発に至る動機

AutoARMの開発に至る動機は3つあります。

“アソシエーションルールマイニングに基づく推薦”の提案

開発に着手する直前、「今こそ“アソシエーションルールマイニングに基づく推薦”が活きる時代なんじゃないか?」と思っていたことです。詳細については以下のブログを読んでください。

daikikatsuragawa.hatenablog.com

簡単にまとめると以下の点で改めて有用な手法だと思っています。

  • 推薦の根拠を説明可能
  • 推薦を実施したい相手に紐づく情報がなくても推薦可能

そして、上記2点は、データ提供・活用が実施されていないサービスに対して有効だと考えられます。そして、日本におけるデータ提供・活用をブレイクスルーするひとつのキッカケになるのではないかと期待しています。

また、ライブラリになっていた場合、プライベートにおける取り組みではあるものの、現在所属している組織でも簡単に再利用が可能になります。それゆえ、現在所属している組織における新規サービス・機能の提案の火種になることも期待していました。

過去の自分(=未来の誰か)の支援

学生時代、利用状況、推薦に関する研究をしていました。そこでアソシエーションルールマイニングに基づく推薦を実装していました。

https://ieeexplore.ieee.org/document/8445832ieeexplore.ieee.org

実は当時、実装がとても苦手で、比較的多くの時間をかけて保守の手間を無視する危なっかしいスクリプトを書いていました。というのも、アソシエーションルールマイニングに基づく推薦ってライブラリとして実装されていなかった(もしくは見つからなかった)のです。

もっと前にも似たようなことがありました。テーブルデータを用意した状態で、「こんな手法で予測したい」という方針は決まっているものの、スクリプトを書けないが故に手が止まってしまうということです。この日は状況共有の機会の前日だったこともあり、すぐにでも実現したいと思い、詳しい同期にスクリプトを書いてもらいました。すると一瞬でやりたいことが実現できました。この出来事をきっかけに学術研究、特に機械学習やデータ分析を必要とするものにおいて、技術がなく実現できない、提案できないアイデアがあるんじゃないかと思っていました。ただ近年、AutoMLが提案されており、機械学習という技術は民主化されています。これにより、上述した過去の自分が助かります。過去の自分の悩みは、未来の自分や誰かの悩みです。AutoMLにより未来の誰かの問題の解決に進んでくのではないかと思っています。

AutoML開発の動機についても技術にしてもとても共感していました。そんな中、“アソシエーションルールマイニングに基づく推薦”って実装されていない、つまり民主化されていないと思いました。個人的に有用な手法だと思っているため、過去の自分(=未来の誰か)のためにも実装し、誰もが利用できる状態にすることが望ましい状態と思いました。

(オーナーとして)OSSの開発

ソフトウェアエンジニアとして、意味のあるOSSを、自分がオーナーとして開発したいと思っていました。下記より既にOSSはリリースしたことがあるのですが、やはり、自分が貢献したいと思っているデータ分析、機械学習に関して、取り組みたいと思いました。

daikikatsuragawa.hatenablog.com

とにかく、ひとつを作りきることによって得られる経験値の大きさを期待していたため、単純にやってみたかったということがあります。OSSのスタンスについてもとても共感しています。完璧なものを開発しきるというよりは、「やりたいことの提示」を一番の目的としたいと思いました。自分よりも素晴らしいソフトウェアエンジニアが世界中にいることもあり、自分の思いに共感してくれた人が何かを直してくれたら、それがOSSのあるべき姿なんじゃないかと思ったためです。そのため、Minimum Viable Product(MVP)の考えに基づき、必要最小限の実装で、体験・価値を実現したいと思いました。このような思いを持ってOSS、特にPythonのライブラリを開発したいと思いました。

ライブラリの内容の設定

上述した動機に基づき、「アソシエーションルールマイニングに基づく推薦を民主化する」といったテーマでライブラリを開発しようと考えました。そこで、既存ライブラリを調査し、以下のような内容を定めました。

  • アソシエーションルールマイニングに基づく推薦に基づく過程を実装
    • テーブルデータの入力
    • 頻出パターンマイニング
    • アソシエーションルールマイニング
    • ルールに基づく推薦
  • 過程が首尾一貫しており誰でも簡単に利用可能
    • 参考とするインタフェースはAutoGluon*1

既存ライブラリに対する新規性は以下2点です。

  • アソシエーションルールマイニングに基づく推薦を対象
  • (アソシエーションルールのライブラリに対して)推薦までを実装

以上より、開発に着手しました。

推薦の流れ

新規性としてもアピールしていた「推薦」の流れを紹介します。MVPに基づき、最もシンプルな方法はこれだなと思う方法を実装しました。インプットは推薦対象の現状のアイテムリストと、事前に出力したルール群、推薦の基準とするメトリック(信頼度またはリフト)です。アウトプットは推薦の参考になるルールN件です。流れを以下に示します。

  • ルール群の結論部を分割(多:1のルールに)
  • 入力したアイテムリストと一致する条件部を持つルールを特定
  • 入力したアイテムリストと結論部が一致するルールを除外
    • この手順はスキップも可能
  • メトリックとして指定された信頼度またはリフトでルールを並び替え
  • 上位のルールに対して結論部が重複するルールを除外
  • 指定されたサイズ(N件)のルールを出力

詳細や実装についてはREADMEやソースコードを見ていただけると幸いです。

これにより、AutoARMは推薦の参考となるルールを出力します。ミソはアイテムではなく、ルール自体を出力することです。つまり、そのアイテムを推薦する根拠を残しています。これにより、根拠を説明しつつアイテムを推薦するため、相手の行動を促す推薦サービスが実現されます。

AutoARMの紹介

上記のような過程を経て開発を進めてAutoARMが完成しました。ざっくりと紹介します。

まず始めにデータを用意しておきます。サンプルとして適当なものを生成しています。

import pandas as pd

sample_dataset = {
    'transaction_id':
    [1, 1, 1, 2, 2, 3, 3, 3, 4, 4, 5, 5, 5, 6, 6, 6, 6, 7, 7],
    'item_id': [
        "X", "Y", "Z", "X", "B", "Y", "A", "C", "A", "C", "X", "Y", "Z",
        "X", "Y", "B", "A", "X", "B"
    ],
}
df = pd.DataFrame.from_dict(sample_dataset)

つまりこのようなテーブルデータが必要です。transaction_id列、item_id列`にあたる列さえあれば他に何かあっても問題はありません。列名は後でしているため、異なっていても問題はありません。

transaction_id item_id
1 X
1 Y
1 Z
2 X
2 B
... ...
7 X
7 B

ここからAutoARMについてです。まずはインストールが必要です。Pypiで公開されているため、以下でインストールが可能です。

pip install autoarm

以下により、アソシエーションルールマイニングに基づく推薦を実現するオブジェクトを生成可能です。import文を除くと、4行です。

from autoarm import AssociationRules, Dataset, FrequentItemsets, Recommender

dataset = Dataset(df, "transaction_id", "item_id")
frequent_itemsets = FrequentItemsets(dataset, min_support=0.01)
association_rules = AssociationRules(frequent_itemsets,
                                     metric="confidence",
                                     min_threshold=0.1)
recommender = Recommender(association_rules)

以下により、推薦が可能です。

items = ["X", "Y"]
recommend_rules = recommender.recommend(items, n=3, metric="confidence")
recommend_rules

以下の表が出力されます。

rank antecedents consequents support confidence lift
1 (X, Y) (Z) 0.285714 0.666667 2.333333
2 (X) (B) 0.428571 0.600000 1.400000
3 (Y) (A) 0.285714 0.500000 1.166667

例えば、この出力に基づいて「既にあなたが購入を検討しているXとYを購入した人の66%がZも購入しています。Zを購入してみてはどうでしょうか?」といった説明を加えた推薦が実現されます。また、XとYの購入を検討している推薦を実施したい相手に紐づく情報がなくてもこのような推薦が実現されます。良いですね。

以上より、簡単にアソシエーションルールマイニングに基づく推薦が実現されます。

今後の展望

正直なことを言うと、“アソシエーションルールマイニングに基づく推薦を民主化”するためにまだまだ追加することが望ましい機能があると思っています。例えば以下です。

  • さまざまな形式の入力データの対応
  • パラメータの自動調整
  • アソシエーションルールマイニングに基づく別の推薦手法の実装

そして、OSSとしてもっとこんな状態にしたかったと思っていることがあります。例えば以下です。

  • Poetryによるプロジェクト管理
  • GitHub Actionsの有効活用
  • 値オブジェクト*2の実装

自分でガンガン開発してしまうのもいいのですが、せっかくGitHubで公開して、指針もあるため、プルリクエストを期待してひとまず手を動かすのは止めようかと思っています。特に、自分にはレビューしてくれる人がいないため、自らバグを混入してしまう可能性もあるためです。また、もっとやりたいこともあるため、世界のソフトウェアエンジニアに任せてみたいと言うこともあります。

そのため、ひとまず開発自体は小休止して、どのようにサービスに組み込むのかなどについて注力して考えようと思います。

最近のお寿司屋さん、タッチパネルでの注文が浸透していますよね。こことかで活躍しそうだなとか妄想しています。

おわりに

アソシエーションルールマイニングに基づく推薦を民主化するライブラリ「AutoARM」をリリースするまでの記録を紹介しました。是非とも使ってください!フィードバックください!コントリビュートしてください!スターください!

おまけ

本文に書くほどではないが、遺しておくべきだと感じた内容について、サクサクと記録します。

  • テストはとても大切(ユニットテスト〜運用テスト)
    • ユニットテストに何度も救われた
    • 運用テストに何度も救われた
    • しかしテスト設計はまだまだ未熟
  • AIソフトウェアのテストは非常に難しい
    • パフォーマンスの担保
    • 「AIソフトウェアのテスト」が参考になる?(めどがついたため未読)

  • 現実的な利用状況に適した速度およびアルゴリズム
    • プロトタイプ作成時はすごく遅かった(レコメンドに数分…)
    • 問題を解決するために別のライブラリ「df4loop」も誕生した

pypi.org

  • 初めてのリリース(Pypiへの公開)はすごく緊張する
    • 10分ほど精神を統一したのちヤケになって「ポチッ!」
    • 多分経験値は大きい
  • 社内LT会で少しだけ紹介した
    • 有識者・初学者それぞれから良いフィードバックが得られました
    • GUIで見せたかった…
    • サービスのイメージまで伝えたかった…
  • “サクっと”API作れるようになりたい
    • 一回テンプレートみたいなものを作るか…
    • 欲を言うとDocker、Kubernetes
  • “サクっと”ウェブアプリケーションを作れるようになりたい
    • Django
    • Flask?
    • 一回テンプレートみたいなものを作るか…(再)
    • 欲を言うとDocker、Kubernetes…(再)
  • 開発環境はほとんどGitpod+Colab!

*1:Amazon Web Services - LabsのAutoMLライブラリgithub.com

*2:ドメイン駆動設計を担う要素のひとつ

アソシエーションルールマイニングに基づく推薦のすゝめ2021

はじめに

2021年某日…「今こそ“アソシエーションルールマイニングに基づく推薦”の時代なんじゃないか!?」と感じました。その思いを綴ります。

アソシエーションルールマイニングに基づく推薦

まずはアソシエーションルールマイニングに基づく推薦についてサラッと説明します。

アソシエーションルールとはアイテム間の関連性の規則を指します。以下のように表現されます。

 \displaystyle
A \rightarrow B

これは「事象Aが起こると事象Bが起こる」という意味です。Aは条件部、Bは結論部と呼ばれます。例えば、「パンとバターを購入した人はミルクを購入する」という事象は“パンとバターを購入”→“ミルク購入”というルールとして表現されます。

任意のアソシエーションルールが有用か否かを判断する指標として、様々な評価値があります。どれだけ一般的なルールかを計る評価値として支持度(Support)があります。

 \displaystyle

支持度(Support)=
\frac{条件部(A)と結論部(B)を含むデータ数}{全データ数}

どれだけ関係の強いルールかを計る評価値として信頼度(Confidence)があります。

 \displaystyle

信頼度(Confidence)=
\frac{条件部(A)と結論部(B)を含むデータ数}{条件部(A)を含むデータ数}

信頼度に対して結論部の発生する頻度を考慮した評価値としてリフト値(Lift)があります。

 \displaystyle
リフト値(Lift)=
\frac{\frac{条件部(A)と結論部(B)を含むデータ数}{条件部(A)を含むデータ数}}{\frac{結論部(B)を含むデータ数}{全データ数}}

上記の評価値に基づいて、推薦に有用なルールを特定することが可能です。

膨大なデータからルールと評価値を出力する手法としてアソシエーションルールマイニングがあります。主に利用されているアルゴリズムであるaprioriにより、一定の支持度を超える有用なルールに絞ることも可能です。

アソシエーションルールマイニングによって得られたアソシエーションルールに基づいて以下のような順序で推薦ができると思われます。

  1. ルール群の条件部を推薦対象の現状でフィルタリング
  2. 信頼度(もしくはリフト値)に基づき結論部をランク付け
  3. 状況に合わせて上位N件を推薦

例えば、ECサイトにおいて「Aの他に何か買おうか」と思案している顧客に「Bがオススメ。(信頼度に基づき)Aの購入者の90%が同時に購入しています。」と伝えることができます。

これが本記事で紹介したい「アソシエーションルールマイニングに基づく推薦」です。

その理由(わけ)とは…

アソシエーションルールマイニング自体は素晴らしい手法です。ただし、最近発表されたものではなく、近年では様々な手法が提案されており、推薦の改善、進化が進んでいます。そんな中、2021年某日…「今こそ“アソシエーションルールマイニングに基づく推薦”の時代なんじゃないか!?」と思った理由を綴ります。その主な理由は以下2点です。

推薦の根拠を説明可能

アソシエーションルールマイニングはルールおよび評価値がシンプルで、どんな人でも理解しやすいため、推薦の根拠を説明できると思われます。Bを推薦するときに、その根拠として、「A→B」というルールに基づき、あなたがAだからという旨を伝えることができます。そして、信頼度に基づき、信頼度がX%である場合、「AのX%がBでもある」という旨も伝えることができます。「説明可能であること」は相手に行動を促すのために良い要素であると考えられます。「説明可能であること」までもサービスとして提供したい場合、有用な選択になるんじゃないかと考えています。

推薦を実施したい相手に紐づく情報がなくても推薦可能

アソシエーションルールマイニングに基づく推薦の場合、推薦を実施したい相手に紐づく情報が必要ありません。例えば、他の推薦手法では、相手の情報(生年月日、性別など)を説明変数として利用する場合があります。それに対して基本的なアソシエーションルールマイニングに基づく推薦では状態(買い物カゴの中身、閲覧履歴など)と過去のルールを組み合わせることで推薦が実現されます。つまり、匿名性が必要な場面や新規顧客にも対応可能です。

日本におけるデータ提供・活用文化の醸成に貢献?

近年モバイル決済が浸透しています。これにより消費者の特徴や行動に基づき様々な恩恵を受けることができる未来*1が想像できます。しかし、データ提供におけるプライバシーなどに関するリスクは存在するため、それを考慮した上で恩恵がない、想像できていない方々がいると言う現状*2もあるかと思います。特に上記2点は、そのような思いを持っている方々にもアプローチが可能です。それゆえ、現状を打破し、今後のデータ活用が浸透するキッカケになるのではないかと考えています。特に、データ提供・活用が実施されていないサービスに対して有効だと考えられます。そして、特に日本におけるデータ提供・活用をブレイクスルーするひとつのキッカケになるのではないかと期待しています。

おわりに

「今こそ“アソシエーションルールマイニングに基づく推薦”の時代なんじゃないか!?」と思ったその思いを綴りました。「推薦の根拠を説明可能」、「推薦を実施したい相手に紐づく情報がなくても推薦可能」という理由より、シチュエーションによってはニーズに応えられる手法なんじゃないでしょうか?今こそ、検討をしてみてはどうでしようか?