这篇博客主要是为大家介绍一下感知机模型(Perception)。感知机模型是机器学习当中的一个二分类器模型,并且是一种线性分类器。模型的输入为样本的特征,输出为样本类别,或者称之为样本标记。
接下来首先给出感知机模型的定义:
其中为符号函数,定义如下:
可以看出定义公式由三部分组成:,
和
,其中
和
分别为权重和偏置,而
为样本的特征向量。感知机的工作原理是通过给定某个样本的特征向量
作为感知机模型的输入,通过感知机模型的定义式,得出
的值为模型的输出,如果
为1,则可以预测样本
为一个正例样本,如果
为-1,则预测样本
为一个负例样本,这便是感知机的工作原理。
感知机的几何解释:
实际上,每一个感知机模型对应了一个分类超平面,如果样本的特征空间为2维的,则分类超平面对应于二维空间中的一直线,如果样本的特征空间为3维的,则分类超平面对应于三维空间中的一个平面。如下图所示:
上图为特征空间为二维空间,对应的分类超平面如上图所示,样本点被分为了两类,在分类超平面右侧的为正例(方块),在分类超平面左侧的为负例(圆圈);
上图为特征空间为3维时对应的分类超平面,同样的样本点也被分成了两类。
接下来所要完成的工作就是找到最合适的和
,从而确定分类超平面
。那么问题来了,如何得出一个比较好的
和
呢?实际上,
和
一旦确定了,模型也就确定了,但是构建模型的好坏与
和
的取值有很大的关系,而我们接下来所要做的任务就是,通过某种方法去确定一组比较好的参数
和
。
接下来就介绍一下感知机的学习策略(确定和
的策略)
在介绍学习策略之前,首先介绍两个概念,即线性可分数据集和线性不可分数据集:
线性可分数据集:给定某个数据集,其中正例和负例能够被特征空间中的某个分类超平面完全分开,则成这个数据集是线性可分的;
线性不可分数据集:在特征空间中,没有一个分类超平面能够将数据集中的正例和负例完全分开,则称这个数据集是线性不可分的;
首先我们假设存在一个线性可分数据集,其中
,即样本的特征空间为n维的,样本标记
,
,接下来便确定学习策略。确定策略实际上就是确定损失函数(loss function),损失函数是用来衡量预测值和真实值之间的差距大小的,我们所要做的就是对损失函数进行优化,从而得到是损失函数值最小的参数
和
。那么,感知机的损失函数应该如何定义呢?
损失函数可以是误分类点的总数,但是这样的损失函数对于参数和
不可导,因此优化起来比较困难。我们在这里所采用的损失函数是所有误分类点到分类超平面的距离总和,我们的目标是使这个距离总和尽可能的小。某个点到平面的距离公式为
,式中
为
的二范数。
如果样本点被误分类了,即如果样本的真实标记
,则
,即预测得到的样本标记为
;反之,如果样本真实标记
,则
,即预测得到的样本标记为
,因此可以总结如下规律,如果样本被误分类,则符合下式:
故而,我们可以得到误分类点到分类超平面的距离可表示为:
假设针对当前的和
,误分类点集合为
,那么误分类点到超平面的总距离为:
不考虑就得到了感知机的损失函数:
可以看出损失函数是非负的,即如果误分类点越少,则损失函数越小,其值越趋近于0。我们的目标就是确定和
,使得损失函数能够尽可能的小。
在这里我们采用梯度下降法来对损失函数进行优化,进而求得相对合适的和
。梯度下降法原理在这里就不再为大家进行介绍了,大家可以从网上或者是一些优化相关书籍上进行学习。这里只介绍一下梯度下降算法的几种形式:
- BGD(Batch Gradient Descent):也叫做批量梯度下降,这种形式的梯度下降是通过计算所有训练样本的损失函数值之和的梯度,来对损失函数进行优化,对参数进行更新迭代,也就是说每一次参数的更新,都将使用所有的训练样本。
- SGD(Stochastic Gradient Descent):也叫做随机梯度下降,这种形式的梯度下降是一个训练样本计算一次损失,计算一次梯度,对参数进行更新一次。
- MSGD(Mini-Batch SGD):小批量随机梯度下降算法是批量梯度下降与随机梯度下降的折中版本,即每次既不取1个训练样本来计算梯度,也不是取全部样本来进行梯度的计算,而是只取训练样本的一部分来进行梯度的计算。
在这里给大家列出一段用python编写的梯度下降算法求解函数最优解的例子以供参考:
import numpy as np
from numpy import random as rd
np.set_printoptions(linewidth=1000, suppress=True)
def gradient_decent():
# 这个函数为使用梯度下降求取y = x1 ** 2 + x2 ** 2的最优解
# 函数在某一点(x1, x2)处的梯度为(2 * x1, 2 * x2)
# 首先随机挑选出一个梯度下降的起始点(x1_start, x2_start)
x_start = rd.uniform(-100, 100, 2)
# 给出学习率alpha
alpha = 0.1
# 梯度下降算法的迭代次数
iter_times = 300
for i in range(iter_times):
x_start -= alpha * 2 * x_start
if i % 30 == 0:
print("第%d次迭代:" % i, x_start)
print("最优解:", x_start)
if __name__ == "__main__":
gradient_decent()
运行结果如下图所示:
可以看出,使用梯度下降算法,最终确实收敛到了最优解(0, 0)处
接下来,我们将使用SGD来对感知机的损失函数进行优化
每一个误分类样本对应的损失函数值为:
由此可计算梯度:
因此,我们便的到了和
的更新迭代公式,下式中
为梯度下降算法的学习率:
通过以上迭代公式,对误分类样本集中的样本进行遍历,每遍历一个误分类样本,对和
进行一次更新。每更新完所有误分类样本一次,为一个epoch,可以进行多次epoch来进行参数的更新迭代,最终得到最优的参数
和
。从而构建出相应的感知机模型
,至此,便求得了最终的感知机模型。当新样本点
出现时,只需将其带入到模型中,便可预测得知新样本点的类别。
最后给出感知机学习算法的整体流程:
step1:选取初始值
step2:在训练集中选取数据
step3:如果当前模型对其误分类则使用上述和
的更新迭代公式进行参数更新
step4:判断是否达到终止条件,如果达到则终止,得到最终模型,否则跳到step2,这里终止条件可以是是否达到迭代次数上限,也可以是误分类样本数量是否为0
到这里,感知机的基本原理已经介绍完毕了,下面附上本人使用python编写的感知机相关程序以供大家参考:
import numpy as np
from numpy import random as rd
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
np.set_printoptions(linewidth=1000, suppress=True)
def generate_sample(sample_count, noise_rate):
# sample_count为样本个数
# 此函数用于产生样本点x=(x1, x2)为特征向量,y为样本标记
# 以2 * x1 + 2 * x2 - 1 = 0 为分类超平面构建样本点
x = rd.uniform(-5, 5, (sample_count, 2))
y = (2 * x[:, 0] + 2 * x[:, 1] - 1 >= 0).astype(np.int8)
y[y == 0] = -1
possitive_sample_x = x[y == 1]
negative_sample_x = x[y == -1]
fig = plt.figure()
ax = plt.subplot(1, 1, 1)
ax.scatter(possitive_sample_x[:, 0], possitive_sample_x[:, 1], label="possitive sample", marker="*", color="r")
ax.scatter(negative_sample_x[:, 0], negative_sample_x[:, 1], label="negative sample", marker="^", color="b")
ax.set_xlabel("x1")
ax.set_ylabel("x2")
ax.set_title("samples")
ax.legend(loc="upper right")
plt.show()
# 为特征加一列1,方便加偏置项b
scalar_1 = np.ones((x.shape[0], 1), dtype=np.float64)
x = np.concatenate([scalar_1, x], axis=1)
noise_num = int(sample_count * noise_rate)
# 构造线性不可分样本集
for i in range(noise_num):
rand_int = rd.randint(0, len(y))
if y[rand_int] == 1:
y[rand_int] = -1
else:
y[rand_int] = 1
return x, y
class Perception(object):
def __init__(self, sample_count, noise_rate, alpha, rho, test_size, epochs, lamda):
"""
:param test_size: 测试样本占总样本的比例
:param noise_rate: 为了构造线性不可分数据而添加的噪音样本,此参数用于指定噪音样本数占总样本数的比例
:param sample_count: 总的样本容量
:param alpha: 梯度下降的学习率
:param rho: 学习率衰减系数
:param epochs: 进行多少个epoch
:param lamda: 正则项系数
"""
# self.x, self.y为随机产生的样本点,self.x为特征向量,self.y为分类标记
self.x, self.y = generate_sample(sample_count, noise_rate)
# self.x_train, self.y_train分别为训练集的输入和标签
# self.x_test, self.y_test分别为测试集的输入和标签
self.x_train, self.x_test, self.y_train, self.y_test = train_test_split(self.x, self.y, test_size=test_size, random_state=1)
self.alpha = alpha
self.rho = rho
# 初始化权重向量w为self.weight,其中第一个即self.weight[0, 0]为偏置b
self.weight = rd.uniform(-10, 10, (1, self.x_test.shape[1]))
self.epochs = epochs
self.lamda = lamda
def train(self):
print("开始训练...")
for i in range(self.epochs):
# 第一层循环用于控制进行多少次epoch
# 将当前权重下正例点索引以及负例点索引取出
positive_index = (np.dot(self.x_train, self.weight.T).ravel() >= 0).astype(np.int8)
negative_index = -(np.dot(self.x_train, self.weight.T).ravel() < 0).astype(np.int8)
# label为当前权重向量下的训练集的分类标记
label = positive_index + negative_index
# 对比真实标记self.y_train以及当前分类标记label找出在当前权重self.weight下的误分类点的索引wrong_classification_sample_index
wrong_classification_sample_index = label != self.y_train
# 将当前误分类点取出,当前误分类点集合为wrong_classification_samples,wrong_sample_true_label为当前误分类点的真实标记
wrong_classification_samples = self.x_train[wrong_classification_sample_index]
wrong_sample_true_label = self.y_train[wrong_classification_sample_index]
# sgd:随机梯度下降
for x, y in zip(wrong_classification_samples, wrong_sample_true_label):
self.weight += (self.alpha * y * x - 2 * self.lamda * self.weight)
self.alpha *= self.rho
y_train_pred_positive = (np.dot(self.x_train, self.weight.T) >= 0).astype(np.int8)
y_train_pred_negative = -(np.dot(self.x_train, self.weight.T) < 0).astype(np.int8)
y_train_pred = y_train_pred_positive + y_train_pred_negative
accuracy_rate = accuracy_score(self.y_train, y_train_pred)
print("epoch %d 训练集准确率: " % (i + 1), "%.2f%s" % (accuracy_rate * 100, "%"))
if np.any(np.isnan(self.weight[0])):
raise Exception("正则项系数设置不合理,应该设置小一些")
print("\n训练得到:")
self.b = self.weight[0, 0]
self.w1 = self.weight[:, 1:].ravel()[0]
self.w2 = self.weight[:, 1:].ravel()[1]
print("b =", self.weight[0, 0])
print("[w1, w2] =", self.weight[:, 1:].ravel())
print("真实的分类超平面:%s" % "2 * x1 + 2 * x2 - 1 = 0")
print("训练得到的分类超平面:%f * x1 + %f * x2 + (%f) = 0" % (self.weight[0, 1] / np.abs(self.weight[0, 0]),
self.weight[0, 2] / np.abs(self.weight[0, 0]), self.weight[0, 0] / np.abs(self.weight[0, 0])))
print("================================")
def test(self):
print("开始测试...")
y_test_positive_pred = (np.dot(self.x_test, self.weight.T) >= 0).astype(np.int8)
y_test_negative_pred = -(np.dot(self.x_test, self.weight.T) < 0).astype(np.int8)
self.y_test_pred = (y_test_negative_pred + y_test_positive_pred).ravel()
accuracy_rate = accuracy_score(self.y_test, self.y_test_pred)
print("测试集准确率为:%.2f%s" % (accuracy_rate * 100, "%"))
def draw_test(self):
# x_test_wrong_classified_samples为测试样本上误分类的样本点集合
# x_test_right_classified_samples为测试样本上正确分类样本点集合
# y_test_right_classified为正确分类样本点对应的标记
x_test_wrong_classified_samples = self.x_test[(self.y_test_pred - self.y_test).ravel() != 0]
self.wrong_count = x_test_wrong_classified_samples.shape[0]
print("测试集错误分类样本数为:%d" % self.wrong_count)
print("测试集样本容量:%d" % self.x_test.shape[0])
x_test_right_classified_samples = self.x_test[(self.y_test_pred - self.y_test).ravel() == 0]
y_test_right_classified = self.y_test[(self.y_test_pred - self.y_test).ravel() == 0]
# x_test_positive为测试集中正确分类样本点中的正例
# x_test_negative为测试集中错误分类样本点中的负例
x_test_positive = x_test_right_classified_samples[y_test_right_classified == 1]
x_test_negative = x_test_right_classified_samples[y_test_right_classified == -1]
fig = plt.figure()
ax = plt.subplot(1, 1, 1)
ax.scatter(x_test_positive[:, 1].ravel(), x_test_positive[:, -1].ravel(), marker="*", color="r", label="positive test samples")
ax.scatter(x_test_negative[:, 1].ravel(), x_test_negative[:, -1].ravel(), marker="^", color="y", label="negative test samples")
if list(x_test_wrong_classified_samples):
ax.scatter(x_test_wrong_classified_samples[:, 1].ravel(), x_test_wrong_classified_samples[:, -1].ravel(), marker="o", color="g", label="wrong classified samples")
x1 = np.linspace(-5, 5, 1000)
x2 = (-self.b - self.w1 * x1) / self.w2
ax.plot(x1, x2, color="b", label="the classification hyperplane obtained by training")
ax.legend(loc="upper right")
ax.set_title("predict result of test dataset")
plt.show()
def main():
perception = Perception(200, 0, 0.1, 0.9, 0.4, 100, 0.005)
perception.train()
perception.test()
perception.draw_test()
if __name__ == "__main__":
main()