一、朴素贝叶斯分类器
与线性模型相似,训练速度更快,泛化能力稍低于线性分类器(LogisticRegression 和 LinearSVC)。单独查看每个特征学习参数,并收集简单的类别统计数据。
scikit-learn 库中实现了三种朴素贝叶斯分类器: GaussianNB、BernoulliNB 和 MultinomialNB。GaussianNB 可应用于任意连续数据, BernoulliNB 假定输入数据为二分类数据,MultinomialNB 假定输入数据为计数数据。BernoulliNB 和 MultinomialNB 主要用于文本数据分类。
BernoulliNB 分类器计算每个类别中每个特征不为 0 的元素个数:
python
import numpy as np
# 4个数据点,每个点4个特征(二分类),
X = np.array([[0, 1, 1, 1],
[1, 0, 1, 1],
[0, 0, 0, 1],
[1, 0, 1, 0]])
y = np.array([0, 1, 0, 1])
counts = {}
for label in np.unique(y):
# 对每个类别进行遍历
# 计算(求和)每个特征中1的个数
counts[label] = X[y == label].sum(axis=0)
print("Feature counts:\n{}".format(counts))
输出结果:
Feature counts:
{0: array([0, 1, 1, 2]), 1: array([2, 0, 2, 1])}
朴素贝叶斯模型(MultinomialNB 和 GaussianNB)计算的统计数据类型不同。
MultinomialNB 计算每个类别中每个特征的平均值; GaussianNB 会保存每个类别中每个特征的平均值和标准差。 要想做出预测,需要将数据点与每个类别的统计数据进行比较,并将最匹配的类别作为预测结果。
MultinomialNB 和 BernoulliNB 预测公式的形式与线性模型完全相同,但朴素贝叶斯模型 coef_ 的含义与线性模型不同。
优点、缺点和参数
MultinomialNB 和 BernoulliNB 只有一个参数 alpha,用于控制模型复杂度。alpha 的工作原理是,算法向数据中添加 alpha 这么多的虚拟数据点,这些点对所有特征都取正值,将统计数据"平滑化"(smoothing)。alpha 越大,平滑化越强,模型复杂度就越低。
算法性能对 alpha 值的鲁棒性相对较好,即:alpha 值对模型性能并不重要。但调整这个参数通常都会使精度略有提高。 GaussianNB 主要用于高维数据,另外两种朴素贝叶斯模型则广泛用于稀疏计数数据(例如文本)。MultinomialNB 的性能通常要优于 BernoulliNB,特别是在包含很多非零特征的数据集(即大型文档)上。 朴素贝叶斯模型的与线性模型有相同的优点,训练和预测速度都很快,训练过程容易理解;模型对高维稀疏数据的效果很好,对参数的鲁棒性也相对较好。朴素贝叶斯模型是很好的基准模型,常用于非常大的数据集。
二、决策树
决策树是用于分类和回归任务的模型。它从一层层的 if/else 问题中进行学习,并得出结论。举例:如何用最少的if/else判断条件区分出动物:熊、鹰、企鹅和海豚。第一问:动物有没有羽毛,使得动物分为两种;第二问动物会不会飞,在有羽毛类中可以区分出鹰和企鹅。第三问:动物有没有鳍,在无羽毛的动物中可以区分出海豚和熊。这是一个决策数问题,mglearn库中实现了这个示例的决策树代码,不过在使用前我们要先安装graphviz。
Graphviz 是一个由AT&T实验室启动的开源工具包,用于绘制DOT语言脚本描述的图形。Graphviz支持多种输出格式,包括PostScirpt、PDF、SVG、PNG等,并且可以在多种操作系统上运行,包括Windows、Linux和Mac OS。下载官网:Graphviz 下载完成后安装,然后设置环境变量。
然后重启Python编辑器VS Code,我们用mglearn库代码示例:
python
import matplotlib.pyplot as plt
import mglearn
mglearn.plots.plot_animal_tree()
plt.show()
输出图形:
上图每个节点代表一个问题或是最终**叶节点 。**上例:利用三个特征(有无羽毛、会不会飞、有没有鳍)来构建模型。可以利用监督学习从数据中学习模型,而无需人为构建。
1. 构造决策树
示例:用一个二分类数据集(two_moons)来构造决策树,每个类别都有50个数据点。学习决策树就是学习一系列if/else问题。机器学习中这些问题叫**测试。数据一般不是二元特征(是/否)形式的,而是连续特征形式。**用于连续数据的测试形式是:"特征 i 的值是否大于 a ?"
python
import matplotlib.pyplot as plt
import mglearn
mglearn.plots.plot_tree_progressive()
plt.show()
输出这个数据集(two_moons)的分布图:
为了构造决策树,算法遍历所有可能的测试(if/else问题),找出对目标变量来说信息量最大的那一个。 下图展示了选出的第一个测试。
将数据集在 x[1]=0.06 处垂直划分可以得到最多信息,它在最大程度上将类别 0 中的点与类别 1 中的点进行区分。根结点(顶结点) 表示整个数据集,包含属于类别 0 的 50 个点和属于类别 1 的 50 个点。通过测试 x[1] <= 0.06 的真假来对数据集进行划分,在图中表示为一条黑线。如果测试结果为真,那么将这个点分配给左结点,左结点里包含属于类别 0 的 2 个点和属于类别 1 的 32 个点。如果为假将这个点分配给右结点,右结点里包含属于类别 0 的 48 个点和属于类别 1 的 18 个点。这两个结点对应于上图的顶部区域和底部区域。第一次划分已经对两个类别做了很好的区分,但底部区域仍包含属于类别 0 的点,顶部区域也仍包含属于类别 1 的点。可以在两个区域中重复寻找最佳测试的过程,从而构建出更准确的模型。下图展示了信息量最大的下一次划分,这次划分是基于 x[0] 做出的,分为左右两个区域。
递归过程生成一棵二元决策树,其中每个结点都包含一个测试 。也可以将每个测试看成沿着一条轴对当前数据进行划分。这是一种将算法看作分层划分的观点。由于每个测试仅关注一个特征,所以划分后的区域边界始终与坐标轴平行。 对数据反复进行递归划分,直到划分后的每个区域(决策树的每个叶结点 )只包含单一目标值(单一类别或单一回归值)。如果树中某个叶结点所包含数据点的目标值都相同,那么这个叶结点就是纯的(pure)。这个数据集的最终划分结果见下图:
要对新数据点进行预测,首先要查看这个点位于特征空间划分的哪个区域,然后将该区域的多数目标值(如果是纯的叶结点,就是单一目标值)作为预测结果 。从根结点开始对树进行遍历就可以找到这一区域,每一步向左还是向右取决于是否满足相应的测试。 决策树也可以用于回归任务,使用的方法完全相同。预测的方法是,基于每个结点的测试对树进行遍历,最终找到新数据点所属的叶结点。这一数据点的输出即为此叶结点中所有训练点的平均目标值。
2. 控制决策树的复杂度
构造决策树至叶结点都是纯的叶结点,会导致模型非常复杂,并且对训练数据高度过拟合。如果有纯叶节点存在,说明决策树在训练集上的精度是1。上图可以看出过拟合。
防止过拟合的两种策略:一是预剪枝(pre-pruning),尽早停止树的生长,限制条件包括:限制树的最大深度、叶结点的最大数目、 或规定一个结点中数据点的最小数目来防止继续划分;二是后剪枝(post-pruning)或 剪枝(pruning),先构造树,之后删除或折叠信息量很少的结点。
scikit-learn 的决策树在 DecisionTreeRegressor 类和 DecisionTreeClassifier 类中实现。 scikit-learn 只实现了预剪枝,没有实现后剪枝。使用乳腺癌(cancer)数据集来使用默认设置的构建模型,将树完全展开(不断分支,直到所有叶节点都是纯的),固定树的random_state,用于在内部解决平局问题:
python
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.datasets import load_breast_cancer
cancer = load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(cancer.data, cancer.target, stratify=cancer.target, random_state=42)
tree = DecisionTreeClassifier(random_state=0)
tree.fit(X_train, y_train)
print("Accuracy on training set: {:.3f}".format(tree.score(X_train, y_train)))
print("Accuracy on test set: {:.3f}".format(tree.score(X_test, y_test)))
输出结果:
Accuracy on training set: 1.000
Accuracy on test set: 0.937
从输出可以看出:训练集上的精度是 100%,这是因为叶结点都是纯的,树的深度很大,能够完美地记住训练数据的所有标签。测试集精度比之前讲过的线性模型略低,线性模型的精度约为 94%。
如果不限制决策树的深度,那深度和复杂度会变得特别大。因此,未剪枝的树容易过拟合,对新数据的泛化性能不好。现在我们将预剪枝应用在决策树上,这可以在完美拟合训练数据之前阻止树的展开。我们在到达一定深度后停止树的展开。这里我们设置 max_depth=4,这意味着只可以连续问 4 个问题。限制树的深度可以减少过拟合,会降低训练集的精度,但可以提高测试集的精度:
python
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.datasets import load_breast_cancer
cancer = load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(cancer.data, cancer.target, stratify=cancer.target, random_state=42)
tree = DecisionTreeClassifier(max_depth=4, random_state=0)
tree.fit(X_train, y_train)
print("Accuracy on training set: {:.3f}".format(tree.score(X_train, y_train)))
print("Accuracy on test set: {:.3f}".format(tree.score(X_test, y_test)))
输出结果:
Accuracy on training set: 0.988
Accuracy on test set: 0.951
3. 分析决策树
可以用 sklearn.tree 模块的 export_graphviz 函数来将树可视化。此函数会生成一个 .dot 格式的文件,这是一种用于保存图形的文本文件格式。我们设置给结点添加颜色的选项,颜色表示每个结点中的多数类别,同时传入类别名称和特征名称,这样可以对树正确标记,这里要先安装graphviz库,pip install graphviz
python
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.datasets import load_breast_cancer
from sklearn.tree import export_graphviz
import graphviz
cancer = load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(cancer.data, cancer.target, stratify=cancer.target, random_state=42)
tree = DecisionTreeClassifier(max_depth=4, random_state=0)
tree.fit(X_train, y_train)
print("Accuracy on training set: {:.3f}".format(tree.score(X_train, y_train)))
print("Accuracy on test set: {:.3f}".format(tree.score(X_test, y_test)))
export_graphviz(tree, out_file="tree.dot", class_names=["malignant","benign"], feature_names=cancer.feature_names, impurity=False, filled=True)
with open("tree.dot") as f:
dot_graph = f.read()
s = graphviz.Source(dot_graph)
s.render('output', view=True)
s.render('output', view=True) 会生成一个output.pdf,同时会打开,截图显示如下:
可视化有助于我们理解算法是如何进行预测的。上图中每个结点的 samples 给出了该结点中的样本个数,values 给出的是每个类别的样本个数。观察 worst radius <= 16.795分支右侧的子结点,它包含 8 个良性样本,但有 134 个恶性样本,这一侧其余分支只是利用一些更精细的区别将这8个良性样本分离,右侧划分的这142个样本,最后几乎所有都进入了最右侧叶节点。
根节点的左侧子节点,得到 25 个恶性样本 和 259 个良性样本。几乎所有良性样本最终都进入左边第二个叶结点中,大部分其他叶结点包含很少的样本。
4. 树的特征重要性(feature importance)
每个特征来是一个介于 0 和 1 之间的数字,其中 0 表示"没用到",1 表示"完美预测目标值"。特征重要性的求和始终为 1:
python
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.datasets import load_breast_cancer
cancer = load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(cancer.data, cancer.target, stratify=cancer.target, random_state=42)
tree = DecisionTreeClassifier(max_depth=4, random_state=0)
tree.fit(X_train, y_train)
print("Accuracy on training set: {:.3f}".format(tree.score(X_train, y_train)))
print("Accuracy on test set: {:.3f}".format(tree.score(X_test, y_test)))
# 特征重要性存储在tree的feature_importances_属性中
print("Feature importances:\n{}".format(tree.feature_importances_))
输出结果:
我们将特征重要性可视化:
python
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.datasets import load_breast_cancer
cancer = load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(cancer.data, cancer.target, stratify=cancer.target, random_state=42)
tree = DecisionTreeClassifier(max_depth=4, random_state=0)
tree.fit(X_train, y_train)
#print("Accuracy on training set: {:.3f}".format(tree.score(X_train, y_train)))
#print("Accuracy on test set: {:.3f}".format(tree.score(X_test, y_test)))
# 特征重要性存储在tree的feature_importances_属性中
print("Feature importances:\n{}".format(tree.feature_importances_))
# 自定义函数可视化特征重要性
def plot_feature_importances_cancer(model):
n_features = cancer.data.shape[1]
plt.barh(range(n_features), model.feature_importances_, align='center')
plt.yticks(np.arange(n_features), cancer.feature_names)
plt.xlabel("Feature importance")
plt.ylabel("Feature")
plt.show()
plot_feature_importances_cancer(tree)
输出图形结果:
从输出可以看出顶部划分用到的特征("worst radius")是最重要的特征。
如果某个特征的 feature_importance_ 很小,并不能说明这个特征没有提供任何信息,只能说明该特征没有被树选中,可能是因为另一个特征也包含了同样的信息。特征重要性始终为正数,不能说明该特征对应哪个类别。例如:特征重要性告诉我们"worst radius"(最大半径)特征很重要,但并没有告诉我们半径大表示样本是良性还是恶性。事实上,在特征和类别之间可能没有这样简单的关系,看如下示例:
python
import numpy as np
import matplotlib.pyplot as plt
import mglearn
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.datasets import load_breast_cancer
cancer = load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(cancer.data, cancer.target, stratify=cancer.target, random_state=42)
tree = DecisionTreeClassifier(max_depth=4, random_state=0)
tree.fit(X_train, y_train)
#print("Accuracy on training set: {:.3f}".format(tree.score(X_train, y_train)))
#print("Accuracy on test set: {:.3f}".format(tree.score(X_test, y_test)))
# 特征重要性存储在tree的feature_importances_属性中
print("Feature importances:\n{}".format(tree.feature_importances_))
tree = mglearn.plots.plot_tree_not_monotone()
plt.show()
输出结果:
Feature importances: [0. 1.]
上图显示的是有两个特征和两个类别的数据集。这里所有信息都包含在 X[1] 中,没有用到 X[0]。但 X[1] 和输出类别之间并不是单调关系,即我们不能说:"较大的 X[1] 对应类别 0,较小的 X[1] 对应类别 1"(反之亦然)。 虽然我们主要讨论的是用于分类的决策树,但对用于回归的决策树来说,所有内容都是类似的,在 DecisionTreeRegressor 中实现,回归树的用法和分析与分类树非常类似。但在将基于树的模型用于回归时,我们想要指出它的一个特殊性质。DecisionTreeRegressor (以及其他所有基于树的回归模型)不能外推(extrapolate),也不能在训练数据范围之外进行预测。
下面使用计算机内存(RAM)价格数据集来做示例研究:
python
import pandas as pd
import matplotlib.pyplot as plt
ram_prices = pd.read_csv("data/ram_price.csv")
plt.semilogy(ram_prices.date, ram_prices.price)
plt.xlabel("Year")
plt.ylabel("Price in $/Mbyte")
plt.show()
输出:
上图x轴为年份,y轴为价格
y 轴是对数刻度。在用对数坐标绘图时,二者的线性关系看起来非常好,所以预测应该相对比较容易,除了一些不平滑之处。 我们将利用 2000 年前的历史数据来预测 2000 年后的价格,只用日期作为特征。我们将对比两个简单的模型:DecisionTreeRegressor 和 LinearRegression。对价格取对数,使得二者关系的线性相对更好。这对 DecisionTreeRegressor 不会产生什么影响,但对 LinearRegression 的影响却很大。训练模型并做出预测之后,应用指数映射来做对数变换的逆运算。为了便于可视化,这里对整个数据集进行预测,但如果是为了定量评估,我们将只考虑测试数据集:
python
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
from sklearn.tree import DecisionTreeRegressor
ram_prices = pd.read_csv("data/ram_price.csv", index_col=0)
print(ram_prices.head())
# 利用历史数据预测2000年后的价格
data_train = ram_prices[ram_prices.date < 2000]
data_test = ram_prices[ram_prices.date >= 2000]
# 基于日期来预测价格
X_train = (data_train.date.to_numpy())[:, np.newaxis]
# 利用对数变换得到数据和目标之间更简单的关系
y_train = np.log(data_train.price)
tree = DecisionTreeRegressor().fit(X_train, y_train)
linear_reg = LinearRegression().fit(X_train, y_train)
# 对所有数据进行预测
X_all = (ram_prices.date.to_numpy())[:, np.newaxis]
pred_tree = tree.predict(X_all)
pred_lr = linear_reg.predict(X_all)
# 对数变换逆运算
price_tree = np.exp(pred_tree)
price_lr = np.exp(pred_lr)
# 创建决策树和线性回归模型的预测结果与真实值进行对比的图形
plt.semilogy(data_train.date, data_train.price, label="Training data")
plt.semilogy(data_test.date, data_test.price, label="Test data")
plt.semilogy(ram_prices.date, price_tree, label="Tree prediction")
plt.semilogy(ram_prices.date, price_lr, label="Linear prediction")
plt.legend()
plt.show()
输出结果:
两个模型之间的差异非常明显。线性模型用一条直线对数据做近似,这是我们所知道的。 这条线对测试数据(2000 年后的价格)给出了相当好的预测,不过忽略了训练数据和测试数据中一些更细微的变化。与之相反,树模型完美预测了训练数据。由于我们没有限制树的复杂度,因此它记住了整个数据集,但是,一旦输入超出了模型训练数据的范围,模型就只能持续预测最后一个已知数据点。树不能在训练数据的范围之外生成"新的"响应,所有基于树的模型都有这个缺点。
5. 优点、缺点和参数
控制决策树模型复杂度的参数是预剪枝参数,一般可以选择一种预剪枝策略(如:设置 max_depth、max_leaf_nodes 或 min_samples_ leaf)可以防止过拟合。 与前面学过的许多算法相比,决策树有两个优点:一是得到的模型很容易可视化,非专家也很容易理解(至少对于较小的树而言);二是算法完全不受数据缩放的影响。由于每个特征被单独处理,而且数据的划分也不依赖于缩放,因此决策树算法不需要特征预处理,比如归一化或标准化。特别是特征的尺度完全不一样时或者二元特征和连续特征同时存在时,决策树的效果很好。 决策树的主要缺点在于,即使做了预剪枝,它也经常会过拟合,泛化性能很差。后面会学习决策树集成方法。