核心摘要
神经网络是参数化函数的组合。仿射变换混合特征,非线性增加可表示函数的范围,优化器根据目标函数的梯度调整参数。反向传播是链式法则的高效应用,不是生物学习的模拟,也不保证模型真正理解任务。
架构、损失、数据切分、初始化、优化器、正则化和评测协议构成一个整体。训练损失更低,可能因为过拟合而伴随校准、鲁棒性、公平性或留出集表现变差。应始终在保留测试集上与简单基线比较。
从神经元到函数
生物神经元类比只适合建立直觉,并不代表二者相同。生物神经元是电化学、循环、自适应系统;人工层是由软件和硬件实现的数值运算。所谓“纳秒神经元”、大脑等价或能耗比较都需要统一工作负载和测量边界,不能从示意图推断。
对于输入批次 X,全连接层计算:
Z = X Wᵀ + b
A = φ(Z)
W 和 b 是学习参数,φ 是非线性函数。没有非线性时,堆叠多个仿射层仍然只是一个仿射函数。输入和输出维度属于数据契约;“输入层神经元”不是独立的生物式计算单元。
网络形状与输出语义
应在公式和测试中写明张量形状:
X: [batch, features]
W: [outputs, features]
b: [outputs]
Z: [batch, outputs]
输出头取决于目标:
| 任务 | 模型输出 | 常见损失 |
|---|---|---|
| 回归 | 实数预测 | MSE、MAE、Huber 或领域损失 |
| 二分类 | 一个 logit | 带 logits 的二元交叉熵 |
| 单标签多分类 | 每个类别一个 logit | 以整数标签为目标的交叉熵 |
| 多标签分类 | 每个标签一个 logit | 带 logits 的二元交叉熵 |
| 分布预测 | 分布参数 | 负对数似然 |
把 logits 交给数值稳定的框架损失。对于要求 logits 的交叉熵,事先调用 softmax 可能重复归一化并降低数值稳定性。
激活函数
ReLU 与变体
ReLU(x) = max(0, x)
ReLU 计算便宜且常作为基线,但某些单元可能对所有输入都失活。Leaky ReLU、GELU、SiLU 等变体会改变优化和计算取舍,没有一种对所有任务都最好。
Sigmoid 与 Tanh
Sigmoid 映射到 (0, 1),适合在二分类输出边界解释概率,但可能饱和。Tanh 映射到 (-1, 1) 且零中心,也会饱和。隐藏层选择应结合架构、归一化、初始化和实际梯度观察。
Softmax
Softmax 把一组 logits 映射为类别分布:
softmax(zᵢ) = exp(zᵢ - max(z)) / Σⱼ exp(zⱼ - max(z))
减去最大值是数值稳定技巧。概率向量不自动代表校准良好,校准需要独立评测。
前向传播与反向传播
对于两层网络:
z₁ = xW₁ᵀ + b₁
a₁ = φ(z₁)
z₂ = a₁W₂ᵀ + b₂
loss = L(z₂, y)
反向传播从损失向前应用链式法则。使用通常的 One-Hot 约定时,交叉熵对输出 Logit 的梯度具有 p - y 的简洁形式。自动微分计算的是 Vector-Jacobian Product,不需要构造完整 Jacobian Matrix。
参数更新可写成:
θ ← θ - η ∇θ L
其中 η 是梯度下降使用的学习率策略。Mini-Batch 训练中的梯度是估计值,优化器状态会改变更新轨迹。
形状安全的 Go 前向计算
下面的无依赖 Go 程序执行一个两层 MLP,并直接从 Logit 计算数值稳定的多分类交叉熵。它会显式校验矩阵形状,避免依赖隐式 Broadcast。固定权重用于保证输出可复现,并不代表已经训练的参数或推荐超参数。
package main
import (
"errors"
"fmt"
"math"
)
func dense(input, weights [][]float64, bias []float64) ([][]float64, error) {
if len(input) == 0 || len(input[0]) == 0 {
return nil, errors.New("input must be a non-empty [batch, features] matrix")
}
if len(weights) == 0 || len(weights) != len(bias) {
return nil, errors.New("weights and bias output dimensions must match")
}
inputFeatures := len(input[0])
for i, row := range input {
if len(row) != inputFeatures {
return nil, fmt.Errorf("input row %d has inconsistent width", i)
}
}
for i, row := range weights {
if len(row) != inputFeatures {
return nil, fmt.Errorf("weight row %d has width %d, want %d", i, len(row), inputFeatures)
}
}
output := make([][]float64, len(input))
for batch, sample := range input {
output[batch] = make([]float64, len(weights))
for unit, row := range weights {
sum := bias[unit]
for feature, value := range sample {
sum += value * row[feature]
}
output[batch][unit] = sum
}
}
return output, nil
}
func relu(matrix [][]float64) {
for i := range matrix {
for j := range matrix[i] {
matrix[i][j] = math.Max(0, matrix[i][j])
}
}
}
func crossEntropy(logits [][]float64, labels []int) (float64, error) {
if len(logits) == 0 || len(logits) != len(labels) {
return 0, errors.New("logits and labels must have the same batch size")
}
var loss float64
for i, row := range logits {
if labels[i] < 0 || labels[i] >= len(row) {
return 0, fmt.Errorf("label %d is outside [0, %d)", labels[i], len(row))
}
maxLogit := row[0]
for _, value := range row[1:] {
maxLogit = math.Max(maxLogit, value)
}
var denominator float64
for _, value := range row {
denominator += math.Exp(value - maxLogit)
}
logProbability := row[labels[i]] - maxLogit - math.Log(denominator)
loss -= logProbability
}
return loss / float64(len(logits)), nil
}
func main() {
features := [][]float64{
{1, 0, 0, 1},
{0, 1, 1, 0},
}
hiddenWeights := [][]float64{
{0.5, -0.5, 0.25, 0.75},
{-0.25, 0.5, 0.75, -0.5},
{0.4, 0.1, -0.3, 0.2},
}
hidden, err := dense(features, hiddenWeights, []float64{0.1, -0.1, 0})
if err != nil {
panic(err)
}
relu(hidden)
outputWeights := [][]float64{
{0.8, -0.2, 0.3},
{-0.4, 0.7, 0.1},
{0.1, -0.3, 0.9},
}
logits, err := dense(hidden, outputWeights, []float64{0, 0.1, -0.1})
if err != nil {
panic(err)
}
loss, err := crossEntropy(logits, []int{0, 1})
if err != nil {
panic(err)
}
fmt.Printf("hidden shape: %d x %d\n", len(hidden), len(hidden[0]))
fmt.Printf("logit shape: %d x %d\n", len(logits), len(logits[0]))
fmt.Printf("mean cross-entropy: %.6f\n", loss)
}
预期输出:
hidden shape: 2 x 3
logit shape: 2 x 3
mean cross-entropy: 0.493668
生产训练还需要反向传播或自动微分、优化器、数据校验、训练/验证/测试切分、Dtype 与设备策略、Checkpoint,以及有意设计的梯度累积。PyTorch CrossEntropyLoss等框架损失接收未归一化 Logit;提前调用 Softmax 会重复其稳定损失计算的一部分。
损失与优化器
损失函数必须匹配标签语义。MSE 对偏差对称处理;MAE 对异常值更不敏感;Huber 在二者之间取舍。交叉熵比较类别目标和 logits,带 logits 的二元交叉熵处理独立二元标签。
SGD、动量、Adam 和 AdamW 是更新规则,不保证找到全局最优。AdamW 的解耦权重衰减也不等同于所有参数化下直接加入 L2 惩罚。warmup、学习率调度、梯度裁剪、批量和归一化可能与优化器名称同样重要。
记录优化器、学习率、betas/动量、权重衰减、裁剪、调度器、有效 batch、精度、硬件和种子。“Adam 永远是默认首选”只是经验捷径,不是定理。
初始化、归一化与梯度健康
不恰当初始化会造成激活和梯度爆炸或消失。Xavier/Glorot 和 He/Kaiming 初始化适用于常见 fan-in 与激活假设,但架构和归一化会改变选择。
应监控:
- 激活和梯度分布;
- NaN/Inf 数量与损失缩放;
- 更新量与权重比值;
- 失活或饱和单元;
- 训练/验证损失和指标差距。
BatchNorm、LayerNorm、残差连接和初始化会改变优化动态,不是可以任意互换的“正则化开关”。
CNN、RNN 与 Transformer
卷积网络
卷积使用局部连接和权重共享,为网格输入提供与平移相关的归纳偏置。padding、stride、dilation、通道顺序、归一化和输入分辨率共同决定形状。除非输入形状固定且有断言,不要硬编码 flatten 尺寸;自适应池化或形状测试更稳妥。
循环网络
RNN 在时间步之间传递隐藏状态。普通循环可能难以处理长依赖,LSTM/GRU 的门控改变了这一取舍。应处理 padding mask,让隐藏状态与输入位于同一设备和 dtype,并明确任务使用最后状态还是每个时间步。循环的延迟还受到顺序依赖和批处理影响。
Transformer
自注意力通过 Query、Key、Value Projection 让 Token 交互。标准 Attention 的成对交互通常随序列长度呈二次增长,但 Kernel、Sparsity、Recurrence 和其他设计会改变实际扩展。Transformer 仍需要 Position、Attention Mask、Residual、Normalization 和 Task Head。
PyTorch 模块有明确形状约定。输入 [batch, sequence, embedding] 时配置 batch_first=True,有意识地传入 padding/causal mask,并测试输出形状。没有 mask 或位置处理的 block 不能作为完整语言模型。
训练协议与泛化
在增强或反复调参前切分数据。验证集用于模型和超参数决策,测试集只用于最终报告。应去重近似样本,检查时间、实体和基准污染。scikit-learn 的数据泄漏指南解释了为什么预处理只能在训练数据上拟合。
正则化包括权重衰减、dropout、数据增强、标签平滑、早停、更小模型和更好的数据。每种方法都改变偏差/方差取舍,“加 dropout”不是诊断。
应报告:
- 数据来源、许可证、预处理和切分策略;
- 模型修订、分词器/特征、架构和参数量;
- 优化器、调度、batch、精度、硬件、种子和运行时间;
- 基线、指标、置信区间和错误切片;
- 相关的校准、鲁棒性、公平性、隐私和分布外行为。
可复现不等同于完全确定。GPU 内核、并行、库版本和硬件即使使用相同种子也可能产生细微差异。
评测与调试
在增加模型规模前先建立最小基线。按以下顺序排查:
- 标签、范围、缺失、泄漏和类别平衡;
- 张量形状、dtype、mask 和标签对齐;
- 损失/激活组合和梯度流;
- 训练/验证曲线和刻意过拟合极小数据测试;
- 保留切片、校准和失败样例;
- 部署前处理一致性和监控。
准确率会掩盖类别不平衡。根据任务增加 precision/recall、F1、AUROC/PR-AUC、校准、混淆矩阵、代价加权错误或专用指标。生成系统还应以固定协议评测事实性、拒答、有害性、工具调用、引用和人工有用性。
常见问题
神经网络是按大脑建模的吗?
类比主要是历史和概念性的。人工层是数字硬件上的数学函数优化,不是生物神经元的忠实模拟。
每个隐藏层都应该用 ReLU 吗?
不应该。ReLU 是常见基线,但 GELU、SiLU、Tanh、门控单元、归一化、初始化和架构都会改变结果,应在任务上测试。
交叉熵前应该先 Softmax 吗?
当使用要求 logits 的框架损失(如 PyTorch CrossEntropyLoss)时通常不应该。它内部会执行稳定的 log-softmax。应遵循准确 API 契约。
为什么训练损失下降但测试变差?
可能是过拟合、切分污染、指标不代表任务或前处理不一致。应检查保留切片、数据血缘、校准和基线。
纳秒或更深网络意味着模型更好吗?
不意味着。硬件计时、参数量和深度本身不是质量指标。应在记录完整协议后比较任务表现、资源成本、鲁棒性和不确定性。
反向传播等同于生物学习吗?
不等同。反向传播是对计算图求导的算法;生物学习有不同机制和约束,普通训练循环没有表达它们。
一手来源
- Goodfellow、Bengio、Courville:Deep Learning
- He 等:Delving Deep into Rectifiers
- Kingma 与 Ba:Adam
- Loshchilov 与 Hutter:Decoupled Weight Decay Regularization
- Vaswani 等:Attention Is All You Need
- PyTorch 文档
- NIST AI Risk Management Framework
总结
神经网络是按目标训练的函数组合,不是魔法抽象,也不是字面意义上的数字大脑。应同时跟踪形状、数值稳定性、梯度、数据血缘、泛化和部署行为。深度学习基础指南进一步把这些机制连接到架构选择与生产评测。