函数与导数
导数描述了函数在某一点变化的速率,是梯度下降的数学源头。深度学习的训练循环就是:前向传播算 loss → 反向传播算每个参数的导数 → 沿导数反方向更新参数。Sigmoid σ'(x)=σ(x)(1-σ(x)) 用于二分类输出层与 LSTM 门控;ReLU 分段导数解决了 Sigmoid 的梯度消失问题,成为 CNN/MLP 默认激活函数;Softmax + 交叉熵的复合导数简化为 ŷ-y,是 GPT、BERT 等分类头的核心公式;Tanh 导数用于 RNN/LSTM 内部状态更新。数值梯度检查 (f(x+ε)-f(x-ε))/2ε 是调试自定义层的标准手段。本节从高中导数公式出发,逐一讲解 AI 中高频出现的每个激活函数及损失函数的导数推导。
一、导数的精确定义
f'(x) = lim[Δx→0] (f(x+Δx) - f(x)) / Δx
直觉理解:
导数 = 切线斜率 = 函数在该点的瞬时变化率
f'(x) > 0 → 函数在增大(向右上方走)
f'(x) < 0 → 函数在减小(向右下方走)
f'(x) = 0 → 函数到达极值点(山顶或谷底)
数值近似(深度学习框架验证梯度时用到):
f'(x) ≈ (f(x+ε) - f(x-ε)) / (2ε),其中 ε 取很小的值如 10⁻⁷
直觉理解:
导数 = 切线斜率 = 函数在该点的瞬时变化率
f'(x) > 0 → 函数在增大(向右上方走)
f'(x) < 0 → 函数在减小(向右下方走)
f'(x) = 0 → 函数到达极值点(山顶或谷底)
数值近似(深度学习框架验证梯度时用到):
f'(x) ≈ (f(x+ε) - f(x-ε)) / (2ε),其中 ε 取很小的值如 10⁻⁷
二、基本求导公式大全
| 函数 f(x) | 导数 f'(x) | 在 AI 中的角色 | 数值示例 (x=2) |
|---|---|---|---|
| c(常数) | 0 | 偏置的常数项 | f=5, f'=0 |
| x | 1 | 恒等连接 | f=2, f'=1 |
| xⁿ | nxⁿ⁻¹ | 多项式特征 | f=x³=8, f'=3×4=12 |
| eˣ | eˣ | Softmax 分母 | f=e²≈7.389, f'≈7.389 |
| ln(x) | 1/x | 交叉熵损失 | f=ln2≈0.693, f'=0.5 |
| sin(x) | cos(x) | 位置编码 | f=sin2≈0.909, f'=cos2≈-0.416 |
| 1/x | -1/x² | 学习率调度 | f=0.5, f'=-0.25 |
| √x | 1/(2√x) | BatchNorm 标准差 | f=√2≈1.414, f'≈0.354 |
运算法则:
加法:(f+g)' = f' + g'
数乘:(kf)' = kf'
乘法:(fg)' = f'g + fg' (莱布尼兹法则)
除法:(f/g)' = (f'g - fg') / g²
复合:(f(g(x)))' = f'(g(x)) × g'(x) (链式法则)
加法:(f+g)' = f' + g'
数乘:(kf)' = kf'
乘法:(fg)' = f'g + fg' (莱布尼兹法则)
除法:(f/g)' = (f'g - fg') / g²
复合:(f(g(x)))' = f'(g(x)) × g'(x) (链式法则)
三、AI 中最重要的函数:激活函数
3.1 Sigmoid 函数
σ(x) = 1 / (1 + e⁻ˣ)
σ'(x) = σ(x)(1 - σ(x))
值域:(0, 1),把任意实数压缩到 0~1 之间,可以解释为概率
特殊值:σ(0) = 0.5,σ(很大) ≈ 1,σ(很小) ≈ 0
σ'(x) = σ(x)(1 - σ(x))
值域:(0, 1),把任意实数压缩到 0~1 之间,可以解释为概率
特殊值:σ(0) = 0.5,σ(很大) ≈ 1,σ(很小) ≈ 0
逐步数值计算:
求 σ(x) 和 σ'(x) 在不同 x 值的表:
关键观察:
导数在 x=0 时最大(0.25),|x| 越大导数越接近 0。
这就是"梯度消失"问题:当 σ(x) 接近 0 或 1 时(饱和区),导数几乎为 0,梯度传不回去。
| x | e⁻ˣ | 1+e⁻ˣ | σ(x) | 1-σ(x) | σ'(x)=σ(1-σ) |
|---|---|---|---|---|---|
| -5 | 148.41 | 149.41 | 0.007 | 0.993 | 0.007 |
| -2 | 7.389 | 8.389 | 0.119 | 0.881 | 0.105 |
| -1 | 2.718 | 3.718 | 0.269 | 0.731 | 0.197 |
| 0 | 1.000 | 2.000 | 0.500 | 0.500 | 0.250 |
| 1 | 0.368 | 1.368 | 0.731 | 0.269 | 0.197 |
| 2 | 0.135 | 1.135 | 0.881 | 0.119 | 0.105 |
| 5 | 0.007 | 1.007 | 0.993 | 0.007 | 0.007 |
导数在 x=0 时最大(0.25),|x| 越大导数越接近 0。
这就是"梯度消失"问题:当 σ(x) 接近 0 或 1 时(饱和区),导数几乎为 0,梯度传不回去。
AI 应用——二分类逻辑回归(垃圾邮件检测):
特征向量 x = [10, 0.5, 3](分别代表:包含"免费"次数、发送时间异常度、链接数量)
权重 w = [0.8, 1.2, 0.6],偏置 b = -5
z = wᵀx + b = 0.8×10 + 1.2×0.5 + 0.6×3 + (-5) = 8 + 0.6 + 1.8 - 5 = 5.4
P(垃圾邮件) = σ(5.4) = 1/(1+e⁻⁵·⁴) = 1/(1+0.0045) = 1/1.0045 = 0.9955
模型判断这封邮件有 99.55% 的概率是垃圾邮件。
"免费"出现 10 次(权重 0.8,贡献 8.0)是最大影响因素。
如果真实标签 y = 1(确实是垃圾邮件):
交叉熵损失 L = -ln(0.9955) = 0.0045(很小,说明预测很准确)
如果真实标签 y = 0(不是垃圾邮件,但模型预测错了):
L = -ln(1-0.9955) = -ln(0.0045) = 5.40(很大,给模型强烈的惩罚信号)
特征向量 x = [10, 0.5, 3](分别代表:包含"免费"次数、发送时间异常度、链接数量)
权重 w = [0.8, 1.2, 0.6],偏置 b = -5
z = wᵀx + b = 0.8×10 + 1.2×0.5 + 0.6×3 + (-5) = 8 + 0.6 + 1.8 - 5 = 5.4
P(垃圾邮件) = σ(5.4) = 1/(1+e⁻⁵·⁴) = 1/(1+0.0045) = 1/1.0045 = 0.9955
模型判断这封邮件有 99.55% 的概率是垃圾邮件。
"免费"出现 10 次(权重 0.8,贡献 8.0)是最大影响因素。
如果真实标签 y = 1(确实是垃圾邮件):
交叉熵损失 L = -ln(0.9955) = 0.0045(很小,说明预测很准确)
如果真实标签 y = 0(不是垃圾邮件,但模型预测错了):
L = -ln(1-0.9955) = -ln(0.0045) = 5.40(很大,给模型强烈的惩罚信号)
3.2 ReLU 函数
ReLU(x) = max(0, x) = { x, 如果x>0; 0, 如果x≤0 }
ReLU'(x) = { 1, 如果x>0; 0, 如果x<0; 未定义, 如果x=0 }
(实践中 x=0 时取 0)
ReLU'(x) = { 1, 如果x>0; 0, 如果x<0; 未定义, 如果x=0 }
(实践中 x=0 时取 0)
| x | ReLU(x) | ReLU'(x) |
|---|---|---|
| -3.0 | 0 | 0(梯度为0,"死亡"状态) |
| -0.5 | 0 | 0 |
| 0 | 0 | 0(边界,取0) |
| 0.5 | 0.5 | 1(梯度完整保留) |
| 2.0 | 2.0 | 1 |
| 10.0 | 10.0 | 1 |
① 正半区导数恒为 1,不会梯度消失
② 计算极快(只需比较大小)
③ 产生稀疏激活(约 50% 的神经元输出 0),节省计算
缺点——Dead ReLU 问题:
如果某神经元的输入 z 恒为负(由于权重初始化不好),
ReLU 恒输出 0,导数恒为 0,该神经元永远无法更新——"死了"。
AI 应用——隐藏层激活过程:
全连接层输出 z = [-1.2, 0.8, -0.3, 2.1, 0.0, -0.5, 1.5, 0.3](8 个神经元)
经过 ReLU:a = [0, 0.8, 0, 2.1, 0, 0, 1.5, 0.3]
8 个中有 4 个被"关闭"(输出 0),4 个被"激活"(保持原值)。
稀疏激活:只有 50% 的神经元在工作,节省了后续层的计算量和存储。
非线性:如果没有 ReLU,多层线性变换等价于一个线性变换(Wx),无法拟合复杂的非线性关系。
全连接层输出 z = [-1.2, 0.8, -0.3, 2.1, 0.0, -0.5, 1.5, 0.3](8 个神经元)
经过 ReLU:a = [0, 0.8, 0, 2.1, 0, 0, 1.5, 0.3]
8 个中有 4 个被"关闭"(输出 0),4 个被"激活"(保持原值)。
稀疏激活:只有 50% 的神经元在工作,节省了后续层的计算量和存储。
非线性:如果没有 ReLU,多层线性变换等价于一个线性变换(Wx),无法拟合复杂的非线性关系。
3.3 ReLU 的改进变体
Leaky ReLU:LeakyReLU(x) = { x, x>0; αx, x≤0 }(α=0.01)
→ 负半区导数 = α ≠ 0,解决 Dead ReLU 问题
ELU:ELU(x) = { x, x>0; α(eˣ-1), x≤0 }(α=1)
→ 负半区输出连续平滑,均值更接近 0
GELU(GPT/BERT 使用):GELU(x) = x × Φ(x)
→ Φ 是标准正态分布的累积分布函数
→ 在正半区接近 x,在负半区平滑衰减到 0
Swish(Google):Swish(x) = x × σ(x)
→ 非单调,在 x 略负时有小负值
→ 负半区导数 = α ≠ 0,解决 Dead ReLU 问题
ELU:ELU(x) = { x, x>0; α(eˣ-1), x≤0 }(α=1)
→ 负半区输出连续平滑,均值更接近 0
GELU(GPT/BERT 使用):GELU(x) = x × Φ(x)
→ Φ 是标准正态分布的累积分布函数
→ 在正半区接近 x,在负半区平滑衰减到 0
Swish(Google):Swish(x) = x × σ(x)
→ 非单调,在 x 略负时有小负值
x = -1.0 时各激活函数对比:
ReLU(-1) = 0
LeakyReLU(-1) = 0.01 × (-1) = -0.01
ELU(-1) = 1 × (e⁻¹ - 1) = 0.368 - 1 = -0.632
GELU(-1) ≈ -1 × Φ(-1) ≈ -1 × 0.159 = -0.159
Swish(-1) = -1 × σ(-1) = -1 × 0.269 = -0.269
ReLU(-1) = 0
LeakyReLU(-1) = 0.01 × (-1) = -0.01
ELU(-1) = 1 × (e⁻¹ - 1) = 0.368 - 1 = -0.632
GELU(-1) ≈ -1 × Φ(-1) ≈ -1 × 0.159 = -0.159
Swish(-1) = -1 × σ(-1) = -1 × 0.269 = -0.269
3.4 Softmax 函数
softmax(zᵢ) = e^(zᵢ) / Σⱼ e^(zⱼ)
把任意实数向量转化为概率分布(所有输出 > 0,且总和 = 1)。
不是对每个元素独立操作,而是所有元素一起竞争。
把任意实数向量转化为概率分布(所有输出 > 0,且总和 = 1)。
不是对每个元素独立操作,而是所有元素一起竞争。
10分类任务(MNIST 手写数字识别)最后一层输出:
logits z = [2.1, -0.3, 0.8, 5.2, -1.0, 0.5, -0.2, 1.1, 0.3, -0.5]
(对应数字 0~9 的"得分",也叫 logits)
计算每个 e^(zᵢ):
e^2.1=8.17, e^-0.3=0.74, e^0.8=2.23, e^5.2=181.27, e^-1.0=0.37
e^0.5=1.65, e^-0.2=0.82, e^1.1=3.00, e^0.3=1.35, e^-0.5=0.61
总和 = 8.17+0.74+2.23+181.27+0.37+1.65+0.82+3.00+1.35+0.61 = 200.21
概率:
P(0) = 8.17/200.21 = 4.08%
P(1) = 0.74/200.21 = 0.37%
P(2) = 2.23/200.21 = 1.11%
P(3) = 181.27/200.21 = 90.54% ← 最高,预测为数字3
P(4) = 0.37/200.21 = 0.18%
P(5) = 1.65/200.21 = 0.82%
P(6) = 0.82/200.21 = 0.41%
P(7) = 3.00/200.21 = 1.50%
P(8) = 1.35/200.21 = 0.67%
P(9) = 0.61/200.21 = 0.30%
总和 = 100% ✓
Softmax 的放大效应:z₃=5.2 只比 z₀=2.1 大 3.1,
但概率从 4.08%→90.54%(差了 22 倍),因为指数函数放大了差异。
logits z = [2.1, -0.3, 0.8, 5.2, -1.0, 0.5, -0.2, 1.1, 0.3, -0.5]
(对应数字 0~9 的"得分",也叫 logits)
计算每个 e^(zᵢ):
e^2.1=8.17, e^-0.3=0.74, e^0.8=2.23, e^5.2=181.27, e^-1.0=0.37
e^0.5=1.65, e^-0.2=0.82, e^1.1=3.00, e^0.3=1.35, e^-0.5=0.61
总和 = 8.17+0.74+2.23+181.27+0.37+1.65+0.82+3.00+1.35+0.61 = 200.21
概率:
P(0) = 8.17/200.21 = 4.08%
P(1) = 0.74/200.21 = 0.37%
P(2) = 2.23/200.21 = 1.11%
P(3) = 181.27/200.21 = 90.54% ← 最高,预测为数字3
P(4) = 0.37/200.21 = 0.18%
P(5) = 1.65/200.21 = 0.82%
P(6) = 0.82/200.21 = 0.41%
P(7) = 3.00/200.21 = 1.50%
P(8) = 1.35/200.21 = 0.67%
P(9) = 0.61/200.21 = 0.30%
总和 = 100% ✓
Softmax 的放大效应:z₃=5.2 只比 z₀=2.1 大 3.1,
但概率从 4.08%→90.54%(差了 22 倍),因为指数函数放大了差异。
四、损失函数的导数
4.1 均方误差(MSE)
L = (1/n) Σᵢ (yᵢ - ŷᵢ)²
∂L/∂ŷᵢ = (2/n)(ŷᵢ - yᵢ)
含义:预测值 ŷᵢ 偏离真实值 yᵢ 越远,梯度越大,惩罚越强。
∂L/∂ŷᵢ = (2/n)(ŷᵢ - yᵢ)
含义:预测值 ŷᵢ 偏离真实值 yᵢ 越远,梯度越大,惩罚越强。
房价预测示例:3 个样本
真实值 y = [200, 350, 450](万元)
预测值 ŷ = [210, 330, 460](万元)
各样本误差:
(210-200)² = 100
(330-350)² = 400
(460-450)² = 100
MSE = (100 + 400 + 100) / 3 = 200
梯度:
∂L/∂ŷ₁ = (2/3)(210-200) = 6.67(正:预测偏高,应该减小)
∂L/∂ŷ₂ = (2/3)(330-350) = -13.33(负:预测偏低,应该增大)
∂L/∂ŷ₃ = (2/3)(460-450) = 6.67
第2个样本梯度最大(绝对值 13.33),因为误差最大(偏了 20 万),所以受到的"拉力"最强。
真实值 y = [200, 350, 450](万元)
预测值 ŷ = [210, 330, 460](万元)
各样本误差:
(210-200)² = 100
(330-350)² = 400
(460-450)² = 100
MSE = (100 + 400 + 100) / 3 = 200
梯度:
∂L/∂ŷ₁ = (2/3)(210-200) = 6.67(正:预测偏高,应该减小)
∂L/∂ŷ₂ = (2/3)(330-350) = -13.33(负:预测偏低,应该增大)
∂L/∂ŷ₃ = (2/3)(460-450) = 6.67
第2个样本梯度最大(绝对值 13.33),因为误差最大(偏了 20 万),所以受到的"拉力"最强。
4.2 交叉熵(Cross Entropy)
二分类:L = -[y·ln(ŷ) + (1-y)·ln(1-ŷ)]
多分类:L = -Σᵢ yᵢ·ln(ŷᵢ)(yᵢ 是 one-hot 编码)
结合 Softmax 后有个极简形式:
∂L/∂zᵢ = ŷᵢ - yᵢ(预测概率 - 真实标签)
多分类:L = -Σᵢ yᵢ·ln(ŷᵢ)(yᵢ 是 one-hot 编码)
结合 Softmax 后有个极简形式:
∂L/∂zᵢ = ŷᵢ - yᵢ(预测概率 - 真实标签)
猫/狗/鸟 三分类示例:
真实类别:猫 → one-hot y = [1, 0, 0]
模型 Softmax 输出 ŷ = [0.7, 0.2, 0.1]
交叉熵 L = -[1×ln(0.7) + 0×ln(0.2) + 0×ln(0.1)]
= -ln(0.7) = 0.357
如果模型输出 ŷ = [0.95, 0.03, 0.02]:
L = -ln(0.95) = 0.051(损失很小,预测很准)
如果模型输出 ŷ = [0.1, 0.6, 0.3]:
L = -ln(0.1) = 2.303(损失很大,把猫误判为狗)
Softmax + 交叉熵的简洁梯度:
∂L/∂z = ŷ - y = [0.7-1, 0.2-0, 0.1-0] = [-0.3, 0.2, 0.1]
z₁ 的梯度为 -0.3(负,应增大 → 提高猫的得分)
z₂ 的梯度为 +0.2(正,应减小 → 降低狗的得分)
z₃ 的梯度为 +0.1(正,应减小 → 降低鸟的得分)
真实类别:猫 → one-hot y = [1, 0, 0]
模型 Softmax 输出 ŷ = [0.7, 0.2, 0.1]
交叉熵 L = -[1×ln(0.7) + 0×ln(0.2) + 0×ln(0.1)]
= -ln(0.7) = 0.357
如果模型输出 ŷ = [0.95, 0.03, 0.02]:
L = -ln(0.95) = 0.051(损失很小,预测很准)
如果模型输出 ŷ = [0.1, 0.6, 0.3]:
L = -ln(0.1) = 2.303(损失很大,把猫误判为狗)
Softmax + 交叉熵的简洁梯度:
∂L/∂z = ŷ - y = [0.7-1, 0.2-0, 0.1-0] = [-0.3, 0.2, 0.1]
z₁ 的梯度为 -0.3(负,应增大 → 提高猫的得分)
z₂ 的梯度为 +0.2(正,应减小 → 降低狗的得分)
z₃ 的梯度为 +0.1(正,应减小 → 降低鸟的得分)
五、导数的数值验证方法
AI 工程中的"梯度检查"(Gradient Checking):
自动微分算出的梯度可能因代码 bug 而出错。怎么验证?
用中心差分近似:f'(x) ≈ (f(x+ε) - f(x-ε)) / (2ε)
例:验证 f(x) = x³ 在 x=2 处的导数
解析解:f'(x) = 3x² → f'(2) = 12.000000
数值近似(ε=10⁻⁴):
f(2.0001) = 2.0001³ = 8.001200060001
f(1.9999) = 1.9999³ = 7.998800060001
f'(2) ≈ (8.001200060001 - 7.998800060001) / 0.0002 = 0.002400000000 / 0.0002 = 12.000000
误差 = |12.000000 - 12.000000| / max(12, 12) < 10⁻⁷ ✓
如果相对误差 < 10⁻⁵,通常认为梯度实现正确。
自动微分算出的梯度可能因代码 bug 而出错。怎么验证?
用中心差分近似:f'(x) ≈ (f(x+ε) - f(x-ε)) / (2ε)
例:验证 f(x) = x³ 在 x=2 处的导数
解析解:f'(x) = 3x² → f'(2) = 12.000000
数值近似(ε=10⁻⁴):
f(2.0001) = 2.0001³ = 8.001200060001
f(1.9999) = 1.9999³ = 7.998800060001
f'(2) ≈ (8.001200060001 - 7.998800060001) / 0.0002 = 0.002400000000 / 0.0002 = 12.000000
误差 = |12.000000 - 12.000000| / max(12, 12) < 10⁻⁷ ✓
如果相对误差 < 10⁻⁵,通常认为梯度实现正确。
六、代码验证(C# / Rust)
C#(.NET 10)
// dotnet run 即可执行
double Sigmoid(double x) => 1.0 / (1.0 + Math.Exp(-x));
double SigmoidDeriv(double x) { double s = Sigmoid(x); return s * (1 - s); }
// ===== Sigmoid 及其导数 =====
double[] xs = { -5, -2, -1, 0, 1, 2, 5 };
Console.Write("x: ");
foreach (var v in xs) Console.Write($"{v,8}");
Console.Write("\nσ(x): ");
foreach (var v in xs) Console.Write($"{Sigmoid(v),8:F4}");
Console.Write("\nσ'(x): ");
foreach (var v in xs) Console.Write($"{SigmoidDeriv(v),8:F4}");
Console.WriteLine();
// ===== ReLU =====
double[] z = { -1.2, 0.8, -0.3, 2.1, 0.0, -0.5, 1.5, 0.3 };
Console.Write("ReLU: ");
foreach (var v in z) Console.Write($"{Math.Max(0, v),6:F1}");
int zeros = z.Count(v => Math.Max(0, v) == 0);
Console.WriteLine($"\n稀疏率: {(double)zeros / z.Length:F2}");
// ===== Softmax =====
double[] logits = { 2.1, -0.3, 0.8, 5.2, -1.0, 0.5, -0.2, 1.1, 0.3, -0.5 };
double logMax = logits.Max();
double[] exps = logits.Select(v => Math.Exp(v - logMax)).ToArray();
double sumExp = exps.Sum();
double[] probs = exps.Select(e => e / sumExp).ToArray();
Console.Write("Softmax概率: ");
foreach (var p in probs) Console.Write($"{p:F4} ");
Console.WriteLine($"\n概率总和: {probs.Sum():F4}");
Console.WriteLine($"预测类别: {Array.IndexOf(probs, probs.Max())}");
// ===== 交叉熵 + Softmax 梯度 =====
double[] yTrue = { 1, 0, 0 };
double[] yPred = { 0.7, 0.2, 0.1 };
double loss = 0;
for (int i = 0; i < 3; i++) loss -= yTrue[i] * Math.Log(yPred[i]);
Console.Write($"交叉熵损失: {loss:F4}\n梯度: ");
for (int i = 0; i < 3; i++) Console.Write($"{yPred[i] - yTrue[i]:F1} ");
Console.WriteLine();
// ===== 数值梯度检查 =====
double F(double x) => x * x * x;
double FGrad(double x) => 3 * x * x;
double x0 = 2.0, eps = 1e-7;
double numGrad = (F(x0 + eps) - F(x0 - eps)) / (2 * eps);
double anaGrad = FGrad(x0);
double relErr = Math.Abs(numGrad - anaGrad) / Math.Max(Math.Abs(numGrad), Math.Abs(anaGrad));
Console.WriteLine($"解析梯度: {anaGrad}, 数值梯度: {numGrad:F10}");
Console.WriteLine($"相对误差: {relErr:E2}");
Rust
fn sigmoid(x: f64) -> f64 { 1.0 / (1.0 + (-x).exp()) }
fn sigmoid_deriv(x: f64) -> f64 { let s = sigmoid(x); s * (1.0 - s) }
fn main() {
// ===== Sigmoid 及其导数 =====
let xs = [-5.0, -2.0, -1.0, 0.0, 1.0, 2.0, 5.0_f64];
print!("x: ");
for i in 0..xs.len() { print!("{:8.0}", xs[i]); }
print!("\nσ(x): ");
for i in 0..xs.len() { print!("{:8.4}", sigmoid(xs[i])); }
print!("\nσ'(x): ");
for i in 0..xs.len() { print!("{:8.4}", sigmoid_deriv(xs[i])); }
println!();
// ===== ReLU =====
let z = [-1.2, 0.8, -0.3, 2.1, 0.0, -0.5, 1.5, 0.3_f64];
print!("ReLU: ");
for i in 0..z.len() { print!("{:6.1}", z[i].max(0.0)); }
let zeros = z.iter().filter(|v| **v <= 0.0).count();
println!("\n稀疏率: {:.2}", zeros as f64 / z.len() as f64);
// ===== Softmax =====
let logits = [2.1, -0.3, 0.8, 5.2, -1.0, 0.5, -0.2, 1.1, 0.3, -0.5_f64];
let max_l = logits.iter().copied().reduce(f64::max).unwrap();
let exps: [f64; 10] = core::array::from_fn(|i| (logits[i] - max_l).exp());
let sum_e: f64 = exps.iter().sum();
let probs: [f64; 10] = core::array::from_fn(|i| exps[i] / sum_e);
print!("Softmax概率: ");
for i in 0..10 { print!("{:.4} ", probs[i]); }
let sum_p: f64 = probs.iter().sum();
println!("\n概率总和: {sum_p:.4}");
let argmax = probs.iter().enumerate()
.max_by(|a, b| a.1.total_cmp(b.1)).unwrap().0;
println!("预测类别: {argmax}");
// ===== 交叉熵 + Softmax 梯度 =====
let y_true = [1.0, 0.0, 0.0_f64];
let y_pred = [0.7, 0.2, 0.1_f64];
let mut loss = 0.0_f64;
for i in 0..3 { loss -= y_true[i] * y_pred[i].ln(); }
print!("交叉熵损失: {loss:.4}\n梯度: ");
for i in 0..3 { print!("{:.1} ", y_pred[i] - y_true[i]); }
println!();
// ===== 数值梯度检查 =====
let f = |x: f64| x.powi(3);
let f_grad = |x: f64| 3.0 * x.powi(2);
let (x0, eps) = (2.0_f64, 1e-7);
let num_g = (f(x0 + eps) - f(x0 - eps)) / (2.0 * eps);
let ana_g = f_grad(x0);
let rel = (num_g - ana_g).abs() / num_g.abs().max(ana_g.abs());
println!("解析梯度: {ana_g}, 数值梯度: {num_g:.10}");
println!("相对误差: {rel:.2e}");
}