ML-求解 SVM 的SMO 算法

这算是我真正意义上认真去读的第一篇ML论文了, but, 我还是很多地方没有搞懂, 想想, 缓缓吧, 还是先熟练调用API 哈哈

原论文地址: https://www.microsoft.com/en-us/research/publication/sequential-minimal-optimization-a-fast-algorithm-for-training-support-vector-machines/

求解w, b

这其实是一个在运筹学里面的经典二次规划 (Quadratic Programming)问题, 可以在多项式时间内求解.

坐标轮转法

Coordinate Descent

  • 每次搜索值允许一个变量变化, 其余不变(像求偏导数), 沿坐标轴轮流进行搜索寻求优化
  • 把多元变量的优化问题,轮转地转为了单个变量的优化问题, 感觉就是偏导数的思想
  • 搜索过程不需要对目标函数求导(不像梯度下降那样来找方向)

将多维的无约束优化问题,转为一系列的最优化问题. 每次搜索值允许一个变量变化(其余不变), 即沿着n个线性无关的方向(通常是坐标方向) 轮流进行搜索.

我感觉最好的对比是梯度下降法, 不清楚可以看我之前的搬运: (有一个小难点是理解向量的方向,及在数学及代码该如何表示)

梯度下降 Python 实现: https://www.cnblogs.com/chenjieyouge/p/11667959.html

每一轮的迭代公式:

(x_i ^k= x_{(i-1)}^k + alpha_i ^k d_i ^k)

  • 坐标方向: (d_i = e_i)

  • 轮次: k = 0,1,2....

  • 坐标: i = 1,2...

收敛条件:

(||x_{now} ^k - x_{before} ^k|| <= xi)

即每两次变换后, 收敛达到了 设定的误差, 这样就 break了.

SMO

回顾Daul SVM

(max_w W(a) = sum limits _{i=1}^n a_i - frac {1}{2} sum limits_{i=1}^n sum limits_{j=1}^n y_i y_j a_i a_j <x_i, x_j> \ s.t.)

(0<= a_i le C \ sum limits_{i=1}^n a_i y_i = 0)

其KKT条件为:

  • (a_i = 0, ightarrow y_i(w^Tx_i + b) ge 1)

  • (a_i = C, ightarrow y_i(w^Tx_i + b) le 1)

  • (0 le a_i le C, ightarrow y_i(w^Tx_i + b) ge 1)

认识SMO

在前面已经贴出paper了, 就还是有点不太懂, 道阻且跻. 革命尚未成功, 同志仍需努力.

其特点是, 一次必须两个元素 (a_i, a_j), 原因在于要满足约束 (sum limits _{i=1}^{n} a_i y_i = 0)

max_iter , count = n, 0

while True:

​ count += 1

