核心摘要

神经网络是参数化函数的组合。仿射变换混合特征,非线性增加可表示函数的范围,优化器根据目标函数的梯度调整参数。反向传播是链式法则的高效应用,不是生物学习的模拟,也不保证模型真正理解任务。

架构、损失、数据切分、初始化、优化器、正则化和评测协议构成一个整体。训练损失更低,可能因为过拟合而伴随校准、鲁棒性、公平性或留出集表现变差。应始终在保留测试集上与简单基线比较。

从神经元到函数

生物神经元类比只适合建立直觉,并不代表二者相同。生物神经元是电化学、循环、自适应系统;人工层是由软件和硬件实现的数值运算。所谓“纳秒神经元”、大脑等价或能耗比较都需要统一工作负载和测量边界,不能从示意图推断。

对于输入批次 X,全连接层计算:

text
Z = X Wᵀ + b
A = φ(Z)

W 和 b 是学习参数,φ 是非线性函数。没有非线性时,堆叠多个仿射层仍然只是一个仿射函数。输入和输出维度属于数据契约;“输入层神经元”不是独立的生物式计算单元。

网络形状与输出语义

应在公式和测试中写明张量形状:

text
X: [batch, features]
W: [outputs, features]
b: [outputs]
Z: [batch, outputs]

输出头取决于目标:

任务 模型输出 常见损失
回归 实数预测 MSE、MAE、Huber 或领域损失
二分类 一个 logit 带 logits 的二元交叉熵
单标签多分类 每个类别一个 logit 以整数标签为目标的交叉熵
多标签分类 每个标签一个 logit 带 logits 的二元交叉熵
分布预测 分布参数 负对数似然

把 logits 交给数值稳定的框架损失。对于要求 logits 的交叉熵,事先调用 softmax 可能重复归一化并降低数值稳定性。

激活函数

ReLU 与变体

text
ReLU(x) = max(0, x)

ReLU 计算便宜且常作为基线,但某些单元可能对所有输入都失活。Leaky ReLU、GELU、SiLU 等变体会改变优化和计算取舍,没有一种对所有任务都最好。

Sigmoid 与 Tanh

Sigmoid 映射到 (0, 1),适合在二分类输出边界解释概率,但可能饱和。Tanh 映射到 (-1, 1) 且零中心,也会饱和。隐藏层选择应结合架构、归一化、初始化和实际梯度观察。

Softmax

Softmax 把一组 logits 映射为类别分布:

text
softmax(zᵢ) = exp(zᵢ - max(z)) / Σⱼ exp(zⱼ - max(z))

减去最大值是数值稳定技巧。概率向量不自动代表校准良好,校准需要独立评测。

前向传播与反向传播

对于两层网络:

text
z₁ = xW₁ᵀ + b₁
a₁ = φ(z₁)
z₂ = a₁W₂ᵀ + b₂
loss = L(z₂, y)

反向传播从损失向前应用链式法则。使用通常的 One-Hot 约定时,交叉熵对输出 Logit 的梯度具有 p - y 的简洁形式。自动微分计算的是 Vector-Jacobian Product,不需要构造完整 Jacobian Matrix。

参数更新可写成:

text
θ ← θ - η ∇θ L

其中 η 是梯度下降使用的学习率策略。Mini-Batch 训练中的梯度是估计值,优化器状态会改变更新轨迹。

形状安全的 Go 前向计算

下面的无依赖 Go 程序执行一个两层 MLP,并直接从 Logit 计算数值稳定的多分类交叉熵。它会显式校验矩阵形状,避免依赖隐式 Broadcast。固定权重用于保证输出可复现,并不代表已经训练的参数或推荐超参数。

go
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)
}

预期输出:

text
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 内核、并行、库版本和硬件即使使用相同种子也可能产生细微差异。

评测与调试

在增加模型规模前先建立最小基线。按以下顺序排查:

  1. 标签、范围、缺失、泄漏和类别平衡;
  2. 张量形状、dtype、mask 和标签对齐;
  3. 损失/激活组合和梯度流;
  4. 训练/验证曲线和刻意过拟合极小数据测试;
  5. 保留切片、校准和失败样例;
  6. 部署前处理一致性和监控。

准确率会掩盖类别不平衡。根据任务增加 precision/recall、F1、AUROC/PR-AUC、校准、混淆矩阵、代价加权错误或专用指标。生成系统还应以固定协议评测事实性、拒答、有害性、工具调用、引用和人工有用性。

常见问题

神经网络是按大脑建模的吗?

类比主要是历史和概念性的。人工层是数字硬件上的数学函数优化,不是生物神经元的忠实模拟。

每个隐藏层都应该用 ReLU 吗?

不应该。ReLU 是常见基线,但 GELU、SiLU、Tanh、门控单元、归一化、初始化和架构都会改变结果,应在任务上测试。

交叉熵前应该先 Softmax 吗?

当使用要求 logits 的框架损失(如 PyTorch CrossEntropyLoss)时通常不应该。它内部会执行稳定的 log-softmax。应遵循准确 API 契约。

为什么训练损失下降但测试变差?

可能是过拟合、切分污染、指标不代表任务或前处理不一致。应检查保留切片、数据血缘、校准和基线。

纳秒或更深网络意味着模型更好吗?

不意味着。硬件计时、参数量和深度本身不是质量指标。应在记录完整协议后比较任务表现、资源成本、鲁棒性和不确定性。

反向传播等同于生物学习吗?

不等同。反向传播是对计算图求导的算法;生物学习有不同机制和约束,普通训练循环没有表达它们。

一手来源

总结

神经网络是按目标训练的函数组合,不是魔法抽象,也不是字面意义上的数字大脑。应同时跟踪形状、数值稳定性、梯度、数据血缘、泛化和部署行为。深度学习基础指南进一步把这些机制连接到架构选择与生产评测。