第 7 章 神经网络基础
学习目标
- 掌握
nn.Module:神经网络组件的基类 - 掌握
nn.Linear全连接层与nn.Sequential - 理解
nn.Parameter与model.parameters() - 理解
forward与model(x)的调用方式 - 会搭建第一个多层感知机(MLP)并做前向传播
7.1 从手动参数到 nn.Module
第 6 章的 w = torch.tensor(..., requires_grad=True) 是手工管理参数。神经网络有成百上千个参数,需要统一管理:能一起 parameters() 取出来、一起进优化器、一起保存。PyTorch 用 nn.Module 完成这件事。
nn.Module 是 PyTorch 所有神经网络组件的基类:一个层、一个网络,都是它的子类。核心约定:
- 在
__init__里定义子层和参数; - 在
forward里写数据怎么流经网络; - 用
model(x)调用网络,不要直接调model.forward(x)。
最常用的层是 nn.Linear(全连接层),它实现了第 4 章的 输入 @ 权重 + 偏置:
import torch
import torch.nn as nn
lin = nn.Linear(2, 1) # 2 个输入特征 → 1 个输出
print(lin)
print(lin.weight.shape, lin.weight.requires_grad)
print(lin.bias.shape)输出:
Linear(in_features=2, out_features=1, bias=True)
torch.Size([1, 2]) True
torch.Size([1])weight 的形状是 (1, 2):输出数 × 输入数;bias 形状是 (1,)。参数创建时自动 requires_grad=True,可以直接参与 autograd。
前向传播(推理)一次:
import torch
import torch.nn as nn
torch.manual_seed(42)
lin = nn.Linear(2, 1)
x = torch.tensor([[1., 2.],
[3., 4.],
[5., 6.]])
y = lin(x)
print(y)
print(y.shape)输出:
tensor([[1.5488],
[3.8038],
[6.0588]], grad_fn=<AddmmBackward0>)
torch.Size([3, 1])输入 (3, 2)(3 个样本、2 个特征)经过 Linear 变成 (3, 1)。输出带 grad_fn=<AddmmBackward0>,说明它参与了计算图——这就是第 6 章自动求导的入口。
7.2 自定义一个最简单的网络
一个「单权重」网络演示 nn.Module 的最小结构:
import torch
import torch.nn as nn
class Net(nn.Module):
def __init__(self):
super().__init__()
self.weight = nn.Parameter(torch.tensor(2.0))
self.bias = nn.Parameter(torch.tensor(1.0))
def forward(self, x):
return self.weight * x + self.bias
net = Net()
print(net)
print("weight requires_grad:", net.weight.requires_grad)
print("forward(3):", net(torch.tensor(3.0)))输出:
Net()
weight requires_grad: True
forward(3): tensor(7., grad_fn=<AddBackward0>)2×3 + 1 = 7,符合预期。要点:
- 必须调用
super().__init__(),否则模块内部注册机制不生效; nn.Parameter是「声明为参数」的张量,创建后自动requires_grad=True,并被登记到模块;forward就是普通 Python 方法,可以写任意张量运算;net(x)会先做一些内部处理(如登记钩子)再调用forward。
用 parameters() 和 named_parameters() 可以拿到模块管理的全部参数:
print("参数数量:", len(list(net.parameters())))
for name, p in net.named_parameters():
print(name, p)输出:
参数数量: 2
weight Parameter containing:
tensor(2., requires_grad=True)
bias Parameter containing:
tensor(1., requires_grad=True)训练时优化器直接接收 net.parameters(),一个不漏。
7.3 nn.Sequential:串起多个层
nn.Sequential(顺序容器)把多个层按顺序排好,输入依次流过:
import torch
import torch.nn as nn
torch.manual_seed(42)
net = nn.Sequential(
nn.Linear(2, 4), # 2 个特征 → 4 个隐藏单元
nn.ReLU(), # 激活函数
nn.Linear(4, 1), # 4 个隐藏单元 → 1 个输出
)
print(net)输出:
Sequential(
(0): Linear(in_features=2, out_features=4, bias=True)
(1): ReLU()
(2): Linear(in_features=4, out_features=1, bias=True)
)前向传播:
x = torch.tensor([[1., 2.]])
print(net(x))输出:
tensor([[-0.0214]], grad_fn=<AddmmBackward0>)数据流:(1, 2) → Linear 变成 (1, 4) → ReLU(逐元素把负数变 0)→ Linear 变成 (1, 1)。这就是一个最简单的 多层感知机(MLP)。
激活函数(activation) 的作用是给网络引入非线性。两个线性层叠在一起本质上还是一个线性函数,ReLU 让网络能表达更复杂的函数关系,深度学习的「深」才真正有意义。
7.4 参数初始化
nn.Linear 默认会做合理的参数初始化(权重按均匀分布,范围由输入输出大小决定),所以示例里没手动初始化。需要指定时,可以在 __init__ 里覆盖:
import torch
import torch.nn as nn
class FixedNet(nn.Module):
def __init__(self):
super().__init__()
self.fc = nn.Linear(2, 1)
with torch.no_grad():
self.fc.weight.fill_(0.5)
self.fc.bias.fill_(0.0)
def forward(self, x):
return self.fc(x)
net = FixedNet()
print(net(torch.tensor([[2., 4.]])))输出:
tensor([[3.]], grad_fn=<AddmmBackward0>)0.5×2 + 0.5×4 + 0 = 3,手工初始化生效。初始化通常包在 torch.no_grad() 里,因为初始化不是训练、不需要梯度。
动手实践
- 创建
nn.Linear(3, 2),打印weight.shape、bias.shape、weight.requires_grad;输入一个(5, 3)张量,验证输出形状。 - 用
nn.Sequential搭一个Linear(4, 8) → ReLU → Linear(8, 2),打印结构,输入(2, 4)张量验证输出形状。 - 自定义一个
ScaleNet(一个可学习缩放因子nn.Parameter,forward返回w * x),打印named_parameters()。 - 对第 7.2 节的
Net,手动调用net.forward(3.0)和net(3.0),观察结果;说出为什么推荐用net(x)。 - 用
torch.no_grad()把nn.Linear(2, 1)的权重填成 1、偏置填成 0,验证net(x)等价于求和。
常见错误
| 错误写法 | 现象 | 原因 |
|---|---|---|
__init__ 里忘了 super().__init__() | AttributeError 或参数不被登记 | 父类初始化负责注册子模块与参数 |
把参数存成普通 torch.tensor 而不是 nn.Parameter | parameters() 里没有它,优化器不更新 | 要声明为 nn.Parameter |
调 model.forward(x) 代替 model(x) | 大部分情况能跑,但钩子等功能失效 | 约定用 model(x) |
forward 忘记 return | 输出是 None,后续报错 | forward 必须返回张量 |
输入维度与 in_features 不匹配 | RuntimeError: mat1 and mat2 shapes cannot be multiplied | 检查输入最后一维是否等于 in_features |
把 nn.ReLU 当函数调用 nn.ReLU(x) | TypeError | 先实例化 relu = nn.ReLU() 再 relu(x);或直接用 torch.relu(x) |
章末练习
基础
- 创建
nn.Linear(2, 1),打印它的weight和bias形状,并说明 weight 的形状为什么是(1, 2)。 - 用
nn.Sequential搭Linear(3, 5) → ReLU → Linear(5, 1),输入(4, 3)张量,验证输出形状是(4, 1)。 - 自定义
AddBias(nn.Module)(只有一个可学习偏置,forward返回x + bias),打印它的参数列表。
提高
- 解释
nn.Parameter与普通torch.tensor(..., requires_grad=True)的区别(提示:参数登记与parameters())。 - 用
nn.Sequential搭一个 3 层 MLP(2 → 16 → 16 → 1,ReLU 激活),统计len(list(net.parameters()))并说明每个参数张量的形状。
挑战
- 不运行代码,预测
nn.Linear(2, 1)输出(3, 2)输入时的形状;再预测nn.Sequential(Linear(2,4), ReLU(), Linear(4,1))对(5, 2)输入的输出形状,运行验证。 - 手写一个不依赖
nn的Linear等价实现(用nn.Parameter存weight、bias,forward用@),并验证它与nn.Linear输出一致(设置相同权重后比较)。
章末自测
每题选择一个最佳答案。本书不附答案:完成后交由老师或 AI 老师批改讲解。
- PyTorch 所有网络组件的基类是?
- A.
nn.Tensor - B.
nn.Module - C.
nn.Network - D.
torch.Model
- A.
nn.Linear(2, 1).weight.shape是?- A.
torch.Size([2, 1]) - B.
torch.Size([1, 2]) - C.
torch.Size([2]) - D.
torch.Size([1])
- A.
nn.Linear的前向计算等价于?- A.
x * w - B.
x @ w + b - C.
w @ x + b - D.
x + b
- A.
- 自定义模块的
__init__里,第一行应该写?- A.
self.init = True - B.
super().__init__() - C.
nn.Module.__init__(self, x) - D. 什么都不用写
- A.
- 要把一个张量声明为模块的可学习参数,应该用?
- A.
torch.tensor(1.0) - B.
nn.Parameter(torch.tensor(1.0)) - C.
nn.Parameter(1.0) - D.
torch.parameter(1.0)
- A.
- 前向传播的正确调用方式是?
- A.
model.forward(x) - B.
model(x) - C.
model.call(x) - D.
model.run(x)
- A.
nn.Sequential(Linear(2, 4), ReLU(), Linear(4, 1))对(5, 2)输入的输出形状是?- A.
torch.Size([5, 1]) - B.
torch.Size([1, 5]) - C.
torch.Size([5, 4]) - D.
torch.Size([5, 2])
- A.
- 激活函数(如 ReLU)的作用是?
- A. 让输出变成整数
- B. 引入非线性,否则多层等价于单层线性变换
- C. 加速训练的唯一手段
- D. 防止过拟合的唯一手段
nn.Linear的参数默认?- A.
requires_grad=False - B.
requires_grad=True - C. 由
forward决定 - D. 创建后不能改
- A.
list(net.parameters())返回的是?- A. 网络中所有子模块
- B. 网络中所有参数张量
- C. 网络的名字
- D. 网络的结构字符串
