PaddlePaddle 详细学习笔记:第二章:神经网络“三件套” (构建与数据)
第二章:神经网络“三件套” (构建与数据)
在第一章中,我们已经成功搭建了运行环境并掌握了 PaddlePaddle 的基本数据单元——Tensor。现在,是时候用这些“砖块”来构建宏伟的“建筑”——神经网络了。一个完整的深度学习项目,其核心代码结构可以被高度概括为三个部分:
- 模型 (Model):定义网络结构,即“长什么样”。
- 数据 (Data):准备和加载数据,即“吃什么料”。
- 目标 (Goal):设定损失函数和优化器,即“往什么方向努力”。
本章将逐一攻克这三大组件。
2.1 搭建模型:paddle.nn.Layer (重中之重)
在飞桨中,所有的神经网络模型,无论是简单的线性回归,还是复杂的 Transformer,都必须继承自一个基类:paddle.nn.Layer。这个类是所有模型结构的“万物之源”,其设计理念与 PyTorch 的 nn.Module 高度一致,如果你有 PyTorch 经验,将会感到非常熟悉。
掌握 paddle.nn.Layer 的核心在于理解其两个关键的成员函数:__init__ 和 forward。
| 方法 | 角色 | 核心任务 |
|---|---|---|
__init__(self, ...) |
“组件定义区” | 在这里,你需要实例化所有你将要用到的“积木块”,例如卷积层、全连接层、激活函数等,并将它们注册为类的属性(self.xxx)。这个阶段只定义,不计算。 |
forward(self, inputs) |
“计算逻辑区” | 在这里,你需要定义数据是如何在 __init__ 中定义的各个组件之间“流动”的。forward 函数接收输入 Tensor,让它依次通过各个层,并最终返回模型的输出 Tensor。这里是真正的计算发生地。 |
简单来说:__init__ 是“买菜备料”,forward 则是“开火炒菜”。
2.1.1 实战:对比 PyTorch 的 nn.Module
为了加深理解,我们来看一个极简的对比。
PaddlePaddle (paddle.nn.Layer)
import paddle
class MyModel(paddle.nn.Layer):
def __init__(self, in_features, out_features):
super(MyModel, self).__init__() # 必须先调用父类的构造函数
self.linear = paddle.nn.Linear(in_features, out_features)
def forward(self, x):
return self.linear(x)
PyTorch (torch.nn.Module)
import torch
class MyModel(torch.nn.Module):
def __init__(self, in_features, out_features):
super(MyModel, self).__init__() # 同样需要调用父类构造函数
self.linear = torch.nn.Linear(in_features, out_features)
def forward(self, x):
return self.linear(x)
可以看到,除了包名不同,二者的结构和写法几乎完全一样。这种设计上的一致性极大地降低了开发者在不同框架间切换的学习成本。
2.1.2 实战:构建一个简单的线性回归模型
线性回归是监督学习的入门任务,其模型可以表示为 y = Wx + b。这在神经网络中,就是一个不带激活函数的全连接层(paddle.nn.Linear)。
import paddle
# 1. 定义模型
class LinearRegressionModel(paddle.nn.Layer):
def __init__(self, input_dim, output_dim):
super(LinearRegressionModel, self).__init__()
# 定义一个全连接层
# input_dim: 输入特征的数量
# output_dim: 输出预测值的数量
self.linear = paddle.nn.Linear(input_dim, output_dim)
def forward(self, x):
# 定义前向计算逻辑:直接通过全连接层
out = self.linear(x)
return out
# 2. 实例化并使用模型
# 假设我们有一个任务:根据房子的面积(1个特征)来预测房价(1个预测值)
model = LinearRegressionModel(input_dim=1, output_dim=1)
# 打印模型结构
print(model)
# 输出:
# LinearRegressionModel(
# (linear): Linear(in_features=1, out_features=1, dtype=float32)
# )
# 模拟输入数据:一个 batch 包含 4 个样本,每个样本 1 个特征
dummy_input = paddle.to_tensor([[100.0], [120.0], [80.0], [150.0]], dtype='float32')
# 3. 执行前向计算
predictions = model(dummy_input) # 调用模型实例等价于调用其 forward 方法
print(f"输入形状: {dummy_input.shape}")
print(f"预测输出形状: {predictions.shape}")
print(f"预测结果:\n{predictions.numpy()}")
在这个例子中,__init__ 负责创建 self.linear 这个“计算工具”,而 forward 则负责使用这个工具来处理输入数据 x。
2.1.3 实战:构建一个 CNN 模型 (LeNet)
现在,我们来构建一个更复杂的、真正的深度学习模型——LeNet-5。这是卷积神经网络(CNN)的开山之作,常用于图像分类。通过它,我们可以熟悉几个核心的 CNN 组件:
paddle.nn.Conv2D: 2D 卷积层,用于提取图像特征。in_channels: 输入图像的通道数(例如,灰度图为1,RGB彩图为3)。out_channels: 输出特征图的通道数,也即卷积核的数量。kernel_size: 卷积核的大小。
paddle.nn.MaxPool2D: 2D 最大池化层,用于降低特征图的分辨率,减少计算量,同时保持特征不变性。paddle.nn.ReLU: 修正线性单元,一种常用的非线性激活函数。通常使用paddle.nn.functional.relu,因为它没有需要学习的参数。
import paddle
import paddle.nn.functional as F
class LeNet(paddle.nn.Layer):
def __init__(self, num_classes=10):
super(LeNet, self).__init__()
# --- 组件定义区 ---
# 卷积层部分
self.conv1 = paddle.nn.Conv2D(in_channels=1, out_channels=6, kernel_size=5)
self.pool1 = paddle.nn.MaxPool2D(kernel_size=2, stride=2)
self.conv2 = paddle.nn.Conv2D(in_channels=6, out_channels=16, kernel_size=5)
self.pool2 = paddle.nn.MaxPool2D(kernel_size=2, stride=2)
# 全连接层部分
# 经过两次池化后,图像尺寸会变小,需要计算一下进入全连接层时的特征维度
# 假设输入是 28x28 的图像,经过 conv1->pool1->conv2->pool2 后尺寸变为 4x4
# 因此,flatten 后的维度是 16 (通道数) * 4 * 4 = 256
self.fc1 = paddle.nn.Linear(in_features=16 * 4 * 4, out_features=120)
self.fc2 = paddle.nn.Linear(in_features=120, out_features=84)
self.fc3 = paddle.nn.Linear(in_features=84, out_features=num_classes)
def forward(self, x):
# --- 计算逻辑区 ---
# 输入 x 的 shape 应该是 [BatchSize, 1, 28, 28]
# 卷积 -> 激活 -> 池化
x = self.conv1(x)
x = F.relu(x)
x = self.pool1(x)
# 卷积 -> 激活 -> 池化
x = self.conv2(x)
x = F.relu(x)
x = self.pool2(x)
# 将 [N, C, H, W] 的四维 Tensor “拉平”成 [N, Features] 的二维 Tensor
x = paddle.flatten(x, start_axis=1, stop_axis=-1)
# 全连接 -> 激活
x = self.fc1(x)
x = F.relu(x)
# 全连接 -> 激活
x = self.fc2(x)
x = F.relu(x)
# 输出层 (通常不加激活,因为损失函数会处理)
x = self.fc3(x)
return x
# 实例化模型
model = LeNet(num_classes=10)
# 打印模型摘要信息,可以看到每一层的名字、类型和参数量
paddle.summary(model, (1, 1, 28, 28)) # (BatchSize, Channel, Height, Width)
最佳实践:
- 对于没有可学习参数的操作,如
relu,softmax,dropout等,推荐使用paddle.nn.functional里的函数式 API,代码更简洁。- 对于有可学习参数的层,如
Conv2D,Linear,必须在__init__中实例化。
2.2 准备数据:paddle.io.Dataset 与 DataLoader
模型已经搭好,现在需要准备“粮草”。在 PaddlePaddle 中,数据加载 pipeline 由两个核心类协作完成:
paddle.io.Dataset:负责定义数据的来源和单一样本的读取方式。它像一个索引目录,告诉程序总共有多少数据,以及如何根据索引(index)获取某一条数据。paddle.io.DataLoader:负责从Dataset中批量、高效地取出数据。它像一个智能的“送料车”,负责将数据打包成batch、打乱顺序(shuffle)、并使用多进程加速读取,以确保 GPU 不会因为等待数据而“挨饿”。
2.2.1 难点:如何自定义 Dataset
虽然飞桨内置了如 paddle.vision.datasets.MNIST 等常用数据集,但在实际项目中,我们 99% 的时间都在处理自己的数据。因此,学会自定义 Dataset 是必修课。
要自定义一个 Dataset,你需要创建一个类,继承 paddle.io.Dataset,并重写两个“魔法方法”:
__len__(self):必须返回数据集中样本的总数。__getitem__(self, index):必须根据传入的索引index,返回一条数据和它对应的标签。这是Dataset的灵魂所在,所有的数据预处理(如图像解码、缩放、归一化、文本分词等)通常都在这里完成。
实战:创建一个简单的自定义图像分类 Dataset
假设我们有如下文件结构:
./my_dataset/
├── cat/
│ ├── 001.jpg
│ ├── 002.png
│ └── ...
└── dog/
├── 101.jpg
├── 102.jpeg
└── ...
import os
from paddle.io import Dataset
from paddle.vision.transforms import transforms
from PIL import Image
# 1. 创建自定义 Dataset 类
class MyImageDataset(Dataset):
def __init__(self, data_dir, transform=None):
super(MyImageDataset, self).__init__()
self.data_dir = data_dir
self.transform = transform
self.data_list = [] # 用于存储所有 (图片路径, 标签)
self.label_map = {"cat": 0, "dog": 1}
# --- 核心:加载数据索引 ---
# 遍历文件夹,获取所有图片路径和对应标签
for label_name, label_id in self.label_map.items():
class_dir = os.path.join(data_dir, label_name)
for filename in os.listdir(class_dir):
self.data_list.append((os.path.join(class_dir, filename), label_id))
def __getitem__(self, index):
# --- 核心:根据索引读取单条数据 ---
# 1. 获取图片路径和标签
img_path, label = self.data_list[index]
# 2. 读取图片 (推荐使用 PIL)
image = Image.open(img_path).convert('RGB') # 统一转为 RGB
# 3. 应用数据预处理 (transform)
if self.transform is not None:
image = self.transform(image)
# 4. 将标签转换为 Tensor
label = paddle.to_tensor(label, dtype='int64')
return image, label
def __len__(self):
# --- 核心:返回数据集大小 ---
return len(self.data_list)
# 2. 定义数据预处理流程
# paddle.vision.transforms 提供了丰富的图像预处理算子
transform_pipeline = transforms.Compose([
transforms.Resize((100, 100)), # 缩放图片到 100x100
transforms.ToTensor(), # 将 PIL Image 或 Numpy Array 转为 Tensor, 并将 HWC 转为 CHW
transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]) # 归一化到 [-1, 1]
])
# 3. 实例化你的 Dataset
# (请先确保你创建了 my_dataset 文件夹和一些图片)
# train_dataset = MyImageDataset(data_dir='./my_dataset', transform=transform_pipeline)
# print(f"数据集大小: {len(train_dataset)}")
# sample_img, sample_label = train_dataset[0] # 测试一下 __getitem__
# print(f"样本图片 shape: {sample_img.shape}, 标签: {sample_label.item()}")
注意: 上述代码仅为示例,运行前需要自己创建对应的文件夹和图片。
2.2.2 实战:使用 DataLoader 进行批处理
Dataset 准备好后,就可以交给 DataLoader 来进行封装。
from paddle.io import DataLoader
# 假设 train_dataset 已经实例化好了
# train_dataset = MyImageDataset(data_dir='./my_dataset', transform=transform_pipeline)
# 1. 实例化 DataLoader
train_loader = DataLoader(
train_dataset,
batch_size=32, # 每个批次包含 32 个样本
shuffle=True, # 在每个 epoch 开始时打乱数据顺序(训练时强烈推荐)
num_workers=4, # 使用 4 个子进程来异步加载数据
drop_last=False # 如果最后一个批次样本数不足 batch_size,是否丢弃(训练时可False)
)
# 2. 遍历 DataLoader
# 在实际训练中,这个 for 循环就是每个 epoch 的核心
# for epoch in range(num_epochs):
# for step, (images, labels) in enumerate(train_loader):
# # 在这里执行模型训练的“五步法” (第三章内容)
# # images 的 shape: [32, 3, 100, 100]
# # labels 的 shape: [32]
# if step % 10 == 0:
# print(f"Step {step}, Image batch shape: {images.shape}, Label batch shape: {labels.shape}")
````DataLoader` 极大地简化了数据加载的复杂性,让我们能专注于模型本身。`num_workers > 0` 在 Linux 系统下能显著提升数据吞吐量,但在 Windows 下可能存在一些问题,初学者可以先设为 0。
## 2.3 损失函数与优化器 (“胶水”)
我们已经有了模型(`nn.Layer`)和数据(`DataLoader`),现在需要最后两个组件来将它们“粘合”在一起,并驱动模型学习:
* **损失函数 (Loss Function)**:一把“尺子”,用来衡量模型预测值(`predictions`)与真实标签(`labels`)之间的差距。差距越大,损失值(`loss`)就越大。训练的目标就是**最小化**这个损失值。
* **优化器 (Optimizer)**:一个“舵手”,它根据损失函数计算出的梯度(`gradient`),来决定如何更新模型中的每一个参数(权重 `weights` 和偏置 `biases`),以使得损失值朝着减小的方向前进。
### 2.3.1 实战:常用损失函数
飞桨在 `paddle.nn` 模块中内置了各类常用的损失函数。
#### `paddle.nn.CrossEntropyLoss` (交叉熵损失)
* **用途**:**多分类问题**的“黄金标准”,例如 MNIST 数字识别(10分类)、ImageNet 图像分类(1000分类)。
* **注意**:飞桨的 `CrossEntropyLoss` 内部已经包含了 `Softmax` 操作,因此你的模型(如前面的 LeNet)的输出层**不需要**额外添加 `Softmax` 激活函数。
```python
import paddle
# 实例化
loss_fn_cls = paddle.nn.CrossEntropyLoss()
# 模拟输入
# 假设 batch_size=4, 类别数=10
predictions = paddle.randn([4, 10]) # 模型输出的原始 logits
labels = paddle.to_tensor([1, 5, 2, 9], dtype='int64') # 真实标签
# 计算损失
loss = loss_fn_cls(predictions, labels)
print(f"交叉熵损失: {loss.item()}")
paddle.nn.MSELoss (均方误差损失)
- 用途:回归问题的首选,用于预测连续值,例如预测房价、股票价格、温度等。它计算的是预测值与真实值之差的平方的均值。
# 实例化
loss_fn_reg = paddle.nn.MSELoss()
# 模拟输入
# 假设 batch_size=4, 预测1个值
predictions = paddle.to_tensor([[101.5], [89.0], [210.3], [145.1]])
labels = paddle.to_tensor([[100.0], [92.0], [205.0], [150.0]])
# 计算损失
loss = loss_fn_reg(predictions, labels)
print(f"均方误差损失: {loss.item()}")
2.3.2 实战:paddle.optimizer.Adam 的基本使用
Adam (Adaptive Moment Estimation) 是目前最流行、最通用的优化器之一,它结合了 Momentum 和 RMSProp 两种优化算法的优点,通常能快速且稳定地收敛。
使用优化器的关键一步是:在实例化时,必须将模型的参数 model.parameters() 传递给它,这样优化器才知道要去更新哪些 Tensor。
# 假设 model 已经是我们实例化的 LeNet 模型
model = LeNet()
# 1. 实例化优化器
# learning_rate (学习率) 是最重要的超参数之一,控制参数更新的步长
optimizer = paddle.optimizer.Adam(
learning_rate=0.001,
parameters=model.parameters() # 核心:将模型所有可学习的参数交给优化器管理
)
# 优化器的使用(更新参数)将在第三章的训练循环中详细讲解
# 主要涉及以下三个步骤:
# 1. loss.backward() # 计算梯度
# 2. optimizer.step() # 根据梯度更新参数
# 3. optimizer.clear_grad() # 清除旧梯度,为下一次迭代做准备
至此,我们已经集齐了构建并训练一个神经网络所需的所有“零件”。下一章,我们将把这些零件组装起来,构建一个完整的训练与评估流程,让模型真正地“学习”起来。
DeepSpeed 详细学习笔记 其它章节 链接
以下是整个系列的5章目录,点击章节标题即可跳转阅读(所有章节统一使用“DeepSpeed 详细学习笔记 第X章”格式;若链接暂未索引,可直接访问作者博客搜索相应标题获取最新链接)。
PaddlePaddle 详细学习笔记 其它章节 链接
以下是整个系列的5章目录,点击章节标题即可跳转阅读(所有章节统一使用“DeepSpeed 详细学习笔记 第X章”格式;若链接暂未索引,可直接访问作者博客搜索相应标题获取最新链接)。
鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。
更多推荐
所有评论(0)