1.什么是SVM
下面我们就来介绍一些SVM(Support Vector Machine),首先什么是SVM,它是做什么的?SVM,中文名是支撑向量机,既可以解决分类问题,也可以解决回归问题,我们来看看它的思想是怎么样的。
这是一个简单的分类问题,我们很容易想到可以找一个决策边界,那么在决策边界上方的分为红色的点、下方则分为蓝色的点。可以这个决策边界选在什么地方好呢?
可以看到图中两个蓝色的线,都可以叫做决策边界,对于这种决策边界不唯一的问题,通常叫做不适定问题。可以回想一下逻辑回归是如何解决不适定问题的,逻辑回归是定义了一个概率函数,也就是所谓的sigmod函数,根据这个概率函数进行建模,形成了一个损失函数,我们最小化这个损失函数,从而求出一个决策边界,这就是逻辑回归的思路。
支撑向量机的方法稍微有一些不同,假设我们以上面那根蓝色的线作为决策边界的话。显然非常好的将我们的训练数据集分成了两部分,但是我们在意的是在测试集、也就是未知的数据表现的怎么样,也就是模型的泛化能力如何。是否能很好的得出那些未知的数据的分类结果呢?
比如又来了一个新的点,根据决策边界我们可以得出,这个点是属于蓝色的点,但是我们发现把它归为红色的点更好一些,显然它距离红色的点更近一些。但是之所以会出现这个结果,是因为决策边界距离红色的点太近了。所以我们说找到的这个决策边界泛化能力太差了。
显然我们的决策边界不仅要能够区分红色、蓝色的点,还要距离红色、蓝色的点比较远。值得一提的是,SVM对模型的泛化,没有寄望在对数据的预处理上、或者找到模型之后再对模型正则化这种方式,而是将对泛化能力的考量放到了算法内部。就是我们找到一个决策边界,使得红色的点和蓝色的点中离决策边界最近的距离尽可能的远,我们认为这样的决策边界泛化能力是比较好的。但是实际上,这不仅仅是一个直观的假设,它的背后也是有数学的理论来支撑的。在数学上,我们是可以严格的证明出,对于这样一种不适定的问题,使用SVM的思路找到的决策边界相应的泛化能力是很好的。也正是因为如此,SVM也是统计学中一种非常重要的方法,背后是有极强的统计理论的支撑。
如果我们在离决策边界最近的红色的点、蓝色的点上,分别做一条与决策边界相平行的直线的话
SVM尝试寻找一个最优的决策边界,距离两个类别的最近的样本最远,并且这两个距离是相等的,我们记作d
其中图中的蓝色的点、红色的点就是支撑向量,它们构成了所谓的区域,我们的最优的决策边界就是被这些所谓的区域定义的,是区域里面的一条边界。
对于整个区域的距离,我们成为margin,也就是二倍的d,那么我们SVM算法就是最大化这个margin。可以看到我们再次将机器学习的思路转化为最优值求解的问题。
我们说最大化这个margin,但其实这是自然语言表达, 我们要求出这个margin的表达式,通过找到一组参数,来求出margin的最大值
2.SVM背后的优化问题
我们之前介绍了,SVM算法本质就是要最大化这个margin,而margin就是我们的两个类别的支撑向量所构成的两根直线之间的距离。也等于二倍的d,我们最大化margin,就等同于最大化d,我们来看看这个d的数学表达式是什么。
首先我们回忆一下解析几何中,点到直线的距离,然后拓展到n维平面
拓展到n为空间时,可以得到Wx + b = 0,注意我们的θ,由于里面多了一个θ0,所以有n+1个元素,同理Xb,我们在每一个样本前面都加上了一个1,所以也有n+1个元素。但是对于Wx+b这种形式,由于b是截距,w是系数,所以w和b都只有n个元素。那么对于多维的方程,距离可以由二维进行推导,其中w的模等于各个系数w1,w2,····,wn的平方之和再开方
显然我们可以看出,所有的点到决策边界的距离都是要大于等于d的。决策边界是Wx+b=0的话,那么在决策边界上方的点,我们归类为1,在决策边界下方的点,我们归类为-1,注意是-1不是0,当然取什么值是无所谓的,只要能区分开就可以。因此如果将绝对值打开的话,那么对于任意y=1,那么距离都应该大于等于d,对于任意y=-1,要小于等于-d,这是显而易见的。
w的模是一个常数,d也是一个常数。如果我们分子分母同时除以这个常数的话,可以得到如下式子。就像2x+y=0和4x+2y=0一样,都是同一根直线,只不过约掉了一些系数。此时的w和之前的w不是同一个w了,但整体意思是一样的。目前这是两个式子,不方便,我们可以再化简一下,得到如下结果。
因此对于所有的点,都要满足此式子。条件有了,再来我们的优化目标。
由于wx+b的绝对值是一个常数,相当于我们要优化w的模分之一,使其最大,等同于是w的模最小,也就是使w模的平方的二分之一最小。之所以这样做,显然是为了求导方便,本质是一样的。
和之前的最优化不同,之前的最优化是没有限定条件的,也就是全局最优化,只要找到导数为0的点即可。但现在是一个有条件的最优化问题,因此求解方法变复杂了很多,会使用到拉普拉斯算子,这里就不介绍了。但是思想是需要了解的,我们是如何从SVM的原理推导出这样的式子的,后面还有一个Soft Margin SVM,其实是在这个式子的基础之上进行了更进一步的深入。
3.Soft Margin SVM
之前我们介绍的SVM其实是Hard Soft SVM,下面我们介绍的是Soft Margin SVM,那么这两者之间有什么区别呢?Hard Margin SVM是要求数据是可分的,在二维空间中我们能找到一条直线、在高维空间中我们能找到一个超平面将不同的样本能完全的区分开。但有时候,想要完全区分开是很难得,Soft Margin SVM就是用来解决这样的一个问题的。
我们注意一下图中画圈的蓝色的点,如果是Hard Margin SVM的话,必须要求将样本完全的区分开,那么决策边界很有可能就是红色箭头所指的那根直线,但是我们显然能够看出,这个模型的泛化能力是不好的,因为受到极端数据的影响太大了。相反,蓝色箭头指向的决策边界对应的那根直线,我们认为是比较好的,尽管它有一个分错了,但是在实际生产中,泛化能力会更好,因此我们的决策边界需要有一定的容错能力,我们的目的还是希望泛化能力尽可能的高。
我们可以例子举的再极端一点,(⊙o⊙)…,也不能说极端吧,毕竟真实样本数据是可能存在这种情况的。
如果是这种情况的话,那么就不是泛化能力强不强的问题了,而是根本就找不到这样的一条决策边界将样本数据完全分隔开。因此不管从哪个角度分析,我们都必须做出一个具有容错能力的SVM,这种SVM就叫做Soft Margin SVM。
那么Soft Margin SVM是如何实现的呢?
我们可以找到一个ξ,让原来的>=1,变成>=1 - ξ,这样就等于一定程度上放宽了条件,那么虚线对应的方程就可以写成wx + b = 1 - ξ,我们是允许一些蓝色的点跑到虚线的上方的。注意这个ξ,并不是唯一的,每一个y都会对应一个ξ。而且ξ是大于0的,如果小于0,等于条件反而变得更严格了。但是我们的条件又不能放得太宽,如果ξ无限大的话,等于虚线跑到下方无穷远的地方了。因此我们可以将原来的损失函数改变一下,加上ξ1+ξ2···+ξn,使得这样的式子最小,这样就兼顾了两者。
但是ξ1+ξ2···+ξn有可能过大或者过小,因此我们需要加上一个系数
如果C=1,那么两者的程度是一样的,如果C>1,那么我们重点是优化后面的式子,如果C<1,那么我们重点是优化前面的式子,因此这个C也是一个超参数,我们可以通过网格搜索的方式来寻找一个最好的C,因此这类似于逻辑回归,相当于进行了正则化。自然会有L1正则,L2正则。
4.sklearn中的SVM
对于SVM,它和KNN一样,是需要进行数据的标准化的。因为涉及到距离,就必须要解决量纲的问题。我们先来看看数据集,我们依旧是用鸢尾花数据集
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris
iris = load_iris()
X = iris.data
y = iris.target
# 为了演示方便我们只取两个特征
# 并且为了可视化,我们只取两个标签
X = X[y < 2, :2]
y = y[y < 2]
%matplotlib inline
plt.figure(figsize=(10, 8))
plt.scatter(X[y == 0, 0], X[y == 0, 1], color="blue")
plt.scatter(X[y == 1, 0], X[y == 1, 1], color="red")
plt.show()
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris
from sklearn.preprocessing import StandardScaler
from sklearn.svm import LinearSVC # 支撑向量机位于svm包下面,为什么叫做LinearSVC呢?表示的是我们使用线性(Linear)支持向量(SV)解决分类(C)问题
iris = load_iris()
X = iris.data
y = iris.target
X = X[y < 2, :2]
y = y[y < 2]
standard_scaler = StandardScaler()
X_standard = standard_scaler.fit_transform(X)
svc = LinearSVC()
svc.fit(X, y)
y_predict = svc.predict(X)
plt.figure(figsize=(10, 8))
plt.scatter(X[y_predict == 0, 0], X[y_predict == 0, 1], color="blue")
plt.scatter(X[y_predict == 1, 0], X[y_predict == 1, 1], color="red")
plt.show()
5.SVM中使用多项式特征和核函数
如果是实现非线性数据分类的话,那么老套路还是使用多项式,先生称数据集吧
import matplotlib.pyplot as plt
from sklearn.datasets import make_moons
# sklearn可以让我们手动生成数据集
X, y = make_moons()
plt.figure(figsize=(10, 8))
plt.scatter(X[y == 0, 0], X[y == 0, 1], color="blue")
plt.scatter(X[y == 1, 0], X[y == 1, 1], color="red")
plt.show()
但是数据集太规整了,我们可以加上一些噪音
import matplotlib.pyplot as plt
from sklearn.datasets import make_moons
X, y = make_moons(noise=0.15, random_state=666)
plt.figure(figsize=(10, 8))
plt.scatter(X[y == 0, 0], X[y == 0, 1], color="blue")
plt.scatter(X[y == 1, 0], X[y == 1, 1], color="red")
plt.show()
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_moons
from sklearn.preprocessing import StandardScaler, PolynomialFeatures
from sklearn.svm import LinearSVC
from sklearn.pipeline import Pipeline
def polynomial_svc(degree, C=1.0):
return Pipeline([
("poly", PolynomialFeatures(degree=degree)),
("std_scaler", StandardScaler()),
("linearSVC", LinearSVC(C=C))
])
X, y = make_moons(noise=0.15, random_state=666)
poly_svc = polynomial_svc(degree=3)
poly_svc.fit(X, y)
# 绘制决策边界
def plot_decision_boundary(model, axis):
x0, x1 = np.meshgrid(
np.linspace(axis[0], axis[1], int((axis[1] - axis[0]) * 100)).reshape(1, -1),
np.linspace(axis[3], axis[2], int((axis[3] - axis[2]) * 100)).reshape(1, -1)
)
X_new = np.c_[x0.ravel(), x1.ravel()]
y_predict = model.predict(X_new)
zz = y_predict.reshape(x0.shape)
from matplotlib.colors import ListedColormap
custom_cmap = ListedColormap(["#EF9A9A", "#FFF59D", "#90CAF9"])
plt.contourf(x0, x1, zz, linewidth=5, cmap=custom_cmap)
plt.figure(figsize=(10, 8))
plot_decision_boundary(poly_svc, axis=[-1.5, 2.5, -1.0, 1.5])
plt.scatter(X[y == 0, 0], X[y == 0, 1], color="blue")
plt.scatter(X[y == 1, 0], X[y == 1, 1], color="red")
plt.show()
此时我们的决策边界已经不再是一条规整的直线,而是一条不规则的曲线。
我们目前使用的方式是,经过多项式处理,再扔进LinearSVC()中,但是sklearn支持我们直接SVM中使用多项式的方式,就是所谓的多项式核函数。关于什么是多项式核函数,我们之后介绍,目前先看看怎么用。
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_moons
from sklearn.preprocessing import StandardScaler
from sklearn.svm import SVC # 此时使用的不再是LinearSVC,而是SVC
from sklearn.pipeline import Pipeline
def polynomial_kernel_svc(degree, C=1.0):
return Pipeline([
# 此时只需要两步即可
("std_scaler", StandardScaler()),
("linearSVC", SVC(kernel="poly", C=C, degree=degree)) # kernel="poly"同样可以达到多项式的效果,当然需要指定degree和C
])
X, y = make_moons(noise=0.15, random_state=666)
poly_kernel_svc = polynomial_kernel_svc(degree=3)
poly_kernel_svc.fit(X, y)
# 绘制决策边界
def plot_decision_boundary(model, axis):
x0, x1 = np.meshgrid(
np.linspace(axis[0], axis[1], int((axis[1] - axis[0]) * 100)).reshape(1, -1),
np.linspace(axis[3], axis[2], int((axis[3] - axis[2]) * 100)).reshape(1, -1)
)
X_new = np.c_[x0.ravel(), x1.ravel()]
y_predict = model.predict(X_new)
zz = y_predict.reshape(x0.shape)
from matplotlib.colors import ListedColormap
custom_cmap = ListedColormap(["#EF9A9A", "#FFF59D", "#90CAF9"])
plt.contourf(x0, x1, zz, linewidth=5, cmap=custom_cmap)
plt.figure(figsize=(10, 8))
plot_decision_boundary(poly_kernel_svc, axis=[-1.5, 2.5, -1.0, 1.5])
plt.scatter(X[y == 0, 0], X[y == 0, 1], color="blue")
plt.scatter(X[y == 1, 0], X[y == 1, 1], color="red")
plt.show()
可以看到,两种结果是不一样的,说明sklearn中的核函数的计算是有自己的方式的。
6.到底什么是核函数
一句话:以多项式核函数为例,就是将所有的数据点添加了多项式项,再将这些有了多项式项的新的数据特征进行点乘,就形成了多项式核函数
7.RBF核函数
图中所示的便是高斯核函数,为什么将其称之为高斯核函数呢?我们之前大学学的正态分布就是也叫作高斯函数,这两者的形式是比较类似的,所以叫做高斯核函数。对于高斯核函数来说,只有一个超参数就是γ。高斯核函数也被叫做RBF核(Radial Basis Function Kernel,径向基函数)
图中所示的便是高斯核函数,为什么将其称之为高斯核函数呢?我们之前大学学的正态分布就是也叫作高斯函数,这两者的形式是比较类似的,所以叫做高斯核函数。对于高斯核函数来说,只有一个超参数就是γ。高斯核函数也被叫做RBF核(Radial Basis Function Kernel,径向基函数)
对于多项式核函数来说,它的本质就是将所有的数据点添加了多项式项,再将这些有了多项式项的新的数据特征进行点乘,就形成了多项式核函数。那么高斯核函数,应该也是将原来的数据点映射成了一种新的特征向量,然后这些新的特征向量点乘的结果。然而事实正是这样,只不过高斯核函数表达的这种数据的映射是非常复杂的。
高斯核函数是将每一个样本点映射到一个无穷维的特征空间
听起来怪吓人的,不过我们可以用一个简单的例子来模拟一下高斯核函数到底在做什么事情。
我们可以回忆一下多项式特征,是通过升维的方式使原来线性不可分的数据变得线性可分。
比如原本的数据是一维数据,我们找不到任何一条直线可以将红色和蓝色的点区分开。如果我们添加多项式的话,相当于是在升维,让这些特征不仅有第一个维度,还有第二个维度,那么数据就变成了这个样子
x轴上面的位置没有变,但是多了一个y轴,此时我们便可以找到一条直线将蓝色和红色的样本点区分开。通过升维,可以使原来线性不可分的数据变得线性可分。那么高斯核函数做的也是同样的事情。
我在这些样本点中随便拎出来两个,l1和l2,这在英文当中叫做landmark(地标),那么每一个样本点x就可以根据l1和l2映射到二维空间中。我们来程序演示一下。
import numpy as np
import matplotlib.pyplot as plt
x = np.arange(-4, 5, 1)
print(x) # [-4 -3 -2 -1 0 1 2 3 4]
# 我们将x位于-2到2之间的数字变成1,之外的变成0
y = np.array((x >= -2) & (x <= 2), dtype=np.int)
print(y) # [0 0 1 1 1 1 1 0 0]
plt.scatter(x[y == 0], [0] * np.sum(y == 0))
plt.scatter(x[y == 1], [0] * np.sum(y == 1))
plt.show()
# 我们手动映射数据集,注意到这个γ,我们先取1.0
def gaussion(x, l, gamma=1.0):
return np.exp(-gamma * (x - l) ** 2)
l1, l2 = -1, 1
x_new = np.empty((len(x), 2))
for i, data in enumerate(x):
x_new[i, 0] = gaussion(data, l1)
x_new[i, 1] =gaussion(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()
那么显然此时的数据就是线性可分的了,感受一下,我们用了一种不同于多项式特征的一种方式,将一维不可分的数据映射成了二维可分的数据
我们目前取的是l1和l2,然而高斯核函数对应的y,因此高斯核函数是对每一个y都取了landmark。
所以我们说为什么高斯核函数是将数据映射到了一个无穷维的空间中,主要是样本m的个数,如果m的个数无限的话,那么维度就是无限的,但是实际上m的个数是有限的。不过从这里也能看出高斯核函数的计算量非常的大,对于那些本来数据就是高维但是样本数量不多的情况,比如自然语言处理,我们可以使用高斯核函数
8.RBF核函数中gamma
在高斯函数中,μ影响的是图像中心的位置,而σ影响的是曲线的形状,σ越大图像越矮越胖、越小图像越高越瘦。但是 对于高斯核函数来说,正好是相反的。因为我们看到σ在分母的位置上,而高斯核函数中的γ则在分子的位置上。
下面我们看看如何在sklearn中使用RBF核函数
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_moons
from sklearn.preprocessing import StandardScaler
from sklearn.svm import SVC
from sklearn.pipeline import Pipeline
def RBFKernelSVC(gamma=1.0):
return Pipeline([
("std_scaler", StandardScaler()),
("svc", SVC(kernel="rbf", gamma=gamma)) # 只需要指定kernel="rbf"即可,然后指定gamma
])
# 绘制决策边界
def plot_decision_boundary(model, axis):
x0, x1 = np.meshgrid(
np.linspace(axis[0], axis[1], int((axis[1] - axis[0]) * 100)).reshape(1, -1),
np.linspace(axis[3], axis[2], int((axis[3] - axis[2]) * 100)).reshape(1, -1)
)
X_new = np.c_[x0.ravel(), x1.ravel()]
y_predict = model.predict(X_new)
zz = y_predict.reshape(x0.shape)
from matplotlib.colors import ListedColormap
custom_cmap = ListedColormap(["#EF9A9A", "#FFF59D", "#90CAF9"])
plt.contourf(x0, x1, zz, linewidth=5, cmap=custom_cmap)
X, y = make_moons(noise=0.15, random_state=666)
svc = RBFKernelSVC()
svc.fit(X, y)
plt.figure(figsize=(10, 8))
plot_decision_boundary(svc, axis=[-1.5, 2.5, -1.0, 1.5])
plt.scatter(X[y == 0, 0], X[y == 0, 1], color="blue")
plt.scatter(X[y == 1, 0], X[y == 1, 1], color="red")
plt.show()
结果貌似和多项式核没有什么区别,这是因为gamma取的值还比较 小,我们试试将gamma取不同的值看看。
svc = RBFKernelSVC(gamma=100) # gamma取100的时候
svc.fit(X, y)
plt.figure(figsize=(10, 8))
plot_decision_boundary(svc, axis=[-1.5, 2.5, -1.0, 1.5])
plt.scatter(X[y == 0, 0], X[y == 0, 1], color="blue")
plt.scatter(X[y == 1, 0], X[y == 1, 1], color="red")
plt.show()
我们再来回忆一下,这个gamma的取值。gamma取值越大,相应的图形就越窄。那么gamma取值过大,决策边界就相当于在每一个样本点周围都形成了类似于正态分布的图案。因此我们的模型判断必须在这样的决策边界内,我们才判断为蓝色的点,但是显然可以看出,我们的这个模型过拟合了。
如果看不出来,我们gamma取的再大一些
svc = RBFKernelSVC(gamma=1000) # gamma取1000的时候
svc.fit(X, y)
plt.figure(figsize=(10, 8))
plot_decision_boundary(svc, axis=[-1.5, 2.5, -1.0, 1.5])
plt.scatter(X[y == 0, 0], X[y == 0, 1], color="blue")
plt.scatter(X[y == 1, 0], X[y == 1, 1], color="red")
plt.show()
显然此时就更夸张了,过拟合的太严重了。我们减小gamma
svc = RBFKernelSVC(gamma=10) # gamma取10的时候
svc.fit(X, y)
plt.figure(figsize=(10, 8))
plot_decision_boundary(svc, axis=[-1.5, 2.5, -1.0, 1.5])
plt.scatter(X[y == 0, 0], X[y == 0, 1], color="blue")
plt.scatter(X[y == 1, 0], X[y == 1, 1], color="red")
plt.show()
可以看到和gamma取100不同,每一个点对应的图案都融合在了一起。很好的拟合了数据,可以看到当gamma过大会发生过拟合,但如果gamma过小,会怎么样呢?
svc = RBFKernelSVC(gamma=0.1) # gamma取0.1的时候
svc.fit(X, y)
plt.figure(figsize=(10, 8))
plot_decision_boundary(svc, axis=[-1.5, 2.5, -1.0, 1.5])
plt.scatter(X[y == 0, 0], X[y == 0, 1], color="blue")
plt.scatter(X[y == 1, 0], X[y == 1, 1], color="red")
plt.show()
显然此时就欠拟合了,决策边界和一条直线没什么区别了。
9.SVM解决回归问题
回忆一下回归算法,回归算法是找到一条直线尽可能的拟合这些点。而SVM解决回归问题,则是支撑向量对应的两根直线要包含尽可能多的数据点,然后以中间的决策边界作为拟合那些样本点的直线。可以看到这和SVM解决分类问题实际上是相反的,SVM解决分类问题,要求尽可能的分开,也就是支撑向量构成的两根直线不能有任何的数据点,但SVM解决分类问题,则是希望包含尽可能多的数据点。那么怎么才能包含尽可能多的数据点呢?因此任意一边的支撑向量组成直线和决策边界在y轴方向的距离ε便成了一个超参数。
下面我们就是用sklearn中提供SVM解决回归问题
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import load_boston
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.svm import LinearSVR # SVR,支撑向量的方式解决回归问题。
#from sklearn.svm import SVR # 当然还可以import SVR,和SVC一样,可以传入核函数
def StandardLinearSVR(epsilon=0.1):
return Pipeline([
("std_scaler", StandardScaler()),
("linear", LinearSVR(epsilon=epsilon))
])
boston = load_boston()
X = boston.data
y = boston.target
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=666)
svr = StandardLinearSVR()
svr.fit(X_train, y_train)
print(svr.score(X_test, y_test)) # 0.6361042373115651
当然分类效果不是很理想,但是我们还有很多的超参数没有调节,比如C的值,或者我们还可以使用SVR,指定kernel为多项式核或者高斯核,调节gamma等等。这里不介绍了。