感知机:假设输入空间是
X
⊆
R
n
\mathcal{X}\subseteq \mathbb{R}^n
X⊆Rn,输出空间为
Y
=
{
+
1
,
−
1
}
\mathcal{Y}=\{+1,-1\}
Y={+1,−1},输入
x
∈
X
\pmb{x}\in\mathcal{X}
xx∈X 表示输入实例的特征向量,输出
y
∈
Y
y\in\mathcal{Y}
y∈Y 表示实例的类别,由以下函数确定
分离超平面,如下所示
线性可分的,即对于所有
y
i
=
−
1
y_i=-1
yi=−1 的样本,有
w
⊤
x
i
+
b
>
0
\pmb{w^\top x_i}+b > 0
w⊤xiw⊤xi+b>0;对于所有
y
i
=
+
1
y_i=+1
yi=+1 的样本,有
w
⊤
x
i
+
b
<
0
\pmb{w^\top x_i}+b< 0
w⊤xiw⊤xi+b<0。感知机模型的训练目标是找到一个能完全正确划分正负样本的超平面误分类点到分离超平面的总距离作为损失使用随机梯度下降法进行优化,具体而言,先任意选取一个超平面
w
0
,
b
0
\pmb{w}_0,b_0
ww0,b0(通常取
0
,
0
0,0
0,0),然后每次随机选取一个误分类点
(
x
i
,
y
i
)
(\pmb{x}_i,y_i)
(xxi,yi),如下执行梯度下降
w
←
w
+
η
y
i
x
i
b
←
b
+
η
y
i
ww←ww+ηyixixib←b+ηyi
wwb←ww+ηyixixi←b+ηyi 其中
η
∈
(
0
,
1
]
\eta\in (0,1]
η∈(0,1] 是学习率
以上流程称为感知机训练的原始形式,给出其伪代码

直观地看,每轮迭代中我们选择一个误分类点,调整
w
,
b
\pmb{w},b
ww,b 的值使分类平面向它靠近,从而减少该误分类点距离分离超平面的距离,直到超平面越过该点使其被正确分类
可以证明
对偶形式的基本想法是:将 w \pmb{w} ww 和 b b b 表示为样本特征 x i \pmb{x}_i xxi 和标记 y i y_i yi 线性组合的形式,通过求解其系数而求得 w \pmb{w} ww 和 b b b,对于感知机而言其原始形式和对偶形式是等价的,下面我们将原始形式转换为对偶形式
下面给出对偶形式算法的伪代码

注意到这里训练样本仅以内积形式出现,为了提高效率,可以先将样本间的内积都计算出来并以矩阵形式存储
Note:这个对偶形式其实很少用,我觉得《统计学习方法(第二版)》里介绍它主要是为了引出后面的 SVM 章节。其实对比一下感知机和 SVM,会发现二者都是在以最大间隔为优化标准做分类任务,只是 SVM 中多了约束项,所以求解 SVM 时常常用拉格朗日乘数法转换为对偶问题,像感知机这种数据线性可分的情况,SVM 的原始问题和对偶问题可以同时取到最优解。事实上感知机就是 SVM 算法的重要基础,也可以类似 SVM 的处理过程那样得到它的对偶求解形式
和感知机学习算法的原始形式一样,对偶形式的算法也对线性可分数据集收敛,并且存在多个解
用 pytorch 实现感知机训练,数据使用《统计学习方法(第二版)》例 2.1 中的数据

