感知机(perceptron)是二类分类的线性分类模型,其输入为实例的特征向量,输出为实例的类别,取+1和-1二值。感知机对应于输入空间(特征空间)中将实例划分为正负两类的分离超平面,属于判别模型。感知机学习旨在求出将训练数据进行线性划分的分离超平面,为此,导入基于误分类的损失函数,利用梯度下降法对损失函数进行极小化,求得感知机模型。
1、介绍
二分类模型
f ( x ) = s i g n ( w ⋅ x + b ) ; sign ( x ) = { + 1 , x ⩾ 0 − 1 , x < 0 f(x) = sign(w\cdot x + b); \quad \operatorname{sign}(x)=\left\{\begin{array}{ll}{+1,} \quad {x \geqslant 0} \\ {-1,} \quad {x\lt0}\end{array}\right. f(x)=sign(w⋅x+b);sign(x)={+1,x⩾0−1,x<0
给定训练集:
T = { ( x 1 , y 1 ) , ( x 2 , y 2 ) , ⋯ , ( x N , y N ) } T=\left\{\left(x_{1}, y_{1}\right),\left(x_{2}, y_{2}\right), \cdots,\left(x_{N}, y_{N}\right)\right\} T={(x1,y1),(x2,y2),⋯,(xN,yN)}
定义感知机的损失函数
L ( w , b ) = − ∑ x i ∈ M y i ( w ⋅ x i + b ) L(w, b)=-\sum_{x_{i} \in M} y_{i}\left(w \cdot x_{i}+b\right) L(w,b)=−xi∈M∑yi(w⋅xi+b)
感知机是二类分类的线性分类模型。
- 损失函数 L ( w , b ) L(w,b) L(w,b)的经验风险最小化
- 本章中涉及到向量内积,有超平面的概念,也有线性可分数据集的说明,在策略部分有说明损关于失函数的选择的考虑,可以和第七章一起看。另外, 感知机和SVM的更多联系源自margin的思想, 实际上在本章的介绍中并没有体现margin的思想,参考文献中有给出对应的文献。
- 本章涉及的两个例子,思考一下为什么 η = 1 \eta=1 η=1,进而思考一下参数空间,这两个例子设计了相应的测试案例实现, 在后面的内容中也有展示。
- 在收敛性证明那部分提到了偏置合并到权重向量的技巧,合并后的权重向量叫做扩充权重向量,这点在LR和SVM中都有应用,但是这种技巧在书中的表示方式是不一样的,采用的不是统一的符号体系,或者说不是统一的。本书三个章节讨论过算法的收敛性,感知机, AdaBoost,EM算法。
- 第一次涉及Gram Matrix G = [ x i ⋅ x j ] N × N G=[x_i\cdot x_j]_{N\times N} G=[xi⋅xj]N×N
- 感知机的激活函数是符号函数。
- 感知机是神经网络和支持向量机的基础。
- 当我们讨论决策边界的时候, 实际上是在考虑算法的几何解释。
2、三要素
模型
输入空间: X ⊆ R n \mathcal X\sube \bf R^n X⊆Rn
输出空间: Y = + 1 , − 1 \mathcal Y={+1,-1} Y=+1,−1
决策函数: f ( x ) = s i g n ( w ⋅ x + b ) f(x)=sign (w\cdot x+b) f(x)=sign(w⋅x+b)
策略
确定学习策略就是定义 (经验) 损失函数并将损失函数最小化。
注意这里提到了经验,所以学习是base在训练数据集上的操作
损失函数选择
损失函数的一个自然选择是误分类点的总数,但是,这样的损失函数不是参数 w , b w,b w,b的连续可导函数,不易优化
损失函数的另一个选择是误分类点到超平面 S S S的总距离,这是感知机所采用的
感知机学习的经验风险函数(损失函数)
L
(
w
,
b
)
=
−
∑
x
i
∈
M
y
i
(
w
⋅
x
i
+
b
)
L(w,b)=-\sum_{x_i\in M}y_i(w\cdot x_i+b)
L(w,b)=−xi∈M∑yi(w⋅xi+b)
其中
M
M
M是误分类点的集合
给定训练数据集 T T T,损失函数 L ( w , b ) L(w,b) L(w,b)是 w w w和 b b b的连续可导函数
算法
原始形式
输入: T = { ( x 1 , y 1 ) , ( x 2 , y 2 ) , … , ( x N , y N ) } x i ∈ X = R n , y i ∈ Y = { − 1 , + 1 } , i = 1 , 2 , … , N ; 0 < η ⩽ 1 T=\{(x_1,y_1),(x_2,y_2),\dots,(x_N,y_N)\}\\ x_i\in \mathcal{X}=\bf{R}^n , y_i\in \mathcal{Y} =\{-1,+1\}, i=1,2,\dots, N; 0< \eta \leqslant 1 T={(x1,y1),(x2,y2),…,(xN,yN)}xi∈X=Rn,yi∈Y={−1,+1},i=1,2,…,N;0<η⩽1
输出: w , b ; f ( x ) = s i g n ( w ⋅ x + b ) w,b;f(x)=sign(w\cdot x+b) w,b;f(x)=sign(w⋅x+b)
选取初值 w 0 , b 0 w_0,b_0 w0,b0
训练集中选取数据 ( x i , y i ) (x_i,y_i) (xi,yi)
如果 y i ( w ⋅ x i + b ) ⩽ 0 y_i(w\cdot x_i+b)\leqslant 0 yi(w⋅xi+b)⩽0
w ← w + η y i x i b ← b + η y i w\leftarrow w+\eta y_i x_i \\ b\leftarrow b+\eta y_i w←w+ηyixib←b+ηyi
转至(2),直至训练集中没有误分类点
注意这个原始形式中的迭代公式,可以对 x x x补1,将 w w w和 b b b合并在一起,合在一起的这个叫做扩充权重向量,书上有提到。
对偶形式
对偶形式的基本思想是将 w w w和 b b b表示为实例 x i x_i xi和标记 y i y_i yi的线性组合的形式,通过求解其系数而求得 w w w和 b b b。
输入: T = { ( x 1 , y 1 ) , ( x 2 , y 2 ) , … , ( x N , y N ) } x i ∈ X = R n , y i ∈ Y = { − 1 , + 1 } , i = 1 , 2 , … , N ; 0 < η ⩽ 1 T=\{(x_1,y_1),(x_2,y_2),\dots,(x_N,y_N)\}\\ x_i\in \mathcal{X}=\bf{R}^n , y_i\in \mathcal{Y} =\{-1,+1\}, i=1,2,\dots, N; 0< \eta \leqslant 1 T={(x1,y1),(x2,y2),…,(xN,yN)}xi∈X=Rn,yi∈Y={−1,+1},i=1,2,…,N;0<η⩽1
输出:
α , b ; f ( x ) = s i g n ( ∑ j = 1 N α j y j x j ⋅ x + b ) a l p h a = ( α 1 , α 2 , ⋯ , α N ) T \alpha ,b; f(x)=sign\left(\sum_{j=1}^N\alpha_jy_jx_j\cdot x+b\right) \\alpha=(\alpha_1,\alpha_2,\cdots,\alpha_N)^T α,b;f(x)=sign(j=1∑Nαjyjxj⋅x+b)alpha=(α1,α2,⋯,αN)T
α ← 0 , b ← 0 \alpha \leftarrow 0,b\leftarrow 0 α←0,b←0
训练集中选取数据 ( x i , y i ) (x_i,y_i) (xi,yi)
如果 y i ( ∑ j = 1 N α j y j x j ⋅ x + b ) ⩽ 0 y_i\left(\sum_{j=1}^N\alpha_jy_jx_j\cdot x+b\right) \leqslant 0 yi(∑j=1Nαjyjxj⋅x+b)⩽0
α i ← α i + η b ← b + η y i \alpha_i\leftarrow \alpha_i+\eta \\b\leftarrow b+\eta y_i αi←αi+ηb←b+ηyi转至(2),直至训练集中没有误分类点
Gram matrix
对偶形式中,训练实例仅以内积的形式出现。
为了方便可预先将训练集中的实例间的内积计算出来并以矩阵的形式存储,这个矩阵就是所谓的Gram矩阵
G
=
[
x
i
⋅
x
j
]
N
×
N
G=[x_i\cdot x_j]_{N\times N}
G=[xi⋅xj]N×N
3、概要总结
1.感知机是根据输入实例的特征向量 x x x对其进行二类分类的线性分类模型:
f ( x ) = sign ( w ⋅ x + b ) f(x)=\operatorname{sign}(w \cdot x+b) f(x)=sign(w⋅x+b)
感知机模型对应于输入空间(特征空间)中的分离超平面 w ⋅ x + b = 0 w \cdot x+b=0 w⋅x+b=0。
2.感知机学习的策略是极小化损失函数:
min w , b L ( w , b ) = − ∑ x i ∈ M y i ( w ⋅ x i + b ) \min _{w, b} L(w, b)=-\sum_{x_{i} \in M} y_{i}\left(w \cdot x_{i}+b\right) w,bminL(w,b)=−xi∈M∑yi(w⋅xi+b)
损失函数对应于误分类点到分离超平面的总距离。
3.感知机学习算法是基于随机梯度下降法的对损失函数的最优化算法,有原始形式和对偶形式。算法简单且易于实现。原始形式中,首先任意选取一个超平面,然后用梯度下降法不断极小化目标函数。在这个过程中一次随机选取一个误分类点使其梯度下降。
4.当训练数据集线性可分时,感知机学习算法是收敛的。感知机算法在训练数据集上的误分类次数 k k k满足不等式:
k ⩽ ( R γ ) 2 k \leqslant\left(\frac{R}{\gamma}\right)^{2} k⩽(γR)2
当训练数据集线性可分时,感知机学习算法存在无穷多个解,其解由于不同的初值或不同的迭代顺序而可能有所不同。
4、问题
损失函数
知乎上有个问题
感知机中的损失函数中的分母为什么可以不考虑?
有些人解释是正数,不影响,但是分母中含有 w,而其也是未知数,在考虑损失函数的最值时候会不影响么?想不通
这个对应了书中
P
27
P_{27}
P27中不考虑1/||w||,就得到感知机学习的损失函数
题中问考虑损失函数最值的时候,不会有影响么?
-
感知机处理线性可分数据集,二分类, Y = { + 1 , − 1 } \mathcal Y=\{+1,-1\} Y={+1,−1} ,所以涉及到的乘以 y i y_i yi的操作实际贡献的是符号;
-
损失函数 L ( w , b ) = − ∑ x i ∈ M y i ( w ⋅ x i + b ) L(w,b)=-\sum_{x_i\in M}y_i(w\cdot x_i+b) L(w,b)=−∑xi∈Myi(w⋅xi+b),其中 M M M 是错分的点集合,线性可分的数据集肯定能找到超平面 S S S, 所以这个损失函数最值是0。
-
如果正确分类, y i ( w ⋅ x i + b ) = ∣ w ⋅ x i + b ∣ y_i(w\cdot x_i+b)=|w\cdot x_i+b| yi(w⋅xi+b)=∣w⋅xi+b∣ ,错误分类的话,为了保证正数就加个负号,这就是损失函数里面那个负号,这个就是函数间隔;
-
1 ∣ ∣ w ∣ ∣ \frac{1}{||w||} ∣∣w∣∣1 用来归一化超平面法向量,得到几何间隔,也就是点到超平面的距离, 函数间隔和几何间隔的差异在于同一个超平面 ( w , b ) (w,b) (w,b) 参数等比例放大成 ( k w , k b ) (kw,kb) (kw,kb) 之后,虽然表示的同一个超平面,但是点到超平面的函数间隔也放大了,但是几何间隔是不变的;
-
具体算法实现的时候, w w w要初始化,然后每次迭代针对错分点进行调整,既然要初始化,那如果初始化个 ∣ ∣ w ∣ ∣ = 1 ||w||=1 ∣∣w∣∣=1 的情况也就不用纠结了,和不考虑 1 ∣ ∣ w ∣ ∣ \frac{1}{||w||} ∣∣w∣∣1 是一样的了;
-
针对错分点是这么调整的
w ← w + η y i x i b ← b + η y i \begin{aligned} w&\leftarrow w+\eta y_ix_i\\ b&\leftarrow b+\eta y_i \end{aligned} wb←w+ηyixi←b+ηyi
前面说了 y i y_i yi 就是个符号,那么感知机就可以解释为针对误分类点,通过调整 w , b w,b w,b 使得超平面向该误分类点一侧移动,迭代这个过程最后全分类正确;
-
感知机的解不唯一,和初值有关系,和误分类点调整顺序也有关系;
-
这么调整就能找到感知机的解?能,Novikoff还证明了,通过有限次搜索能找到将训练数据完全正确分开的分离超平面。
所以,
如果只考虑损失函数的最值,那没啥影响,线性可分数据集,最后这个损失就是0; 那个分母用来归一化法向量,不归一化也一样用,感知机的解不唯一;说正数不影响的应该考虑的是不影响超平面调整方向吧。
当实例点被误分类,即位于分离超平面的错误侧,则调整w, b的值,使分离超平面向该无分类点的一侧移动,直至误分类点被正确分类
拿出iris数据集中两个分类的数据和[sepal length,sepal width]作为特征
import pandas as pd
import numpy as np
from sklearn.datasets import load_iris
import matplotlib.pyplot as plt
# 加载数据
iris = load_iris()
df = pd.DataFrame(iris.data, columns=iris.feature_names)
df['label'] = iris.target
df.head()
Out:
df.columns = ['sepal length', 'sepal width', 'petal length', 'petal width', 'label']
df.label.value_counts()
# 绘图数据集
plt.scatter(df[:50]['sepal length'], df[:50]['sepal width'], label='-1')
plt.scatter(df[50:100]['sepal length'], df[50:100]['sepal width'], label='1')
plt.xlabel('sepal length')
plt.ylabel('sepal width')
plt.legend()
# 将df前100行的第一二及最后一列作为数据集
data = np.array(df.iloc[:100, [0, 1, -1]])
X, y = data[:,:-1], data[:,-1]
y = np.array([1 if i == 1 else -1 for i in y])
X.shape #(100, 2)
y.shape #(100,)
感知机
# 原始形式 2.1
class PLA:
def __init__(self, max_iter=1000, shuffle=False):
self.b = 0
self.lr = 0.1
self.max_iter = max_iter
self.iter = 0
self.shuffle = shuffle
def sign(self, x, w, b):
return np.dot(x, w) + b
def fit(self, X, y):
N, M = X.shape
self.w = np.ones(M)
for n in range(self.max_iter):
self.iter = n
wrong_items = 0
if self.shuffle: #每次迭代,是否打乱
idx = np.random.permutation(range(N))
X,y = X[idx],y[idx]
for i in range(N):
if y[i] * self.sign(X[i], self.w, self.b) <= 0:
self.w += self.lr * np.dot(y[i], X[i])
self.b += self.lr * y[i]
wrong_items += 1
if wrong_items == 0:
print("finished at iters: {}, w: {}, b: {}".format(self.iter, self.w, self.b))
return
print("finished for reaching the max_iter: {}, w: {}, b: {}".format(self.max_iter, self.w, self.b))
# 对偶形式 2.3
class PLA_dual:
def __init__(self, max_iter=1000):
self.b = 0
self.lr = 0.1
self.max_iter = max_iter
self.iter = 0
def mathcal_w(self, X):
w = 0
for i in range(len(self.alpha)):
w += self.alpha[i]*y[i]*X[i]
return w
def gram_matrix(self, X):
return np.dot(X, X.T)
def fit(self, X, y):
N, M = X.shape
self.alpha = np.zeros(N)
gram = self.gram_matrix(X)
for n in range(self.max_iter):
self.iter = n
wrong_items = 0
for i in range(N):
tmp = 0
for j in range(N):
tmp += self.alpha[j] * y[j] * gram[i,j]
tmp += self.b
if y[i] * tmp <= 0:
self.alpha[i] += self.lr
self.b += self.lr * y[i]
wrong_items += 1
if wrong_items == 0:
self.w = self.mathcal_w(X)
print("finished at iters: {}, w: {}, b: {}".format(self.iter, self.w, self.b))
return
self.w = self.mathcal_w(X)
print("finished for reaching the max_iter: {}, w: {}, b: {}".format(self.max_iter, self.w, self.b))
return
perceptron1 = PLA()
perceptron1.fit(X, y)
# finished at iters: 678, w: [ 7.8 -10. ], b: -12.099999999999973
perceptron2 = PLA(shuffle=True)
perceptron2.fit(X, y)
# finished at iters: 474, w: [ 6.87 -8.71], b: -10.899999999999977
perceptron3 = PLA_dual()
perceptron3.fit(X, y)
# finished at iters: 691, w: [ 7.88 -10.07], b: -12.299999999999972
def plot(model, tilte):
x_points = np.linspace(4, 7, 10)
y_ = -(model.w[0]*x_points + model.b)/model.w[1]
plt.plot(x_points, y_)
print(y_)
plt.plot(data[:50, 0], data[:50, 1], 'bo', color='blue', label='-1')
plt.plot(data[50:100, 0], data[50:100, 1], 'bo', color='orange', label='1')
plt.xlabel('sepal length')
plt.ylabel('sepal width')
plt.title(tilte)
plt.legend()
PLA with no shuffle
plot(perceptron1, 'PLA with no shuffle')
Out:
[1.91 2.17 2.43 2.69 2.95 3.21 3.47 3.73 3.99 4.25]
PLA with shuffle
plot(perceptron2, 'PLA with shuffle')
Out:
[1.90355913 2.16647532 2.4293915 2.69230769 2.95522388 3.21814007
3.48105626 3.74397245 4.00688863 4.26980482]
PLA_dual
plot(perceptron3, 'PLA_dual')
Out:
[1.90863952 2.1694803 2.43032109 2.69116187 2.95200265 3.21284343
3.47368421 3.73452499 3.99536577 4.25620655]
scikit-learn 感知机
from sklearn.linear_model import Perceptron
clf = Perceptron()
clf.fit(X, y)
Out:
Perceptron(alpha=0.0001, class_weight=None, early_stopping=False, eta0=1.0,
fit_intercept=True, max_iter=1000, n_iter_no_change=5, n_jobs=None,
penalty=None, random_state=0, shuffle=True, tol=0.001,
validation_fraction=0.1, verbose=0, warm_start=False)
# 指定给特征的权重。
clf.coef_ # array([[ 23.2, -38.7]])
# 截距 Constants in decision function.
clf.intercept_ # array([-5.])
clf.n_iter_ # 8
x_ponits = np.linspace(4, 7, 10)
y_ = -(clf.coef_[0][0]*x_ponits + clf.intercept_)/clf.coef_[0][1]
plt.plot(x_ponits, y_)
plt.plot(data[:50, 0], data[:50, 1], 'bo', color='blue', label='0')
plt.plot(data[50:100, 0], data[50:100, 1], 'bo', color='orange', label='1')
plt.xlabel('sepal length')
plt.ylabel('sepal width')
plt.legend()
在上图中,有一个位于左下角的蓝点没有被正确分类,这是因为 SKlearn 的 Perceptron 实例中有一个tol参数。
tol 参数规定了如果本次迭代的损失和上次迭代的损失之差小于一个特定值时,停止迭代。我们需要设置 tol=None 使之可以继续迭代。
作业
mydata = np.array([[3,3, 1],[4,3, 1],[1,1, -1]])
X = mydata[:,:-1]
y = mydata[:,-1]
def plot1(model):
x_ponits = np.linspace(0, 7, 10)
y_ = -(model.w[0]*x_ponits + model.b)/(model.w[1] + 1e-10)
plt.plot(x_ponits, y_)
plt.plot(mydata[:2, 0], mydata[:2, 1], 'bo', color='blue', label='+1')
plt.plot(mydata[2:, 0], mydata[2:, 1], 'bo', color='orange', label='-1')
plt.legend()
pla1 = PLA() # no shuffle
pla1.fit(X, y) # finished at iters: 7, w: [0.3 0.3], b: -0.7
plot1(pla1)
pla2 = PLA(shuffle = True) # shuffle
pla2.fit(X, y) # finished at iters: 7, w: [0.3 0.3], b: -0.7
plot1(pla2)
pla0 = PLA_dual()
pla0.fit(X,y) # finished at iters: 5, w: [0.1 0.1], b: -0.30000000000000004
plot1(pla0)
# scikit-learn
pla3 = Perceptron()
pla3.fit(X,y)
Out:
Perceptron(alpha=0.0001, class_weight=None, early_stopping=False, eta0=1.0,
fit_intercept=True, max_iter=1000, n_iter_no_change=5, n_jobs=None,
penalty=None, random_state=0, shuffle=True, tol=0.001,
validation_fraction=0.1, verbose=0, warm_start=False)
pla3.coef_ # array([[1., 0.]])
pla3.intercept_ # array([-2.])
x_ponits = np.linspace(0, 7, 10)
y_ = -(pla3.coef_[0][0]*x_ponits + pla3.intercept_)/(pla3.coef_[0][1] + 1e-10)
plt.plot(x_ponits, y_)
plt.plot(mydata[:2, 0], mydata[:2, 1], 'bo', color='blue', label='+1')
plt.plot(mydata[2:, 0], mydata[2:, 1], 'bo', color='orange', label='-1')
plt.legend()
from time import time
lrs = np.arange(0.1, 1.0, 0.1)
t = []
for lr in lrs:
start = time()
pla = Perceptron(eta0 = lr)
pla.fit(X, y)
end = time()
t.append(end - start) #[0.005453586578369141, 0.004275083541870117, 0.0010335445404052734, 0.0007185935974121094, 0.000614166259765625, 0.000614166259765625, 0.0004963874816894531, 0.00043654441833496094, 0.0004296302795410156]
plt.figure()
plt.plot(lrs,t)
plt.title('time cost with different lr')
Out:
Text(0.5, 1.0, 'time cost with different lr')