决策树和k近邻
决策树算法
定义
决策树是分类与回归问题中常用的方法之一。其实不仅是机器学习领域,在每天的日常决策中,我们都在使用决策树。
决策树常常是专家经验的概括,是一种分享特定过程知识的方式。例如,在引入可扩展机器学习算法之前,银行业的信用评分任务是由专家解决的,能否放贷是基于一些直观(或经验)的规则,这些规则就可以表示为决策树的形式,如下图所示:

作为机器学习算法的决策树基本上和上图差不多,它合并一连串逻辑规则,使之成为一个树形的数据结构,这些规则的形式为「特征 a 的值小于 x,特征 b 的值小于 y … => 类别 1」。
如何构建决策树
下面,我们基于「年龄」、「房产」、「收入」、「教育」特征使用决策树解决一个二元分类问题,即「是否允许贷款」。
年龄、房产、收入、教育,这么多的特征首先应该关注哪个呢?
为了回答上述问题,先看一个简单的游戏,即「20个问题」游戏,这个游戏是这样玩的:A 心里想着一个名人,B 问 A 20 个问题,A 只能回答「是」或「否」,20 个问题之后 B 要猜出 A 心里想的那个名人是谁。首先问一个可以最大程度压缩剩余选项数目的问题会使 B 占据极大优势,例如询问「是不是安吉丽娜·朱莉?」,最多剔除一个选项,而询问「这个名人是女人吗?」将消除大约一半的选项。就是说,「性别」特征相比「安吉丽娜·朱莉」、「西班牙人」、「喜欢足球」等其他特征更能区分名人数据集。这背后的道理与熵有关,下面介绍熵的概念。
熵
熵是一个在物理、信息论和其他领域中广泛应用的重要概念,可以衡量获得的信息量。对于具有 N 种可能状态的系统而言,熵的定义如下

其中,pi是系统位于第 i 个状态的概率。熵可以描述为系统的混沌程度,熵越高,系统的有序性越差,反之亦然。熵将帮助我们高效的分割数据,类似帮助我们找出在「20个问题」游戏中先问什么问题较好。
决策树构建算法
在之前的例子中构建的决策树是最优的:它只需提 5 个「问题」(基于变量 Q),就完全拟合了训练集。其他分割条件会使得到的树更深,即需要更多「问题」才能获得答案。
构建决策树的流行算法(如 ID3 或 C4.5)的核心,是贪婪最大化信息增益:在每一步,算法都会选择能在分割后给出最大信息增益的变量。接着递归重复这一流程,直到熵为零(或者,为了避免过拟合,直到熵为某个较小的值)。不同的算法使用不同的推断,通过「提前停止」或「截断」以避免构建出过拟合的树。
分类问题中其他的分割质量标准
上面我们讨论了熵是如何衡量树的分区的,但还有其他指标来衡量分割的好坏:
基尼不确定性(Gini uncertainty):
错分率(Misclassification error):
实践中几乎从不使用错分率,而基尼不确定性和信息增益的效果差不多。
二元分类问题的熵和基尼不确定性为:

其中 p +是对象具有标签 + 的概率。p +为坐标,绘制上面两个函数的图像。
import warnings
from matplotlib import pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
sns.set()
warnings.filterwarnings('ignore')
plt.figure(figsize=(6, 4))
xx = np.linspace(0, 1, 50)
plt.plot(xx, [2 * x * (1-x) for x in xx], label='gini')
plt.plot(xx, [4 * x * (1-x) for x in xx], label='2*gini')
plt.plot(xx, [-x * np.log2(x) - (1-x) * np.log2(1 - x)
for x in xx], label='entropy')
plt.plot(xx, [1 - max(x, 1-x) for x in xx], label='missclass')
plt.plot(xx, [2 - 2 * max(x, 1-x) for x in xx], label='2*missclass')
plt.xlabel('p+')
plt.ylabel('criterion')
plt.title('Criteria of quality as a function of p+ (binary classification)')
plt.legend()

