概述
前面几篇文章已经讲过 Tensor、数学操作和自动微分。现在开始进入模型构建方式:动态图。
PaddlePaddle 从 2.0 版本开始默认开启动态图开发模式。在动态图模式下,每执行一个 Paddle API,就会立即得到结果;你可以像写普通 Python 程序一样写模型、打印中间变量、使用 if、for、while,也可以用断点调试。
这和早期静态图写法的心智模型不同。动态图不要求你先完整声明一个计算图再执行,而是:
写一行
|
执行一行
|
立即得到 Tensor 结果
|
继续写后续 Python 逻辑
这篇文章重点解决这些问题:
- 动态图和静态图的核心区别是什么?
- 为什么动态图更像普通 Python?
- 如何用 Python 控制流组织 Tensor 计算?
- 如何用 paddle.nn.Layer 写一个可复用模型?
- __init__ 和 forward 分别应该写什么?
- 如何查看模型参数、切换训练/评估模式?
- 动态图训练循环应该长什么样?
- 动态图代码调试时应该注意哪些坑?
读完这篇文章,你应该能用动态图方式写出一个可训练的小模型,并理解 Paddle 为什么能“像写 Python 一样构建模型”。
核心认知:动态图是立即执行
先看一个最小示例:
import paddle
a = paddle.randn([4, 2])
b = paddle.to_tensor([1.0, 2.0])
c = a + b
d = paddle.matmul(a, b)
print("a:", a)
print("b:", b)
print("c:", c)
print("d:", d)
这段代码的特点是:每一步都会立刻执行。
a = paddle.randn([4, 2]) 立即生成 Tensor
b = paddle.to_tensor(…) 立即创建 Tensor
c = a + b 立即完成广播加法
d = paddle.matmul(a, b) 立即完成矩阵乘法
你可以直接打印 a、b、c、d,看到它们的数值、shape、dtype、place 和 stop_gradient。
这就是动态图最重要的开发体验:
Tensor 不是未来才会计算的符号
Tensor 是当前已经算出来的结果
因此,动态图非常适合:
- 初学者理解模型计算过程。
- 打印中间 Tensor。
- 快速验证模型结构。
- 使用 Python 调试器定位问题。
- 写包含条件判断、循环和分支的模型逻辑。
动态图与静态图:先理解开发体验差异
动态图和静态图不是“新旧好坏”的关系,而是侧重点不同。
| 动态图 | 边写边执行 | 灵活、直观、易调试 | 入门学习、模型开发、实验验证 |
| 静态图 | 先构建图再整体执行 | 便于图优化、部署、编译加速 | 推理部署、性能优化、生产环境 |
可以这样理解:
动态图:
Python 控制流程就是模型执行流程
静态图:
先把计算过程描述成图,再交给执行系统优化和运行
Paddle 支持动静统一。日常开发时,可以先用动态图写出清晰正确的模型;当需要部署或性能优化时,再考虑动转静、推理导出等能力。
动态图解决“好写、好看、好调试”,静态图更偏“好优化、好部署、好执行”。
Python 控制流:if、for、while 可以直接用
动态图最直观的优势,就是可以直接使用 Python 控制流。
使用 if 条件判断
import paddle
x = paddle.to_tensor([1.0, –2.0, 3.0])
if paddle.mean(x).item() > 0:
y = x * 2
else:
y = x – 2
print(y)
这里 paddle.mean(x) 会立刻得到一个 Tensor,.item() 把 0 维 Tensor 转成 Python 标量,因此可以进入普通 Python if 判断。
注意:如果条件本身是 Tensor,通常要转换成 Python bool 或使用 Paddle 条件类 API。入门阶段,用 .item() 取单个标量最直观。
使用 for 循环
import paddle
x = paddle.to_tensor([1.0, 2.0, 3.0])
y = x
for i in range(3):
y = y * 2 + i
print("step:", i, "y:", y)
每一次循环都会立刻执行 Tensor 运算,y 也会被更新。
使用 while 循环
import paddle
x = paddle.to_tensor([1.0])
step = 0
while x.item() < 10:
x = x * 1.5
step += 1
print("step:", step, "x:", x)
这类写法在动态图中非常自然。只要你的逻辑能用 Python 表达,就可以把 Paddle Tensor 运算嵌进去。
动态图中的计算图:不是没有图,而是边执行边记录
动态图虽然“立即执行”,但自动微分仍然需要知道计算关系。因此,更准确的说法是:
动态图不是没有计算图
动态图是在前向执行过程中动态记录计算关系
示例:
import paddle
x = paddle.to_tensor([1.0, 2.0, 3.0], stop_gradient=False)
y = x ** 2
loss = paddle.sum(y)
loss.backward()
print("x.grad:", x.grad)
前向阶段:
x -> square -> y -> sum -> loss
调用 loss.backward() 时,Paddle 沿着这条动态记录下来的计算路径反向计算梯度。
这也是动态图能同时做到“像 Python 一样执行”和“支持自动微分”的关键。
用函数组织动态图代码
在写模型类之前,可以先用普通 Python 函数组织计算。
import paddle
def linear_forward(x, w, b):
return paddle.matmul(x, w) + b
x = paddle.randn([4, 3])
w = paddle.randn([3, 2], dtype="float32")
b = paddle.zeros([2], dtype="float32")
y = linear_forward(x, w, b)
print(y)
这段代码没有任何特殊语法。函数输入是 Tensor,输出也是 Tensor。
但是普通函数有一个问题:它不会自动管理参数。真实模型里,我们希望模型自己持有参数,并能通过 model.parameters() 交给优化器。这就需要 paddle.nn.Layer。
nn.Layer:动态图模型的基本封装方式
Paddle 中自定义模型通常继承 paddle.nn.Layer。
一个最小模型:
import paddle
import paddle.nn as nn
class LinearModel(nn.Layer):
def __init__(self):
super().__init__()
self.linear = nn.Linear(in_features=3, out_features=2)
def forward(self, x):
y = self.linear(x)
return y
model = LinearModel()
x = paddle.randn([4, 3])
y = model(x)
print(y)
这里有两个关键方法:
| __init__ | 定义子层、参数、固定组件 | 创建模型时调用一次 |
| forward | 定义输入到输出的计算过程 | 每次 model(x) 时调用 |
model(x) 会自动调用 model.forward(x)。日常使用中推荐写:
y = model(x)
而不是直接写:
y = model.forward(x)
因为 model(x) 会经过 Layer 内部的调用逻辑,更符合框架习惯。
__init__ 放“模型有哪些层”,forward 放“数据怎么流过这些层”。
一个两层 MLP:把 Layer 写得更像真实模型
下面写一个两层多层感知机:
import paddle
import paddle.nn as nn
import paddle.nn.functional as F
class MLP(nn.Layer):
def __init__(self, input_dim, hidden_dim, num_classes):
super().__init__()
self.fc1 = nn.Linear(input_dim, hidden_dim)
self.fc2 = nn.Linear(hidden_dim, num_classes)
def forward(self, x):
h = self.fc1(x)
h = F.relu(h)
logits = self.fc2(h)
return logits
model = MLP(input_dim=4, hidden_dim=8, num_classes=3)
x = paddle.randn([5, 4])
logits = model(x)
print("logits shape:", logits.shape)
print(logits)
形状变化:
x: [5, 4]
fc1: [5, 4] -> [5, 8]
relu: [5, 8] -> [5, 8]
fc2: [5, 8] -> [5, 3]
logits: [5, 3]
这已经是一个真实分类模型的基本结构。后续只要接上损失函数和优化器,就可以训练。
查看模型参数:parameters 和 named_parameters
nn.Layer 会自动管理子层中的参数。
model = MLP(input_dim=4, hidden_dim=8, num_classes=3)
for param in model.parameters():
print(param.shape, param.dtype, param.stop_gradient)
如果想看参数名字:
for name, param in model.named_parameters():
print(name, param.shape)
常见输出类似:
fc1.weight [4, 8]
fc1.bias [8]
fc2.weight [8, 3]
fc2.bias [3]
参数默认通常是 stop_gradient=False,因此会参与梯度计算。
这就是为什么优化器可以这样写:
optimizer = paddle.optimizer.SGD(
learning_rate=0.01,
parameters=model.parameters(),
)
优化器不需要你手动把每个 weight、bias 都列出来,Layer 已经帮你组织好了。
子层注册:为什么要写成 self.fc1
在 __init__ 中,应该把子层赋值给 self:
self.fc1 = nn.Linear(input_dim, hidden_dim)
不要写成局部变量:
fc1 = nn.Linear(input_dim, hidden_dim)
原因是:只有注册到 self 上的子层,才会被 Layer 管理,参数才会出现在 model.parameters() 中。
错误示例:
import paddle
import paddle.nn as nn
class BadModel(nn.Layer):
def __init__(self):
super().__init__()
fc = nn.Linear(4, 3)
def forward(self, x):
return x
model = BadModel()
print(list(model.parameters()))
这个模型并没有把 fc 注册为子层,所以参数列表是空的。
正确写法:
class GoodModel(nn.Layer):
def __init__(self):
super().__init__()
self.fc = nn.Linear(4, 3)
def forward(self, x):
return self.fc(x)
希望被框架管理的层和参数,要挂到 self 上。
动态图里的 Python 分支:根据数据选择路径
动态图允许在 forward 中使用 Python 条件分支。
import paddle
import paddle.nn as nn
import paddle.nn.functional as F
class BranchModel(nn.Layer):
def __init__(self):
super().__init__()
self.small = nn.Linear(4, 4)
self.large = nn.Linear(4, 4)
self.out = nn.Linear(4, 2)
def forward(self, x):
if paddle.mean(x).item() > 0:
h = F.relu(self.large(x))
else:
h = F.relu(self.small(x))
return self.out(h)
model = BranchModel()
x = paddle.randn([3, 4])
y = model(x)
print(y)
这段代码会根据输入均值选择不同分支。
这种能力在研究型模型中很有用,例如:
- 根据输入长度选择不同处理路径。
- 根据训练阶段启用不同模块。
- 根据采样结果决定下一步计算。
- 写一些动态路由或条件执行逻辑。
不过也要注意:如果后续要动转静或部署,过于复杂的 Python 控制流可能需要额外处理。入门阶段先用动态图保证逻辑正确,再考虑部署限制。
动态图里的循环:重复使用同一层
动态图中可以在 forward 里写循环。
import paddle
import paddle.nn as nn
import paddle.nn.functional as F
class RecurrentStyleBlock(nn.Layer):
def __init__(self, dim, steps):
super().__init__()
self.fc = nn.Linear(dim, dim)
self.steps = steps
def forward(self, x):
h = x
for _ in range(self.steps):
h = F.relu(self.fc(h))
return h
model = RecurrentStyleBlock(dim=4, steps=3)
x = paddle.randn([2, 4])
y = model(x)
print(y.shape)
这里 self.fc 在循环中被重复使用,权重是共享的。
形状关系:
x: [2, 4]
每轮 fc: [2, 4] -> [2, 4]
循环 3 次
y: [2, 4]
这类写法可以帮助理解循环网络、迭代模块、共享权重结构等概念。
共享权重:同一个 Layer 调用多次
在 Paddle 中,同一个子层对象调用多次,使用的是同一组参数。
import paddle
import paddle.nn as nn
class SharedWeightModel(nn.Layer):
def __init__(self):
super().__init__()
self.proj = nn.Linear(4, 4)
def forward(self, x1, x2):
y1 = self.proj(x1)
y2 = self.proj(x2)
return y1 + y2
model = SharedWeightModel()
x1 = paddle.randn([2, 4])
x2 = paddle.randn([2, 4])
y = model(x1, x2)
print(y.shape)
这里 x1 和 x2 都经过 self.proj,因此共享同一个 weight 和 bias。
共享权重常见于:
- Siamese 网络。
- 文本匹配模型。
- 图像相似度模型。
- 多输入但同编码器的结构。
- 循环展开中的重复模块。
如果你想要两套独立参数,就应该创建两个不同子层:
self.proj1 = nn.Linear(4, 4)
self.proj2 = nn.Linear(4, 4)
train 和 eval:切换训练与评估模式
Layer 有 train() 和 eval() 方法,用于切换模式。
model.train()
表示训练模式。
model.eval()
表示评估模式。
为什么需要模式切换?因为有些层在训练和评估时行为不同,例如:
- Dropout:训练时随机丢弃部分激活,评估时通常关闭随机丢弃。
- BatchNorm:训练时使用当前 batch 统计量,评估时使用累计统计量。
示例:
import paddle
import paddle.nn as nn
class DropoutModel(nn.Layer):
def __init__(self):
super().__init__()
self.fc = nn.Linear(4, 4)
self.dropout = nn.Dropout(p=0.5)
def forward(self, x):
x = self.fc(x)
x = self.dropout(x)
return x
model = DropoutModel()
x = paddle.ones([2, 4])
model.train()
y_train = model(x)
model.eval()
y_eval = model(x)
print("train:", y_train)
print("eval:", y_eval)
训练时和评估时输出可能不同,这不是错误,而是层本身的设计。
训练前用 model.train(),验证和推理前用 model.eval()。
no_grad:评估和推理时关闭梯度记录
评估和推理时通常不需要梯度。可以使用:
with paddle.no_grad():
y = model(x)
完整示例:
model.eval()
with paddle.no_grad():
logits = model(x)
pred = paddle.argmax(logits, axis=1)
print(pred)
这样做有几个好处:
- 减少内存占用。
- 不构建反向传播所需的计算图。
- 避免不必要的梯度记录。
- 让代码语义更清晰。
训练阶段不要把前向和 loss 放进 no_grad,否则梯度会断掉。
动态图训练循环:最小完整流程
下面用动态图写一个完整的小训练循环。
任务:随机生成二分类数据,用 MLP 拟合。
import paddle
import paddle.nn as nn
import paddle.nn.functional as F
class TinyClassifier(nn.Layer):
def __init__(self, input_dim, hidden_dim, num_classes):
super().__init__()
self.fc1 = nn.Linear(input_dim, hidden_dim)
self.fc2 = nn.Linear(hidden_dim, num_classes)
def forward(self, x):
x = self.fc1(x)
x = F.relu(x)
x = self.fc2(x)
return x
paddle.seed(2026)
x = paddle.randn([64, 2], dtype="float32")
labels = (x[:, 0] + x[:, 1] > 0).astype("int64")
model = TinyClassifier(input_dim=2, hidden_dim=8, num_classes=2)
optimizer = paddle.optimizer.SGD(
learning_rate=0.1,
parameters=model.parameters(),
)
for step in range(50):
model.train()
logits = model(x)
loss = F.cross_entropy(logits, labels)
loss.backward()
optimizer.step()
optimizer.clear_grad()
if step % 10 == 0:
pred = paddle.argmax(logits, axis=1)
acc = paddle.mean((pred == labels).astype("float32"))
print("step:", step, "loss:", float(loss.numpy()), "acc:", float(acc.numpy()))
这就是动态图训练循环的基本骨架:
准备数据
|
model = 自定义 Layer
|
optimizer = 优化器
|
for step in range(…):
logits = model(x)
loss = 损失函数(logits, labels)
loss.backward()
optimizer.step()
optimizer.clear_grad()
这个训练循环没有使用 paddle.Model 高层 API,所有步骤都是显式写出来的,因此特别适合理解底层训练机制。
动态图调试:直接打印中间 Tensor
动态图下可以直接在 forward 中打印中间结果。
class DebugMLP(nn.Layer):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(4, 8)
self.fc2 = nn.Linear(8, 3)
def forward(self, x):
print("input shape:", x.shape)
h = self.fc1(x)
print("after fc1:", h.shape)
h = F.relu(h)
out = self.fc2(h)
print("output shape:", out.shape)
return out
这是动态图非常实用的地方。模型报 shape 错误时,可以逐层打印:
print(tensor.shape)
print(tensor.dtype)
print(tensor.place)
print(tensor.stop_gradient)
如果输出里出现 NaN,也可以检查:
print("has nan:", paddle.any(paddle.isnan(tensor)).item())
print("has inf:", paddle.any(paddle.isinf(tensor)).item())
调试建议:
- 先用很小的 batch 跑通。
- 先打印 shape,再看数值。
- 先跑一轮前向,再接 loss 和 backward。
- 先用随机数据验证模型结构,再接真实数据。
paddle.summary:查看模型结构和参数量
官方模型组网文档中提到,Paddle 提供 paddle.summary() 方便查看网络结构、每层输入输出 shape 和参数信息。
示例:
import paddle
model = TinyClassifier(input_dim=2, hidden_dim=8, num_classes=2)
paddle.summary(model, input_size=(1, 2))
它可以帮助你快速确认:
- 每层输出 shape 是否符合预期。
- 参数量是否异常。
- 模型结构是否按 forward 正确连接。
如果 summary 报错,通常说明模型前向本身存在 shape 或 dtype 问题。此时先用普通输入 model(x) 跑通,再使用 summary。
动态图与自动微分:训练时计算图每轮重新构建
动态图训练中,每一次前向都会根据当前 Python 执行路径构建一条新的计算记录。
第 1 轮:
model(x) -> loss -> backward -> 清理
第 2 轮:
model(x) -> loss -> backward -> 清理
第 3 轮:
model(x) -> loss -> backward -> 清理
这意味着:
- 每轮都要重新计算 logits 和 loss。
- 不要试图复用上一轮的 loss 做 backward。
- 分支和循环可以每轮根据输入走不同路径。
- backward() 后计算图通常会被释放,下一轮重新构建。
这也是动态图灵活性的来源。
常见错误
错误一:把子层写成局部变量
错误写法:
class BadModel(nn.Layer):
def __init__(self):
super().__init__()
fc = nn.Linear(4, 2)
def forward(self, x):
return x
fc 没有注册到 self,优化器拿不到它的参数。
正确写法:
class GoodModel(nn.Layer):
def __init__(self):
super().__init__()
self.fc = nn.Linear(4, 2)
def forward(self, x):
return self.fc(x)
错误二:在 forward 里每次新建参数层
错误写法:
class BadModel(nn.Layer):
def __init__(self):
super().__init__()
def forward(self, x):
fc = nn.Linear(4, 2)
return fc(x)
每次 forward 都创建新层,新参数不会被稳定训练。
正确做法:层在 __init__ 中创建,forward 中调用。
错误三:忘记调用 optimizer.clear_grad
动态图训练中梯度会累积。如果忘记清理:
loss.backward()
optimizer.step()
下一轮梯度会叠加旧梯度。正确写法:
loss.backward()
optimizer.step()
optimizer.clear_grad()
错误四:评估时忘记 model.eval
包含 Dropout 或 BatchNorm 的模型,训练和评估行为不同。验证集和推理前应该:
model.eval()
训练前再切回:
model.train()
错误五:训练前向被 no_grad 包住
错误写法:
with paddle.no_grad():
logits = model(x)
loss = F.cross_entropy(logits, labels)
loss.backward()
这样会导致没有可用的反向图。训练时不要把前向和 loss 包进 no_grad。
错误六:用 .item() 断开了 Tensor 计算
.item() 会把 0 维 Tensor 转成 Python 标量,适合做日志或控制流判断,但不能用于需要回传梯度的核心 loss 计算。
可以用于:
print(float(loss.numpy()))
或:
if paddle.mean(x).item() > 0:
...
不要把需要训练的计算改成 Python 数字再继续算 loss。
错误七:动态图能跑,不代表一定能直接部署
动态图代码可以写复杂 Python 控制流,但部署或动转静时可能受支持语法限制。如果你的目标是推理部署,建议:
- 先用动态图完成开发和验证。
- 避免过度依赖难以转换的 Python 对象操作。
- 需要部署时再学习动转静和推理导出。
总结
这一篇围绕 Paddle 动态图讲了几个核心点:
- Paddle 2.0 起默认开启动态图开发模式。
- 动态图是立即执行,API 调用后能直接得到 Tensor 结果。
- 动态图可以自然使用 Python 的 if、for、while。
- 自动微分会在动态图前向执行过程中记录计算关系。
- 自定义模型通常继承 paddle.nn.Layer。
- __init__ 中定义子层,forward 中定义计算流程。
- 子层要写成 self.xxx,才能被模型管理和优化器更新。
- 同一个子层调用多次表示共享权重。
- 训练用 model.train(),评估和推理用 model.eval()。
- 评估和推理时通常配合 paddle.no_grad()。
- 动态图适合调试,但部署前还需要关注动转静和推理导出。
如果只能记住一句话,那就是:
动态图让你用普通 Python 的写法组织深度学习计算,同时保留 Paddle 的 Tensor、自动微分和参数管理能力。



