支持向量机SVM及sklearn处理问题实现

1 SVM基本原理

1.1 svm简介

支持向量机是在深度学习流行起来之前效果最好的模型,据说流行的时候是05年左右。

基本原理是二分类线性分类器,但现在也可以解决多分类问题,非线性问题和回归问题。

说实话感觉在学完其它主流分类之后比如决策树、贝叶斯,再来学习svm会好一点。因为确实数学理论相对来说比较复杂。我这里也打算做一个简单的介绍,推理啥的就不搞了,主要说一些结论。

svm的全称是support vector machines,支持向量机,汉字都懂,组合一块就懵了。实际上,支持向量是我们需要搞懂的,知道了什么是支持向量,那么自然也就明白这个算法再干什么事了。这个算法可以由一个这样的问题引出:

img

先有感知机的问题,即上面这个图,如何画一条直线,把两类很好的分开。感知机我目前也还没学到,后面会补。但是可以画出这么一条线,把两个类分开。但是在这个空隙中我们其实可以找到无数条线把这两簇点分开,那哪条线是最好的那条呢?

img

Margin:将分界面向两个方向平移至不能平移的位置(他碰到了一个点),可以平移的距离叫做Margin(间隔)。正好卡住这些分界面的点称为Support Vectors。不同方向的Margin不同,Support Vectors也不同。直观上说,Margin越大,容错性越强。所以,希望这个分界面的Margin越大越好。SVM就可以最大化Margin(线性支持向量机)。

我们就定义找到的那条线就是:沿着这条线法向量平移,向法向量的前后都平移,所碰到第一个点的距离(因为两个簇所以至少有两个点)最大的那条线,并且这两个点的距离距离这条线相等。这条线就是我们认为最好的那条决策边界。所碰到的点就被称之为支持向量。

img

SVM模型的求解最大分割超平面问题,就是确定一个超平面能最合理的对二分类问题分类,感知机是找到一个超平面,svm是找到一个最好的超平面,可以使点尽可能远离决策超平面,从而更好地分类。

支持向量机(SVM)——原理篇

1.2 svm的数学原理

如果真想去了解一下数学原理的,可以看一下b站的这个老师,讲了四个小时,逻辑很清晰,但是由于不是搞数学的,里面涉及到一些理论他还是以结论的方式来用了,不过相对来说还是讲的不错的,能让我一个工科生刚好明白的(虽然过几天就忘了),但确实更好理解了,比直接硬记要好一丢丢,链接:https://www.bilibili.com/video/BV1jt4y1E7BQ/。

这个博客也不错:https://aistudio.baidu.com/projectdetail/1691063?ad-from=1694

我们还是简单记录一下svm的数学原理。

  • 两个目标:样本分对;最大化Margin(最小化 w乘以w的转置 )
  • 样本是两类:+1,-1(标签),+1的样本必须wx+b>=1,才是将样本分对。如下图
img
  • 拉格朗日乘数法:(拉格朗日系数:α),分别对w、b求导,结果很重要。将结果带回,得到新的目标函数(与上面的函数是对偶问题)。原问题和对偶问题一般情况下是不等价的,但在SVM情况下满足一些条件,所以是等价的。所以转为求 ��*L**D* 的问题,其是由α组成的(有条件约束),简化了问题。
  • 解方程,得到很多α的值。很多α都是=0的,只有少数是不等于0的,这些不等于0的是Support Vectors。因为α=0的不对w作任何contribution。随便挑选一个support vector就可以将b求出来。用多个support vectors也可以,求解完累加,再除上个数就可以。
img
img

看式子可以知道:训练完成后,大部分的训练样本都不需要保留,最终模型仅与支持向量有关。

  • Soft Margin(软间隔)
image-20231001102948762

松弛变量虽然可以让我们表示那些被错误分类的样本,但是我们当然不希望它随意松弛,这样模型的效果就不能保证了。所以我们把它加入损失函数当中,希望在松弛得尽量少的前提下保证模型尽可能划分正确。

img

这里的C是一个常数,可以理解成惩罚参数。我们希望||w||2尽量小,也希望∑尽量小,这个参数C就是用来协调两者的。C越大代表我们对模型的分类要求越严格,越不希望出现错误分类的情况,C越小代表我们对松弛变量的要求越低。

从形式上来看模型的学习目标函数和之前的硬间隔差别并不大,只是多了一个变量而已。这也是我们希望的,在改动尽量小的前提下让模型支持分隔错误的情况。

  • 根据拉格朗日乘子法,两组不等式引入两个拉格朗日函数,α和μ,最后写出L。
img
  • 仍按原来的方式求解:引入了一个soft margin,但最终结果并没有很复杂。发现与原来的类似,只有<=C不同。
img

1.3 高斯核函数

核函数是机器学习算法中一个重要的概念。简单来讲,核函数就是样本数据点的转换函数。我们来看看应用非常广泛的一个核函数,高斯核函数。