上图可见,熵的图像和两倍的基尼不确定性图像非常接近。因此,在实践中,这两个指标的效果基本上是一样的。
示例
下面用一棵决策树拟合一些合成数据。这些合成数据属于两个不同的类别,这两个类别的均值不同,但都呈现正态分布。
# 第一类
np.random.seed(17)
train_data = np.random.normal(size=(100, 2))
train_labels = np.zeros(100)
# 第二类
train_data = np.r_[train_data, np.random.normal(size=(100, 2), loc=2)]
train_labels = np.r_[train_labels, np.ones(100)]
下面绘制数据。通俗地讲,这种情况下的分类问题就是构造一个「边界」,能够较好的分开两个类别(红点和黄点)。这个「边界」若是一条直线的话可能太过简单,若是沿着每个红点画出的蛇形曲线又太过复杂(这将导致其在新数据上的表现很差)。从直觉上说,某种平滑的边界,在新数据上的效果会比较好。
plt.figure(figsize=(10, 8))
plt.scatter(train_data[:, 0], train_data[:, 1], c=train_labels, s=100,
cmap='autumn', edgecolors='black', linewidth=1.5)
plt.plot(range(-2, 5), range(4, -3, -1))

下面训练一棵 sklearn 决策树,区分这两类数据点。最后可视化所得的边界。
from sklearn.tree import DecisionTreeClassifier
# 编写一个辅助函数,返回之后的可视化网格
def get_grid(data):
x_min, x_max = data[:, 0].min() - 1, data[:, 0].max() + 1
y_min, y_max = data[:, 1].min() - 1, data[:, 1].max() + 1
return np.meshgrid(np.arange(x_min, x_max, 0.01), np.arange(y_min, y_max, 0.01))
# max_depth参数限制决策树的深度
clf_tree = DecisionTreeClassifier(criterion='entropy', max_depth=3,
random_state=17)
# 训练决策树
clf_tree.fit(train_data, train_labels)
# 可视化
xx, yy = get_grid(train_data)
predicted = clf_tree.predict(np.c_[xx.ravel(),
yy.ravel()]).reshape(xx.shape)
plt.pcolormesh(xx, yy, predicted, cmap='autumn')
plt.scatter(train_data[:, 0], train_data[:, 1], c=train_labels, s=100,
cmap='autumn', edgecolors='black', linewidth=1.5)

树的关键参数
理论上讲构建一个决策树,直到每个叶节点只有一个实例,但这样做容易过拟合,导致其在新数据上的表现不佳。如果这么做,在树的最深处,可能会存在由无关紧要的特征组成的分区,例如根据「客户裤子的颜色」这一特征进行分区,这是不希望发生。
但在两种情况下,树可以被构建到最大深度(每个叶节点只有一个实例):
随机森林。它将构建为最大深度的单个树的响应进行平均(稍后我们将讨论为什么要这样做)。
决策树修剪。在这种方法中,树首先被构造成最大深度。然后,从底部开始,基于交叉验证来比较有分区/无分区情形下树的质量情况,进而移除树的一些节点。
下图是过拟合的决策树给出的分界。

常见的解决决策树过拟合的方法为:
-
人工限制深度或叶节点的最少样本数。
-
对树进行剪枝。
scikit-learn 的 DecisionTreeClassifier 类
sklearn.tree.DecisionTreeClassifier 类的主要参数为:
- max_depth 树的最大深度;
- max_features 搜索最佳分区时的最大特征数(特征很多时,设置这个参数很有必要,因为基于所有特征搜索分区会很「昂贵」);
- min_samples_leaf 叶节点的最少样本数。
树的参数需要根据输入数据设定,通常通过交叉验证可以确定参数范围。
回归问题中的决策树
当对数值变量进行预测时,我们构造决策树的思路和分类问题时所用的思路是一样的,但衡量决策树好坏的质量标准改变了,现在它的质量标准如下:

其中,ℓ 是叶节点中的样本数,yi是目标变量的值。简单来说,通过最小化方差,使每个叶子中的目标特征的值大致相等,以此来划分训练集的特征。
示例
让我们基于以下函数生成一些带噪数据:

