模型构造
-
- 继承`Module`类来构造模型
-
- 导包
- 继承Module类构造MLP
- 测试MLP
- [2. 实现MySequential类](#2. 实现MySequential类)
- 测试MySequential
- [3. 构造复杂模型FancyMLP](#3. 构造复杂模型FancyMLP)
-
- python知识拓展(三种方法)
-
- [**实例方法(Instance Method)**](#实例方法(Instance Method))
- [**类方法(Class Method)**](#类方法(Class Method))
- [**静态方法(Static Method)**](#静态方法(Static Method))
- **调用方式的区别**
- 测试FancyMLP
- [4. 嵌套调用模型](#4. 嵌套调用模型)
- 测试嵌套模型
让我们回顾一下在回顾一下在"多层感知机的实现"一节中含单隐藏层的多层感知机的实现方法。我们首先构造Sequential实例,然后依次添加两个全连接层。其中第一层的输出大小为256,即隐藏层单元个数是256;第二层的输出大小为10,即输出层单元个数是10。我们在上一章的其他节中也使用了Sequential类构造模型。这里我们介绍另外一种基于Module类的模型构造方法:它让模型构造更加灵活。
继承Module类来构造模型
Module类是torch.nn模块里提供的一个模型构造类,我们可以继承它来定义我们想要的模型。下面继承Module类构造本节开头提到的多层感知机。这里定义的MLP类重载了Module类的__init__函数和forward函数。它们分别用于创建模型参数和定义前向计算。前向计算也即正向传播。
导包
python
import torch
from torch import nn
import torch.nn.functional as F
继承Module类构造MLP
Python的__init__ 起着构造器的作用,这里的 super(MLP, self).__init__(**kwargs)不能省略相当于调用父类的构造函数。nn.Module.__init__() 负责初始化:参数管理系统 _parameters, _buffers,子模块管理系统 _modules,钩子系统 :前向/反向传播钩子,状态标志:training 模式等管理神经网络所需的所有基础架构。所以必须调用父类的构造器。super(MLP, self).__init__(**kwargs)是老版本python的写法,实际上更推荐使用super().__init__(**kwargs)。
python
class MLP(nn.Module):
# 声明带有模型参数的层,这里声明了两个全连接层
def __init__(self, **kwargs):
# 调用MLP父类Module的构造函数来进行必要的初始化
#这里也可以改写成super().__init__(**kwargs)
super(MLP, self).__init__(**kwargs)
self.hidden = nn.Linear(20, 256) # 隐藏层
self.output = nn.Linear(256, 10) # 输出层
# 定义模型的前向计算
def forward(self, x):
return self.output(F.relu(self.hidden(x)))
测试MLP
python
X = torch.randn(2, 20)
net = MLP()
print(net(X))
2. 实现MySequential类
Module类是一个通用的部件。事实上,Sequential类继承自Module类。当模型的前向计算为简单串联各个层的计算时,可以通过更加简单的方式定义模型。这正是Sequential类的目的:它提供add_module函数来逐一添加串联的Module子类实例,而模型的前向计算就是将这些实例按添加的顺序逐一计算。通俗的来讲nn.Sequential本质上是继承了nn.module然后调用了父类的add_module方法,然后在 forward 中按顺序调用所有子模块。
python
class MySequential(nn.Module):
def __init__(self, *args):
super(MySequential, self).__init__()
for idx, module in enumerate(args):
# 这里,module是Module子类实例
# 我们把它保存在'Module'类型的成员变量中
self.add_module(str(idx), module)
def forward(self, x):
# 按添加顺序遍历所有子模块
for module in self.children():
x = module(x)
return x
测试MySequential
我们用MySequential类来实现前面描述的MLP类,并使用随机初始化的模型做一次前向计算。
python
net = MySequential(
nn.Linear(20, 256),
nn.ReLU(),
nn.Linear(256, 10)
)
print(net(X))
3. 构造复杂模型FancyMLP
虽然Sequential类可以使模型构造更加简单,且不需要定义forward函数,但直接继承Module类可以极大地拓展模型构造的灵活性。下面我们构造一个稍微复杂点的网络FancyMLP。在这个网络中,我们通过register_buffer函数创建训练中不被迭代的参数,即常数参数。在前向计算中,除了使用创建的常数参数外,我们还使用Tensor的函数和Python的控制流,并多次调用相同的层。
python
class FancyMLP(nn.Module):
def __init__(self, **kwargs):
super().__init__(**kwargs)
# 使用register_buffer创建训练中不被迭代的参数(常数参数)
self.register_buffer('rand_weight', torch.rand(20, 20))
self.dense = nn.Linear(20, 20)
def forward(self, x):
x = self.dense(x)
# 使用创建的常数参数,以及torch的relu函数和matmul函数
x = F.relu(torch.mm(x, self.rand_weight) + 1)
# 复用全连接层。等价于两个全连接层共享参数
x = self.dense(x)
# 控制流
while x.norm() > 1:
x /= 2
if x.norm() < 0.8:
x *= 10
return x.sum()
python知识拓展(三种方法)
在Python中,类里面定义的方法分为三种:
python
class Example:
def instance_method(self, x): # 实例方法
return f"实例方法: {self} 收到 {x}"
@classmethod
def class_method(cls, x): # 类方法
return f"类方法: {cls} 收到 {x}"
@staticmethod
def static_method(x): # 静态方法
return f"静态方法: 收到 {x}"
实例方法(Instance Method)
- 第一个参数必须是
self - 通过实例调用
- 可以访问实例属性和其他实例方法
python
class MLP(nn.Module):
def __init__(self): # ✅ 实例方法
super().__init__()
self.layer = nn.Linear(10, 5)
def forward(self, x): # ✅ 实例方法
return self.layer(x) # 通过self访问实例属性
# 使用
model = MLP() # 创建实例
output = model.forward(torch.randn(2, 10)) # 实例调用方法
类方法(Class Method)
- 第一个参数必须是
cls - 使用
@classmethod装饰器 - 通过类或实例调用
- 可以访问类属性,但不能访问实例属性
python
class ModelFactory:
model_count = 0 # 类属性
@classmethod
def create_model(cls, input_size, output_size):
cls.model_count += 1
return f"创建第{cls.model_count}个模型: {input_size}→{output_size}"
# 使用
print(ModelFactory.create_model(10, 5)) # 通过类调用
print(ModelFactory.create_model(20, 3)) # 通过类调用
静态方法(Static Method)
- 没有特殊的第一个参数
- 使用
@staticmethod装饰器 - 通过类或实例调用
- 不能访问实例属性或类属性
python
class MathUtils:
@staticmethod
def relu(x):
return max(0, x) # 纯函数,不依赖实例或类状态
@staticmethod
def normalize(x):
return (x - x.mean()) / x.std()
# 使用
print(MathUtils.relu(-5)) # 0
print(MathUtils.relu(5)) # 5
调用方式的区别
python
class TestClass:
def instance_method(self):
return f"实例方法, self={self}"
@classmethod
def class_method(cls):
return f"类方法, cls={cls}"
@staticmethod
def static_method():
return "静态方法"
# 创建实例
obj = TestClass()
# 调用方式对比
print(obj.instance_method()) # ✅ 实例调用实例方法
print(TestClass.instance_method(obj)) # ✅ 类调用实例方法(不常见)
print(obj.class_method()) # ✅ 实例调用类方法
print(TestClass.class_method()) # ✅ 类调用类方法
print(obj.static_method()) # ✅ 实例调用静态方法
print(TestClass.static_method()) # ✅ 类调用静态方法
| 方法类型 | 第一个参数 | 装饰器 | 可访问的数据 | PyTorch中使用频率 |
|---|---|---|---|---|
| 实例方法 | self |
无 | 实例属性、类属性 | ⭐⭐⭐⭐⭐ |
| 类方法 | cls |
@classmethod |
类属性 | ⭐⭐ |
| 静态方法 | 无 | @staticmethod |
无 | ⭐⭐ |
所以可以这样理解没有装饰器的一定是实例方法,并且需要第一个参数是self。三种方法都可以通过类和实例(实例等价于对象)调用。
测试FancyMLP
在这个FancyMLP模型中,我们使用了常数权重rand_weight(注意它不是模型参数)、做了矩阵乘法操作(torch.mm)并重复使用了相同的Dense层。下面我们来测试该模型的随机初始化和前向计算。
python
net = FancyMLP()
print(net(X))
4. 嵌套调用模型
因为FancyMLP和Sequential类都是Module类的子类,所以我们可以嵌套调用它们。
python
class NestMLP(nn.Module):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.net = nn.Sequential(
nn.Linear(20, 64),
nn.ReLU(),
nn.Linear(64, 32),
nn.ReLU()
)
self.dense = nn.Linear(32, 16)
def forward(self, x):
return self.dense(self.net(x))
测试嵌套模型
python
net = nn.Sequential(
NestMLP(),
nn.Linear(16, 20),
FancyMLP()
)
print(net(X))
扩展知识
net(X)的调用流程:
net(X)→ 调用net.__call__(X)net.__call__(X)→ 执行前置处理 → 调用net.forward(X)→ 执行后置处理- 返回结果
nn.Module 的 __call__ 方法大致是这样的:
python
class Module:
def __call__(self, *input, **kwargs):
# 前向传播前的处理
for hook in self._forward_pre_hooks.values():
input = hook(self, input)
# 调用用户定义的forward方法
result = self.forward(*input, **kwargs)
# 前向传播后的处理
for hook in self._forward_hooks.values():
result = hook(self, input, result)
return result
__call__方法提供的重要功能:
钩子执行 - 前向/后向传播的钩子函数
训练模式检查 - 自动处理dropout、batchnorm等
梯度计算 - 设置是否需要梯度
错误处理 - 统一的异常处理