[关闭]
@kerrwy 2017-09-19T09:27:37.000000Z 字数 12523 阅读 2123

来理解Support Vector Machine吧!


本文内容参考机器学习与自然语言算法公众号,知乎,周志华Machine Learning 等整理而来,原版资料已在文末以Ref标明,如有疏忽敬请谅解。


第一部分:感知机的介绍

感知机是个相当简单的模型,但它既可以发展成支持向量机(通过简单地修改一下损失函数)、又可以发展成神经网络(通过简单地堆叠),所以它也拥有一定的地位。
在此,我们先讨论二分类问题,样本类别为正样本负样本

1. 感知机能干嘛

什么叫线性可分?在二维平面上、线性可分意味着能用一条线将正负样本分开,在三维空间中、线性可分意味着能用一个平面将正负样本分开。
直观的显示可分与不可分就是这样:

  • // gif图片不可显示 请于附加的word中查看

2. 搭建一个感知机模型

为了搭建感知机,我们先要知道数学上的的线性可分是指什么。

cmd-markdown-logo
cmd-markdown-logo

3. 训练感知机,更新参数

为了保证收敛性,我们需要把w初始化为n维0向量,将b初始化为标量0。

  1. import numpy as np
  2. class Perceptron:
  3. def __init__(self):
  4. self.w = self.b = None
  5. def fit(self, x, y, lr=0.01, epoch=1000):
  6. # 将输入的样本x和标签y转化为NumpyN维数组对象
  7. x, y = np.asarray(x, np.float32), np.asarray(y, np.float32)
  8. self.w = np.zeros(x.shape[1])
  9. self.b = 0.

fit函数中的lr和epoch分别代表梯度下降法中的学习速率和迭代上限。
梯度下降法的步骤:
- 对损失函数求导得到梯度值。
- 梯度是函数值增长最快的方向,我们想让损失函数最小化,就要让参数沿着梯度的反方向更新。
那么感知机的损失函数是什么呢??
我们知道:


综上:


因为只有错分类的点才贡献梯度,所以在训练时,我们挑选一个使最大的样本,如果被正确分类的话,说明所有样本均已正确分类,则停止模型的训练。


代码体现损失函数减小和参数更新,实现线性可分

  1. import numpy as np
  2. for _ in range(epoch):
  3. # 先计算w*x+b
  4. y_pred = x.dot(self.w) + self.b
  5. # 选出使损失函数最大的样本
  6. idx = np.argmax(np.maximun(0,-y*y_pred)) # SVM这不同,还有间隔的最大化
  7. # 若该样本被正确分类,则停止模型训练
  8. if y[idx] * y_pred[idx] > 0: # SVM这不同
  9. break
  10. # 否则,让参数沿着负梯度方向走一步
  11. delta = lr * y[idx]
  12. self.w += delta * x[idx]
  13. self.b += delta

第二部分:介绍LinearSVM

在这里,我们先讲SVM的原始版本,其本身只是一个线性模型,只有在应用了核方法后,SVM才会升级成为非线性模型。第三部分我们再讲解带核技巧的SVM。

1. SVM与感知机

第一部分中我们提过,LinearSVM可以说是改了感知机的损失函数而已。感知机中样本被正确分类的标志是,而SVM中样本被正确分类的标志是。为什么是大于1,这个1是怎么来的?我们先来看一下感知机决策面的劣势。
如(word图1)中,感知机要求样本被正确分类,而不要求样本。那么如何从这些很多的决策面中选择一个最好的,使该划分的超平面对训练样本的局部扰动“容忍”性最好。

2. SVM模型搭建


其中A,B决定了直线的法线方向。
所以有,样本空间中任意点到超平面的距离可写为


硬间隔最大化
假设存在超平面能将训练样本正确分类,即有:对于D,若,则;若,则

定义一个很好的分类:令


如下图,使
cmd-markdown-logo
所以两个异类支持向量到超平面的距离之和为
,称为“间隔”。
欲使划分的超平面对训练样本的局部扰动“容忍”性最好,需要使间隔最大。故得到如下的约束优化模型。(见word图2)
软间隔最大化
但是,对于上面的问题,线,也就是说的这个约束太硬了(硬间隔)。缓解该问题的一个方法是允许支持向量在一些样本上出错(见下图和word图3),为此对于线性不可分的情况引入“软间隔”的概念(word图4)。
cmd-markdown-logo
返回来,我们再看一下感知机的损失函数是写作:

而SVM的损失函数呢?
刚才看到了:模型的优化目标为

现在对上面的模型进行转化:

为使最小,必有:

也就是
所以,SVM的损失函数就是:

于是综上可以看出,LinearSVM在形式上和感知机的损失函数确实长得很像。

另外:就是hinge损失,也可以写作:

3. LinearSVM的训练

