深度学习与计算机视觉:一场认知革命
第一部分:奠基——深度学习核心概念
在深入计算机视觉的 spezifische Anwendungen (特定应用) 之前,我们必须对驱动这场革命的核心引擎——深度学习——有深刻且坚实的理解。这一部分将详细阐述深度学习的基本原理、关键组件和数学思想。
第一章:机器学习与深度学习的渊源
1.1 人工智能、机器学习与深度学习的关系
为了精确地定位深度学习,我们首先需要理解它在更广阔的人工智能领域中的位置。
-
人工智能 (Artificial Intelligence, AI):
- 定义: 人工智能是一个广阔的计算机科学分支,致力于创造能够执行通常需要人类智能才能完成的任务的机器或系统。这些任务包括学习、解决问题、理解语言、感知环境、做出决策等。
- 目标: AI的最终目标是创造出能够模拟、延伸甚至超越人类智能的机器。
- 范畴: AI包含了众多的子领域,如机器学习、自然语言处理 (NLP)、计算机视觉 (CV)、机器人学、专家系统等。
-
机器学习 (Machine Learning, ML):
- 定义: 机器学习是实现人工智能的一种重要方法。它专注于开发能够让计算机系统从数据中“学习”并改进其性能的算法,而无需进行显式的编程来指定每一步操作。
- 核心思想: 给予算法大量数据(经验),让算法自动从数据中发现模式、规律或做出预测。
- 与传统编程的区别:
- 传统编程:
输入数据
+程序 (显式规则)
->输出结果
- 机器学习:
输入数据
+期望输出 (部分场景)
->程序 (学习到的模型/规则)
- 传统编程:
- 主要类型:
- 监督学习 (Supervised Learning): 训练数据包含输入特征和对应的“正确答案”(标签或目标值)。算法学习从输入到输出的映射关系。例如,根据房屋大小、位置等特征预测房价(回归),或者根据邮件内容判断是否为垃圾邮件(分类)。
- 无监督学习 (Unsupervised Learning): 训练数据只有输入特征,没有对应的标签。算法需要自己从数据中发现结构、模式或关系。例如,将相似的用户聚类(聚类),或者降低数据维度以去除冗余信息(降维)。
- 强化学习 (Reinforcement Learning): 算法(称为智能体 Agent)通过与环境 (Environment) 交互来学习。智能体在环境中采取行动 (Action),环境反馈奖励 (Reward) 或惩罚 (Punishment)。智能体的目标是学习一个策略 (Policy) 来最大化累积奖励。例如,训练机器下棋或控制机器人行走。
-
深度学习 (Deep Learning, DL):
- 定义: 深度学习是机器学习的一个特定分支,它主要基于人工神经网络 (Artificial Neural Networks, ANNs),特别是包含多个“深”层次(即多个隐藏层)的神经网络。
- 核心特征:
- 层次化特征学习 (Hierarchical Feature Learning): 深度学习模型能够自动地从原始数据中学习到一系列层次化的特征表示。浅层学习简单的低级特征(如图像中的边缘、角点),深层则基于浅层特征组合出更复杂、更抽象的高级特征(如物体的部件、整个物体)。这种自动特征工程是深度学习强大的主要原因之一。
- 大规模数据驱动: 深度学习模型通常包含大量参数,需要大规模的标注数据才能充分发挥其潜力并避免过拟合。
- 计算密集型: 训练深度学习模型通常需要强大的计算资源,尤其是GPU(图形处理器),因为它们非常适合并行计算神经网络中的大量矩阵运算。
- 与传统机器学习中特征工程的区别: 在传统的机器学习流程中,特征工程(即从原始数据中提取和选择有用的特征)往往是最耗时、最依赖领域知识的步骤。深度学习在很大程度上自动化了这个过程。
它们之间的关系可以用集合图来表示:
graph TD
A[人工智能 (AI)] --> B(机器学习 (ML));
B --> C(深度学习 (DL));
style A fill:#f9f,stroke:#333,stroke-width:2px,color:#000
style B fill:#ccf,stroke:#333,stroke-width:2px,color:#000
style C fill:#9f9,stroke:#333,stroke-width:2px,color:#000
人工智能 (AI)
: 是最广阔的领域。机器学习 (ML)
: 是实现AI的一种方法。深度学习 (DL)
: 是机器学习中一种基于深层神经网络的强大技术。
深度学习的成功,尤其是在计算机视觉、自然语言处理等领域,极大地推动了人工智能的边界。
1.2 为什么是“深度”学习?“深”的含义与优势
“深度 (Deep)” 指的是神经网络中隐藏层的数量。传统的(浅层)神经网络通常只有1到2个隐藏层,而深度神经网络可以有数十、数百甚至数千个隐藏层。
“深”的优势:
-
更强的表示能力 (Increased Representational Power):
- 理论上,具有一个足够宽的单隐藏层的神经网络可以逼近任何连续函数(通用逼近定理 Universal Approximation Theorem)。然而,在实践中,用一个非常宽的浅层网络来实现复杂函数的逼近,可能需要指数级数量的神经元,这在参数效率和泛化能力上都不理想。
- 深度结构允许网络以更有效的方式(使用更少的参数)来表示复杂函数。每一层可以被看作是对前一层输出进行一次非线性变换,提取更高级别的特征。通过多层堆叠,网络可以学习到数据中非常复杂和抽象的模式。
- 可以认为,深度网络通过组合简单的非线性变换来构建高度复杂的非线性变换。每一层学习到的特征都是对输入数据的一种新的、更有用的表示。
-
层次化特征提取 (Hierarchical Feature Extraction):
- 这是深度学习最核心的优势之一,尤其在处理感知数据(如图像、语音)时。
- 以图像为例:
- 第一层 (靠近输入): 可能学习检测图像中的基本边缘、角点、颜色斑块等低级特征。
- 中间层: 可能将低级特征组合起来,学习检测更复杂的纹理、物体的局部部件(如眼睛、鼻子、轮子)。
- 更深层: 可能将部件组合起来,学习识别整个物体(如人脸、汽车、猫)。
- 最高层: 可能基于物体识别进行场景理解或更高级的推理。
- 这种层次化的方式与人类视觉系统的感知过程有一定的相似性。人类也是从简单的视觉元素开始,逐步构建对复杂场景的理解。
- 这种自动学习特征的能力,使得我们不再需要手动设计复杂的特征提取器,大大简化了模型开发流程,并往往能发现比人工设计更有效的特征。
-
参数共享与效率 (Parameter Sharing and Efficiency - 特别是CNN中):
- 在特定类型的深度网络(如卷积神经网络CNN,后续会详细讲解)中,参数共享机制(例如卷积核在图像不同位置的复用)极大地减少了模型的参数数量,使得训练深层网络成为可能,同时也增强了模型的平移不变性等良好特性。
-
更好的泛化能力 (Potentially Better Generalization):
- 虽然深度模型参数众多,容易过拟合,但如果数据量充足且配合适当的正则化技术,深度模型学习到的层次化特征往往具有更好的泛化能力。因为它们能抓住数据本质的、可迁移的模式,而不是仅仅记住训练数据中的噪声或特例。
-
端到端学习 (End-to-End Learning):
- 深度学习使得构建端到端模型成为可能。即从原始输入(如图像像素、原始文本)直接到最终输出(如物体类别、翻译结果),中间的特征提取和转换过程都由网络自动学习。这避免了传统多阶段系统中各模块误差累积的问题。
“深”也带来了挑战:
-
梯度消失/爆炸 (Vanishing/Exploding Gradients):
- 在非常深的网络中,使用基于梯度的优化算法(如反向传播)进行训练时,梯度信号在逐层向后传播的过程中可能会变得非常小(梯度消失)或非常大(梯度爆炸),导致浅层网络的参数难以更新或训练不稳定。
- 解决方法:ReLU等激活函数、合适的权重初始化方法、残差连接 (ResNets)、批归一化 (Batch Normalization)、梯度裁剪 (Gradient Clipping) 等。
-
计算复杂度高 (High Computational Complexity):
- 深层网络通常包含大量参数和运算,训练和推理都需要强大的计算资源(尤其是GPU)。
-
需要大量数据 (Need for Large Amounts of Data):
- 为了训练好包含大量参数的深度模型并避免过拟合,通常需要大规模的标注数据集。数据获取和标注本身就是一项巨大的挑战。
-
过拟合 (Overfitting):
- 深度模型强大的表示能力也使其容易在训练数据上过拟合,即模型在训练集上表现很好,但在未见过的测试集上表现差。
- 解决方法:正则化(L1/L2正则化、Dropout)、数据增强、早停 (Early Stopping) 等。
-
可解释性差 (Poor Interpretability - “Black Box” Problem):
- 深度神经网络的决策过程往往难以直观理解,它们像一个“黑箱”。理解模型为什么做出某个特定预测是一个活跃的研究领域。
尽管存在这些挑战,深度学习通过不断发展的技术和方法,在许多领域取得了突破性进展,尤其是在计算机视觉领域,它已经成为主导范式。
第二章:神经网络基础——感知机与多层感知机
深度学习的核心是人工神经网络。我们将从最简单的神经网络单元——感知机开始,逐步构建到更复杂的多层感知机。
2.1 生物神经元与人工神经元 (感知机)
人工神经网络的最初灵感来源于对生物神经系统的观察和简化。
生物神经元 (Biological Neuron):
- 组成:
- 细胞体 (Soma): 神经元的主要部分,包含细胞核。
- 树突 (Dendrites): 从细胞体延伸出的分支状结构,负责接收来自其他神经元的信号。
- 轴突 (Axon): 从细胞体延伸出的长纤维,负责将信号传递给其他神经元。
- 突触 (Synapse): 轴突末端与下一个神经元的树突(或细胞体)之间的连接点,信号通过化学物质(神经递质)在此传递。
- 工作方式 (高度简化):
- 树突接收来自多个其他神经元的输入信号。
- 这些信号在细胞体内被整合。
- 如果整合后的信号强度超过某个阈值,神经元就会被“激活 (fire)”,并通过轴突产生一个动作电位(一个电信号)传递给下游神经元。
- 突触的强度(连接权重)可以调节信号传递的效率,并且这种强度是可塑的(可以通过学习改变)。
人工神经元 (Artificial Neuron) / 感知机 (Perceptron):
感知机是最早也是最简单的人工神经元模型之一,由 Frank Rosenblatt 于1957年提出。它模拟了生物神经元的基本功能。
- 模型结构:
- 输入 (Inputs):
x_1, x_2, ..., x_n
,可以是一组特征值。 - 权重 (Weights):
w_1, w_2, ..., w_n
,每个输入对应一个权重,表示该输入的重要性。 - 偏置 (Bias):
b
,一个额外的参数,可以看作是神经元激活的难易程度的调整项。可以将其视为一个权重为b
,输入恒为1
的特殊输入x_0=1, w_0=b
。 - 加权和 (Weighted Sum / Net Input):
z = (w_1*x_1 + w_2*x_2 + ... + w_n*x_n) + b = Σ(w_i*x_i) + b
。这是对所有输入进行加权求和,再加上偏置。 - 激活函数 (Activation Function):
f(z)
,对加权和z
应用一个非线性(或线性)函数,产生神经元的输出y
。
- 输入 (Inputs):
感知机的数学表示:
y = f( Σ(w_i * x_i) + b )
或者使用向量表示:
z = w^T * x + b
y = f(z)
其中 w = [w_1, ..., w_n]^T
是权重向量, x = [x_1, ..., x_n]^T
是输入向量。
感知机中常用的激活函数 (早期):
- 阶跃函数 (Step Function / Heaviside Step Function):
f(z) = 1
ifz >= θ
(阈值)
f(z) = 0
ifz < θ
如果将偏置b
定义为-θ
,则变为:
f(z) = 1
ifΣ(w_i*x_i) + b >= 0
f(z) = 0
ifΣ(w_i*x_i) + b < 0
这种感知机输出二元值 (0或1),常用于二分类问题。它在输入空间中定义了一个线性决策边界(超平面)。
感知机的学习规则 (Perceptron Learning Rule - 针对阶跃激活函数和二分类):
感知机的学习目标是找到一组权重 w
和偏置 b
,使得对于给定的训练样本 (x, t)
(其中 t
是真实标签,例如0或1),感知机的输出 y
尽可能接近 t
。
对于每个训练样本 (x, t)
:
- 计算感知机的输出
y = f(w^T * x + b)
。 - 更新权重和偏置:
w_new = w_old + η * (t - y) * x
b_new = b_old + η * (t - y)
其中:η
(eta) 是学习率 (learning rate),一个小的正数,控制每次更新的步长。(t - y)
是误差。- 如果
y = t
(预测正确),则(t - y) = 0
,权重不更新。 - 如果
y = 0, t = 1
(假阴性),则(t - y) = 1
,权重向x
的方向增加 (w_new = w_old + η*x
),使得w^T*x
更可能为正。 - 如果
y = 1, t = 0
(假阳性),则(t - y) = -1
,权重向x
的反方向减少 (w_new = w_old - η*x
),使得w^T*x
更可能为负。
- 如果
感知机的局限性:
- 线性可分性 (Linear Separability): 单个感知机(使用阶跃激活函数)只能解决线性可分的问题。也就是说,它只能找到一个超平面来完美地分离开属于不同类别的样本点。如果数据不是线性可分的(例如XOR异或问题),单个感知机无法收敛到一个正确的解。
- XOR问题:
输入 (x1, x2) 输出 (t) (0, 0) 0 (0, 1) 1 (1, 0) 1 (1, 1) 0 你无法用一条直线在二维平面上将 (0,1), (1,0)
与(0,0), (1,1)
分开。
- XOR问题:
这个局限性导致了第一次AI寒冬,直到多层感知机和更强大的学习算法的出现。
代码示例:简单感知机实现 (用于理解,非实际DL库用法)
import numpy as np # 导入NumPy库,用于数值运算
class Perceptron: # 定义感知机类
def __init__(self, num_inputs, learning_rate=0.01, epochs=100): # 构造函数
# num_inputs: 输入特征的数量
# learning_rate: 学习率
# epochs: 训练迭代的轮数
self.weights = np.random.rand(num_inputs) # 初始化权重为0到1之间的随机数,数量与输入特征数相同
self.bias = np.random.rand(1) # 初始化偏置为0到1之间的随机数 (一个标量)
self.learning_rate = learning_rate # 设置学习率
self.epochs = epochs # 设置训练轮数
print(f"感知机初始化完成。权重形状: {
self.weights.shape}, 偏置: {
self.bias}") # 打印初始化信息
def _step_function(self, z): # 定义阶跃激活函数 (私有方法)
# z: 加权和
return 1 if z >= 0 else 0 # 如果z大于等于0,输出1,否则输出0
def predict(self, inputs): # 定义预测方法
# inputs: 输入特征向量 (NumPy数组)
weighted_sum = np.dot(inputs, self.weights) + self.bias # 计算加权和: inputs和weights的点积,然后加上偏置
prediction = self._step_function(weighted_sum) # 应用阶跃函数得到预测结果
return prediction # 返回预测值 (0或1)
def train(self, training_inputs, labels): # 定义训练方法
# training_inputs: 训练输入数据 (一个包含多个样本的列表或NumPy数组,每个样本是一个特征向量)
# labels: 对应的真实标签 (一个包含多个标签的列表或NumPy数组)
print(f"\n开始训练感知机...") # 打印开始训练信息
print(f"学习率: {
self.learning_rate}, 训练轮数: {
self.epochs}") # 打印训练参数
num_samples = len(training_inputs) # 获取训练样本数量
if num_samples != len(labels): # 检查输入和标签数量是否匹配
raise ValueError("训练输入和标签的数量必须相同。") # 抛出值错误
for epoch in range(self.epochs): # 外层循环,迭代多轮
num_errors = 0 # 初始化本轮错误计数
for i in range(num_samples): # 内层循环,遍历每个训练样本
inputs = training_inputs[i] # 获取当前样本的输入特征
label = labels[i] # 获取当前样本的真实标签
prediction = self.predict(inputs) # 使用当前权重进行预测
error = label - prediction # 计算误差 (t - y)
if error != 0: # 如果预测错误 (error不为0)
num_errors += 1 # 错误计数加1
# 更新权重和偏置
# w_new = w_old + learning_rate * error * x
# b_new = b_old + learning_rate * error
self.weights += self.learning_rate * error * inputs # 更新权重向量
self.bias += self.learning_rate * error # 更新偏置 (error是标量,inputs是向量,NumPy会自动广播)
if epoch % 10 == 0 or epoch == self.epochs - 1: # 每10轮或最后一轮打印信息
print(f"轮次 {
epoch+1}/{
self.epochs}, 本轮错误数: {
num_errors}/{
num_samples}") # 打印当前轮次和错误数
if num_errors == 0 and epoch > 0: # 如果某一轮没有错误 (且不是第一轮,避免未开始就停止)
print(f"在轮次 {
epoch+1} 时,模型已收敛 (无错误)。停止训练。") # 打印收敛信息
break # 提前停止训练
print("感知机训练完成。") # 打印训练完成信息
print(f"最终权重: {
self.weights}") # 打印最终权重
print(f"最终偏置: {
self.bias}") # 打印最终偏置
# --- 使用感知机解决一个简单的线性可分问题:AND逻辑门 ---
# AND门真值表:
# x1 | x2 | AND
# ---|----|----
# 0 | 0 | 0
# 0 | 1 | 0
# 1 | 0 | 0
# 1 | 1 | 1
print("--- 测试感知机:实现AND逻辑门 ---") # 打印测试标题
# 训练数据
training_data_and = np.array([ # AND门的输入特征
[0, 0],
[0, 1],
[1, 0],
[1, 1]
])
labels_and = np.array([0, 0, 0, 1]) # AND门的对应标签
# 创建并训练感知机
perceptron_and = Perceptron(num_inputs=2, learning_rate=0.1, epochs=50) # 创建感知机实例,输入维度为2
perceptron_and.train(training_data_and, labels_and) # 训练感知机
# 测试训练好的感知机
print("\n测试训练好的AND感知机:") # 打印测试信息
print(f"输入 [0, 0] -> 预测: {
perceptron_and.predict(np.array([0, 0]))}, 期望: 0") # 测试输入[0,0]
print(f"输入 [0, 1] -> 预测: {
perceptron_and.predict(np.array([0, 1]))}, 期望: 0") # 测试输入[0,1]
print(f"输入 [1, 0] -> 预测: {
perceptron_and.predict(np.array([1, 0]))}, 期望: 0") # 测试输入[1,0]
print(f"输入 [1, 1] -> 预测: {
perceptron_and.predict(np.array([1, 1]))}, 期望: 1") # 测试输入[1,1]
# --- 尝试用感知机解决XOR问题 (预期会失败或效果不佳) ---
print("\n--- 测试感知机:尝试实现XOR逻辑门 (预期失败) ---") # 打印XOR测试标题
# XOR门真值表:
# x1 | x2 | XOR
# ---|----|----
# 0 | 0 | 0
# 0 | 1 | 1
# 1 | 0 | 1
# 1 | 1 | 0
training_data_xor = np.array([ # XOR门的输入特征
[0, 0],
[0, 1],
[1, 0],
[1, 1]
])
labels_xor = np.array([0, 1, 1, 0]) # XOR门的对应标签
# 创建并训练感知机
perceptron_xor = Perceptron(num_inputs=2, learning_rate=0.1, epochs=200) # 增加训练轮数,看是否能找到解
perceptron_xor.train(training_data_xor, labels_xor) # 训练
# 测试XOR感知机
print("\n测试训练好的XOR感知机 (很可能不完美):") # 打印测试信息
correct_xor_predictions = 0 # 初始化正确预测计数
for i in range(len(training_data_xor)): # 遍历XOR测试数据
inputs = training_data_xor[i] # 获取输入
label = labels_xor[i] # 获取真实标签
prediction = perceptron_xor.predict(inputs) # 进行预测
print(f"输入 {
inputs} -> 预测: {
prediction}, 期望: {
label}") # 打印预测结果
if prediction == label: # 如果预测正确
correct_xor_predictions +=1 # 正确计数加1
print(f"XOR问题上的准确率: {
correct_xor_predictions / len(training_data_xor) * 100:.2f}%") # 打印准确率
# 你会发现,对于XOR问题,单个感知机通常无法达到100%的准确率,因为它不是线性可分的。
# 训练过程可能不会收敛到0错误,或者找到一个次优的线性边界。
这个简单的感知机代码清晰地展示了其初始化、预测和基于误差的权重更新过程。对于AND这样的线性可分问题,它可以很好地工作。但对于XOR这样的非线性问题,它的局限性就显现出来了。
为了克服单个感知机的线性限制,研究者们开始探索将多个感知机组合起来,形成了多层感知机 (Multi-Layer Perceptron, MLP)。
2.2 多层感知机 (Multi-Layer Perceptron, MLP)
多层感知机通过引入一个或多个隐藏层 (Hidden Layers) 来克服单个感知机的线性局限性。每个隐藏层包含若干神经元,这些神经元对前一层的输出进行处理,并将结果传递给下一层。
MLP的结构:
-
输入层 (Input Layer):
- 接收原始的输入特征数据。
- 输入层的节点数量等于输入特征的维度。
- 通常输入层本身不进行计算,只是将数据传递给第一个隐藏层。
-
隐藏层 (Hidden Layer(s)):
- 位于输入层和输出层之间。可以有一个或多个隐藏层。
- 每个隐藏层的神经元都与前一层的所有神经元全连接(Dense Connection 或 Fully Connected Layer),也与下一层的所有神经元全连接。
- 隐藏层神经元的数量和隐藏层的层数是MLP模型的超参数,需要根据具体问题进行设计和调整。
- 关键点: 隐藏层中的神经元通常使用非线性激活函数 (如Sigmoid, Tanh, ReLU等)。正是这些非线性激活函数赋予了MLP学习非线性映射的能力。如果隐藏层使用线性激活函数,那么多层线性网络的组合仍然是一个线性网络,无法解决非线性问题。
-
输出层 (Output Layer):
- 产生模型的最终输出。
- 输出层神经元的数量和激活函数的选择取决于具体的任务类型:
- 二分类问题: 输出层通常有一个神经元,使用Sigmoid激活函数,输出一个0到1之间的概率值。
- 多类别分类问题 (单标签): 输出层神经元数量等于类别数,通常使用Softmax激活函数,输出每个类别的概率分布(所有概率之和为1)。
- 回归问题: 输出层通常有一个或多个神经元(取决于要预测的值的数量),使用线性激活函数(即没有激活函数,或者说恒等激活
f(z)=z
),直接输出预测的连续值。 - 多标签分类问题: 输出层神经元数量等于类别数,每个神经元使用Sigmoid激活函数,独立地输出该标签存在的概率。
MLP如何解决非线性问题 (如XOR)?
以XOR问题为例,一个包含一个隐藏层的MLP可以解决它:
- 输入层: 2个神经元 (对应x1, x2)。
- 隐藏层: 例如,2个神经元,使用非线性激活函数(如Sigmoid或ReLU)。
- 可以想象,隐藏层的第一个神经元可能学习识别
x1 OR x2
(或者类似的功能,使得 (0,0) 与其他分开)。 - 隐藏层的第二个神经元可能学习识别
x1 NAND x2
(或者类似的功能,使得 (1,1) 与其他分开)。 - 这些隐藏单元将原始输入空间映射到一个新的特征空间,在这个新的特征空间中,问题可能变得线性可分。
- 可以想象,隐藏层的第一个神经元可能学习识别
- 输出层: 1个神经元,使用合适的激活函数(例如Sigmoid,然后根据阈值判断)。输出层的神经元再对隐藏层学习到的新特征进行线性组合,从而做出最终的分类。
MLP的“前向传播 (Forward Propagation)”过程:
数据从输入层开始,逐层向前传递,直到输出层。
假设一个MLP有L层 (包括输入层视为第0层,输出层为第L-1层)。
对于第 l
层的第 j
个神经元:
-
计算加权和 (Net Input):
z_j^(l) = Σ (w_ji^(l) * a_i^(l-1)) + b_j^(l)
其中:a_i^(l-1)
是前一层 (第l-1
层) 第i
个神经元的激活输出。对于输入层,a_i^(0) = x_i
(输入特征)。w_ji^(l)
是从前一层第i
个神经元到当前层第j
个神经元的连接权重。b_j^(l)
是当前层第j
个神经元的偏置。
-
计算激活输出 (Activation Output):
a_j^(l) = f(z_j^(l))
其中f
是该层神经元使用的激活函数。
这个过程从第一层隐藏层开始,一直计算到输出层,得到最终的预测结果。
MLP的训练:反向传播算法 (Backpropagation Algorithm)
MLP的参数(所有权重 w
和偏置 b
)需要通过学习算法从训练数据中进行优化。最常用且核心的算法是反向传播算法。
-
目标: 最小化一个预定义的损失函数 (Loss Function) 或 代价函数 (Cost Function)。损失函数衡量模型预测输出与真实标签之间的差异。
-
核心思想:
- 前向传播: 将一个训练样本输入网络,计算每一层神经元的激活输出,直到得到最终的预测结果。
- 计算损失: 根据预测结果和真实标签,计算损失函数的值。
- 反向传播误差:
- 首先计算输出层神经元的误差项 (error term,通常表示为
δ_j^(L-1)
)。这个误差项反映了该神经元的激活值对最终损失的贡献程度(通常与损失函数关于该神经元加权和的偏导数有关)。 - 然后,将误差从输出层逐层向前(反向)传播到每个隐藏层。对于第
l
层的神经元j
,其误差项δ_j^(l)
可以根据下一层 (第l+1
层) 所有神经元的误差项以及它们之间的连接权重来计算。
δ_j^(l) = ( Σ_k (w_kj^(l+1) * δ_k^(l+1)) ) * f'(z_j^(l))
(这里的f'(z_j^(l))
是激活函数f
在点z_j^(l)
处的导数。这就是为什么激活函数需要是可微的或至少是分段可微的。)
- 首先计算输出层神经元的误差项 (error term,通常表示为
- 计算梯度: 利用计算得到的各层误差项
δ
,可以计算出损失函数对于网络中每个权重w_ji^(l)
和偏置b_j^(l)
的偏导数(梯度):
∂L / ∂w_ji^(l) = a_i^(l-1) * δ_j^(l)
∂L / ∂b_j^(l) = δ_j^(l)
- 更新参数: 使用梯度下降 (Gradient Descent) 或其变体(如SGD, Adam, RMSprop等优化器)来更新网络中的所有权重和偏置,以减小损失:
w_new = w_old - η * (∂L / ∂w_old)
b_new = b_old - η * (∂L / ∂b_old)
-
迭代: 对训练集中的所有样本(或一批样本,即mini-batch)重复上述步骤,进行多轮 (epochs) 训练,直到损失函数收敛到足够小的值,或者达到预设的训练轮数。
反向传播算法是深度学习训练的核心,它使得我们能够有效地计算庞大网络中所有参数的梯度,并进行优化。我们将在后续章节更详细地讨论损失函数、激活函数、优化器以及反向传播的数学细节。
代码示例:使用Keras/TensorFlow构建和训练一个简单的MLP解决XOR问题
现代深度学习框架(如TensorFlow, Keras, PyTorch)极大地简化了MLP及其他复杂网络的构建和训练过程。我们不再需要手动实现反向传播的细节。
import numpy as np # 导入NumPy库
import tensorflow as tf # 导入TensorFlow库
from tensorflow import keras # 从TensorFlow中导入Keras API
from tensorflow.keras.models import Sequential # 导入Sequential模型类,用于构建序列化的网络层
from tensorflow.keras.layers import Dense # 导入Dense层类,即全连接层
from tensorflow.keras.optimizers import Adam # 导入Adam优化器
print(f"TensorFlow 版本: {
tf.__version__}") # 打印TensorFlow版本
print(f"Keras 版本: {
keras.__version__}") # 打印Keras版本
# --- 准备XOR问题的训练数据 ---
# 输入特征 (x1, x2)
training_data_xor_tf = np.array([
[0, 0],
[0, 1],
[1, 0],
[1, 1]
], dtype=np.float32) # 使用float32类型,深度学习常用
# 对应标签 (XOR结果)
labels_xor_tf = np.array([0, 1, 1, 0], dtype=np.float32) # 使用float32类型
# 对于二分类问题,Keras的Dense层配合sigmoid激活,标签通常是0或1的浮点数。
# 如果是多分类,标签通常是one-hot编码。
# --- 构建MLP模型 ---
# 使用Keras Sequential API,它允许我们像堆叠积木一样添加网络层
model_xor = Sequential([ # 创建一个Sequential模型实例
# 第一个隐藏层
# Dense(units, activation, input_shape)
# units: 该层神经元的数量 (例如4个)
# activation: 激活函数 (例如 'relu' 或 'sigmoid' 或 'tanh')
# input_shape: 输入数据的形状 (仅在第一层需要指定,这里是2个输入特征,所以是 (2,))
Dense(units=4, activation='relu', input_shape=(2,), name="hidden_layer_1"), # 添加第一个全连接层(隐藏层),4个神经元,ReLU激活,输入维度为2
# (可选) 可以添加更多隐藏层
# Dense(units=4, activation='relu', name="hidden_layer_2"),
# 输出层
# units=1: 因为是二分类问题,输出一个值
# activation='sigmoid': Sigmoid激活函数将输出压缩到0和1之间,可以解释为概率
Dense(units=1, activation='sigmoid', name="output_layer") # 添加输出层,1个神经元,Sigmoid激活
])
# 打印模型摘要,显示网络结构和参数数量
print("\n--- XOR MLP模型结构 ---") # 打印模型结构标题
model_xor.summary() # 打印模型的详细摘要信息
# --- 编译模型 ---
# 在训练模型之前,需要对其进行编译,配置学习过程。
# optimizer: 优化算法,用于更新网络权重 (例如 'adam', 'sgd', 'rmsprop')
# loss: 损失函数,衡量模型预测与真实标签之间的差异。
# 对于二分类问题 (Sigmoid输出层),常用 'binary_crossentropy'。
# metrics: 评估指标列表,用于在训练和测试期间监控模型的性能 (例如 'accuracy')。
model_xor.compile(optimizer=Adam(learning_rate=0.1), # 使用Adam优化器,设置学习率
loss='binary_crossentropy', # 使用二元交叉熵作为损失函数
metrics=['accuracy']) # 监控准确率指标
print("\n模型编译完成。") # 打印编译完成信息
# --- 训练模型 ---
# model.fit(x_train, y_train, epochs, batch_size, verbose)
# x_train: 训练输入数据
# y_train: 训练标签数据
# epochs: 训练轮数 (整个训练数据集被遍历的次数)
# batch_size: 批处理大小 (每次权重更新所使用的样本数量)。
# 如果未指定,默认为32。对于小数据集,可以设置为1或整个数据集大小。
# verbose: 日志显示模式 (0=安静, 1=进度条, 2=每轮一行)
print("\n开始训练XOR MLP模型...") # 打印开始训练信息
history = model_xor.fit(training_data_xor_tf, labels_xor_tf, # 传入训练数据和标签
epochs=200, # 训练200轮
batch_size=1, # 每1个样本更新一次权重 (随机梯度下降的极端情况)
verbose=1) # 显示每轮的训练进度和指标
print("XOR MLP模型训练完成。") # 打印训练完成信息
# --- 评估模型 (可选,因为数据集很小,训练和测试相同) ---
loss, accuracy = model_xor.evaluate(training_data_xor_tf, labels_xor_tf, verbose=0) # 在训练数据上评估模型
print(f"\n在训练数据上的最终损失: {
loss:.4f}") # 打印最终损失
print(f"在训练数据上的最终准确率: {
accuracy*100:.2f}%") # 打印最终准确率
# --- 使用训练好的模型进行预测 ---
print("\n使用训练好的XOR MLP模型进行预测:") # 打印预测信息
predictions_tf = model_xor.predict(training_data_xor_tf) # 对训练数据进行预测
# predict()的输出是Sigmoid激活后的原始概率值 (0到1之间)
for i in range(len(training_data_xor_tf)): # 遍历每个样本
input_sample = training_data_xor_tf[i] # 获取输入样本
true_label = labels_xor_tf[i] # 获取真实标签
predicted_prob = predictions_tf[i][0] # 获取预测的概率 (输出层只有一个神经元)
# 将概率转换为类别 (例如,阈值为0.5)
predicted_class = 1 if predicted_prob >= 0.5 else 0 # 如果概率大于等于0.5,则为类别1,否则为类别0
print(f"输入 {
input_sample} -> 预测概率: {
predicted_prob:.4f} -> 预测类别: {
predicted_class}, 期望类别: {
int(true_label)}") # 打印详细预测结果
# 我们可以检查一下隐藏层的权重和偏置 (仅为演示)
print("\n--- 模型权重和偏置 (示例) ---") # 打印权重信息标题
for layer in model_xor.layers: # 遍历模型的每一层
layer_weights = layer.get_weights() # 获取该层的权重 (列表,通常第一个是权重矩阵,第二个是偏置向量)
if layer_weights: # 如果权重列表不为空
print(f"层名称: {
layer.name}") # 打印层名称
print(f" 权重矩阵形状: {
layer_weights[0].shape}") # 打印权重矩阵形状
# print(f" 权重矩阵:\n{layer_weights[0]}") # (可选) 打印权重矩阵
print(f" 偏置向量形状: {
layer_weights[1].shape}") # 打印偏置向量形状
# print(f" 偏置向量:\n{layer_weights[1]}") # (可选) 打印偏置向量
这个Keras示例展示了构建、编译、训练和使用MLP是多么简洁。你可以尝试调整隐藏层神经元数量、激活函数、学习率、训练轮数等超参数,观察它们对解决XOR问题的效果。通常情况下,一个具有非线性激活函数的隐藏层的MLP可以很好地学习XOR函数。
MLP是更复杂的深度神经网络(如卷积神经网络CNN、循环神经网络RNN)的基础。理解其结构、前向传播和反向传播(即使是由框架自动处理)的基本原理至关重要。
第三章:激活函数——为神经网络注入非线性
激活函数是神经网络中的一个关键组件,它决定了神经元的输出是否被激活以及如何被激活。更重要的是,非线性激活函数是深度神经网络能够学习复杂非线性映射(从而解决非线性问题)的根本原因。
3.1 为什么需要激活函数?
想象一下,如果一个多层神经网络中所有的神经元都只进行加权求和,或者都使用线性激活函数(例如 f(z) = a*z + c
,其中 a
和 c
是常数,最简单的是 f(z) = z
,即恒等激活),那么无论这个网络有多少层,它本质上仍然是一个线性模型。
证明:多层线性网络的等价性
假设我们有一个两层的网络(一个隐藏层,一个输出层),所有激活函数都是线性的 f(z) = z
。
-
隐藏层第j个神经元的输出:
a_j^(1) = z_j^(1) = Σ_i (w_ji^(1) * x_i) + b_j^(1)
用矩阵表示为:a^(1) = W^(1) * x + b^(1)
-
输出层第k个神经元的输出:
y_k = z_k^(2) = Σ_j (w_kj^(2) * a_j^(1)) + b_k^(2)
用矩阵表示为:y = W^(2) * a^(1) + b^(2)
将 a^(1)
代入 y
的表达式:
y = W^(2) * (W^(1) * x + b^(1)) + b^(2)
y = (W^(2) * W^(1)) * x + (W^(2) * b^(1) + b^(2))
令 W_new = W^(2) * W^(1)
(一个新的权重矩阵)
令 b_new = W^(2) * b^(1) + b^(2)
(一个新的偏置向量)
则 y = W_new * x + b_new
这表明,这个两层的线性网络等价于一个单层的线性网络(具有不同的权重和偏置)。无论你堆叠多少个纯线性层,最终的结果仍然是一个线性变换。这样的网络无法学习数据中复杂的非线性关系,其表示能力与单个感知机(没有非线性激活)或线性回归/逻辑回归模型相当。
因此,非线性激活函数的引入至关重要。它们使得神经网络能够:
- 学习非线性模式: 真实世界中的数据(如图像、语音、文本)通常包含高度复杂的非线性结构。非线性激活函数允许网络从这些数据中学习和逼近任意复杂的非线性函数。
- 增加模型表示能力: 通过在每一层引入非线性,网络可以将输入空间进行非线性扭曲和变换,从而在更高层次的特征空间中更容易地分离或表示数据。
- 构建深层网络: 如果没有非线性,深层网络的优势(如层次化特征学习)将无法发挥。
3.2 常用激活函数的特性与选择考量
选择合适的激活函数对于神经网络的训练速度、收敛性以及最终性能都有显著影响。理想的激活函数通常希望具备以下一些特性:
- 非线性 (Non-linearity): 这是最基本的要求,如上所述。
- 可微性 (Differentiability): 为了能够使用基于梯度的优化算法(如反向传播)来训练网络,激活函数需要在其定义域内几乎处处可微(或者至少是分段可微,并且在不可微点有次梯度)。
- 单调性 (Monotonicity): 单调的激活函数(即其导数符号保持不变)有时能保证损失函数的凸性(对于单层网络),但这在深层网络中不一定成立,也不是必需的。
- 输出范围 (Output Range):
- 有界输出: 一些激活函数(如Sigmoid, Tanh)的输出是有界的(例如0到1,或-1到1)。这有助于控制网络中激活值的范围,可能使训练更稳定,特别是在网络的较深层。
- 无界输出: 一些激活函数(如ReLU及其变体)的输出是无界的(例如0到正无穷)。这有时能提供更大的灵活性。
- 计算效率 (Computational Efficiency): 激活函数及其导数的计算应该尽可能快,因为它们在每次前向传播和反向传播中都会被大量调用。
- 梯度消失/爆炸问题 (Vanishing/Exploding Gradient Problem):
- 一些激活函数(尤其是Sigmoid和Tanh)在其饱和区域(即输入值非常大或非常小时)的导数非常接近于0。在深层网络中,这可能导致梯度信号在反向传播过程中逐层衰减,使得浅层网络的参数难以得到有效更新(梯度消失)。
- 选择能够缓解梯度消失问题的激活函数(如ReLU及其变体)对于训练深层网络非常重要。
- 零中心化 (Zero-centered Output): 如果激活函数的输出大致以0为中心(例如Tanh的输出在-1到1之间),这有时可以帮助梯度在反向传播时更有效地流动,并可能加速收敛。因为如果输入总是正的,那么权重更新的方向可能会受到限制。
- 稀疏激活 (Sparse Activation - 针对ReLU类): 某些激活函数(如ReLU)会导致一部分神经元的输出为0,这可以引入网络的稀疏性,可能有助于特征选择和减少计算量,但也可能导致“神经元死亡”问题。
3.3 常见的激活函数及其详解
3.3.1 Sigmoid (Logistic) 函数
-
公式:
σ(z) = 1 / (1 + e^(-z))
-
形状: S形曲线。
-
输出范围:
(0, 1)
-
导数:
σ'(z) = σ(z) * (1 - σ(z))
-
图形:
(Mermaid无法直接绘制函数曲线,这里用文字描述)
- 当
z -> -∞
,σ(z) -> 0
- 当
z -> +∞
,σ(z) -> 1
- 当
z = 0
,σ(z) = 0.5
- 导数在
z=0
时最大 (为0.25),在z
远离0时迅速减小到接近0。
- 当
-
历史与应用:
- Sigmoid曾是神经网络中最流行的激活函数之一,尤其是在早期的MLP和循环神经网络(RNN)中。
- 它的输出范围
(0, 1)
非常适合解释为概率,因此常用于二分类问题的输出层神经元。
-
优点:
- 输出有界: 输出值在0和1之间,可以用作概率解释,并且有助于控制网络中信号的幅度。
- 平滑可微: 在整个定义域内都是平滑可微的。
-
缺点:
- 梯度消失 (Vanishing Gradient):
- 这是Sigmoid函数最主要的问题。当输入
z
非常大或非常小时(即神经元处于饱和状态),Sigmoid函数的导数σ'(z)
会非常接近于0。 - 在深层网络中,如果许多神经元都处于饱和状态,梯度在反向传播时会逐层乘以这些接近0的导数,导致梯度信号迅速衰减,使得网络较浅层的权重几乎无法更新。这严重阻碍了深层网络的训练。
- 这是Sigmoid函数最主要的问题。当输入
- 输出非零中心 (Output Not Zero-centered):
- Sigmoid的输出恒为正 (0到1之间)。如果一个神经元的输入总是正的(因为前一层的激活输出总是正的),那么在反向传播时,该神经元权重的梯度
∂L/∂w = δ * a_prev
(其中a_prev
是前一层的正激活输出) 的符号将完全由误差项δ
的符号决定。 - 这意味着所有权重要么同时增加,要么同时减少(取决于
δ
)。这种“之字形”更新路径可能会降低梯度下降的效率。
- Sigmoid的输出恒为正 (0到1之间)。如果一个神经元的输入总是正的(因为前一层的激活输出总是正的),那么在反向传播时,该神经元权重的梯度
- 计算成本相对较高: 指数运算
e^(-z)
相对于ReLU等函数来说计算量稍大。
- 梯度消失 (Vanishing Gradient):
-
当前使用场景:
- 由于梯度消失问题,Sigmoid作为隐藏层激活函数已不常用,尤其是在非常深的网络中。
- 主要用于二分类问题的输出层,将输出转换为概率。
- 在某些特定结构的RNN变体(如LSTM的门控单元)中仍然可能使用,因为它需要0到1之间的门控信号。
代码示例:Sigmoid函数及其导数
import numpy as np # 导入NumPy库
import matplotlib.pyplot as plt # 导入Matplotlib绘图库
def sigmoid(z): # 定义Sigmoid函数
"""计算Sigmoid激活函数的值。"""
return 1 / (1 + np.exp(-z)) # Sigmoid公式
def sigmoid_derivative(z): # 定义Sigmoid函数的导数
"""计算Sigmoid激活函数的导数。"""
s = sigmoid(z) # 先计算Sigmoid(z)
return s * (1 - s) # 导数公式: σ(z) * (1 - σ(z))
# 生成输入数据 z
z_values = np.linspace(-10, 10, 200) # 在-10到10之间生成200个等间距点
# 计算Sigmoid值和导数值
sigmoid_values = sigmoid(z_values) # 计算每个z对应的Sigmoid值
sigmoid_derivatives = sigmoid_derivative(z_values) # 计算每个z对应的Sigmoid导数值
# --- 绘图 ---
plt.figure(figsize=(10, 5)) # 创建一个10x5英寸的图形窗口
# 绘制Sigmoid函数
plt.subplot(1, 2, 1) # 创建第一个子图 (1行2列中的第1个)
plt.plot(z_values, sigmoid_values, label='σ(z) = 1 / (1 + e^-z)', color='blue') # 绘制Sigmoid曲线
plt.title('Sigmoid Activation Function') # 设置子图标题
plt.xlabel('Input (z)') # 设置x轴标签
plt.ylabel('Output σ(z)') # 设置y轴标签
plt.grid(True) # 显示网格
plt.axhline(0, color='black', linewidth=0.5) # 绘制x轴
plt.axvline(0, color='black', linewidth=0.5) # 绘制y轴
plt.legend() # 显示图例
# 绘制Sigmoid函数的导数
plt.subplot(1, 2, 2) # 创建第二个子图 (1行2列中的第2个)
plt.plot(z_values, sigmoid_derivatives, label="σ'(z) = σ(z)(1-σ(z))", color='red') # 绘制Sigmoid导数曲线
plt.title('Derivative of Sigmoid Function') # 设置子图标题
plt.xlabel('Input (z)') # 设置x轴标签
plt.ylabel("Output σ'(z)") # 设置y轴标签
plt.grid(True) # 显示网格
plt.axhline(0, color='black', linewidth=0.5) # 绘制x轴
plt.axvline(0, color='black', linewidth=0.5) # 绘制y轴
plt.legend() # 显示图例
plt.tight_layout() # 自动调整子图布局,防止重叠
plt.show() # 显示图形
print(f"Sigmoid(0) = {
sigmoid(0)}") # 打印Sigmoid(0)的值
print(f"Sigmoid导数在 z=0 时: {
sigmoid_derivative(0)}") # 打印Sigmoid导数在z=0的值
print(f"Sigmoid导数在 z=5 时: {
sigmoid_derivative(5)}") # 打印Sigmoid导数在z=5的值 (接近0)
print(f"Sigmoid导数在 z=-5 时: {
sigmoid_derivative(-5)}") # 打印Sigmoid导数在z=-5的值 (接近0)
运行此代码,你会看到Sigmoid函数的S形曲线和其导数的钟形曲线(在z=0处最大,两端迅速趋于0)。
3.3.2 Tanh (双曲正切) 函数
-
公式:
tanh(z) = (e^z - e^(-z)) / (e^z + e^(-z))
也可以表示为:tanh(z) = 2 * σ(2z) - 1
(是Sigmoid函数的一个缩放和平移版本) -
形状: S形曲线,与Sigmoid类似,但关于原点对称。
-
输出范围:
(-1, 1)
-
导数:
tanh'(z) = 1 - tanh^2(z)
-
图形:
(Mermaid无法直接绘制)- 当
z -> -∞
,tanh(z) -> -1
- 当
z -> +∞
,tanh(z) -> 1
- 当
z = 0
,tanh(z) = 0
- 导数在
z=0
时最大 (为1),在z
远离0时也迅速减小到接近0。
- 当
-
历史与应用:
- Tanh也曾是隐藏层常用的激活函数。
-
优点:
- 输出零中心 (Zero-centered Output):
- Tanh的输出范围是
(-1, 1)
,均值为0。这被认为比Sigmoid的非零中心输出更好,因为它使得下一层神经元的输入更可能具有正负值,有助于避免Sigmoid中提到的梯度更新方向受限问题,可能加速收敛。
- Tanh的输出范围是
- 输出有界: 与Sigmoid类似,输出有界。
- 平滑可微: 在整个定义域内平滑可微。
- 输出零中心 (Zero-centered Output):
-
缺点:
- 梯度消失 (Vanishing Gradient):
- Tanh同样存在梯度消失问题。当输入
z
的绝对值较大时,Tanh函数也处于饱和状态,其导数接近于0。虽然其导数范围 (0到1) 比Sigmoid的导数范围 (0到0.25) 更大一些,但梯度消失问题依然显著。
- Tanh同样存在梯度消失问题。当输入
- 计算成本相对较高: 同样涉及指数运算。
- 梯度消失 (Vanishing Gradient):
-
当前使用场景:
- 由于梯度消失问题,Tanh作为隐藏层激活函数在非常深的网络中也已不常用,但其性能通常略好于Sigmoid(因为零中心输出)。
- 在某些RNN结构(如LSTM, GRU的一些变体)中,Tanh有时仍被用作状态或输出的激活。
- 如果网络的层数不太多,或者对输出范围有特定要求(例如希望特征值在-1到1之间),Tanh有时仍可考虑。
代码示例:Tanh函数及其导数
import numpy as np # 导入NumPy库
import matplotlib.pyplot as plt # 导入Matplotlib绘图库
def tanh(z): # 定义Tanh函数
"""计算Tanh激活函数的值。"""
return np.tanh(z) # NumPy内置了tanh函数
def tanh_derivative(z): # 定义Tanh函数的导数
"""计算Tanh激活函数的导数。"""
return 1 - np.tanh(z)**2 # 导数公式: 1 - tanh^2(z)
# 生成输入数据 z
z_values_tanh = np.linspace(-7, 7, 200) # 在-7到7之间生成200个点 (Tanh饱和更快)
# 计算Tanh值和导数值
tanh_values = tanh(z_values_tanh) # 计算Tanh值
tanh_derivatives = tanh_derivative(z_values_tanh) # 计算Tanh导数值
# --- 绘图 ---
plt.figure(figsize=(10, 5)) # 创建图形窗口
# 绘制Tanh函数
plt.subplot(1, 2, 1) # 创建第一个子图
plt.plot(z_values_tanh, tanh_values, label='tanh(z)', color='green') # 绘制Tanh曲线
plt.title('Tanh Activation Function') # 设置标题
plt.xlabel('Input (z)') # 设置x轴标签
plt.ylabel('Output tanh(z)') # 设置y轴标签
plt.grid(True) # 显示网格
plt.axhline(0, color='black', linewidth=0.5) # 绘制x轴
plt.axvline(0, color='black', linewidth=0.5) # 绘制y轴
plt.legend() # 显示图例
# 绘制Tanh函数的导数
plt.subplot(1, 2, 2) # 创建第二个子图
plt.plot(z_values_tanh, tanh_derivatives, label="tanh'(z) = 1 - tanh^2(z)", color='purple') # 绘制Tanh导数曲线
plt.title('Derivative of Tanh Function') # 设置标题
plt.xlabel('Input (z)') # 设置x轴标签
plt.ylabel("Output tanh'(z)") # 设置y轴标签
plt.grid(True) # 显示网格
plt.axhline(0, color='black', linewidth=0.5) # 绘制x轴
plt.axvline(0, color='black', linewidth=0.5) # 绘制y轴
plt.legend() # 显示图例
plt.tight_layout() # 自动调整布局
plt.show() # 显示图形
print(f"Tanh(0) = {
tanh(0)}") # 打印Tanh(0)的值
print(f"Tanh导数在 z=0 时: {
tanh_derivative(0)}") # 打印Tanh导数在z=0的值
print(f"Tanh导数在 z=3 时: {
tanh_derivative(3)}") # 打印Tanh导数在z=3的值 (已较小)
print(f"Tanh导数在 z=-3 时: {
tanh_derivative(-3)}") # 打印Tanh导数在z=-3的值 (已较小)
运行此代码,你会看到Tanh函数的S形曲线(关于原点对称)和其导数的钟形曲线(在z=0处为1,两端趋于0)。
3.3.3 ReLU (Rectified Linear Unit) - 修正线性单元
ReLU是目前深度学习(尤其是卷积神经网络CNN)中最常用也是最重要的激活函数之一。它的出现极大地缓解了梯度消失问题,并加速了深层网络的训练。
-
公式:
ReLU(z) = max(0, z)
也可以写为:
ReLU(z) = z
ifz > 0
ReLU(z) = 0
ifz <= 0
-
形状: 斜坡函数,在负半轴为0,在正半轴为线性。
-
输出范围:
[0, +∞)
(非负) -
导数:
ReLU'(z) = 1
ifz > 0
ReLU'(z) = 0
ifz < 0
在z = 0
处,ReLU函数在数学上是不可微的。但在实践中,通常将其在该点的次梯度 (subgradient) 设为0或1(例如,在TensorFlow/PyTorch中通常实现为当z=0
时导数为0)。这在实际训练中通常不会造成问题。 -
图形:
(Mermaid无法直接绘制)- 对于所有
z <= 0
,输出为0
。 - 对于所有
z > 0
,输出为z
(一条斜率为1的直线)。 - 导数在
z < 0
时为0
,在z > 0
时为1
。
- 对于所有
-
历史与应用:
- 虽然ReLU的概念很早就存在,但它在2010年代初由于在深度学习中的成功应用(例如AlexNet)而变得非常流行。
- 目前是大多数深度神经网络隐藏层的默认和首选激活函数。
-
优点:
- 有效缓解梯度消失问题 (Alleviates Vanishing Gradient):
- 当输入
z > 0
时,ReLU的导数恒为1。这意味着在反向传播过程中,只要神经元的输入是正的,梯度就可以无衰减地向前传播。这使得训练非常深的网络成为可能。
- 当输入
- 计算效率高 (Computationally Efficient):
- ReLU的计算非常简单(只是一个
max(0, z)
操作),远快于Sigmoid和Tanh中的指数运算。这可以显著加速网络的训练和推理。
- ReLU的计算非常简单(只是一个
- 引入稀疏性 (Induces Sparsity):
- 当输入
z <= 0
时,ReLU的输出为0。这意味着网络中的一部分神经元会被“关闭”(激活值为0),从而使得网络的激活具有稀疏性。 - 稀疏激活被认为有一些好处:
- 特征选择: 只有一部分特征被激活,可能有助于学习更有判别力的特征。
- 信息解耦 (Information Disentanglement): 不同的神经元可能对不同的输入模式做出响应。
- 减少计算量: 激活值为0的神经元在后续计算中贡献为0。
- 可能有助于防止过拟合 (类似Dropout的效果,但机制不同)。
- 当输入
- 有效缓解梯度消失问题 (Alleviates Vanishing Gradient):
-
缺点:
- 输出非零中心 (Output Not Zero-centered):
- 与Sigmoid类似,ReLU的输出总是非负的 (
>=0
)。这可能导致与Sigmoid类似的梯度更新效率问题(尽管ReLU的梯度消失缓解效果通常更重要)。
- 与Sigmoid类似,ReLU的输出总是非负的 (
- Dying ReLU (死亡ReLU) 问题:
- 如果在训练过程中,一个ReLU神经元的输入
z
总是负的(例如,由于一个过大的负偏置,或者在学习过程中权重被更新得不恰当),那么这个神经元的输出将恒为0,其梯度也将恒为0。 - 这意味着这个神经元将不再对任何输入数据产生响应,也无法通过梯度下降进行学习和更新。它就“死掉”了。
- 如果网络中有大量神经元死亡,模型的表示能力会受到很大影响。
- 原因:
- 学习率设置过高。
- 权重初始化不当。
- 大的负偏置。
- 缓解方法:
- 使用ReLU的变体,如Leaky ReLU, PReLU, ELU (稍后介绍)。
- 选择合适的学习率。
- 良好的权重初始化。
- 使用Adam等自适应学习率优化器。
- 如果在训练过程中,一个ReLU神经元的输入
- 输出非零中心 (Output Not Zero-centered):
-
当前使用场景:
- 绝大多数现代深度神经网络(尤其是CNN)的隐藏层中广泛使用。
- 通常不用于输出层(除非输出本身是非负的且无上界,例如预测物体数量,但即使这样也可能有更合适的选择)。
代码示例:ReLU函数及其导数
import numpy as np # 导入NumPy库
import matplotlib.pyplot as plt # 导入Matplotlib绘图库
def relu(z): # 定义ReLU函数
"""计算ReLU激活函数的值。"""
return np.maximum(0, z) # ReLU公式: max(0, z)
def relu_derivative(z): # 定义ReLU函数的导数 (在z=0处设为0)
"""计算ReLU激活函数的导数。在z=0处导数设为0。"""
return np.where(z > 0, 1, 0) # 如果z>0,导数为1,否则为0
# 生成输入数据 z
z_values_relu = np.linspace(-5, 5, 200) # 在-5到5之间生成200个点
# 计算ReLU值和导数值
relu_values = relu(z_values_relu) # 计算ReLU值
relu_derivatives = relu_derivative(z_values_relu) # 计算ReLU导数值
# --- 绘图 ---
plt.figure(figsize=(10, 5)) # 创建图形窗口
# 绘制ReLU函数
plt.subplot(1, 2, 1) # 创建第一个子图
plt.plot(z_values_relu, relu_values, label='ReLU(z) = max(0, z)', color='orange') # 绘制ReLU曲线
plt.title('ReLU Activation Function') # 设置标题
plt.xlabel('Input (z)') # 设置x轴标签
plt.ylabel('Output ReLU(z)') # 设置y轴标签
plt.grid(True) # 显示网格
plt.axhline(0, color='black', linewidth=0.5) # 绘制x轴
plt.axvline(0, color='black', linewidth=0.5) # 绘制y轴
plt.legend() # 显示图例
# 绘制ReLU函数的导数
plt.subplot(1, 2, 2