一、作业环境准备:导入依赖包
1. 核心依赖说明
| 库 / 模块 | 作用 |
|---|---|
numpy as np |
Python 科学计算基础库,用于数组运算、矩阵操作 |
tensorflow as tf |
主流深度学习框架,本作业用其 Keras 接口搭建神经网络 |
Sequential(tf.keras.models) |
Keras 中的序贯模型,用于线性堆叠神经网络层 |
Dense(tf.keras.layers) |
全连接层(密集层),用于构建神经网络的基础层 |
activations(tf.keras) |
激活函数集合,包含 linear/relu/sigmoid 等 |
matplotlib.pyplot as plt |
数据可视化库,用于绘制图像、模型结果 |
logging/tf.autograph |
用于屏蔽 TensorFlow 的冗余日志,简化输出 |
2. 完整导入代码
import numpy as np
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense
from tensorflow.keras.activations import linear, relu, sigmoid
%matplotlib widget
import matplotlib.pyplot as plt
plt.style.use('./deeplearning.mplstyle') # 加载课程预设的绘图样式
import logging
logging.getLogger("tensorflow").setLevel(logging.ERROR) # 屏蔽 TensorFlow 错误以外的日志
tf.autograph.set_verbosity(0) # 关闭 AutoGraph 的冗余输出
# 课程自定义工具(单元测试、绘图工具等)
from public_tests import *
from autils import *
from lab_utils_softmax import plt_softmax
np.set_printoptions(precision=2) # 设置 numpy 数组输出保留 2 位小数
二、激活函数:ReLU 详解
1. ReLU 核心概念
- 公式:\(a = \max(0, z)\)
- 含义:修正线性单元(Rectified Linear Unit),输入为负时输出 0,输入为正时直接输出本身。
- 核心特点 :
- 非线性激活:引入 "关闭" 特性(负输入输出为 0),让多层神经网络能拟合复杂非线性关系。
- 缓解梯度消失:相比 Sigmoid,ReLU 在正区间梯度恒为 1,反向传播时梯度不易消失,训练深层网络更稳定。
- 计算高效:无复杂指数运算,比 Sigmoid/Tanh 计算更快。
2. ReLU vs Sigmoid(作业场景对比)
| 特性 | ReLU | Sigmoid |
|---|---|---|
| 适用场景 | 连续特征、多分类 / 回归任务 | 二元分类、概率输出场景 |
| 输出范围 | \([0, +\infty)\) | \((0, 1)\) |
| 核心作用 | 引入非线性,让多层网络叠加有效 | 输出 "开 / 关" 类二元概率 |
| 关键优势 | 解决梯度消失、计算快 | 输出可直接解释为概率 |
三、Softmax 函数:多分类的概率输出层
1. Softmax 核心概念
- 公式:对于输入向量 z,第 j 个输出为\(a_j = \frac{e^{z_j}}{\sum_{k=0}^{N-1} e^{z_k}}\)其中 N 是输出类别数量,\(z = W \cdot x + b\) 是线性层的输出。
- 核心特性 :
- 输出所有值都在 \((0,1)\) 之间,且所有输出之和为 1,可直接解释为 "每个类别的预测概率"。
- 输入值越大,对应的输出概率越高,能放大输入间的微小差异,让模型更 "坚定" 地预测类别。
2. 手写 Softmax 实现(两种方式)
方式 1:向量化实现(推荐,高效简洁)
def my_softmax(z):
""" Softmax converts a vector of values to a probability distribution.
Args:
z (ndarray (N,)) : input data, N features
Returns:
a (ndarray (N,)) : softmax of z
"""
ez = np.exp(z) # 对每个元素取指数
a = ez / np.sum(ez) # 每个元素除以指数和,得到概率分布
return a
方式 2:循环实现(原理直观,适合理解)
def my_softmax(z):
N = len(z)
a = np.zeros(N) # 初始化输出数组
ez_sum = 0 # 初始化指数和为 0
# 第一步:先计算所有元素的指数和(分母)
for k in range(N):
ez_sum += np.exp(z[k])
# 第二步:逐个计算每个元素的 softmax 输出
for j in range(N):
a[j] = np.exp(z[j]) / ez_sum
return a
3. 验证 Softmax 正确性
# 测试输入
z = np.array([1., 2., 3., 4.])
a = my_softmax(z)
atf = tf.nn.softmax(z)
print(f"my_softmax(z): {a}")
print(f"tensorflow softmax(z): {atf}")
# 单元测试(课程提供)
test_my_softmax(my_softmax)
- 预期输出:两个 Softmax 结果完全一致,且所有值之和为 1。
四、多分类神经网络:手写数字识别任务
4.1 任务背景
- 目标:识别 0-9 共 10 个手写数字,属于多分类任务(10 选 1)。
- 应用场景:邮政编码识别、银行支票数字识别等。
4.2 数据集说明
1. 数据加载与结构
# 加载数据集(课程自定义函数)
X, y = load_data()
- 训练集规模:5000 个样本
- 输入矩阵
X:形状为(5000, 400)- 每个样本是 20×20 像素的灰度图像,展开为 400 维向量(每个像素为浮点数,表示灰度强度)。
- 每一行对应一个手写数字样本。
- 标签向量
y:形状为(5000, 1)- 每个元素是 0-9 的整数,对应图像的数字标签(如
y=4表示图像是数字 4)。
- 每个元素是 0-9 的整数,对应图像的数字标签(如
2. 数据维度与内容查看
# 查看单个元素
print('The first element of X is: ', X[0])
print('The first element of y is: ', y[0,0])
print('The last element of y is: ', y[-1,0])
# 查看数据形状
print('The shape of X is: ' + str(X.shape))
print('The shape of y is: ' + str(y.shape))
- 预期输出:
X.shape = (5000, 400),y.shape = (5000, 1)y[0,0] = 0,y[-1,0] = 9
3. 数据可视化(查看手写数字样本)
import warnings
warnings.simplefilter(action='ignore', category=FutureWarning)
m, n = X.shape # m=样本数 5000,n=特征数 400
# 创建 8×8=64 张图像的子图
fig, axes = plt.subplots(8, 8, figsize=(5,5))
fig.tight_layout(pad=0.13, rect=[0, 0.03, 1, 0.91]) # 调整布局,给标题留空间
for i,ax in enumerate(axes.flat):
# 随机选择一个样本
random_index = np.random.randint(m)
# 将 400 维向量还原为 20×20 图像,并转置(课程数据的格式要求)
X_random_reshaped = X[random_index].reshape((20,20)).T
# 显示灰度图像
ax.imshow(X_random_reshaped, cmap='gray')
# 显示标签
ax.set_title(y[random_index,0])
ax.set_axis_off() # 隐藏坐标轴
fig.suptitle("Label, image", fontsize=14)
- 效果:随机展示 64 个手写数字样本,每个图像上方标注对应的数字标签。
4.3 模型结构设计
作业中使用的是三层全连接神经网络,结构如下:
| 层 | 单元数 | 激活函数 | 输入维度 | 输出维度 | 参数维度(W, b) |
|---|---|---|---|---|---|
| 输入层 | 400 | - | - | 400 | - |
| 第 1 层(L1) | 25 | ReLU | 400 | 25 | W1: (400,25), b1: (25,) |
| 第 2 层(L2) | 15 | ReLU | 25 | 15 | W2: (25,15), b2: (15,) |
| 第 3 层(L3) | 10 | Linear(无激活) | 15 | 10 | W3: (15,10), b3: (10,) |
- 关键说明:
- 前两层用 ReLU 激活引入非线性,让模型能拟合复杂模式。
- 输出层用
linear激活,不直接加 Softmax,而是在损失函数中处理 Softmax,提升数值稳定性。
4.4 Keras 模型实现(Sequential 方式)
完整代码
tf.random.set_seed(1234) # 设置随机种子,保证结果可复现
model = Sequential(
[
### START CODE HERE ###
# 输入层:指定输入维度为 400(可选,Keras 会自动推断)
tf.keras.layers.InputLayer((400,)),
# 第1层:25个单元,ReLU 激活
tf.keras.layers.Dense(25, activation="relu", name="L1"),
# 第2层:15个单元,ReLU 激活
tf.keras.layers.Dense(15, activation="relu", name="L2"),
# 第3层:10个单元,线性激活(输出 logits)
tf.keras.layers.Dense(10, activation="linear", name="L3")
### END CODE HERE ###
], name = "my_model"
)
# 模型编译:损失函数用 SparseCategoricalCrossentropy,from_logits=True 表示输出是 logits
model.compile(
loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)
)
关键细节:from_logits=True
- 含义:告诉损失函数,模型输出的是未经过 Softmax 的 logits,损失函数会自动计算 Softmax 再计算交叉熵。
- 优势:
- 数值更稳定:Softmax + 交叉熵合并计算,避免中间指数运算导致的数值溢出。
- 训练更高效:TensorFlow 内部优化了合并计算的梯度。
- 注意:此时模型的输出不是概率,而是 logits;如果需要概率,需要手动调用
tf.nn.softmax(model.predict(x))。
学习笔记补充:关键易错点
- Softmax 实现的数值稳定性 :当输入
z很大时,np.exp(z)可能溢出,实际工程中会先减去z的最大值(z = z - np.max(z)),避免指数溢出。 - 数据维度匹配 :模型输入
X的形状必须和网络输入层维度一致(400),否则会报错。 - 多分类标签格式 :这里的标签是 0-9 的整数,所以用
SparseCategoricalCrossentropy;如果标签是 one-hot 编码(如[1,0,0,...]),则用CategoricalCrossentropy。 - ReLU 的 "死亡 ReLU" 问题:如果学习率太高,部分神经元可能永远输出 0,梯度消失;可以通过合理设置学习率缓解。
二.深度学习多类别分类可选实验笔记
一、实验目标与背景
1. 核心目标
探索使用神经网络解决多类别分类问题,理解多分类与二分类的区别,以及 Softmax、ReLU 在多分类场景下的作用。
2. 多分类 vs 二分类
| 维度 | 二分类 | 多分类 |
|---|---|---|
| 任务目标 | 区分 "是 / 否" 两类(如 "猫 / 非猫") | 从多个类别中选一个(如 "猫 / 狗 / 马 / 其他") |
| 输出层 | 1 个单元,Sigmoid 激活输出概率 | 多个单元,线性激活 + 损失函数内 Softmax |
| 决策边界 | 单条直线 / 曲线,划分两类 | 多条决策边界,划分多个类别区域 |
| 典型损失函数 | BinaryCrossentropy |
SparseCategoricalCrossentropy |
二、实验工具与依赖导入
| 库 / 模块 | 作用 |
|---|---|
numpy as np |
科学计算,处理数组与矩阵 |
matplotlib.pyplot as plt |
数据可视化,绘制分类边界、决策图 |
sklearn.datasets.make_blobs |
生成人工多分类数据集 |
tensorflow as tf |
搭建和训练多分类神经网络 |
Sequential/Dense(tf.keras) |
构建序贯模型和全连接层 |
lab_utils_multiclass_TF |
课程自定义工具,包含多分类绘图函数 |
import numpy as np
import matplotlib.pyplot as plt
%matplotlib widget
from sklearn.datasets import make_blobs
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense
np.set_printoptions(precision=2) # 输出保留2位小数
from lab_utils_multiclass_TF import * # 导入课程绘图工具
import logging
logging.getLogger("tensorflow").setLevel(logging.ERROR) # 屏蔽冗余日志
tf.autograph.set_verbosity(0) # 关闭AutoGraph输出
三、数据集准备与可视化
1. 生成多分类数据集
使用 make_blobs 生成 4 个类别的人工数据集,模拟多分类场景:
# 生成4类数据集
classes = 4 # 类别数
m = 100 # 样本数
centers = [[-5, 2], [-2, -2], [1, 2], [5, -2]] # 每个类别的中心
std = 1.0 # 数据标准差(分散程度)
X_train, y_train = make_blobs(
n_samples=m,
centers=centers,
cluster_std=std,
random_state=30
)
2. 数据信息查看
# 查看类别、标签表示和数据维度
print(f"unique classes {np.unique(y_train)}")
print(f"class representation {y_train[:10]}")
print(f"shape of X_train: {X_train.shape}, shape of y_train: {y_train.shape}")
- 预期输出:
unique classes [0 1 2 3]:4 个类别shape of X_train: (100, 2), shape of y_train: (100,):100 个样本,每个样本 2 个特征y_train是 0-3 的整数标签,对应 4 个类别
3. 数据可视化
plt_mc(X_train, y_train, classes, centers, std=std)
- 效果:不同颜色代表 4 个类别,每个样本点的坐标为两个输入特征,直观展示数据分布。
四、多分类神经网络模型实现
1. 模型结构设计
本实验使用两层全连接网络,结构如下:
| 层 | 单元数 | 激活函数 | 作用 |
|---|---|---|---|
| 输入层 | 2 | - | 输入两个特征 x0, x1 |
| 第 1 层(L1) | 2 | ReLU | 引入非线性,学习特征变换 |
| 第 2 层(L2) | 4 | Linear | 输出 4 个类别的 logits(未归一化分数) |
2. Keras 模型构建与编译
tf.random.set_seed(1234) # 设置随机种子,保证结果可复现
# 构建序贯模型
model = Sequential(
[
Dense(2, activation='relu', name="L1"),
Dense(4, activation='linear', name="L2")
]
)
# 编译模型
model.compile(
loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
optimizer=tf.keras.optimizers.Adam(0.01)
)
# 训练模型
model.fit(
X_train, y_train,
epochs=200
)
3. 关键细节解读
(1)from_logits=True 的作用
- 含义:告诉损失函数,模型输出的是未经过 Softmax 的 logits,损失函数会自动计算 Softmax + 交叉熵。
- 优势:避免手动 Softmax 带来的数值溢出,提升训练稳定性;同时 TensorFlow 会优化合并计算的梯度,训练更高效。
- 注意:模型直接输出的不是概率,而是每个类别的分数;如果需要概率,需调用
tf.nn.softmax(model.predict(X_train))。
(2)SparseCategoricalCrossentropy 损失函数
- 适用场景:标签是 ** 整数形式(0-3)** 的多分类任务。
- 对比:如果标签是 one-hot 编码(如
[1,0,0,0]),则使用CategoricalCrossentropy。
五、模型训练结果与底层原理解析
1. 模型分类边界可视化
plt_cat_mc(X_train, y_train, model, classes)
- 效果:展示模型学习到的决策边界,将输入空间划分为 4 个区域,每个区域对应一个类别。
2. 第 1 层(ReLU 层)的工作原理
(1)提取层权重
# 获取第一层的权重和偏置
l1 = model.get_layer("L1")
W1, b1 = l1.get_weights()
# 绘制第一层单元的功能图
plt_layer_relu(X_train, y_train.reshape(-1,), W1, b1, classes)
(2)单元功能解读
- 单元 0:将类别 0/1 与 2/3 分开。线左侧的点(类别 0、1)输出为 0,线右侧的点输出大于 0。
- 单元 1:将类别 0/2 与 1/3 分开。线上方的点(类别 0、2)输出为 0,线下方的点输出大于 0。
- 本质:ReLU 层的每个单元都在学习一条 "分界线",将输入空间分为两部分,为后续层构建新的特征表示。
3. 第 2 层(输出层)的工作原理
(1)提取层权重并生成新特征
# 获取第二层的权重和偏置
l2 = model.get_layer("L2")
W2, b2 = l2.get_weights()
# 第一层的输出(新特征)
X12 = np.maximum(0, np.dot(X_train, W1) + b1)
# 绘制输出层的决策功能
plt_output_layer_linear(
X12, y_train.reshape(-1,), W2, b2, classes,
x0_rng=(-0.25, np.amax(X12[:, 0])),
x1_rng=(-0.25, np.amax(X12[:, 1]))
)
(2)输出层单元功能解读
第一层的输出 X12 是新的特征(2 维),第二层基于这些特征学习分类规则:
- 单元 0 :在
(0,0)附近输出最大值,对应类别 0(蓝色样本)。 - 单元 1:在左上角区域输出最大值,对应类别 1(绿色样本)。
- 单元 2:在右下角区域输出最大值,对应类别 2(橙色样本)。
- 单元 3:在右上角区域输出最大值,对应类别 3(紫色样本)。
(3)Softmax 的协调作用
- 每个单元不仅要为自己的类别输出最大值,还要保证该值是所有单元中最高的。
- 这一过程由损失函数中的 Softmax 实现:Softmax 会将所有输出转换为概率分布,让正确类别的概率最大化,同时抑制其他类别的概率。
六、核心知识点总结
1. 多分类神经网络的关键要点
- 输出层设计:使用线性激活 + 损失函数内 Softmax,比直接在输出层加 Softmax 更稳定。
- ReLU 层的作用:通过多条 "分界线" 学习非线性特征变换,将输入空间映射到更易分类的新特征空间。
- 损失函数选择 :标签为整数时用
SparseCategoricalCrossentropy,标签为 one-hot 编码时用CategoricalCrossentropy。 - 模型预测 :模型直接输出 logits,取
argmax即可得到预测类别;如需概率,需额外调用 Softmax。
2. 底层原理:层与层的配合
- 第一层(ReLU):学习 "分而治之" 的线性分界线,将复杂多分类问题拆解为多个二分类子问题。
- 第二层(线性):基于第一层生成的新特征,学习每个类别的专属区域,最终通过 Softmax 统一为概率分布。
三.ReLU 激活
一、实验工具与依赖导入
| 库 / 模块 | 作用 |
|---|---|
numpy as np |
科学计算,处理数组与矩阵运算 |
matplotlib.pyplot as plt |
数据可视化,绘制激活函数图像、分段线性函数 |
GridSpec(matplotlib.gridspec) |
自定义绘图布局,用于复杂可视化 |
tensorflow as tf |
搭建和训练包含 ReLU 层的神经网络 |
Sequential/Dense/LeakyReLU(tf.keras) |
构建序贯模型、全连接层和 ReLU 变体 |
activations(tf.keras) |
包含 linear/relu/sigmoid 等激活函数 |
Slider(matplotlib.widgets) |
交互式滑块,用于调整模型权重 / 偏置并实时观察效果 |
| 课程自定义工具 | lab_utils_relu.py/autils.py,提供实验专用绘图函数 |
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.gridspec import GridSpec
plt.style.use('./deeplearning.mplstyle') # 课程预设绘图样式
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, LeakyReLU
from tensorflow.keras.activations import linear, relu, sigmoid
%matplotlib widget # 开启交互式绘图
from matplotlib.widgets import Slider
from lab_utils_common import dlc
from autils import plt_act_trio
from lab_utils_relu import *
import warnings
warnings.simplefilter(action='ignore', category=UserWarning) # 屏蔽用户警告
二、ReLU 激活函数基础
1. 核心定义与公式
- 全称:修正线性单元(Rectified Linear Unit)
- 公式:\(a = \max(0, z)\)
- 含义 :
- 当输入 \(z \ge 0\) 时,输出等于输入本身(线性关系);
- 当输入 \(z < 0\) 时,输出为 0("关闭" 状态)。
2. ReLU vs Sigmoid 对比
| 特性 | ReLU | Sigmoid |
|---|---|---|
| 输出范围 | \([0, +\infty)\) | \((0, 1)\) |
| 适用场景 | 连续特征、回归 / 多分类任务 | 二元分类、概率输出场景 |
| 核心优势 | 缓解梯度消失、计算高效、引入非线性 | 输出可直接解释为概率 |
| 关键特点 | 存在 "关闭" 状态,能实现分段线性拟合 | 全程非线性,梯度易消失 |
3. 实验中的应用场景:需求预测
在课程示例中,ReLU 用于处理具有连续值范围的 "感知特征"(如价格、运费、营销等),而 Sigmoid 仅适合开 / 关类二元场景。ReLU 的线性部分可以建模连续特征的线性关系,"关闭" 状态则能引入非线性,让模型拟合复杂的需求变化模式。
三、为什么需要非线性激活函数?
1. 核心问题:纯线性网络的局限性
如果神经网络中所有层都使用线性激活,那么无论叠加多少层,整个网络都等价于一个单一的线性模型,无法拟合复杂的非线性关系(如分段线性、曲线等)。
2. ReLU 如何实现非线性?
ReLU 的 "关闭" 特性让它能将多个线性函数拼接成分段线性函数,从而近似任意非线性模式:
- 每个 ReLU 单元可以看作一个 "开关",在特定输入区间内开启(输出线性),其他区间关闭(输出 0);
- 多个 ReLU 单元的输出叠加,就能组合出斜率变化的分段线性曲线,近似复杂的非线性目标函数。
四、实验:用 ReLU 拟合分段线性函数
1. 实验模型结构
本实验使用一个简单的两层网络,拟合一个三段式分段线性目标函数:
- 输入层:1 维输入 x
- 隐藏层:3 个 ReLU 单元(单元 0 固定,单元 1、2 可调整权重 / 偏置)
- 输出层:线性激活,将隐藏层 3 个单元的输出直接相加
2. 单元分工与拟合原理
| 单元 | 作用 | 拟合逻辑 |
|---|---|---|
| 单元 0 | 拟合第一个线性片段 | 固定权重 / 偏置,在 \(x \in [0,1]\) 区间内输出线性值,\(x>1\) 时 ReLU 截断为 0,不影响后续片段 |
| 单元 1 | 拟合第二个线性片段 | 调整权重(斜率)和偏置,让 \(x<1\) 时输出为负(ReLU 关闭,无贡献),\(x \ge 1\) 时开启,输出对应斜率的线性值 |
| 单元 2 | 拟合第三个线性片段 | 调整权重(斜率)和偏置,让 \(x<2\) 时输出为负(ReLU 关闭,无贡献),\(x \ge 2\) 时开启,输出对应斜率的线性值 |
3. 关键公式与逻辑
隐藏层单元的输出:
\(\begin{align*} a_0^{[1]} &= \text{relu}(x \cdot w_0^{[1]} + b_0^{[1]}) \\ a_1^{[1]} &= \text{relu}(x \cdot w_1^{[1]} + b_1^{[1]}) \\ a_2^{[1]} &= \text{relu}(x \cdot w_2^{[1]} + b_2^{[1]}) \end{align*}\)
输出层的最终结果:
\(a_0^{[2]} = a_0^{[1]} + a_1^{[1]} + a_2^{[1]}\)
4. 交互式实验代码与步骤
# 启动交互式实验,调整权重/偏置拟合目标函数
_ = plt_relu_ex()
- 操作提示:
- 先将单元 1、2 的权重和偏置设为 0,观察单元 0 单独拟合的第一段;
- 调整单元 1 的权重和偏置,让其在 \(x \ge 1\) 时开启,拟合第二段;
- 调整单元 2 的权重和偏置,让其在 \(x \ge 2\) 时开启,拟合第三段;
- 观察三个单元输出叠加后,是否与目标分段线性函数完全重合。
五、核心知识点总结
1. ReLU 激活函数的关键特性
- 分段线性本质:通过 "开启 / 关闭" 状态,将多个线性函数拼接成复杂的分段线性曲线,近似任意非线性模式;
- 计算高效:无复杂指数运算,比 Sigmoid/Tanh 计算更快;
- 缓解梯度消失:正区间梯度恒为 1,反向传播时梯度不易消失,训练深层网络更稳定;
- "死亡 ReLU" 问题:如果学习率过高,部分神经元可能永远处于关闭状态(输出 0),梯度消失;可通过合理设置学习率、使用 LeakyReLU 缓解。
2. 为什么 ReLU 是深度学习的主流激活函数?
- 解决了 Sigmoid 等传统激活函数的梯度消失问题;
- 计算成本低,训练速度快;
- 能有效引入非线性,让多层神经网络拟合复杂模式;
- 适配深度学习框架的优化实现,工业界应用广泛。
四.Softmax 功能
一、实验工具与依赖导入
| 库 / 模块 | 作用 |
|---|---|
numpy as np |
科学计算,实现 Softmax 函数、处理数组运算 |
matplotlib.pyplot as plt |
数据可视化,绘制 Softmax 输出、概率分布 |
tensorflow as tf |
搭建多分类神经网络,对比两种 Softmax 实现方式 |
Sequential/Dense(tf.keras) |
构建序贯模型和全连接层 |
make_blobs(sklearn.datasets) |
生成多分类人工数据集 |
| 课程自定义工具 | lab_utils_softmax.py/lab_utils_common.py,提供 Softmax 专用绘图函数 |
import numpy as np
import matplotlib.pyplot as plt
plt.style.use('./deeplearning.mplstyle') # 课程预设绘图样式
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense
from IPython.display import display, Markdown, Latex
from sklearn.datasets import make_blobs
%matplotlib widget
from matplotlib.widgets import Slider
from lab_utils_common import dlc
from lab_utils_softmax import plt_softmax
import logging
logging.getLogger("tensorflow").setLevel(logging.ERROR) # 屏蔽冗余日志
tf.autograph.set_verbosity(0) # 关闭AutoGraph输出
二、Softmax 函数基础
1. 核心定义与公式
- 作用 :将线性层输出的向量
z转换为概率分布,每个输出值在 0-1 之间,且所有输出之和为 1,可直接解释为 "输入属于每个类别的概率"。 - 公式:\(a_j = \frac{e^{z_j}}{\sum_{k=1}^{N} e^{z_k}}\)其中 N 是类别数量,\(z_j\) 是第 j 个类别的线性输出(logit)。
2. 两种应用场景
| 场景 | 结构 | 输入 | 输出 |
|---|---|---|---|
| Softmax 回归 | 单层线性层 + Softmax | 原始输入特征 x | 每个类别的概率 |
| 带 Softmax 输出的神经网络 | 多层隐藏层 + 线性输出层 + Softmax | 隐藏层输出的特征向量 | 每个类别的概率 |
3. 关键特性
- 放大差异:指数运算会放大输入向量中数值的微小差异,让模型更 "坚定" 地预测类别;
- 全局影响:Softmax 作用于所有输出单元,修改任意一个输入值都会改变所有输出概率;
- 归一化特性:所有输出之和恒为 1,符合概率分布的定义。
三、Softmax 函数的 NumPy 实现
1. 基础实现代码
def my_softmax(z):
# 对每个元素取指数
ez = np.exp(z)
# 每个元素除以指数和,得到概率分布
sm = ez / np.sum(ez)
return sm
2. 交互式验证
# 关闭之前的绘图窗口
plt.close("all")
# 启动交互式实验,调整输入向量 z 观察 Softmax 输出变化
plt_softmax(my_softmax)
- 操作提示:通过滑块修改输入向量的数值,观察输出概率的变化,验证 "输入越大,对应输出概率越高" 的特性,以及所有输出之和为 1 的归一化特性。
四、Softmax 损失函数:交叉熵损失
1. 损失函数定义
-
作用:衡量 Softmax 输出的概率分布与真实标签的差异,作为模型训练的优化目标。
-
公式:单个样本的损失:\(L(a, y) = -\log(a_y)\)其中 y 是真实类别,\(a_y\) 是 Softmax 输出中对应类别的概率。
批量样本的成本函数:\(J(\mathbf{w}, \mathbf{b}) = -\frac{1}{m} \left[ \sum_{i=1}^{m} \sum_{j=1}^{N} \mathbf{1}\{y^{(i)} == j\} \log \frac{e^{z_j^{(i)}}}{\sum_{k=1}^{N} e^{z_k^{(i)}}} \right]\)其中 \(\mathbf{1}\{y^{(i)} == j\}\) 是指示函数,当样本 i 的标签为 j 时取 1,否则取 0。
2. 与二分类交叉熵的对比
| 特性 | 二分类交叉熵 | 多分类交叉熵(Softmax) |
|---|---|---|
| 适用场景 | 二元分类任务 | 多分类任务 |
| 输出层激活 | Sigmoid | Softmax(或线性 + 损失内 Softmax) |
| 标签格式 | 0/1 或 one-hot 编码 | 整数索引或 one-hot 编码 |
| 损失计算 | 同时计算正负类别的损失 | 仅计算真实类别对应的损失 |
五、TensorFlow 中 Softmax 的两种实现方式
1. 数据集准备
# 生成4类人工数据集
centers = [[-5, 2], [-2, -2], [1, 2], [5, -2]]
X_train, y_train = make_blobs(
n_samples=2000,
centers=centers,
cluster_std=1.0,
random_state=30
)
2. 方式一:输出层直接加 Softmax(不推荐)
模型代码
model = Sequential([
Dense(25, activation='relu'),
Dense(15, activation='relu'),
Dense(4, activation='softmax') # Softmax 直接放在输出层
])
model.compile(
loss=tf.keras.losses.SparseCategoricalCrossentropy(),
optimizer=tf.keras.optimizers.Adam(0.001)
)
model.fit(X_train, y_train, epochs=10)
特点与问题
- 优点:模型直接输出概率向量,使用直观;
- 缺点:数值稳定性差,指数运算容易出现溢出 / 下溢,导致训练不稳定。
输出验证
p_nonpreferred = model.predict(X_train)
print(p_nonpreferred[:2])
print("largest value", np.max(p_nonpreferred), "smallest value", np.min(p_nonpreferred))
- 预期输出:所有值在 0-1 之间,且每行之和为 1。
3. 方式二:输出层线性激活 + 损失函数内 Softmax(推荐)
模型代码
preferred_model = Sequential([
Dense(25, activation='relu'),
Dense(15, activation='relu'),
Dense(4, activation='linear') # 输出层用线性激活,不直接加 Softmax
])
preferred_model.compile(
loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True), # from_logits=True
optimizer=tf.keras.optimizers.Adam(0.001)
)
preferred_model.fit(X_train, y_train, epochs=10)
关键参数 from_logits=True
- 含义:告诉损失函数,模型输出的是未经过 Softmax 的 logits,损失函数会自动计算 Softmax + 交叉熵;
- 优势:
- 数值更稳定:合并 Softmax 和交叉熵计算,避免中间指数运算的溢出;
- 训练更高效:TensorFlow 内部优化了梯度计算;
- 输出灵活:模型直接输出 logits,如需概率可手动调用
tf.nn.softmax(),直接取argmax也能得到预测类别。
输出验证
# 模型直接输出 logits(非概率)
p_preferred = preferred_model.predict(X_train)
print(f"two example output vectors:\n {p_preferred[:2]}")
print("largest value", np.max(p_preferred), "smallest value", np.min(p_preferred))
# 手动转换为概率
sm_preferred = tf.nn.softmax(p_preferred).numpy()
print(f"two example output vectors:\n {sm_preferred[:2]}")
print("largest value", np.max(sm_preferred), "smallest value", np.min(sm_preferred))
# 直接取 argmax 得到预测类别(无需转换为概率)
for i in range(5):
print(f"{p_preferred[i]}, category: {np.argmax(p_preferred[i])}")
六、两种交叉熵损失的区别
| 损失函数 | 标签格式 | 适用场景 |
|---|---|---|
SparseCategoricalCrossentropy |
整数索引(如 0/1/2/3) | 标签为单个整数的多分类任务,无需 one-hot 编码 |
CategoricalCrossentropy |
one-hot 编码(如 [1,0,0,0]) |
标签为独热向量的多分类任务 |
七、核心知识点总结
1. Softmax 函数的关键要点
- 作用:将 logits 转换为概率分布,用于多分类任务;
- 实现:NumPy 可通过
np.exp(z)/np.sum(np.exp(z))实现; - 最佳实践:在 TensorFlow 中优先使用
from_logits=True的方式,数值更稳定。
2. 多分类任务的 TensorFlow 实现流程
- 数据准备:生成 / 加载多分类数据集,标签为整数索引;
- 模型构建:隐藏层用 ReLU 激活,输出层用线性激活;
- 模型编译:使用
SparseCategoricalCrossentropy(from_logits=True)作为损失函数; - 模型训练:调用
model.fit()训练; - 模型预测:直接用
np.argmax(model.predict(x))得到预测类别,如需概率可手动调用tf.nn.softmax()。