吴恩达机器学习作业ex6:支持向量机Python实现)详细注释

1 支持向量机

1.1 示例数据集1

我们将从二维示例数据集开始,该数据集可以用线性边界分隔。脚本 ex6.m 将绘制训练数据(图 1)。在这个数据集中,正例(用 + 表示)和负例(用 o 表示)的位置显示了一个由间隙表示的自然分隔。但是,请注意,在最左侧约(0.1,4.1)处有一个离群正例 +。作为本练习的一部分,您还将看到这个离群点如何影响 SVM 的决策边界。

在这里插入图片描述
在这部分练习中,您将尝试在 SVM 中使用不同的 C 参数值。非正式地讲,C 参数是一个正值,用于控制对分类错误的训练示例的惩罚。C 参数越大,SVM 就会尝试对所有示例进行正确分类。C 的作用类似于 1/λ,其中 λ 是我们之前用于逻辑回归的正则化参数。
在这里插入图片描述
在这里插入图片描述

# %matplotlib inline 是一个魔术命令,用于在 Jupyter Notebook 中内嵌绘图。
# 它确保所有 Matplotlib 绘制的图形在 Notebook 中直接显示。
%matplotlib inline

# 导入所需的库
import numpy as np  # 用于科学计算的基础库
import pandas as pd  # 用于数据处理和分析的库
import matplotlib.pyplot as plt  # 用于绘制图形的库
import seaborn as sb  # 基于 Matplotlib 的数据可视化库,提供更美观的图形
from scipy.io import loadmat  # 用于加载 MATLAB 文件的函数
from sklearn import svm  # 用于支持向量机 (SVM) 的库

# 使用 loadmat 函数加载 MATLAB 格式的文件
# 这里假设文件位于当前目录的 data 文件夹中,文件名为 ex6data1.mat
mat = loadmat('./data/ex6data1.mat')

# 打印加载的字典的所有键,以了解数据的结构和内容
print(mat.keys())
# 这将输出:dict_keys(['__header__', '__version__', '__globals__', 'X', 'y'])
# __header__, __version__, __globals__ 是 MATLAB 文件的一些元数据
# X 是特征矩阵,包含数据点的特征
# y 是标签向量,包含每个数据点的标签(0 或 1)

# 从加载的字典中提取特征矩阵 X 和标签向量 y
X = mat['X']
y = mat['y']

# 提取特征矩阵和标签向量后,可以进一步探索和处理数据
# 定义一个函数 plotData,用于绘制数据点
def plotData(X, y):
    # 创建一个新的图形,设置图形的大小为 8x5 英寸
    plt.figure(figsize=(8, 5))
    
    # 绘制散点图
    # X[:,0] 表示所有数据点的第一个特征
    # X[:,1] 表示所有数据点的第二个特征
    # c=y.flatten() 设置颜色根据标签 y 来区分
    # cmap='rainbow' 使用彩虹颜色图
    plt.scatter(X[:, 0], X[:, 1], c=y.flatten(), cmap='rainbow')
    
    # 设置 x 轴标签为 'X1'
    plt.xlabel('X1')
    
    # 设置 y 轴标签为 'X2'
    plt.ylabel('X2')
    
    # 添加图例(虽然这里没有明确的标签,但习惯上加上图例更规范)
    plt.legend()
    
# 调用 plotData 函数,传入特征矩阵 X 和标签向量 y
plotData(X, y)

在这里插入图片描述

# 定义一个函数 plotBoundary,用于绘制分类器的决策边界
def plotBoundary(clf, X):
    '''plot decision boundary'''
    
    # 获取特征矩阵 X 的第一个特征的最小值和最大值,并稍微扩展范围
    x_min, x_max = X[:, 0].min() * 1.2, X[:, 0].max() * 1.1
    
    # 获取特征矩阵 X 的第二个特征的最小值和最大值,并稍微扩展范围
    y_min, y_max = X[:, 1].min() * 1.1, X[:, 1].max() * 1.1
    
    # 生成网格点
    # np.linspace 用于生成从 x_min 到 x_max 之间的 500 个均匀分布的点
    # np.meshgrid 用于生成二维网格坐标矩阵
    xx, yy = np.meshgrid(np.linspace(x_min, x_max, 500),
                         np.linspace(y_min, y_max, 500))
    
    # 使用分类器对网格点进行预测
    # np.c_ 将 xx 和 yy 展平并拼接成一个二维数组,每一行是一个网格点的坐标
    Z = clf.predict(np.c_[xx.ravel(), yy.ravel()])
    
    # 将预测结果 Z 重塑为网格的形状
    Z = Z.reshape(xx.shape)
    
    # 绘制等高线图,显示决策边界
    plt.contour(xx, yy, Z)