在这里,由于对偶问题的复杂性,我们“偷懒”一下,使用朴素的随机梯度下降(SGD)来求解参数
首先计算损失函数对参数的导数,得到了下降的梯度之后,再进行参数更新。
cmd-markdown-logo

代码实现线性不可分情况

  1. import numpy as np
  2. class LinearSVM:
  3. def __init__(self):
  4. self.w = self.b = None
  5. def fit(self, x, y, lr=0.01, epoch=10000):
  6. # 将输入的样本x和标签y转化为Numpy数组
  7. x, y = np.asarray(x, np.float32), np.asarray(y, np.float32)
  8. # 随机选取batch_size个样本
  9. batch = np.random.choice(len(x), batch_size)
  10. x_batch, y_batch = x[batch], y[batch]
  11. self.w = np.zeros(x_batch.shape[1])
  12. self.b = 0.
  13. for _ in range(epoch):
  14. self.w *= 1 + lr
  15. err = 1-y_batch * self.predict(x_batch, true)
  16. if np.max(err) <= 0
  17. # 全部样本均已分类正确,就截断当前的梯度下降,但是不能终止训练,还需要使超平面间隔最大
  18. #self.w += (-lr * w)
  19. continue
  20. # 因为分类正确的样本,惩罚部分梯度为0,所以我们这里只分类误分类的样本
  21. mask = err > 0
  22. delta = lr * c * y_batch[mask]
  23. # 取各梯度平均并做进一步梯度下降
  24. self.w += np.mean(delta * x_batch[mask], axis=0)
  25. self.b += np.mean(delta)
  26. def predict(self, x, raw=False) # 补充一句,当要用到类中定义的属性时,类方法中写上self,相当于this。
  27. x = np.asarray(x, np.float32)
  28. y_pred = x.dot(self.w) + self.b
  29. if raw:
  30. return y_pred
  31. return np.sign(y_pred).astype(np.float32)

当然,朴素的梯度下降法是有问题存在的。当数据的scale很大时,模型对参数极为敏感,从而会导致超平面持续的震荡,原因估计是因为数据太大梯度很大导致跑得太快了,解决的方法可以进行数据的归一化
还有一个做法是:用更好的梯度下降法来代替朴素的SGD,如Adam。

最后,请允许我不加证明地给出两个结论(因为结论直观且证明太长……):
- 若数据集线性可分,则最大硬间隔分离超平面存在且唯一
- 若数据集线性不可分,则最大软间隔分离超平面的解存在但不唯一,其中:
法向量()唯一
偏置量()可能不唯一


