01 先导知识索引
此前,就周志华老师的《机器学习》,俗称西瓜书,进行音频讲解。
现在贴出代码,主要参考知乎文章《
机器学习(周志华)课后习题——第六章——支持向量机 》,笔者添加了注释和更改了一些细节。尚未完全调通。
另外,欢迎读者参考《
周志华西瓜书:第六章 支持向量机 音频 》。
面向过程编程
02 《统计学方法》SMO算法
关于如何搜索合适的拉格朗日乘子 alpha(i)
1. 第一层搜索。计算每个 alpha(i),分别令 alpha(i) = 0 OR C OR 介于 (0,C)之间的值。
2. 检测上述 alpha(i) 取值是否满足 KKT 条件。找出违背 KKT 条件最严重的,通常是距离最远的。
3. 选定第一层的 alpha(i) 之后,开始进入内循环,选择 alpha(j) ,选择标准是,使得 alpha(j) 有最大的变化。
注意
(1) alpha_new(i)是依赖于 abs(E1 - E2),其中 E1 和 E2 的计算读者可以见代码所示。
以下算法迭代的是,alpha_new(i), alpha_new(j), E1_new, E2_new, b_1_new, b_2_new。
(2) 算法在编写的时候,不是返回迭代的数值,而是存在一个向量 cache 中,搜索的时候,检查向量是否被更新(如果某个维度上不满足条件,是不会更新的)。如果函数返回,未形成迭代,则函数返回边界点。
SVM 凸二次规划对偶问题,展开之后问题表述
SVM 二维案例
SVM 凸二次规划对偶问题 with Kernal Distance
SMO Algorithm Intuitive Understanding
03 代码解读
点击阅读
源码 。
# -- 算法类:主要定义参数如何生成,如何拟合,如何预测,如何返回
class SVM_2023(object):
  def __init__(self, C=1.0, kernel='rbf', degree=3, gamma='auto', tol=1e-3, max_iter=-1, seed=16, tag_test=False):
    1.定义 alpha 上限、 核函数类型
    2.核函数、多项式阶数、校准系数 gamma、边界、最大迭代次数、抽样种子
    3.alpha 向量、非零向量、caching 向量、非边界向量、截距
    4.支持向量系数、支持向量、Y、alpha 向量
  def pred_model(self, X):
    m=X.shape[0]
    output=np.ones((m,))
    for i in range(m):
      逐个计算基于 Kernel 的距离,调用 generate_kdist()
      计算预测值 w.T * x + b
    return 返回校准之后的output
  def fit_model(self, X, y):
    if self.gamma == 'auto':
      self._gamma=1.0/X.shape[1]     # 没太理解
    else:
      self._gamma=self.gamma
    初始化 alpha, b 均为 0向量和0
    非 0 alpha 初始化,BOOL
    非边界点初始化,均为 0
    cache 初始化,均为 0
    self.SMO_2023(X, y) # 这个函数直接在 object 上修改
    对支持向量(边界点)的 alpha, X, y, 进行赋值
    return self
  def SMO_2023(self, X, y):
    while ( 满足条件 ):
      alpha_paris_changed=0
      if 标志位==1:
      for i in range(m):
        逐个预测响应变量 0/1 并累加
      else:
        非边界点的 Index 赋值
      for i in non_bound_index:
        逐个预测响应变量 0/1 并累加
      计数器++
    if 标志位==1:
      if 首轮未更新: # 首轮,未更新
        return self
      标志位==0:# 首轮,更新了,产生平面两边的点
    elif 非首轮无法更新:
      标志位==1
    return self
  def _inner_predict_one(self, X, y, i):
    # output 0/1
    if 存在非边界点:
      Ei = self._e_cache[i] # GET value from last round / caching
    else:
      Ei = 计算误差
    if ( alpha 属于 [0, C] ): # tol 为边界
      best_j=None
      if 存在非边界点:
        # case-1: 存在非边界点,第二轮搜索(找出 Error 变化最大的点)
        # After 2nd search 使用 Supposed Most Important Vector to Update 则结束
        j, Ej = 再选一次点并返回 Error
        if 更新 alpha 成功:
          return 1
        # case-2: After 2nd search 未更新成功
        best_j=j
        # 非边界点向量随机排序,找寻首个非上述最优点进行更新
        # 非边界点集合有 Cache,边界点则计算 Error
      non_bound_index = 获取非边界点 index
      for j in np.random.permutation(non_bound_index):
        非边界点中随机抽取,第一个非上述点,则更新 alpha
      for j in np.random.permutation(m):
        全部点中随机抽取,第一个非上述点,更新 alpha
    return 0
  def _update_alpha(self, X, y, i, j, Ei, Ej=None):
    # --- 该函数更新迭代公式,取自于《统计学习方法》
    # --- Jingyi 学习程度是,找到 Search 最优的方式,跳掉证明和推导部分
    if i==j:
      return 0
    获取 alpha(i), alpha(j), y(i), y(j)
    # 上下限 P126 左下角两个式子
    if yi != yj:
      L=max(0, alpha_j_old - alpha_i_old)
      H=min(self.C, self.C + alpha_j_old - alpha_i_old)
    else:
      L=max(0, alpha_j_old + alpha_i_old - self.C)
      H=min(self.C, alpha_j_old + alpha_i_old)
    if L==H:
      return 0
    kii=self.kernel_ij(X, i, i)
    kjj=self.kernel_ij(X, j, j)
    kij=self.kernel_ij(X, i, j)
    eta=kii+kjj-2*kij
    # 《统计学方法》 P127 式7.107 / 定理7.6 提出了 alpha 如何迭代
    if eta<=0:
      return 0 # 保证 alpha 是增长的
    alpha_j_new=alpha_j_old+yj*(Ei-Ej)/eta # 李航的书, 式 7.106
    # new alpha cap and floor
    if alpha_j_new > H:
      alpha_j_new = H
    elif alpha_j_new < L:
      alpha_j_new = L
    if abs(alpha_j_new-alpha_j_old) < 0.00001:
      return 0
    alpha_i_new=alpha_i_old+yi*yj*(alpha_j_old-alpha_j_new) # 式 7.109
    bj=self.b-Ej-yj*kjj*(alpha_j_new-alpha_j_old)-yi*kij*(alpha_i_new-alpha_i_old) # 式 7.115
    bi=self.b-Ei-yi*kij*(alpha_j_new-alpha_j_old)-yi*kii*(alpha_i_new-alpha_i_old) # 式 7.116
    if 0 < alpha_j_new < self.C:
      self.b=bj
    elif 0 < alpha_i_new < self.C:
      self.b=bi
    else:
      self.b=(bi+bj)/2
    更新至 BOOL 向量以及 alpha 向量
    更新 Cache
    return 1
  def _update_alpha_adjoint_arr(self, alpha_new, i):
    获取非零向量、非边界点的 index
  def _update_e_cache(self, X, y):
    for i in self._non_bound_alpha.nonzero()[0]:
      存取非边界点的 Error
  def _select_second_alpha(self, Ei):
    # 找出所有非边界点产生的 Error 变化 / Caching
    获取 index
    更新 Error,Error(i-1) - Error in Cache
    # 找出变化最大的点
    找出变化最大的点
    Cache 中的 Error 更新对应的 Error
    return j, Ej
  def calculate_error(self, X, y, i):
    xi 行向量
    yi=y[i,0]
    if 如果存在非零样本: # True or False
      取出对应样本
      X_vec_2=self.generate_kdist(valid_X, xi) # Generate K-dist via non_zero on 2 samples
      tmp_out=逐个计算输出值 0/1
      # X_vec_2 列向量 输出value / w.T*x + b
    else:
      tmp_out=self.b
    Ei=tmp_out-yi # 逐个样本计算 Error
    return np.squeeze(Ei)
  def generate_kdist(self, X, xi):
    # --- k_X 为列向量
    if self.kernel=='linear':
      k_X_mat=np.dot(X, xi.T)
    elif self.kernel =='poly':
      k_X_mat=np.power(np.dot(X, xi.T), self.degree)
    else:
      k_X_mat=np.exp(-self._gamma*np.sum(np.power(X-xi, 2), axis=1))
    return k_X_mat
  def kernel_ij(self, X, i, j):
    xi=X[[i],:]
    xj=X[[j],:]
    if self.kernel=='linear':
      k_X_mat=np.dot(xj, xi.T)
    elif self.kernel=='poly':
      k_X_mat=np.power(np.dot(xj, xi.T), self.degree)
    else:
      k_X_mat=np.exp(-self._gamma * np.sum(np.power(xj-xi, 2), axis=1))
    return np.squeeze(k_X_mat)