【附源码】一看就懂的感知机算法PLA_感知机


AI有道


一个有情怀的公众号


【附源码】一看就懂的感知机算法PLA_迭代_02



本文所有的源代码均放在了我的GitHub上,需要的点击文末「阅读原文」获取。如果对你有用的话,别忘了Fork和Star哦!


什么是感知机「Perceptron」


PLA全称是Perceptron Linear Algorithm,即线性感知机算法,属于一种最简单的感知机(Perceptron)模型。

感知机模型是机器学习二分类问题中的一个非常简单的模型。它的基本结构如下图所示:

【附源码】一看就懂的感知机算法PLA_权重_03

其中,xi 是输入, w表示权重系数, b 表示偏移常数。感知机的线性输出为:

【附源码】一看就懂的感知机算法PLA_感知机_04

为了简化计算,通常我们将 b 作为权重系数的一个维度,即 w0 。同时,将输入 x 扩展一个维度,为1。这样,上式简化为:

【附源码】一看就懂的感知机算法PLA_感知机_05

scores 是感知机的输出,接下来就要对 scores 进行判断:


  • 若 scores≥0 ,则 y_pred=1 「正类」
  • 若 scores<0 ,则 y_pred=−1 「负类」


线性得分计算阈值比较两个过程组成,最后根据比较结果判断样本属于正类还是负类。


PLA理论解释


对于二分类问题,可以使用感知机模型来解决。PLA的基本原理就是逐点修正,首先在超平面上随意取一条分类面,统计分类错误的点;然后随机对某个错误点就行修正,即变换直线的位置,使该错误点得以修正;接着再随机选择一个错误点进行纠正,分类面不断变化,直到所有的点都完全分类正确了,就得到了最佳的分类面。

利用二维平面例子来进行解释,第一种情况是错误地将正样本「y=+1」分类为负样本「y=-1」。此时,wx<0,即w与x的夹角大于90度,分类线 的两侧。修正的方法是让夹角变小,修正w值,使二者位于直线同侧:

【附源码】一看就懂的感知机算法PLA_感知机_06

修正过程示意图如下所示:

【附源码】一看就懂的感知机算法PLA_感知机_07

第二种情况是错误地将负样本「y=-1」分类为正样本「y=+1」。此时, wx>0 ,即 w 与 x 的夹角小于90度,分类线 l 的同一侧。修正的方法是让夹角变大,修正 w 值,使二者位于直线两侧:

【附源码】一看就懂的感知机算法PLA_迭代_08

修正过程示意图如下所示:

【附源码】一看就懂的感知机算法PLA_感知机_09

经过两种情况分析,我们发现PLA每次 w 的更新表达式都是一样的: w:=w+yx 。掌握了每次 w 的优化表达式,那么PLA就能不断地将所有错误的分类样本纠正并分类正确。


数据准备


该数据集包含了100个样本,正负样本各50,特征维度为2。


import numpy as np
import pandas as pd

data = pd.read_csv('./data/data1.csv', header=None)
# 样本输入,维度(100,2)
X = data.iloc[:,:2].values
# 样本输出,维度(100,)
y = data.iloc[:,2].values


下面我们在二维平面上绘出正负样本的分布情况。


import matplotlib.pyplot as plt

plt.scatter(X[:50, 0], X[:50, 1], color='blue', marker='o', label='Positive')
plt.scatter(X[50:, 0], X[50:, 1], color='red', marker='x', label='Negative')
plt.xlabel('Feature 1')
plt.ylabel('Feature 2')
plt.legend(loc = 'upper left')
plt.title('Original Data')
plt.show()


【附源码】一看就懂的感知机算法PLA_迭代_10


PLA算法


首先分别对两个特征进行归一化处理,即:

【附源码】一看就懂的感知机算法PLA_权重_11

其中, μ 是特征均值, σ 是特征标准差。


# 均值
u = np.mean(X, axis=0)
# 方差
v = np.std(X, axis=0)

X = (X - u) / v

# 作图
plt.scatter(X[:50, 0], X[:50, 1], color='blue', marker='o', label='Positive')
plt.scatter(X[50:, 0], X[50:, 1], color='red', marker='x', label='Negative')
plt.xlabel('Feature 1')
plt.ylabel('Feature 2')
plt.legend(loc = 'upper left')
plt.title('Normalization data')
plt.show()


【附源码】一看就懂的感知机算法PLA_迭代_12

接下来对预测直线进行初始化,包括权重 w 初始化:


# X加上偏置项
X = np.hstack((np.ones((X.shape[0],1)), X))
# 权重初始化
w = np.random.randn(3,1)


接下来,计算scores,得分函数与阈值0做比较,大于零则 ŷ =1 ,小于零则 ŷ =−1。


s = np.dot(X, w)
y_pred = np.ones_like(y) # 预测输出初始化
loc_n = np.where(s < 0)[0] # 大于零索引下标
y_pred[loc_n] = -1


接着,从分类错误的样本中选择一个,使用PLA更新权重系数 w。


# 第一个分类错误的点
t = np.where(y != y_pred)[0][0]
# 更新权重w
w += y[t] * X[t, :].reshape((3,1))


更新权重 w 是个迭代过程,只要存在分类错误的样本,就不断进行更新,直至所有的样本都分类正确(注意,前提是正负样本完全可分)。

整个迭代训练过程如下:


for i in range(100):
s = np.dot(X, w)
y_pred = np.ones_like(y)
loc_n = np.where(s < 0)[0]
y_pred[loc_n] = -1
num_fault = len(np.where(y != y_pred)[0])
print('第%2d次更新,分类错误的点个数:%2d' % (i, num_fault))
if num_fault == 0:
break
else:
t = np.where(y != y_pred)[0][0]
w += y[t] * X[t, :].reshape((3,1))


迭代完毕后,得到更新后的权重系数 w ,绘制此时的分类直线是什么样子。


# 直线第一个坐标(x1,y1)
x1 = -2
y1 = -1 / w[2] * (w[0] * 1 + w[1] * x1)
# 直线第二个坐标(x2,y2)
x2 = 2
y2 = -1 / w[2] * (w[0] * 1 + w[1] * x2)
# 作图
plt.scatter(X[:50, 1], X[:50, 2], color='blue', marker='o', label='Positive')
plt.scatter(X[50:, 1], X[50:, 2], color='red', marker='x', label='Negative')
plt.plot([x1,x2], [y1,y2],'r')
plt.xlabel('Feature 1')
plt.ylabel('Feature 2')
plt.legend(loc = 'upper left')
plt.show()


【附源码】一看就懂的感知机算法PLA_迭代_13

其实,PLA算法的效率还算不错,只需要数次更新就能找到一条能将所有样本完全分类正确的分类线。所以得出结论,对于正负样本线性可分的情况,PLA能够在有限次迭代后得到正确的分类直线。


总结与疑问


本文导入的数据本身就是线性可分的,可以使用PLA来得到分类直线。但是,如果数据不是线性可分,即找不到一条直线能够将所有的正负样本完全分类正确,这种情况下,似乎PLA会永远更新迭代下去,却找不到正确的分类线。

对于线性不可分的情况,该如何使用PLA算法呢?我们下次将对PLA进行改进和优化。




【附源码】一看就懂的感知机算法PLA_权重_14