image-20231209111402107
image-20231209111415967
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import numpy as np
import matplotlib.pyplot as plt
# 构建样本数据,x值从-4到5,每个数间隔为1
x = np.arange(-4, 5, 1)
x
# 结果
array([-4, -3, -2, -1, 0, 1, 2, 3, 4])
# y构建为0,1向量,且是线性不可分的
y = np.array((x >= -2) & (x <= 2), dtype='int')
y
# 结果
array([0, 0, 1, 1, 1, 1, 1, 0, 0])
# 绘制样本数据
plt.scatter(x[y==0], [0]*len(x[y==0]))
plt.scatter(x[y==1], [0]*len(x[y==1]))
plt.show()

image-20231209111440895

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def gaussian(x, l):
# 这一节对gamma先不做探讨,先定为1
gamma = 1.0
# 这里x-l是一个数,不是向量,所以不需要取模
return np.exp(-gamma * (x - l)**2)
# 将每一个x值通过高斯核函数和l1,l2地标转换为2个值,构建成新的样本数据
l1, l2 = -1, 1
X_new = np.empty((len(x), 2))
for i, data in enumerate(x):
X_new[i, 0] = gaussian(data, l1)
X_new[i, 1] = gaussian(data, l2)
# 绘制新的样本点
plt.scatter(X_new[y==0, 0], X_new[y==0, 1])
plt.scatter(X_new[y==1, 0], X_new[y==1, 1])
plt.show()
image-20231209111508370

1.4 算法运行流程

可以得出svm算法流程大致如下:

xi表示输入数据的向量表示,yi表示对应数据的分类情况。

分类决策函数就是那个数的值,如果大于0,则分到第一类,否则分到第二类。

image-20231001100045379

求解过程举例:

img

最主要的是求出w和b。

就算上面的数学全部不懂也没关系,你最起码需要知道svm到底干了什么事情,在解决什么样的问题,配合例子理解其实也就够了。

1.5 基于sklearn的样例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
import numpy as np
import os
import matplotlib as mpl
import matplotlib.pyplot as plt
from sklearn import datasets
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.svm import LinearSVC


# 设置字体大小
mpl.rc("axes",labelsize=14)
mpl.rc("xtick",labelsize=12)
mpl.rc("ytick",labelsize=12)

# 创建数据集
iris = datasets.load_iris()

# 这次选择其中两个作为特征,二分类问题,分类的结果就是是否是“2”这个类,转换为二分类的问题
X = iris.data[:,(2,3)]
Y = (iris["target"]==2).astype(np.float64)


# 创建管道,管道就是来定义一系列操作,然后可以一起操作的 包括标准化和创建支持向量机
# linear_svc步骤使用LinearSVC作为线性支持向量机分类器
# LinearSVC和普通的SVC设置参数是line基本一样,主要是linearSVC还有一些优化功能,这里直接看作等效即可
svm_clf = Pipeline([
("scaler",StandardScaler()),
("linear_svc",LinearSVC(C=1,loss="hinge",random_state=42))
])

# 模型训练
svm_clf.fit(X,Y)

# 预测
svm_clf.predict([[5.5,1.7]])

# 改变C的值,观看间隙大小
scaler = StandardScaler()
Linesvc1 = LinearSVC(C=1,loss="hinge",random_state=42)
Linesvc2 = LinearSVC(C=100,loss="hinge",random_state=42)
svm_clf1 = Pipeline([
("scaler",scaler),
("linear_svc",Linesvc1)
])

svm_clf2 = Pipeline([
("scaler",scaler),
("linear_svc",Linesvc2)
])
svm_clf1.fit(X,Y)
svm_clf2.fit(X,Y)

# 获取两个模型的参数
# 计算样本点到超平面的距离
b1 = Linesvc1.decision_function([-scaler.mean_/scaler.scale_])
b2 = Linesvc2.decision_function([-scaler.mean_/scaler.scale_])

w1 = Linesvc1.coef_[0] / scaler.scale_
w2 = Linesvc2.coef_[0] / scaler.scale_

# 转换为数组
Linesvc1.intercept_ = np.array([b1])
Linesvc2.intercept_ = np.array([b2])
Linesvc1.coef_ = np.array([w1])
Linesvc2.coef_ = np.array([w2])

# 寻找支持向量
t = Y*2-1

support_idx1 = (t*(X.dot(w1)+b1)<1).ravel()
support_idx2 = (t*(X.dot(w2)+b2)<1).ravel()

Linesvc1.support_vectors_ = X[support_idx1]
Linesvc2.support_vectors_ = X[support_idx2]

# 定义决策边界函数
# 可视化决策边界
def plot_svc(svm_clf,xmin,xmax):
w = svm_clf.coef_[0]
b = svm_clf.intercept_[0]

X0 = np.linspace(xmin,xmax,200)

decision_boundary = -w[0]/w[1] * X0 - b/w[1]

margin = 1/w[1]
# 得到两边
gutter_up = decision_boundary + margin
gutter_down = decision_boundary - margin
#得到支持向量
svs = svm_clf.support_vectors_

