• 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 推导

  • 相关阅读:
    JAVA HDFS API Client 连接HA
    jdo pom
    iOS/OC 笛卡尔积算法 递归求笛卡尔积 求N个数组中元素任意组合
    Tableview如何实现流畅的展开折叠?
    iOS证书那些事儿
    iOS 状态栏颜色设置
    iOS 可以正常跳转WEB支付宝 无法跳转支付宝APP
    iOS代码管理工具的使用
    *** Assertion failure in -[UICollectionView _createPreparedSupplementaryViewForElementOfKind:atIndexPath:withLayoutAttributes:applyAttributes:]1555错误
    iOS UISearchBar 设置光标颜色和取消按钮颜色
  • 原文地址:https://www.cnblogs.com/chenjieyouge/p/11952884.html
Copyright © 2020-2023  润新知