从决策树到残差网络:手把手教你掌握机器学习核心算法(附BUAA考试重点)
从决策树到残差网络:手把手教你掌握机器学习核心算法(附BUAA考试重点)
最近几年,机器学习从实验室走向了工业界,成了不少同学求职和深造路上的“硬通货”。无论是想在北京航空航天大学(BUAA)的相关课程中取得好成绩,还是希望真正掌握这些算法的精髓,光靠死记硬背公式和推导是远远不够的。算法的生命力在于理解其背后的思想,并能在代码和实际问题中“跑”起来。这篇文章,我就想和你聊聊从经典的决策树到前沿的残差网络这一系列核心算法,不仅拆解它们的原理,更会分享如何用代码实现,以及如何将这些知识转化为应对考试和实际项目的利器。我们的旅程,将从最直观的“树”开始。
1. 决策树:从“如果-那么”到信息度量
决策树大概是机器学习中最符合人类直觉的模型之一。想象一下医生诊断病情:如果发烧,那么检查是否有咳嗽;如果咳嗽,再询问是否有接触史……这一连串的“如果-那么”规则,最终导向一个结论。决策树就是将这种决策过程形式化、自动化。
构建一棵树的核心在于:在每个节点上,选择哪个特征进行分裂,能让后续的路径“最清晰”?这里就引入了信息增益或基尼不纯度等量化指标。信息增益基于信息论中的熵,熵衡量了数据的混乱程度。一个节点的数据越纯(比如全是正例或全是反例),其熵就越低。
提示:在BUAA的考试中,决策树相关的计算题常涉及信息增益的具体计算,务必熟悉
log2运算,并理解其物理意义是“不确定性减少的量”。
计算信息增益的步骤,可以概括为以下几步:
- 计算数据集的初始熵:
Entropy(D) = - Σ (p_i * log2(p_i)),其中p_i是第i类样本在数据集D中的比例。 - 针对每个特征,计算按该特征分裂后的条件熵:对于特征
A,其有v个取值,将D划分为v个子集D_1, D_2, ..., D_v。条件熵Entropy_A(D) = Σ (|D_v|/|D|) * Entropy(D_v)。 - 计算信息增益:
Gain(D, A) = Entropy(D) - Entropy_A(D)。 - 选择信息增益最大的特征作为当前节点的分裂特征。
除了信息增益,增益率和基尼指数也是常用的划分标准。C4.5算法使用增益率来克服信息增益对取值数目多的特征的偏好,而CART树则常用基尼指数。它们的对比如下:
| 指标 | 计算公式 | 特点 | 常用算法 |
|---|---|---|---|
| 信息增益 | Gain(D,A) = Entropy(D) - Σ(|D_v|/|D|)*Entropy(D_v) | 偏向选择取值多的特征 | ID3 |
| 增益率 | Gain_ratio(D,A) = Gain(D,A) / SplitInfo_A(D) | 对取值多的特征施加惩罚 | C4.5 |
| 基尼指数 | Gini(D) = 1 - Σ(p_i^2);Gini_index(D,A) = Σ(|D_v|/|D|)*Gini(D_v) | 衡量数据的不纯度,计算更简单 | CART(分类) |
在实际编码中,递归地构建决策树是一个经典的练习。下面是一个使用 scikit-learn 快速构建并可视化决策树的例子:
from sklearn.datasets import load_iris
from sklearn.tree import DecisionTreeClassifier, plot_tree
import matplotlib.pyplot as plt
# 加载鸢尾花数据集
iris = load_iris()
X, y = iris.data, iris.target
# 创建决策树分类器,使用‘entropy’作为划分标准(即信息增益)
clf = DecisionTreeClassifier(criterion='entropy', max_depth=3, random_state=42)
clf.fit(X, y)
# 可视化决策树
plt.figure(figsize=(12, 8))
plot_tree(clf, filled=True, feature_names=iris.feature_names, class_names=iris.target_names)
plt.show()
这段代码会生成一棵深度为3的决策树图,其中节点颜色深度代表了类别的纯度。通过动手运行和调整参数(如 max_depth, min_samples_split),你能直观感受到预剪枝如何防止过拟合。考试中,除了计算,可能还会问及决策树的优缺点:优点是可解释性强、能处理数值和类别数据、不需要特征缩放;缺点是容易过拟合、对数据微小变化敏感(不稳定),而这正是集成学习要解决的问题。
2. 支持向量机(SVM):寻找最优边界的故事
如果说决策树是在特征空间里划出一个个矩形区域,那么支持向量机(SVM)的目标则是找到一条“最宽”的街道来分隔不同类别的数据。这条街道的边界就是分离超平面,而位于街道边缘上的样本点,就是至关重要的支持向量。SVM的优雅之处在于,最终的模型仅由这些支持向量决定,这使得它具有一定的稀疏性。
我们从最简单的情况——线性可分说起。硬间隔SVM的优化目标非常清晰:最大化间隔(Margin)。间隔定义为两个平行支撑超平面之间的距离。通过数学推导,这个最大化间隔的问题可以转化为一个凸二次规划问题:
最小化: (1/2) * ||w||^2
约束条件: y_i * (w^T * x_i + b) >= 1, 对于所有 i
这里的 w 是法向量,决定了超平面的方向,b 是偏置项。||w||^2 的最小化等价于间隔的最大化。
然而,现实中的数据很少完美线性可分。噪声和异常点的存在,使得我们必须允许一些样本点“越界”或落入间隔之内。这就是软间隔SVM引入的原因。通过引入松弛变量 ξ_i,优化目标变为:
最小化: (1/2) * ||w||^2 + C * Σ(ξ_i)
约束条件: y_i * (w^T * x_i + b) >= 1 - ξ_i, 且 ξ_i >= 0
其中,参数C成了一个关键的调节旋钮。它控制了“最大化间隔”和“最小化分类错误”之间的权衡。C值越大,对误分类的惩罚越重,间隔可能变窄,模型倾向于拟合更复杂的边界;C值越小,则允许更多的误分类,间隔变宽,模型更简单。
注意:在推导软间隔SVM的拉格朗日函数及KKT条件时,需要正确处理松弛变量及其对应的拉格朗日乘子。考试中常考察对偶问题的形式以及KKT条件的含义。
对于非线性问题,SVM的“杀手锏”是核技巧。其核心思想是,将原始特征空间中的样本映射到一个更高维(甚至是无穷维)的希尔伯特空间,使得在这个新空间中样本线性可分。我们不需要显式地知道这个映射函数 Φ(x) 的具体形式,只需要知道两个样本在高维空间的内积 K(x_i, x_j) = <Φ(x_i), Φ(x_j)>,这个函数 K 就是核函数。常用的核函数包括:
- 线性核:
K(x_i, x_j) = x_i^T * x_j。就是原始的线性SVM。 - 多项式核:
K(x_i, x_j) = (γ * x_i^T * x_j + r)^d。d控制多项式次数。 - 径向基函数核(RBF核/高斯核):
K(x_i, x_j) = exp(-γ * ||x_i - x_j||^2)。这是最常用的非线性核,γ控制了单个样本的影响范围。
使用 scikit-learn 实践SVM时,关键就在于调节 C 和核函数参数。下面是一个使用RBF核SVM的示例:
from sklearn.svm import SVC
from sklearn.datasets import make_moons
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
import numpy as np
# 生成非线性可分的“月亮”数据集
X, y = make_moons(n_samples=200, noise=0.2, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
# 创建RBF核SVM分类器
svm_clf = SVC(kernel='rbf', C=10.0, gamma=0.5) # gamma即RBF核中的γ参数
svm_clf.fit(X_train, y_train)
# 预测并评估
y_pred = svm_clf.predict(X_test)
print(f"测试集准确率: {accuracy_score(y_test, y_pred):.4f}")
# 可以尝试不同的C和gamma,观察决策边界的变化
# 例如:C=0.1, gamma=5 可能会导致过拟合;C=1000, gamma=0.1 可能欠拟合。
理解SVM的推导,特别是对偶问题和KKT条件,是BUAA考试中的一个重点。这不仅仅是为了应试,更是理解其最优性保证和核方法思想的基石。
3. 集成学习:团结就是力量
“三个臭皮匠,顶个诸葛亮。”集成学习完美诠释了这句话。其核心思想是构建并结合多个学习器(称为“基学习器”或“弱学习器”)来完成学习任务。通过聚合多个模型的预测,集成方法通常能获得比单一模型更优越的泛化性能、更强的鲁棒性。根据基学习器的生成方式,集成学习主要分为两大类:并行方法和串行方法。
Bagging 是并行方法的代表,其全称是Bootstrap Aggregating。它的工作流程非常直观:
- 从原始训练集中使用自助采样法(Bootstrap Sampling,即有放回抽样)抽取多个子训练集。
- 每个子训练集独立训练一个基学习器(通常是决策树这样的不稳定学习器)。
- 对所有基学习器的预测结果进行投票(分类)或平均(回归)。
Bagging通过降低方差来提高模型稳定性,特别适用于高方差、低偏差的模型(如深度决策树)。随机森林是Bagging的一个扩展和特化,它在决策树训练过程中,不仅对样本进行随机采样,还对特征进行随机选择,进一步增强了基学习器的多样性,提升了集成的效果。
与Bagging并行独立训练不同,Boosting 是一种串行迭代的方法。它的思想是:后续的模型专注于纠正前序模型犯的错误。最经典的算法是AdaBoost。其步骤如下:
- 初始化所有训练样本的权重为相等值。
- 对于每一轮迭代
t=1 to T: a. 使用当前样本权重分布训练一个弱学习器。 b. 计算该弱学习器的加权错误率。 c. 根据错误率计算该弱学习器的权重(错误率越低,权重越高)。 d. 更新样本权重:增加被当前弱学习器分错的样本的权重,减少分对样本的权重。这使得下一轮的学习器更关注难分的样本。 - 将所有弱学习器按其权重进行加权结合,得到最终模型。
Boosting通过不断调整样本权重,将一系列弱学习器组合成一个强学习器,主要降低的是模型的偏差。梯度提升树(如XGBoost, LightGBM, CatBoost)则是将Boosting的思想与决策树结合,并用梯度下降来优化任意可微的损失函数,成为了当今数据科学竞赛和工业界表格数据建模的绝对主力。
为了直观对比Bagging和Boosting,我们可以看下面这个表格:
| 特性 | Bagging (如随机森林) | Boosting (如AdaBoost, GBDT) |
|---|---|---|
| 学习器关系 | 并行,相互独立 | 串行,依赖前序结果 |
| 样本权重 | 每轮平等采样,权重不变 | 每轮调整,错误样本权重增加 |
| 目标 | 降低方差,提高稳定性 | 降低偏差,提高准确性 |
| 基学习器 | 通常为强学习器(如深树),不稳定 | 通常为弱学习器(如浅树) |
| 过拟合风险 | 不易过拟合(因平均效应) | 容易过拟合(若迭代轮数过多) |
| 典型算法 | 随机森林 | AdaBoost, GBDT, XGBoost |
在考试中,对于集成学习的考察往往要求理解Bagging和Boosting的根本区别,并能解释为什么随机森林需要随机选择特征,以及AdaBoost中样本权重更新公式的含义。在实际应用中,面对一个具体问题,如果你的基模型(如单棵决策树)已经足够复杂、方差很大,那么Bagging(随机森林)通常是安全有效的首选;如果你的简单模型(如浅树)表现不佳(偏差大),那么可以尝试Boosting系列算法。
4. 神经网络进阶:从BP到ResNet的深度理解
神经网络,特别是深度学习,是机器学习近年爆发式发展的引擎。要真正掌握它,不能停留在调包层面,必须深入其训练的核心——反向传播算法,并理解现代网络架构如残差网络是如何解决深层网络训练难题的。
反向传播是神经网络训练的基石。它通过链式法则,将最终损失函数的梯度,从输出层逐层反向传播至每一层,从而更新每一层的权重参数。我们以一个简单的三层网络(输入层、一个隐藏层、输出层)为例,使用均方误差损失和Sigmoid激活函数,其关键步骤的推导如下:
设网络输出为 a^L,真实标签为 y,损失 L = 1/2 * (a^L - y)^2。
- 输出层误差:
δ^L = ∂L/∂z^L = (a^L - y) ⊙ σ'(z^L),其中z^L是加权输入,⊙表示逐元素乘法。 - 隐藏层误差反向传播:
δ^l = ((W^{l+1})^T * δ^{l+1}) ⊙ σ'(z^l)。这就是误差从后一层l+1传播到前一层l的过程。 - 计算梯度:
∂L/∂W^l = δ^l * (a^{l-1})^T,∂L/∂b^l = δ^l。 - 参数更新:使用梯度下降
W^l = W^l - α * ∂L/∂W^l。
提示:在BUAA考试中,BP推导是经典题目。务必清晰写出每一步的符号定义,并注意激活函数求导。常见的失分点是忘记对激活函数求导,或矩阵/向量的维度不匹配。
随着网络层数加深,梯度消失/爆炸和网络退化问题变得突出。梯度消失/爆炸源于链式法则中连乘的激活函数导数(如Sigmoid导数最大0.25),导致前面层的梯度变得极小或极大。ReLU等激活函数部分缓解了此问题。但更本质的“退化”问题在于:更深的网络在训练集上的表现反而比更浅的网络更差,这并非过拟合,而是优化困难。
残差网络的提出,革命性地解决了深层网络的退化问题。它的核心思想是引入“快捷连接”或“恒等映射”。对于一个堆叠层,我们不再期望它直接拟合一个潜在的复杂映射 H(x),而是让它拟合残差映射 F(x) = H(x) - x。这样,原始映射就变成了 H(x) = F(x) + x。
这个简单的加法操作带来了深远的影响:
- 梯度流动更顺畅:在反向传播时,梯度可以通过快捷连接直接流向更浅的层,避免了在多层非线性变换中连乘导致的梯度衰减,有效缓解了梯度消失。
- 恒等映射成为可能:如果某一堆叠层的最优解就是恒等映射(即输入等于输出),那么让
F(x)学习为0比让一个非线性层直接学习H(x)=x要容易得多。 - 避免了精度饱和:网络可以轻松地通过将残差块中的权重推向零来近似实现恒等映射,从而确保增加深度至少不会降低网络性能。
一个基础的残差块可以用如下代码示意(使用PyTorch风格):
import torch
import torch.nn as nn
import torch.nn.functional as F
class BasicBlock(nn.Module):
def __init__(self, in_channels, out_channels, stride=1):
super().__init__()
self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(out_channels)
self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(out_channels)
# 如果输入输出维度不一致(如通过下采样),需要用1x1卷积调整捷径连接的维度
self.shortcut = nn.Sequential()
if stride != 1 or in_channels != out_channels:
self.shortcut = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(out_channels)
)
def forward(self, x):
identity = x
out = F.relu(self.bn1(self.conv1(x)))
out = self.bn2(self.conv2(out))
out += self.shortcut(identity) # 关键:残差连接
out = F.relu(out)
return out
关于残差网络的损失函数求导,其过程与标准BP类似,但因为增加了快捷连接,梯度多了一条传播路径。设残差块输出为 y = F(x, {W_i}) + x,损失函数为 L。则对输入 x 的梯度为:
∂L/∂x = ∂L/∂y * (∂F/∂x + 1)。
这里的 +1 项正是来自快捷连接的恒等映射,它保证了梯度至少有一条值为1的路径回传,使得深层网络能够被有效训练。
从BP的推导到ResNet的理解,这条线索勾勒出了深度学习理论如何一步步解决工程实践中的核心挑战。在准备考试时,不仅要会推导公式,更要理解每一个设计(如ReLU、BatchNorm、Residual Connection)背后要解决的问题是什么。当你带着“解决问题”的视角去看待这些算法时,它们就不再是冰冷的数学符号,而是一个个精巧的工程解决方案。
更多推荐
所有评论(0)