0%

Machine-Learning-Lab6

实验介绍

在本练习中,您将使用支持向量机 (SVM) 来构建垃圾邮件分类器

  • ex6.m - 练习前半部分的 Octave/MATLAB 脚本
  • ex6data1.mat - 示例数据集 1
  • ex6data2.mat - 示例数据集 2
  • ex6data3.mat - 示例数据集 3
  • svmTrain.m - SVM 训练函数
  • svmPredict.m - SVM 预测函数
  • plotData.m - 绘制二维数据
  • visualizeBoundaryLinear.m - 绘制线性边界
  • visualizeBoundary.m - 绘制非线性边界
  • linearKernel.m - 支持向量机的线性内核
  • [?] gaussianKernel.m - 支持向量机的高斯核
  • [?] dataset3Params.m - 用于 ex6data3.mat 的参数
  • ex6_spam.m - 练习后半部分的 Octave/MATLAB 脚本
  • spamTrain.mat - 用“垃圾邮件训练集”进行训练
  • Test.mat - 垃圾邮件测试集
  • emailSample1.txt - 示例电子邮件 1
  • emailSample2.txt - 示例电子邮件 2
  • spamSample1.txt - 示例电子邮件 3
  • spamSample2.txt - 示例电子邮件 4
  • vocab.txt - 词汇表
  • getVocabList.m - 加载词汇表
  • porterStemmer.m - 词干功能
  • readFile.m - 将文件读入字符串
  • submit.m - 将您的解决方案发送到我们的服务器的提交脚本
  • [?] processEmail.m - 电子邮件预处理
  • [?] emailFeatures.m - 从电子邮件中提取特征

在整个练习中,您将使用脚本 ex6.m,这些脚本为问题设置数据集并调用您将编写的函数,您只需按照本作业中的说明修改其他文件中的功能

Support Vector Machines(支持向量机)

在本练习的前半部分,您将使用支持向量机 (SVM) 和各种示例 2D 数据集,对这些数据集进行试验将帮助您直观地了解 SVM 的工作原理以及如何将高斯核与 SVM 一起使用

在练习的下半部分,您将使用支持向量机来构建垃圾邮件分类器,提供的脚本 ex6.m 将帮助您逐步完成练习的前半部分

Example Dataset 1(示例数据集 1)

我们将从一个可以由线性边界分隔的 2D 示例数据集开始

  • 脚本 ex6.m 将绘制训练数据,在这个数据集中,正例(用 + 表示)和负例(用 o 表示)的位置表明了由间隙表示的自然分离
  • 但是,请注意,在最左侧大约 (0.1, 4.1) 处有一个异常正例 +
  • 在下一部分中,您还将看到这个异常值如何影响 SVM 决策边界

实现过程:

1
2
3
4
5
6
7
8
9
10
11
np.set_printoptions(formatter={'float': '{: 0.6f}'.format}) # 用于控制Python中小数的显示精度

# ===================== 1.读取数据并可视化 =====================
data = scio.loadmat('data/ex6data1.mat') # 以字典格式读取数据
# 分别取出特征数据X和对应的输出Y(都是narray格式)
X = data['X']
Y = data['y'].flatten()
plot_data(X,Y) # 绘制散点图
plt.xlabel('Feature 1')
plt.ylabel('Feature 2')
plt.show()

绘图:

  • 观察数据,发现一条直线就可以区分正负样例
  • 所以,可以直接使用 SVM

利用 SVM 算法进行拟合:

1
2
3
4
5
6
7
8
9
10
11
from sklearn import svm

# ===================== 2.训练SVM使用线性核函数 =====================
# PS:线性核函数,就是不使用核函数的意思

C =100 # 异常点的权重
clf = svm.SVC(C, kernel='linear', tol=1e-3) # 使用sklearn自带的svm函数进行训练
clf.fit(X, Y) # 开始训练
plot_data(X,Y)
vb.visualize_boundary(clf, X, 0, 4.5, 1.5, 5) # 利用训练结果clf绘制决策边界
plt.show()

