在传统编程中,你需要对程序的行为进行硬编码。对于任何解决方案,第一个任务都是创建最合适的算法并编写代码。然后,必须设置输入参数,实际上,如果实现的算法是正确的,它将产生预期的结果。如下面流程图所示,在传统编程中,在对问题进行研究之后,对解决方案按照编写规则进行代码编写,之后对代码、算法进行评估。如果理想,正常运行从而产生预期的结果,如果不理想;分析错误重新对问题进行研究分析。
在机器学习中,你将大量内容留给机器去学习数据。机器学习的一个显著特征是不需要构建模型。这个复杂而有意义的任务是由机器学习算法执行的。如下图所示,与传统编程最大的区别在于机器学习把数据交给机器进行学习,而传统编程由程序员自己编写。
在我们进行机器学习时,数据是一般都会发生一些变化,比如说,产生一些新的数据,那我们就需要对数据进行一定的升级,然后对升级之后的数据重新进行训练。如果,我们按照之前的方法,需要的步骤就比较多,而且比较麻烦。我们可以对整个流程进行升级,让机器去适应这个流程,这样可以大大的减少我们工作。
应用机器学习技术来训练海量数据,可以帮助我们发 现那些此前并非立见端倪的模式,称为数据挖掘。
机器学习是指:让机器可以自主获得事物规律。要让机器可以 “学习”,必须将生活中的数据(包括但不限于图像、文字、语音)数值化,将不同事物的变化和关联转化为运算。
机器学习可以成立的原因是:概念和数值、关系和运算可以相互映射
机器学习三要素之数据、模型、算法
我们通过下面的例子来加深对机器学习原理的学习。
机器学习是一门多领域交叉学科,涉及概率论、统计学、逼近论、凸分析、算法复杂度理论等多门学科。
下图是对知识掌握与前景发展的大体结构图。
让机器像人一样看世界,看到图像,视频等媒体。会把图像进行识别,分类。
与机器进行语音交流,让机器明白你说什么
让机器像人一样理解语言,理解人写的文字等的含义。并作出一定的反应。
推荐系统主要解决信息过载的问题。它有两个目的:一是让用户找到感兴趣的物品;二是让物品出现在对它感兴趣的用户前。
数据挖掘主要是应用于大数据领域,利用机器学习的模型来挖掘数据中的潜在价值。发现数据之间的关系。
给机器各种特征描述,从而让机器对未知的事物进行判断
机器学习分为四种,分别是监督学习、无监督学习、半监督学习和增强学习。使用比较多的的是监督学习与无监督学习。
在解决问题的时候,必须考虑下面两个问题:一、使用机器学习算法的目的,想要算法完成何种任务;二、需要分析或者收集的数据是什么。
首先考虑使用机器学习算法的目的。如果想要预测目标变量的值,则可以选择监督学习算法,否则可以选择无监督学习算法,确定选择监督学习算法之后,需要进一步确定目标变量类型,如果目标变量是离散型,则可以选择分类算法;如果目标变量是连续的数值,则需要选择回归算法。
如果不想预测目标变量的值,则可以选择无监督算法。进一步分析是否需要将数据划分为离散的组。如果这是唯一的需求,则使用聚类算法。
利用无标签的数据学习数据的分布或数据与数 据之间的关系被称作无监督学习,也就是说,通过过往的一些数据的特征以及最终结果来进行训练的方式就是监督学习法,监督学习算法的训练数据源需要由特征值以及目标队列两部分组成。
用来训练模型,估计参数
用来测试和评估训练出来的模型好坏,不能用于训练模型
监督学习是从标记的训练数据来推断一个功能的机器学习任务。然后使用预测数据对训练出来的模型进行验证。
监督学习依赖于每个样本,可以得到每个特征序列映射到的确切的目标值是什么,所以常用于回归以及分类场景。常见的监督学习算法有:K近邻、朴素贝叶斯、决策树、随机森林、逻辑回归、线性回归、支持向量机SVM等。
KNN法即K近邻法,它的思路非常简单:找到训练集样本空间中的K个距离预测样本x最近的点,统计K个距离最近的点的类别,找出个数最多的类别,将x归入该类别。
本案例使用最著名的”鸢尾“数据集,该数据集曾经被Fisher用在经典论文中,目前作为教科书般的数据样本预存在Scikit-learn的工具包中。
from sklearn.datasets import load_iris
from sklearn.cross_validation import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import classification_report
from sklearn.model_selection import GridSearchCV
def knniris():
"""
鸢尾花分类
:return: None
"""
# 数据集获取和分割
lr = load_iris()
x_train, x_test, y_train, y_test = train_test_split(lr.data, lr.target, test_size= 0.25)
# 进行标准化
std = StandardScaler()
x_train = std.fit_transform(x_train)
x_test = std.transform(x_test)
# estimator流程
knn = KNeighborsClassifier()
# # 得出模型
# knn.fit(x_train,y_train)
#
# # 进行预测或者得出精度
# y_predict = knn.predict(x_test)
#
# # score = knn.score(x_test,y_test)
# 通过网格搜索,n_neighbors为参数列表
param = {"n_neighbors": [3, 5, 7]}
gs = GridSearchCV(knn, param_grid= param, cv= 10)
# 建立模型
gs.fit(x_train,y_train)
# print(gs)
# 预测数据
print(gs.score(x_test,y_test))
# 分类模型的精确率和召回率
# print("每个类别的精确率与召回率:",classification_report(y_test, y_predict,target_names=lr.target_names))
return None
if __name__ == "__main__":
knniris()
朴素贝叶斯(Naive Bayes)是一个非常简单,但是实用性很强的分类模型。朴素贝叶斯分类器的构造基础是贝叶斯理论。朴素贝叶斯分类器的主要思路:通过联合概率P(x,y)=P(x|y)P(y)建模,运用贝叶斯定理求解后验概率P(y|x);将后验概率最大者对应的的类别作为预测类别,因为后验概率最大化,可以使得期望风险最小化。
# 读取20类新闻文本的数据细节
from sklearn.datasets import fetch_20newsgroups
news = fetch_20newsgroups(subset= 'all')
# 20类新闻文本数据分割
from sklearn.cross_validation import train_test_split
X_train,X_test,y_train,y_test = train_test_split(news.data, news.target, test_size= 0.25,random_state= 42)
# 文本转换为特征向量进行TF特征抽取
from sklearn.feature_extraction.text import CountVectorizer
vec = CountVectorizer()
# 训练数据输入,并转换为特征向量
X_train = vec.fit_transform(X_train)
print(X_train)
# 测试数据转换
X_test = vec.transform(X_test)
print(X_test)
# 朴素贝叶斯分类器对文本数据进行类别预测
from sklearn.naive_bayes import MultinomialNB
# 使用平滑处理初始化的朴素贝叶斯模型
mnb = MultinomialNB(alpha=1.0)
# 利用训练数据对模型参数进行估计
mnb.fit(X_train,y_train)
# 对测试验本进行类别预测。结果存储在变量y_predict中
y_predict = mnb.predict(X_test)
print(y_predict)
决策树(decision tree)归纳是经典的分类算法。它采用自顶向下递归的方式构造决策树。可以从生成的决策树中提取规则。通常决策树学习包括三个步骤:决策树的生成和决策树的修剪。决策树的优缺点
优点:
缺点:
import pandas as pd
import sklearn
from sklearn.cross_validation import train_test_split
from sklearn.feature_extraction import DictVectorizer
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import classification_report
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
titanic = pd.read_csv('http://biostat.mc.vanderbilt.edu/wiki/pub/Main/DataSets/titanic.txt')
#选取一些特征作为我们划分的依据
x = titanic[['pclass', 'age', 'sex']]
y = titanic['survived']
# 填充缺失值
x['age'].fillna(x['age'].mean(), inplace= True)
# 分割数据集
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size= 0.25)
dt = DictVectorizer(sparse= False)
print(x_train.to_dict(orient= "record"))
# 按行,样本名字为键,列名也为键,[{"1":1,"2":2,"3":3}]
x_train = dt.fit_transform(x_train.to_dict(orient= "record"))
x_test = dt.fit_transform(x_test.to_dict(orient= "record"))
# 使用决策树
dtc = DecisionTreeClassifier()
dtc.fit(x_train, y_train)
dt_predict = dtc.predict(x_test)
print(dtc.score(x_test, y_test))
print(classification_report(y_test, dt_predict, target_names= ["died", "survived"]))
随机森林是一个包含多个决策树的分类器,并且其输出的类别是由个别树输出的类别的众数而定。个标准的决策树会根据每维特征对预测结果的影响程度进行排序,进而决定不同的特征从上至下构建分裂节点的顺序,如此以来,所有在随机森林中的决策树都会受这一策略影响而构建的完全一致,从而丧失的多样性。所以在随机森林分类器的构建过程中,每一棵决策树都会放弃这一固定的排序算法,转而随机选取特征。
根据下列算法而建造每棵树:
import pandas as pd
import sklearn
from sklearn.cross_validation import train_test_split
from sklearn.feature_extraction import DictVectorizer
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import classification_report
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
titanic = pd.read_csv('http://biostat.mc.vanderbilt.edu/wiki/pub/Main/DataSets/titanic.txt')
# 选取一些特征作为我们划分的依据
x = titanic[['pclass', 'age', 'sex']]
y = titanic['survived']
# 填充缺失值
x['age'].fillna(x['age'].mean(), inplace= True)
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size= 0.25)
dt = DictVectorizer(sparse= False)
print(x_train.to_dict(orient= "record"))
# 按行,样本名字为键,列名也为键,[{"1": 1, "2": 2, "3": 3}]
x_train = dt.fit_transform(x_train.to_dict(orient= "record"))
x_test = dt.fit_transform(x_test.to_dict(orient= "record"))
# 使用随机森林
rfc = RandomForestClassifier()
rfc.fit(x_train, y_train)
rfc_y_predict = rfc.predict(x_test)
print(rfc.score(x_test, y_test))
print(classification_report(y_test, rfc_y_predict, target_names= ["died", "survived"]))
逻辑回归(Logistic Regression),简称LR。它的特点是能够使我们的特征输入集合转化为0和1这两类的概率。一般来说,回归不用在分类问题上,因为回归是连续型模型,而且受噪声影响比较大。如果非要应用进入,可以使用逻辑回归。了解过线性回归之后再来看逻辑回归可以更好的理解。原始数据的下载地址为:https://archive.ics.uci.edu/ml/machine-learning-databases/breast-cancer-wisconsin/
Logistic回归优点:
缺点:
import numpy as np
import pandas as pd
from sklearn.metrics import roc_auc_score
from sklearn.preprocessing import StandardScaler # 标准化
from sklearn.linear_model import LogisticRegression # 逻辑回归
from sklearn.metrics import classification_report # 召回率
from sklearn.model_selection import train_test_split # 训练集 测试集 拆分
# 加载数据
data = pd.read_csv('data/breast-cancer-wisconsin.data', header= None)
# 列名称
columns = ['Sample code number', 'Clump Thickness', 'Uniformity of Cell Size', 'Uniformity of Cell Shape', 'Marginal Adhesion', 'SingleEpithelial Cell Size', 'Bare Nuclei', 'Bland Chromatin', 'Normal Nucleoli', 'Mitoses', 'Class']
data.columns = columns
print(data)
# 缺失值处理
data.replace('?',np.nan, inplace= True)
# 检测缺失值
res_null = data.isnull().sum()
# 删除缺失值
data.dropna(axis= 0, how= 'any', inplace= True)
# 进行筛选数据集 --- 去除编号的第一列
data = data.iloc[:,1:]
# 获取特征值
feature = data.iloc[:,:-1].values
target = data.iloc[:,-1].values
# 拆分数据
x_train, x_test, y_train, y_test = train_test_split(feature, target, test_size= 0.3, random_state= 1)
# 特征值需要标准化,目标值不需要标准化
stand = StandardScaler()
# 训练集的特征值
x_train = stand.fit_transform(x_train)
# 测试集的特征值
x_test = stand.fit_transform(x_test)
# 利用逻辑回归进行分类
lr = LogisticRegression() # 参考sgd下降优化
# 训练数据
lr.fit(x_train, y_train)
# 进行预测数据
y_predict = lr.predict(x_test)
# 准确率
score = lr.score(x_test, y_test)
# 获取权重与偏置
weight = lr.coef_
bias = lr.intercept_
# 计算召回率
# 召回率越高越好--查的全
# f1-score -越高越好---体现模型越稳健
res_report = classification_report(y_test, y_predict, labels= [2, 4], target_names= ["良性", "恶性"])
print(res_report)
# 如果 y_test>3设置为1,否则为0
y_test = np.where(y_test > 3, 1, 0)
print(y_test)
# 计算AUC指标 ---针对的样本不平衡的状态
auc = roc_auc_score(y_test, y_predict)
print(auc)
线性回归,是利用数理统计中回归分析,来确定两种或两种以上变量间相互依赖的定量关系的一种统计分析方法,运用十分广泛。
线性模型中的向量W值,客观的表达了各属性在预测中的重要性,因此线性模型有很好的解释性。对于这种“多特征预测”也就是(多元线性回归),那么线性回归就是在这个基础上得到这些W的值,然后以这些值来建立模型,预测测试数据。简单的来说就是学得一个线性模型以尽可能准确的预测实值输出标记。
实现简单,计算简单;
不能拟合非线性数据;
import numpy
import pandas
from matplotlib import pyplot
from sklearn.datasets import load_boston # 数据
from sklearn.preprocessing import StandardScaler # 标准化
from sklearn.linear_model import LinearRegression # 正规方程求解的线性回归
from sklearn.model_selection import train_test_split # 训练集 特征值拆分
def show_res(y_text,y_predict):
'''
结果展示
:param y_text: 测试集目标值真实值
:param y_predict: 预测值
:return:
'''
# 画布
pyplot.figure()
# 默认不支持中文,需要配置RC 参数
pyplot.rcParams['font.sans-serif'] = 'SimHei'
# 设置字体之后不支持负号,需要去设置RC参数更改编码
pyplot.rcParams['axes.unicode_minus'] = False
# 绘图
# 折线图
x = numpy.arange(0,len(y_predict))
pyplot.plot(x,y_test,marker='*')
pyplot.plot(x,y_predict,marker='.' )
# 增加标题
pyplot.title('房价预测与真实值的走势')
# 增加横轴,纵轴名称
pyplot.xlabel('x')
pyplot.ylabel('房价')
# 图例
pyplot.legend(['真实值','预测值'])
# 展示
pyplot.show()
# 加载数据
data = load_boston()
# 获取特征值
feature = data['data'] # 特征值
target = data['target'] #目标值
feature_names = data['feature_names'] # 特征值的列名
print(feature)
print(target)
print(feature_names)
print('*='*40)
# 返回值---先特征值(先训练集,再测试集),再目标值(先训练集,再测试集)
x_train,x_test,y_train,y_test = train_test_split(feature,target,test_size=0.3)
# 进行标准化---目标值是具体的房价,特征值是各个特征,--量级减小--W量级变大
# 目标值不需要标准化--特征值需要标准化
stand = StandardScaler()
# 先计算均值与标准差,在进行转化
x_train = stand.fit_transform(x_train)
x_test = stand.fit_transform(x_test)
# # 正规方程进行求解的线性回归---特征不是特别多,数据不是特别复杂的情况
# 进行构建模型---线性模型
lr = LinearRegression()
# 训练数据
lr.fit(x_train,y_train)
# 预测数据
y_predict = lr.predict(x_test)
# 计算准确率
score = lr.score(x_test,y_test)
# 获取权重和偏执
weight = lr.coef_
bias = lr.intercept_
# 绘图展示
show_res(y_test,y_predict)
支持向量机把分类问题转化为寻找分类平面的问题,并通过最大化分类边界点距离分类平面的距离来实现分类。故SVM法亦被称为最大边缘(maximum margin)算法。
SVM的主要思想可以概括为两点(源自百度百科):
它是针对线性可分情况进行分析,对于线性不可分的情况,通过使用非线性映射算法将低维输入空间线性不可分的样本转化为高维特征空间使其线性可分,从而使得高维特征空间采用线性算法对样本的非线性特征进行线性分析成为可能。
它基于结构风险最小化理论之上在特征空间中构建最优超平面,使得学习器得到全局最优解,并且在整个样本空间的期望以某个概率满足一定上界。
优点
缺点
from sklearn import svm
import numpy as np
import matplotlib.pyplot as plt
import matplotlib
import sklearn
from sklearn.model_selection import train_test_split
# 1.读取数据集
from sklearn.datasets import load_iris
data = load_iris()
#data = np.loadtxt(path, dtype=float, delimiter=',', converters={4: Iris_label})
# converters={4:Iris_label}中“4”指的是第5列:将第5列的str转化为label(number)
# print(data.shape)
# 2.划分数据与标签
x = data.data
y = data.target# x为数据,y为标签
print(y.shape)
x = x[:, 0:2]
train_data, test_data, train_label, test_label = train_test_split(x, y, random_state=1, train_size=0.6,
test_size=0.4) # sklearn.model_selection.
# print(train_data.shape)
# 3.训练svm分类器
classifier = svm.SVC(C=2, kernel='rbf', gamma=10, decision_function_shape='ovo') # ovr:一对多策略
classifier.fit(train_data, train_label.ravel()) # ravel函数在降维时默认是行序优先
# 4.计算svc分类器的准确率
print("训练集:", classifier.score(train_data, train_label))
print("测试集:", classifier.score(test_data, test_label))
# 也可直接调用accuracy_score方法计算准确率
from sklearn.metrics import accuracy_score
tra_label = classifier.predict(train_data) # 训练集的预测标签
tes_label = classifier.predict(test_data) # 测试集的预测标签
print("训练集:", accuracy_score(train_label, tra_label))
print("测试集:", accuracy_score(test_label, tes_label))
# 查看决策函数
print('train_decision_function:\n', classifier.decision_function(train_data)) # (90,3)
print('predict_result:\n', classifier.predict(train_data))
训练样本不依赖于打标数据的机器学习算法,它主要是用来解决一些聚类场景的问题。相较于监督学习,无监督学习的一大好处就是不依赖于打标数据。
非监督学习没有训练集,只有一组数据,直接训练改组数据,在该组数据集内寻找规律。
通过训练出来的工具,来测试数据,等到结果。
模型学习的数据没有标签,因此无监督学习的目标是通过对这些无标签样本的学习来揭示数据的内在特性及规律,
无监督学习,顾名思义,就是不受监督的学习,一种自由的学习方式。该学习方式不需要先验知识进行指导,而是不断地自我认知,自我巩固,最后进行自我归纳,在机器学习中,无监督学习可以被简单理解为不为训练集提供对应的类别标识
在有监督学习中,我们把对样本进行分类的过程称之为分类(Classification),而在无监督学习中,我们将物体被划分到不同集合的过程称之为聚类(Clustering)
聚类算法
可视化和降维
关联规则学习
K-means通常被称为劳埃德算法,这在数据聚类中是最经典的,也是相对容易理解的模型。算法执行的过程分为4个阶段。
K均值等于具有小的全对称协方差矩阵的期望最大化算法
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.cluster import KMeans
from sklearn.datasets.samples_generator import make_blobs
from sklearn import metrics
import matplotlib.pyplot as plt
x,y = make_blobs(n_samples=1000,n_features=4,centers=[[-1,-1],[0,0],[1,1],[2,2]],cluster_std=[0.4,0.2,0.2,0.4],random_state=10)
k_means = KMeans(n_clusters=3, random_state=10)
k_means.fit(x)
y_predict = k_means.predict(x)
plt.scatter(x[:,0],x[:,1],c=y_predict)
plt.show()
print(k_means.predict((x[:30,:])))
print(metrics.calinski_harabaz_score(x,y_predict))
print(k_means.cluster_centers_)
print(k_means.inertia_)
print(metrics.silhouette_score(x,y_predict))
层次聚类是聚类算法的一种,通过计算不同类别数据点间的相似度来创建一棵有层次的嵌套聚类树。在聚类树中,不同类别的原始数据点是树的最低层,树的顶层是一个聚类的根节点。创建聚类树有自下而上合并和自上而下分裂两种方法。
EM算法(最大期望算法) 是在依赖于无法观测的隐藏变量的概率模型中,寻找参数最大似然估计或者最大后验估计的算法。EM的求解思路。
我们先根据经验为每个类别(即隐藏变量)赋予一个初始分布,这相当于是假定了分布参数。然后根据分布的参数可以求取每个数据元组的隐藏变量的期望(相当于实施了归类操作)
再根据归类结果计算分布参数(向量)的最大似然值,然后根据这个最大似然值在反过来重新计算每个元组的隐藏变量的期望。这样循环往复,最终如果隐藏变量的期望与参数的最大似然值趋于稳定了,EM算法就算是执行完毕了。
PCA即主成分分析,是图像处理中经常用到的降维方法,PCA的主要思想是将n维特征映射到k维上,这k维是全新的正交特征也被称为主成分,是在原有n维特征的基础上重新构造出来的k维特征。PCA有两种通俗易懂的解释:(1)最大方差理论;(2)最小化降维造成的损失。
LLE属于流形学习的一种。因此我们首先看看什么是流形学习。流形学习是一大类基于流形的框架。数学意义上的流形比较抽象,不过我们可以认为LLE中的流形是一个不闭合的曲面。这个流形曲面有数据分布比较均匀,且比较稠密的特征,有点像流水的味道。基于流行的降维算法就是将流形从高维到低维的降维过程,在降维的过程中我们希望流形在高维的一些特征可以得到保留。
LLE是广泛使用的图形图像降维方法,它实现简单,但是对数据的流形分布特征有严格的要求。比如不能是闭合流形,不能是稀疏的数据集,不能是分布不均匀的数据集等等,这限制了它的应用。下面总结下LLE算法的优缺点。
优点
缺点
T – 分布随机近邻嵌入(t – SNE)是一种用于降维的机器学习方法,它能帮我们识别相关联的模式。t – SNE 主要的优势就是保持局部结构的能力。这意味着高维数据空间中距离相近的点投影到低维中仍然相近。t-SNE 能生成漂亮的可视化。
特点
t – SNE 在低维空间下使用更重长尾分布的 t 分布来避免 crowding 问题和优化问题。
优点
缺点
Apriori算法是常用于挖掘出数据关联规则的算法,Eclat 垂直数据格式有效挖掘频繁项集,主要用于数据挖掘,将在下一章“海量数据处理与挖掘”中进行详解。
以上是两种常见的机器学习方式,监督与无监督的所有内容,下面简单的介绍一下,其他两种不常用的机器学习方式,半监督与强化学习(了解即可)。
是一种比较复杂的机器学习种类, 强调的是系统与外界不断地交互,获得外界的反馈,然后决定自身的行为。强化学习主要是针对流程中不断需要推理的场景,比如:隐马尔可夫。在这种学习模式下,输入数据作为对模型的反馈,不像监督模型那样,输入数据仅仅是作为一个检查模型对错的方式,在强化学习下,输入数据直接反馈到模型,模型必须对此立刻作出调整。常见的应用场景包括动态系统以及机器人控制等。
通过对样本的部分打标来进行机器学习算法的使用,很多半监督学习算法都是监督学习算法的变形。半监督学习解决的是一些打标数据比较难获得的分类场景,比如:标签传播。在此学习方式下,输入数据部分被标识,部分没有被标识,这种学习模型可以用来进行预测,但是模型首先需要学习数据的内在结构以便合理的组织数据来进行预测。应用场景包括分类和回归,算法包括一些对常用监督式学习算法的延伸,这些算法首先试图对未标识数据进行建模,在此基础上再对标识的数据进行预测。
在企业数据应用的场景下, 人们最常用的可能就是监督式学习和非监督式学习的模型。 在图像识别等领域,由于存在大量的非标识的数据和少量的可标识数据, 目前半监督式学习是一个很热的话题。 而强化学习更多的应用在机器人控制及其他需要进行系统控制的领域。
复杂问题而言,数据比算法更重要
获得额外数据成本并不低廉
中小型数据很普遍
需要算法优化
“数据决定了机器学习的上限,而算法只是尽可能逼近这个上限”,这句话很好的阐述了数据在机器学习中的重要性。机器学习算法需要大量的数据来完成任务。即使是简单的任务,也需要大量的样本,对于复杂的问题,数据要比算法重要得多,但是通常数据集都是小型的,或者中等大小的数据集,获取额外的数据并不便宜,也不简单,如果我们无法在增加数据,就需要对我们的算法进行优化。
样本集太小,非代表性数据被选中,出现采样噪声
样本集太大,如果采样方式欠妥,会造成采样偏差
为了使得模型泛化足够好,训练数据要包含泛化到新数据的特征。如果用的训练集表征性不够,那么训练得到的模型就不太可能得到精确的预测值。但这个要求其实很高。一方面,如果数据样本太小,则很容易受样本噪音的影响;另一方面,即使是样本数量足够大,如果采样方法不够好,也得不到具有表征性的样本数据。这个问题被称作采样偏差。简言之,在训练时看到的数据要能够代表数据背后的模式。
质量差:训练集中错误,异常值,噪声多
处理:
这个比较明显,如果你的训练数据充满了错误,异常以及噪音,那你的模型很难习得数据背后的模式,你的模型也就不可能表现得好。花费时间和精力来清洗训练数据是十分必要,且重要的。
训练数据中包含足够多的相关特征,以及较少的无关特征
处理:
在进行特征选择中,我们需要提出一些对数据没有影响的特征,将有用的特征进行选择,将已经选好的特征进行整合,提取更有用的特征。以此来降低维度。
过拟合问题,根本的原因则是特征维度过多,导致拟合的函数完美的经过训练集,但是对新数据的预测结果则较差。增加模型的复杂度,不要用简单的线性回归,适当的采用二次回归,将训练集合扩大,采集更多的数据。
欠拟合问题,根本的原因是特征维度过少,导致拟合的函数无法满足训练集,误差较大。欠拟合问题可以通过增加特征维度来解决。
MNIST手写体数字数据库有一个包含60,000个示例的训练集和一个包含10,000个示例的测试集。它是NIST提供的更大集合的子集。数字的大小已标准化并集中在固定大小的图像中。数据集下载
import struct
struct.pack('>i',2) # 将2打包
import struct
with open('./MNIST_data/train-images-idx3-ubyte', 'rb') as f:
buffer = f.read(4*4) # 4个int
# 解析出4个int类型
head = struct.unpack('>iiii',buffer)
print(head)
# 读取图片的像素
length = head[1] * head[2] * head[3]
print(length)
buffer = f.read(length)
# 解压像素包
data = struct.unpack('>{}B'.format(length),buffer)
# 将数据集规划为一个三维矩阵
import numpy as np
imgs = np.reshape(data, (head[1], head[2], head[3]))
# 显示图片
import matplotlib.pyplot as plt
for i in range(5):
plt.imshow(imgs[i], cmap = 'gray')
plt.show()
# 手写字,读取简单
from sklearn.datasets import fetch_mldata
mnist = fetch_mldata('MNIST original', data_home= './')
# DESCR 数据集描述
# data 包含一个数组,每个实例为一行,每个特征为一列
# target 包含一个带有标签的数组
# 提取数据
X, y = mnist['data'], mnist['target']
# 展示数据
%matplotlib inline
import matplotlib
import matplotlib.pyplot as plt
some_digit = X[36000]
some_digit_image = some_digit.reshape(28, 28)
plt.imshow(some_digit_image, cmap = matplotlib.cm.binary)
plt.show()
# 拆分测试集与训练集
X_train, X_test, y_train, y_test = X[:60000], X[60000:], y[:60000], y[60000:]
# 将数据集合交叉洗牌,交叉验证时,每个子集合数据分布均匀,有些机器学习算法对训练实例的顺序敏感
import numpy as np
shuffle_index = np.random.permutation(60000)
shuffle_index
# 重新赋值训练集
X_train, y_train = X_train[shuffle_index], y_train[shuffle_index]
# 识别数字5 ,二元分类5或者非5
# 创建目标向量
y_train_5 = (y_train == 5)
y_train_5.reshape(20, -1)
y_test_5 = (y_test == 5)
# SGD 梯度下降 分类器, 适合非常大的数据集,独立处理训练集数据,一次一个,适合在线学习,
from sklearn.linear_model import SGDClassifier
sgd_clf = SGDClassifier(random_state = 42)
# 调用模型
sgd_clf.fit(X_train, y_train_5)
# 预测数据
sgd_clf.predict([some_digit])
基本思想就是将原始数据进行分组,一部分做为训练集来训练模型,另一部分做为测试集来评价模型。
交叉验证用于评估模型的预测性能,尤其是训练好的模型在新数据上的表现,可以在一定程度上减小过拟合。
还可以从有限的数据中获取尽可能多的有效信息。
# 评估分类器比评估回归器要困难得多
# 3个折叠(cv),正确率达到 95% 以上
from sklearn.model_selection import cross_val_score
cross_val_score(sgd_clf, X_train, y_train_5, cv=3, scoring="accuracy")
# 把每张图都分类成 非5
# 定义一个估算器
from sklearn.base import BaseEstimator
class Never5Classifier(BaseEstimator):
def fit(self, X, y=None):
pass
def predict(self, X):
"""
预测
"""
return np.zeros((len(X), 1), dtype=bool)
# X 所有图片数据
np.zeros((len(X), 1), dtype=bool)
never_5_clf = Never5Classifier()
# 进行交叉验证
cross_val_score(never_5_clf, X_train, y_train_5, cv=3, scoring="accuracy")
# 准确率超过90% ,因为5的图像大约只有10%,你猜一张图不是5, 90%的时间你都是正确的
# 这说明准确率无法成为分类器的首要性能指标,特别是当你处理偏科数据集, 某些类比其他类更为频繁
评估分类器性能的更好方法是混淆矩阵,混淆矩阵是ROC曲线绘制的基础,同时它也是衡量分类型模型准确度中最基本,最直观,计算最简单的方法。
混淆矩阵就是分别统计分类模型归错类,归对类的观测值个数,然后把结果放在一个表里展示出来。这个表就是混淆矩阵。混淆矩阵其实就是一张表格而已。
# 想要知道分类器将数字3和数字5混淆多少次,通过混淆矩阵的5行3列
from sklearn.model_selection import cross_val_predict
y_train_pred = cross_val_predict(sgd_clf, X_train, y_train_5, cv=3)
### 与 cross_val_score 相比
# 同样执行交叉验证
# 返回的不是评估分数,是每个折叠的预测
# 每一个实例在模型预测时使用的数据,在训练期间从未见过
# 混淆矩阵
from sklearn.metrics import confusion_matrix
confusion_matrix(y_train_5, y_train_pred)
# 行表示实际类别,列表示预测类别
# 第一行 第一列 53272 被正确的分为 非5 ,真负类
# 第一行 第二列 1307 被错误的分类成 5 ,假正类
# 第二行 第一列 1077 张被错误的分为 非5, 假负类
# 第二行 第二列 4344 张被正确的分在了5 ,真正类
# 这种衡量方式太复杂,我们可以用更简单的指标
y_train_perfect_predictions = y_train_5
confusion_matrix(y_train_5, y_train_perfect_predictions)
公式 | |
---|---|
准确率 | |
精度 | |
召回率 |
## 精度和召回率
from sklearn.metrics import precision_score, recall_score
precision_score(y_train_5, y_train_pred) # 4327 / 4327 + 1276
recall_score(y_train_5, y_train_pred) # 4327 / 4327 + 1094
# 说明 检测一张图的时候,只有90%的概率是准确的,而且只有64%的数字5 被它检测出来
# 精度和召回率合成单一指标,成为 F1 分数,谐波平均值
# 平均值平等对待所有的值,谐波平均值会给予较低值更高的权重,只有召回率和精度都很高时,才能获得较高的F1分数
from sklearn.metrics import f1_score
f1_score(y_train_5, y_train_pred)
# F1分数对那些具有相近精度和召回率 分类器更有利,这不一定符合你的期望
# 有时候你更关心精度,有时你能关心召回率
# 训练一个分类器检测儿童可以放心观看的视频,你可能要求拦截了很多好的视频,低召回率,保留下来的都是安全的视频,高精度
# 不能同时增加精度并减少召回率,反之亦然
SGD线性模型
阀值设置
# 用predict_proba得到每个实例属于正类的概率,然后对概率切一下。
# 以LogisticRegression为例
clf = LogisticRegression()
clf.fit(X_train, y_train)
pred_proba = clf.predict_proba(X_test)[:, 1]
threshold = 0.75 # 阀值设置为0.75
pred_label = pred_proba > threshold
# pred_proba是每个实例为真的概率
# 假设阈值是0.75
# pred_label里True就是概率大于0.75的
# 返回决策值decision_function
y_scores = sgd_clf.decision_function([some_digit])
threshold = 0
y_some_digit_pred = (y_scores > threshold)
# 提高阀值可以降低召回率,提高阀值到200000,就错了这个图
threshold = 200000
y_some_digit_pred = (y_scores > threshold)
y_some_digit_pred
# 返回决策值
y_scores = cross_val_predict(sgd_clf, X_train, y_train_5, cv= 3,
method= "decision_function")
# 有了y_scores,可以计算所有可能的阀值的精度和召回率
y_scores.shape
from sklearn.metrics import precision_recall_curve
precisions, recalls, thresholds = precision_recall_curve(y_train_5, y_scores)
# 使用matplotlib 绘制精度和召回相对于阀值的函数图
def plot_precision_recall_vs_threshold(precisions, recalls, thresholds):
plt.plot(thresholds, precisions[:-1], "b--", label= "Precision", linewidth= 2)
plt.plot(thresholds, recalls[:-1], "g-", label= "Recall", linewidth= 2)
plt.xlabel("Threshold", fontsize= 16)
plt.legend(loc="upper left", fontsize= 16)
plt.ylim([0, 1])
plt.figure(figsize=(8, 4))
plot_precision_recall_vs_threshold(precisions, recalls, thresholds)
plt.xlim([-700000, 700000])
plt.show()
plt.rcParams['font.sans-serif'] = ['SimHei']
def plot_precision_vs_recall(precisions, recalls):
plt.plot(recalls, precisions, "b-", linewidth=2)
plt.xlabel("召回", fontsize=16)
plt.ylabel("精度", fontsize=16)
plt.axis([0, 1, 0, 1])
plt.figure(figsize=(8, 6))
plot_precision_vs_recall(precisions, recalls)
plt.show()
## 通过选择阀值来实现最佳的精度/召回率权衡
# 目标设定为90%的精度,阀值大概在30000左右 , 设置了阀值为30000
y_train_pred_90 = (y_scores > 30000)
precision_score(y_train_5, y_train_pred_90)
recall_score(y_train_5, y_train_pred_90)
ROC的全称是Receiver Operating Characteristic Curve,中文名字叫“受试者工作特征曲线”,顾名思义,其主要的分析方法就是画这条特征曲线。
ROC曲线的应用场景有很多,其最直观的应用就是能反映模型在选取不同阈值的时候其敏感性和其精确性的趋势走向。不过,相比于其他的P-R曲线(精确度和召回率),ROC曲线有一个巨大的优势就是,当正负样本的分布发生变化时,其形状能够基本保持不变,而P-R曲线的形状一般会发生剧烈的变化,因此该评估指标能降低不同测试集带来的干扰,更加客观的衡量模型本身的性能。
from sklearn.metrics import roc_curve
fpr, tpr, thresholds = roc_curve(y_train_5, y_scores)
plt.rcParams['font.sans-serif'] = ['SimHei']
def plot_roc_curve(fpr, tpr, label=None):
plt.plot(fpr, tpr, linewidth=2, label=label)
plt.plot([0, 1], [0, 1], 'k--')
plt.axis([0, 1, 0, 1])
plt.xlabel('假正类率', fontsize=16)
plt.ylabel('真正类率', fontsize=16)
plt.figure(figsize=(8, 6))
plot_roc_curve(fpr, tpr)
plt.show()
# 计算曲线下面积AUC,虚线是随机分类0.5到1
from sklearn.metrics import roc_auc_score
roc_auc_score(y_train_5, y_scores)
# 召回率TPR越高,分类器的假正类FPR就越多
# 虚线表示纯随机分类器的ROC曲线,好的分类器应该远离这条线,向左上角
# 是使用精度/召回率 PR曲线,还是使用ROC,关键在于正类非常少或者更关注假正类而不是假负类,选择PR,反之ROC
# 例如:前面例子ROC曲线很不错是因为跟负类 非5 相比, 正类 数据5 数量真的很少
# 获取训练集中每个实例的分数
# RandomForestClassifier 没有descision_function(),但是拥有dict_proda()方法,sklearn中分类器都有这两个中的一个
# dict_proda返回一个矩阵,每行一个实例,每列代表一个类别的概率,比如这个图片 70%是5
from sklearn.ensemble import RandomForestClassifier
forest_clf = RandomForestClassifier(n_estimators=10, random_state=42)
y_probas_forest = cross_val_predict(forest_clf, X_train, y_train_5, cv=3,
method="predict_proba")
# 绘制ROC曲线,需要决策值不是概率,直接使用正类的概率作为决策值:
y_scores_forest = y_probas_forest[:, 1]
fpr_forest, tpr_forest, thresholds_forest = roc_curve(y_train_5,y_scores_forest)
# 绘制图形
plt.figure(figsize=(8, 6))
plt.plot(fpr, tpr, "b:", linewidth=2, label="SGD")
plot_roc_curve(fpr_forest, tpr_forest, "Random Forest")
plt.legend(loc="lower right", fontsize=16)
plt.show()
# Rand 比SGD 好很多,ROC AUC的分数也高很多
roc_auc_score(y_train_5, y_scores_forest)
# 再看一下 精度和召回率 也很高
y_train_pred_forest = cross_val_predict(forest_clf, X_train, y_train_5, cv=3)
precision_score(y_train_5, y_train_pred_forest)
recall_score(y_train_5, y_train_pred_forest)
将数字图片分类0到9,训练10个二元分类器,每个数字一个,检测一张图片时,获取每个分类器的决策分数,哪个最高属于哪个,称为一对多OvA
将 N 个类别两两配对,形成 N(N-1)/2 个二分类任务。在测试阶段,新样本被提交给所有二分类器,然后我们将得到 N(N-1)/2 个分类结果,最终结果可通过投票产生:即把被预测得最多的类别作为最终分类结果。
# 重新训练模型
sgd_clf.fit(X_train, y_train)
sgd_clf.predict([some_digit])
# 内部实际上训练了10个二元分类器,获得图片的决策分数,然后选择了分数最高的类别
# 返回10个分数,每个类别1个
some_digit_scores = sgd_clf.decision_function([some_digit])
np.argmax(some_digit_scores)
# 目标类别列表会存储在classes_这个属性中,按值大小排列,
sgd_clf.classes_
sgd_clf.classes_[5]
# 使用OvO策略,一对一或者一对多
from sklearn.multiclass import OneVsOneClassifier
ovo_clf = OneVsOneClassifier(SGDClassifier(max_iter=5, tol=-np.infty, random_state=42))
ovo_clf.fit(X_train, y_train)
ovo_clf.predict([some_digit])
# 使用随机森林
forest_clf.fit(X_train, y_train)
forest_clf.predict([some_digit])
# 随机森林直接将实例分为多个类别,调用predict_proba()可以获得分类器将每个实例分类为每个类别的概率列表
forest_clf.predict_proba([some_digit])
# 使用交叉验证评估SGD的准确率
cross_val_score(sgd_clf, X_train, y_train, cv=3, scoring="accuracy")
# 将输入进行简单缩放 ,可以得到准确率 90 %以上
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train.astype(np.float64))
cross_val_score(sgd_clf, X_train_scaled, y_train, cv=3, scoring="accuracy")
## 错误分析
### 项目流程
1. 探索数据准备的选项
2. 尝试多个模型
3. 选择最佳模型并用GridSearchCV对参数进行微调
4. 尽可能自动化
### 确定了一个相对合适的模型,进一步优化,分析其错误类型
* 查看混淆矩阵
* 使用cross_val_predict()进行预测
* 调用confusion_matrix()
y_train_pred = cross_val_predict(sgd_clf, X_train_scaled, y_train, cv=3)
conf_mx = confusion_matrix(y_train, y_train_pred)
conf_mx
# 使用matplotlib的matshow 函数来查看混淆矩阵的图像表示
plt.matshow(conf_mx, cmap = plt.cm.gray)
plt.show()
# 看起来不错,大多数图片都在主对角线上,说明它们被正确分类
# 数字5 看起来比较暗,说明1. 数字5图片较少 2. 分类器在数字5上执行效果不如其他数字上好
# 假设把焦点放在错误上,为取得错误率,而不是错误绝对值,需要将混淆矩阵中每个值除以相应类别中的图片数量
row_sums = conf_mx.sum(axis=1, keepdims=True)
norm_conf_mx = conf_mx / row_sums
# 用0填充对角线 只保留错误,重新绘制
np.fill_diagonal(norm_conf_mx, 0)
plt.matshow(norm_conf_mx, cmap=plt.cm.gray)
plt.show()
# 尝试多收集这些数字的训练集
# 开发一些新特征来改进分类器
# 优化分类器算法
# 使用pillow或opencv对图片预处理,让显示模型更突出
# 分析单个错误
def plot_digits(instances, images_per_row=10, **options):
size = 28
images_per_row = min(len(instances), images_per_row)
images = [instance.reshape(size,size) for instance in instances]
n_rows = (len(instances) - 1) // images_per_row + 1
row_images = []
n_empty = n_rows * images_per_row - len(instances)
images.append(np.zeros((size, size * n_empty)))
for row in range(n_rows):
rimages = images[row * images_per_row : (row + 1) * images_per_row]
row_images.append(np.concatenate(rimages, axis= 1))
image = np.concatenate(row_images, axis= 0)
plt.imshow(image, cmap = matplotlib.cm.binary, **options)
plt.axis("off")
# 查看数字3和数字5的例子
cl_a, cl_b = 3, 5
X_aa = X_train[(y_train == cl_a) & (y_train_pred == cl_a)]
X_ab = X_train[(y_train == cl_a) & (y_train_pred == cl_b)]
X_ba = X_train[(y_train == cl_b) & (y_train_pred == cl_a)]
X_bb = X_train[(y_train == cl_b) & (y_train_pred == cl_b)]
plt.figure(figsize=(8,8))
plt.subplot(221);
plot_digits(X_aa[:25], images_per_row= 5)
plt.subplot(222);
plot_digits(X_ab[:25], images_per_row= 5)
plt.subplot(223);
plot_digits(X_ba[:25], images_per_row= 5)
plt.subplot(224);
plot_digits(X_bb[:25], images_per_row= 5)
plt.show()
# 左侧两个是被分类为3的图片
# 右侧两个是被分类为5的图片
# 大多数错误分类的图片看起来还是非常明显的错误
# 原因:SGD是一个线性模型,它所做就是为每个像素分配一个各个类别的权重,当它看到新的图像,将加权后的像素强度汇总,从而得到一个分数进行分类
# 数字3和5在一部分像素位上有区别,所以分类器很容易将其弄混
# 通过上面图像,如果书写3 的连接点左移,分类器可能将其分类为数字5,这个分类器对图像位移和旋转敏感
# 减少混淆的方法之一,就是对图像进行预处理,确保位于中心位置并且没有旋转
目前有很多关于多标签的学习算法,依据解决问题的角度,这些算法可以分为两大类:一是基于问题转化的方法,二是基于算法适用的方法。基于问题转化的方法是转化问题数据,使之使用现有算法;基于算法适用的方法是指针对某一特定的算法进行扩展,从而能够处理多标记数据,改进算法,适用数据。
# 为每个实例产生多个类别 ,例如 照片识别多个人脸
# 分类器经过训练可以识别小红,小白,小军,一张照片 里有 小红,小白
# 经过分类器,应该输出[1,1,0], 是小红,是小白,不是小军
# 输出多个二元标签的分类系统称为多标签分类系统
from sklearn.neighbors import KNeighborsClassifier
y_train_large = (y_train >= 7)
y_train_odd = (y_train % 2 == 1)
y_multilabel = np.c_[y_train_large, y_train_odd]
knn_clf = KNeighborsClassifier()
knn_clf.fit(X_train, y_multilabel)
# knn支持多标签分类,不是所有的分类器都支持
knn_clf.predict([some_digit])
# # 评估多标签分类器方法很多,方法之一就是测量每个标签的F1分数,或者其他二元分类器指标,然后简单平均
# y_train_knn_pred = cross_val_predict(knn_clf, X_train, y_multilabel, cv=3)
# f1_score(y_multilabel, y_train_knn_pred, average="macro")
0.977090
# 上面假设了所有标签都同等重要,也可以给每个标签设置一个等于自身支持的权重(该目标标签实例的数量),设置average='weighted'
同时预测一个输入样例的多个离散输出值,其中多个输出可由多种离散变量(如二元变量、名义变量和有序变量)表征。
### 例子:构建一个系统去除图片中的噪声,输入一张有噪声的图片,它将输入一张干净的数字图片,分类器输出是多个标签,一个像素一个标签,每个标签多个值0到255
# 增加噪声,目标将图片还原为原始图片 创建训练集和测试集
noise = np.random.randint(0, 100, (len(X_train), 784))
X_train_mod = X_train + noise
noise = np.random.randint(0, 100, (len(X_test), 784))
X_test_mod = X_test + noise
y_train_mod = X_train
y_test_mod = X_test
some_index = 5500
plt.subplot(121);plt.imshow(X_test_mod[some_index].reshape(28, 28), cmap = matplotlib.cm.binary)
plt.subplot(122);plt.imshow(y_test_mod[some_index].reshape(28, 28), cmap = matplotlib.cm.binary)
plt.show()
knn_clf.fit(X_train_mod, y_train_mod)
clean_digit = knn_clf.predict([X_test_mod[some_index]])
plt.imshow(clean_digit.reshape(28, 28), cmap = matplotlib.cm.binary)
plt.show()
# 垃圾邮件分类
# 加载数据
import os
SPAM_PATH = os.path.join("datasets", "spam")
# 正常文件与垃圾文件
HAM_DIR = os.path.join(SPAM_PATH, "easy_ham")
SPAM_DIR = os.path.join(SPAM_PATH, "spam")
# 获得文件名
ham_filenames = [name for name in sorted(os.listdir(HAM_DIR)) if len(name) > 20]
spam_filenames = [name for name in sorted(os.listdir(SPAM_DIR)) if len(name) > 20]
# 我们可以使用python的“email”模块解析这些电子邮件(它处理邮件头、编码等)
import email
import email.policy
# 读取文件
def load_email(is_spam, filename, spam_path=SPAM_PATH):
directory = "spam" if is_spam else "easy_ham"
with open(os.path.join(spam_path, directory, filename), "rb") as f:
return email.parser.BytesParser(policy=email.policy.default).parse(f)
# 让我们看一个ham示例和一个spam示例,了解数据的外观:
ham_emails = [load_email(is_spam=False, filename=name) for name in ham_filenames]
spam_emails = [load_email(is_spam=True, filename=name) for name in spam_filenames]
# 查看正常文件
print(ham_emails[1].get_content().strip())
# 查看垃圾文件
print(spam_emails[6].get_content().strip())
# 电子邮件实际上有很多部分,带有图像和附件(它们可以有自己的附件)。查看邮件的各种类型的结构:
def get_email_structure(email):
if isinstance(email, str):
return email
payload = email.get_payload()
if isinstance(payload, list):
return "multipart({})".format(", ".join([
get_email_structure(sub_email)
for sub_email in payload
]))
else:
return email.get_content_type()
# 计算邮件中出现 get_content_type类型的次数
from collections import Counter
def structures_counter(emails):
structures = Counter()
for email in emails:
structure = get_email_structure(email)
structures[structure] += 1
return structures
structures_counter(ham_emails).most_common()
structures_counter(spam_emails).most_common()
# 正常邮件更多的是纯文本,而垃圾邮件有相当多的HTML。
# 查看邮件头
for header, value in spam_emails[0].items():
print(header,":",value)
# 里面可能有很多有用的信息,比如发件人的电子邮件地址([email protected]看起来很可疑),
# 查看“主题”标题:
spam_emails[0]["Subject"]
# 拆分训练集和测试集合
import numpy as np
from sklearn.model_selection import train_test_split
X = np.array(ham_emails + spam_emails)
y = np.array([0] * len(ham_emails) + [1] * len(spam_emails))
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# 首先需要一个函数来将html转换为纯文本,使用[Beautifulsoup]库,下面的函数首先删除``部分,然后将所有``标记转换为单词hyperlink,然后去掉所有html标记,只留下纯文本。为了可读性,它还用一个换行符替换多个换行符,最后它取消了HTML实体(例如`>;`或` ;`)
import re
from html import unescape
def html_to_plain_text(html):
text = re.sub('.*?' , '', html, flags=re.M | re.S | re.I)
text = re.sub('', ' HYPERLINK ', text, flags=re.M | re.S | re.I)
text = re.sub('<.*?>', '', text, flags=re.M | re.S)
text = re.sub(r'(\s*\n)+', '\n', text, flags=re.M | re.S)
return unescape(text)
html_spam_emails = [email for email in X_train[y_train==1]
if get_email_structure(email) == "text/html"]
sample_html_spam = html_spam_emails[7]
print(sample_html_spam.get_content().strip()[:1000], "...")
print(html_to_plain_text(sample_html_spam.get_content())[:1000], "...")
# 编写一个函数,它以电子邮件为输入,并以纯文本形式返回其内容,无论其格式是什么
def email_to_text(email):
html = None
for part in email.walk():
ctype = part.get_content_type()
if not ctype in ("text/plain", "text/html"):
continue
try:
content = part.get_content()
except: # 解决编码问题
content = str(part.get_payload())
if ctype == "text/plain":
return content
else:
html = content
if html:
return html_to_plain_text(html)
print(email_to_text(sample_html_spam)[:100], "...")
# 将所有处理整合到一个转换器中,我们将使用它将电子邮件转换为文字计数器。
# 注意,我们使用python的'split()'方法将句子拆分为单词,该方法使用空格作为单词边界。
# 但例如,汉语和日语脚本通常不在单词之间使用空格。
# 在这个练习中没关系,因为数据集(主要)是英文的,中文可以使用结巴分词来进行拆分
# 装自然语言工具包(http://www.nltk.org/)
# pip install nltk
# 用“url”替换url的方法
# pip install urlextract
import nltk
from urlextract import URLExtract
try:
import nltk
stemmer = nltk.PorterStemmer()
for word in ("Computations", "Computation", "Computing", "Computed", "Compute", "Compulsive"):
print(word, "=>", stemmer.stem(word))
except ImportError:
print("Error: stemming requires the NLTK module.")
stemmer = None
# 转换器
from sklearn.base import BaseEstimator, TransformerMixin
class EmailToWordCounterTransformer(BaseEstimator, TransformerMixin):
def __init__(self, strip_headers=True, lower_case=True, remove_punctuation=True,
replace_urls=True, replace_numbers=True, stemming=True):
self.strip_headers = strip_headers
self.lower_case = lower_case
self.remove_punctuation = remove_punctuation
self.replace_urls = replace_urls
self.replace_numbers = replace_numbers
self.stemming = stemming
def fit(self, X, y=None):
return self
def transform(self, X, y=None):
X_transformed = []
for email in X:
text = email_to_text(email) or ""
if self.lower_case:
text = text.lower()
if self.replace_urls:
extractor = URLExtract()
urls = list(set(extractor.find_urls(text)))
urls.sort(key=lambda url: len(url), reverse=True)
for url in urls: # 替换url 为 ‘URL’
text = text.replace(url, " URL ")
if self.replace_numbers: # 替换数字
text = re.sub(r'\d+(?:\.\d*(?:[eE]\d+))?', 'NUMBER', text)
if self.remove_punctuation: # 删除标点符号
text = re.sub(r'\W+', ' ', text, flags=re.M)
word_counts = Counter(text.split())
if self.stemming and stemmer is not None:
stemmed_word_counts = Counter()
for word, count in word_counts.items():
stemmed_word = stemmer.stem(word)
stemmed_word_counts[stemmed_word] += count
word_counts = stemmed_word_counts
X_transformed.append(word_counts)
return np.array(X_transformed)
# 在一些邮件上 测试 转换器
X_few = X_train[:3]
X_few_wordcounts = EmailToWordCounterTransformer().fit_transform(X_few)
X_few_wordcounts
# 有了单词计数,我们需要把它们转换成向量。为此,我们将构建另一个转换器。
# “fit()”方法将构建词汇表(最常用单词的有序列表)
# “transform()”方法将使用词汇表将单词计数转换为向量--稀疏矩阵
from scipy.sparse import csr_matrix
class WordCounterToVectorTransformer(BaseEstimator, TransformerMixin):
def __init__(self, vocabulary_size = 1000):
self.vocabulary_size = vocabulary_size # 词汇量
def fit(self, X, y = None):
total_count = Counter()
for word_count in X:
for word, count in word_count.items():
total_count[word] += min(count, 10)
most_common = total_count.most_common()[:self.vocabulary_size]
self.most_common_ = most_common
self.vocabulary_ = {word: index + 1 for index, (word, count) in enumerate(most_common)}
return self
def transform(self, X, y = None):
rows = []
cols = []
data = []
for row, word_count in enumerate(X):
for word, count in word_count.items():
rows.append(row) # 训练集 实例个数
cols.append(self.vocabulary_.get(word, 0)) # 取得单词在词汇表中的索引位置,0代表未出现在词汇表中
data.append(count)
return csr_matrix((data, (rows, cols)), shape= (len(X), self.vocabulary_size + 1)) # 输出稀疏矩阵
vocab_transformer = WordCounterToVectorTransformer(vocabulary_size=10)
X_few_vectors = vocab_transformer.fit_transform(X_few_wordcounts)
X_few_vectors.shape
# 我们现在准备训练我们的第一个垃圾邮件分类器!让我们转换整个数据集:
from sklearn.pipeline import Pipeline
preprocess_pipeline = Pipeline([
("email_to_wordcount", EmailToWordCounterTransformer()),
("wordcount_to_vector", WordCounterToVectorTransformer()),
])
X_train_transformed = preprocess_pipeline.fit_transform(X_train)
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score
log_clf = LogisticRegression(solver="liblinear", random_state=42) # 采用逻辑回归分类器
score = cross_val_score(log_clf, X_train_transformed, y_train, cv=3, verbose=3)
score.mean()
# 得到分数超过98.7%,可以尝试多个模型,选择最好的模型,并使用交叉验证对它们进行微调。在测试集上得到的精度/召回率:
from sklearn.metrics import precision_score, recall_score
X_test_transformed = preprocess_pipeline.transform(X_test)
log_clf = LogisticRegression(solver="liblinear", random_state=42)
log_clf.fit(X_train_transformed, y_train)
y_pred = log_clf.predict(X_test_transformed)
print("精度: {:.2f}%".format(100 * precision_score(y_test, y_pred)))
print("召回: {:.2f}%".format(100 * recall_score(y_test, y_pred)))