这里,我们给出LinearSVM的官方代码,由于复杂,只挑选了部分

  1. ```
  2. from .base import _fit_liblinear, BaseSVC, BaseLibSVM
  3. from ..base import BaseEstimator, RegressorMixin
  4. from ..linear_model.base import LinearClassifierMixin, SparseCoefMixin, LinearModel
  5. from ..utils import check_X_y
  6. from ..utils.validation import _num_samples
  7. from ..utils.multiclass import check_classification_targets
  8. class LinearSVC(BaseEstimator, LinearClassifierMixin,
  9. SparseCoefMixin):
  10. def __init__(self, penalty='l2', loss='squared_hinge', dual=True, tol=1e-4, C=1.0, multi_class='ovr', fit_intercept=True, intercept_scaling=1, class_weight=None, verbose=0,
  11. random_state=None, max_iter=1000):
  12. def fit(self, X, y, sample_weight=None):
  13. # FIXME Remove l1/l2 support in 1.0
  14. msg = ("loss='%s' has been deprecated in favor of "
  15. "loss='%s' as of 0.16. Backward compatibility"
  16. " for the loss='%s' will be removed in %s")
  17. if self.loss in ('l1', 'l2'):
  18. old_loss = self.loss
  19. self.loss = {'l1': 'hinge', 'l2': 'squared_hinge'}.get(self.loss)
  20. warnings.warn(msg % (old_loss, self.loss, old_loss, '1.0'),
  21. DeprecationWarning
  22. if self.C < 0:
  23. raise ValueError("Penalty term must be positive; got (C=%r)"
  24. % self.C)
  25. X, y = check_X_y(X, y, accept_sparse='csr',
  26. dtype=np.float64, order="C")
  27. check_classification_targets(y)
  28. self.classes_ = np.unique(y)
  29. self.coef_, self.intercept_, self.n_iter_ = _fit_liblinear(X, y, self.C, self.fit_intercept, self.intercept_scaling, self.class_weight, self.penalty, self.dual, self.verbose, self.max_iter, self.tol, self.random_state, self.multi_class, self.loss, sample_weight=sample_weight)
  30. if self.multi_class == "crammer_singer" and len(self.classes_) == 2:
  31. self.coef_ = (self.coef_[1] - self.coef_[0]).reshape(1, -1)
  32. if self.fit_intercept:
  33. intercept = self.intercept_[1] - self.intercept_[0]
  34. self.intercept_ = np.array([intercept])
  35. return self
  36. # ----------------------------------------------------------------------------------------------
  37. from .base import _fit_liblinear
  38. def _fit_liblinear(X, y, C, fit_intercept, intercept_scaling, class_weight, penalty, dual, verbose, max_iter, tol, random_state=None, multi_class='ovr',loss='logistic_regression', epsilon=0.1, sample_weight=None):
  39. solver_type = _get_liblinear_solver_type(multi_class, penalty, loss, dual)
  40. raw_coef_, n_iter_ = liblinear.train_wrap(X, y_ind, sp.isspmatrix(X), solver_type, tol, bias, C, class_weight_, max_iter, rnd.randint(np.iinfo('i').max), epsilon, sample_weight)
  41. return coef_, intercept_, n_iter_
  42. # ----------------------------------------------------------------------------------------------
  43. cimport liblinear
  44. def train_wrap(X, np.ndarray[np.float64_t, ndim=1, mode='c'] Y, bint is_sparse, int solver_type, double eps, double bias, double C, np.ndarray[np.float64_t, ndim=1] class_weight, int max_iter, unsigned random_seed, double epsilon, np.ndarray[np.float64_t, ndim=1, mode='c'] sample_weight):
  45. model = train(problem, param)
  46. # ----------------------------------------------------------------------------------------------
  47. model* train(const problem *prob, const parameter *param)
  48. return model_;

源码如此复杂,还是放弃吧。。。
那现在,再来看一下怎么用:

  1. Examples_1 回归
  2. --------
  3. >>> from sklearn.svm import LinearSVR
  4. >>> from sklearn.datasets import make_regression
  5. >>> X, y = make_regression(n_features=4, random_state=0) #n_features表示一个样本有4个数据 4*1
  6. >>> regr = LinearSVR(random_state=0)
  7. >>> regr.fit(X, y)
  8. LinearSVR(C=1.0, dual=True, epsilon=0.0, fit_intercept=True, intercept_scaling=1.0, loss='epsilon_insensitive', max_iter=1000, random_state=0, tol=0.0001, verbose=0)
  9. >>> print(regr.coef_) #1*4
  10. [ 16.35750999 26.91499923 42.30652207 60.47843124]
  11. >>> print(regr.intercept_)
  12. [-4.29756543]
  13. >>> print(regr.predict([[0, 0, 0, 0]]))
  14. [-4.29756543]
  15. Examples_2 分两类
  16. --------
  17. >>> from sklearn.svm import LinearSVC
  18. >>> from sklearn.datasets import make_classification
  19. >>> X, y = make_classification(n_features=4, random_state=0)
  20. >>> clf = LinearSVC(random_state=0)
  21. >>> clf.fit(X, y)
  22. LinearSVC(C=1.0, class_weight=None, dual=True, fit_intercept=True, intercept_scaling=1 loss='squared_hinge', max_iter=1000, multi_class='ovr', penalty='l2', random_state=0, tol=0.0001, verbose=0)
  23. >>> print(clf.coef_)
  24. [[ 0.08551385 0.39414796 0.49847831 0.37513797]]
  25. >>> print(clf.intercept_)
  26. [ 0.28418066]
  27. >>> print(clf.predict([[0, 0, 0, 0]]))
  28. [1]
  29. Examples_3 分多类
  30. --------
  31. # 对于一对一的分类,如果要分n类,则需要构造n*(n-1)/2个分类器
  32. >>> X = [[0], [1], [2], [3]]
  33. >>> Y = [0, 1, 2, 3]
  34. >>> clf = svm.SVC(decision_function_shape='ovo')
  35. >>> clf.fit(X, Y)
  36. SVC(C=1.0, cache_size=200, class_weight=None, coef0=0.0, decision_function_shape='ovo', degree=3, gamma='auto', kernel='rbf', max_iter=-1, probability=False, random_state=None, shrinking=True, tol=0.001, verbose=False)
  37. >>> dec = clf.decision_function([[1]]) # 得到的是矩阵
  38. >>> dec.shape[1] # 4 classes: 4*3/2 = 6 # 矩阵才可以.shape[],得到的是分类器的个数
  39. 6
  40. >>> clf.decision_function_shape = "ovr"
  41. >>> dec = clf.decision_function([[1]])
  42. >>> dec.shape[1] # 4 classes
  43. 4
  44. >>> print(clf.predict([[5]]))
  45. [3]

行文至此,可以做个小结,不准确的说,SVM它本质上即是一个分类方法,用定义分类函数,于是求w、b,为寻最大间隔,引出,继而引入拉格朗日乘子,化为对拉格朗日乘子a的求解(求解过程中会涉及到一系列最优化或凸二次规划等问题),如此,求w,b与求a等价,而a的求解可以用一种快速学习算法SMO。
至于核函数,是为处理非线性情况,若直接映射到高维计算唯恐维度爆炸,故在低维计算,等效高维表现。


第三部分:核方法

前面我们说的都是建立在线性分类问题上(线性可分与线性不可分),对于非线性的分类问题,SVM引入了核方法来解决问题。

1. 什么是核方法

往简单里说,核方法是将一个低维的线性不可分的数据映射到一个“高维”的空间、并期望映射后的数据在“高维空间”里是线性可分的。如下图很好的表示了这样的一个过程。
cmd-markdown-logo
Cover定理从理论上说明了:当空间维数d越大时,其中的点线性可分的概率就越大。所以,在解决线性不可分的问题时,我们要做的就是寻找合适的映射,使得数据集在被它映射到高维空间后变得线性可分。
表示将映射后的特征向量,于是在特征向量中划分超平面所对应的模型可表示为。类似于上面的硬间隔最大化,优化模型为:


其对偶问题是

可以看到,映射后的特征空间中涉及到计算
但是,现实任务中的数据要复杂得多,直接构造一个恰当的难度可能要高于原问题本身,而且有时候特征空间的维数是无穷维,直接计算通常是很困难的。为了避开这个难题,设想了这样一个函数:称为核函数。

在特征空间的内积等于它们在原始空间通过核函数计算的结果。

2. 核函数的思想

但是,每一个核函数都能找到一个映射与之对应吗?显而易见这是不可能的,并不是所有的都能拆成。1909年Mercer定理给出了解决方案:想让这个核函数总能找到内积映射,也就是说这个核函数总能对应一个特征空间的话,我们就要找到一个对称函数且对应的核矩阵半正定,那么这个函数就可以作为核函数使用,定义的空间称为:再生核希尔伯特空间(RKHS)。
核函数选择的是否合适决定了能否能将样本映射到合适的希尔伯特空间,因此核函数的选择是做非线性分类的重要问题。
如下图,是应用核方法的分类器的分类结果:
cmd-markdown-logo

3. 常用核函数

再看一张图:
cmd-markdown-logo
这个例子从侧面简单说明了SVM使用非线性分类器的优势,而logstic回归(感知机线性分类器)以及决策树模式都是使用了直线方法。
看了这么多,说白了
其实,核函数的应用场景非常广泛。表示定理中说了:
cmd-markdown-logo

第四部分:完整代码

  1. from sklearn.datasets import load_digits
  2. from sklearn.cross_validation import train_test_split
  3. from sklearn.preprocessing import StandardScaler
  4. from sklearn.svm import LinearSVC
  5. from sklearn.metrics import classification_report
  6. digits = load_digits()
  7. print digits.data.shape #load datas
  8. x_train, x_test, y_train, y_test = train_test_split(digits.data, digits.target, test_size = 0.25, random_state = 33)
  9. print x_train.shape,x_test.shape,y_test.shape,y_train.shape #split train datas and test datas
  10. ss = StandardScaler() # 标准化数据,使每个维度上的特征数据方差为1,均值为0,使得预测结果不会被某些维度过大的特征值而主导。
  11. x_train = ss.fit_transform(x_train)
  12. x_test = ss.transform(x_test) #standard datas
  13. lsvc = LinearSVC() # 线性分类,没有核函数;非线性分类使用SVC(kernel='rbf')或者SVC(kernel='poly')等。
  14. lsvc.fit(x_train,y_train)
  15. y_predict = lsvc.predict(x_test) #use svc(support vectors classification) to train and predict
  16. print 'The Accurary of linear SVC is',lsvc.score(x_test,y_test)
  17. print classification_report(y_test, y_predict, target_names=digits.target_names.astype(str))
  18. # from sklearn import svm
  19. #
  20. # x = [[0,0],[1,1],[1,0],[13,113]]
  21. # y = [0,1,1,1]
  22. # clf = svm.SVC()
  23. # clf.fit(x,y)
  24. # print clf.predict([[2,2]]) #result
  25. # print clf.support_vectors_ #get support vectors
  26. # print clf.support_ #get indices of support vectors
  27. # print clf.n_support_ #get number of support vectors for each class

Ref:
周志华《机器学习》
范筱《python机器学习及其实战》
微信公众号:射命丸咲 机器学习算法与自然语言处理
https://github.com/scikit-learn/scikit-learn/tree/ef5cb84a805efbe4bb06516670a9b8c690992bd7/sklearn/svm 机器学习官方代码
还有很多从博客或者是网上看到后做的笔记,已经找不到是在哪里了,如果侵犯,敬请谅解。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注