函数 visualize_boundary 的实现:绘制决策边界

1
2
3
4
5
6
7
def visualize_boundary(clf, X, x_min, x_max, y_min, y_max):
h = .02
xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h))

Z = clf.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
plt.contour(xx, yy, Z, levels=[0], colors='k')
  • meshgrid(X , Y):快速生成坐标矩阵 (X , Y)
  • arange(x , y):返回一个有终点和起点的固定步长的排列
  • predict(x , y):返回样本属于每一个类别的概率(SVM 自带的方法)

绘图:

实现并验证高斯核函数:

1
2
3
4
5
6
7
8
9
# ===================== 3.实现并验证高斯核函数 =====================

x1 = np.array([1, 2, 1]) # 实例1
x2 = np.array([0, 4, -1]) # 实例2
sigma = 2
sim = gk.gaussian_kernel(x1, x2, sigma) # 高斯核函数的简单实现

print('Gaussian kernel between x1 = [1, 2, 1], x2 = [0, 4, -1], sigma = {} : {:0.6f}\n'.format(sigma, sim))
print('(for sigma = 2, this value should be about 0.324652')

高斯核函数公式:

代码实现 gaussian_kernel:

1
2
3
4
5
6
7
import numpy as np

def gaussian_kernel(x1, x2, sigma):
x1 = x1.flatten()
x2 = x2.flatten()
sim = np.exp(-sum((x1 - x2) ** 2) / (2 * sigma ** 2))
return sim
  • 可以类比一下高斯核函数公式

Example Dataset 2(示例数据集 2)

ex6.m 中的下一部分将加载并绘制数据集 2,具体过程:

1
2
3
4
5
6
7
8
9
10
11
12
# ===================== 4.读取数据并可视化2 =====================

print('Loading and Visualizing Data ...')
data = scio.loadmat('data/ex6data2.mat')
# 分别取出特征数据X和对应的输出Y(都是narray格式)
X = data['X']
y = data['y'].flatten()
m = y.size
plot_data(X, y) # 绘制散点图
plt.xlabel('Feature 1')
plt.ylabel('Feature 2')
plt.show()

绘图:

  • 从图中可以看出,该数据集没有区分正负样本的线性决策边界
  • 这是一个非线性的决策边界,如果像上一部分一样使用只 SVM,拟合的效果就不好
  • 但是,通过将高斯核与 SVM 结合使用,您将能够学习一个非线性决策边界,该边界可以对数据集执行得相当好

在这部分练习中,您将使用 SVM 进行非线性分类,特别是,您将在非线性可分的数据集上使用具有高斯核的 SVM

要使用 SVM 找到非线性决策边界,我们需要首先实现一个高斯核,您可以将高斯核视为一个相似度函数,用于测量一对示例之间的“距离”

高斯核函数的公式:

接下来就利用“SVM”和“高斯核”绘制一个非线性区域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# ===================== 5.使用RBF内核训练SVM =====================
# PS:上一部分已经实现内核函数了,但这一部分使用"svm"模块自带的高斯核

print('Training SVM with RFB(Gaussian) Kernel (this may take 1 to 2 minutes) ...')

c = 1
sigma = 0.1

clf = svm.SVC(c, kernel='rbf', gamma=np.power(sigma, -2)) # 同时使用SVM和高斯核
#clf = svm.SVC(c, kernel='linear', tol=1e-3) # 只使用SVM
clf.fit(X, y) # 开始训练

print('Training complete!')

plot_data(X, y)
vb.visualize_boundary(clf, X, 0, 1, .4, 1.0) # 绘制决策边界
plt.show()

绘图:

  • 只使用 SVM 算法:
  • 同时使用 SVM 算法和高斯核函数:

Example Dataset 3(示例数据集 3)

在这部分练习中,您将获得更多关于如何使用具有高斯核的 SVM 的实用技能

ex6.m 的下一部分将加载并显示第三个数据集,您将在此数据集上使用带有高斯核的 SVM

