感知机理论与实践代码

本文主要参考李航老师的《统计学习方法》第二章感知机

感知机(perceptron)是一个二分类的线性分类模型,输入为实例的特征向量,输出为实例的类别,取值为+1或-1。感知机模型是神经网络SVM的基础。

注:下面用到的xi为行向量, ω为列向量

原始感知机

感知机的模型为:

其中,ω称为权值(weight),b称为偏置(bias)。

损失函数为:

对于误分类样本点(xi, yi),损失函数为:L(ω,b ) = -yi ( xi · ω + b ),对其求ω和b的导数为:

故,只要找出误分类点(xi , yi)通过 ω += yi · xiT和 b += yi,即可实现损失函数的减小。

算法的思路为:

  1. 对权重ω,偏置b进行初始化
  2. 通过循环语句从训练样本中找出误分类点,若不存在误分类点,模型训练完成
  3. 通过误分类点更新 ω 和 b,跳转到第2步

以《统计学习方法》上的一个例子进行编码:对训练集( (3,3), 1),( (4,3), 1),( (1,1), 1) 训练感知机。

代码如下:

# 初始化 w=[[0],[0]], b=0
w = np.array([0,0]).reshape(-1,1)
b = 0
def predict(x):
    x = x.reshape(1,-1)
    v = np.dot(x,w)+b
    if(v > 0):return 1
    if(v < 0):return -1
    if(v==0):return 0

# 设置训练速率为1
eta = 1
# 设置训练集
x = np.array([[3,3], [4,3], [1,1]])
y = np.array([1, 1, -1])

# 设置最多迭代次数为100,避免死循环
for i in range(100):
    for j in range(len(y)):
        if(predict(x[j])*y[j]<=0):
            w += x[j].reshape(-1, 1)*y[j]*eta
            b += y[j]*eta
            break
    else:
        # 不存在误分类点,跳出迭代循环
        break
print("w =", w.tolist())
print("b =", b)

结果为:

w = [[1], [1]]
b = -3

将上述代码封装成一个原始感知机类:

class originPerceptron:
    def __init__(this):
        this.w = 0
        this.b = 0
    # 参数更新一次
    def trainOnce(this, x, y, eta):
        indexs = np.arange(len(x))
        # 为了避免总是拿靠前的数据进行测试,在此处打乱顺序
        np.random.shuffle(indexs)
        for i in indexs:
            if this.predict(x[i])*y[i] <= 0:
                this.w += (x[i]*y[i]).reshape(-1,1)*eta
                this.b += y[i]*eta
                return True
        else:
            # 没有误分点后,返回false,表示没有更新参数
            return False
    # x:训练的输入, y:训练的输出, eta:学习速率, times:最多迭代次数
    def train(this, x, y, eta, times):
        this.w = np.zeros(len(x[0])).reshape(-1,1)
        for i in range(times):
            if this.trainOnce(x, y, eta)==False:
                return
    # 获取参数
    def getWB(this):
        return this.w, this.b
    # 预测
    def predict(this, xi):
        return np.dot(xi, this.w)+this.b

# 设置训练集
x = np.array([[3,3], [4,3], [1,1]])
y = np.array([1, 1, -1])
# 使用原始感知机类
oP = originPerceptron()
oP.train(x, y, 1, 100)
w, b = oP.getWB()
print("w=", w.tolist())
print("b=", b)

由于使用了随机函数,故迭代次数和循环次数并不确定,可以为

w= [[1.0], [1.0]]
b= -3
或者是:
w= [[1.0], [0.0]]
b= -2
或者是:
w= [[2.0], [1.0]]
b= -5

这样,一个原始感知机模型就实现了。

感知机对偶形式

既然原始感知机已经能够实现对数据的二分类,为什么需要感知机的对偶形式 ?

我认为算法的设计主要用于解决两类问题:

  1. 没有现成算法可以解决的问题,新设计算法可以看成是 功能型算法 ,就是为了解决某个问题而设计;
  2. 存在解决某个问题的现成算法,但是现成算法的性能不能达到要求,新设计算法可以看成是 性能型算法

而感知机对偶形式就是解决感知机原始形式的性能问题;在原始感知机中,为了找到一个误分类点,我们需要使用for循环遍历,如果我们的样本数量非常大,性能会大大降低,特别是后期,误分类的样本点数量非常少之后,寻找这些误分类点时间会大大增加;

现在考虑,能不能使用矩阵运算一次性求出所有的误分类点,毕竟python的numpy库非常善于矩阵运算;