# 这个函数假设你已经有一个训练好的分类器 clf 和特征矩阵 X
# 它将绘制出分类器的决策边界
# 定义模型参数
# 创建一个列表,其中包含两个 SVM 模型,每个模型使用不同的正则化参数 C
models = [svm.SVC(C=C, kernel='linear') for C in [1, 100]]

# 训练模型
# 遍历 models 列表,并对每个模型进行训练,将训练好的模型存储在 clfs 列表中
clfs = [model.fit(X, y.ravel()) for model in models]

# 为每个模型生成标题
# 生成一个列表,其中包含每个模型的标题,标题中包括模型的正则化参数 C
titles = ['SVM Decision Boundary with C = {} (Example Dataset 1)'.format(C) for C in [1, 100]]

# 绘制每个模型的决策边界
# 遍历 clfs 和 titles 列表,分别绘制每个模型的决策边界,并为每个图形添加标题
for model, title in zip(clfs, titles):
    # 创建一个新的图形,并设置图形的大小为 8x5 英寸
    plt.figure(figsize=(8, 5))
    
    # 绘制数据点
    plotData(X, y)
    
    # 绘制决策边界
    plotBoundary(model, X)
    
    # 添加图形标题
    plt.title(title)

# 显示所有图形
plt.show()

在这里插入图片描述
在这里插入图片描述
可以发现在C值较大时,容易被特殊的数据所影响,容易造成过拟合现象。

1.2 高斯核与SVM

1.2.1 高斯核

要利用 SVM 找到非线性决策边界,我们首先需要实现一个高斯核。您可以将高斯核视为一个相似性函数,用于测量一对示例(x(i), x(j))之间的 “距离”。高斯核的参数还包括带宽参数 σ,它决定了当例子之间的距离越远时,相似度指标下降(为 0)的速度。
现在您应该完成 gaussianKernel.m 中的代码,以计算两个示例 (x(i), x(j)) 之间的高斯核。高斯核函数被定义为:
在这里插入图片描述
完成函数 gaussianKernel.m 后,脚本 ex6.m 将在提供的两个示例中测试您的核函数,您应该能看到 0.324652 的值。

def gaussKernel(x1, x2, sigma):
    return np.exp(- ((x1 - x2) ** 2).sum() / (2 * sigma ** 2))

gaussKernel(np.array([1, 2, 1]),np.array([0, 4, -1]), 2.)  # 0.32465246735834974

1.2.2 示例数据集2

在这里插入图片描述

ex6.m 的下一部分将加载并绘制数据集 2(图 4)。从图中可以看出,该数据集没有线性判定边界来区分正例和负例。但是,通过将高斯核与 SVM 结合使用,您将能够学习到一个非线性判定边界,该边界在数据集上的表现还算不错。如果您正确执行了高斯核函数,ex6.m 将继续在该数据集上使用高斯核训练 SVM。

X2 = mat['X']
y2 = mat['y']
plotData(X2, y2)

在这里插入图片描述


# 设置高斯核参数
sigma = 0.1
gamma = np.power(sigma, -2.0) / 2  # 计算 gamma 参数,公式为 gamma = 1 / (2 * sigma^2)

# 训练 SVM 模型
clf = svm.SVC(C=1, kernel='rbf', gamma=gamma)  # 创建 SVM 模型,使用 RBF 核函数,设置正则化参数 C 和 gamma
model = clf.fit(X2, y2.flatten())  # 使用训练数据 X2 和 y2 训练模型,y2.flatten() 将标签展平成一维数组

# 绘制数据点和决策边界
plotData(X2, y2)  # 绘制数据点
plotBoundary(model, X2)  # 绘制决策边界
plt.title(f'SVM with Gaussian Kernel (σ = {sigma}, C = 1)')  # 设置图像标题,包含 sigma 和 C 参数
plt.show()  # 显示图像

在这里插入图片描述

1.2.3 示例数据集3