接着在生成的数据上训练一颗决策树,并进行预测,调用 plt 方法画出结果示意图。
from sklearn.tree import DecisionTreeRegressor
n_train = 150
n_test = 1000
noise = 0.1
def f(x):
x = x.ravel()
return np.exp(-x ** 2) + 1.5 * np.exp(-(x - 2) ** 2)
def generate(n_samples, noise):
X = np.random.rand(n_samples) * 10 - 5
X = np.sort(X).ravel()
y = np.exp(-X ** 2) + 1.5 * np.exp(-(X - 2) ** 2) + \
np.random.normal(0.0, noise, n_samples)
X = X.reshape((n_samples, 1))
return X, y
X_train, y_train = generate(n_samples=n_train, noise=noise)
X_test, y_test = generate(n_samples=n_test, noise=noise)
reg_tree = DecisionTreeRegressor(max_depth=5, random_state=17)
reg_tree.fit(X_train, y_train)
reg_tree_pred = reg_tree.predict(X_test)
plt.figure(figsize=(10, 6))
plt.plot(X_test, f(X_test), "b")
plt.scatter(X_train, y_train, c="b", s=20)
plt.plot(X_test, reg_tree_pred, "g", lw=2)
plt.xlim([-5, 5])
plt.title("Decision tree regressor, MSE = %.2f" %
(np.sum((y_test - reg_tree_pred) ** 2) / n_test))
plt.show()

决策树的优势和劣势
优点:
-
解释性强:决策树可以直观地呈现出决策过程,易于理解和解释,因此非常适合用于可视化和教学。
-
对特征的处理能力强:决策树能够自动选择特征,并且不需要对数据进行过多的预处理,比如归一化或标准化。
-
能够同时处理数值型和类别型数据:决策树可以处理混合型的数据,这意味着它不需要对数据进行特殊的处理。
-
鲁棒性较好:对于异常值不敏感,对噪声数据的影响较小。
缺点: -
容易过拟合:决策树容易生成复杂的树结构,从而导致过拟合,为了避免过拟合,需要通过剪枝等方法进行调节。
-
对数据的微小变化敏感:数据的微小变化可能导致生成完全不同的树结构,因此决策树的稳定性不高。
-
不适合处理高维稀疏数据:当特征空间非常大时,决策树容易变得非常复杂,因此不适合处理高维稀疏数据。
-
不一定能够达到全局最优:决策树的生成过程是一个贪心算法,容易陷入局部最优,无法保证一定能够得到全局最优的模型。
总的来说,决策树由于其简单、直观和易于解释的特点,在一些应用场景下仍然表现出色。但在处理复杂、高维数据时,可能会受到一些限制,需要结合实际情况进行选择。
学习链接:
scikit-learn 文档:https://scikit-learn.org/stable/index.html
K 近邻
最近邻方法
最近邻方法在分类任务上构建模型,并通过交叉验证对模型进行调优。最近邻方法(K 近邻或 k-NN)是另一个非常流行的分类方法。当然,也可以用于回归问题。和决策树类似,这是最容易理解的分类方法之一。这一方法遵循紧密性假说:如果样本间的距离能以足够好的方法衡量,那么相似的样本更可能属于同一分类。
在最近邻方法中,为了对测试集中的每个样本进行分类,需要依次进行以下操作:
- 计算训练集中每个样本之间的距离。
- 从训练集中选取 k 个距离最近的样本。
- 测试样本的类别将是它 k 个最近邻中最常见的分类。
在回归问题中应用最近邻方法很简单,仅需将上述步骤做一个小小的改动:第三步不返回分类,而是返回一个数字,即目标变量在邻居中的均值或中位数。这一方式的显著特点是它具有惰性:当需要对测试样本进行分类时,计算只在预测阶段进行。由于这种特点,最近邻方法事先并不基于训练样本创建模型,这与上文提到的决策树不同。决策树是基于训练集构建的,在预测阶段仅通过遍历决策树就可以实现快速地分类。
k-NN 分类/回归的效果取决于一些参数:
- 邻居数 k。
- 样本之间的距离度量(常见的包括 Hamming,欧几里得,余弦和 Minkowski 距离)。注意,大部分距离要求数据在同一尺度下,例如「薪水」特征的数值在千级,「年龄」特征的数值却在百级,如果直接将他们丢进最近邻模型中,「年龄」特征就会受到比较大的影响。
- 邻居的权重(每个邻居可能贡献不同的权重,例如,样本越远,权重越低)。
scikit-learn 的 KNeighborsClassifier 类
sklearn.neighbors.KNeighborsClassifier 类的主要参数为:
- weights:可设为 uniform(所有权重相等),distance(权重和到测试样本的距离成反比),或任何其他用户自定义的函数。
- algorithm(可选):可设为 brute、ball_tree、KD_tree、auto。若设为 brute,通过训练集上的网格搜索来计算每个测试样本的最近邻;若设为 ball_tree 或 KD_tree,样本间的距离储存在树中,以加速寻找最近邻;若设为 auto,将基于训练集自动选择合适的寻找最近邻的方法。
- leaf_size(可选):若寻找最近邻的算法是 BallTree 或 KDTree,则切换为网格搜索所用的阈值。
- metric:可设为 minkowski、manhattan、euclidean、chebyshev 或其他。
选择模型参数和交叉验证
机器学习算法的主要任务是可以「泛化」未曾见过的数据。由于我们无法立刻得知模型在新数据上的表现(因为还不知道目标变量的真值),因此有必要牺牲一小部分数据,来验证模型的质量,即将一小部分数据作为留置集。
通常采用下述两种方法之一来验证模型的质量:
- 留置法。保留一小部分数据(一般是 20% 到 40%)作为留置集,在其余数据上训练模型(原数据集的 60%-80%),然后在留置集上验证模型的质量。
- 交叉验证。最常见的情形是 k 折交叉验证,如下图所示。

