1次元のロジスティック回帰は,
d次元ベクトルxをf(x)=wTx+bによって1次元に写像
それをシグモイド関数で(0,1)区間へ写像:s(x)≡σ(f(x))
これを用いてラベルc(0 or 1)に対する尤度を定義:
p(y=c∣x;w,b)=s(x)c(1−s(x))1−c
(w,bは確率変数ではなくロジスティックモデルのパラメータなので,;によって区切られた形で示されている)
与えられたN個のデータ点xnとそのラベル値cnについて,各データ点が全て独立であるとの仮定のもとで以下の尤度関数を最大化するw,bを求める l(w,b)=N∏n=1p(y∣x;w,b)=N∏n=1s(xn)cn(1−s(xn))1−cn
という手順によってxから2値ラベルcを予測する写像を得る(すなわち2クラス分類を行う)手法である.以下に書く手順の詳細を述べる.
シグモイド関数はσ(a)=11+exp(−a)のような関数で,以下のような形をしている.
import numpy as np
import matplotlib.pyplot as plt
a = np.arange(-10, 10, 0.1)
s = 1.0 / (1.0 + np.exp(-a))
plt.plot(a, s)
plt.show()
これは(−∞,∞)の区間の実数を(0,1)に写像する関数なので,あるデータ点xをf(x)=wTx+bによって1次元に写像したあと,シグモイド関数に通した値σ(f(x))は,(0,1)の値をとる確率値であると考えることができるようになる.
今,データ点xにはラベルとして0か1のどちらかが対応しているとする.このとき,xのラベルが1である確率と0である確率は,それぞれ
p(y=1∣x;w,b)=σ(f(x))≡s(x)p(y=0∣x;w,b)=1−σ(f(x))≡1−s(x)と表すことができる.ラベルをc(0 or 1)で表せば,データ点xが与えられたときのラベルの尤度は
p(y=c∣x;w,b)=s(x)c(1−s(x))1−cとまとめて書くことができる.今,N個のデータ点があるとして,それぞれが全て独立であると考えると,データ点の集合xn(n=1,…,N)とラベルの集合cn(n=1,…,N)に対する,このモデルのパラメータであるx,bの尤度は各データの尤度の積で表せるので,
l(w,b)=N∏n=1p(y∣x;w,b)=N∏n=1s(xn)cn(1−s(xn))1−cnと定義できる.この最大化を考えるのだが,まず(0,1)区間の値の積をデータ数分だけ計算しようとすると,浮動小数点のオーバーフローが起こりやすいため,対数をとって和の形で表すことにする.対数関数は単調増加であるので,対数をとったとしても大小関係は変わらない.また,実装上の容易さの観点から,符号を反転することで最大化ではなく最小化問題に置き換えることにする.すると,目的関数は以下のように書き下せる.
L(w,b)=−N∑n=1ln{s(xn)cn(1−s(xn))1−cn}=−N∑n=1{cnlns(xn)+(1−cn)ln(1−s(xn))}これを負の対数尤度(negative log likelihood)関数と呼ぶ.また,モデルの予測のはずれ具合を表すので,損失関数(loss function)とも呼ばれる.これを最小化するパラメータw,bは,この関数をパラメータそれぞれで偏微分して0とおけば求まるような気がする.シグモイド関数の導関数が
σ(a)=11+exp(−a)dσda=exp(−a)(1+exp(−a))2=11+exp(−a)exp(−a)1+exp(−a)=σ(a)1+exp(−a)−11+exp(−a)=σ(a)(1−σ(a))となることに注意して,パラメータw,bそれぞれにおける偏導関数を求めてみる.
以上の2つの導関数
∂L∂w=−N∑n=1xn(cn−s(xn))∂L∂b=−N∑n=1(cn−s(xn))は,シグモイド関数s(xn)を含んでいるため,=0とおいて解析的にwとbの値を決定することができない.そこで,この偏導関数を使って,勾配降下法(gradient descent)によるパラメータw,bの更新を行うことで,損失関数L(w,b)の最小化を行う.
このための更新式は以下である.
w←w−η∂L∂wb←b−η∂L∂bただし,ηは学習率とよばれ,一回の更新でどのくらいパラメータを変化させるかという度合いを表す.これが大きいと振動しながら最適解へ近づくことになり,小さいと収束に時間を要することになる.
上記の更新式によるパラメータの更新では,勾配を計算するために毎回,全てのデータ点に渡って和をとる必要がある.これは,データセットが大きいな場合は非常に計算コストが大きくなってしまう.
そこで,データセットの中から部分的にいくつかのデータ点を選びとって,それらに対してだけ上記の和を計算して勾配とし,パラメータを更新する方法がよくとられる.これを確率的勾配降下法(stochastic gradient descent: SGD)と呼ぶ.どのデータ点部分集合を選択するかを確率的に選ぶためこう呼ばれる.このとき,一回の更新に用いられるデータセットの部分集合のことをミニバッチ(minibatch)と呼ぶことがある.
また,勾配の計算の際,上式のようにただ総和をとるのではなく,平均を計算することで,通常の勾配降下法の場合はデータセットの大きさに,確率的勾配降下法の場合はミニバッチの大きさに,勾配の大きさが影響されないようにすることができる.このようにすることで学習率ηの決定も容易になる.
まず,2つのクラスから生成されたデータ点群が与えられたとする.それぞれのデータ点は中心を(0,0),(5,5)にもつ分散1の正規分布から100個ずつサンプルされた点である.
d = 2
N = 1000
x1 = np.random.randn(N, d)
x2 = np.random.randn(N, d) + np.array([5, 5])
plt.scatter(x1[:, 0], x1[:, 1], c='r')
plt.scatter(x2[:, 0], x2[:, 1], c='b')
plt.show()
これをひとつのデータ行列にまとめ,対応するラベルベクトルを作成する.ここでは,クラス1に属するデータ点にはラベル値c=0を,クラス2に属するデータ点にはラベル値c=1を対応させる.
x = np.vstack((x1, x2))
label1 = np.zeros(N)
label2 = np.ones(N)
label = np.hstack((label1, label2))
シグモイド関数と,データ点群行列(N×d)からクラス尤度を算出する関数p_y_given_x(x)を定義する.
def sigmoid(a):
return 1.0 / (1.0 + np.exp(-a))
def p_y_given_x(x):
return sigmoid(np.dot(x, w) + b)
データ点群と正解ラベルベクトルをとり,勾配を返す関数を定義する.
def grad(x, label):
error = label - p_y_given_x(x)
w_grad = -np.mean(x.T * error, axis=1)
b_grad = -np.mean(error)
return w_grad, b_grad
パラメータを適当に初期化したのち,データセットを10個ずつのミニバッチに分けて,確率的勾配降下法を用いてパラメータを最適化する.
dataset = np.column_stack((x,label))
np.random.shuffle(dataset) #データ点の順番をシャッフル
x = dataset[:, :2]
label = dataset[:, 2]
w = np.random.rand(d)
b = np.random.random()
eta = 0.1
minibatch_size = 10
errors = list()
for _ in range(10):
for index in range(0, x.shape[0], minibatch_size):
_x = x[index:index + minibatch_size]
_label = label[index:index + minibatch_size]
w_grad, b_grad = grad(_x, _label)
w -= eta * w_grad
b -= eta * b_grad
errors.append(np.mean(np.abs(label - p_y_given_x(x))))
print np.mean(np.abs(label - p_y_given_x(x)))
plt.plot(errors)
plt.show()
0.0129718198572
上のコードではミニバッチ最適化をさらに10回繰り返している.繰り返しごとにエラーを記録し,最後にその変化をグラフ化している.パラメータを更新するたびに誤差が減っていっていることが分かる.
bx = np.arange(-6, 10, 0.1)
by = -b/w[1] - w[0]/w[1]*bx
plt.xlim([-5, 10])
plt.ylim([-5, 9])
plt.plot(bx, by)
plt.scatter(x1[:, 0], x1[:, 1], c='r')
plt.scatter(x2[:, 0], x2[:, 1], c='b')
plt.show()
上記の例では1次元のロジスティック回帰を考えた.これは2クラス分類問題を考えるのに利用できることが分かった.では,Kクラス分類を考える場合はどうなるだろうか.この場合,出力がK次元のクラス所属度確率(class-membership probability)ベクトルとなるようにソフトマックス関数というのを用いる.ソフトマックス関数はシグモイド関数の多変量版であり,K次元ベクトルaのk番目の要素をak(k=1,…,K)と書くことにすると,
π(ak)=exp(ai)∑Kj=1exp(aj)と定義される.以降,便宜上(π(a1),…,π(aK))Tというベクトルをπ(a)と書くことにする.
ソフトマックス関数にベクトルを入力すると,出力される値は同次元のベクトルとなるが,各要素の合計値が必ず1となるようなベクトルとなる.すなわち
K∑k=1π(ak)=1が常に成り立つ.よって,出力されるベクトルは離散確率分布であると考えることができる.
K = 10
a = np.random.rand(K)
def softmax(a):
return np.exp(a) / np.sum(np.exp(a))
y = softmax(a)
plt.xlim([-1, K])
plt.bar(np.arange(K), y, width=0.1)
plt.show()
print 'summation:', np.sum(y) # 要素の合計値出力
summation: 1.0
Kクラス分類問題において,D次元ベクトルxと,それに対応する正解クラスckが与えられたとき,多次元ロジスティック回帰モデルは以下のような手順で計算を行う.
D次元ベクトルであるデータ点xをK次元に写像:f(x)=WTx+b(WはD×Kの重み行列,bはK次元ベクトル)
ソフトマックス関数に通してクラス所属度確率ベクトルを計算:y=π(f(x))
xが所属するクラスがckのとき,正解データとしてk個目の要素だけが1でその他の要素が0であるような教師ベクトルt(例:(0,…,1,…,0)T)が用意されているとする.これを用いて,データ点が与えられたときのパラメータW,bの尤度を以下のように定義する: p(y=t∣x;W,b)=K∏k=1ytkk このとき,tの形を念頭に,ytkkに着目すると,これはkがxが所属しているクラスのインデックスに等しいときだけtk=1となってykに等しくなり,それ以外のkのときはtk=0よりy0k=1となる仕組みをとっていることが分かる.これを全ての要素k=1,…,Kに渡って掛けあわせているので,結果的にクラス所属度確率ベクトルの要素のうち,正解クラスへの所属度確率だけを取り出す操作に等しい.
N個のデータ点がそれぞれ独立に観測されるとして,データセット全体に渡るパラメータの尤度を以下のように定義する: l(W,b)=p(Y=T∣X;W,b)=N∏n=1K∏k=1ytnknk ただし
YはN個のデータのクラス所属度ベクトル(列ベクトル)を行方向に並べたもの(y1,…,yN)であり,
Y=(y11yN1⋮⋮y1k⋯yNk⋮⋮y1KyNK)=π(WTX+B)
というK×N行列
TはN個のデータの教師ベクトル(列ベクトル)を行方向に並べたもの(t1,…,tN)であり,
T=(t11tN1⋮⋮t1k⋯tNk⋮⋮t1KtNK)
というK×N行列
Xはデータ行列(デザイン行列とも呼ばれる)であり,データベクトル(列ベクトル)を行方向に並べたもの(x1,…,xN)で,
X=(x11xN1⋮⋮x1d⋯xNd⋮⋮x1DxND)
というD×N行列
Bは全く同じK次元バイアスベクトルbを行方向にN個コピーしたK×N行列 B=(b1b1⋮⋮bk⋯bk⋮⋮bKbK)
というK×N行列になる.
上記の尤度を最大にするようにW,bを求めることで学習を行う
尤度l(W,b)を例によって負の対数尤度に変換すると,
L(W,b)=−N∑n=1K∑k=1tnklnynkとなる.更新式を求めるために勾配を算出しよう.
パラメータW,bに関する勾配は,
∂L∂W=(∂L∂w1,…,∂L∂wj,…,∂L∂wK)∂L∂b=(∂L∂b1,…,∂L∂bj,…,∂L∂bK)を考えればよいから,問題となるのは∂L∂wj,∂L∂bjである.
ここで,an=WTxn+bとおき,このk個目の要素をa_{nk}と書いた.また,
{\bf y}_n = \softmax({\bf a}_n) = (\pi(a_{n1}),\dots,\pi(a_{nK}))^{\rm T}であるから,y_{nk} = \pi(a_{nk})である.微分の連鎖律の部分については,
\begin{eqnarray} y_{nk} &=& \pi(a_{nk}) \\ a_{nk} &=& {\bf w}_k^{\rm T}{\bf x}_n + b_k \end{eqnarray}に注意する.
では,\frac{\partial y_{nk}}{\partial a_{nj}}および\frac{\partial a_{nj}}{\partial {\bf w}_j}について個別に考えていく.
ここで添字の都合上\frac{\partial y_{nk}}{\partial a_{nj}}を一旦\frac{\partial y_{nk}}{\partial a_{ni}}と書くと,
i = kのとき \begin{eqnarray} \frac{\partial y_{nk}}{\partial a_{ni}} = \frac{\partial \pi(a_{nk})}{\partial a_{ni}} &=& \frac{\exp(a_{nk})}{\sum_{j=1}^K \exp(a_{nj})} - \frac{\exp(a_{nk})^2}{\left( \sum_{j=1}^K \exp(a_{nj}) \right)^2} \\ &=& \pi(a_{nk})(1 - \pi(a_{nk})) \end{eqnarray}
i \neq kのとき \begin{eqnarray} \frac{\partial y_{nk}}{\partial a_{ni}} = \frac{\partial \pi(a_{nk})}{\partial a_{ni}} &=& -\frac{\exp(a_{ni})\exp(a_{nk})}{\left( \sum_{j=1}^K \exp(a_{nj}) \right)^2} \\ &=& -\pi(a_{ni})\pi(a_{nk}) \\ &=& \pi(a_{nk})(0 - \pi(a_{ni})) \\ \end{eqnarray}
となるので,再び添字iをjに置き換えると,
\frac{\partial y_{nk}}{\partial a_{nj}} = y_{nk}({\bf I}_{jk} - y_{nj})と書ける.ここで{\bf I}_{jk}はK \times K単位行列{\bf I}の(j,k)要素である.これはj=kのとき1であり,それ以外の場合は0をとる.
である.
上記の結果を用いると,
\frac{\partial \mathcal{L}}{\partial {\bf w}_j} = -\sum_{n=1}^N \sum_{k=1}^K t_{nk} {\bf x}_n ({\bf I}_{jk} - y_{nj}) = -\sum_{n=1}^N {\bf x}_n \left\{ \sum_{k=1}^K t_{nk}{\bf I}_{kj} - \sum_{k=1}^K t_{nk}y_{nj} \right\}となる.ここで,{\bf I}_{kj}は,j=kのときだけ1で,それ以外のときは0だから,\sum_{k=1}^K t_{nk}{\bf I}_{kj} = t_{nj}と書ける.また,\sum_{k=1}^N t_{nk}y_{nj}におけるy_{nj}は,この和をとる間は不変であり,またこのときt_{nk}はk=1,\dots,Kのいずれか1つでだけ1をとり,それ以外のときは0である教師信号だから,\sum_{k=1}^N t_{nk}y_{nj} = y_{nj}として問題ない.
すると,結局
\frac{\partial \mathcal{L}}{\partial {\bf w}_j} = \sum_{n=1}^N (y_{nj} - t_{nj}){\bf x}_nというシンプルな形に変形できる.
b_jで偏微分することで変化するのは,連鎖律の最後が\frac{\partial a_{nj}}{\partial b_j} = 1になるだけなので,
\frac{\partial \mathcal{L}}{\partial b_j} = \sum_{n=1}^N (y_{nj} - t_{nj})である.
上記の勾配を用いて更新式を定義する.
{\bf w}_j \leftarrow {\bf w}_j - \eta \frac{\partial \mathcal{L}}{\partial {\bf w}_j} = {\bf w}_j - \eta \sum_{n=1}^N (y_{nj} - t_{nj}){\bf x}_nb_j \leftarrow b_j - \eta \frac{\partial \mathcal{L}}{\partial b_j} = b_j - \eta \sum_{n=1}^N (y_{nj} - t_{nj})1次元/多次元ロジスティック回帰双方においてモデルのパラメータである重みとバイアスを{\bf w}, b(or\ {\bf b})のように別々に扱っていたが,これはデータ点ベクトルを\tilde{\bf x} = (1, x_0, \dots, x_D)のように最初の次元がつねに1となる拡張ベクトルとして扱い,重みを\tilde{\bf w} = (w_0, w_1, \dots, w_K)のようにしてバイアスを1つめの次元に追加して扱えば,\tilde{\bf w}^{\rm T}\tilde{\bf x}としてこれまでの{\bf w}^{\rm T}{\bf x} + bを扱える.多次元の場合も同様で,\tilde{\bf W}^{\rm T}\tilde{\bf x}と扱えるようになる.するとこれまでの更新式の導出などにおけるバイアスの考慮は(数式上)必要がなくなる.自動的に重みの中の要素のひとつとして扱われるようになるからである.
まずはデータ点とラベルのセットを用意する.
D = 2
N = 1500
K = 3
# データ点を用意
mean1 = [-2, 2] # クラス1の平均
mean2 = [0, 0] # クラス2の平均
mean3 = [2, -2] # クラス3の平均
cov = [[1.0,0.8], [0.8,1.0]] # 共分散行列(全クラス共通)
x1 = np.random.multivariate_normal(mean1, cov, N / 3)
x2 = np.random.multivariate_normal(mean2, cov, N / 3)
x3 = np.random.multivariate_normal(mean3, cov, N / 3)
x = np.vstack((x1, x2, x3))
# 教師ベクトルを用意
label1 = np.zeros((N / 3, 3), dtype=np.int32) + np.array([1, 0, 0])
label2 = np.zeros((N / 3, 3), dtype=np.int32) + np.array([0, 1, 0])
label3 = np.zeros((N / 3, 3), dtype=np.int32) + np.array([0, 0, 1])
label = np.vstack((label1, label2, label3))
# 図示
plt.xlim((-6, 6))
plt.ylim((-6, 6))
plt.scatter(x1[:, 0], x1[:, 1], c='r')
plt.scatter(x2[:, 0], x2[:, 1], c='g')
plt.scatter(x3[:, 0], x3[:, 1], c='b')
plt.show()
上で定義したソフトマックス関数は複数の多次元ベクトルに対してベクトルごとにsoftmaxを計算する仕様になっていないので,複数ベクトルを行列として渡した場合にも正しくベクトルごとにsoftmaxが適用されたものを返す関数として再定義する.
次に,\softmax({\bf W}^{\rm T}{\bf X} + {\bf b})を計算する関数p_y_given_x(x)を用意する.これは与えられたデータ点が列方向に並んだN \times D次元の行列を与えると,行ごと(データ点ごと)に各クラスへの所属度確率ベクトルを計算し,これを列方向へ並べたN \times K行列を返す関数である.
grad関数は上で定義した勾配を求める関数である.定義では総和をとっていたが,ここでは平均をとることにする.
dataset = np.column_stack((x, label))
np.random.shuffle(dataset) #データ点の順番をシャッフル
x = dataset[:, :2]
label = dataset[:, 2:]
def softmax(x):
return (np.exp(x).T / np.sum(np.exp(x), axis=1)).T
def p_y_given_x(x):
return softmax(np.dot(x, w.T) + b)
def grad(x, label):
error = p_y_given_x(x) - label
w_grad = np.zeros_like(w)
b_grad = np.zeros_like(b)
for j in range(w.shape[0]):
w_grad[j] = np.mean(error[:, j] * x.T, axis=1)
b_grad[j] = np.mean(error[:, j])
return w_grad, b_grad, np.mean(np.abs(error), axis=0)
実際の学習を行う前に,学習率\etaとミニバッチサイズを設定する.学習ループではミニバッチごとにデータセット全体を学習することを100回繰り返している.結果として各クラスへ所属度確率の誤差の平均の推移をプロットしている.
w = np.random.rand(K, D)
b = np.random.rand(K)
eta = 0.1
minibatch_size = 500
# import numpy.linalg as LA
errors = []
for _ in range(100):
for index in range(0, N, minibatch_size):
_x = x[index: index+minibatch_size]
_label = label[index: index+minibatch_size]
w_grad, b_grad, error = grad(_x, _label)
w -= eta * w_grad
b -= eta * b_grad
errors.append(error)
errors = np.asarray(errors)
plt.plot(errors[:, 0])
plt.plot(errors[:, 1])
plt.plot(errors[:, 2])
plt.show()
さらに,識別境界を以下に図示する.
bx = np.arange(-10, 10, 0.1)
by0 = -(w[0, 0] - w[1, 0]) / (w[0, 1] - w[1, 1]) * bx - (b[0] - b[1]) / (w[0, 1] - w[1, 1])
by1 = -(w[1, 0] - w[2, 0]) / (w[1, 1] - w[2, 1]) * bx - (b[1] - b[2]) / (w[1, 1] - w[2, 1])
plt.plot(bx, by0)
plt.plot(bx, by1)
plt.xlim((-6, 6))
plt.ylim((-6, 6))
plt.scatter(x1[:, 0], x1[:, 1], c='r')
plt.scatter(x2[:, 0], x2[:, 1], c='g')
plt.scatter(x3[:, 0], x3[:, 1], c='b')
plt.show()