在这部分练习中,您将获得更多关于如何使用带高斯内核的 SVM。ex6.m 的下一部分将加载并显示第三个数据集(图 6)。第三个数据集(图 6)。您将得到变量 X、y、Xval、yval。ex6.m 中提供的代码使用从 dataset3Params.m 加载的参数使用训练集 (X, y) 训练 SVM 分类器。您的任务是使用交叉验证集 Xval, yval 来确定要使用的最佳 C 和 σ 参数。您应编写任何必要的附加代码,以帮助您搜索参数 C 和 σ。对于 C 和 σ,我们建议按乘法步长尝试取值(如 0.01、0.03、0.1、0.3、1、3、10、30)。请注意,您应尝试 C 和 σ 的所有可能的成对值(如 C = 0.3 和 σ = 0.1)。例如,如果您尝试上面列出的 C 和 σ2 的 8 个值中的每一个,您将最终训练和评估(在交叉验证集上)总共 82 = 64 个不同的模型。

mat3 = loadmat('data/ex6data3.mat')
X3, y3 = mat3['X'], mat3['y']
Xval, yval = mat3['Xval'], mat3['yval']
plotData(X3, y3)

在这里插入图片描述

Cvalues = (0.01, 0.03, 0.1, 0.3, 1., 3., 10., 30.)
sigmavalues = Cvalues
best_pair, best_score = (0, 0), 0

# 遍历所有可能的 C 和 sigma 值组合
for C in Cvalues:
    for sigma in sigmavalues:
        gamma = np.power(sigma,-2.)/2  # 根据 sigma 计算 gamma 参数
        model = svm.SVC(C=C, kernel='rbf', gamma=gamma)  # 使用 RBF 核和指定的 C 和 gamma 参数初始化 SVM 模型
        model.fit(X3, y3.flatten())  # 使用训练数据 X3 和 y3 训练模型
        this_score = model.score(Xval, yval)  # 评估模型在验证集 Xval 和 yval 上的准确率
        if this_score > best_score:  # 如果当前组合的评分更高,则更新最佳参数对
            best_score = this_score
            best_pair = (C, sigma)
print('best_pair={}, best_score={}'.format(best_pair, best_score))
# 输出最佳参数对和对应的评分
# best_pair=(1.0, 0.1), best_score=0.965
# 设置高斯核参数
sigma = 0.1
gamma = np.power(sigma, -2.0) / 2  # 计算 gamma 参数,公式为 gamma = 1 / (2 * sigma^2)

# 初始化 SVM 模型
model = svm.SVC(C=1.0, kernel='rbf', gamma=gamma)

# 训练 SVM 模型
model.fit(X3, y3.flatten())

# 绘制数据点
plotData(X3, y3)

# 绘制决策边界
plotBoundary(model, X3)

在这里插入图片描述

2 垃圾邮件分类

2.1 预处理电子邮件