​ 1. 选择下一步需优化的 (a_i, a_j) (选择 使得值 向 最大值发展快的方向, 的 (a_i, a_j)

​ 2. 固定其他 a 值, 仅仅通过改变 (a_i, a_j) 来优化(W(a))

​ IF 满足KKT条件收敛:

​ break

​ IF count >= max_iter :

​ break

SMO 算法过程

假设现在要优化 (a1 和a2), 根据约束条件 (sum limits _{i=1}^{n} a_i y_i = 0) , 可展开,移项得:

$a1y1 + a2y2 = 0 - sum limits _{i=3}^n a_i y_i $

就类似 已知 1+2+3+4 = 10 必然 1 + 2 = 10 - (3 + 4)

而等式右边必然是固定的, 可以用一个常数 (zeta) 读作(zeta) 来代替, 则:

(a1y1 + a2y2 = zeta)

则 a1 可以用 a2 的函数, 即:

(a1 = frac {(zeta - a2y2)} {y1} = (zeta -a2y2)y1)

因为 y 的值只能去 + - 1, 除和乘是一样的

原优化问题可转化为:

(W(a1, a2...an) = W((zeta - a2y2)y1, a2, a3....an))

将 a3, a4...an 固定, 看作常数, 于是优化函数可以写为:

(m a2^2 + n a2 + c) 的形式

如果忽略 a1, a2 的约束关系, 对该二次函数求导就可以得到最优解.

如果此时 a2' 是 a2 的解, 那么 a2 的新值为:

a1, a2 还受到 (a1y1 + a2y2 = zeta) 的约束, 则必然存在一个上下界 H, L

$a2^{new} = $

IF (a2^{new} ge H: ightarrow H)

IF (L le a2^{new} ge H: ightarrow a2^{new})

IF (a2^{new} le L : ightarrow L)

斯坦福smo: http://cs229.stanford.edu/materials/smo.pdf

Python 实现SMO

我目前的半吊子水平, 就大概明白, 还是在 github 搬运一波 jerry 大佬的代码., 也是简化版.

我只是做了详细的注释, 当然, 要真正理解下面的代码, 需要掌握SVM的关键点

  • 凸优化推导及KKT条件

  • margin 的推导

  • svm 目标函数推导

  • svm 的拉格朗日函数 形式 推导

  • svm 的对偶形式推导

  • 带松弛变量的 svm 推导

  • 核函数的 svm 推导

"""
    Author: Lasse Regin Nielsen
"""

from __future__ import division, print_function
import os
import numpy as np
import random as rnd
filepath = os.path.dirname(os.path.abspath(__file__))

class SVM():
    """
        Simple implementation of a Support Vector Machine using the
        Sequential Minimal Optimization (SMO) algorithm for training.
    """
    def __init__(self, max_iter=10000, kernel_type='linear', C=1.0, epsilon=0.001):
        self.kernels = {
            'linear' : self.kernel_linear,
            'quadratic' : self.kernel_quadratic
        }
        self.max_iter = max_iter
        self.kernel_type = kernel_type
        self.C = C
        self.epsilon = epsilon
    def fit(self, X, y):
        # Initialization
        n, d = X.shape[0], X.shape[1]
        alpha = np.zeros((n))
        kernel = self.kernels[self.kernel_type]
        count = 0
        while True:
            count += 1
            alpha_prev = np.copy(alpha)
            for j in range(0, n):
                i = self.get_rnd_int(0, n-1, j) # Get random int i~=j
                x_i, x_j, y_i, y_j = X[i,:], X[j,:], y[i], y[j]
                k_ij = kernel(x_i, x_i) + kernel(x_j, x_j) - 2 * kernel(x_i, x_j)
                if k_ij == 0:
                    continue
                alpha_prime_j, alpha_prime_i = alpha[j], alpha[i]
                (L, H) = self.compute_L_H(self.C, alpha_prime_j, alpha_prime_i, y_j, y_i)

                # Compute model parameters
                self.w = self.calc_w(alpha, y, X)
                self.b = self.calc_b(X, y, self.w)

                # Compute E_i, E_j
                E_i = self.E(x_i, y_i, self.w, self.b)
                E_j = self.E(x_j, y_j, self.w, self.b)

                # Set new alpha values
                alpha[j] = alpha_prime_j + float(y_j * (E_i - E_j))/k_ij
                alpha[j] = max(alpha[j], L)
                alpha[j] = min(alpha[j], H)

                alpha[i] = alpha_prime_i + y_i*y_j * (alpha_prime_j - alpha[j])

            # Check convergence
            diff = np.linalg.norm(alpha - alpha_prev)
            if diff < self.epsilon:
                break

            if count >= self.max_iter:
                print("Iteration number exceeded the max of %d iterations" % (self.max_iter))
                return
        # Compute final model parameters
        self.b = self.calc_b(X, y, self.w)
        if self.kernel_type == 'linear':
            self.w = self.calc_w(alpha, y, X)
        # Get support vectors
        alpha_idx = np.where(alpha > 0)[0]
        support_vectors = X[alpha_idx, :]
        return support_vectors, count
    def predict(self, X):
        return self.h(X, self.w, self.b)
    def calc_b(self, X, y, w):
        b_tmp = y - np.dot(w.T, X.T)
        return np.mean(b_tmp)
    def calc_w(self, alpha, y, X):
        return np.dot(X.T, np.multiply(alpha,y))
    # Prediction
    def h(self, X, w, b):
        return np.sign(np.dot(w.T, X.T) + b).astype(int)
    # Prediction error
    def E(self, x_k, y_k, w, b):
        return self.h(x_k, w, b) - y_k
    def compute_L_H(self, C, alpha_prime_j, alpha_prime_i, y_j, y_i):
        if(y_i != y_j):
            return (max(0, alpha_prime_j - alpha_prime_i), min(C, C - alpha_prime_i + alpha_prime_j))
        else:
            return (max(0, alpha_prime_i + alpha_prime_j - C), min(C, alpha_prime_i + alpha_prime_j))
    def get_rnd_int(self, a,b,z):
        i = z
        cnt=0
        while i == z and cnt<1000:
            i = rnd.randint(a,b)
            cnt=cnt+1
        return i
    # Define kernels
    def kernel_linear(self, x1, x2):
        return np.dot(x1, x2.T)
    def kernel_quadratic(self, x1, x2):
        return (np.dot(x1, x2.T) ** 2)

详细注释版

我这里只是做了一个可能无关痛痒的注释, 然后从我平时写代码的习惯顺序, 重写排了下

"""
    Author: Lasse Regin Nielsen
"""
import numpy as np
import random as rnd

class SVM():
    """
        Simple implementation of a Support Vector Machine using the
        Sequential Minimal Optimization (SMO) algorithm for training.
    """

    def __init__(self, max_iter=10000, kernel_type='linear', C=1.0, epsilon=0.001):
        self.kernels = {
            'linear': self.kernel_linear,
            'quadratic': self.kernel_quadratic
        }
        # 最大迭代次数,比如1000次, 在之内没有解也程序结束
        self.max_iter = max_iter
        # 核函数: 自定义了线性核, 二次项核 两种可选其一
        self.kernel_type = kernel_type
        # C 表示 alpha 的上界, 受KKT中, 偏导为0的约束
        self.C = C
        # 误差精度(判断收敛的)
        self.epsilon = epsilon

    # 定义两个核函数, 多个也行,随意
    def kernel_linear(self, x1, x2):
        """线性核(无核)"""
        return np.dot(x1, x2.T)

    def kernel_quadratic(self, x1, x2):
        """多项式核(2次方)"""
        return (np.dot(x1, x2.T) ** 2)

    def get_rnd_int(self, a, b, z):
        """随处初始化 [a,b]内的整数,作为下标"""
        i = z
        cnt = 0
        while i == z and cnt < 1000:
            i = rnd.randint(a, b)
            cnt = cnt + 1
        return i

    def compute_L_H(self, C, alpha_prime_j, alpha_prime_i, y_j, y_i):
        """计算函数的上下界,之前的 zeta 约束"""
        if (y_i != y_j):
            return (max(0, alpha_prime_j - alpha_prime_i), min(C, C - alpha_prime_i + alpha_prime_j))
        else:
            return (max(0, alpha_prime_i + alpha_prime_j - C), min(C, alpha_prime_i + alpha_prime_j))

    def calc_w(self, alpha, y, X):
        """计算w的值"""
        return np.dot(X.T, np.multiply(alpha, y))

    def calc_b(self, X, y, w):
        """计算偏置量b"""
        b_tmp = y - np.dot(w.T, X.T)
        return np.mean(b_tmp)

    # Prediction
    def h(self, X, w, b):
        """根据值的正负号来分类"""
        # y = np.sign(*args)
        # -1 if y < 0, 0 if y == 0
        # 1 if y > 0
        return np.sign(np.dot(w.T, X.T) + b).astype(int)

    def predict(self, X):
        """输入x,对其做预测是 1 or -1"""
        return self.h(X, self.w, self.b)

    # Prediction error
    def E(self, x_k, y_k, w, b):
        """计算误差"""
        return self.h(x_k, w, b) - y_k

    def fit(self, X, y):
        # Initialization
        # n表示样本的个数(行), d表示每个样本的维度(列)
        n, d = X.shape[0], X.shape[1]
        # 初始化 alpha为 [0,0,0...0], 每个分量对一个样本(1行数据)
        alpha = np.zeros((n))
        # 选择定义的核函数, kernels是字典, key是名称, value是自定义的方法名
        kernel = self.kernels[self.kernel_type]
        # 用监测控制最外层,最大迭代次数的, 每迭代一次,则 计数+1
        count = 0
        while True:
            count += 1
            # 对alpha 变化前的值,进行深拷贝
            alpha_prev = np.copy(alpha)
            for j in range(0, n):
                # 随机选择[0, n-1]的整数作为下标
                i = self.get_rnd_int(0, n - 1, j)
                # 选中优化的2个样本xi, xj, yi, yj
                x_i, x_j, y_i, y_j = X[i, :], X[j, :], y[i], y[j]
                # 计算该样本在核函数映射(R^n->n 下的实数值
                k_ij = kernel(x_i, x_i) + kernel(x_j, x_j) - 2 * kernel(x_i, x_j)
                # dual是要求max,等于0的就跳过,不用优化了呀
                if k_ij == 0:
                    continue

                alpha_prime_j, alpha_prime_i = alpha[j], alpha[i]
                # 计算出当前 a_i 的上下界
                (L, H) = self.compute_L_H(self.C, alpha_prime_j, alpha_prime_i, y_j, y_i)

                # Compute model parameters
                self.w = self.calc_w(alpha, y, X)
                self.b = self.calc_b(X, y, self.w)

                # Compute E_i, E_j
                E_i = self.E(x_i, y_i, self.w, self.b)
                E_j = self.E(x_j, y_j, self.w, self.b)

                # Set new alpha values
                alpha[j] = alpha_prime_j + float(y_j * (E_i - E_j)) / k_ij
                alpha[j] = max(alpha[j], L)
                alpha[j] = min(alpha[j], H)

                alpha[i] = alpha_prime_i + y_i * y_j * (alpha_prime_j - alpha[j])

            # 计算向量alpha新前后的范数,得到实数值,并判断是否收敛
            diff = np.linalg.norm(alpha - alpha_prev)
            if diff < self.epsilon:
                break

            if count >= self.max_iter:
                print("Iteration number exceeded the max of %d iterations" % (self.max_iter))
                return
        # Compute final model parameters
        self.b = self.calc_b(X, y, self.w)
        if self.kernel_type == 'linear':
            self.w = self.calc_w(alpha, y, X)
        # Get support vectors
        alpha_idx = np.where(alpha > 0)[0]
        support_vectors = X[alpha_idx, :]
        return support_vectors, count

小结

关于写篇还是很有难度的, 涉及到2篇论文和前面的smv推导是想联系的. 此篇目的除了巩固svm之外, 更多是想回归到代码层面, 毕竟能写出代码,才是我的本职, 与我而言, 理论推导的目的为了成功更加自信的调参侠, 当发现调参搞不定业务需求的时候, 那我就自己写一个, 也不是很难, 毕竟,如果数学原理都整明白了, 代码还很难嘛, 我不相信

其次, 关于代码,正如 jerry 大佬说认为, 跟咱平时写作文是一样的. 一开始都是认识单词, 然后能组句子, 然后能写一个段落, 然后抄满分作文, 增删改一下, 然后开始去大量阅读, 慢慢地才有了自己的思路和想法, 才著文章. 抄和前人的认知,我觉得是最为重要的一点.

钱钟书先生曾认为, "牢骚之人善著文章". 包含两个关键点: 一是阅读量, 一个是"牢骚", 即不安于现状, 善于发现不合理和提出自己的观点. 这岂不就是我写代码的真谛? 有空就多抄写和模仿大佬的代码, 然后有想法,自己去实现, "改变世界"?, 不存在的, 改变自我认识的同时, 顺带解决一些工作问题, 嗯....coding 其乐无穷, 我目标是能做当将其当做作文来写哦

还是重复一些, smo吧, 我也就掌握了70%左右, 代码自己整还是有点难度, 但要理解代码, 嗯, 如下的理论要求,真的是必须的:

  • 凸优化推导及KKT条件

  • margin 的推导

  • svm 目标函数推导

  • svm 的拉格朗日函数 形式 推导

  • svm 的对偶形式推导

  • 带松弛变量的 svm 推导

  • 核函数的 svm 推导

原文地址:https://www.cnblogs.com/chenjieyouge/p/11952884.html