関連ベクトルマシンをPythonコードを交えて紹介
はじめに
PRML(Pattern Recognition and Machine Learning)で関連ベクトルマシンについて学んだ内容をまとめて、実際のデータを使って学習しました。主に7章2節の内容です。学習アルゴリズムの全体像を示すことを意識したので、導出は端折っている部分が多いです。詳しい解の導出は、PRMLの本文や演習問題にあります。そのほかに参考になるものは最後にまとめてあります。
関連ベクトルマシンのアルゴリズム
変数一覧
- 訓練データの数
N - 基底関数の数
M - 訓練データの説明変数
\mathbf{X} = \left\{\mathbf{x}_1, \mathbf{x}_2, \cdots \mathbf{x}_N\right\}, \mathbf{x}_n = (x_{n, 1}, x_{n, 2}, \cdots, x_{n_D})^T - 訓練データの目的変数
\mathbf{t} = \left\{t_1, t_2, \cdots t_N\right\} - 基底関数
\mathbf{\phi}(\mathbf{x}_n) = (\phi_1(\mathbf{x}_n), \cdots \phi_{M}(\mathbf{x}_n))^T - 重み
\mathbf{w} = (w_1, \cdots, w_{M})^T - 新たな説明変数
\mathbf{x}
また以下の計画行列
目的とモデル
最終的な目標は以下の予測分布を求めることです。訓練データから、新たな説明変数に対する目的変数の分布を求めます。
今回は以下のような線形回帰モデルを使用します。
これ以降、見やすさのために
事前分布と事後分布
パラメータ
ここで、
ここで、
予測分布とエビデンス近似
予測分布は以下のようにかけます。
しかし、この積分をそのまま計算するのはこんなんです。そこで、まず
ここで、
\alpha, \beta の最尤推定
この分布の対数
この式に従って、
もう一つの定式化
ここで、スパース性係数
実装
環境
- Google Colaboratory
- Python 3.7.13
- numpy 1.21.6
- pandas 1.3.5
- sklearn 1.0.2
import文
import numpy as np
import pandas as pd
from sklearn.metrics import r2_score
from sklearn.model_selection import train_test_split
np.random.seed(28)
np.set_printoptions(precision=3)
使用するデータセット
scikit-learnの「糖尿病データセット」というトイデータセットを使います。このデータセットでは、年齢、性別、BMI、平均血圧、6つの血清検査測定値という10の説明変数から、1年後の糖尿病の進行状況を予測します。データ数は442です。このうちの8割に当たる353個を訓練用データとし、残りをテスト用データとします。また、データは標準化しておきます。
input_variables = ['age', 'sex', 'bmi', 'map', 'tc', 'ldl', 'hdl', 'tch', 'ltg', 'glu']
data_url = 'https://www4.stat.ncsu.edu/~boos/var.select/diabetes.tab.txt'
raw_df = pd.read_csv(data_url, sep="\s+")
diabetes_df = raw_df.set_axis(input_variables + ['y'], axis=1)
t_origin_df = diabetes_df.loc[:, 'y']
t_df = (t_origin_df - t_origin_df.mean()) / t_origin_df.std()
x_origin_df = diabetes_df.loc[:, input_variables]
x_df = (x_origin_df - x_origin_df.mean()) / x_origin_df.std()
train_x_df, test_x_df, train_t_df, test_t_df = train_test_split(x_df, t_df, test_size=0.2)
train_x = train_x_df.to_numpy(copy=True)
train_t = train_t_df.to_numpy(copy=True)
test_x = test_x_df.to_numpy(copy=True)
test_t = test_t_df.to_numpy(copy=True)
diabetes_df.head()
index | age | sex | bmi | map | tc | ldl | hdl | tch | ltg | glu | y |
---|---|---|---|---|---|---|---|---|---|---|---|
0 | 59 | 2 | 32.1 | 101.0 | 157 | 93.2 | 38.0 | 4.0 | 4.8598 | 87 | 151 |
1 | 48 | 1 | 21.6 | 87.0 | 183 | 103.2 | 70.0 | 3.0 | 3.8918 | 69 | 75 |
2 | 72 | 2 | 30.5 | 93.0 | 156 | 93.6 | 41.0 | 4.0 | 4.6728 | 85 | 141 |
3 | 24 | 1 | 25.3 | 84.0 | 198 | 131.4 | 40.0 | 5.0 | 4.8903 | 89 | 206 |
4 | 50 | 1 | 23.0 | 101.0 | 192 | 125.4 | 52.0 | 4.0 | 4.2905 | 80 | 135 |
基底関数
基底関数には各データ点を中心とするガウスカーネルを用いています。よって、基底関数の数は訓練用データの数と同じく353個です。
def gauss_kernel(gamma=0.1):
gamma = gamma
def closure(x, x_star):
return np.exp(-gamma * np.dot(x - x_star, x - x_star))
return closure
関連ベクトルマシン
ここでは、もう一つの定式化で示した更新アルゴリズムを用いています。
class RVMReg:
def __init__(self, kernel):
self.kernel = kernel
self.xs = None
self.ts = None
self.alphas = None
self.beta = None
self.m = None
def fit(self, xs, ts, iter_max=1000):
INF = 1e18
self.xs = xs
self.ts = ts
N = len(xs)
Phi = np.array([np.array([self.kernel(x1, x2) for x1 in xs]) \
for x2 in xs])
self.alphas = np.full(N, INF)
self.beta = 0.1 * np.var(self.ts)
for iter in range(iter_max):
params = np.hstack([self.alphas, self.beta])
update_flg = True
# アルファの更新
for i in range(N):
phi_i = Phi[:, i]
if update_flg:
inv_A = np.diag(1 / self.alphas)
C = np.identity(N) / self.beta + np.dot(np.dot(Phi, inv_A), Phi.T)
inv_C = np.linalg.inv(C)
Qi = np.dot(np.dot(phi_i.T, inv_C), ts)
Si = np.dot(np.dot(phi_i.T, inv_C), phi_i)
qi = self.alphas[i] * Qi / (self.alphas[i] - Si)
si = self.alphas[i] * Si / (self.alphas[i] - Si)
if qi**2 > si:
update_flg = True
self.alphas[i] = si**2 / (qi**2 - si)
else:
if self.alphas[i] == INF:
update_flg = False
else:
update_flg = True
self.alphas[i] = INF
# ベータの更新
A = np.diag(self.alphas)
Sigma = np.linalg.inv(A + self.beta * np.dot(Phi.T, Phi))
self.m = self.beta * np.dot(np.dot(Sigma, Phi.T), ts)
denominator = ts - np.dot(Phi, self.m)
denominator = np.dot(denominator.T, denominator)
gammas = np.array([1 - self.alphas[j] * Sigma[j, j] for j in range(N)])
numerator = N - np.sum(gammas)
self.beta = numerator / denominator
# 収束したら終了
if np.allclose(params, np.hstack([self.alphas, self.beta])):
print(f"Converged iteration: {iter+1}, The number of non-infinite alphas: {np.sum(self.alphas != INF)}")
break
if iter == iter_max - 1:
print("No convergence was achieved")
def predict(self, x_star):
phi_x = np.array([self.kernel(x, x_star) for x in self.xs])
return np.dot(self.m, phi_x)
結果
ガウスカーネルのパラメータは、
model = RVMReg(gauss_kernel(0.018))
model.fit(train_x, train_t)
pred_train = np.array([model.predict(x) for x in train_x])
pred_test = np.array([model.predict(x) for x in test_x])
print(f"train_r2: {r2_score(train_t, pred_train)}, test_r2: {r2_score(test_t, pred_test)}")
Converged iteration: 23, The number of non-infinite alphas: 6
train_r2: 0.5370609799956878, test_r2: 0.5183172061767674
参考
PRMLでReferenceとしてある論文です。もう一つの定式化で示したアルゴリズムが詳しくのっています。この記事では、この論文と全く同じように実装をしているわけではありません。
Tipping, M. E. and A. Faul (2003). Fast marginal likelihood maximization for sparse Bayesian models.
この記事では、もう一つの定式化で示した更新式を用いましたが、
PRML第7章 回帰問題に対する関連ベクトルマシン Python実装
Discussion