统计学习方法---感知机

来源:互联网 发布:大连嘉友软件 编辑:程序博客网 时间:2024/05/16 12:17

《统计学习方法》系列笔记的第一篇,对应原著第二章。大量引用原著讲解,加入了自己的理解。对书中算法采用Python实现,并用Matplotlib可视化了动画出来。

概念

感知机是二分类模型,输入实例的特征向量,输出实例的±类别

感知机模型

定义

假设输入空间是,输出空间是,x和y分属这两个空间,那么由输入空间到输出空间的如下函数:

称为感知机。其中,w和b称为感知机模型参数,叫做权值或权值向量叫做偏置,w·x表示向量w和x的内积。sign是一个函数:

感知机的几何解释是,线性方程

将特征空间划分为正负两个部分:

这个平面(2维时退化为直线)称为分离超平面。

感知机学习策略

数据集的线性可分性

定义

给定数据集

其中如果存在某个超平面S

能够完全正确地将正负实例点全部分割开来,则称T线性可分,否则称T线性不可分

感知机学习策略

假定数据集线性可分,我们希望找到一个合理的损失函数

一个朴素的想法是采用误分类点的总数,但是这样的损失函数不是参数w,b的连续可导函数,不可导自然不能把握函数的变化,也就不易优化(不知道什么时候该终止训练,或终止的时机不是最优的)。

另一个想法是选择所有误分类点到超平面S的总距离。为此,先定义点x0到平面S的距离:

分母是w的L2范数,所谓L2范数,指的是向量各元素的平方和然后求平方根(长度)。这个式子很好理解,回忆中学学过的点到平面的距离:

此处的点到超平面S的距离的几何意义就是上述距离在多维空间的推广。

又因为,如果点i被误分类,一定有

成立,所以我们去掉了绝对值符号,得到误分类点到超平面S的距离公式:

假设所有误分类点构成集合M,那么所有误分类点到超平面S的总距离为

分母作用不大,反正一定是正的,不考虑分母,就得到了感知机学习的损失函数:

感知机学习算法

原始形式

感知机学习算法是对以下最优化问题的算法:

感知机学习算法是误分类驱动的,先随机选取一个超平面,然后用梯度下降法不断极小化上述损失函数。损失函数的梯度由:

给出。所谓梯度,是一个向量,指向的是标量场增长最快的方向,长度是最大变化率。所谓标量场,指的是空间中任意一个点的属性都可以用一个标量表示的场(个人理解该标量为函数的输出)。

随机选一个误分类点i,对参数w,b进行更新:

上式是学习率。损失函数的参数加上梯度上升的反方向,于是就梯度下降了。所以,上述迭代可以使损失函数不断减小,直到为0。于是得到了原始形式的感知机学习算法:

对于此算法,使用下面的例子作为测试数据: 

       例题


         

                 这里写图片描述


      这里写图片描述

给出Python实现和可视化代码如下:

感知机算法代码

