8. 支持向量机SVM原理以及代码实现

#! https://zhuanlan.zhihu.com/p/611475806

支持向量机原理以及代码实现

完整的实验代码在我的github上QYHcrossover/ML-numpy: 机器学习算法numpy实现 (github.com) 欢迎star⭐

支持向量机(SVM)是一种常用的二分类模型,其通过在数据集中找到一个超平面(或分割线)来分割两个不同的分类,同时使得该超平面距离两个分类的最近数据点最大化,从而实现了对未知数据进行分类的目的。

SVM原理

支持向量机(Support Vector Machine,SVM)是一种常见的二分类模型。SVM的目标是在数据集中找到一个超平面(或分割线)来分割两个不同的分类,同时使得该超平面距离两个分类的最近数据点最大化,从而实现了对未知数据进行分类的目的。

假设我们有一个二分类问题,数据集为 { ( x i , y i ) } i = 1 m \{(x_i,y_i)\}_{i=1}^m {(xi,yi)}i=1m,其中 x i x_i xi为数据点, y i ∈ { − 1 , 1 } y_i\in\{-1,1\} yi{1,1}为该数据点所属的类别。假设我们的数据集是线性可分的,也就是说,可以找到一个超平面 ω T x + b = 0 \omega^Tx+b=0 ωTx+b=0来将两个类别分开。此时,我们的模型可以表示为:

f ( x ) = sign ( ω T x + b ) f(x) = \text{sign}(\omega^Tx+b) f(x)=sign(ωTx+b)

其中, sign \text{sign} sign为符号函数,即:

