支持向量机(Support Vector Machine,SVM)是一种常用的机器学习算法,用于分类和回归问题。SVM的基本思想是寻找一个最优的超平面,将不同类别的数据样本分隔开来。设样本点为
(
x
i
,
y
i
)
(
i
=
1
,
⋯
,
n
)
(x_i,y_i)(i = 1,\cdots,n)
(xi,yi)(i=1,⋯,n) ,其中
x
i
∈
R
m
x_i\in \mathbb{R}^m
xi∈Rm ,标签
y
i
∈
{
1
,
−
1
}
y_i\in\{1, -1\}
yi∈{1,−1} ,线性分类面方程为
w
T
x
+
b
=
0
,
w
∈
R
m
,
b
∈
R
w^Tx + b = 0, w\in\mathbb{R}^m, b\in\mathbb{R}
wTx+b=0,w∈Rm,b∈R 。SVM希望找到超平面参数
w
,
b
w, b
w,b,满足如下的优化问题
$$\min \frac{1}{2} w^Tw \
\text{s.t.} y_i(w^Tx_i + b) \geq 1$$
这是一个凸的二次规划问题,因此一定有最优解。引入对偶变量
α
i
≥
0
(
i
=
1
,
⋯
,
n
)
\alpha_i\geq 0(i = 1,\cdots,n)
αi≥0(i=1,⋯,n),拉格朗日函数为
L
(
w
,
b
,
α
)
=
1
2
w
T
w
−
∑
i
=
1
n
α
i
[
y
i
(
w
T
x
i
+
b
)
−
1
]
L(w, b, \alpha) = \frac{1}{2} w^Tw - \sum_{i = 1}^n \alpha_i[y_i(w^Tx_i + b) - 1]
L(w,b,α)=21wTw−i=1∑nαi[yi(wTxi+b)−1]
KKT条件为
$$\nabla_w L(w, b, \alpha) = w - \sum_{i = 1}^n \alpha_iy_ix_i = 0 \
\nabla_b L(w, b, \alpha) = -\sum_{i = 1}^n \alpha_iy_i = 0 \
\alpha_i \geq 0 \
\alpha_i[y_i(w^Tx_i + b) - 1] = 0 \
将第一个
K
K
T
条件和第二个
K
K
T
条件带入带入拉格朗日函数,我们就得到了对偶问题
将第一个KKT条件和第二个 KKT条件带入带入拉格朗日函数,我们就得到了对偶问题
将第一个KKT条件和第二个KKT条件带入带入拉格朗日函数,我们就得到了对偶问题\max Q(\alpha) = \sum_{i = 1}^n \alpha_i - \frac{1}{2}\sum_{i = 1}^n\sum_{j = 1}^n \alpha_i\alpha_jy_iy_jx_i^Tx_j \
\text{s.t.} \alpha_i \geq 0, \sum_{i = 1}^n \alpha_iy_i = 0$$
这同样是一个凸的二次规划问题。理论上,我们可以通过求解对偶问题得到对偶变量
α
\alpha
α,将其带入KKT条件就可得到超平面的参数。同时根据第四个KKT条件,我们得到
α
i
≠
0
\alpha_i \neq 0
αi=0 处
y
i
(
w
T
x
i
+
b
)
=
1
y_i(w^Tx_i + b) = 1
yi(wTxi+b)=1。这些点恰好位于边界上,因此被称为支持向量。这也是该算法被称为支持向量机的原因。
CVXPY是一个用于凸优化问题建模和求解的Python库。它提供了一种简洁而直观的方式来描述凸优化问题,并使用底层求解器来求解这些问题。我们将用CVXPY实现SVM算法。
CVXPY的安装非常简单。首先确保电脑已配置Python环境。在终端中输入
pip install cvxpy
即可。进入Python Console,输入以下命令
import cvxpy as cp
print(cp.installed_solvers())
如果出现以下输出说明CVXPY安装成功。输出显示了已安装的求解器。
首先我们生成一组线性可分的数据。从均值为 [ − 3 − 3 ] \begin{bmatrix}-3 \\-3 \\\end{bmatrix} [−3−3],协方差矩阵为 [ 2 − 1 − 1 2 ] \begin{bmatrix}2 & -1 \\-1 & 2 \\\end{bmatrix} [2−1−12]的二元正态分布中抽取100个样本作为正例,从均值为 [ 3 3 ] \begin{bmatrix}3 \\3 \\\end{bmatrix} [33],协方差矩阵为 [ 2 − 1 − 1 2 ] \begin{bmatrix}2 & -1 \\-1 & 2 \\\end{bmatrix} [2−1−12]的二元正态分布中抽取100个样本作为负例。核心代码如下。
def load_data():
mean1 = [-3, -3]
sigma1 = [[2, -1], [-1, 2]]
mean2 = [3, 3]
sigma2 = [[2, -1], [-1, 2]]
X1 = np.random.multivariate_normal(mean1, sigma1, 100)
X2 = np.random.multivariate_normal(mean2, sigma2, 100)
X = np.vstack((X1, X2))
y = np.hstack((np.ones(100), -np.ones(100)))
return X, y
画出散点图如下。可见样本点线性可分。
接下来我们进行数据的拟合。为了统一运算,对负例样本乘 -1。设样本矩阵
X
=
[
x
1
,
⋯
,
x
n
]
T
X = [x_1, \cdots, x_n]^T
X=[x1,⋯,xn]T,全一向量
1
n
=
[
1
,
⋯
,
1
]
T
1_n = [1, \cdots, 1]^T
1n=[1,⋯,1]T ,则对偶问题
Q
(
α
)
Q(\alpha)
Q(α)的优化目标可写作
1
n
T
α
−
1
2
(
X
T
α
)
T
(
X
T
α
)
1_n^T\alpha - \frac{1}{2}(X^T\alpha)^T(X^T\alpha)
1nTα−21(XTα)T(XTα)
然后定义优化目标和约束,调用CVXOPT求解器进行求解。核心代码如下。
def fit(X, y):
x = X.copy()
x[y == -1, :] = -x[y == -1, :]
n = x.shape[0]
alpha = cp.Variable(n)
objective = cp.Minimize(0.5 * cp.sum_squares(x.T @ alpha) - np.ones(n) @ alpha)
constraint = [alpha >= 0, y @ alpha == 0]
prob = cp.Problem(objective, constraint)
prob.solve(solver='CVXOPT')
print(f"dual variable = {alpha.value}")
得到对偶变量的最优解后,代入第一个KKT条件可以计算出 w w w。 α i ≠ 0 \alpha_i \neq 0 αi=0的对偶变量对应了支持向量。代入支持向量满足的边界方程可以计算出 b b b。核心代码如下。
w = x.T @ alpha.value
index = np.where(abs(alpha.value) > 1e-3)[0]
print(f"support vector index = {index}")
b = np.mean(y[index] - X[index, :] @ w)
return w, b, index
程序运行结果如下。可见有3个支持向量。其余的对偶变量非常接近0。
dual variable = [ 1.43531769e-09 3.28358675e-11 -4.70081346e-11 ... -1.23193041e-12
1.46991949e-11 -4.72866927e-11]
support vector index = [ 11 42 111]
w = [-0.30023937 -0.26737101], b = 0.007281446816055766
将分界面和支持向量可视化,可见SVM确实找到了最优分类面。
附程序完整代码。
import numpy as np
from matplotlib import pyplot as plt
import cvxpy as cp
def load_data():
mean1 = [-3, -3]
sigma1 = [[2, -1], [-1, 2]]
mean2 = [3, 3]
sigma2 = [[2, -1], [-1, 2]]
X1 = np.random.multivariate_normal(mean1, sigma1, 100)
X2 = np.random.multivariate_normal(mean2, sigma2, 100)
X = np.vstack((X1, X2))
y = np.hstack((np.ones(100), -np.ones(100)))
return X, y
def fit(X, y):
x = X.copy()
x[y == -1, :] = -x[y == -1, :]
n = x.shape[0]
alpha = cp.Variable(n)
objective = cp.Minimize(0.5 * cp.sum_squares(x.T @ alpha) - np.ones(n) @ alpha)
constraint = [alpha >= 0, y @ alpha == 0]
prob = cp.Problem(objective, constraint)
prob.solve(solver='CVXOPT')
print(f"dual variable = {alpha.value}")
w = x.T @ alpha.value
index = np.where(abs(alpha.value) > 1e-3)[0]
print(f"support vector index = {index}")
b = np.mean(y[index] - X[index, :] @ w)
return w, b, index
if __name__=="__main__":
X, y = load_data()
w, b, index = fit(X, y)
print(f"w = {w}, b = {b}")
plt.scatter(X[y == 1, 0], X[y == 1, 1])
plt.scatter(X[y == -1, 0], X[y == -1, 1])
plt.scatter(X[index, 0], X[index, 1], marker='*', s=100)
plt.plot((-4, 4), ((-b + 4 * w[0]) / w[1], (-b - 4 * w[0]) / w[1]))
plt.show()