注:在数学领域中,对偶一般来说是以一对一的方式,把一种概念、公理、数学结构转化为另一种概念、公理、数据结构,如果A的对偶是B,那么B的对偶就是A

在学习率为1的原始感知机中,权重ω的第i个分量 ωi = (yi·xiT)*αi,其中αi为第i个样本点作为误分类点的次数。

例如,我们一共有n个样本点,共进行了N次参数修正,其中使用样本1进行修正α1次,使用样本2进行修正α2次,...,使用样本n进行修正αn次,可知α1 + α2 +...+ αn = N;

ω = α1y1·x1T + α2y2·x2T +...+ αnyn·xnT ,写成矩阵形式为:


因此,一次计算所有样本xω+b的值可以表示为:


上述公式中 * 表示Hadamard乘积,即两个同型矩阵(或向量)对应元素相乘,形状不变;

Y*XXT在训练前计算一次即可,故每次计算误分类点时的运算量并不大。之后,用sign函数作用于求得的列向量,与真实的标签比较即可获得误分类点,在实际代码中,我们直接用上述公式求得的列向量和标签向量做Hadamard乘积,并判断所得列向量中元素是否大于0得到分类是否正确。

注:XXT 称为Gram矩阵

代码如下:

x = np.array([[3,3], [4,3], [1,1]])
y = np.array([1, 1, -1]).reshape(-1, 1)

b = 0
yGram = y.T*np.dot(x, x.T)

alpha = np.zeros(len(x)).reshape(-1,1)
for i in range(100):
    v = np.dot(yGram, alpha)+b
    # 获取布尔数组,True为分类正确,False为分类错误
    check_right = ((v*y).flatten()>0)
    # 没有分类错误,算法结束
    if(np.all(check_right)):break
    # 获取第一个分类错误的点
    error_index = check_right.tolist().index(False)
    # 更新alpha、b
    alpha[error_index, 0] += 1
    b += label[error_index]
# 根据公式求w
w = np.dot(y.T*x.T, alpha)
print("w =", w.tolist())
print("b =", b)

输出为:

w = [[1.0], [1.0]]
b = -3

将对偶感知机也封装成一个类,代码如下:

import random
x = np.array([[3,3], [4,3], [1,1]])
y = np.array([1, 1, -1]).reshape(-1, 1)
class dualPerceptron:
    def __init__(this):
        this.b = 0
        this.w = 0
        
    def train(this, x, y, eta, times):
        alpha = np.zeros(len(x)).reshape(-1, 1)
        y = y.reshape(-1,1)
        yGram = np.dot(x, (x*y).T)
        for i in range(times):
            # 获取所有分类错误的索引
            err = np.arange(len(alpha))[
                ((np.dot(yGram, alpha)+this.b)*y<=0).flatten()]
            if(len(err)==0):
                this.w = np.dot((x*y).T,alpha)
                return
            # 随机选一个分类错误的样本i,其alpha[i]加一
            randerr = random.randint(0, len(err)-1)
            alpha[err[randerr]] += eta
            this.b += y.flatten()[err[randerr]]
        else:
            # 到达指定的迭代次数,更新权重
            this.w = np.dot((x*y).T,alpha)
    def predict(this, xi):
        v = np.dot(xi, this.w)+this.b
        if v>0 :return 1
        if v<0 :return -1
        if v==0 : return 0
    
    def getWB(this):
        return this.w, this.b
# 使用对偶感知机
dP = dualPerceptron()
dP.train(x, y, 1, 100)
w, b = dP.getWB()
print("w =", w.tolist())
print("b =", b)

结果可能为:

w = [[1.0], [0.0]]
b = -2

使用scikit-learn中的感知机进行训练

在scikit-learn库中,存在感知机模型,我们可以直接使用,还是训练上面那个例子:

from sklearn.linear_model import Perceptron
p = Perceptron(max_iter=100, shuffle=False)
p.fit([[3,3],[3,4],[1,1]],[1,1,-1])
print("w =", p.coef_)
print("b =", p.intercept_)

输出为:

w = [[ 1.  1.]]
b = [-3.]

与我们写的非随机的感知机数据结果是完全相同的;

训练数据量更多的数据集

前面的例子中,样本数据非常少,现在,我们使用scikit-learning创建更多数据量的数据集,并进行测试;

创建数据

import numpy as np
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
# 创建1000个样本的数据集
x, y = make_classification(n_samples=1000, n_features=2, 
        n_redundant=0, n_informative=1,
        n_clusters_per_class=1)