具体过程:

1
2
3
4
5
6
7
8
9
10
11
# ===================== 6.读取数据并可视化3 =====================

print('Loading and Visualizing Data ...')
data = scio.loadmat('data/ex6data3.mat')
X = data['X']
y = data['y'].flatten()
m = y.size
plot_data(X, y)
plt.xlabel('Feature 1')
plt.ylabel('Feature 2')
plt.show()

绘图:

  • 看上去是一条线性的决策边界
  • 不过我们仍然需要使用高斯核

你的任务是使用交叉验证集 Xval, yval 来确定最佳 C 和 sigma(σ)

  • sigma(σ) 过小,会导致方差较大(过拟合)
  • sigma(σ) 过大,会导致偏差较大(欠拟合)
1
2
3
4
5
6
7
8
9
10
11
# ===================== 7.使用RBF内核训练SVM2 =====================

c = 1 # 可变数据1
sigma = 0.1 # 可变数据2

clf = svm.SVC(c, kernel='rbf', gamma=np.power(sigma, -2))
clf.fit(X, y) # 开始训练
plot_data(X, y)
vb.visualize_boundary(clf, X, -.5, .3, -.8, .6)

plt.show()

绘图:

  • c = 1,sigma = 0.1
  • c = 1,sigma = 0.9
  • c = 100,sigma = 0.1
  • c = 100,sigma = 0.9

大体的规律如下:

  • c 越大,模型的拟合程度越高,过大会导致过拟合
  • sigma 越大,决策边界越直,越趋近于“线性核函数”

Spam Classification(垃圾邮件分类)

当今的许多电子邮件服务都提供垃圾邮件过滤器,能够将电子邮件高精度地分类为垃圾邮件和非垃圾邮件

  • 在这部分练习中,您将使用 SVM 构建您自己的垃圾邮件过滤器
  • 您将训练一个分类器来分类给定的电子邮件 x 是垃圾邮件 (y = 1) 还是非垃圾邮件 (y = 0)
  • 特别是,您需要将每封电子邮件转换为一个特征向量 x
  • 练习的以下部分将引导您了解如何从电子邮件构建这样的特征向量,在本练习的其余部分,您将使用脚本 ex6_spam.m
  • 本练习包含的数据集基于 SpamAssassin 公共语料库的一个子集,在本练习中,您将仅使用电子邮件正文(不包括电子邮件标题)

Preprocessing Emails(预处理电子邮件)

在开始执行机器学习任务之前,查看数据集中的示例通常很有见地

  • 上图显示了一个示例电子邮件,其中包含一个 URL、一个电子邮件地址(在末尾)、数字和美元金额,虽然许多电子邮件包含相似类型的实体(例如,数字、其他 URL 或其他电子邮件地址)
  • 但几乎每封电子邮件中的特定实体(例如,特定 URL 或特定金额)都会有所不同
  • 因此,处理电子邮件时常用的一种方法是“规范化”这些值,以便所有 URL 都被视为相同,所有数字都被视为相同等
  • 例如,我们可以将电子邮件中的每个 URL 替换为唯一的字符串 “httpaddr”表示存在 URL
  • 这具有让垃圾邮件分类器根据是否存在任何 URL 而不是特定 URL 是否存在来做出分类决定的效果
  • 这通常会提高垃圾邮件分类器的性能,因为垃圾邮件发送者通常会随机化 URL,因此在新的垃圾邮件中再次看到任何特定 URL 的几率非常小

