练习六-SVM

250 阅读7分钟

SVM

一、引言

在本次练习中,我们将会使用SVM来建立一个垃圾邮件分类器。同理,我们需要去GitHub下载所需的数据文件便于后续使用。

二、支持向量机

本练习的前半部分,我们会将SVM运用于大量2D数据集样本。在这些数据集上面实现SVM将会帮助我们理解SVM的工作过程和在SVM中如何使用高斯内核。后半部分我们则会将SVM运用于创建垃圾邮件分类器。

1. 导入数据集

我们使用的第一个数据集是一个线性可分的2D数据集。我们先将此数据集进行可视化操作:

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sb
from scipy.io import loadmat

raw_data = loadmat('data/ex6data1.mat')
raw_data
{'__header__': b'MATLAB 5.0 MAT-file, Platform: GLNXA64, Created on: Sun Nov 13 14:28:43 2011',
 '__version__': '1.0',
 '__globals__': [],
 'X': array([[1.9643  , 4.5957  ],
        [2.2753  , 3.8589  ],
        [2.9781  , 4.5651  ],
        [2.932   , 3.5519  ],
        [3.5772  , 2.856   ],
       ...
        [0],
        [0],
        [0],
        [0],
        [0],
        [0],
        [0],
        [0],
        [0],
        [1]], dtype=uint8)}

我们将其用散点图表示,其中类标签由符号表示(+表示正类,o表示负类)。

# 将特征向量和对应标签整合到data二维数组内
data = pd.DataFrame(raw_data['X'], columns=['X1', 'X2'])
data['y'] = raw_data['y']

# 分离正负样本,数据类型不变都是DF
positive = data[data['y'].isin([1])]
negative = data[data['y'].isin([0])]

fig, ax = plt.subplots(figsize=(12,8))
ax.scatter(positive['X1'], positive['X2'], s=50, marker='x', label='Positive')
ax.scatter(negative['X1'], negative['X2'], s=50, marker='o', label='Negative')
ax.legend()
plt.show()

download.png

从上图我们可以出,正负样本很自然地被一条沟分离,但正样本有一个偏离了。当然,我们后续的决策边界会受到此“出局者”的一定影响。

本次练习中,我们会将不同的参数C值运用于SVM。严肃说来,参数C是一个能控制对模型做出错误分类时的惩罚力度的正值。若C值较大(做错事的惩罚较大),那么SVM就会努力的将所有样本都正确归类。C类似于我们在正则化逻辑回归中的1λ\frac{1}{\lambda}

图示:

download.png

download.png

2. 得到2D数据集的决策边界

我们并不会从头开始执行SVM的任务,所以我要用scikit-learn。

from sklearn import svm
# 导入带参数C的SVM模型,1000的迭代次数难以收敛
svc = svm.LinearSVC(C=1, loss='hinge', max_iter=1000)
svc
LinearSVC(C=1, loss='hinge')

首先,我们使用 C=1 看下结果如何。

