1 引言¶
感知機是一種簡單且易於實現的二分類判別模型,主要思想是通過誤分類驅動的損失函數結合梯度下降發求解一個超平面將線性可分的數據集划分為兩個不同的類別(+1類和-1類)。
在神經網絡、支持向量機等算法盛行的當下,感知機模型應用得並不多,但必須承認,感知機卻是神經網絡和支持向量機的基礎,所以還是很有必要學習一下的,本文接下來的內容將從感知機數學描述、損失函數、兩種不同學習形式等方面詳細介紹感知機,最后使用Python實現感知機兩種學習形式。
2 感知機模型及損失函數¶
2.1 數學描述¶
對於給定訓練樣本數據集$D = \{ ({x_i},{y_i})\} _{i = 1}^m$,${x_i} \in X \subseteq {R^n}$表示訓練樣本的特征向量,${y_i} \in Y = \{ + 1, - 1\} $表示樣本類別。$x$與$y$之間的如下函數關系: $$y = f(x) = sign(w \cdot x + b)$$ 稱為感知機。其中,$w \in {R^n}$稱為感知機的權值系數或者權值向量,$b \in R$稱為偏置,$sign$是符號函數,有: $$sign = \left\{ {_{ - 1, x < 0}^{ + 1, x \geqslant 0}} \right.$$ 從定義上可以看出,感知機最終目標就是求解出$w$和$b$。我們可以從幾何上對感知機進行理解,如果以$w$為法向量,以$b$為截距,可以確定一超平面: $$w \cdot x + b = 0$$ 通過這一超平面,可以順利將對數據集進行划分。以二維數據為例,如下圖所示,當樣本點$x$剛好落在超平面上時,有$w \cdot x + b = 0$,當$x$落在超平面下方時,有$w \cdot x + b < 0$,通過$sign$函數后輸出為$-1$,也就是標記為$-1$類;當$x$落在超平面上方時,有$w \cdot x + b > 0$,通過$sign$函數后輸出為$+1$,也就是標記為$+1$類。注意,這樣的超平面一般不唯一,也就是說感知機最終解可以有很多個,受參數初始值、訓練樣本輸入順序等因素的影響,每次訓練時所獲得的超平面都可能不一樣。
2.2 損失函數¶
為了求解參數$w$和$b$,確定最終的分割超平面,我們需要定義一個目標函數或者說損失函數,通過最小化損失函數來達到目的。在感知機模型中,以誤分類的樣本對象與分割超平面間的距離之和最為損失函數。我們高中時學過,對於點$({x_0},{y_0})$,到平面$A \cdot x + B \cdot y + C = 0$的距離為: $$dist = \frac{{|A \cdot {x_0} + B \cdot {y_0} + C|}}{{\sqrt {{A^2} + {B^2}} }} $$ 將這一公式擴展到超平面中,對於超平面$w \cdot x + b = 0$,誤分類點$x_i$到超平面的距離為: $$dist = \frac{{|w \cdot xi + b|}}{{\left\| w \right\|}} $$ 式中,${\left\| w \right\|}$是$w$的$L_2$范數,等同於上面的${\sqrt {{A^2} + {B^2}} }$。 為了方便計算,我們需要將分子中的絕對值去掉,怎么去掉呢?因為$({x_i},{y_i})$是誤分類樣本點,$y_i$與${w \cdot xi + b}$一定是異號的,所以: $$ - {y_i} \cdot (w \cdot x_i + b) > 0 $$ 且因為$|{y_i}| = 1$,所以: $$ - {y_i} \cdot (w \cdot x_i + b) = |w \cdot x_i + b| $$ 於是,$({x_i},{y_i})$到超平面的距離可以表示為: $$\frac{{ - {y_i} \cdot (w \cdot x_i + b)}}{{\left\| w \right\|}} $$ 假設$M$是所有誤分類點組成的集合,那么所有誤分類點到超平面距離總和為: $$\sum\limits_{{x_i} \in M} {\frac{{ - {y_i} \cdot (w \cdot {x_i} + b)}}{{\left\| w \right\|}}} \tag{1}$$ 這就是我們需要的損失函數的雛形了,之所以說是雛形,是因為我們還可以通過令$\left\| w \right\|{\text{ = }}1$對上式進一步化簡,於是有: $$L(w,b) = \sum\limits_{{x_i} \in M} { - {y_i} \cdot (w \cdot {x_i} + b)} \tag{2}$$ $L(w,b)$就是我們最終需要的損失函數。 為什么可以直接令$\left\| w \right\|{\text{ = }}1$來化簡式(1)呢?
我們可以在權值向量$w$中添加一個$w_0$元素,在特征向量$x_i$中添加一個第0緯度${x^{(0)}} = 1$,令偏置$b = {w_0} \cdot {x^{(0)}}$,這樣,我們就把偏置$b$也放進了權值向量$w$中,那式(1)就變為: $$\sum\limits_{{x_i} \in M} {\frac{{ - {y_i} \cdot w \cdot {x_i}}}{{\left\| w \right\|}}} $$ 此時,分子和分母都含有$w$,當分子的$w$擴大$N$倍時,分母的$L_2$范數也會擴大N倍。也就是說,分子和分母有固定的倍數關系。那么我們可以固定分子或者分母為1,然后求分母的倒數或者分子自己的最小化作為損失函數,這樣可以簡化我們的損失函數。在感知機模型中,采用的是保留分子的策略。
另一種解釋,當把偏置$b$包含進$w$后,超平面表達式也簡化成$w \cdot {x_i} = 0$,無論是把$w$擴大多少倍、縮小多少倍,都對超平面沒有影響(就像$x + y - 1 = 0$與$2x + 2y - 2 = 0$始終表示同一條直線),那么我們總能找到一個倍數,將$w$縮小到滿足${\left\| w \right\|}=1$,但並不影響我們獲得最終的超平面,但是令${\left\| w \right\|}=1$后卻有助於我們化簡和求解。
3 優化方法¶
上一節中,我們介紹了感知機模型損失函數$L(w,b)$的由來,接下來就要說說怎么通過優化損失函數來獲得最終的超平面。在感知機模型中,有兩種優化方式:原始形式和對偶形式。
3.1 原始形式¶
原始形式采用的是梯度下降法進行求解,如果對梯度下降法不了解,可以參看前面寫過的一篇博客。這里需要注意的是,在上一小節中說過,感知機是基於誤分類驅動的一種模型,所以不能使用整個數據集進行梯度下降優化,只能對誤分類樣本集合$M$采用隨機梯度下降法或者小批量梯度下降法進行優化。 對損失函數$L(w,b)$求偏導: $$\frac{{\partial L(w,b)}}{{\partial w}} = - \sum\limits_{{x_i} \in M} {{y_i} \cdot {x_i}} $$ $$\frac{{\partial L(w,b)}}{{\partial b}} = - \sum\limits_{{x_i} \in M} {{y_i}} $$ 那么,$w$的梯度下降迭代公式為: $$w = w + \alpha \cdot \sum\limits_{{x_i} \in M} {{y_i} \cdot {x_i}} $$ 偏置$b$的梯度下降迭代公式為: $$b = b + \alpha \cdot \sum\limits_{{x_i} \in M} {{y_i}} $$ 式中,$\alpha $是學習率。 感知機模型中,一般采用隨機梯度下降法進行優化,每次使用一個誤分類樣本點進行梯度更新。假設$(x_i,y_i)$是$M$中的一個誤分類點,進行梯度更新: $$w = w + \alpha \cdot {y_i}{x_i} \tag{3}$$ $$b = b + \alpha \cdot {y_i} \tag{4}$$ 總結一下原始形式優化步驟。 輸入:訓練樣本數據集$D = \{ ({x_i},{y_i})\} _{i = 1}^m$,${x_i} \in X \subseteq {R^n}$,${y_i} \in Y = \{ + 1, - 1\} $,學習率% $\alpha \in (0,1)$
輸出:$w$,$b$;感知機模型$f(x) = sign(w \cdot x + b) $
(1)初始化$w_0$,$b_0$;
(2)在$D$中選取任意點$(x_i,y_i)$;
(3)通過${y_i} \cdot (w \cdot {x_i} + b)$的值判斷是否是誤分類點,如果是,使用式(3)、(4)更新參數;
(4)回到步驟(2)直到准確率滿足條件。
3.2 對偶形式¶
對偶形式時原始形式在執行效率上的優化。通過3.1小節中,我們知道,每當一個樣本點$x_i$被錯誤分類一次時,都會使用式(3)(4)更新一次參數,那么,如果樣本點$x_i$在迭代過程中被錯誤分類多次(假設$n_i$次),那么就回有$n_i$次參與到參數更新中,我們假設參數$w$和$b$的初始值都為0向量,那么,最終獲得的參數$w$和$b$為: $$w = \sum\limits_{i = 1}^N {{\beta _i} {y_i}{x_i}} \tag{5}$$ $$b = \sum\limits_{i = 1}^N {{\beta _i} {y_i}} \tag{6}$$ 這是在對偶形式中的參數更新方式,式中,${\beta _i} ={n_i}\alpha$。另外,在原始形式中,我們使用${y_i}(w \cdot {x_i} + b) \leqslant 0$來判斷樣本點$x_i$是否被錯誤分類,將式(5)(6)代入這一判別式中,得: $${y_i}(\sum\limits_{i = 1}^N {{\beta _i} {y_i}{x_i}} \cdot {x_j} + \sum\limits_{i = 1}^N {{\beta _i} {y_i}}) \leqslant 0 \tag{7}$$ 在對偶形式中,采用式(7)判斷樣本點是否正確分類,觀察后可以發現,式(7)中有兩個樣本點$x_i$和$x_j$內積計算,這個內積計算的結果在下面的迭代過程中需要多次被重復使用,如果我們事先用矩陣運算計算出所有的樣本之間的內積,那么在算法迭代過程中, 僅僅一次的矩陣內積運算比原始形式中每遍歷一個樣本點都要計算$w$與$x_i$的內積要省時得多,這也是對偶形式的感知機模型比原始形式優的原因。
在感知機模型中,樣本的內積矩陣稱為Gram矩陣,它是一個對稱矩陣,記為$G = {[{x_i},{x_j}]_{m \times m}}$。
總結一下對偶形式的步驟。
輸入:訓練樣本數據集$D = \{ ({x_i},{y_i})\} _{i = 1}^m$,${x_i} \in X \subseteq {R^n}$,${y_i} \in Y = \{ + 1, - 1\} $,學習率% $\alpha \in (0,1)$
輸出:$w$,$b$;感知機模型$f(x) = sign(w \cdot x + b) $
(1)初始化所有$n_i$值為0;
(2)計算Gram矩陣;
(3)在$D$中選取任意點$(x_i,y_i)$;
(4)如果${y_i}(\sum\limits_{i = 1}^N {{\beta _i} {y_i}{x_i}} \cdot {x_j} + \sum\limits_{i = 1}^N {{\beta _i} {y_i}}) \leqslant 0 $,令${\beta _i} = {\beta _i} + \alpha $;
(5)檢查是否還有誤分類樣本點,如果有,回到步驟(2);如果沒有,(5)(6)計算$w$、$b$最終值。
4 算法實現¶
import numpy as np
import matplotlib.pyplot as plt
import copy
# 先來制造一批數據
a = np.random.normal(20,5,300)
b = np.random.normal(15,5,300)
cluster1 = np.array([[x, y, -1] for x, y in zip(a,b)])
a = np.random.normal(45,5,300)
b = np.random.normal(40,5,300)
cluster2 = np.array([[x, y, 1] for x, y in zip(a,b)])
dataset = np.append(cluster1,cluster2, axis=0)
for i in dataset:
plt.scatter(i[0], i[1],c='black',s=6)
plt.show()
len(dataset)
600
class Perception(object):
def __init__(self):
"""
感知機模型
"""
self.w = 0
self.b = 0
def fit_raw_mod(self, train_data, lr=1, max_epoch=None, min_error_rate=0):
"""
原始模式的感知機訓練方法,當達到最大迭代次數或錯誤率降到最小范圍退出
train_data:訓練數據集
lr:學習率
max_epoch:最大迭代次數
min_error_rate:最小錯誤率
"""
self.w = np.zeros(train_data.shape[1]-1) # 根據訓練集維度初始化權重系數
epoch = 1 # 記錄迭代次數
while True:
error_count = 0 # 記錄錯誤分類樣本數
for sample in train_data:
xi = sample[0:-1]
yi = sample[-1]
distance = yi * (self.w @ xi + self.b) # yi*(w⋅xi*+b)
if distance <= 0: # 對於判斷錯誤的樣本點
self.w += lr * sample[-1] * sample[0:-1]
self.b += lr * sample[-1]
error_count += 1
# 每完成一次迭代之后,驗證一次准確率,准確率達標則退出
current_error_rate = float(error_count) / train_data.shape[0]
# print('epoch {0},current_error_rate: {1}'.format(epoch+1, current_error_rate))
# print('w:{0}, b:{1}'.format(self.w, self.b))
# self.show_graph(train_data) # 每一次迭代都展示一次圖像
if current_error_rate <= min_error_rate:
break
if isinstance(max_epoch, int) and epoch >= maxepoch:
break
epoch += 1
print('w:{0}, b:{1}'.format(self.w, self.b))
self.show_graph(train_data)
def fit_dual_mod(self,train_data,lr=1):
"""
對偶模式的感知機訓練方法
train_data:訓練數據集
lr:學習率
"""
x_train = train_data[:,:-1]
y_train = train_data[:,-1]
num_samples, num_features = x_train.shape
beta = np.zeros((num_samples,))
self.b = 0
# 計算 Gram 矩陣
gram = np.dot(x_train, x_train.T)
while True:
error_count = 0
for i in range(num_samples):
inner_product = gram[i]
y_i = y_train[i]
distance = y_i * (np.sum(beta * y_train * inner_product) + self.b)
# 對於誤分類點,修正 beta 和 偏置b,跳出本層循環,重新遍歷數據計算,開始新的循環
if distance <= 0:
error_count += 1
beta[i] = beta[i] + lr
self.b = self.b + lr * y_i
break
# 數據沒有誤分類點,跳出 while 循環
if error_count == 0:
break
self.w = np.sum(beta * y_train * x_train.T, axis=1) # 計算w參數最終值
print('w:{0}, b:{1}'.format(self.w, self.b))
self.show_graph(train_data) # 展示圖像
def predict(self, sample):
"""
輸入一個樣本點,判斷是-1類還是+1類
sample:樣本點
"""
output = self.w @ sample + self.b
return 1 if output >= 0 else -1
def show_graph(self, train_data):
"""
把訓練出來的超平面圖像展示出來
"""
for sample in train_data:
if sample[-1] == 1:
plt.scatter(sample[0], sample[1],c='black',s=6)
else:
plt.scatter(sample[0], sample[1],c='red',s=6)
x = np.linspace(0.,60.,200)
y = -(self.w[0]*x + self.b) / self.w[1]
plt.plot(x,y)
plt.show()
model = Perception()
model.fit_raw_mod(dataset, lr=1)
w:[8.54367705 9.34962314], b:-542.0
model = Perception()
model.fit_dual_mod(dataset, lr=1)
w:[ 6.06428531 19.42194872], b:-749.0
model.predict(np.array([20,30]))
-1
model.predict(np.array([50,50]))
1