sign ( x ) = { 1 , x > 0 − 1 , x < 0 0 , x = 0 \text{sign}(x) = \begin{cases} 1, & x>0 \\ -1, & x<0 \\ 0, & x=0 \end{cases} sign(x)= 1,1,0,x>0x<0x=0

但是,很多时候,我们的数据集并不是线性可分的,也就是说,不存在一个超平面可以将两个类别完全分开。此时,我们需要使用一些技巧,将数据集映射到一个更高维的空间中,使得数据集在该空间中是线性可分的。这个技巧称为核方法(kernel trick)。

核方法的基本思想是将原始空间中的数据点 x x x映射到一个更高维的特征空间中,然后在该特征空间中寻找一个超平面来进行分类。具体地,我们定义一个核函数 K ( x i , x j ) K(x_i,x_j) K(xi,xj),它表示将 x i x_i xi x j x_j xj映射到特征空间中后的内积。于是,我们的模型可以表示为:

f ( x ) = sign ( ∑ i = 1 m α i y i K ( x i , x ) + b ) f(x) = \text{sign}\left(\sum_{i=1}^m\alpha_iy_iK(x_i,x)+b\right) f(x)=sign(i=1mαiyiK(xi,x)+b)

其中, α i \alpha_i αi为拉格朗日乘子, b b b为截距。

我们的目标是找到一组 α \alpha α b b b,使得分类超平面距离两个分类的最近数据点最大化。也就是说,我们要最大化分类超平面到最近数据点的距离。这个距离可以用间隔(margin)来度量,即分类超平面到两个类别最近数据点的距离之和。我们的优化目标可以表示为:

max ⁡ α , b W ( α ) = ∑ i = 1 m α i − 1 2 ∑ i , j = 1 m α i α j y i y j K ( x i , x j ) s.t. ∑ i = 1 m α i y i = 0 0 ≤ α i ≤ C , i = 1 , … , m \begin{aligned} \max_{\alpha,b} \quad & W(\alpha) = \sum_{i=1}^m\alpha_i - \frac{1}{2}\sum_{i,j=1}^m\alpha_i\alpha_jy_iy_jK(x_i,x_j) \\ \text{s.t.} \quad & \sum_{i=1}^m\alpha_iy_i=0 \\ & 0\leq\alpha_i\leq C,\quad i=1,\ldots,m \end{aligned} α,bmaxs.t.W(α)=i=1mαi21i,j=1mαiαjyiyjK(xi,xj)i=1mαiyi=00αiC,i=1,,m

其中, C C C为正则化参数,用于平衡模型的复杂度和错误率。 C C C越大,模型越复杂,错误率越低; C C C越小,模型越简单,错误率越高。

我们使用拉格朗日对偶性(Lagrange duality)来求解上述优化问题。经过一些推导,我们可以得到以下对偶问题:

max ⁡ α ∑ i = 1 m α i − 1 2 ∑ i , j = 1 m α i α j y i y j K ( x i , x j ) s.t. ∑ i = 1 m α i y i = 0 0 ≤ α i ≤ C , i = 1 , … , m \begin{aligned} \max_{\alpha} \quad & \sum_{i=1}^m\alpha_i - \frac{1}{2}\sum_{i,j=1}^m\alpha_i\alpha_jy_iy_jK(x_i,x_j) \\ \text{s.t.} \quad & \sum_{i=1}^m\alpha_iy_i=0 \\ & 0\leq\alpha_i\leq C,\quad i=1,\ldots,m \end{aligned} αmaxs.t.i=1mαi21i,j=1mαiαjyiyjK(xi,xj)i=1mαiyi=00αiC,i=1,,m

其中, α \alpha α为拉格朗日乘子。我们可以使用一些优化算法来求解该对偶问题,进而得到分类超平面。

在实际使用中,我们可以选择不同的核函数来进行数据的映射。常用的核函数有线性核、多项式核和高斯核等。在上述公式中,我们使用了高斯核:

K ( x i , x j ) = exp ⁡ ( − ∥ x i − x j ∥ 2 2 σ 2 ) K(x_i,x_j) = \exp\left(-\frac{\|x_i-x_j\|^2}{2\sigma^2}\right) K(xi,xj)=exp(2σ2xixj2)

其中, σ \sigma σ为高斯核的参数,用于控制高斯函数的宽度,即支持向量的影响在使用高斯核时,我们需要选择合适的 σ \sigma σ值。如果 σ \sigma σ太小,那么高斯函数的宽度就会变窄,模型会过度拟合;如果 σ \sigma σ太大,那么高斯函数的宽度就会变宽,模型会出现欠拟合。

支持向量机(SVM)是一种常用的二分类模型,其通过在数据集中找到一个超平面(或分割线)来分割两个不同的分类,同时使得该超平面距离两个分类的最近数据点最大化,从而实现了对未知数据进行分类的目的。

SVM代码实现

在下面代码实现中,我们使用了cvxopt库来求解二次规划问题,并使用了高斯分布数据集来进行训练和预测。在预测函数中,我们使用已经求解得到的支持向量,通过核函数计算预测结果。在评分函数中,我们计算了预测结果的准确率。

import numpy as np
import numpy as np
from sklearn.datasets import make_blobs
import matplotlib.pyplot as plt
from cvxopt import matrix, solvers
from tqdm import tqdm
from sklearn.datasets import make_gaussian_quantiles

def plot_clf(X,y,cls):
    x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
    y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
    xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.02),
                         np.arange(y_min, y_max, 0.02))
    points = np.c_[xx.ravel(), yy.ravel()]
    Z = cls.predict(points).reshape(xx.shape)
    cs = plt.contourf(xx, yy, Z)
    plt.scatter(X[:, 0], X[:, 1], marker='o', c=y)
    plt.show()

