分步实现机器学习 V - 支持向量机指南
易于实现的机器学习
引言
支持向量机(SVM)是基于特征空间最大间隔的分类器。SVM的学习策略是最大化间隔,这可以转化为一个凸二次规划问题。在学习SVM算法之前,我先介绍一些术语。
函数间隔定义为:对于给定的训练集T
和超平面(w,b)
,超平面(w,b)
与样本(xi, yi)
之间的函数间隔为
超平面(w,b)
与训练集T之间的函数间隔是的最小值。
函数间隔表示分类结果的置信度。如果超参数(w,b)
成比例地变化,例如,将(w,b)
变为(2w,2b)
。尽管超平面没有改变,但函数间隔会加倍。因此,我们对w
施加一些约束以使超平面明确化,例如归一化||w|| = 1
。然后,该间隔被称为几何间隔:对于给定的训练集T
和超平面(w,b)
,超平面(w,b)
与样本(xi, yi)
之间的函数间隔为
类似地,超平面(w,b)
与训练集T之间的几何间隔是的最小值。
现在,我们可以总结函数间隔和几何间隔之间的关系。
SVM模型
SVM模型由优化问题、优化算法和分类组成。
优化问题
当数据集线性可分时,支持向量是离超平面最近的样本,如下图所示。
H1
和H2
上的样本是支持向量。H1
和H2
之间的距离称为硬间隔。那么,SVM的优化问题是
当数据集不可线性分割时,训练集中的一些样本不满足函数间隔大于或等于1
的条件。为了解决这个问题,我们为每个样本 (xi, yi)
添加一个松弛变量 。然后,约束条件为
同时,为每个松弛变量添加成本。目标函数为
其中C
是惩罚系数。当C
较大时,误分类的惩罚会增加,而误分类的惩罚会减少。那么,SVM的优化问题是
支持向量位于间隔边界上或边界与超平面之间,如下图所示。在这种情况下,间隔称为软间隔。
当数据集不可线性分割时,需要应用核技巧将数据从原始空间转换为特征空间。用于核技巧的函数称为核函数,定义为
其中 是从输入空间到特征空间的映射,即:
核技巧的代码如下所示:
def kernelTransformation(self, data, sample, kernel):
sample_num, feature_dim = np.shape(data)
K = np.zeros([sample_num])
if kernel == "linear": # linear function
K = np.dot(data, sample.T)
elif kernel == "poly": # polynomial function
K = (np.dot(data, sample.T) + self.c) ** self.n
elif kernel == "sigmoid": # sigmoid function
K = np.tanh(self.g * np.dot(data, sample.T) + self.c)
elif kernel == "rbf": # Gaussian function
for i in range(sample_num):
delta = data[i, :] - sample
K[i] = np.dot(delta, delta.T)
K = np.exp(-self.g * K)
else:
raise NameError('Unrecognized kernel function')
return K
应用核技巧后,SVM的优化问题为
其中 是拉格朗日乘子。
优化算法
使用传统的凸二次规划算法可以解决SVM优化问题。但是,当训练集很大时,算法会花费很长时间。为了解决这个问题,Platt提出了一种称为序列最小最优化(SMO)的高效算法。
SMO是一种启发式算法。当所有变量都满足KKT条件时,优化问题就解决了。否则,选择两个变量,固定其他变量,并构造一个只有这两个变量的凸二次规划问题。该问题有两个变量:一个选择违反KKT条件的alpha,另一个由约束决定。设 为变量,固定
。因此,
由下式计算:
如果 确定,则
也随之确定。在SMO的每次迭代中,都会更新两个变量,直到问题解决。然后,SVM的优化问题为:
当只有一个变量时,这是一个简单的二次规划问题,如下所示:
因为约束是:
当 时,
当 时,因为
,所以
。
优化后的值 遵循:
其中 L
和 H
是对角线的下界和上界,计算方法如下:
未裁剪的优化值 遵循:
其中 E1 和 E2 是预测值 g(x)
与真实值之间的差值。g(x)
定义为:
至此,我们得到了 的可行解。
SMO中有两个循环,即外循环和内循环。
在外循环中,选择违反KKT条件的 alpha,即:
首先,搜索满足 的样本。如果所有样本都满足条件,则搜索整个数据集。
在内循环中,首先搜索使得 最大的
。如果选择的
没有足够减小,则首先搜索边界上的样本。如果选择的
减小得足够多,则停止搜索。否则,搜索整个数据集。如果在搜索整个数据集后找到一个可行的
,我们将选择一个新的
。
在每次迭代中,我们通过以下方式更新b
:
为了方便起见,我们必须存储 的值,计算方法如下:
搜索和更新的代码如下所示:
def innerLoop(self, i):
Ei = self.calculateErrors(i)
if self.checkKKT(i):
j, Ej = self.selectAplha2(i, Ei) # select alpha2 according to alpha1
# copy alpha1 and alpha2
old_alpha1 = self.alphas[i]
old_alpha2 = self.alphas[j]
# determine the range of alpha2 L and H in page of 126
# if y1 != y2 L = max(0, old_alpha2 - old_alpha1),
H = min(C, C + old_alpha2 - old_alpha1)
# if y1 == y2 L = max(0, old_alpha2 + old_alpha1 - C),
H = min(C, old_alpha2 + old_alpha1)
if self.train_label[i] != self.train_label[j]:
L = max(0, old_alpha2 - old_alpha1)
H = min(self.C, self.C + old_alpha2 - old_alpha1)
else:
L = max(0, old_alpha2 + old_alpha1 - self.C)
H = min(self.C, old_alpha2 + old_alpha2)
if L == H:
# print("L == H")
return 0
# calculate eta in page of 127 Eq.(7.107)
# eta = K11 + K22 - 2K12
K11 = self.K[i, i]
K12 = self.K[i, j]
K21 = self.K[j, i]
K22 = self.K[j, j]
eta = K11 + K22 - 2 * K12
if eta <= 0:
# print("eta <= 0")
return 0
# update alpha2 and its error in page of 127 Eq.(7.106) and Eq.(7.108)
self.alphas[j] = old_alpha2 + self.train_label[j]*(Ei - Ej)/eta
self.alphas[j] = self.updateAlpha2(self.alphas[j], L, H)
new_alphas2 = self.alphas[j]
self.upadateError(j)
# update the alpha1 and its error in page of 127 Eq.(7.109)
# new_alpha1 = old_alpha1 + y1y2(old_alpha2 - new_alpha2)
new_alphas1 = old_alpha1 + self.train_label[i] *
self.train_label[j] * (old_alpha2 - new_alphas2)
self.alphas[i] = new_alphas1
self.upadateError(i)
# determine b in page of 130 Eq.(7.115) and Eq.(7.116)
# new_b1 = -E1 - y1K11(new_alpha1 - old_alpha1) -
y2K21(new_alpha2 - old_alpha2) + old_b
# new_b2 = -E2 - y1K12(new_alpha1 - old_alpha1) -
y2K22(new_alpha2 - old_alpha2) + old_b
b1 = - Ei - self.train_label[i] * K11 * (old_alpha1 - self.alphas[i]) -
self.train_label[j] * K21 * (old_alpha2 - self.alphas[j]) + self.b
b2 = - Ej - self.train_label[i] * K12 * (old_alpha1 - self.alphas[i]) -
self.train_label[j] * K22 * (old_alpha2 - self.alphas[j]) + self.b
if (self.alphas[i] > 0) and (self.alphas[i] < self.C):
self.b = b1
elif (self.alphas[j] > 0) and (self.alphas[j] < self.C):
self.b = b2
else:
self.b = (b1 + b2)/2.0
return 1
else:
return 0
分类
我们可以使用优化后的参数进行预测,计算公式为:
for i in range(test_num):
kernel_data = self.kernelTransformation(support_vectors, test_data[i, :], self.kernel)
probability[i] = np.dot(kernel_data.T, np.multiply
(support_vectors_label, support_vectors_alphas)) + self.b
if probability[i] > 0:
prediction[i] = 1
else:
prediction[i] = -1
结论与分析
SVM比之前的算法更复杂。在本文中,我们简化了搜索过程,使其更容易理解。最后,我们将我们的SVM与Sklearn中的SVM进行比较,并显示检测性能如下:
检测性能比sklearn的略差,这是因为我们的SVM中的SMO比sklearn的更简单。
本文相关的代码和数据集可以在 MachineLearning 中找到。