终于到了最激动人心的时刻了,有了上述知识,就可以完美地可视化这个简单的算法:

  1. # -*- coding:utf-8 -*-

  2. import copy
  3. from matplotlib import pyplot as plt
  4. from matplotlib import animation
  5.  
  6. training_set = [[(3, 3), 1], [(4, 3), 1], [(1, 1), -1]]
  7. w = [0, 0]
  8. b = 0
  9. history = []
  10.  
  11.  
  12. def update(item):
  13.     """
  14.     update parameters using stochastic gradient descent
  15.     :param item: an item which is classified into wrong class
  16.     :return: nothing
  17.     """
  18.     global w, b, history
  19.     w[0] += 1 * item[1] * item[0][0]
  20.     w[1] += 1 * item[1] * item[0][1]
  21.     b += 1 * item[1]
  22.     print w, b
  23.     history.append([copy.copy(w), b])
  24.     # you can uncomment this line to check the process of stochastic gradient descent
  25.  
  26.  
  27. def cal(item):
  28.     """
  29.     calculate the functional distance between 'item' an the dicision surface. output yi(w*xi+b).
  30.     :param item:
  31.     :return:
  32.     """
  33.     res = 0
  34.     for i in range(len(item[0])):
  35.         res += item[0][i] * w[i]
  36.     res += b
  37.     res *= item[1]
  38.     return res
  39.  
  40.  
  41. def check():
  42.     """
  43.     check if the hyperplane can classify the examples correctly
  44.     :return: true if it can
  45.     """
  46.     flag = False
  47.     for item in training_set:
  48.         if cal(item) <= 0:
  49.             flag = True
  50.             update(item)
  51.     # draw a graph to show the process
  52.     if not flag:
  53.         print "RESULT: w: " + str(w) + " b: " + str(b)
  54.     return flag
  55.  
  56.  
  57. if __name__ == "__main__":
  58.     for i in range(1000):
  59.         if not check(): break
  60.  
  61.     # first set up the figure, the axis, and the plot element we want to animate
  62.     fig = plt.figure()
  63.     ax = plt.axes(xlim=(0, 2), ylim=(-2, 2))
  64.     line, = ax.plot([], [], 'g', lw=2)
  65.     label = ax.text([], [], '')
  66.  
  67.     # initialization function: plot the background of each frame
  68.     def init():
  69.         line.set_data([], [])
  70.         x, y, x_, y_ = [], [], [], []
  71.         for p in training_set:
  72.             if p[1] > 0:
  73.                 x.append(p[0][0])
  74.                 y.append(p[0][1])
  75.             else:
  76.                 x_.append(p[0][0])
  77.                 y_.append(p[0][1])
  78.  
  79.         plt.plot(x, y, 'bo', x_, y_, 'rx')
  80.         plt.axis([-6, 6, -6, 6])
  81.         plt.grid(True)
  82.         plt.xlabel('x')
  83.         plt.ylabel('y')
  84.         plt.title('Perceptron Algorithm ()')
  85.         return line, label
  86.  
  87.  
  88.     # animation function.  this is called sequentially
  89.     def animate(i):
  90.         global history, ax, line, label
  91.  
  92.         w = history[i][0]
  93.         b = history[i][1]
  94.         if w[1] == 0: return line, label
  95.         x1 = -7
  96.         y1 = -(b + w[0] * x1) / w[1]
  97.         x2 = 7
  98.         y2 = -(b + w[0] * x2) / w[1]
  99.         line.set_data([x1, x2], [y1, y2])
  100.         x1 = 0
  101.         y1 = -(b + w[0] * x1) / w[1]
  102.         label.set_text(history[i])
  103.         label.set_position([x1, y1])
  104.         return line, label
  105.  
  106.     # call the animator.  blit=true means only re-draw the parts that have changed.
  107.     print history
  108.     anim = animation.FuncAnimation(fig, animate, init_func=init, frames=len(history), interval=1000, repeat=True,
  109.                                    blit=True)
  110.     plt.show()
  111.     anim.save('perceptron.gif', fps=2, writer='imagemagick')


可视化

可见超平面被误分类点所吸引,朝着它移动,使得两者距离逐步减小,直到正确分类为止。通过这个动画,是不是对感知机的梯度下降算法有了更直观的感悟呢?

算法的收敛性

记输入向量加进常数1的拓充形式,其最大长度为,记感知机的参数向量,设满足条件的超平面可以将数据集完全正确地分类,定义最小值伽马:

则误分类次数k满足:

证明请参考《统计学习方法》P31。

感知机学习算法的对偶形式

对偶指的是,将w和b表示为测试数据i的线性组合形式,通过求解系数得到w和b。具体说来,如果对误分类点i逐步修改wb修改了n次,则w,b关于i的增量分别为,这里,则最终求解到的参数分别表示为:

于是有算法2.2:

感知机对偶算法代码

