欢迎光临
我们一直在努力

07-动态图基础:像写 Python 一样构建模型

概述

前面几篇文章已经讲过 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、自动微分和参数管理能力。

赞(0)
未经允许不得转载:171主机测评 » 07-动态图基础:像写 Python 一样构建模型
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址