class SVM:
    def __init__(self,sigma=1,C=1,kind="linear"):
        assert kind in ["linear","gaussian"]
        self.sigma = sigma
        self.C = C
        gaussian = lambda x,z: np.exp(-0.5*np.sum((x-z)**2)/(self.sigma**2))
        linear = lambda x,z: np.sum(x*z)
        self.kernel = linear if kind == "linear" else gaussian
    
    def fit(self,X,y):
        mat = np.zeros((X.shape[0],X.shape[0]))
        for i in range(X.shape[0]):
            for j in range(i,X.shape[0]):
                result = self.kernel(X[i],X[j])
                mat[i,j] = result
                mat[j,i] = result
        P = mat * (y.reshape(-1,1) @ y.reshape(1,-1))
        q = -1*np.ones(X.shape[0]).reshape(-1,1)
        
        G = np.zeros((2*X.shape[0],X.shape[0]))
        G[0:X.shape[0]] = - np.identity(X.shape[0])
        G[X.shape[0]:] = np.identity(X.shape[0])
        h = np.zeros(2*X.shape[0])
        h[X.shape[0]:] = self.C
        h = h.reshape(-1,1)
        
        A = y.reshape(1,-1)
        b = np.zeros(1).reshape(-1,1)
        
        [P,q,G,h,A,b] = [matrix(i,i.shape,"d")for i in [P,q,G,h,A,b]]
        result = solvers.qp(P,q,G,h,A,b)
        self.A = np.array(result["x"])
        support_vector_index = np.where(self.A > 1e-4)[0]
        self.support_vectors = X[support_vector_index]
        self.support_vector_as = self.A[support_vector_index,0]
        self.support_vector_ys = y[support_vector_index]
        for i,a in enumerate(self.A):
            if a>0+1e-4 and a<self.C-1e-4:
                self.b = y[i] - np.sum(self.A.ravel()*y*mat[i])
                break
    
    def predict(self,X):
        preds = []
        for x in tqdm(X):
            Ks = [self.kernel(x,support_vector) for support_vector in self.support_vectors]
            pred = np.sum(self.support_vector_as * self.support_vector_ys * Ks) + self.b
            pred = 1 if pred >=0 else -1
            preds.append(pred)
        return np.array(preds)

    def score(self,X,y):
        return np.sum(self.predict(X)==y) / len(y)

if __name__ == "__main__":
    X, y = make_gaussian_quantiles(n_samples=200, n_features=2, n_classes=2, mean=[1,2],cov=2,random_state=222)
    y[y==0] = -1

    svc = SVM(kind="linear")
    svc.fit(X,y)
    plot_clf(X,y,svc)

接下来,我们看一下代码的实现。代码实现中,我们定义了一个SVM的类,其中包括了初始化函数、拟合函数、预测函数和评分函数。在初始化函数中,我们定义了模型中使用的核函数,包括线性核和高斯核。SVM类的初始化函数包括三个参数:sigma、C和kind。它们的含义如下:

  • sigma:高斯核函数的参数,用于控制高斯函数的宽度,即支持向量的影响范围。
  • C:SVM的正则化参数,用于平衡模型的复杂度和错误率。C越大,模型越复杂,错误率越低;C越小,模型越简单,错误率越高。
  • kind:SVM模型中使用的核函数类型,可以是线性核或高斯核。
class SVM:
    def __init__(self,sigma=1,C=1,kind="linear"):
        assert kind in ["linear","gaussian"]
        self.sigma = sigma
        self.C = C
        gaussian = lambda x,z: np.exp(-0.5*np.sum((x-z)**2)/(self.sigma**2))
        linear = lambda x,z: np.sum(x*z)
        self.kernel = linear if kind == "linear" else gaussian

在拟合函数中,我们计算了核矩阵,并使用cvxopt库中的二次规划函数来求解支持向量。

