概述
前面几篇文章已经讲过 Tensor、数学操作、自动微分和动态图。现在开始进入 paddle.nn:也就是用神经网络层来搭建模型。
如果只用 Tensor 和数学 API,也可以写出一个线性计算:
python
y = paddle.matmul(x, w) + b
但真实模型通常不建议一直手写 w 和 b。更常见的写法是:
python
linear = paddle.nn.Linear(4, 3)
y = linear(x)
这一层 Linear 会帮你管理权重、偏置、初始化、参数注册和前向调用。后续优化器也能直接通过 model.parameters() 找到这些可训练参数。
本篇文章重点讲三类最基础但最常用的组件:
paddle.nn.Linear:全连接层,完成线性变换。- 激活函数 :给网络引入非线性,例如
ReLU、Sigmoid、Tanh。 paddle.nn.Sequential:按顺序把多个层串起来。
读完这篇文章,你应该能看懂 Linear -> ReLU -> Linear 这种最基础神经网络结构,并能独立写出一个小型前馈网络。
背景:为什么不能只堆线性层
线性层的数学形式是:
Y=XW+b Y = XW + b Y=XW+b
如果连续堆多个线性层,但中间没有激活函数:
Y=((XW1+b1)W2+b2) Y = ((XW_1 + b_1)W_2 + b_2) Y=((XW1+b1)W2+b2)
它仍然可以合并成一个更大的线性变换:
Y=XW′+b′ Y = XW' + b' Y=XW′+b′
也就是说,多个线性层直接相连,本质上仍然只是线性模型。为了让神经网络拟合更复杂的非线性关系,需要在层与层之间加入激活函数。
典型结构:
text
输入 x
|
Linear
|
ReLU
|
Linear
|
输出 logits
这就是很多神经网络最基本的骨架。
准备代码:统一打印 shape 和参数
本文示例可以在 CPU 上运行。先准备两个工具函数:
python
import paddle
import paddle.nn as nn
import paddle.nn.functional as F
def show(name, tensor):
print(f"\n{name}")
print("-" * 60)
print(tensor)
print("shape:", tensor.shape)
print("dtype:", tensor.dtype)
print("stop_gradient:", tensor.stop_gradient)
def show_parameters(layer):
print("\nparameters")
print("-" * 60)
for name, param in layer.named_parameters():
print(name, param.shape, param.dtype, "stop_gradient=", param.stop_gradient)
paddle.seed(2026)
建议保存为:
text
paddle_nn_linear_activation_sequential.py
学习神经网络层时,不要只看输出值,重点看:
text
输入 shape -> 每一层输出 shape -> 参数 shape
这三件事一旦清楚,很多模型结构错误会很容易定位。
Linear 层:最基础的可学习变换
paddle.nn.Linear 是全连接层,也叫线性变换层。
官方 API 形式:
python
paddle.nn.Linear(
in_features,
out_features,
weight_attr=None,
bias_attr=None,
name=None,
)
它计算:
Out=XW+b Out = XW + b Out=XW+b
其中:
X是输入 Tensor。W是可学习权重。b是可学习偏置。in_features是输入最后一维大小。out_features是输出最后一维大小。
最小示例:
python
import paddle
import paddle.nn as nn
linear = nn.Linear(in_features=4, out_features=3)
x = paddle.randn([2, 4], dtype="float32")
y = linear(x)
show("x", x)
show("y", y)
show_parameters(linear)
形状关系:
text
x: [2, 4]
Linear(4, 3)
y: [2, 3]
参数形状通常是:
text
weight: [4, 3]
bias: [3]
这和公式完全对应:
text
[2, 4] @ [4, 3] + [3] -> [2, 3]
Linear 的输入形状:最后一维必须匹配 in_features
官方文档说明,Linear 接受形状为:
text
[batch_size, *, in_features]
的多维 Tensor,其中 * 可以是任意额外维度。输出形状为:
text
[batch_size, *, out_features]
也就是说,Linear 只变换最后一维。
二维输入:
python
linear = nn.Linear(4, 3)
x = paddle.randn([2, 4])
y = linear(x)
show("2D output", y)
形状:
text
[2, 4] -> [2, 3]
三维输入:
python
linear = nn.Linear(4, 3)
x = paddle.randn([2, 5, 4])
y = linear(x)
show("3D output", y)
形状:
text
[2, 5, 4] -> [2, 5, 3]
这里 [2, 5] 会被保留,最后一维 4 被映射成 3。
这个规则在 NLP 中非常常见。例如序列表示:
text
[batch_size, seq_len, hidden_size]
经过:
python
nn.Linear(hidden_size, num_classes)
会变成:
text
[batch_size, seq_len, num_classes]
Linear 不关心前面有多少维,只要求输入最后一维等于 in_features。
Linear 的权重和偏置:Parameter 与自动注册
Linear 内部包含可学习参数:
linear.weightlinear.bias
示例:
python
linear = nn.Linear(4, 3)
print(linear.weight)
print(linear.bias)
这些参数是 Parameter,默认通常 stop_gradient=False,会参与自动微分和优化器更新。
可以查看参数:
python
for name, param in linear.named_parameters():
print(name, param.shape, param.stop_gradient)
输出类似:
text
weight [4, 3] False
bias [3] False
这就是神经网络层比手写 Tensor 更方便的地方:
text
层自己持有参数
|
Layer 自动注册参数
|
model.parameters() 可以收集参数
|
optimizer 可以更新参数
如果你把 Linear 放到自定义 nn.Layer 的 self.fc 上,它的参数也会被外层模型管理。
bias_attr:什么时候不需要偏置
默认情况下,Linear 会创建偏置。
python
linear = nn.Linear(4, 3)
print(linear.bias is not None)
如果不想使用偏置,可以设置:
python
linear_no_bias = nn.Linear(4, 3, bias_attr=False)
print(linear_no_bias.bias)
什么时候可能不需要偏置?
- 后面紧跟某些归一化层,偏置可能被抵消。
- 想严格复现某个论文结构。
- 做简化实验,故意只观察权重影响。
入门阶段一般保留默认偏置即可。
自定义初始化:weight_attr 和 bias_attr
Linear 的 weight_attr 和 bias_attr 可以控制参数属性,例如初始化方式。
示例:把权重初始化为 0.5,偏置初始化为 1.0。
python
import paddle
import paddle.nn as nn
weight_attr = paddle.ParamAttr(
initializer=nn.initializer.Constant(value=0.5)
)
bias_attr = paddle.ParamAttr(
initializer=nn.initializer.Constant(value=1.0)
)
linear = nn.Linear(
in_features=2,
out_features=4,
weight_attr=weight_attr,
bias_attr=bias_attr,
)
print(linear.weight)
print(linear.bias)
这样做适合教学和调试,因为参数值可控。真实训练中,通常使用默认初始化或根据模型结构选择合适初始化。
常见初始化包括:
ConstantNormalUniformXavierUniformXavierNormalKaimingUniformKaimingNormal
后续讲模型训练和调参时,再深入初始化策略。当前阶段先知道:层参数不是凭空来的,它们有初始化方式,并在训练中被优化器更新。
手写 Linear 对比 nn.Linear
为了彻底理解 Linear,可以手写一次。
python
import paddle
import paddle.nn as nn
paddle.seed(2026)
x = paddle.randn([2, 4], dtype="float32")
linear = nn.Linear(4, 3)
y_layer = linear(x)
y_manual = paddle.matmul(x, linear.weight) + linear.bias
show("y_layer", y_layer)
show("y_manual", y_manual)
show("difference", y_layer - y_manual)
如果没有额外特殊设置,y_layer 和 y_manual 应该非常接近。
这说明:
text
nn.Linear 不是魔法
它主要封装了参数管理、初始化和前向计算
理解这一点后,后续学习更复杂的层,比如 Conv2D、Embedding、BatchNorm,也会更容易。
激活函数:让网络具备非线性表达能力
激活函数通常放在线性层之后:
text
Linear -> Activation -> Linear
如果没有激活函数,多层线性变换可以合并成一层线性变换。激活函数的作用是引入非线性,使网络能拟合更复杂的数据关系。
常见激活函数:
| 激活函数 | Paddle 层 | 常见特点 |
|---|---|---|
| ReLU | nn.ReLU() |
简单高效,深度学习中最常用 |
| Sigmoid | nn.Sigmoid() |
输出范围 (0, 1) |
| Tanh | nn.Tanh() |
输出范围 (-1, 1) |
| LeakyReLU | nn.LeakyReLU() |
负半轴保留小斜率 |
| GELU | nn.GELU() |
Transformer 等模型中常见 |
| Softmax | nn.Softmax() |
把分数转成概率分布 |
激活函数一般不会改变 Tensor 形状,只改变每个元素的数值。
ReLU:最常用的激活函数
ReLU 的公式是:
ReLU(x)=max(0,x) ReLU(x) = max(0, x) ReLU(x)=max(0,x)
Paddle 示例:
python
import paddle
import paddle.nn as nn
x = paddle.to_tensor([-2.0, -0.5, 0.0, 1.0, 3.0])
relu = nn.ReLU()
y = relu(x)
show("x", x)
show("relu(x)", y)
输出含义:
text
负数变成 0
0 保持 0
正数保持原值
ReLU 的优点:
- 计算简单。
- 收敛通常比较快。
- 对正数区域梯度稳定。
- 在 MLP、CNN 中非常常见。
缺点:
- 负半轴梯度为 0。
- 某些神经元可能长期输出 0,出现"死亡 ReLU"问题。
入门阶段优先掌握 ReLU,它是最常见的默认选择之一。
Sigmoid:把数值压到 0 到 1
Sigmoid 输出范围是 (0, 1):
Sigmoid(x)=11+e−x Sigmoid(x) = \frac{1}{1 + e^{-x}} Sigmoid(x)=1+e−x1
示例:
python
import paddle
import paddle.nn as nn
x = paddle.to_tensor([-5.0, -1.0, 0.0, 1.0, 5.0])
sigmoid = nn.Sigmoid()
y = sigmoid(x)
show("sigmoid(x)", y)
常见用途:
- 二分类概率输出。
- 多标签分类中每个标签独立概率。
- 门控结构,例如某些 RNN 或注意力机制。
注意:如果使用 Paddle 的某些二分类损失函数,可能要求输入是 logits,而不是已经过 Sigmoid 的概率。训练时要看损失函数要求,不要盲目先 Sigmoid。
Tanh:把数值压到 -1 到 1
Tanh 输出范围是 (-1, 1)。
python
import paddle
import paddle.nn as nn
x = paddle.to_tensor([-5.0, -1.0, 0.0, 1.0, 5.0])
tanh = nn.Tanh()
y = tanh(x)
show("tanh(x)", y)
常见用途:
- 某些循环神经网络结构。
- 需要输出带正负号且范围受限的中间表示。
- 传统神经网络中的隐藏层激活。
和 Sigmoid 类似,Tanh 在输入绝对值很大时容易饱和,梯度变小。因此现代前馈网络中,ReLU 及其变体更常见。
LeakyReLU 和 GELU:常见 ReLU 变体
LeakyReLU 在负半轴保留一个很小的斜率:
python
import paddle
import paddle.nn as nn
x = paddle.to_tensor([-2.0, -1.0, 0.0, 1.0, 2.0])
act = nn.LeakyReLU(negative_slope=0.1)
y = act(x)
show("leaky_relu(x)", y)
它可以缓解 ReLU 负半轴完全为 0 的问题。
GELU 在 Transformer、BERT、ERNIE 等模型里很常见:
python
import paddle
import paddle.nn as nn
x = paddle.linspace(-3, 3, 7)
gelu = nn.GELU()
y = gelu(x)
show("gelu(x)", y)
入门阶段不需要记住所有公式,先知道它们都是"非线性变换",用于增强模型表达能力。
Softmax:分类概率不是普通激活函数
Softmax 会把一组分数转成概率分布,常用于分类推理。
python
import paddle
import paddle.nn as nn
logits = paddle.to_tensor(
[[0.1, 2.0, 0.3], [3.0, 0.5, 0.2]],
dtype="float32",
)
softmax = nn.Softmax(axis=1)
prob = softmax(logits)
show("prob", prob)
show("prob sum", paddle.sum(prob, axis=1))
对于 [batch_size, num_classes] 的分类输出,通常在 axis=1 上做 Softmax。
重要提醒:
- 推理展示概率时可以用
Softmax。 - 训练分类模型时,不一定要手动加
Softmax。 - 交叉熵损失通常更适合直接接收 logits,具体以 API 要求为准。
典型分类训练结构是:
text
Linear -> ReLU -> Linear -> logits -> cross_entropy
而不是:
text
Linear -> ReLU -> Linear -> Softmax -> cross_entropy
这是初学者非常常见的误区。
Layer 形式和 functional 形式:nn.ReLU 与 F.relu
Paddle 中很多激活函数既有 Layer 形式,也有 functional 形式。
Layer 形式:
python
relu = nn.ReLU()
y = relu(x)
Functional 形式:
python
y = F.relu(x)
两者常见区别:
| 写法 | 特点 | 适合场景 |
|---|---|---|
nn.ReLU() |
是一个 Layer 对象,可以放进 Sequential |
组网结构清晰 |
F.relu(x) |
是函数调用,直接对 Tensor 计算 | forward 中临时使用 |
示例:
python
import paddle
import paddle.nn as nn
import paddle.nn.functional as F
x = paddle.to_tensor([-1.0, 0.0, 2.0])
relu_layer = nn.ReLU()
y1 = relu_layer(x)
y2 = F.relu(x)
show("y1", y1)
show("y2", y2)
一般建议:
- 用
Sequential搭模型时,使用nn.ReLU()。 - 在自定义
forward中临时写激活,使用F.relu(x)也很自然。
Sequential:把层按顺序串起来
当模型就是简单的顺序结构时,可以使用 paddle.nn.Sequential。
官方文档说明,Sequential 是顺序容器,子 Layer 会按构造函数参数顺序添加并执行。
示例:
python
import paddle
import paddle.nn as nn
model = nn.Sequential(
nn.Linear(4, 8),
nn.ReLU(),
nn.Linear(8, 3),
)
x = paddle.randn([5, 4])
logits = model(x)
show("logits", logits)
show_parameters(model)
执行顺序是:
text
x
|
Linear(4, 8)
|
ReLU()
|
Linear(8, 3)
|
logits
形状变化:
text
[5, 4] -> [5, 8] -> [5, 8] -> [5, 3]
Sequential 适合这类"从前到后一路执行"的模型。
Sequential 的命名写法
如果希望给每一层取名字,可以传入 (name, layer) 形式。
python
import paddle.nn as nn
model = nn.Sequential(
("fc1", nn.Linear(4, 8)),
("relu1", nn.ReLU()),
("fc2", nn.Linear(8, 3)),
)
print(model)
print(model["fc1"])
这样做的好处:
- 输出结构更清楚。
- 可以通过名字访问子层。
- 调试和保存结构时更容易阅读。
也可以通过索引访问:
python
print(model[0])
print(model[1])
对简单模型来说,普通顺序写法已经足够;对稍复杂模型,命名写法更清晰。
Sequential 的适用边界
Sequential 很方便,但不是所有模型都适合它。
适合 Sequential:
text
x -> layer1 -> layer2 -> layer3 -> output
不适合单纯使用 Sequential:
- 有多个输入。
- 有多个输出。
- 有跳连或残差连接。
- 有复杂条件分支。
- 中间结果要复用。
- 前向过程中需要动态循环。
例如残差结构:
text
y = layer(x) + x
这种结构通常更适合自定义 nn.Layer:
python
class ResidualBlock(nn.Layer):
def __init__(self, dim):
super().__init__()
self.net = nn.Sequential(
nn.Linear(dim, dim),
nn.ReLU(),
nn.Linear(dim, dim),
)
def forward(self, x):
return self.net(x) + x
顺序结构用 Sequential,有分支、复用、跳连时用自定义 Layer。
用 Layer 包装 Sequential:兼顾简洁和扩展性
真实项目中,经常把 Sequential 放进自定义 Layer 里。
python
import paddle
import paddle.nn as nn
class MLP(nn.Layer):
def __init__(self, input_dim, hidden_dim, num_classes):
super().__init__()
self.net = nn.Sequential(
nn.Linear(input_dim, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, num_classes),
)
def forward(self, x):
return self.net(x)
model = MLP(input_dim=4, hidden_dim=8, num_classes=3)
x = paddle.randn([5, 4])
logits = model(x)
show("logits", logits)
show_parameters(model)
这种写法的好处:
- 内部结构简单,用
Sequential表达。 - 外部仍然是一个标准
nn.Layer。 - 后续可以方便地添加额外逻辑。
- 参数能被
model.parameters()正常收集。
当模型从简单顺序结构逐步变复杂时,这种写法很自然。
完整示例:用 Linear、ReLU、Sequential 训练一个小分类器
下面写一个完整可运行的小例子:随机生成二维点,根据 x1 + x2 > 0 判断类别。
python
import paddle
import paddle.nn as nn
import paddle.nn.functional as F
paddle.seed(2026)
x = paddle.randn([128, 2], dtype="float32")
labels = (x[:, 0] + x[:, 1] > 0).astype("int64")
model = nn.Sequential(
nn.Linear(2, 8),
nn.ReLU(),
nn.Linear(8, 2),
)
optimizer = paddle.optimizer.SGD(
learning_rate=0.1,
parameters=model.parameters(),
)
for step in range(80):
logits = model(x)
loss = F.cross_entropy(logits, labels)
loss.backward()
optimizer.step()
optimizer.clear_grad()
if step % 20 == 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()),
)
这个例子串起了本篇文章的核心:
text
Linear(2, 8)
|
ReLU
|
Linear(8, 2)
|
logits
|
cross_entropy
|
backward + optimizer
注意最后一层没有加 Softmax,因为训练时直接把 logits 交给 F.cross_entropy。
如果要在训练后展示概率,可以这样写:
python
model.eval()
with paddle.no_grad():
logits = model(x[:5])
prob = F.softmax(logits, axis=1)
pred = paddle.argmax(prob, axis=1)
print(prob)
print(pred)
完整示例:对比有无激活函数
为了理解激活函数的重要性,可以对比两个模型。
没有激活函数:
python
model_linear_only = nn.Sequential(
nn.Linear(2, 8),
nn.Linear(8, 2),
)
有激活函数:
python
model_with_relu = nn.Sequential(
nn.Linear(2, 8),
nn.ReLU(),
nn.Linear(8, 2),
)
第一个模型本质上仍然是线性变换的组合,表达能力有限;第二个模型引入了非线性,能拟合更复杂的边界。
如果数据本身线性可分,两者都可能表现不错;如果数据边界非线性,激活函数的价值会更明显。
构造一个异或数据例子:
python
import paddle
import paddle.nn as nn
import paddle.nn.functional as F
x = paddle.to_tensor(
[[0.0, 0.0], [0.0, 1.0], [1.0, 0.0], [1.0, 1.0]],
dtype="float32",
)
labels = paddle.to_tensor([0, 1, 1, 0], dtype="int64")
model = nn.Sequential(
nn.Linear(2, 8),
nn.Tanh(),
nn.Linear(8, 2),
)
optimizer = paddle.optimizer.Adam(
learning_rate=0.05,
parameters=model.parameters(),
)
for step in range(300):
logits = model(x)
loss = F.cross_entropy(logits, labels)
loss.backward()
optimizer.step()
optimizer.clear_grad()
pred = paddle.argmax(model(x), axis=1)
print("pred:", pred)
异或任务不是单层线性模型能解决的问题,中间激活函数是关键。
参数量计算:Linear 到底有多少参数
Linear(in_features, out_features) 的参数量是:
text
weight: in_features * out_features
bias: out_features
total: in_features * out_features + out_features
例如:
python
linear = nn.Linear(4, 8)
参数量:
text
weight: 4 * 8 = 32
bias: 8
total: 40
可以用代码验证:
python
linear = nn.Linear(4, 8)
total = 0
for name, param in linear.named_parameters():
num = param.numel()
total += num
print(name, param.shape, num)
print("total:", total)
理解参数量很重要:
- 参数越多,模型表达能力可能更强。
- 参数越多,过拟合风险也可能更高。
- 参数越多,显存和计算量通常越大。
- 隐藏层宽度会直接影响参数量。
常见错误
错误一:Linear 的 in_features 和输入最后一维不匹配
错误示例:
python
linear = nn.Linear(4, 3)
x = paddle.randn([2, 5])
y = linear(x)
Linear(4, 3) 要求输入最后一维是 4,但 x 的最后一维是 5。
排查方式:
python
print(x.shape)
print(linear.weight.shape)
正确示例:
python
linear = nn.Linear(5, 3)
错误二:误以为 Linear 只接受二维输入
Linear 可以接受多维输入,只要最后一维匹配。
python
linear = nn.Linear(4, 3)
x = paddle.randn([2, 6, 4])
y = linear(x)
print(y.shape)
输出:
text
[2, 6, 3]
错误三:分类训练最后手动加 Softmax
常见错误:
python
model = nn.Sequential(
nn.Linear(4, 8),
nn.ReLU(),
nn.Linear(8, 3),
nn.Softmax(axis=1),
)
loss = F.cross_entropy(model(x), labels)
很多交叉熵实现期望输入是 logits,而不是 softmax 概率。更稳的训练结构是:
python
model = nn.Sequential(
nn.Linear(4, 8),
nn.ReLU(),
nn.Linear(8, 3),
)
logits = model(x)
loss = F.cross_entropy(logits, labels)
推理展示概率时再用:
python
prob = F.softmax(logits, axis=1)
错误四:把 functional 激活函数放进 Sequential
错误倾向:
python
model = nn.Sequential(
nn.Linear(4, 8),
F.relu,
nn.Linear(8, 3),
)
Sequential 里应该放 Layer 对象。使用:
python
model = nn.Sequential(
nn.Linear(4, 8),
nn.ReLU(),
nn.Linear(8, 3),
)
如果想用 F.relu,放在自定义 forward 里:
python
def forward(self, x):
x = self.fc1(x)
x = F.relu(x)
x = self.fc2(x)
return x
错误五:Sequential 不适合复杂分支结构
如果模型需要:
text
y = branch1(x) + branch2(x)
不要硬塞进单一路径 Sequential。应该自定义 Layer:
python
class TwoBranch(nn.Layer):
def __init__(self):
super().__init__()
self.branch1 = nn.Linear(4, 4)
self.branch2 = nn.Linear(4, 4)
def forward(self, x):
return self.branch1(x) + self.branch2(x)
错误六:忘记查看参数是否被注册
如果训练时 loss 不下降,先检查参数是否存在:
python
for name, param in model.named_parameters():
print(name, param.shape, param.stop_gradient)
如果参数列表为空,通常说明子层没有挂到 self 上,或者模型结构写法有问题。
调试方法:神经网络层先看四类 shape
写 Linear 和 Sequential 时,优先检查:
- 输入 Tensor 的 shape。
- 每个
Linear的weight.shape。 - 每层输出的 shape。
- label 的 shape 和 dtype。
调试代码:
python
def debug_model(model, x):
print("input:", x.shape)
out = x
for idx, layer in enumerate(model):
out = layer(out)
print(idx, layer.__class__.__name__, out.shape)
return out
model = nn.Sequential(
nn.Linear(4, 8),
nn.ReLU(),
nn.Linear(8, 3),
)
x = paddle.randn([2, 4])
debug_model(model, x)
输出会清楚显示每一层如何改变 shape。
对于自定义 Layer,可以在 forward 中临时打印:
python
print("after fc1:", x.shape)
等模型稳定后再删除。
总结
这一篇围绕 Paddle 神经网络层讲了几个核心点:
nn.Linear(in_features, out_features)完成Out = XW + b。Linear要求输入最后一维等于in_features。Linear输出最后一维变成out_features,前面维度保持不变。weight和bias是可学习参数,会被Layer自动管理。- 激活函数为网络引入非线性,常见选择包括
ReLU、Sigmoid、Tanh、LeakyReLU、GELU。 - 分类训练通常输出 logits,推理展示概率时再使用
Softmax。 nn.ReLU()是 Layer 形式,F.relu(x)是函数形式。nn.Sequential适合简单顺序结构。- 有分支、跳连、多输入、多输出时,应该自定义
nn.Layer。
如果只能记住一句话,那就是:
Linear 负责可学习的线性变换,激活函数负责引入非线性,Sequential 负责把简单顺序结构清晰地串起来。