涉及到比较多的矩阵计算,于是用NumPy比较多:

  1. # -*- coding:utf-8 -*-

  2. import numpy as np
  3. from matplotlib import pyplot as plt
  4. from matplotlib import animation
  5.  
  6. # An example in that book, the training set and parameters' sizes are fixed
  7. training_set = np.array([[[3, 3], 1], [[4, 3], 1], [[1, 1], -1]])
  8.  
  9. a = np.zeros(len(training_set), np.float)
  10. b = 0.0
  11. Gram = None
  12. y = np.array(training_set[:, 1])
  13. x = np.empty((len(training_set), 2), np.float)
  14. for i in range(len(training_set)):
  15.     x[i] = training_set[i][0]
  16. history = []
  17.  
  18. def cal_gram():
  19.     """
  20.     calculate the Gram matrix
  21.     :return:
  22.     """
  23.     g = np.empty((len(training_set), len(training_set)), np.int)
  24.     for i in range(len(training_set)):
  25.         for j in range(len(training_set)):
  26.             g[i][j] = np.dot(training_set[i][0], training_set[j][0])
  27.     return g
  28.  
  29.  
  30. def update(i):
  31.     """
  32.     update parameters using stochastic gradient descent
  33.     :param i:
  34.     :return:
  35.     """
  36.     global a, b
  37.     a[i] += 1
  38.     b = b + y[i]
  39.     history.append([np.dot(a * y, x), b])
  40.     # print a, b # you can uncomment this line to check the process of stochastic gradient descent
  41.  
  42.  
  43. # calculate the judge condition
  44. def cal(i):
  45.     global a, b, x, y
  46.  
  47.     res = np.dot(a * y, Gram[i])
  48.     res = (res + b) * y[i]
  49.     return res
  50.  
  51.  
  52. # check if the hyperplane can classify the examples correctly
  53. def check():
  54.     global a, b, x, y
  55.     flag = False
  56.     for i in range(len(training_set)):
  57.         if cal(i) <= 0:
  58.             flag = True
  59.             update(i)
  60.     if not flag:
  61.  
  62.         w = np.dot(a * y, x)
  63.         print "RESULT: w: " + str(w) + " b: " + str(b)
  64.         return False
  65.     return True
  66.  
  67.  
  68. if __name__ == "__main__":
  69.     Gram = cal_gram()  # initialize the Gram matrix
  70.     for i in range(1000):
  71.         if not check(): break
  72.  
  73.     # draw an animation to show how it works, the data comes from history
  74.     # first set up the figure, the axis, and the plot element we want to animate
  75.     fig = plt.figure()
  76.     ax = plt.axes(xlim=(0, 2), ylim=(-2, 2))
  77.     line, = ax.plot([], [], 'g', lw=2)
  78.     label = ax.text([], [], '')
  79.  
  80.     # initialization function: plot the background of each frame
  81.     def init():
  82.         line.set_data([], [])
  83.         x, y, x_, y_ = [], [], [], []
  84.         for p in training_set:
  85.             if p[1] > 0:
  86.                 x.append(p[0][0])
  87.                 y.append(p[0][1])
  88.             else:
  89.                 x_.append(p[0][0])
  90.                 y_.append(p[0][1])
  91.  
  92.         plt.plot(x, y, 'bo', x_, y_, 'rx')
  93.         plt.axis([-6, 6, -6, 6])
  94.         plt.grid(True)
  95.         plt.xlabel('x')
  96.         plt.ylabel('y')
  97.         plt.title('Perceptron Algorithm 2 (www.hankcs.com)')
  98.         return line, label
  99.  
  100.  
  101.     # animation function.  this is called sequentially
  102.     def animate(i):
  103.         global history, ax, line, label
  104.  
  105.         w = history[i][0]
  106.         b = history[i][1]
  107.         if w[1] == 0: return line, label
  108.         x1 = -7.0
  109.         y1 = -(b + w[0] * x1) / w[1]
  110.         x2 = 7.0
  111.         y2 = -(b + w[0] * x2) / w[1]
  112.         line.set_data([x1, x2], [y1, y2])
  113.         x1 = 0.0
  114.         y1 = -(b + w[0] * x1) / w[1]
  115.         label.set_text(str(history[i][0]) + ' ' + str(b))
  116.         label.set_position([x1, y1])
  117.         return line, label
  118.  
  119.     # call the animator.  blit=true means only re-draw the parts that have changed.
  120.     anim = animation.FuncAnimation(fig, animate, init_func=init, frames=len(history), interval=1000, repeat=True,
  121.                                    blit=True)
  122.     plt.show()
  123.     # anim.save('perceptron2.gif', fps=2, writer='imagemagick')



可视化

与算法1的结果相同,我们也可以将数据集改一下:

  1. training_set = np.array([[[3, 3], 1], [[4, 3], 1], [[1, 1], -1], [[5, 2], -1]])

会得到一个复杂一些的结果:

读后感

通过最简单的模型,学习到ML中的常用概念和常见流程。

另外本文只是个人笔记,服务于个人备忘用,对质量和后续不做保证。还是那句话,博客只做补充,要入门,还是得看经典著作。

Reference

文中部分代码参考了OldPanda的实现。