plt.scatter(svs[:,0],svs[:,1],s=180,facecolors="#FFAAAA")
plt.plot(X0,decision_boundary,"k-",linewidth=2)
plt.plot(X0,gutter_up,"k--",linewidth=2)
plt.plot(X0,gutter_down,"k--",linewidth=2)


fig,axes = plt.subplots(ncols=2,figsize=(10,2.7),sharey=True)
plt.sca(axes[0])
plt.plot(X[:,0][Y==1],X[:,1][Y==1],"g^",label="Iris virginica")
plt.plot(X[:,0][Y==0],X[:,1][Y==0],"bs",label="Iris versicolor")
plt.axis([4,5.9,0.8,2.8])
plot_svc(Linesvc1,4,5.9)
plt.xlabel("length")
plt.ylabel("width")
plt.legend(loc="upper left")
plt.title("$C={}$".format(Linesvc1.C))


plt.sca(axes[1])
plt.plot(X[:,0][Y==1],X[:,1][Y==1],"g^",label="Iris virginica")
plt.plot(X[:,0][Y==0],X[:,1][Y==0],"bs",label="Iris versicolor")
plt.axis([4,5.9,0.8,2.8])
plot_svc(Linesvc2,4,5.9)
plt.title("$C={}$".format(Linesvc2.C))

plt.savefig("ouput_plot/demo.png")
demo

可以看到C较大时容忍度较小,C小时,容忍度较大。

2 SVM处理非线性问题

总有一些是无法用直线进行分类的。

对于其他算法来说,比如决策树,贝叶斯,它们的方法是放松拟合的要求,即我的分界线或者叫分界超平面可以不是直线、平面之类的,可以是椭圆,可以是树,可以是其他形状。

svm解决非线性问题的思路是把当前的问题的维度升高,以此来获得高位空间的线性效果。

img

从低维映射至高维,通过公式,大概含义例如有一个100维的数据,映射至5000维。理论上,映射到无限维就可以尽量接近线性,首先从操作上不可能,计算机只能尽可能地提高维度而不能设置无限维度,还有就是计算问题,越高维度,计算量越大。

img

但是存在一个表达式(核函数,Kernel),数学上证明,低维度该计算公式可以得到与高维度计算得出相同的结果,既保证的提高维度,又降低了计算复杂度。

发现转为高维的求解函数,仍与低维的基本一致。核函数的强悍!

常见的有线性核函数,多项式核函数,高斯核函数(最常用)等。