预测处理的条目如下:

  • 小写:整个电子邮件被转换为小写,因此忽略大写(例如:IndIcaTE 被视为与 Indicate 相同)
  • 剥离 HTML:从电子邮件中删除所有 HTML 标记,许多电子邮件通常带有 HTML 格式,我们删除了所有的 HTML 标签,这样就只剩下内容了
  • 规范化 URL:所有 URL 都替换为文本 “httpaddr”
  • 标准化电子邮件地址:所有电子邮件地址都替换为文本 “emailaddr”
  • 规范化数字:所有数字都替换为文本 “数字”
  • 标准化美元:所有美元符号 ($) 都替换为文本 “美元”
  • 词干:词被简化为词干形式,例如:“discount” 、 “discounts” 、 “discounted” 和 “discounting” 都替换为 “discount”,有时,Stemmer 实际上会从末尾去掉额外的字符,因此“include” 、 “includes” 、 “included” 和 “include” 都替换为 “include”
  • 删除非单词:已删除非单词和标点符号,所有空格(制表符、换行符、空格)都已被修剪为单个空格字符

要使用 SVM 将电子邮件分类为垃圾邮件和非垃圾邮件,您首先需要将每封电子邮件转换为特征向量,在这一部分中,您将为每封电子邮件实施预处理步骤,您应该完成 processEmail.py 中的代码以生成给定电子邮件的单词索引向量

预处理函数 processEmail 的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import numpy as np
import re
import nltk, nltk.stem.porter

def process_email(email_contents):

# ===================== Preprocess Email =====================
vocab_list = get_vocab_list() # 导入词汇表
word_indices = []
email_contents = email_contents.lower()
email_contents = re.sub('<[^<>]+>', ' ', email_contents)
# 任何数字都被替换为字符串'number'
email_contents = re.sub('[0-9]+', 'number', email_contents)
# 以http或https开头的任何内容都替换为'httpaddr'
email_contents = re.sub('(http|https)://[^\s]*', 'httpaddr', email_contents)
# 中间带“@”的字符串被视为电子邮件 --> 'emailaddr'
email_contents = re.sub('[^\s]+@[^\s]+', 'emailaddr', email_contents)
# '$'符号被替换为'dollar'
email_contents = re.sub('[$]+', 'dollar', email_contents)

# ===================== Tokenize Email =====================
print('==== Processed Email ====')
stemmer = nltk.stem.porter.PorterStemmer() # 英文词干提取算法(Porter stemmer)
#print('email contents : {}'.format(email_contents)) # 输出电子邮件
tokens = re.split('[@$/#.-:&*+=\[\]?!(){\},\'\">_<;% ]', email_contents) # 对电子邮件进行拆分

for token in tokens:
token = re.sub('[^a-zA-Z0-9]', '', token) # 用正则来匹配合适的字符
token = stemmer.stem(token)
vocab_value_list = list(vocab_list.values())
vocab_key_list = list(vocab_list.keys())

if len(token) < 1:
continue

try:
num = vocab_key_list[vocab_value_list.index(token)]
except:
continue

word_indices.append(np.array(num))
print(token)

print('==================')
word_indices = np.array(word_indices) # 从'list'转化为'numpy.array'
return word_indices

def get_vocab_list(): # 导入词汇表
vocab_dict = {}
with open('data/vocab.txt') as f:
for line in f:
(val, key) = line.split()
vocab_dict[int(val)] = key

return vocab_dict

具体实现:

1
2
3
4
5
6
7
8
9
10
# ===================== 1.电子邮件预处理 =====================

print('Preprocessing sample email (emailSample1.txt) ...')

file_contents = open('data/emailSample1.txt', 'r').read()
word_indices = pe.process_email(file_contents)

# Print stats
print('Word Indices: ')
print(word_indices)

Extracting Features from Emails(从电子邮件中提取特征)

您现在应该完成 emailFeatures.m 中的代码,以在给定单词索引的情况下为电子邮件生成特征向量

  • 您现在将实现将每封电子邮件转换为 Rn 中的向量的特征提取,对于本练习,您将在词汇表中使用 n = # 个单词
  • 具体来说,电子邮件的特征 xi(“0” or “1”)对应于字典中的第 i 个单词是否出现在电子邮件中
    • 如果电子邮件中存在第 i 个单词,则 xi = 1
    • 如果电子邮件中不存在第 i 个单词,则 xi = 0
  • 因此,对于典型的电子邮件,此功能看起来像您现在应该完成 emailFeatures.m 中的代码用于生成电子邮件的特征向量,给定单词 indices