x0 = x[y==0]
x1 = x[y==1]

plt.scatter(x0[:,0], x0[:,1], c="m")
plt.scatter(x1[:,0], x1[:,1], c="r")
plt.show()

显示的散点图为:


该散点图应该不能用一条直线将不同类的点分开;各位显示的图应该和上图不一样,毕竟是随机产生的;

现在,我们用上面的3中方法进行测试;

使用scikit-learning中的感知机测试

from sklearn.linear_model import Perceptron
clf = Perceptron(fit_intercept=False, max_iter=300, shuffle=False)
clf.fit(x, y)
# 绘制分类情况
plt.scatter(x0[:,0], x0[:,1], c="m")
plt.scatter(x1[:,0], x1[:,1], c="r")
line_p1 = np.linspace(-3,3,1000)

line_p2 = (-clf.intercept_[0]-clf.coef_[0][0]*line_p1)/clf.coef_[0][1]
print(clf.score(x, y))
plt.plot(line_p1, line_p2)
plt.show()

通过感知机类的 score 方法可以计算正确率,打印出来的正确率为 0.92,还是挺不错,打印出的图为:

使用对偶感知机测试

为了能过定量地计算正确率,我们需要定义一个计算正确率的函数:

# perceptron 感知机实例,x输入,y标签
def calcAccurate(perceptron, x, y):
    sum_cnt = len(x)
    right_cnt = 0
    for xi,yi in zip(x, y):
        if perceptron.predict(xi)*yi>0:
            right_cnt += 1
    return right_cnt/sum_cnt

应用类的多态性,对偶感知机和原始感知机都可以用该函数测试正确率;

由于scikit-learning创建的样本标签为0或1,所以我们需要将0的标签改为-1,之后,定义并测试对偶感知机:

dP = dualPerceptron()
# 由于原始数据中,负例标记为0,故先修改标签
ty = y.copy()
ty[y==0]=-1
dP.train(x, ty, 1, 300)
w, b = dP.getWB()
# 绘制分类情况
plt.scatter(x0[:,0], x0[:,1], c="m")
plt.scatter(x1[:,0], x1[:,1], c="r")
line_p1 = np.linspace(-3,3,1000)

line_p2 = (-b-w[0]*line_p1)/w[1]
plt.plot(line_p1, line_p2)
print(calcAccurate(dP, x, ty))
plt.show()

打印出来的正确率为 0.95,比scikit-learning的还高一点点。当然,不能保证每次训练的正确率都高,不过基本上在0.9以上,打印出的图为:

使用原始感知机测试

oP = originPerceptron()
# 由于原始数据中,负例标记为0,故先修改标记
ty = y.copy()
ty[y==0]=-1
oP.train(x, ty, 1, 300)
w, b = oP.getWB()
# 绘制分类情况
plt.scatter(x0[:,0], x0[:,1], c="m")
plt.scatter(x1[:,0], x1[:,1], c="r")
line_p1 = np.linspace(-3,3,1000)

line_p2 = (-b-w[0]*line_p1)/w[1]
plt.plot(line_p1, line_p2)
print(calcAccurate(oP, x, ty))
plt.show()

打印出的正确率为 0.942,也是挺高的;绘制的图为:

其实,上面3个的测试代码长度都不多,非常简洁;

总结

感知机作为基本的训练模型,是很多模型的基础,值得花时间真正掌握。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容

  • 【概述】 SVM训练分类器的方法是寻找到超平面,使正负样本在超平面的两侧(分类正确性即“分得开”),且样本到超平面...
    sealaes阅读 10,558评论 0 7
  • 机器学习是做NLP和计算机视觉这类应用算法的基础,虽然现在深度学习模型大行其道,但是懂一些传统算法的原理和它们之间...
    在河之简阅读 20,397评论 4 65
  • 感知机 概述 感知机是二类分类的线性分类模型,其输入为实例的特征向量,输出为实例的类别,取+1和-1二值。感知机学...
    _Joe阅读 5,059评论 2 7
  • 【概述】 1、感知机模型特征:感知机对应于输入空间中将实例划分为正负两类的分离超平面,属于判别模型。 2、感知机策...
    sealaes阅读 3,043评论 2 3
  • 北京时间8:38,终于在发车的前一刻赶上了那一趟5026的硬座,今天月圆,车上人不多,选定靠窗座位,便如老翁入定一...
    乔小逸阅读 255评论 0 0