本篇博客主要讲解,吴恩达机器学习第七周的编程作业,包含两个实验,一是线性svm和带有高斯核函数的svm的基本使用;二是利用svm进行垃圾邮件分类。原始实验使用Matlab实现,本篇博客提供Python版本。
Ŀ¼
1.实验包含文件

| 文件名称 | 含义 |
| ex6.py | 实验1主程序 |
| ex6data1.mat | 实验1的数据集1 |
| ex6data2.mat | 实验1的数据集2 |
| ex6data3.mat | 实验1的数据集3 |
| plotData.py | 可视化2D数据集 |
| visualizeBoundary.py | 可视化决策边界 |
| gaussianKernel.py | 高斯核函数 |
| ex6_spam.py | 实验2垃圾邮件分类主程序 |
| spamTrain.mat | 邮件训练集 |
| spamTest.mat | 邮件测试集 |
| spamSample1.txt | 垃圾邮件1例子 |
| spamSample2.txt | 垃圾邮件2例子 |
| vocab.txt | 词汇表 |
| emailSample1.txt | 邮件1例子 |
| emailSample2.txt | 邮件2例子 |
| processEmail.py | 邮件预处理 |
| emailFeatures.py | 从邮件中提取特征 |
完成红色部分程序的关键代码。
- 打开实验1主程序ex6.py
'''第1部分 加载并可视化数据集''' print('Loading and Visualizing data ... ') data = scio.loadmat('ex6data1.mat') #加载矩阵格式的数据集 X = data['X'] #提取原始输入特征矩阵 y = data['y'].flatten() #提取标签 并转换为1维数组 m = y.size #样本数 # 可视化训练集 pd.plot_data(X, y) - 编写plotData.py
def plot_data(X, y): plt.figure() positive=X[y==1] #提取正样本 negtive=X[y==0] #提取负样本 plt.scatter(positive[:,0],positive[:,1],marker='+',label='y=1') #画出正样本 plt.scatter(negtive[:,0],negtive[:,1],marker='o',label='y=0') #画出负样本 plt.legend() #显示图例 - 数据集1可视化效果

数据集1只有两个原始输入特征,是线性可分的。
- 训练线性SVM
'''第2部分 训练SVM(线性核函数) 并可视化决策边界''' print('Training Linear SVM') #SVM的代价函数以及训练不用自己写 直接调用程序包 c = 1000 #SVM参数 可以通过改变他来观察决策边界的变化 clf = svm.SVC(c, kernel='linear', tol=1e-3) #声明一个线性SVM clf.fit(X, y) #训练线性SVM pd.plot_data(X, y) #可视化训练集 vb.visualize_boundary(clf, X, 0, 4.5, 1.5, 5) #可视化决策边界 - 查看可视化决策边界的程序visualizeBoundary.py
def visualize_boundary(clf, X, x_min, x_max, y_min, y_max): #x,y轴的取值范围 h = .02 xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h))#在x,y轴上以0.02为间隔,生成网格点 Z = clf.predict(np.c_[xx.ravel(), yy.ravel()])#预测每个网格点的类别0/1 Z = Z.reshape(xx.shape) #转型为网格的形状 plt.contour(xx, yy,Z, level=[0],colors='r') #等高线图 将0/1分界线(决策边界)画出来 - 查看决策边界
当参数C=1000比较大时,可以让我们对大间隔有一个直观的理解。但是如果训练集中存在异常点,C很大时模型会对异常点敏感,高方差(过拟合),如下所示:

这种情况下,应该把C调的小一些,此次会忽略异常点的影响,得到一个大间距的决策边界。
当C=10时:

当C=1时:

但不要把C调的太小,否则会出现高偏差(欠拟合).
- 利用高斯核函数计算向量相似度
'''第3部分 实现高斯核函数''' print('Evaluating the Gaussian Kernel') x1 = np.array([1, 2, 1]) x2 = np.array([0, 4, -1]) 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' '(for sigma = 2, this value should be about 0.324652'.format(sigma, sim)) - 编写高斯核函数gaussianKernel.py

def gaussian_kernel(x1, x2, sigma): x1 = x1.flatten() #z转换为1维数组 x2 = x2.flatten() sim = 0 sim=np.exp(((x1-x2).dot(x1-x2))/(-2*sigma*sigma)) return sim 与期望值进行比较,验证程序正确性:

- 可视化训练集2
'''第4部分 可视化数据集2''' print('Loading and Visualizing Data ...') data = scio.loadmat('ex6data2.mat')#加载矩阵格式的数据集2 X = data['X'] #提取原始输入特征矩阵 2个原始特征 y = data['y'].flatten() #提取标签 转换为1维数组 m = y.size #样本数 #可视化训练集2 pd.plot_data(X, y) - 可视化效果

很显然训练集2是线性不可分的,而且原始输入特征维度n=2,比较小,训练样本数m比较多,此时可以考虑使用带高斯核函数的SVM。
- 使用带有高斯核函数的SVM进行训练
'''第5部分 对训练集2使用带有高斯核函数的SVM进行训练''' print('Training SVM with RFB(Gaussian) Kernel (this may take 1 to 2 minutes) ...') #参数设置 c = 1 sigma = 0.1 #调用自己写的高斯核函数 返回新的特征向量矩阵 def gaussian_kernel(x_1, x_2): n1 = x_1.shape[0] n2 = x_2.shape[0] result = np.zeros((n1, n2)) for i in range(n1): for j in range(n2): result[i, j] = gk.gaussian_kernel(x_1[i], x_2[j], sigma) return result # clf = svm.SVC(c, kernel=gaussian_kernel) #使用自己手写的高斯核函数 clf = svm.SVC(c, kernel='rbf', gamma=np.power(sigma, -2)) #使用封装好的高斯核函数 rbf clf.fit(X, y) #进行训练 print('Training complete!') pd.plot_data(X, y) #可视化训练集 vb.visualize_boundary(clf, X, 0, 1, .4, 1.0) #可视化决策边界 注意当原始特征取值范围差异很大时,要先进行特征缩放,再使用核函数生成新的特征矩阵,再用SVM训练。不过上例中不需要特征缩放。
- 可视化分类效果

- 可视化训练集3
'''第6部分 可视化训练集3''' print('Loading and Visualizing Data ...') data = scio.loadmat('ex6data3.mat')#加载矩阵格式的数据集2 X = data['X']#提取原始输入特征矩阵 2个原始特征 y = data['y'].flatten()#提取标签 转换为1维数组 m = y.size #样本数 # 可视化训练集 pd.plot_data(X, y) - 训练集3可视化效果

训练集3存在一些异常点。
- 使用带有高斯核函数的SVM进行训练
'''第7部分 对训练集3使用带有高斯核函数的SVM进行训练''' clf = svm.SVC(c, kernel='rbf', gamma=np.power(sigma, -2)) #使用封装好的高斯核函数 rbf clf.fit(X, y)#进行训练 pd.plot_data(X, y) #可视化训练集 vb.visualize_boundary(clf, X, -.5, .3, -.8, .6) #可视化决策边界 - 可视化分类效果

4.垃圾邮件分类
- 打开主程序ex6_spam.py
'''第1部分 邮件预处理''' #提取邮件中的单词 并得到这些单词在词汇表中的序号 存储在数组中 print('Preprocessing sample email (emailSample1.txt) ...') file_contents = open('emailSample1.txt', 'r').read() #读入示例邮件内容 word_indices = pe.process_email(file_contents) #对邮件内容进行预处理 print('Word Indices: ') print(word_indices) - 编写邮件处理程序processEmail.py
def process_email(email_contents): vocab_list = get_vocab_list() #得到词汇字典 word_indices = np.array([], dtype=np.int64) #存储邮件中的单词在词汇标中的序号 word_list=[] #邮件预处理 email_contents = email_contents.lower() #将邮件内容转换为小写 #去除所有html标签 email_contents = re.sub('<[^<>]+>', ' ', email_contents) # 把邮件内容中任何数字替换为number email_contents = re.sub('[0-9]+', 'number', email_contents) # 把邮件中任何以http或https开头的url 替换为httpadddr 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) # 对邮件分词 print('==== Processed Email ====') stemmer = nltk.stem.porter.PorterStemmer() # 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) if len(token) < 1: continue if token in vocab_list.values(): word_list.append(list(vocab_list.keys())[list(vocab_list.values()).index(token)]) print(token) print('==================') word_indices=np.array(word_list,dtype=np.int64) return word_indices def get_vocab_list(): #得到词汇表 vocab_dict = {} #以字典形式获取 with open('vocab.txt') as f: #打开txt格式的词汇表 for line in f: (val, key) = line.split() #读取每一行的键和值 vocab_dict[int(val)] = key #存放到字典中 return vocab_dict - 邮件中的单词在词汇表中的序号

- 提取邮件特征
'''第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)) #向量中非0项的数量 - 编写特征提取程序emailFeatures.py
def email_features(word_indices): n = 1899 #词汇表中词的数量 特征向量大小 features = np.zeros(n + 1) #n+1 单词序号从1开始 不+1的话 出现序号为1899的单词 就会越界 features[word_indices]=1 features=features[1:] return features - 训练线性SVM进行垃圾邮件分类
'''第3部分 训练线性SVM进行垃圾邮件分类''' data = scio.loadmat('spamTrain.mat') #加载矩阵格式的邮件数据集 X = data['X'] #输入特征矩阵 y = data['y'].flatten() #标签 并转换为一维数组 0/1 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) #训练svm p = clf.predict(X) #在训练集上进行预测 print('Training Accuracy: {}'.format(np.mean(p == y) * 100)) #训练集上的准确率 
- 计算线性svm分类器在测试集上的准确率
'''第4部分 在测试集上测试线性svm分类器的准确率''' # 加载测试集 data = scio.loadmat('spamTest.mat') Xtest = data['Xtest'] ytest = data['ytest'].flatten() print('Evaluating the trained linear SVM on a test set ...') p = clf.predict(Xtest) #在测试集上的预测结果 print('Test Accuracy: {}'.format(np.mean(p == ytest) * 100)) #测试集上的准确率 
- 返回权重最大的前15个单词及其权重
'''第5部分 通过训练好的分类器 打印权重最高的前15个词 邮件中出现这些词更容易是垃圾邮件''' vocab_list = pe.get_vocab_list() #得到词汇表 存在字典中 indices = np.argsort(clf.coef_).flatten()[::-1] #对权重序号进行从大到小排序 并返回 print(indices) for i in range(15): #打印权重最大的前15个词 及其对应的权重 print('{} ({:0.6f})'.format(vocab_list[indices[i]], clf.coef_.flatten()[indices[i]])) 