特征提取函数 emailFeatures 的实现:

1
2
3
4
5
6
7
8
9
import numpy as np

def email_features(word_indices):
n = 1899
features = np.zeros(n + 1)
for i in range(len(word_indices)):
j = word_indices[i]
features[j]=1;
return features
  • 有点类似于位图
  • word_indices 就是邮件的各个单词提取出来后,在单词表中的位置
  • 创建数组 features(各个条目初始化为“0”),然后把对应位置的值置为“1”

具体过程:

1
2
3
4
5
6
7
8
9
# ===================== 2.特征提取 =====================

print('Extracting Features from sample email (emailSample1.txt) ... ')

features = ef.email_features(word_indices) # 提取特征

# 打印统计数据
print('Length of feature vector: {}'.format(features.size))
print('Number of non-zero entries: {}'.format(np.flatnonzero(features).size))

Training SVM for Spam Classification(为垃圾邮件分类训练 SVM)

完成特征提取功能后,ex6 spam.m 的下一步将加载一个预处理的训练数据集,该数据集将用于训练 SVM 分类器

  • spamTrain.mat 包含 4000 个垃圾邮件和非垃圾邮件的训练示例
  • spamTest.mat 包含 1000 个测试示例
  • 每封原始电子邮件都使用 processEmail 和 emailFeatures 函数进行处理,并转换为向量 x(i)
  • 加载数据集后,ex6 spam.m 将继续训练 SVM 以在垃圾邮件(y=1)和非垃圾邮件(y=0)之间进行分类,训练完成后,您应该看到分类器的训练准确率约为 99.8%,测试准确率约为 98.5%

具体过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# ===================== 3.为垃圾邮件分类训练线性SVM =====================
data = scio.loadmat('data/spamTrain.mat')
X = data['X']
y = data['y'].flatten()

print('Training Linear SVM (Spam Classification)')
print('(this may take 1 to 2 minutes)')

c = 0.1
clf = svm.SVC(c, kernel='linear')
clf.fit(X, y)

p = clf.predict(X)

print('Training Accuracy: {}'.format(np.mean(p == y) * 100))

接下来进行测试:

1
2
3
4
5
6
7
8
9
10
11
# ===================== 4.测试垃圾邮件分类 =====================

data = scio.loadmat('data/spamTest.mat')
Xtest = data['Xtest']
ytest = data['ytest'].flatten()

print('Evaluating the trained linear SVM on a test set ...')

p = clf.predict(Xtest) # 预测SVM的结果

print('Test Accuracy: {}'.format(np.mean(p == ytest) * 100))

Top Predictors for Spam(垃圾邮件的主要预测指标)

为了更好地理解垃圾邮件分类器的工作原理,我们可以检查参数以查看分类器认为哪些词最能预测垃圾邮件

  • ex6_spam.m 的下一步是在分类器中找到具有最大正值的参数(最高频)并显示相应的单词
  • 因此,如果一封电子邮件包含诸如“保证”、“删除”、“美元”和“价格”之类的词(垃圾邮件的高频词汇),它很可能被归类为垃圾邮件

由于我们正在训练的模型是线性 SVM,我们可以检查模型学习的 w 权重,以更好地了解它如何确定电子邮件是否为垃圾邮件,以下代码查找分类器中权重最高的单词,非正式地,分类器“认为”这些词最有可能是垃圾邮件的指标

1
2
3
4
5
6
7
8
# ===================== 5.垃圾邮件的主要预测指标 =====================

vocab_list = pe.get_vocab_list() # 导入词汇表
indices = np.argsort(clf.coef_).flatten()[::-1] # "-1"代表倒置,顺序改为从大到小了
print(indices)

for i in range(15):
print('{} ({:0.6f})'.format(vocab_list[indices[i]], clf.coef_.flatten()[indices[i]]))
  • argsort(arr):返回的是元素值从小到大排序后的索引值的数组