在SVM类中,拟合函数fit实现了SVM算法的核心部分,其作用是通过求解二次规划问题来得到分类超平面。下面是拟合函数fit的具体实现流程:

  1. 根据模型中使用的核函数类型,计算出核矩阵。核矩阵是一个对称矩阵,其中每个元素表示两个样本之间的相似度(即核函数的值)。
  2. 将核矩阵与标签y相乘,得到带有正负号的目标函数。其中,y的取值为1或-1,表示对应样本所属的类别。
  3. 使用cvxopt库中的二次规划函数来求解带有约束条件的二次函数。具体地,我们首先使用两层for循环计算出样本之间的核函数值,填充核矩阵mat;然后使用mat、y以及惩罚超参数C来构造出二次规划问题的目标函数P、线性约束G、等式约束A以及上下界h和b;最后使用cvxopt库中的solvers.qp函数求解二次规划问题,得到支持向量的系数self.A和截距self.b。
  4. 求解出拉格朗日乘子后,根据支持向量的定义,选择拉格朗日乘子大于0且小于C的样本作为支持向量。
  5. 根据支持向量计算分类超平面的截距b,同时记录支持向量的权重alpha。
    def fit(self,X,y):
        mat = np.zeros((X.shape[0],X.shape[0]))
        for i in range(X.shape[0]):
            for j in range(i,X.shape[0]):
                result = self.kernel(X[i],X[j])
                mat[i,j] = result
                mat[j,i] = result
        P = mat * (y.reshape(-1,1) @ y.reshape(1,-1))
        q = -1*np.ones(X.shape[0]).reshape(-1,1)
        
        G = np.zeros((2*X.shape[0],X.shape[0]))
        G[0:X.shape[0]] = - np.identity(X.shape[0])
        G[X.shape[0]:] = np.identity(X.shape[0])
        h = np.zeros(2*X.shape[0])
        h[X.shape[0]:] = self.C
        h = h.reshape(-1,1)
        
        A = y.reshape(1,-1)
        b = np.zeros(1).reshape(-1,1)
        
        [P,q,G,h,A,b] = [matrix(i,i.shape,"d")for i in [P,q,G,h,A,b]]
        result = solvers.qp(P,q,G,h,A,b)
        self.A = np.array(result["x"])
        support_vector_index = np.where(self.A > 1e-4)[0]
        self.support_vectors = X[support_vector_index]
        self.support_vector_as = self.A[support_vector_index,0]
        self.support_vector_ys = y[support_vector_index]
        for i,a in enumerate(self.A):
            if a>0+1e-4 and a<self.C-1e-4:
                self.b = y[i] - np.sum(self.A.ravel()*y*mat[i])
                break

在预测函数predict中,我们首先定义了一个空列表preds用于存储预测结果,然后遍历输入的数据点x。对于每一个数据点x,我们计算其与所有支持向量的核函数值,并将其相乘后求和,得到该数据点的预测结果。最后,将预测结果加入到preds列表中,并返回该列表作为整个预测函数的输出。在评分函数score中,我们计算了预测结果的准确率。

    def predict(self,X):
        preds = []
        for x in tqdm(X):
            Ks = [self.kernel(x,support_vector) for support_vector in self.support_vectors]
            pred = np.sum(self.support_vector_as * self.support_vector_ys * Ks) + self.b
            pred = 1 if pred >=0 else -1
            preds.append(pred)
        return np.array(preds)

    def score(self,X,y):
        return np.sum(self.predict(X)==y) / len(y)

最后,我们使用make_gaussian_quantiles函数生成了一个二维高斯分布数据集,并使用SVM模型进行了训练和预测。

def plot_clf(X,y,cls):
    x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
    y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
    xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.02),
                         np.arange(y_min, y_max, 0.02))
    points = np.c_[xx.ravel(), yy.ravel()]
    Z = cls.predict(points).reshape(xx.shape)
    cs = plt.contourf(xx, yy, Z)
    plt.scatter(X[:, 0], X[:, 1], marker='o', c=y)
    plt.show()

if __name__ == "__main__":
    X, y = make_gaussian_quantiles(n_samples=200, n_features=2, n_classes=2, mean=[1,2],cov=2,random_state=222)
    y[y==0] = -1

    svc = SVM(kind="linear")
    svc.fit(X,y)
    plot_clf(X,y,svc)    

总结

本文介绍了支持向量机(SVM)的原理和代码实现。SVM是一种二分类模型,其核心思想是找到一个最优的超平面来将不同类别的数据点分开。在代码实现部分,我们定义了一个SVM类,并实现了拟合函数、预测函数和评分函数。通过使用make_gaussian_quantiles函数生成的二维高斯分布数据集进行训练和预测,我们展示了SVM模型的性能。

完整的实验代码在我的github上QYHcrossover/ML-numpy: 机器学习算法numpy实现 (github.com) 欢迎star⭐

你可能感兴趣的:(经典机器学习算法原理及代码实现,支持向量机,机器学习,python)