看一下sklearn的参数解释,我们先会用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- C:C-SVC的惩罚参数C?默认值是1.0
C越大,相当于惩罚松弛变量,希望松弛变量接近0,即对误分类的惩罚增大,趋向于对训练集全分对的情况,这样对训练集测试时准确率很高,但泛化能力弱。C值小,对误分类的惩罚减小,允许容错,将他们当成噪声点,泛化能力较强。
- kernel :核函数,默认是rbf,可以是‘linear’, ‘poly’, ‘rbf’, ‘sigmoid’, ‘precomputed’
– 线性:u'v
– 多项式:(gamma*u'*v + coef0)^degree
– RBF函数:exp(-gamma|u-v|^2)
– sigmoid:tanh(gamma*u'*v + coef0)
- degree :多项式poly函数的维度,默认是3,选择其他核函数时会被忽略。
- gamma : ‘rbf’,‘poly’ 和‘sigmoid’的核函数参数。默认是’auto’,则会选择1/n_features
- coef0 :核函数的常数项。对于‘poly’和 ‘sigmoid’有用。
- probability :是否采用概率估计?.默认为False
- shrinking :是否采用shrinking heuristic方法,默认为true
- tol :停止训练的误差值大小,默认为1e-3
- cache_size :核函数cache缓存大小,默认为200
- class_weight :类别的权重,字典形式传递。设置第几类的参数C为weight*C(C-SVC中的C
- verbose :允许冗余输出?
- max_iter :最大迭代次数。-1为无限制。
- decision_function_shape :‘ovo’, ‘ovr’ or None, default=None3
- random_state :数据洗牌时的种子值,int值
主要调节的参数有:C、kernel、degree、gamma、coef0。

举个例子:

分别利用多项式和高斯核函数对数据进行分析。

make_moons 函数生成的数据集由两个半圆形状组成,其中一个半圆表示一个类别,另一个半圆表示另一个类别。这个数据集通常用于二分类问题的演示和实验。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import matplotlib.pyplot as plt
from sklearn.datasets import make_moons

# 获取数据集
X,y = make_moons(n_samples=100,noise=0.15,random_state=42)

def plot_data(X,y,axes):
plt.plot(X[:,0][y==0],X[:,1][y==0],"bs")
plt.plot(X[:,0][y==1],X[:,1][y==1],"g^")
plt.axis(axes)
plt.grid(True,which="both")
plt.xlabel(r"$x_1$")
plt.ylabel(r"$x_2$")

plot_data(X,y,[-1.5,2.5,-1,1.5])
plt.show()

数据大致如下:

image-20231001193421850

由此可见是一个非线性可分的二分类任务。

先用多项式核进行分类,分别设置他们的最高次项,一个为3一个为10

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
from sklearn.preprocessing import StandardScaler
from sklearn.svm import SVC
from sklearn.pipeline import Pipeline
from sklearn.svm import LinearSVC

# 可视化函数
def poly_predictions(clf,axis):
x0s = np.linspace(axis[0],axis[1],100)
x1s = np.linspace(axis[2],axis[3],100)
# 生成网格图标
x0,x1 = np.meshgrid(x0s,x1s)
X = np.c_[x0.ravel(),x1.ravel()]
# 预测值
y_pred = clf.predict(X).reshape(x0.shape)
# 计算到超平面的距离
y_decision = clf.decision_function(X).reshape(x0.shape)
plt.contourf(x0,x1,y_pred,cmap=plt.cm.brg,alpha=0.2)
plt.contourf(x0,x1,y_decision,cmap=plt.cm.brg,alpha=0.1)

# 创建管道

poly_kernel_svm_clf1 = Pipeline([
("scaler",StandardScaler()),
("svm_clf",SVC(kernel="poly",degree=3,coef0=1,C=5))
])
# 这里也改变了coef0的值,也可以不改变,它是表示多项式核函数的常量。
# 当次数变大时,建议同时修改常数项以平衡。
poly_kernel_svm_clf2 = Pipeline([
("scaler",StandardScaler()),
("svm_clf",SVC(kernel="poly",degree=10,coef0=1,C=5))
])
# 训练
poly_kernel_svm_clf1.fit(X,y)
poly_kernel_svm_clf2.fit(X,y)
# 画图
fig,axes = plt.subplots(ncols=2,figsize=(12,4),sharey=True)
plt.sca(axes[0])
poly_predictions(poly_kernel_svm_clf1,[-1.5,2.5,-1,1.5])
plot_data(X,y,[-1.5,2.5,-1,1.5])
# 管道二
plt.sca(axes[1])
poly_predictions(poly_kernel_svm_clf2,[-1.5,2.5,-1,1.5])
plot_data(X,y,[-1.5,2.5,-1,1.5])
plt.savefig("ouput_plot/poly.png")

poly

可以看出来,次项较高时对数据模拟效果更好,边界越准,同时容错率也较低。

再用一下高斯核。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 高斯核函数是比较好的核函数
# 需要设置参数gamma和C

# 这里直接使用四个gamma和C 来对比看一下
# 因为说实话设置什么参数咱也不好确定
gamma1,gamma2 = 0.1,1
C1,C2 = 1,100
hyperparams = [(gamma1,C1),(gamma1,C2),(gamma2,C1),(gamma2,C2)]
svm_guss_clfs = []

for gamma,C in hyperparams:
guss_kernel_svm_clf = Pipeline([
("scaler",StandardScaler()),
("svm_clf",SVC(kernel="rbf",gamma=gamma,C=C))
])
guss_kernel_svm_clf.fit(X,y)
svm_guss_clfs.append(guss_kernel_svm_clf)

fig,axes = plt.subplots(ncols=2,nrows=2,figsize=(10,6),sharex=True,sharey=True)

for i,svm_guss_clf1 in enumerate(svm_guss_clfs):
plt.sca(axes[i//2,i%2])

poly_predictions(svm_guss_clf1,[-1.5,2.5,-1,1.5])
plot_data(X,y,[-1.5,2.5,-1,1.5])
gamma,C = hyperparams[i]
plt.title(r"$\gamma={},C={}$".format(gamma,C))
plt.savefig("ouput_plot/guss.png")

guss

参数过小会欠拟合,如第一个子图,参数过大会过拟合,比如第四个图。

边界较为平滑的我们认为分类效果不错。关于参数的选择及调优问题我们放在第五节讲。

3 SVM处理多分类问题

二分类问题是多分类问题的特殊情况。如果能处理二分类,那么对于多分类,可以把其看成一类和其他类来处理,递归下去,总会处理完。svm就可以借助这样的思想处理多分类问题。

比较直接的方式就是:直接在目标函数上进行修改,将多个分类面的参数求解合并到一个最优化问题中,通过求解该最优化问题“一次性”实现多类分类。这种方法看似简单,但其计算复杂度比较高,实现起来比较困难,只适合用于小型问题中。而且我目前刚学,还没接触到谁这么干的。

一般采用间接方法:

主要是通过组合多个二分类器来实现多分类器的构造,常见的方法有 one-against-one 和 one-against-all 两种,不懂得可以查一下。我这里简单说一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
一对多

A分类正样本,BC那个类分为负样本
B分类正样本,AC那个类分为负样本
C分类正样本,AB那个分类为负样本
先右测试数据D,分别丢到3个分类器中,然后看那个分类器的得分高,那么就把数据判别为哪个类别

一对一

AB分为一组正负样本
AC分为一组正负样本
BC分为一组正负样本
现有测试数据D,分别丢到3个分类器中,统计哪个类别出现的次数最多,那就把数据判别为哪个类别
一般情况,使用OVR还是比较多的,默认也就是OVR。如果有n个类别,那么使用OVO训练的分类器就是,因此一般情况下使用OVR这种分类。

下面我将使用scikit-learn中的鸢尾花(Iris)数据集作为示例,演示如何使用SVM处理多分类问题。

其实默认情况下就是一对一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.svm import SVC
from sklearn.metrics import accuracy_score

# 加载鸢尾花数据集
iris = datasets.load_iris()
X = iris.data
y = iris.target

# 将数据集拆分为训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# 创建SVM分类器
svm = SVC(kernel='linear')

# 在训练集上训练SVM模型
svm.fit(X_train, y_train)

# 在测试集上进行预测
y_pred = svm.predict(X_test)

# 计算准确率
accuracy = accuracy_score(y_test, y_pred)
print("Accuracy:", accuracy)

使用类OneVsRestClassifier实现一对多

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.svm import SVC
from sklearn.multiclass import OneVsRestClassifier
from sklearn.metrics import accuracy_score

# 加载鸢尾花数据集
iris = datasets.load_iris()
X = iris.data
y = iris.target

# 将数据集拆分为训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# 创建SVM分类器
svm = SVC(kernel='linear')

# 创建One-vs-Rest分类器
ovr = OneVsRestClassifier(svm)

# 在训练集上训练One-vs-Rest模型
ovr.fit(X_train, y_train)

# 在测试集上进行预测
y_pred = ovr.predict(X_test)

# 计算准确率
accuracy = accuracy_score(y_test, y_pred)
print("Accuracy:", accuracy)

在这个问题上,一对一的准确率是1,不知道为啥,就是很高,有点难以置信,然后一对多是0.96左右。

4 SVM处理回归问题

svm处理分类问题,是找到一个最大的间隔,让点尽可能地让点进行分开。svm处理回归问题,是找到一个最小的间隔,让点尽可能的落在间隔内。

线性回归举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import numpy as np
from sklearn.svm import LinearSVR

def find_support_vectors(svm_reg,X,y):
y_pred = svm_reg.predict(X)
#选择误差小于给定epsilon的样本点
off_margin = (np.abs(y-y_pred)>=svm_reg.epsilon)
#使用np.argwhere()函数,找到布尔数组中值为True的索引
# 即支持向量
return np.argwhere(off_margin)

def plot_svm_regression(svm_reg,X,y,axes):
xls = np.linspace(axes[0],axes[1],100).reshape(100,1)
y_pred = svm_reg.predict(xls)
#可视化预测点
plt.plot(xls,y_pred,"k-")
#可视化预测边界
plt.plot(xls,y_pred+svm_reg.epsilon,"k--")
plt.plot(xls,y_pred-svm_reg.epsilon,"k--")
# 绘制支持向量
plt.scatter(X[svm_reg.support_],y[svm_reg.support_],s=180,facecolor="#FFAAAA")
plt.plot(X,y,"bo")
plt.xlabel(r"$x_1$")
plt.axis(axes)

np.random.seed(42)
m = 50
X = 2* np.random.rand(m,1)
y = (5+3*X+np.random.randn(m,1)).ravel()
#epsilon是一个控制回归模型的容忍度(tolerance)的参数。
# 在支持向量机回归中,模型的目标是使大部分样本点位于间隔带(回归边界)内,
# 而epsilon定义了间隔带的宽度。
# 较大的epsilon值允许更多的样本点位于间隔带之外,从而使模型更容忍噪声和离群点。
svm_reg1 = LinearSVR(epsilon=0.5,random_state=42)
svm_reg2 = LinearSVR(epsilon=1.5,random_state=42)
svm_reg1.fit(X,y)
svm_reg2.fit(X,y)

#计算支持向量
svm_reg1.support_ = find_support_vectors(svm_reg1,X,y)
svm_reg2.support_ = find_support_vectors(svm_reg2,X,y)

eps_x1 = 1
eps_y_pred = svm_reg1.predict([[eps_x1]])
fig,axes = plt.subplots(ncols=2,figsize=(10,6),sharey=True)
plt.sca(axes[0])
plot_svm_regression(svm_reg1,X,y,[0,2,3,11])
plt.title(r"$\epsilon = {}$".format(svm_reg1.epsilon))
plt.ylabel(r"$y$")
#标注epsilon
#使用plt.annotate()函数在图中添加一个箭头和文本来标注epsilon的范围。
#xy参数指定箭头的起点坐标,xytext参数指定文本的坐标。箭头的样式和线宽通过arrowprops参数设置。
plt.annotate(
'',xy = (eps_x1,eps_y_pred),xycoords="data",
xytext=(eps_x1,eps_y_pred-svm_reg1.epsilon),
textcoords="data",arrowprops={'arrowstyle':'<->',"linewidth":1.5}
)
plt.text(0.91,5.6,r"$\epsilon$")

plt.sca(axes[1])
plot_svm_regression(svm_reg2,X,y,[0,2,3,11])
plt.title(r"$\epsilon = {}$".format(svm_reg2.epsilon))
plt.savefig("ouput_plot/line_reg.png")
plt.show()

line_reg

epsilon越大,容忍性越大。黑线就是我们回归找到的回归线。

非线性回归举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 非线性回归

from sklearn.svm import SVR
np.random.seed(42)
m = 100
# 这个函数返回一个形状为(100, 1)的随机数组,其中的值在[0, 1)之间。
X = 2*np.random.rand(m,1)-1
y = (0.2+0.2*X+0.6*X**2+np.random.randn(m,1)/10).ravel()
# 创建支持向量回归

svm_poly_reg1 = SVR(kernel="poly",degree=2,C=0.01,epsilon=0.1)
svm_poly_reg2 = SVR(kernel="poly",degree=2,C=100,epsilon=0.1)

#较小的gamma值会导致高斯核函数的曲线更宽,模型更加平滑。
#较大的gamma值会导致高斯核函数的曲线更窄,模型更加复杂。
svm_gmm_reg1 = SVR(kernel="rbf",C=1,epsilon=0.1,gamma=0.1)
svm_gmm_reg2 = SVR(kernel="rbf",C=1,epsilon=0.1,gamma=0.1)
svm_gmm_reg3 = SVR(kernel="rbf",C=100,epsilon=0.1,gamma=10)
svm_gmm_reg4 = SVR(kernel="rbf",C=100,epsilon=0.1,gamma=10)
# 训练
svm_poly_reg1.fit(X,y)
svm_poly_reg2.fit(X,y)
svm_gmm_reg1.fit(X,y)
svm_gmm_reg2.fit(X,y)
svm_gmm_reg3.fit(X,y)
svm_gmm_reg4.fit(X,y)

# 可视化
fig,axes = plt.subplots(ncols=2,figsize=(16,9),sharey=True)
plt.sca(axes[0])
plot_svm_regression(svm_poly_reg1,X,y,[-1,1,0,1])
plt.title(r"$degree={} , C={} ,\epsilon={}$".format(svm_poly_reg1.degree,svm_poly_reg1.C,svm_poly_reg1.epsilon))
plt.ylabel(r"$y$")

plt.sca(axes[1])
plot_svm_regression(svm_poly_reg2,X,y,[-1,1,0,1])
plt.title(r"$degree={}, C={} ,\epsilon={}$".format(svm_poly_reg2.degree,svm_poly_reg2.C,svm_poly_reg2.epsilon))
plt.savefig("ouput_plot/ploy_reg.png")

ploy_reg

epsilon越大,越宽。

当C较小时,模型对误分类的惩罚增加,会倾向于选择较大间隔(margin)的决策边界。这会使得模型更加简单,偏向于做出更平滑的预测。当C较大时,模型对误分类的惩罚减小,会倾向于选择较小间隔的决策边界。这会使得模型更加复杂,可能更好地拟合训练数据,但也可能更容易过拟合。

较小的C值和较大的epsilon值会导致模型更加简单,对噪声和异常值更具鲁棒性,但可能会损失一定的拟合能力。较大的C值和较小的epsilon值会使模型更加复杂,更好地拟合训练数据,但可能会对噪声和异常值更敏感。较大的C和较大的epsilon值会使支持向量机(SVM)模型更加自由,更容易拟合训练数据。较小的C和较小的epsilon值会使支持向量机(SVM)模型更加正则化和对预测误差更加敏感。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 高斯核预测一波
svm_gmm_reg1 = SVR(kernel="rbf",C=1,epsilon=0.1,gamma=0.1)
svm_gmm_reg2 = SVR(kernel="rbf",C=1,epsilon=0.1,gamma=0.1)
svm_gmm_reg3 = SVR(kernel="rbf",C=100,epsilon=0.1,gamma=10)
svm_gmm_reg4 = SVR(kernel="rbf",C=100,epsilon=0.1,gamma=10)

svm_gmm_reg1.fit(X,y)
svm_gmm_reg2.fit(X,y)
svm_gmm_reg3.fit(X,y)
svm_gmm_reg4.fit(X,y)


# 可视化
fig,axes = plt.subplots(ncols=2,nrows=2,figsize=(16,9),sharey=True)
plt.sca(axes[0,0])
plot_svm_regression(svm_gmm_reg1,X,y,[-1,1,0,1])
plt.title(r"$gamma={}, C={} ,\epsilon={}$".format(svm_gmm_reg1.gamma,svm_gmm_reg1.C,svm_gmm_reg1.epsilon))
plt.ylabel(r"$y$")

plt.sca(axes[0,1])
plot_svm_regression(svm_gmm_reg2,X,y,[-1,1,0,1])
plt.title(r"$gamma={}, C={} ,\epsilon={}$".format(svm_gmm_reg2.gamma,svm_gmm_reg2.C,svm_gmm_reg2.epsilon))

plt.sca(axes[1,0])
plot_svm_regression(svm_gmm_reg3,X,y,[-1,1,0,1])
plt.title(r"$gamma={}, C={} ,\epsilon={}$".format(svm_gmm_reg3.gamma,svm_gmm_reg3.C,svm_gmm_reg3.epsilon))

plt.sca(axes[1,1])
plot_svm_regression(svm_gmm_reg4,X,y,[-1,1,0,1])
plt.title(r"$gamma={}, C={} ,\epsilon={}$".format(svm_gmm_reg4.gamma,svm_gmm_reg4.C,svm_gmm_reg4.epsilon))
plt.savefig("ouput_plot/guss_reg.png")
plt.show()



guss_reg

左右比看gamma,上下比看C。

C越大,越复杂,gamma越大越复杂。都容易过拟合。epsilon没拿出来对比,和多项式差不多一个道理。

所以选择好的参数是比较重要的。

5 SVM的优化

在第2节和第4节的多项式和高斯分类及回归中,我们可以知道,参数的选择对于svm的高斯核来说至关重要。

选择合适的gamma和C是重要的。

另外,一个模型的训练速度是必要的。我们前文提到无限维的运算量是接受不了的,所以转换成高斯核,极大的降低了计算量,但是是否还有其它优化速度的方法呢?

5.1 交叉验证法+网格搜索法确定超参数

交叉验证可以网上搜索原理及过程,不难,网格搜索法说白了也可以认为就是暴力枚举。

就还是拿第2节中的月牙数据集做分析。

在不知道用哪个核函数的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from sklearn.model_selection import GridSearchCV
from sklearn.preprocessing import StandardScaler
X,y = make_moons(n_samples=100,noise=0.15,random_state=42)
std_scaler = StandardScaler()
x_std = std_scaler.fit_transform(X)
# 定义参数的组合
params = {
'kernel': ('linear', 'rbg', 'poly'),
'C': [0.01, 0.1, 0.5, 1, 2, 10, 100,1000,10000]
}

# 用网格搜索方式拟合模型
svm_classification = SVC()
model = GridSearchCV(svm_classification, param_grid=params, cv=10)
model.fit(x_std, y)

# 查看结果
print('最好的参数组合:', model.best_params_)

print('最好的score:', model.best_score_)

这里给出了是结果是:

最好的参数组合: {'C': 100, 'kernel': 'poly'} 最好的score: 0.9

紧接着网格搜索其他参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from sklearn.model_selection import GridSearchCV
from sklearn.svm import SVC
from sklearn.metrics import accuracy_score
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split

# 创建模拟数据集
X, y = make_moons(n_samples=100,noise=0.15,random_state=42)
std_scaler = StandardScaler()
x_std = std_scaler.fit_transform(X)


svm_model = SVC(kernel='poly')
param_grid = {
'degree': [2, 3, 4], # 多项式的度数
'C': [0.001,0.01,0.1, 1, 10], # 正则化参数
#'epsilon': [0.1, 0.01, 0.001] # 误差容忍度
}


# 定义评估指标
scoring = 'accuracy'

model = GridSearchCV(svm_model, param_grid=param_grid, cv=10)
model.fit(x_std, y)
print('最好的参数组合:', model.best_params_)

print('最好的score:', model.best_score_)

最好的参数组合: {'C': 0.1, 'degree': 3} 最好的score: 0.8900000000000002

有点怪,但是也不怪,因为加了个参数,所以C又变了。。。

5.2 多分类任务选择一对多还是一对一

先说结论:数据量大用一对多,数据量小用一对一

一对多策略适用于数据集规模较大的情况,其中每个类别都有一个二分类器来区分该类别与其他所有类别。在训练阶段,针对每个类别训练一个二分类器,将该类别作为正例,其他类别作为负例。在预测阶段,通过这些二分类器的结果来确定最终的类别。

一对一策略适用于数据集规模较小的情况,其中针对每对类别都训练一个二分类器。例如,对于K个类别,将任意两个类别组合成一对,总共需要训练K(K-1)/2个二分类器。在预测阶段,通过所有二分类器的投票来确定最终的类别。

一对多策略的优势在于训练时间较短,因为每个二分类器只需要针对一个类别进行训练。而一对一策略的优势在于预测时间较短,因为只需要对每个二分类器进行一次预测,并通过投票来确定最终的类别。

在实际应用中,通常根据数据集的规模和性能需求来选择使用哪种策略。如果数据集较大,一对多策略可能更合适。如果数据集较小,一对一策略可能更适合。

5.3 优化svm的训练速度

当数据量较大,需要交叉验证,需要多分类时会让训练速度变得很慢。那么提高svm的训练速度就是必要的。

这里主要想说明的就是部分数据的训练就可以达到很好的效果+交叉验证加速。

运行下面代码需要保证网络,从网上下载数据集。

1
2
3
4
5
6
7
# 优化速度
# 加载MNIST数据集
from sklearn.datasets import fetch_openml

data_path = "/optimize/"
mnist = fetch_openml("mnist_784",version=1,data_home=data_path,as_frame=False)

查看大小

1
mnist.data.shape

(70000, 784)

还是蛮大的,70000行,784个列。使用LinearSVC训练。

所用时间1m36.5s,时间比较长。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from sklearn.svm import LinearSVC
import numpy as np
# 获取数据集minst
X = mnist["data"]
y = mnist["target"].astype(np.uint)

# 原数据集已经打乱了,直接取就行.
X_train = X[:60000]
y_train = y[:60000]
X_test = X[60000:]
y_test = y[60000:]

# 使用LinearSVC 这个规则是一对多
line_svm = LinearSVC(random_state=42)
line_svm.fit(X_train,y_train)


评价一下模型的准确率:

1
2
3
4
from sklearn.metrics import accuracy_score

y_pred = line_svm.predict(X_train)
accuracy_score(y_train,y_pred)

输出:0.8348666666666666

能不能提升准确率呢,可以,标准化对svm又比较好的影响。

1
2
3
4
5
6
7
8
9
from sklearn.preprocessing import StandardScaler

std_scaler1 = StandardScaler()
std_train_x = std_scaler1.fit_transform(X_train.astype(np.float32))
std_test_x = std_scaler1.fit_transform(X_test.astype(np.float32))
# 再创建一个LinearSVC
line_svm1 = LinearSVC(random_state=42)
line_svm1.fit(std_train_x,y_train)

时间用了10m21.9s,时间很长,再看一下准确率

1
2
3
4
from sklearn.metrics import accuracy_score

y_pred = line_svm1.predict(std_train_x)
accuracy_score(y_train,y_pred)

得到0.9214,确实提升了不少。

但是对于minst数据集来说,这样的结果其实并不是很好。

到这里你只需要知道线性拟合效果不是特别理想,并且速度比较慢。

所以我们改用另一个拟合,高斯拟合。且默认使用SVC,多分类采用一对一,速度更快一点。

1
2
3
4
5
6
7
8
# 使用rbf拟合
from sklearn.svm import SVC

svm_guss_clf = SVC(gamma="scale")
# 只要前10000条
svm_guss_clf.fit(std_train_x[:10000],y_train[:10000])


时间4.8s

1
2
3
# 用训练了10000条数据的模型评估60000条的结果
y_pred = svm_guss_clf.predict(std_train_x)
accuracy_score(y_train,y_pred)

准确率:0.9455333333333333

能否更高呢,我们知道高斯需要合适的参数,交叉验证使用随机交叉验证。

尝试更小的参数数据集,1000个。

1
2
3
4
5
6
7
8
9
10
from sklearn.model_selection import RandomizedSearchCV
from scipy.stats import reciprocal,uniform



#reciprocal(0.001, 0.1)表示gamma参数的倒数在0.001到0.1之间均匀分布
#而uniform(1, 10)表示C参数在1到10之间均匀分布。
param_distributions = {"gamma":reciprocal(0.001,0.1),"C":uniform(1,10)}
rnd_search_cv = RandomizedSearchCV(svm_guss_clf,param_distributions,n_iter=10,verbose=2,cv=3)
rnd_search_cv.fit(std_train_x[:1000],y_train[:1000])

很快

image-20231002125624837

1
2
print(rnd_search_cv.best_estimator_)
print(rnd_search_cv.best_score_)

输出:

image-20231002125829437

效果并不好,可以考虑扩大数据集,因为毕竟才用了1000个。

继续在原有的基础上训练:

1
rnd_search_cv.best_estimator_.fit(std_train_x,y_train)

用时3m28.2s。

再看在训练集上的准确率:

1
2
3
4
from sklearn.metrics import accuracy_score
y_pred = rnd_search_cv.best_estimator_.predict(std_train_x)
print(accuracy_score(y_train,y_pred))

0.9963333333333333

99.6%,嗯挺高了。

再看在测试集的:

1
2
3
4
#测试
std_X_test = std_scaler1.fit_transform(X_test.astype(np.float32))
y_pred = rnd_search_cv.best_estimator_.predict(std_X_test)
print(accuracy_score(y_test,y_pred))

0.9719

97.2%,也不错。

可以看出使用较小样本进行交叉验证,确定大致的参数后,随后再在原有的模型追加训练,时间更短,效果也不错。

这5.3小节主要是讲了随机小样本交叉验证的提速。

如果这篇博客给到您帮助,我希望您能给我的仓库点一个star,这将是我继续创作下去的动力。

我的仓库地址,https://github.com/Guoxn1?tab=repositories

like

支持向量机SVM及sklearn处理问题实现
http://example.com/2023/10/02/支持向量机SVM原理及sklearn实现/
作者
Guoxin
发布于
2023年10月2日
许可协议