svc.fit(data[['X1', 'X2']], data['y'])
svc.score(data[['X1', 'X2']], data['y'])
E:\anaconda\lib\site-packages\sklearn\svm\_base.py:1206: ConvergenceWarning: Liblinear failed to converge, increase the number of iterations.
  warnings.warn(





0.9803921568627451

我们逐渐增大C值,看看精度是否会提高。

svc2 = svm.LinearSVC(C=100, loss='hinge', max_iter=1000)
svc2.fit(data[['X1', 'X2']], data['y'])
svc2.score(data[['X1', 'X2']], data['y'])
E:\anaconda\lib\site-packages\sklearn\svm\_base.py:1206: ConvergenceWarning: Liblinear failed to converge, increase the number of iterations.
  warnings.warn(





0.9411764705882353

精度反而降低了,但是通过增加C的值,我们创建了一个不再适合数据的决策边界。 我们可以通过查看每个类别预测的置信水平来看出这一点,这是该点与超平面距离(欧氏距离)的函数。

# 在数据集中加入C = 1时的SVM决策边界置信度信息
data['SVM 1 Confidence'] = svc.decision_function(data[['X1', 'X2']])

fig, ax = plt.subplots(figsize=(12,8))
ax.scatter(data['X1'], data['X2'], s=50, c=data['SVM 1 Confidence'], cmap='seismic')
ax.set_title('SVM (C=1) Decision Confidence')
plt.show()

download.png

可以看出将“偏离者”的正样本竟识别为了负样本。

# 在数据集中加入C = 1时的SVM决策边界置信度信息
data['SVM 2 Confidence'] = svc2.decision_function(data[['X1', 'X2']])

fig, ax = plt.subplots(figsize=(12,8))
ax.scatter(data['X1'], data['X2'], s=50, c=data['SVM 2 Confidence'], cmap='seismic')
ax.set_title('SVM (C=100) Decision Confidence')
plt.show()

download.png

可以看出将“偏离者”的正样本识别为了正样本。但我们难以线性的决策边界,也即分离出线性可分的2D数据集。

3. 带高斯内核的SVM

现在我们将从线性SVM转移到能够使用内核进行非线性分类的SVM。 我们首先负责实现一个高斯核函数。 虽然scikit-learn具有内置的高斯内核,但为了真是实现过程,我们将从头开始实现。

Kgaussian(x(i),x(j))=exp(x(i)x(j)22σ2)=exp(k=1n(xk(i)xk(j))22σ2)K_{g a u s s i a n}\left(x^{(i)}, x^{(j)}\right)=\exp \left(-\frac{\left\|x^{(i)}-x^{(j)}\right\|^{2}}{2 \sigma^{2}}\right)=\exp \left(-\frac{\sum_{k=1}^{n}\left(x_{k}^{(i)}-x_{k}^{(j)}\right)^{2}}{2 \sigma^{2}}\right)

# 根据公式定义高斯内核函数
def gaussian_kernel(x1, x2, sigma):
    return np.exp(-(np.sum((x1 - x2) ** 2) / (2 * (sigma ** 2))))
# 测试高斯函数
x1 = np.array([1.0, 2.0, 1.0])
x2 = np.array([0.0, 4.0, -1.0])
sigma = 2

gaussian_kernel(x1, x2, sigma)
0.32465246735834974

该结果与练习中的预期值相符。 接下来,我们将检查另一个数据集,此数据集用于练习非线性决策边界。

raw_data = loadmat('data/ex6data2.mat')

data = pd.DataFrame(raw_data['X'], columns=['X1', 'X2'])
data['y'] = raw_data['y']

positive = data[data['y'].isin([1])]
negative = data[data['y'].isin([0])]

fig, ax = plt.subplots(figsize=(12,8))
ax.scatter(positive['X1'], positive['X2'], s=30, marker='x', label='Positive')
ax.scatter(negative['X1'], negative['X2'], s=30, marker='o', label='Negative')
ax.legend()
plt.show()

download.png

从上图中我们可以看出,我们找不到一条直线分隔正负样本,但是我们可以利用高斯内核来获取一条非线性决策边界分离正负样本。

图示:

download.png 对于该数据集,我们将使用内置的RBF内核构建支持向量机分类器,并检查其对训练数据的准确性。 为了可视化决策边界,这一次我们将根据实例具有负类标签的预测概率来对点做阴影。 从结果可以看出,它们大部分是正确的。

svc = svm.SVC(C=100, gamma=10, probability=True)
svc
SVC(C=100, gamma=10, probability=True)
svc.fit(data[['X1', 'X2']], data['y'])
svc.score(data[['X1', 'X2']], data['y'])
0.9698725376593279
# 第一列是可能性,第二列是结果
data['Probability'] = svc.predict_proba(data[['X1', 'X2']])[:,0]

fig, ax = plt.subplots(figsize=(12,8))
ax.scatter(data['X1'], data['X2'], s=30, c=data['Probability'], cmap='Reds')
plt.show()

download.png

对于第三个数据集,我们给出了训练集和验证集,并且基于验证集性能为SVM模型找到最优超参数。 虽然我们可以使用scikit-learn的内置网格搜索来做到这一点,但是本着遵循练习的目的,我们将从头开始实现一个简单的网格搜索。

raw_data = loadmat('data/ex6data3.mat')
# 分离训练集和验证集,并且将标签数据转化为行向量
X = raw_data['X']
Xval = raw_data['Xval']
y = raw_data['y'].ravel()
yval = raw_data['yval'].ravel()

C_values = [0.01, 0.03, 0.1, 0.3, 1, 3, 10, 30, 100]
gamma_values = [0.01, 0.03, 0.1, 0.3, 1, 3, 10, 30, 100]

# 便于存储最高精度与最优参数
best_score = 0
best_params = {'C': None, 'gamma': None}

for C in C_values:
    for gamma in gamma_values:
        svc = svm.SVC(C=C, gamma=gamma)
        svc.fit(X, y)
        score = svc.score(Xval, yval)
        
        if score > best_score:
            best_score = score
            best_params['C'] = C
            best_params['gamma'] = gamma

best_score, best_params
(0.965, {'C': 0.3, 'gamma': 100})

现在,我们将进行第二部分的练习。 在这一部分中,我们的目标是使用SVM来构建垃圾邮件过滤器。 在练习文本中,有一个任务涉及一些文本预处理,以获得适合SVM处理的格式的数据。 然而,这个任务很简单(将字词映射到为练习提供的字典中的ID),而其余的预处理步骤(如HTML删除,词干,标准化等)已经完成。 我将跳过机器学习任务,而不是重现这些预处理步骤,其中包括从预处理过的训练集构建分类器,以及将垃圾邮件和非垃圾邮件转换为单词出现次数的向量的测试数据集。

三、SVM构建垃圾邮件分类器

spam_train = loadmat('data/spamTrain.mat')
spam_test = loadmat('data/spamTest.mat')

spam_train
{'__header__': b'MATLAB 5.0 MAT-file, Platform: GLNXA64, Created on: Sun Nov 13 14:27:25 2011',
 '__version__': '1.0',
 '__globals__': [],
 'X': array([[0, 0, 0, ..., 0, 0, 0],
        [0, 0, 0, ..., 0, 0, 0],
        [0, 0, 0, ..., 0, 0, 0],
        ...,
        [0, 0, 0, ..., 0, 0, 0],
        [0, 0, 1, ..., 0, 0, 0],
        [0, 0, 0, ..., 0, 0, 0]], dtype=uint8),
 'y': array([[1],
        [1],
        [0],
        ...,
        [1],
        [0],
        [0]], dtype=uint8)}
# 分离训练集,测试集并查看对应维度
X = spam_train['X']
Xtest = spam_test['Xtest']
y = spam_train['y'].ravel()
ytest = spam_test['ytest'].ravel()

X.shape, y.shape, Xtest.shape, ytest.shape
((4000, 1899), (4000,), (1000, 1899), (1000,))
y[:5]
array([1, 1, 0, 0, 0], dtype=uint8)

我们将4000个文档都转化为了特征向量表示,特征向量中的1899个维度对应着单词表中的1899个不同单词,这些分量的值都是二进制(0或1),用于表示文档中是否含有对应单词。

x=[010100]Rn,n=1899,m=4000x=\left[\begin{array}{c} 0 \\ \vdots \\ 1 \\ 0 \\ \vdots \\ 1 \\ 0 \\ \vdots \\ 0 \end{array}\right] \in \mathbb{R}^{n}, n = 1899, m= 4000

svc = svm.SVC()
svc.fit(X, y)
# 训练集精度
print('Training accuracy = {0}%'.format(np.round(svc.score(X, y) * 100, 2)))
Training accuracy = 99.32%
# 测试集精度
print('Test accuracy = {0}%'.format(np.round(svc.score(Xtest, ytest) * 100, 2)))
Test accuracy = 98.7%