決定木・クラスタリング・ニューラルネットワーク
入力を予測へ変換する三つの方式について、分岐条件・距離・重み付き和を求めます。
説明
予測の仕組みはモデルによって異なります。決定木は条件分岐をたどり、クラスタリングはデータを似た集団へ分けます。ニューラルネットワークのニューロンは入力の重み付き和と非線形関数を組み合わせます。
まず小さな分類データから一段の決定木を作り、分割の良さを計算します。続いて正解ラベルのない数値を二つの中心へ割り当て、最後に一つのニューロンの中間値と重み更新を追います。方式の違いを計算の過程から確認します。
1分割前の混ざり具合
入力1・2・3はクラス0、8・9・10はクラス1とします。Gini不純度は1−各クラス比率の二乗和です。
from collections import Counterクラスごとの件数; data = [(1,0),(2,0),(3,0),(8,1),(9,1),(10,1)]入力と正解; gini = lambda labels: 1-sum((n/len(labels))**2 for n in Counter(labels).values()) if labels else 0Gini不純度; gini([label for x,label in data])分割前の値各クラス半分なので不純度0.5です。一方のクラスだけなら0になります。これは予測の正解率そのものではなく、節点内のクラスの混ざり具合です。
2分岐条件を比較する
xがしきい値以下かどうかで二分し、子の不純度を件数の比率で加重平均します。
split_score = lambda threshold: sum(len(group)/len(data)*gini(group) for group in [[label for x,label in data if x <= threshold],[label for x,label in data if x > threshold]])分割後の加重不純度; [(t,split_score(t)) for t in [1.5,2.5,5.5,8.5,9.5]]候補しきい値と評価5.5で分けると両側が一つのクラスだけになり、加重不純度0です。親の0.5からの減少が最も大きい候補を選びます。
3木の経路と予測
最良候補5.5を使う一段の木で、入力を左右の葉へ送ります。葉は訓練データのクラスを返します。
tree = lambda x: 0 if x <= 5.5 else 1選んだ分岐と葉; [(x,'left' if x <= 5.5 else 'right',tree(x)) for x in [2,5.5,6,12]]入力・経路・予測5.5ちょうどは左、6は右です。12も右のクラス1になりますが、訓練範囲外の12が現実に正しいことを木の分岐だけで保証することはできません。
4ラベルを使わず中心へ割り当てる
k-meansでは各点を最も近い中心へ割り当てます。ここでは一次元の値1・2・3・8・9・10と、初期中心1・8を使います。
values = [1,2,3,8,9,10]; centers = [1.0,8.0]点と初期中心; labels = [min(range(2),key=lambda j: (x-centers[j])**2) for x in values]二乗距離が最小の中心; list(zip(values,labels))点と所属1・2・3はクラスタ0、8・9・10はクラスタ1になります。クラスタ番号は分類の正解ラベルではなく、集団を識別するための番号です。
5所属点の平均へ中心を動かす
各クラスタの平均を新しい中心にします。ここでは両クラスタに点があるため平均を計算できます。
old_loss = sum((x-centers[j])**2 for x,j in zip(values,labels))更新前の二乗距離合計; centers = [sum(x for x,j in zip(values,labels) if j == c)/labels.count(c) for c in range(2)]所属点の平均へ更新; new_loss = sum((x-centers[j])**2 for x,j in zip(values,labels))更新後の損失; centers,old_loss,new_loss中心と損失の変化中心は2と9になり、二乗距離の合計は10から4へ減ります。割当てと中心更新を繰り返す方法ですが、初期中心やクラスタ数によって結果が変わり得ます。
6再割当てで変化が止まる
更新した中心2・9から所属を計算し直します。
new_labels = [min(range(2),key=lambda j: (x-centers[j])**2) for x in values]新中心で再割当て; new_labels,new_labels == labels所属が変わったか今回の所属は変わりません。同じ所属で平均を取り直しても中心は変わらず、この例では停止できます。局所的に変化が止まることと、あらゆる初期値で同じ最適解を得ることは別です。
7一つのニューロンの中間値
入力(1,2)、重み(0.5,−0.25)、バイアス0.1を使います。重み付き和zにsigmoidを適用し、0〜1の出力へ変換します。
import math指数関数; x = [1.0,2.0]; weights = [0.5,-0.25]; bias = 0.1入力・重み・バイアス; sigmoid = lambda z: 1/(1+math.exp(-z))非線形変換; z = sum(a*b for a,b in zip(x,weights))+bias重み付き和; prediction = sigmoid(z); z,prediction変換前後の値重み付き和0.1から出力約0.525になります。sigmoidが範囲を0〜1にすることだけでは、その出力が実際の頻度に合った確率である保証はありません。
8正解に向けて重みを更新する
正解1に対する二値交差エントロピーを使います。sigmoidとこの損失の組では、zに関する勾配が予測−正解になります。
target = 1; rate = 0.1正解と学習率; gradient = prediction-targetzの勾配; new_weights = [w-rate*gradient*a for w,a in zip(weights,x)]各重みの勾配更新; new_bias = bias-rate*gradientバイアス更新; updated = sigmoid(sum(a*b for a,b in zip(x,new_weights))+new_bias)更新後の予測; new_weights,new_bias,prediction,updated,-math.log(prediction),-math.log(updated)パラメータ・予測・損失出力が1へ近づき、この1件に対する損失が減ります。学習はこのような重み更新をデータに対して行いますが、1件の損失減少だけでは未使用データの性能は分かりません。
まとめ
決定木は分岐によるクラスの混ざり具合の減少を使って規則を作ります。
k-meansは正解ラベルを使わず、中心への割当てと平均更新を繰り返します。
ニューロンは重み付き和と非線形変換で出力し、損失の勾配でパラメータを更新します。
操作履歴と解説を振り返れます。
Python 3
from collections import Counter; data = [(1,0),(2,0),(3,0),(8,1),(9,1),(10,1)]; gini = lambda labels: 1-sum((n/len(labels))**2 for n in Counter(labels).values()) if labels else 0; gini([label for x,label in data])0.5
split_score = lambda threshold: sum(len(group)/len(data)*gini(group) for group in [[label for x,label in data if x <= threshold],[label for x,label in data if x > threshold]]); [(t,split_score(t)) for t in [1.5,2.5,5.5,8.5,9.5]][(1.5, 0.4), (2.5, 0.25), (5.5, 0.0), (8.5, 0.25), (9.5, 0.4)]
tree = lambda x: 0 if x <= 5.5 else 1; [(x,'left' if x <= 5.5 else 'right',tree(x)) for x in [2,5.5,6,12]][(2, 'left', 0), (5.5, 'left', 0), (6, 'right', 1), (12, 'right', 1)]
values = [1,2,3,8,9,10]; centers = [1.0,8.0]; labels = [min(range(2),key=lambda j: (x-centers[j])**2) for x in values]; list(zip(values,labels))[(1, 0), (2, 0), (3, 0), (8, 1), (9, 1), (10, 1)]
old_loss = sum((x-centers[j])**2 for x,j in zip(values,labels)); centers = [sum(x for x,j in zip(values,labels) if j == c)/labels.count(c) for c in range(2)]; new_loss = sum((x-centers[j])**2 for x,j in zip(values,labels)); centers,old_loss,new_loss([2.0, 9.0], 10.0, 4.0)
new_labels = [min(range(2),key=lambda j: (x-centers[j])**2) for x in values]; new_labels,new_labels == labels([0, 0, 0, 1, 1, 1], True)
import math; x = [1.0,2.0]; weights = [0.5,-0.25]; bias = 0.1; sigmoid = lambda z: 1/(1+math.exp(-z)); z = sum(a*b for a,b in zip(x,weights))+bias; prediction = sigmoid(z); z,prediction(0.1, 0.52497918747894)
target = 1; rate = 0.1; gradient = prediction-target; new_weights = [w-rate*gradient*a for w,a in zip(weights,x)]; new_bias = bias-rate*gradient; updated = sigmoid(sum(a*b for a,b in zip(x,new_weights))+new_bias); new_weights,new_bias,prediction,updated,-math.log(prediction),-math.log(updated)([0.547502081252106, -0.154995837495788], 0.147502081252106, 0.52497918747894, 0.5950814828546528, 0.6443966600735709, 0.5190569368385448)
演習完了