在 k 折交叉验证中,模型在原数据集的 K−1 个子集上进行训练),然后在剩下的 1 个子集上验证表现,重复训练和验证的过程,每次使用不同的子集,总共进行 K 次,由此得到 K 个模型质量评估指数,通常用这些评估指数的求和平均数来衡量分类/回归模型的总体质量。相比留置法,交叉验证能更好地评估模型在新数据上的表现。然而,当你有大量数据时,交叉验证对机器计算能力的要求会变得很高。
在客户离网率预测任务中使用决策树和k近邻算法
首先读取数据至 DataFrame 并进行预处理。将 State 特征从 DateFrame 转移到单独的 Series 对象中。我们训练的第一个模型将不包括 State 特征,之后再考察 State 特征是否有用。
import pandas as pd
df = pd.read_csv(
'https://labfile.oss.aliyuncs.com/courses/1283/telecom_churn.csv')
df['International plan'] = pd.factorize(df['International plan'])[0]
df['Voice mail plan'] = pd.factorize(df['Voice mail plan'])[0]
df['Churn'] = df['Churn'].astype('int')
states = df['State']
y = df['Churn']
df.drop(['State', 'Churn'], axis=1, inplace=True)
df.head()

之后将数据集的 70% 划分为训练集(X_train,y_train),30% 划分为留置集(X_holdout,y_holdout)。留置集的数据在调优模型参数时不会被用到,在调优之后,用它评定所得模型的质量。
接下来,训练 2 个模型:决策树和 k-NN。一开始,我们并不知道如何设置模型参数能使模型表现好,所以可以使用随机参数方法,假定树深(max_dept)为 5,近邻数量(n_neighbors)为 10。
from sklearn.model_selection import train_test_split, StratifiedKFold
from sklearn.neighbors import KNeighborsClassifier
from sklearn.tree import DecisionTreeClassifier
X_train, X_holdout, y_train, y_holdout = train_test_split(df.values, y, test_size=0.3,
random_state=17)
tree = DecisionTreeClassifier(max_depth=5, random_state=17)
knn = KNeighborsClassifier(n_neighbors=10)
tree.fit(X_train, y_train)
knn.fit(X_train, y_train)
使用准确率(Accuracy)在留置集上评价模型预测的质量。
决策树
from sklearn.metrics import accuracy_score
tree_pred = tree.predict(X_holdout)
accuracy_score(y_holdout, tree_pred)
# 0.94
k近邻
knn_pred = knn.predict(X_holdout)
accuracy_score(y_holdout, knn_pred)
# 0.881
从上可知,决策树的准确率约为 94%,k-NN 的准确率约为 88%,于是仅使用我们假定的随机参数(即没有调参),决策树的表现更好。
现在,使用交叉验证确定树的参数,对每次分割的 max_dept(最大深度 h)和 max_features(最大特征数)进行调优。GridSearchCV() 函数可以非常简单的实现交叉验证,下面程序对每一对 max_depth 和 max_features 的值使用 5 折验证计算模型的表现,接着选择参数的最佳组合。
from sklearn.model_selection import GridSearchCV, cross_val_score
tree_params = {'max_depth': range(5, 7),
'max_features': range(16, 18)}
tree_grid = GridSearchCV(tree, tree_params,
cv=5, n_jobs=-1, verbose=True)
tree_grid.fit(X_train, y_train)
列出交叉验证得出的最佳参数和相应的训练集准确率均值。
tree_grid.best_params_
# 'max_depth': 6, 'max_features': 17}
tree_grid.best_score_
# 0.94257014456259
accuracy_score(y_holdout, tree_grid.predict(X_holdout))
# 0.946
绘制所得的决策树
from sklearn.tree import export_graphviz
import pydotplus
from IPython.display import Image, display
dot_data = export_graphviz(tree_grid.best_estimator_, feature_names=df.columns, filled=True)
graph = pydotplus.graph_from_dot_data(dot_data)
# 显示图像
display(Image(graph.create_png()))

