統計學習方法之感知機

《統計學習方法》系列筆記的第一篇,對應原著第二章。大量引用原著講解,加入了自己的理解。對書中算法採用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. # Filename: train2.1.py
  3. # Author:hankcs
  4. # Date: 2015/1/30 16:29
  5. import copy
  6. from matplotlib import pyplot as plt
  7. from matplotlib import animation
  8.  
  9. training_set = [[(3, 3), 1], [(4, 3), 1], [(1, 1), -1]]
  10. = [0, 0]
  11. = 0
  12. history = []
  13.  
  14.  
  15. def update(item):
  16.     """
  17.     update parameters using stochastic gradient descent
  18.     :param item: an item which is classified into wrong class
  19.     :return: nothing
  20.     """
  21.     global w, b, history
  22.     w[0] += 1 * item[1] * item[0][0]
  23.     w[1] += 1 * item[1] * item[0][1]
  24.     b += 1 * item[1]
  25.     print w, b
  26.     history.append([copy.copy(w), b])
  27.     # you can uncomment this line to check the process of stochastic gradient descent
  28.  
  29.  
  30. def cal(item):
  31.     """
  32.     calculate the functional distance between 'item' an the dicision surface. output yi(w*xi+b).
  33.     :param item:
  34.     :return:
  35.     """
  36.     res = 0
  37.     for i in range(len(item[0])):
  38.         res += item[0][i] * w[i]
  39.     res += b
  40.     res *= item[1]
  41.     return res
  42.  
  43.  
  44. def check():
  45.     """
  46.     check if the hyperplane can classify the examples correctly
  47.     :return: true if it can
  48.     """
  49.     flag = False
  50.     for item in training_set:
  51.         if cal(item) <= 0:
  52.             flag = True
  53.             update(item)
  54.     # draw a graph to show the process
  55.     if not flag:
  56.         print "RESULT: w: " + str(w) + " b: " + str(b)
  57.     return flag
  58.  
  59.  
  60. if __name__ == "__main__":
  61.     for i in range(1000):
  62.         if not check(): break
  63.  
  64.     # first set up the figure, the axis, and the plot element we want to animate
  65.     fig = plt.figure()
  66.     ax = plt.axes(xlim=(0, 2), ylim=(-2, 2))
  67.     line, = ax.plot([], [], 'g', lw=2)
  68.     label = ax.text([], [], '')
  69.  
  70.     # initialization function: plot the background of each frame
  71.     def init():
  72.         line.set_data([], [])
  73.         x, y, x_, y_ = [], [], [], []
  74.         for p in training_set:
  75.             if p[1] > 0:
  76.                 x.append(p[0][0])
  77.                 y.append(p[0][1])
  78.             else:
  79.                 x_.append(p[0][0])
  80.                 y_.append(p[0][1])
  81.  
  82.         plt.plot(x, y, 'bo', x_, y_, 'rx')
  83.         plt.axis([-6, 6, -6, 6])
  84.         plt.grid(True)
  85.         plt.xlabel('x')
  86.         plt.ylabel('y')
  87.         plt.title('Perceptron Algorithm (www.hankcs.com)')
  88.         return line, label
  89.  
  90.  
  91.     # animation function.  this is called sequentially
  92.     def animate(i):
  93.         global history, ax, line, label
  94.  
  95.         w = history[i][0]
  96.         b = history[i][1]
  97.         if w[1] == 0: return line, label
  98.         x1 = -7
  99.         y1 = -(+ w[0] * x1) / w[1]
  100.         x2 = 7
  101.         y2 = -(+ w[0] * x2) / w[1]
  102.         line.set_data([x1, x2], [y1, y2])
  103.         x1 = 0
  104.         y1 = -(+ w[0] * x1) / w[1]
  105.         label.set_text(history[i])
  106.         label.set_position([x1, y1])
  107.         return line, label
  108.  
  109.     # call the animator.  blit=true means only re-draw the parts that have changed.
  110.     print history
  111.     anim = animation.FuncAnimation(fig, animate, init_func=init, frames=len(history), interval=1000, repeat=True,
  112.                                    blit=True)
  113.     plt.show()
  114.     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. # Filename: train2.2.py
  3. # Author:hankcs
  4. # Date: 2015/1/31 15:15
  5. import numpy as np
  6. from matplotlib import pyplot as plt
  7. from matplotlib import animation
  8.  
  9. # An example in that book, the training set and parameters' sizes are fixed
  10. training_set = np.array([[[3, 3], 1], [[4, 3], 1], [[1, 1], -1]])
  11.  
  12. = np.zeros(len(training_set), np.float)
  13. = 0.0
  14. Gram = None
  15. = np.array(training_set[:, 1])
  16. = np.empty((len(training_set), 2), np.float)
  17. for i in range(len(training_set)):
  18.     x[i] = training_set[i][0]
  19. history = []
  20.  
  21. def cal_gram():
  22.     """
  23.     calculate the Gram matrix
  24.     :return:
  25.     """
  26.     g = np.empty((len(training_set), len(training_set)), np.int)
  27.     for i in range(len(training_set)):
  28.         for j in range(len(training_set)):
  29.             g[i][j] = np.dot(training_set[i][0], training_set[j][0])
  30.     return g
  31.  
  32.  
  33. def update(i):
  34.     """
  35.     update parameters using stochastic gradient descent
  36.     :param i:
  37.     :return:
  38.     """
  39.     global a, b
  40.     a[i] += 1
  41.     b = b + y[i]
  42.     history.append([np.dot(* y, x), b])
  43.     # print a, b # you can uncomment this line to check the process of stochastic gradient descent
  44.  
  45.  
  46. # calculate the judge condition
  47. def cal(i):
  48.     global a, b, x, y
  49.  
  50.     res = np.dot(* y, Gram[i])
  51.     res = (res + b) * y[i]
  52.     return res
  53.  
  54.  
  55. # check if the hyperplane can classify the examples correctly
  56. def check():
  57.     global a, b, x, y
  58.     flag = False
  59.     for i in range(len(training_set)):
  60.         if cal(i) <= 0:
  61.             flag = True
  62.             update(i)
  63.     if not flag:
  64.  
  65.         w = np.dot(* y, x)
  66.         print "RESULT: w: " + str(w) + " b: " + str(b)
  67.         return False
  68.     return True
  69.  
  70.  
  71. if __name__ == "__main__":
  72.     Gram = cal_gram()  # initialize the Gram matrix
  73.     for i in range(1000):
  74.         if not check(): break
  75.  
  76.     # draw an animation to show how it works, the data comes from history
  77.     # first set up the figure, the axis, and the plot element we want to animate
  78.     fig = plt.figure()
  79.     ax = plt.axes(xlim=(0, 2), ylim=(-2, 2))
  80.     line, = ax.plot([], [], 'g', lw=2)
  81.     label = ax.text([], [], '')
  82.  
  83.     # initialization function: plot the background of each frame
  84.     def init():
  85.         line.set_data([], [])
  86.         x, y, x_, y_ = [], [], [], []
  87.         for p in training_set:
  88.             if p[1] > 0:
  89.                 x.append(p[0][0])
  90.                 y.append(p[0][1])
  91.             else:
  92.                 x_.append(p[0][0])
  93.                 y_.append(p[0][1])
  94.  
  95.         plt.plot(x, y, 'bo', x_, y_, 'rx')
  96.         plt.axis([-6, 6, -6, 6])
  97.         plt.grid(True)
  98.         plt.xlabel('x')
  99.         plt.ylabel('y')
  100.         plt.title('Perceptron Algorithm 2 (www.hankcs.com)')
  101.         return line, label
  102.  
  103.  
  104.     # animation function.  this is called sequentially
  105.     def animate(i):
  106.         global history, ax, line, label
  107.  
  108.         w = history[i][0]
  109.         b = history[i][1]
  110.         if w[1] == 0: return line, label
  111.         x1 = -7.0
  112.         y1 = -(+ w[0] * x1) / w[1]
  113.         x2 = 7.0
  114.         y2 = -(+ w[0] * x2) / w[1]
  115.         line.set_data([x1, x2], [y1, y2])
  116.         x1 = 0.0
  117.         y1 = -(+ w[0] * x1) / w[1]
  118.         label.set_text(str(history[i][0]) + ' ' + str(b))
  119.         label.set_position([x1, y1])
  120.         return line, label
  121.  
  122.     # call the animator.  blit=true means only re-draw the parts that have changed.
  123.     anim = animation.FuncAnimation(fig, animate, init_func=init, frames=len(history), interval=1000, repeat=True,
  124.                                    blit=True)
  125.     plt.show()
  126.     # 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中的常用概念和常見流程。

另外本文只是個人筆記,服務於個人備忘用,對質量和後續不做保證。還是那句話,博客只做補充,要入門,還是得看經典著作。

轉載:http://www.hankcs.com/ml/the-perceptron.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章