在开始执行机器学习任务之前,先看看数据集中的示例通常会很有帮助。图 8 显示了一封包含 URL、电子邮件地址(位于末尾)、数字和美元金额的示例电子邮件。虽然许多电子邮件都包含类似类型的实体(如数字、其他 URL 或其他电子邮件地址),但几乎每封邮件中的特定实体(如特定 URL 或特定美元金额)都不尽相同。因此,在处理电子邮件时经常使用的一种方法是将这些值 “规范化”,以便对所有 URL 和数字等进行相同处理。例如,我们可以用唯一字符串 "httpaddr "替换电子邮件中的每个 URL,以表示存在 URL。
在这里插入图片描述
这样做的效果是让垃圾邮件分类器根据是否存在任何 URL 根据,而不是是否存在某个特定 URL 来做出分类决定。这通常会提高垃圾邮件分类器的性能,因为垃圾邮件发送者通常会随机化 URL,因此在新的垃圾邮件中再次出现任何特定 URL 的几率非常小。
在 processEmail.m 中,我们实施了以下电子邮件预处理和规范化步骤:

  • 小写: 将整封邮件转换为小写字母,从而忽略首字母缩写(例如,IndIcaTE 与 Indicate 处理相同。
  • 去除 HTML: 删除电子邮件中的所有 HTML 标记。 许多电子邮件通常带有 HTML 格式;我们会移除所有 HTML标签,只保留内容。
  • 规范 URL: 将所有 URL 替换为文本 “httpaddr”。
  • 规范化电子邮件地址: 所有电子邮件地址替换为文本 “emailaddr”。
  • 规范化数字: 所有数字均替换为"number"(数字)。
  • 规范化美元: 将所有美元符号 ($) 替换为文本 “美元”。
  • 词干化: 将单词简化为词干形式。例如,“discount”、“discounts”、"discounted "和 "discounting "均被替换为 “discount”。有时,词干转换器实际上会去掉词尾的因此,“include”、“includes”、"included "和 "including "都会被替换为 “includ”、
  • 删除非单词: 去除了非单词和标点符号。所有空格(制表符、换行符、空格)都被修剪为一个空格字符。
# 使用 with 语句打开文件,确保文件在使用完毕后会被正确关闭
with open('data/emailSample1.txt', 'r') as f:
    # 使用文件对象 f 的 read() 方法读取整个文件的内容,并将内容赋值给变量 email
    # 模式为只读模式 ('r'表示读取)
    email = f.read()
    # 打印读取的文本内容
    print(email)

在这里插入图片描述

%matplotlib inline  # 在 Jupyter Notebook 中使用 matplotlib 绘图时需要加上这行,用于显示图形
import numpy as np  # 导入数值计算库 numpy
import matplotlib.pyplot as plt  # 导入绘图库 matplotlib 的 pyplot 模块
from scipy.io import loadmat  # 导入 scipy 库中用于加载 Matlab 文件的函数
from sklearn import svm  # 导入 scikit-learn 库中的支持向量机模块
import re  # 导入用于处理正则表达式的模块,用于处理电子邮件
from stemming.porter2 import stem  # 导入 Porter stemmer 算法,用于英文单词的词干提取

# 也可以使用 NLTK 库中的 Porter stemmer 算法
import nltk
import nltk.stem.porter
def processEmail(email):
    """做除了Word Stemming和Removal of non-words的所有处理"""
    
    email = email.lower()  # 将整个邮件文本转换为小写,以便统一处理
    
    # 以下逐步进行正则表达式替换
    email = re.sub('<[^<>]+>', ' ', email)  # 匹配尖括号中的 HTML 标签,并将其替换为空格
    email = re.sub('(http|https)://[^\s]*', 'httpaddr', email)  # 匹配 URL,并将其替换为 'httpaddr'
    email = re.sub('[^\s]+@[^\s]+', 'emailaddr', email)  # 匹配电子邮件地址,并将其替换为 'emailaddr'
    email = re.sub('[$]+', 'dollar', email)  # 匹配美元符号,并将其替换为 'dollar'
    email = re.sub('[\d]+', 'number', email)  # 匹配数字,并将其替换为 'number'
    
    return email
def email2TokenList(email):
    """预处理数据,返回一个干净的单词列表"""
    
    # 使用 NLTK 的 Porter stemmer,因为它更准确地复制了作业中的 OCTAVE 实现的性能
    stemmer = PorterStemmer()
    
    # 预处理电子邮件内容
    email = preProcess(email)

    # 将邮件分割为单个单词,re.split() 可以设置多种分隔符
    tokens = re.split('[ \@\$\/\#\.\-\:\&\*\+\=\[\]\?\!\(\)\{\}\,\'\"\>\_\<\;\%]', email)
    
    # 遍历每个分割出来的内容
    tokenlist = []
    for token in tokens:
        # 删除任何非字母数字的字符
        token = re.sub('[^a-zA-Z0-9]', '', token)
        # 使用 Porter 词干提取算法提取词根
        stemmed = stemmer.stem(token)
        # 去除空字符串(空字符串里不含任何字符)
        if not len(token): continue
        tokenlist.append(stemmed)
            
    return tokenlist  

2.1.1 词汇表

经过预处理后,我们就得到了每封电子邮件的单词列表(如图 9)。每封邮件的单词列表。下一步是选择我们希望在我们的 我们的分类器中使用,哪些我们不希望使用。在本练习中,我们只选择出现频率最高的单词作为我们考虑的单词集(词汇表)。由于在训练集中很少出现的单词只出现在少数几封电子邮件中,它们可能会导致它们的模型过度拟合我们的训练集。完整的词汇表在 vocab.txt 文件中,也如图 10 所示。我们的词汇表是通过选择所有在垃圾邮件语料库中至少出现 100 次的单词,从而得到一个包含 1899 个单词的词汇表。在实践中,通常会使用包含约 10,000 到 50,000 个单词的词汇表。
有了词汇表,我们就可以将预处理邮件(如图 9)中的每个单词映射到单词索引列表中,该列表包含词汇表中单词的索引。图 11 显示了样本电子邮件的映射。具体来说,在样本邮件中,单词 "anyone "首先被规范化为 “anyon”,然后映射到词汇表中的索引 86。
在这里插入图片描述

def email2VocabIndices(email, vocab):
    """提取存在单词的索引"""
    
    # 调用 email2TokenList 函数将邮件预处理并转化为单词列表
    token = email2TokenList(email)
    
    # 遍历词汇表,找到在 token 列表中的单词,并返回其索引
    index = [i for i in range(len(vocab)) if vocab[i] in token]
    
    return index

2.2 从电子邮件中提取特征

现在,您将实现特征提取,将每封邮件转换成 Rn 中的向量。在本练习中,您将使用 n = # words词汇表中的单词。具体来说,电子邮件的特征 xi∈ {0, 1} 与字典中第 i 个单词是否出现在电子邮件中相对应。也就是说,如果电子邮件中出现了第 i 个单词,则 xi = 1;如果电子邮件中没有出现第 i 个单词,则 xi = 0。因此,对于一封典型的电子邮件,这一特征如下:
在这里插入图片描述

def email2FeatureVector(email):
    """
    将email转化为词向量,n是vocab的长度。存在单词的相应位置的值置为1,其余为0
    """
    
    # 读取词汇表文件,创建一个数据框
    df = pd.read_table('data/vocab.txt', names=['words'])
    
    # 将数据框转换为numpy数组,返回一个词汇数组
    vocab = df.as_matrix()  # 在pandas的新版中应使用 df.values 或 np.array(df),因为 as_matrix() 已弃用

    # 初始化一个全零的特征向量,长度为词汇表的长度
    vector = np.zeros(len(vocab))  # init vector

    # 调用 email2VocabIndices 函数,将email转化为包含词汇索引的列表
    vocab_indices = email2VocabIndices(email, vocab)  # 返回含有单词的索引

    # 将有单词的索引位置置为1
    for i in vocab_indices:
        vector[i] = 1
    
    return vector
vector = email2FeatureVector(email)
print('length of vector = {}\nnum of non-zero = {}'.format(len(vector), int(vector.sum())))

在这里插入图片描述

2.3 训练 SVM 进行垃圾邮件分类

完成特征提取函数后,ex6 spam.m 的下一步将加载预处理过的训练数据集,用于训练 SVM 分类器。spamTrain.mat 包含 4000 封垃圾邮件和非垃圾邮件的训练示例,而 spamTest.mat 包含 1000 封测试示例。每封原始电子邮件都使用 processEmail 和 emailFeatures 函数进行了处理,并转换成向量 x (i) ∈ R 1899。加载数据集后,ex6 spam.m 将继续训练 SVM,对垃圾邮件(y = 1)和非垃圾邮件(y = 0)进行分类。训练完成后,您会看到分类器的训练准确率约为 99.8%,测试准确率约为 98.5%。

# 导入需要的库
import scipy.io

# 加载训练集数据
mat1 = loadmat('data/spamTrain.mat')
X, y = mat1['X'], mat1['y']

# 加载测试集数据
mat2 = scipy.io.loadmat('data/spamTest.mat')
Xtest, ytest = mat2['Xtest'], mat2['ytest']
# 创建一个SVM分类器,使用线性核函数和C参数为0.1
clf = svm.SVC(C=0.1, kernel='linear')

# 使用训练数据训练SVM分类器
clf.fit(X, y)

2.4 垃圾邮件的主要预测因素

为了更好地理解垃圾邮件分类器的工作原理,我们可以检查参数,看看分类器认为哪些词最有可能是垃圾邮件。ex6 spam.m 的下一步会找出分类器中正值最大的参数,并显示相应的单词(图 12)。因此,如果一封电子邮件包含 “guarantee”、“remove”、"dollar "和 “price”(图 12 中显示的最大预测值)等词,则很可能被归类为垃圾邮件。
在这里插入图片描述

# 使用训练好的SVM模型计算训练集上的准确率
predTrain = clf.score(X, y)

# 使用训练好的SVM模型计算测试集上的准确率
predTest = clf.score(Xtest, ytest)

# 输出训练集和测试集的准确率
predTrain, predTest

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Jimmy Ding

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值