现在,再次使用交叉验证对 k-NN 的 k 值(即邻居数)进行调优。
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
knn_pipe = Pipeline([('scaler', StandardScaler()),
('knn', KNeighborsClassifier(n_jobs=-1))])
knn_params = {'knn__n_neighbors': range(6, 8)}
knn_grid = GridSearchCV(knn_pipe, knn_params,
cv=5, n_jobs=-1,
verbose=True)
knn_grid.fit(X_train, y_train)
knn_grid.best_params_, knn_grid.best_score_
# Fitting 5 folds for each of 2 candidates, totalling 10 fits
# ({'knn__n_neighbors': 7}, 0.8859867109023905)
knn_grid.best_params_
# {'knn__n_neighbors': 7}
accuracy_score(y_holdout, knn_grid.predict(X_holdout))
# 0.89
从上可知,在 1-9 范围(range 不包括 10)内最优的 k 值为 7,其交叉验证的准确率约为 88.5%,调优后 k-NN 在留置集上的准确率约为 89%。
综上所述,在这个任务里,决策树有着 94%/94.6%(留置法/交叉验证调优后)的准确率,k-NN 有着 88%/89%(留置法/交叉验证调优后)的准确率,显然决策树的表现更好。
使用 RandomForestClassifier() 方法再训练一个随机森林(可以把它想象成一群互相协作的决策树),看看能否在这个任务上有更好的表现。
import numpy as np
from sklearn.ensemble import RandomForestClassifier
forest = RandomForestClassifier(n_estimators=100, n_jobs=-1,
random_state=17)
np.mean(cross_val_score(forest, X_train, y_train, cv=5))
# 0.9494233119813256
forest_params = {'max_depth': range(8, 10),
'max_features': range(5, 7)}
forest_grid = GridSearchCV(forest, forest_params,
cv=5, n_jobs=-1, verbose=True)
forest_grid.fit(X_train, y_train)
forest_grid.best_params_, forest_grid.best_score_
# Fitting 5 folds for each of 4 candidates, totalling 20 fits
# ({'max_depth': 9, 'max_features': 6}, 0.9511372931045574)
accuracy_score(y_holdout, forest_grid.predict(X_holdout))
# 0.953
从上可知,随机森林有着 95.3% 的准确率。不得不说,决策树在这个任务上的表现非常好,即使是训练时间长得多的随机森林也无法取得比它更好的表现。
决策树的复杂情况
首先,在一个平面上创建一组具有 2 个分类的数据点,每个数据点是两个分类中的一个(红色表示 x1>x2,黄色表示 x1<x2),其实用一条直线 x1=x2就可以完成它们的分类,那么决策树会这么做吗?
import matplotlib.pyplot as plt
def form_linearly_separable_data(n=500, x1_min=0, x1_max=30,
x2_min=0, x2_max=30):
data, target = [], []
for i in range(n):
x1 = np.random.randint(x1_min, x1_max)
x2 = np.random.randint(x2_min, x2_max)
if np.abs(x1 - x2) > 0.5:
data.append([x1, x2])
target.append(np.sign(x1 - x2))
return np.array(data), np.array(target)
X, y = form_linearly_separable_data()
plt.scatter(X[:, 0], X[:, 1], c=y, cmap='autumn', edgecolors='black')

训练一个决策树对上面的数据进行分类,并绘制分类边界。
tree = DecisionTreeClassifier(random_state=17).fit(X, y)
def get_grid(data, eps=0.01):
x_min, x_max = data[:, 0].min() - 1, data[:, 0].max() + 1
y_min, y_max = data[:, 1].min() - 1, data[:, 1].max() + 1
return np.meshgrid(np.arange(x_min, x_max, eps),
np.arange(y_min, y_max, eps))
xx, yy = get_grid(X)
predicted = tree.predict(np.c_[xx.ravel(), yy.ravel()]).reshape(xx.shape)
plt.pcolormesh(xx, yy, predicted, cmap='autumn')
plt.scatter(X[:, 0], X[:, 1], c=y, s=100,
cmap='autumn', edgecolors='black', linewidth=1.5)
plt.title('Easy task. Decision tree compexifies everything')

