先把全部代码放在这,注释写的很详细,亲测可用。想继续看我废话(×)讲解代码(√)的请往下看↓
import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
class SMO:
def __init__(self, X, y, C, kernel, tol, max_passes=10):
self.X = X # 样本特征 m*n m个样本 n个特征
self.y = y # 样本标签 m*1
self.C = C # 惩罚因子, 用于控制松弛变量的影响
self.kernel = kernel # 核函数
self.tol = tol # 容忍度
self.max_passes = max_passes # 最大迭代次数
self.m, self.n = X.shape
self.alpha = np.zeros(self.m)
self.b = 0
self.w = np.zeros(self.n)
# 计算核函数
def K(self, i, j):
if self.kernel == 'linear':
return np.dot(self.X[i].T, self.X[j])
elif self.kernel == 'rbf':
gamma = 0.5
return np.exp(-gamma * np.linalg.norm(self.X[i] - self.X[j]) ** 2)
else:
raise ValueError('Invalid kernel specified')
def predict(self, X):
pred = np.zeros_like(X[:, 0])
pred = np.dot(X_test, self.w) + self.b
return np.sign(pred)
def train(self):
"""
训练模型
:return:
"""
passes = 0
while passes < self.max_passes:
num_changed_alphas = 0
for i in range(self.m):
# 计算E_i, E_i = f(x_i) - y_i, f(x_i) = w^T * x_i + b
# 计算误差E_i
E_i = 0
for ii in range(self.m):
E_i += self.alpha[ii] * self.y[ii] * self.K(ii, i)
E_i += self.b - self.y[i]
# 检验样本x_i是否满足KKT条件
if (self.y[i] * E_i < -self.tol and self.alpha[i] < self.C) or (self.y[i] * E_i > self.tol and self.alpha[i] > 0):
# 随机选择样本x_j
j = np.random.choice(list(range(i)) + list(range(i + 1, self.m)), size=1)[0]
# 计算E_j, E_j = f(x_j) - y_j, f(x_j) = w^T * x_j + b
# E_j用于检验样本x_j是否满足KKT条件
E_j = 0
for jj in range(self.m):
E_j += self.alpha[jj] * self.y[jj] * self.K(jj, j)
E_j += self.b - self.y[j]
alpha_i_old = self.alpha[i].copy()
alpha_j_old = self.alpha[j].copy()
# L和H用于将alpha[j]调整到[0, C]之间
if self.y[i] != self.y[j]:
L = max(0, self.alpha[j] - self.alpha[i])
H = min(self.C, self.C + self.alpha[j] - self.alpha[i])
else:
L = max(0, self.alpha[i] + self.alpha[j] - self.C)
H = min(self.C, self.alpha[i] + self.alpha[j])
# 如果L == H,则不需要更新alpha[j]
if L == H:
continue
# eta: alpha[j]的最优修改量
eta = 2 * self.K(i, j) - self.K(i, i) - self.K(j, j)
# 如果eta >= 0, 则不需要更新alpha[j]
if eta >= 0:
continue
# 更新alpha[j]
self.alpha[j] -= (self.y[j] * (E_i - E_j)) / eta
# 根据取值范围修剪alpha[j]
self.alpha[j] = np.clip(self.alpha[j], L, H)
# 检查alpha[j]是否只有轻微改变,如果是则退出for循环
if abs(self.alpha[j] - alpha_j_old) < 1e-5:
continue
# 更新alpha[i]
self.alpha[i] += self.y[i] * self.y[j] * (alpha_j_old - self.alpha[j])
# 更新b1和b2
b1 = self.b - E_i - self.y[i] * (self.alpha[i] - alpha_i_old) * self.K(i, i) \
- self.y[j] * (self.alpha[j] - alpha_j_old) * self.K(i, j)
b2 = self.b - E_j - self.y[i] * (self.alpha[i] - alpha_i_old) * self.K(i, j) \
- self.y[j] * (self.alpha[j] - alpha_j_old) * self.K(j, j)
# 根据b1和b2更新b
if 0 < self.alpha[i] and self.alpha[i] < self.C:
self.b = b1
elif 0 < self.alpha[j] and self.alpha[j] < self.C:
self.b = b2
else:
self.b = (b1 + b2) / 2
num_changed_alphas += 1
if num_changed_alphas == 0:
passes += 1
else:
passes = 0
# 提取支持向量和对应的参数
idx = self.alpha > 0 # 支持向量的索引
# SVs = X[idx]
selected_idx = np.where(idx)[0]
SVs = X[selected_idx]
SV_labels = y[selected_idx]
SV_alphas = self.alpha[selected_idx]
# 计算权重向量和截距
self.w = np.sum(SV_alphas[:, None] * SV_labels[:, None] * SVs, axis=0)
self.b = np.mean(SV_labels - np.dot(SVs, self.w))
print("w", self.w)
print("b", self.b)
def score(self, X, y):
predict = self.predict(X)
print("predict", predict)
print("target", y)
return np.mean(predict == y)
# 加载鸢尾花数据集
iris = datasets.load_iris()
X = iris.data
y = iris.target
y[y != 0] = -1
y[y == 0] = 1
# 为了方便可视化,只取前两个特征,并且只取两类
# X = X[y < 2, :2]
# y = y[y < 2]
# # 分别画出类别 0 和 1 的点
plt.scatter(X[y != 0, 0], X[y != 0, 1], color='red')
plt.scatter(X[y == 0, 0], X[y == 0, 1], color='blue')
plt.show()
# 数据预处理,将特征进行标准化,并将数据划分为训练集和测试集
scaler = StandardScaler()
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=3706)
X_train_std = scaler.fit_transform(X_train)
# 创建SVM对象并训练模型
svm = SMO(X_train_std, y_train, C=0.6, kernel='linear', tol=0.001)
svm.train()
# 预测测试集的结果并计算准确率
X_test_std = scaler.transform(X_test)
accuracy = svm.score(X_test_std, y_test)
print('正确率: {:.2%}'.format(accuracy))
参考:
通俗易懂举栗子–怎么理解支持向量机(SVM)?
【ML】支持向量机(SVM)从入门到放弃再到掌握
SVM原理篇之手撕SVM_chenchenchenchenyi的博客-CSDN博客
支持向量机(Support Vector Machines,SVM)是一种有监督的机器学习算法,可以用于回归和分类任务,主要用于分类。SVM算法在做的事情就是找到一个最优的分类边界,把不同类别的样本分开。
对于二维空间中的点,假设我们有了将数据点划分为两类的决策边界,那么距离边界较远的点将很容易的被划分为某一类,可非常靠近边界的点要如何被分类呢?考虑下图中的A、B两点:
现在我们可以清楚的看到,B点属于绿点类,因为它远离决策线。但A点呢?它属于哪一类呢?看起来它好像也属于绿点类,但事实可能并非如此,如果决策边界发生变化怎么办呢?如下图:
如果我们将灰色的线作为决策边界的话,那么点A将被归为蓝色类点,如果我们任务红色线为决策边界的话,那A点就被划分为绿色的点。这就是麻烦所在了……为了解决这种模糊不定,SVM就需要引入” margin” 的概念了。
支持向量(support vector)距离超平面最近的几个训练样本点使上式的等号成立,它们被称为支持向量。两个异类支持向量到超平面的距离之和称为间隔。
” margin” 是使得两个类之间边界最大化的超平面,怎么理解呢?最靠近彼此的绿点和蓝点应当处在黄色区域的边界线上(观察上图),也就是这个黄色区域会不断的扩大,直到遇到各类的第一个点,然后停下。然后真正的决策边界就位于两个边界的中间(红线)。
SVM就是要找到具有“最大间隔”的划分超平面。
用数学公式表示就是:
我们计算每一个样本数据点的 γ ,定义 M 是其中我们能得到的最小的 γ ,在一些论文文献中, M 被称为”geometric margin”。
最终我们应当选择M最大的超平面作为最优超平面。
为了找到最优超平面的w和b值,我们需要解决以下优化问题,约束条件是任何一个样本的 γ 都应大于或等于M:
我们在前面知道
,那么上面的约束条件就可以改为:
可以理解的一点,不论我们怎么缩放w和b,我们的约束条件是不会改变的,既然如此,那就让我们继续缩放w和b(平面系数等比缩放是不会改变平面本身的!),使得F=1,上述问题就可以重新写为:
上面的最大值问题就等效于下面的最小值问题:
上面的最小值问题又可以等效于下面的最小值问题(对于范数,平方后乘常数是不会改变大小关系的):
上面的式子就是SVM算法的优化问题,也被称为凸二次优化问题。这是SVM的基本型。
那么我们需要做的就是在这个限制条件下,不断的去更新参数w,b,从而寻找到我们要的最优SVM分类超平面。
通常我们需要求解的最优化问题有如下几类:
(a) 无约束优化问题,可以写为:
(b) 有等式约束的优化问题,可以写为:
© 有不等式约束的优化问题,可以写为:
对于第(a)类的优化问题,尝尝使用的方法就是费马大定理(Fermat),即使用求取函数f(x)的导数,然后令其为零,可以求得候选最优值,再在这些候选值中验证;如果是凸函数,可以保证是最优解。这也就是我们高中经常使用的求函数的极值的方法。
对于第(b)类的优化问题,常常使用的方法就是拉格朗日乘子法(Lagrange Multiplier) ,即把等式约束h_i(x)用一个系数与f(x)写为一个式子,称为拉格朗日函数,而系数称为拉格朗日乘子。通过拉格朗日函数对各个变量求导,令其为零,可以求得候选值集合,然后验证求得最优值。
对于第©类的优化问题,常常使用的方法就是KKT条件。同样地,我们把所有的等式、不等式约束与f(x)写为一个式子,也叫拉格朗日函数,系数也称拉格朗日乘子,通过一些条件,可以求出最优值的必要条件,这个条件称为KKT条件。
必要条件和充要条件如果不理解,可以看下面这句话:
什么是凸集?
凸集。在凸几何中,凸集(convex set)是在)凸组合下闭合的放射空间的子集。看一幅图可能更容易理解:
左右量图都是一个集合。**如果集合中任意2个元素连线上的点也在集合中,那么这个集合就是凸集。**显然,上图中的左图是一个凸集,上图中的右图是一个非凸集。
凸函数的定义也是如此,其几何意义表示为函数任意两点连线上的值大于对应自变量处的函数值。若这里凸集C即某个区间L,那么,设函数f为定义在区间L上的函数,若对L上的任意两点x1,x2和任意的实数λ,λ属于(0,1),总有:
对于我们的目标函数:
很显然,它是一个凸函数。所以,可以使用求解凸函数的方法求取最优解。
现在让我们再看一下我们的最优化问题:
根据2中提到的优化问题,了解到我们的最优化问题属于第©类问题。因为,在学习求解最优化问题之前,我们还要学习两个东西:拉格朗日函数和KKT条件。
下面,进行第一步:将有约束的原始目标函数转换为无约束的新构造的拉格朗日目标函数
公式变形如下:
其中αi是拉格朗日乘子,αi大于等于0,是我们构造新目标函数时引入的系数变量(我们自己设置)。
首先固定α,要让L(w,b,α)关于w和b最小化,我们分别对w和b偏导数,令其等于0,即:
将上述结果带回L(w,b,α)得到:
从上面的最后一个式子,我们可以看出,此时的L(w,b,α)函数只含有一个变量,即αi。
我们求解外侧的最大值,从上面的式子得到
现在我们的优化问题变成了如上的形式。对于这个问题,我们有更高效的优化算法,即序列最小优化(SMO)算法。我们通过这个优化算法能得到α,再根据α,我们就可以求解出w和b,进而求得我们最初的目的:找到超平面,即”决策平面”。
KKT条件的全称是Karush-Kuhn-Tucker条件,KKT条件是说最优值条件必须满足以下条件:
从深入理解拉格朗日乘子法(Lagrange Multiplier) 和KKT条件可知,上述过程满足KKT条件。
用于节省开销。先固定ai之外的所有参数,然后求ai上的极值。SMO每次选择两个变量ai和aj,并固定其它参数。
SMO算法的步骤:
步骤1:计算误差:
步骤2:计算上下界L和H:
步骤3:计算η:
步骤4:更新αj:
步骤5:根据取值范围修剪αj:
步骤6:更新αi:
步骤7:更新b1和b2:
步骤8:根据b1和b2更新b:
在之前的讨论中,都是假设训练样本线性可分。但是在现实任务中,原始样本空间也许并不存在一个能正确划分两类样本的超平面。对这样的问题,可将样本从原始空间映射到一个更高维的特征空间,使得样本在这个特征空间里线性可分。
常见核函数
多项核中,d=1时,退化为线性核;
⾼斯核亦称为RBF核。
线性核和多项式核:
这两种核的作⽤也是⾸先在属性空间中找到⼀些点,把这些点当做基本点,而核函数的作⽤就是找与该点距离和⻆度满⾜某种关系的样本点。
当样本点与该点的夹⻆近乎垂直时,两个样本的欧式⻓度必须⾮常⻓才能保证满⾜线性核函数⼤于0;⽽当样本点与基本点的⽅向相同时,⻓度就不必很⻓;⽽当⽅向相反时,核函数值就是负的,被判为反类。即,它在空间上划分出⼀个梭形,按照梭形来进⾏正反类划分。
RBF核:
⾼斯核函数就是在属性空间中找到⼀些点,这些点可以是也可以不是样本点,把这些点当做基本点,以这些基本点为圆⼼向外扩展,扩展半径即为带宽,即可划分数据。
换句话说,在属性空间中找到⼀些超圆,⽤这些超圆来判定正反类。
Sigmoid核:
同样地是定义⼀些基本点,
核函数就是将线性核函数经过⼀个tanh函数进⾏处理,把值域限制在了-1到1上。
总之,核函数都是在定义距离,⼤于该距离,判为正,⼩于该距离,判为负。⾄于选择哪⼀种核函数,要根据具体的样本分布情况来确定。
⼀般有如下指导规则:
1) 如果特征的数量很⼤,甚⾄和样本数量差不多时,往往线性可分,这时选⽤LR或者线性核Linear;
2) 如果特征的数量很⼩,样本数量正常,不算多也不算少,这时选⽤RBF核;
3) 如果特征的数量很⼩,⽽样本的数量很⼤,这时⼿动添加⼀些特征,使得线性可分,然后选⽤LR或者线性核Linear;
4) 多项式核⼀般很少使⽤,效率不⾼,结果也不优于RBF;
5) Linear核参数少,速度快;RBF核参数多,分类结果⾮常依赖于参数,需要交叉验证或⽹格搜索最佳参数,⽐较耗时;
6)应⽤最⼴的应该就是RBF核,⽆论是⼩样本还是⼤样本,⾼维还是低维等情况,RBF核函数均适⽤。
直接使用sklearn的datasets包导入鸢尾花数据集,令标签不为0的数据的标签为-1。
iris = datasets.load_iris()
X = iris.data
y = iris.target
y[y != 0] = -1
y[y == 0] = 1
数据预处理,将特征进行标准化:
scaler = StandardScaler()
采用9:1的比例划分训练集和测试集:
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=3706)
X_train_std = scaler.fit_transform(X_train)
为了方便可视化,只取前两个特征,并且只取两类,得到如下的数据分布:
iris = datasets.load_iris()
X = iris.data
Y = iris.target
X = X[Y < 2, :2] # 只取y<2的类别,也就是0 1 并且只取前两个特征
Y = Y[Y < 2] # 只取y<2的类别
\# 分别画出类别 0 和 1 的点
plt.scatter(X[Y == 0, 0], X[Y == 0, 1], color='red')
plt.scatter(X[Y == 1, 0], X[Y == 1, 1], color='blue')
plt.show()
图示说明,我们可以的训练样本应该是线性可分的,但为了避免特殊情况,后续还是加入了核函数。
我们在这里定义了两种核函数:线性核函数和高斯核函数:
计算核函数
def K(self, i, j):
if self.kernel == 'linear':
return np.dot(self.X[i].T, self.X[j])
elif self.kernel == 'rbf':
gamma = 0.5
return np.exp(-gamma * np.linalg.norm(self.X[i] - self.X[j]) ** 2)
else:
raise ValueError('Invalid kernel specified')
先定义一个大循环,循环逐轮更新alpha和b的值。循环结束后,用更新后的alpha值计算权重向量****w****和截距b。
passes = 0
while passes < self.max_passes:
……
# 提取支持向量和对应的参数
idx = self.alpha > 0 # 支持向量的索引
# SVs = X[idx]
selected_idx = np.where(idx)[0]
SVs = X[selected_idx]
SV_labels = y[selected_idx]
SV_alphas = self.alpha[selected_idx]
# 计算权重向量和截距
self.w = np.sum(SV_alphas[:, None] * SV_labels[:, None] * SVs, axis=0)
self.b = np.mean(SV_labels - np.dot(SVs, self.w))
print("w", self.w)
print("b", self.b)
再定义一个小循环,更新每一个alpha[i]的值:
for i in range(self.m):
……
if num_changed_alphas == 0:
passes += 1
else:
passes = 0
for循环里,根据SMO算法的过程训练模型:
①计算误差
计算误差后,要看是否满足一3(2)中提到的KKT条件中的几个条件。如果满足,就继续进行;不满足就“continue”进行下一个循环步。代码如下:
# 计算误差E_i
E_i = 0
for ii in range(self.m):
E_i += self.alpha[ii] * self.y[ii] * self.K(ii, i)
E_i += self.b - self.y[i]
# 检验样本x_i是否满足KKT条件
if (self.y[i] * E_i < -self.tol and self.alpha[i] < self.C) or (self.y[i] * E_i > self.tol and self.alpha[i] > 0):
# 随机选择样本x_j
j = np.random.choice(list(range(i)) + list(range(i + 1, self.m)), size=1)[0]
# 计算E_j, E_j = f(x_j) - y_j, f(x_j) = w^T * x_j + b
# E_j用于检验样本x_j是否满足KKT条件
E_j = 0
for jj in range(self.m):
E_j += self.alpha[jj] * self.y[jj] * self.K(jj, j)
E_j += self.b - self.y[j]
alpha_i_old = self.alpha[i].copy()
alpha_j_old = self.alpha[j].copy()
②计算上下界L和H
L和H用于将alpha[j]调整到[0, C]之间,根据公式就可以计算得到。如果L == H,则不需要更新alpha[j]。代码如下:
if self.y[i] != self.y[j]:
L = max(0, self.alpha[j] - self.alpha[i])
H = min(self.C, self.C + self.alpha[j] - self.alpha[i])
else:
L = max(0, self.alpha[i] + self.alpha[j] - self.C)
H = min(self.C, self.alpha[i] + self.alpha[j])
# 如果L == H,则不需要更新alpha[j]
if L == H:
continue
③计算η
eta是alpha[j]的最优修改量,如果eta >= 0, 则不需要更新alpha[j]。代码如下:
eta = 2 * self.K(i, j) - self.K(i, i) - self.K(j, j)
if eta >= 0:
continue
④更新αj
根据公式更新alpha[j]。代码如下:
self.alpha[j] -= (self.y[j] * (E_i - E_j)) / eta
⑤根据取值范围修剪αj
修剪αj后要检查alpha[j]是否只有轻微改变,如果是,则退出for循环。代码如下:
self.alpha[j] = np.clip(self.alpha[j], L, H)
# 检查alpha[j]是否只有轻微改变,如果是则退出for循环
if abs(self.alpha[j] - alpha_j_old) < 1e-5:
continue
⑥更新αi
根据公式更新alpha[i]。代码如下:
self.alpha[i] += self.y[i] * self.y[j] * (alpha_j_old - self.alpha[j])
⑦更新b1和b2
根据公式更新b1和b2。代码如下:
b1 = self.b - E_i - self.y[i] * (self.alpha[i] - alpha_i_old) * self.K(i, i)
- self.y[j] * (self.alpha[j] - alpha_j_old) * self.K(i, j)
b2 = self.b - E_j - self.y[i] * (self.alpha[i] - alpha_i_old) * self.K(i, j)
- self.y[j] * (self.alpha[j] - alpha_j_old) * self.K(j, j)
⑧根据b1和b2更新b
根据取值范围更新b值。代码如下:
if 0 < self.alpha[i] and self.alpha[i] < self.C:
self.b = b1
elif 0 < self.alpha[j] and self.alpha[j] < self.C:
self.b = b2
else:
self.b = (b1 + b2) / 2
输入预测的矩阵X,根据w^T X+b预测各条数据对应的类别。代码如下:
def predict(self, X):
pred = np.zeros_like(X[:, 0])
pred = np.dot(X_test, self.w) + self.b
return np.sign(pred)
通过比较模型测试集中各条数据预测的类别和标签是否一致,可以得到准确率accuracy的值。评估代码如下:
def score(self, X, y):
predict = self.predict(X)
print("predict", predict)
print("target", y)
return np.mean(predict == y)
预测测试集的结果并计算准确率:
X_test_std = scaler.transform(X_test)
accuracy = svm.score(X_test_std, y_test)
print('正确率: {:.2%}'.format(accuracy))
多跑了几次,得到的结果输出如下:
效果都很不错,说明这个SVM模型性能良好。