代码如下,可以直接复制进 vscode 运行
import numpy as np
import torch
import random
import matplotlib.pyplot as plt
def data_iter(batch_size, features, labels):
num_examples = len(features)
indices = list(range(num_examples))
random.shuffle(indices) # 打乱一下,样本的读取顺序是随机的
# 使用 yield 关键字将此函数转为迭代器,每次访问取 batch_size 数据返回
for i in range(0, num_examples, batch_size):
j = torch.LongTensor(indices[i: min(i + batch_size, num_examples)]) # 最后一次可能不足一个batch
yield features.index_select(0, j)[0], labels.index_select(0, j)[0]
# 损失函数
def steploss(y, x, w, b):
return -y*(torch.dot(x, w) + b)
# 模型
def perceptron(x, w, b):
return 1 if (torch.dot(x, w) + b).item() >= 0 else -1
# 优化方法使用小批量梯度下降
def mbgd(params, lr, batch_size):
for param in params:
param.data -= lr * param.grad / batch_size # 注意这里更改param时用的param.data
# 训练
def train(net, features, labels, loss, param_w, param_b, batch_size=1, lr=1):
w, b = param_w, param_b
done = False
while not done:
for X, y in data_iter(batch_size, features, labels):
_ = net(X, w, b)
l = loss(y, X, w, b).sum()
if w.grad != None:
w.grad.data.zero_()
if b.grad != None:
b.grad.data.zero_()
if y*(torch.dot(X, w) + b) <= 0:
l.backward()
mbgd((w, b), lr, batch_size)
print('误分类点:', X, end='; ')
print('更新后参数: w={}, b={}'.format(w.tolist(), b.tolist()))
# 检查是否全部分类正确了
done = True
for X, y in data_iter(batch_size, features, labels):
if y*(torch.dot(X, w) + b) <= 0:
done = False
break
if __name__ == '__main__':
# 参数初始化
w = torch.tensor([0,0], dtype=torch.float32, requires_grad=True)
b = torch.zeros(1, dtype=torch.float32, requires_grad=True)
# 训练样本 & 标记
features = torch.tensor([[3,3],[4,3],[1,1]],dtype=torch.float32)
labels = torch.tensor([1,1,-1],dtype=torch.float32).view(3,1)
# 训练
train(perceptron, features, labels, steploss, w, b, 1, 1)
# 绘制分离超平面
plt.figure(figsize=(6, 6))
x1 = np.linspace(0, 5, 500)
if w[1].item() != 0:
x2 = (-x1*w[0].item()-b.item())/w[1].item()
else:
x2 = np.linspace(0, 5, 500)
x1 = torch.full((500,), -b.item()/w[0].item())
positive = torch.masked_select(features, labels==1).view(-1,2)
negative = torch.masked_select(features, labels==-1).view(-1,2)
plt.scatter(x1, x2, s=1,alpha=1,cmap="r")
plt.scatter(positive[:,0], positive[:,1], s=50,alpha=1,cmap="b")
plt.scatter(negative[:,0], negative[:,1], s=50,alpha=1,cmap="b")
plt.show()
'''
误分类点: tensor([3., 3.]); 更新后参数: w=[3.0, 3.0], b=[1.0]
误分类点: tensor([1., 1.]); 更新后参数: w=[2.0, 2.0], b=[0.0]
误分类点: tensor([1., 1.]); 更新后参数: w=[1.0, 1.0], b=[-1.0]
误分类点: tensor([1., 1.]); 更新后参数: w=[0.0, 0.0], b=[-2.0]
误分类点: tensor([4., 3.]); 更新后参数: w=[4.0, 3.0], b=[-1.0]
误分类点: tensor([1., 1.]); 更新后参数: w=[3.0, 2.0], b=[-2.0]
误分类点: tensor([1., 1.]); 更新后参数: w=[2.0, 1.0], b=[-3.0]
误分类点: tensor([1., 1.]); 更新后参数: w=[1.0, 0.0], b=[-4.0]
误分类点: tensor([3., 3.]); 更新后参数: w=[4.0, 3.0], b=[-3.0]
误分类点: tensor([1., 1.]); 更新后参数: w=[3.0, 2.0], b=[-4.0]
误分类点: tensor([1., 1.]); 更新后参数: w=[2.0, 1.0], b=[-5.0]
'''
'''
误分类点: tensor([3., 3.]); 更新后参数: w=[3.0, 3.0], b=[1.0]
误分类点: tensor([1., 1.]); 更新后参数: w=[2.0, 2.0], b=[0.0]
误分类点: tensor([1., 1.]); 更新后参数: w=[1.0, 1.0], b=[-1.0]
误分类点: tensor([1., 1.]); 更新后参数: w=[0.0, 0.0], b=[-2.0]
误分类点: tensor([3., 3.]); 更新后参数: w=[3.0, 3.0], b=[-1.0]
误分类点: tensor([1., 1.]); 更新后参数: w=[2.0, 2.0], b=[-2.0]
误分类点: tensor([1., 1.]); 更新后参数: w=[1.0, 1.0], b=[-3.0]
'''
'''
误分类点: tensor([4., 3.]); 更新后参数: w=[4.0, 3.0], b=[1.0]
误分类点: tensor([1., 1.]); 更新后参数: w=[3.0, 2.0], b=[0.0]
误分类点: tensor([1., 1.]); 更新后参数: w=[2.0, 1.0], b=[-1.0]
误分类点: tensor([1., 1.]); 更新后参数: w=[1.0, 0.0], b=[-2.0]
'''
这里取数据是随机的,多次运行即可看到训练时使用样本顺序不同对结果造成的影响。最后的三段注释过程分别对应下面三个结果

另外,这里参数都初始化为 0,修改初始参数训练结果也会有所不同