可视化决策树
dot_data = export_graphviz(tree_grid.best_estimator_, feature_names=df.columns, filled=True)
graph = pydotplus.graph_from_dot_data(dot_data)
# 显示图像
display(Image(graph.create_png()))

从上可知,决策树构建的边界过于复杂,而且树的深度过深,产生了过拟合现象。
再训练一个 k-NN 模型,看看它在这个任务上的表现情况。该单元格执行时间较长,建议在线下练习:
knn = KNeighborsClassifier(n_neighbors=1).fit(X, y)
xx, yy = get_grid(X)
predicted = knn.predict(np.c_[xx.ravel(), yy.ravel()]).reshape(xx.shape)
plt.pcolormesh(xx, yy, predicted, cmap='autumn')
plt.scatter(X[:, 0], X[:, 1], c=y, s=100,
cmap='autumn', edgecolors='black', linewidth=1.5)
plt.title('Easy task, kNN. Not bad')

从上可知,最近邻方法的表现比决策树好一点.
在 MNIST 手写数字识别任务中应用决策树和 k-NN
现在可以看看这两个算法应用到实际任务上的表现如何,首先载入 sklearn 内置的 MNIST 手写数字数据集,该数据库中手写数字的图片为 8x8 的矩阵,矩阵中的值表示每个像素的白色亮度。
from sklearn.datasets import load_digits
data = load_digits()
X, y = data.data, data.target
X[0, :].reshape([8, 8])
绘制一些 MNIST 手写数字。
f, axes = plt.subplots(1, 4, sharey=True, figsize=(16, 6))
for i in range(4):
axes[i].imshow(X[i, :].reshape([8, 8]), cmap='Greys')

使用 train_test_split() 方法分割数据集,其中的 70% 作为训练集(X_train,y_train),30% 作为留置集(X_holdout,y_holdout)
X_train, X_holdout, y_train, y_holdout = train_test_split(
X, y, test_size=0.3, random_state=17)
使用随机参数训练决策树和 k-NN。
tree = DecisionTreeClassifier(max_depth=5, random_state=17)
knn_pipe = Pipeline([('scaler', StandardScaler()),
('knn', KNeighborsClassifier(n_neighbors=10))])
tree.fit(X_train, y_train)
knn_pipe.fit(X_train, y_train)
训练好之后,分别在留置集上做出预测。
tree_pred = tree.predict(X_holdout)
knn_pred = knn_pipe.predict(X_holdout)
accuracy_score(y_holdout, knn_pred), accuracy_score(
y_holdout, tree_pred) # (0.976, 0.666)
# (0.975925925925926, 0.6666666666666666)
从上可知,k-NN 做得更好,不过别忘了我们用的是随机参数。现在,使用交叉验证调优决策树模型,因为这次任务所需考虑的特征比之前任务中的更多,所以可以增加参数的大小。
tree_params = {'max_depth': [10, 20, 30],
'max_features': [30, 50, 64]}
tree_grid = GridSearchCV(tree, tree_params,
cv=5, n_jobs=-1, verbose=True)
tree_grid.fit(X_train, y_train)
查看交叉验证得到的最佳参数组合和相应的准确率。
tree_grid.best_params_, tree_grid.best_score_
# ({'max_depth': 10, 'max_features': 50}, 0.8568203376968316)
调优后决策树模型的准确率达到了 84.4%,但还不到使用随机参数的 k-NN 的准确率(97%)。现在,使用交叉验证调优 k-NN 模型。
np.mean(cross_val_score(KNeighborsClassifier(
n_neighbors=1), X_train, y_train, cv=5))
# 0.9864858028204642
从上可知,调优后的 k-NN 在这一数据集上可以达到 98.7% 的准确率。
下面在这一数据集上训练随机森林模型,在大多数数据集上,它的效果比 k-NN 要好。
0.9753462341111744
从上可知,在这个数据集中随机森林的准确率(93.5%)不如 k-NN(98.7%)。当然,我们没有对随机森林的参数进行任何调优,但即使经过调优,训练精确度也无法超过 k-NN。
决策树、k-NN、随机森林在这个数据集上的准确率如下所示:

从这个任务中得到的结论(同时也是一个通用的建议):首先查看简单模型(决策树、最近邻)在你的数据上的表现,因为可能仅使用简单模型就已经表现得足够好了。
最近邻方法的复杂情形
下面考虑另一种情况,即在一个分类问题中,某个特征直接和目标变量成比例的情况。
def form_noisy_data(n_obj=1000, n_feat=100, random_seed=17):
np.seed = random_seed
y = np.random.choice([-1, 1], size=n_obj)
# 第一个特征与目标成比例
x1 = 0.3 * y
# 其他特征为噪声
x_other = np.random.random(size=[n_obj, n_feat - 1])
return np.hstack([x1.reshape([n_obj, 1]), x_other]), y
X, y = form_noisy_data()
使用最近邻方法训练模型后,查看交叉验证和留置集的准确率,并绘制这两个准确率随 n_neighbors 最近邻数目 参数变化的曲线,这样的曲线被称为验证曲线。
from sklearn.model_selection import cross_val_score
X_train, X_holdout, y_train, y_holdout = train_test_split(
X, y, test_size=0.3, random_state=17)
cv_scores, holdout_scores = [], []
n_neighb = [1, 2, 3, 5] + list(range(50, 550, 50))
for k in n_neighb:
knn_pipe = Pipeline([('scaler', StandardScaler()),
('knn', KNeighborsClassifier(n_neighbors=k))])
cv_scores.append(np.mean(cross_val_score(
knn_pipe, X_train, y_train, cv=5)))
knn_pipe.fit(X_train, y_train)
holdout_scores.append(accuracy_score(
y_holdout, knn_pipe.predict(X_holdout)))
plt.plot(n_neighb, cv_scores, label='CV')
plt.plot(n_neighb, holdout_scores, label='holdout')
plt.title('Easy task. kNN fails')
plt.legend()

上图表明,即使我们尝试在较广范围内改变 n_neighbors 参数,基于欧几里得距离的 k-NN 在这个问题上依旧表现不佳。
下面用决策树训练一个模型,看看它在这个任务上的表现如何。
tree = DecisionTreeClassifier(random_state=17, max_depth=1)
tree_cv_score = np.mean(cross_val_score(tree, X_train, y_train, cv=5))
tree.fit(X_train, y_train)
tree_holdout_score = accuracy_score(y_holdout, tree.predict(X_holdout))
print('Decision tree. CV: {}, holdout: {}'.format(
tree_cv_score, tree_holdout_score))
在这一任务中,决策树完美地解决了问题,在交叉验证和留置集上都得到了 100% 的准确率。其实,k-NN 之所以在这个任务上表现不佳并非该方法本身的问题,而是因为使用了欧几里得距离,因为欧几里得距离没能察觉出有一个特征(成比例)比其他所有特征(噪声)更重要。
决策树和最近邻方法的优势和劣势
决策树:
优势:
- 易于理解和解释:决策树可以直观地展示决策过程,易于理解和解释,可以为决策提供可视化。
- 能够处理数值型和类别型数据:决策树可以处理混合类型的数据,无需进行特征归一化。
- 适用于大数据集:在较大的数据集上表现良好,训练速度相对较快。
- 可以捕捉特征之间的交互作用。
劣势:
- 容易过拟合:决策树容易生成复杂的树结构,导致过拟合问题,需要进行剪枝等操作。
- 对噪声敏感:容易受到数据中噪声的影响。
- 不稳定性:数据的细微变化可能导致生成完全不同的决策树。
- 不擅长处理连续型变量:对于连续型变量,决策树可能需要大量的数据分割点才能准确表示。
最近邻方法(K-最近邻):
优势:
- 无需训练:K-最近邻方法无需显式训练模型,新实例加入时算法不需要重新训练。
- 非参数方法:对数据分布没有假设,适用于非线性数据。
- 容易实现:K-最近邻算法简单直观,易于实现。
- 适用于多分类问题。
劣势:
- 需要大量内存:需要保存所有训练数据,对内存消耗较大。
- 预测速度较慢:在预测时需要计算新实例与所有训练实例的距离,计算量大。
- 对异常值敏感:因为预测结果依赖于最近邻实例,所以异常值可能会对结果产生较大影响。
- 需要进行特征缩放:对特征尺度敏感,需要进行特征缩放处理。
本文来自博客园,作者:PaleKernel,转载请注明原文链接:https://www.cnblogs.com/PaleKernel/articles/18090914

浙公网安备 33010602011771号