ラプラス正則化(Laplacian Regularization)を用いた半教師あり学習をPythonで実装 - 6月 21, 2019 こんにちは、ぐぐりら(@guglilac)です。 今回は、半教師あり学習の手法の一つであるラプラス正則化について書いてみます。 半教師あり学習とは、データの一部のみラベル(正解データ)がついていて、残りは入力データしか与えられないような状況で、精度の良い予測を行うモデルを学習することを指します。 ラベルがついているデータは少量で、教師なしデータが大量にある、という問題設定はとても現実的です。 ラベルをつけるコストは大きく、簡単に用意できないということがよくあります。 ラプラス正則化に限らず、半教師あり学習では限られたラベルのついた教師ありデータを用いて、大量の教師なしデータを有効活用していくか、というところがポイントになってきます。 (大量の教師なしデータすらない場合はfew shot learningとか、deep learningで研究されているようなまた違った分野になるのだと思います。) まず、ラプラス正則化の説明からはじめて、次に人工データを生成してラプラス正則化をガウスカーネルモデルにつけて最小二乗分類をPythonで実装します。 最後にその実験結果を載せます。 ## ラプラス正則化 ラプラス正則化はどのようにして教師なしデータを有効活用するか、というところが注目ポイントです。 ラプラス正則化では、「近くにある入力データ同士は同じラベルを持つはず」という仮定のもと、lossを設計します。 ラベルのあるデータの近くのデータはそのラベルと同じラベルを持つはず、ということですね。これが全データに伝搬していくことで、存在しないラベルに対しても「まあこのデータはこのラベルがついているはずだろう」みたいなことをモデルが学習してくれる、という気持ちです。 具体的なlossは以下のようになります。 $$ \sum_{i=1}^{N_l}(y_i - f_{\theta}(x_i))^2+\lambda|\theta|^2 + \nu \sum_{i,i'=1}^{N_u+N_l}W_{i,i'}(f_{\theta}(x_i)-f_{\theta}(x_i'))^2 $$ 第一項はラベルがあるデータで最小二乗誤差を計算しているだけで、いつも通りです。 labelがあるデータに関しては普通にfitしてね、という。 第二項はパラメータについての正則化で、これもいつも通り。over fittingを回避しているだけで、ラプラス正則化の正則化とは関係ないです。 第三項がラプラス正則化です。 $W_{i,i'}$はデータ点$x_i,x_{i'}$の近さを表す近傍グラフのエッジの重みです。 気持ち的には、データ点$x_i,x_{i'}$が近い場合に$W_{i,i'}$は大きくなり、その時にデータ点$x_i,x_{i'}$の出力の差が$(f_{\theta}(x_i)-f_{\theta}(x_i'))^2$が大きくなるとペナルティとしてlossが大きくなる、という寸法です。 つまり、ラプラス正則化は近傍の入力点間の出力が滑らかでないとペナルティがかかる、という正則化だと言えます。 これはいろんなモデルに適用することができてお手軽です。 $f_{\theta}$の形によらないし、第一項、第二項は自由でラプラス正則化だけつければいいので。 分類にも回帰にも使えます。 線形モデルやガウスカーネルモデルにこれをつけた場合、このlossを最小化する$\theta$は解析的に求めることができます。 詳細は書かないですが、それぞれ行列、ベクトルで表して$\theta$で微分するだけです。 途中で近傍グラフのラプラシアン$L=D-W$($D$は$W$の次数行列)が出てきて綺麗にかけます。 ## 実装 Pythonで実装しました。 近傍グラフの作り方は自由度がありますが、今回はデータ点のペアをガウスカーネルにいれた値を近傍グラフのエッジの重みにしました。 ```python import numpy as np import math from sklearn.utils.validation import check_is_fitted class LaplacianRegularizationClassifier: """ラプラス正則化ガウスカーネルモデルで最小二乗分類. 近傍グラフはガウスカーネルを用いる. label : {+1,-1,None (unlabeled)} """ def __init__(self, lam, nu, h_w, h_m): """Intialization. :params lam: paramの正則化係数 :params nu: laplace正則化の係数 :params h_w: kernel window for W (neighborhood matrix) :params h_m: kernel window for K (gram matrix) """ self.lam = lam self.nu = nu self.h_w = h_w self.h_m = h_m def fit(self, X, y): self.X_train = X X_labeled = X[(y == 1) | (y == -1)] y = y[(y == 1) | (y == -1)] y = np.reshape(y, (len(y), 1)) y = y.astype(float) # Noneだったからobjectになってるはずなので直す data_n = X.shape[0] K_tilde = self.create_kernel_matrix(X_labeled, X, self.h_m) K = self.create_kernel_matrix(X, X, self.h_m) W = self.create_kernel_matrix(X, X, self.h_w) L = self.get_laplacian(W) A = K_tilde.T@K_tilde + self.lam * \ np.eye(data_n) + 2 * self.nu * K.T@L@K b = K_tilde.T @ y theta = np.linalg.solve(A, b) self.coef_ = theta return def get_laplacian(self, W): D = self.get_degree_matrix(W) return D - W def get_degree_matrix(self, W): return np.diag([sum(W[i]) for i in range(len(W))]) def create_kernel_matrix(self, X, Y, h): """create Kernel matrix :param X: data matrix (data_nX,feature_n) :param Y: data matrix (data_nY,feature_n) :return K or W: kernel matrix (data_nX,data_nY) """ K = [] for x in X: K_i = [] for y in Y: K_i.append(self.kernel(x, y, h)) K.append(K_i) return np.array(K) def kernel(self, x_i, x_j, h): """kernel function""" return math.e**(-np.dot(x_i - x_j, x_i - x_j) / (2 * h**2)) def predict(self, X): """return 1 or -1""" check_is_fitted(self, 'coef_') K = self.create_kernel_matrix(X, self.X_train, self.h_m) scores = K @ self.coef_ return np.where(scores.flatten() > 0, 1, -1) ``` ## 実験と結果 生成したデータはこんな感じです。よく見るタイプのあれです。 ガウスカーネルモデルなので、非線形の境界もしっかり学習してくれるはず。 全部で200点生成して、そのうち教師ありデータは二点(正例負例ともに一つずつ)、残りが教師なしデータとしました。 比較対象として、ラベルありのデータ二点のみから教師あり学習するガウスカーネル最小二乗分類器も実験に用います。 結果がこちら 教師ありの方はまあ当然ですがラベルが二つしかないので真ん中に線を引かざるを得ないですが、半教師あり学習はラベルありデータのほかに教師なしデータをラプラス正則化を用いて有効活用することで正しく分類境界を学習することができています。 ## まとめ ラプラス正則化について簡単に解説して、ガウスカーネル最小二乗分類にラプラス正則化を適用したモデルをPythonで実装し、人工データで実験してみました。 半教師あり学習のモデルを実装したのがはじめてで、上の実装をみるとわかるのですが、unlabeledデータはNoneで表現しています。 このせいで、fitの最初の方でコメントに書いているとおりNoneをfilter outしたyのtypeがおかしくてエラーがでてしまっていました。 floatに直して回避しましたが、普通はlabeledデータとunlabeledデータは別で引数として渡すのがいいんだろうなと思いました。(だったらなおさんかい) ありがとうございました。 この記事をシェアする Twitter Facebook Google+ B!はてブ Pocket Feedly コメント
コメント
コメントを投稿