读完这篇,你能做什么
能计算两层网络的形状与参数量,解释为什么只叠加线性层仍有限制,并运行小型训练、说明结果的适用范围。
学习理由、准备要求与知识点
为什么学它
MLP 把矩阵、导数和训练循环合在一起。看懂每层怎样计算,就有了阅读复杂网络的基础。
先读这些内容
知识点清单
- 仿射层与权重方向
- 批次和特征维度
- 纯线性层合并
ReLU与分段线性- Tanh 与饱和梯度
- XOR 非线性表达
- 参数初始化与对称性
- 二分类 logit 与损失
- 表达能力和优化的区别
运行前先确认Java 17;Python 3.14、PyTorch 2.9.1+cpu。Java 验证手工构造的 ReLU XOR 网络,Python 实际训练小型 Tanh 网络;两者参数和机制证据分别说明,不涉及 GPU 或真实任务泛化。
01先弄懂它是什么
要从一组输入算出预测,可以让数字先经过加权求和,再经过非线性处理,重复几层,这就是多层感知机。对一批 B 个样本,每个样本有 d 个特征,输入 X 的形状是 [B,d]。若中间层要产生 h 个特征,就使用 W1:[h,d] 和 b1:[h],计算 H=activation(X*W1^T+b1),输出形状为 [B,h]。第一维区分样本,最后一维区分每个样本的特征。
为什么每层加权求和以后,还要接一个激活函数?因为如果两层之间没有非线性处理,W2*(W1*x+b1)+b2 仍可以合并成一次新的矩阵运算再加偏置。也就是说,只多堆这样的层,表达能力仍受同一类仿射变换限制。激活函数会按输入值作非线性转换,让网络能够表达更复杂、随输入区域变化的关系。
XOR 的四个输入为零一对:(0,0)、(0,1)、(1,0)、(1,1),只有两个位不同时输出一。它们不能被单个仿射阈值正确分开,但简单带非线性的网络可以表达。这个例子用来区分模型能否表达与训练能否找到参数,不代表四个点能证明模型适合真实复杂数据。
文中这几个词,先认一认
- 隐藏层
- 输入与最终输出之间的中间层,用来逐步形成对输入的新表示。
- 激活函数
- 常对一层输出中的每个数单独作非线性转换,使多层网络能表达比单次加权求和更复杂的关系。
- 饱和区
- 输入继续变化,函数输出却几乎不再变化的范围;这里的导数通常接近零。
- Logit
- 模型给某个类别的原始分数,可以是任意实数,此时还不是概率。
02看看它怎么工作
ReLU(z)=max(0,z),正半轴导数为一、负半轴为零,零点没有普通唯一导数。组合多层 ReLU 得到分段仿射函数;它不是只把负数丢掉这么简单,而是使不同输入激活不同子网络。若某单元长期对所有训练样本输出负区间,局部梯度可能持续为零。
Tanh 把实数压到负一与一之间,导数为 1-tanh(z)^2。输入绝对值很大时,输出变化很小,导数也接近零;多层导数连乘后,早期层收到的梯度可能就很小。Sigmoid 也有这种饱和区,而且输出不以零为中心。选激活函数时应一起考虑初始化、网络深度、归一化和任务,没有一个名称能保证所有情况都最好。
可以手工构造一个 ReLU XOR:令 s=x1+x2,h1=ReLU(s)、h2=ReLU(s-1),输出 h1-2*h2。对布尔输入 s 只可能为零、一、二,输出分别为零、一、零。这个函数在其他实数输入上仍可计算,但输出不一定是概率,因此完整算法把 XOR 输入契约明确限定为零一对。
Python 实验使用 2→8→1 的 Tanh 网络,通过 BCEWithLogitsLoss 训练。最后一个数是二分类 logit,损失内部以稳定方式组合 sigmoid 与二元交叉熵,所以模型末尾不再额外添加 sigmoid。标签形状为 [4,1],预测也为 [4,1],避免自动广播把目标配对弄错。
参数量需要包括偏置:第一层有 2*8+8=24 个参数,第二层有 8*1+1=9 个,总共三十三。随机初始化用来打破不同隐藏单元的对称性,固定种子便于复现当前实验。四点全部分类正确只证明这个配置能拟合完整 XOR 真值表,不说明任意初始化都能收敛或对其他问题有泛化收益。
先跑通一个最小例子
先按第 13 篇安装隔离环境。使用 Python 3.14、PyTorch 2.9.1+cpu 运行 experiment.py,例如 .\.venv\Scripts\python.exe experiment.py;程序只在 CPU 训练四个点。Sequential 顺序调用层,Linear 的最后一维为输入特征数。
import torch
from torch import nn
torch.set_num_threads(1)
torch.manual_seed(2026)
x=torch.tensor([[0.,0.],[0.,1.],[1.,0.],[1.,1.]],dtype=torch.float64)
y=torch.tensor([[0.],[1.],[1.],[0.]],dtype=torch.float64)
model=nn.Sequential(nn.Linear(2,8),nn.Tanh(),nn.Linear(8,1)).double()
criterion=nn.BCEWithLogitsLoss()
optimizer=torch.optim.Adam(model.parameters(),lr=.05)
assert model(x).shape==y.shape
assert sum(p.numel() for p in model.parameters())==33
initial=criterion(model(x),y).item()
optimizer.zero_grad()
criterion(model(x),y).backward()
assert all(p.grad is not None and torch.isfinite(p.grad).all() for p in model.parameters())
for _ in range(600):
optimizer.zero_grad() # 每轮梯度独立,避免非预期累积。
loss=criterion(model(x),y)
loss.backward()
optimizer.step()
with torch.no_grad():
logits=model(x)
final=criterion(logits,y).item()
predicted=(logits>=0).to(y.dtype) # sigmoid 等于 .5 时 logit 为零。
assert final<.02
assert final<initial
assert torch.equal(predicted,y)
# 两个仿射层无激活时确实可以合并。
a=nn.Linear(2,3).double(); b=nn.Linear(3,1).double()
merged=x@(b.weight@a.weight).T+(b.weight@a.bias+b.bias)
assert torch.allclose(b(a(x)),merged,atol=1e-12)
print("PASS 7; initial =",round(initial,6),"final =",round(final,6))
核对运行结果打印 PASS 7;固定配置下训练损失低于 0.02,四个 XOR 点预测正确;同时验证无激活仿射层的合并恒等式。
正在加载流程图…
查看流程图源码(Mermaid)
flowchart LR
A["输入 B乘2"] --> B["仿射层 2到8"]
B --> C["Tanh 非线性"]
C --> D["仿射层 8到1"]
D --> E["二分类 logits"]
E --> F["稳定二元交叉熵"]
F --> G["反向求导和参数更新"]按顺序看,每一步做了什么
- 先写清每一层输入、权重与输出形状。
- 用手工参数证明一个简单非线性函数可表达。
- 构建对应任务的训练网络和损失。
- 固定种子,检查首步梯度与参数数目。
- 训练后报告当前数据上的误差及证据边界。
03对照实现代码
PyTorch v2.9.1;torch/nn/modules/linear.py · Linear.forward
以下为原创精简教学实现,不是原样库源码;只演示本文说明的机制,省略真实实现的优化、调度和完整兼容边界。
def dense_one(x,weights,bias):
# 原创单样本模型:每一行权重对应一个输出特征。
if len(weights)!=len(bias) or any(len(row)!=len(x) for row in weights):
raise ValueError("shape")
return [sum(w*v for w,v in zip(row,x))+b for row,b in zip(weights,bias)]
# 本模型没有 Parameter 注册、批次广播或自动微分。
Linear 构造函数创建形状 (out_features,in_features) 的 Parameter 权重,可选创建 out_features 长度的偏置;禁用偏置时注册为 None。读取权重方向有助于理解为什么批次输入写成 X*W^T,而不是盲目转置直到形状碰巧能乘。
reset_parameters 调用 kaiming_uniform_ 并使用特定 a 值,源码注释说明它对应输入宽度决定的均匀范围;偏置也按 fan_in 计算边界。这个默认初始化是实际实现的选择,不意味着它对所有激活、深度和数据尺度都最合适。
Linear.forward 直接委托 F.linear(input,self.weight,self.bias)。这个模块没有自动插入 ReLU、Tanh 或 sigmoid,因此网络是否具有非线性取决于组合层。Module 负责组织可学习参数,实际矩阵计算和自动微分由后续框架路径承担。
原创单样本列表函数仅帮助核对权重行的意义,不包含 Parameter、设备、批次或求导。Java 网络使用固定手工参数,Python 网络则由实际优化器学习;两者都实现非线性映射,但不能把手工真值表验证描述为已经训练出的同一个模型。
代码先调用谁,再调用谁
Linear.__init__注册权重与可选偏置reset_parameters初始化参数- Sequential 调用
Linear.forward - forward 委托
F.linear - 显式激活层再处理输出
04动手写一个例子
两层 ReLU 网络的前向计算与 XOR 真值表
实现单样本稠密仿射层和可选 ReLU,使用手工参数构造仅接受两个布尔数值的 XOR 网络;有限参数、矩形权重和维度必须验证。
- 输入 1
[0,1]- 输出 1
1
- 输入 2
[1,1]- 输出 2
0
稠密层逐行计算权重与输入点积,再加本行偏置,因此输出长度等于权重行数。所有权重行都必须与输入长度一致;不能让 zip 截断或只检查第一行,从而静默忽略错误特征。
隐藏层使用两行相同的 [1,1] 权重,偏置分别为零和负一,再应用 ReLU。输出层权重为 [1,-2]、偏置为零,没有输出激活。这个结构在三个可能的输入和上形成所需折线。
算法将通用层的实数前向与 XOR 业务入口分开:层检查形状及有限性,XOR 额外限制两个输入为零或一。四个真值表测试覆盖完整离散定义域,再加入维度错误与非有限输入测试;这些测试没有宣称对连续分类问题训练有效。
为什么这样写是对的完成第 i 行时,输出前 i 项分别等于对应权重行的点积加偏置,再按配置应用一次激活。
隐藏层计算 h 次长度 d 的点积,输出层计算 k 次长度 h 的点积;本例 d=2,h=2,k=1 为常数,但公式可推广。
保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。
展开收起代码 · Main.java · 完整可运行程序
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
public class Main {
static double[] dense(double[] x,double[][] w,double[] b,boolean relu){
if(x==null||w==null||b==null||x.length==0||w.length==0||w.length!=b.length)throw new IllegalArgumentException("shape");
for(double v:x)if(!Double.isFinite(v))throw new IllegalArgumentException("finite input");
double[] out=new double[w.length];
for(int i=0;i<w.length;i++){
if(w[i]==null||w[i].length!=x.length||!Double.isFinite(b[i]))throw new IllegalArgumentException("row");
double value=b[i];
for(int j=0;j<x.length;j++){
if(!Double.isFinite(w[i][j]))throw new IllegalArgumentException("weight");
value+=w[i][j]*x[j]; // 每行定义一个输出特征。
}
if(!Double.isFinite(value))throw new ArithmeticException("range");
out[i]=relu?Math.max(0,value):value; // 激活是显式选择,不藏在线性层内。
}
return out;
}
static double xor(double[] x){
if(x==null||x.length!=2)throw new IllegalArgumentException("two bits");
for(double v:x)if(v!=0&&v!=1)throw new IllegalArgumentException("bit");
double[] h=dense(x,new double[][]{{1,1},{1,1}},new double[]{0,-1},true);
return dense(h,new double[][]{{1,-2}},new double[]{0},false)[0];
}
static void near(double a,double b){if(!Double.isFinite(a)||Math.abs(a-b)>1e-12)throw new AssertionError();}
static void bad(Runnable r){try{r.run();}catch(IllegalArgumentException e){return;}throw new AssertionError();}
public static void main(String[] args){
near(xor(new double[]{0,0}),0); // 四个测试穷尽布尔真值表。
near(xor(new double[]{0,1}),1);
near(xor(new double[]{1,0}),1);
near(xor(new double[]{1,1}),0);
bad(()->dense(new double[]{1,2},new double[][]{{1}},new double[]{0},true));
bad(()->xor(new double[]{Double.NaN,1}));
System.out.println("PASS 6");
}
}这些特殊情况也要试一试
- 布尔 XOR 入口拒绝任意非零一实数。
- 稠密层拒绝空输入、锯齿权重和维度不匹配。
- 有限输入仍可能造成中间溢出。
- 输出实数不自动具有概率语义。
进阶选读 · 宽度、深度与可训练性
场景增加网络容量后训练或验证表现没有改善。
做法区分表达能力、优化状态和统计泛化,记录层激活与梯度尺度,并在相同数据边界下比较宽度、深度和初始化。
复杂度稠密前向成本约随相邻层宽度乘积之和增长,反向还需保存激活。
好处与代价表达定理通常不保证有限数据、固定优化器和预算下能找到好解,容量增加也可能加剧过拟合。
05项目里怎么用
构建网络时先打印一次形状和参数量,再开始长训练。错误的批次轴、误用最后维度或额外 sigmoid 可能仍返回某个可计算损失,却训练了不同目标。小数据过拟合测试是计算链条诊断工具,但正式评价必须遵守独立验证数据边界。
激活分布能帮助诊断训练困难。大量 ReLU 单元长期为零、Tanh 输出集中在接近正负一的位置,都提示需要检查输入缩放、初始化与学习率。观察到这些现象并不直接证明唯一原因,应结合各层梯度、参数变化和具体样本逐步定位。
生产实现应把模型结构、权重、预处理和标签约定一同版本化。固定四点实验没有涵盖噪声、类别不平衡、分布变化与部署延迟;当用于真实服务,还需要明确输入校验、批处理成本和输出解释。本篇的价值是形成可核验的完整训练小循环。
容易踩的坑
- 叠加多个 Linear 却忘记加入非线性。
- 将输入宽度与批次大小混为一谈。
- 在
BCEWithLogitsLoss前额外 sigmoid。 - 把四点拟合成功当作真实任务泛化证据。
几种做法,怎么选
表格有四列,窄屏可左右滑动查看代价和适用条件。
| 方案 | 优势 | 代价与限制 | 适用条件 |
|---|---|---|---|
ReLU | 计算简单,正区间梯度稳定 | 负区间梯度为零,存在失活风险 | 常见隐藏层基线 |
| Tanh | 输出有界且以零为中心 | 大幅输入易饱和 | 小型示例与有界表示 |
| 纯仿射模型 | 易解释、成本低 | 不能表达 XOR 这类边界 | 线性参考基线 |
06检验是否读懂了
先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。
先用自己的话解释
01为什么两个无激活层仍是仿射映射?
展开参考答案与推导
展开组合后,矩阵乘积形成一个新矩阵,前层偏置经后层变换再加后层偏置,仍保持矩阵乘输入加常量的形式。
02Tanh 为何可能使梯度变小?
展开参考答案与推导
其导数为一减输出平方,输出接近正负一时导数接近零;多层链式相乘可能进一步减小传向早期层的梯度。
03表达能力足够就一定训练成功吗?
展开参考答案与推导
不一定。存在一组正确参数与优化过程能够在给定初始化、数据和预算中找到它是两件事,仍需实际验证。
换个情况,还会处理吗
01输出接近零一,能直接当概率吗?
展开参考答案与推导
先看输出层和损失定义。手工 ReLU XOR 在布尔点的输出恰为零一,并不使整个实数函数都成为合法或校准的概率模型。
02四点训练成功后可以报告什么?
展开参考答案与推导
可以报告该固定环境和配置拟合了 XOR 真值表及其损失,不应扩展为真实业务准确率、普遍收敛结论或硬件吞吐指标。
03加深网络后效果变差先查什么?
展开参考答案与推导
先检查层形状、非线性和梯度路径,再观察激活饱和、初始化、学习率及数据边界,区分优化困难和泛化恶化。
写代码试一试
01计算参数量。
- 输入 / 条件
网络2→8→1,两层都有偏置- 输出 / 预期
33
提示 每层输入乘输出再加输出。
展开参考答案与推导
第一层十六个权重加八个偏置为二十四,第二层八个权重加一个偏置为九,总计三十三。
02手算 XOR 隐藏表示。
- 输入 / 条件
x=[1,1]- 输出 / 预期
h=[2,1],output=0
提示 先算输入和s=2。
展开参考答案与推导
两个 ReLU 分别作用于二和一,最后二减二乘一得到零,展示了非线性折线的作用。
03修复二分类损失输入。
- 输入 / 条件
模型末尾sigmoid,损失BCEWithLogitsLoss- 输出 / 预期
移除额外sigmoid
提示 损失需要原始logit。
展开参考答案与推导
保留输出实数分数直接交给损失;需要展示概率时,在评估阶段对 logit 计算 sigmoid,并明确其校准情况另需评估。
读完之后,独立完成
课后作业:自己做一遍
本篇共 10 道作业,先完成 8 道基础与综合任务,再按前置知识挑战进阶题。其中包含 1 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。
能计算两层网络的形状与参数量,解释为什么只叠加线性层仍有限制,并运行小型训练、说明结果的适用范围。
本篇重点加练
非线性表达结构和能否优化到解是两回事,需要对函数区域和失败原因分别考核。
ReLU与分段线性- 表达能力和优化的区别
重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。
先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。
基础必做
- 作业 01代码推演
批量 X 形状 [3,2],网络隐藏宽度 4、输出宽度 1。写出每层权重、偏置、激活形状和参数数目,并追踪单样本进入批量后的轴。
- 提交内容
- 逐层形状及参数表
- 怎样算完成
- 权重方向一致
- 批次不计入参数量
练到的知识点仿射层与权重方向 · 批次和特征维度
- 作业 02原理问答
比较无激活、
ReLU、Tanh 三种两层网络能表示的函数类别;为每种选择一个可以手工分析的输入区间。- 提交内容
- 函数草图与说明
- 怎样算完成
- 线性和分段性质准确
- 饱和讨论限定输入区间
练到的知识点纯线性层合并 · ReLU 与分段线性 · Tanh 与饱和梯度
- 作业 03代码推演
自定一个一维输入、两个
ReLU隐藏单元的网络参数,枚举激活模式改变的位置,分区间写出输出表达式并核对区间边界。- 提交内容
- 区间图与分段表达式
- 怎样算完成
- 所有断点覆盖完整
- 边界左右函数值可核验
练到的知识点ReLU 与分段线性
- 作业 04原理问答
“训练失败说明网络无法表示目标”这一推断还需要排除哪些可能?为容量不足、优化失败、实现错误分别提出不同的证据标准。
- 提交内容
- 三类判据及自拟案例
- 怎样算完成
- 判据能区分原因
- 不把未收敛当不可能证明
练到的知识点表达能力和优化的区别
综合应用
- 作业 05编程实践
用 CPU 训练隐藏宽度为 2 和 8 的 XOR 分类器,每种配置运行五个预定种子;保留四点预测、损失曲线与失败运行。
- 提交内容
- 训练程序和完整运行表
- 怎样算完成
- 分类损失输入契约正确
- 种子及初始化可复现
练到的知识点XOR 非线性表达 · 参数初始化与对称性 · 二分类 logit 与损失
- 作业 06排错实验
比较隐藏层全零初始化、独立随机初始化及大幅值初始化,在固定首批样本上记录各神经元输出和梯度。
- 提交内容
- 受控初始化实验
- 怎样算完成
- 对称性与饱和分别观察
- 数据及优化器固定
练到的知识点Tanh 与饱和梯度 · 参数初始化与对称性
- 作业 07面试场景
网络能拟合四个训练点,但新输入表现未知。设计检验插值与外推的样本集,并给这项实验写不夸大的能力说明。
- 提交内容
- 评价样本与能力声明
- 怎样算完成
- 训练点与评价点分开
- 表达能力不冒充泛化证据
练到的知识点二分类 logit 与损失 · 表达能力和优化的区别
- 作业 08编程实践
在 CPU 上针对同一组已知可拟合的八个样本,分别从活跃和长期不活跃的
ReLU状态开始优化;记录隐藏激活、梯度和误差。- 提交内容
- 受控初始化实验
- 怎样算完成
- 目标及预算固定
- 表达可行性有独立依据
练到的知识点ReLU 与分段线性 · 表达能力和优化的区别
进阶挑战 · 掌握前置后选做
- 作业 09设计评审
构造一个小型非线性分类任务,在参数数量相近的条件下比较浅宽与深窄网络;预先定义训练预算及模型选择规则。
- 提交内容
- 实验协议、实现与报告
- 怎样算完成
- 参数和计算预算分别说明
- 全部配置结果保留
练到的知识点ReLU 与分段线性 · XOR 非线性表达 · 表达能力和优化的区别
思维拓展
先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。
- 作业 10思维拓展 · 反例构造
构造一个有限区间内两网络完全一致、区间外行为不同的例子,讨论在训练样本上拟合相同能否确定全局函数。
- 提交内容
- 两个小网络与区间比较
- 怎样算完成
- 网络参数和区间明确
- 有限观察与全局结论分开
练到的知识点ReLU 与分段线性 · 表达能力和优化的区别
完成后,再检验一次
给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。
继续阅读
先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。
- 必读参考文档英文
PyTorch v2.9.1:Linear 的参数注册、初始化与 forward (新标签页)↗
核对权重方向、Parameter 注册、默认初始化与 forward 的职责边界。
- 深入参考文档英文
PyTorch 2.9 Tanh (新标签页)↗
核对逐元素 Tanh 定义与形状。
- 深入参考文档英文
PyTorch 2.9 ReLU (新标签页)↗
核对分段线性定义和原地操作选项。
- 深入参考文档英文
PyTorch 2.9 BCEWithLogitsLoss (新标签页)↗
阅读原始 logits、目标形状和数值稳定损失的组合。