2、Python基础回顾:语法、数据结构、函数、面向对象与NumPy矩阵运算
深度学习模型的开发本质上是对高维数据的变换与优化,而PyTorch作为动态计算图的代表框架,其底层张量操作与Python语法、NumPy数组运算有着密不可分的联系。本章并非简单的语法罗列,而是从“为深度学习打基础”这一核心目标出发,梳理Python中必须熟练掌握的五个关键模块:基础语法与容器、函数式编程思维、面向对象设计、NumPy向量化操作以及矩阵运算的工程语义。理解这些内容,将直接决定你后续能否高效阅读PyTorch源码、自定义数据集类(Dataset)、构建网络层(nn.Module)以及调试梯度传播问题。
上图展示了本章的知识架构与它们在深度学习中的最终落点。Python的容器与语法是数据加载与预处理的基础;函数式编程帮助理解PyTorch的forward计算逻辑;面向对象是构建网络层与自定义数据集的核心范式;NumPy的向量化思维则是理解GPU并行计算与张量运算的桥梁。下面我们逐一深入。
2.1 核心语法与数据结构:从容器到数据批处理
核心原理:深度学习中的数据通常以批量(Batch)形式组织,而Python的列表(List)、字典(Dict)和元组(Tuple)正是构建数据管道(Data Pipeline)的原始积木。列表用于存储有序的可变序列,例如一个Batch中所有图像的路径;字典用于存储键值对,例如样本ID到标签的映射;元组则用于表示不可变的数据记录,例如坐标点或模型返回的多个输出。
操作流程:在实际项目中,我们通常遵循以下模式:
- 使用列表推导式快速生成数据路径列表,避免显式for循环。
- 使用字典存储超参数配置,并通过
**kwargs解包传递给函数。 - 使用元组作为函数多返回值的标准容器,便于解包赋值。
案例拆解:假设我们需要加载100张图片的路径及其标签。使用列表与字典的组合可以高效完成:
import os
# 使用列表推导式生成文件路径
img_dir = "./data/train"
img_paths = [os.path.join(img_dir, f) for f in os.listdir(img_dir) if f.endswith('.jpg')]
# 使用字典存储标签映射
label_map = {'cat': 0, 'dog': 1}
# 使用元组表示一条样本记录
sample = (img_paths[0], label_map['cat'])
print(sample) # ('./data/train/cat_001.jpg', 0)
常见误区:初学者常混淆列表与元组的适用场景。在定义网络层权重形状时,应使用元组(如(3, 224, 224))防止误修改;而在需要动态增删数据的场景(如收集训练日志)中,应使用列表。此外,字典在Python 3.7+中保持插入顺序,这在构建有序配置时非常有用。
| 容器类型 | 可变性 | 典型深度学习场景 | 性能注意点 |
|---|---|---|---|
| List | 可变 | 存储样本路径、中间特征列表 | 尾部append为O(1),头部插入为O(n) |
| Tuple | 不可变 | 网络输入尺寸、函数多返回值 | 内存占用更小,可作为字典键 |
| Dict | 可变 | 超参数配置、权重名称映射 | 查找平均O(1),但哈希开销较大 |
collate_fn来灵活组织Batch数据。2.2 函数:从过程到计算图的思维转变
核心原理:深度学习模型的前向传播本质上是一个复合函数:y = f(g(h(x)))。Python函数不仅支持位置参数、默认参数,还支持*args和**kwargs,这为构建灵活的模型层提供了语法基础。更重要的是,函数是一等公民,可以像变量一样传递,这正是PyTorch中nn.Sequential和自定义激活函数的基础。
判断方法:在编写深度学习代码时,应遵循“单一职责”原则。如果一个函数超过20行且做了两件以上不同的事情,就应该拆分。同时,善用lambda表达式定义简单的匿名函数,例如在数据预处理中作为map的参数。
实战场景:考虑一个数据增强流程,我们需要对图像进行随机翻转和归一化。使用函数式编程可以清晰表达这一流水线:
import numpy as np
# 定义处理函数
def normalize(img, mean, std):
return (img - mean) / std
def random_flip(img, prob=0.5):
if np.random.rand() < prob:
return np.fliplr(img)
return img
# 使用lambda组合操作
pipeline = lambda img: normalize(random_flip(img), mean=0.5, std=0.25)
# 模拟一张3x3图像
sample_img = np.random.rand(3, 3)
processed = pipeline(sample_img)
print(processed.shape) # (3, 3)
常见误区:可变默认参数是Python的经典陷阱。在定义函数时,不要使用def func(x, lst=[]),而应使用def func(x, lst=None)并在函数体内初始化。此外,过度使用lambda会降低代码可读性,建议仅在简单表达式时使用。
forward()函数必须接收输入张量并返回输出张量,不能在其中修改全局变量或执行非张量操作,否则会破坏自动求导的计算图追踪。2.3 面向对象编程:构建网络层与数据集类
核心原理:PyTorch的核心抽象——nn.Module和Dataset——都是基于面向对象(OOP)设计的。理解类、继承、多态是自定义网络结构的前提。一个网络层类通常继承nn.Module,在__init__中定义子层,在forward中定义计算逻辑。
操作流程:自定义一个网络层或数据集需要遵循以下步骤:
- 定义类并继承基类(
nn.Module或Dataset)。 - 在
__init__中调用super().__init__()并初始化参数。 - 实现
forward()方法(对于Module)或__len__与__getitem__(对于Dataset)。
案例拆解:以下是一个简单的线性层实现,展示了OOP在模型构建中的核心地位:
import torch.nn as nn
import torch
class MyLinear(nn.Module):
def __init__(self, in_features, out_features):
super().__init__()
# 定义可学习参数
self.weight = nn.Parameter(torch.randn(in_features, out_features))
self.bias = nn.Parameter(torch.zeros(out_features))
def forward(self, x):
# x: (batch, in_features)
return x @ self.weight + self.bias
# 使用自定义层
layer = MyLinear(4, 3)
input_tensor = torch.randn(2, 4)
output = layer(input_tensor)
print(output.shape) # torch.Size([2, 3])
常见误区:初学者常忘记调用super().__init__(),导致模块无法正确注册参数。另外,不要在forward中使用Python的原生for循环处理序列数据,应使用nn.Sequential或张量操作来向量化计算。
| OOP概念 | 在PyTorch中的体现 | 常见错误 |
|---|---|---|
| 继承 | 自定义层继承nn.Module | 忘记调用super().__init__() |
| 多态 | 不同层实现相同的forward接口 | 在forward中改变输入张量的形状而不返回 |
| 封装 | 参数通过self.weight管理 | 使用普通Tensor而非nn.Parameter |
2.4 NumPy数组操作:向量化思维与性能关键
核心原理:NumPy的ndarray是Python中实现数值计算的基础。深度学习中的数据预处理(如图像归一化、数据增强)几乎全部依赖NumPy。其核心优势在于向量化:使用C语言实现的底层循环替代Python层的显式循环,从而获得接近两个数量级的性能提升。
操作流程:掌握NumPy的关键在于理解广播(Broadcasting)机制。当两个数组形状不同时,NumPy会尝试将较小的数组“拉伸”到与较大数组相同的形状,前提是维度对齐后,其中一个维度为1或相等。
import numpy as np
# 广播示例:对每行减去均值
matrix = np.random.rand(3, 4) # 3行4列
row_mean = matrix.mean(axis=1, keepdims=True) # 形状(3,1)
centered = matrix - row_mean # 广播减法
print(centered.shape) # (3, 4)
实战场景:在图像分类任务中,我们经常需要将RGB图像的像素值从[0,255]归一化到[0,1],并减去均值、除以标准差。使用NumPy可以一行完成:
# 模拟一张224x224x3的图像
image = np.random.randint(0, 255, (224, 224, 3), dtype=np.uint8)
# 归一化
image_float = image.astype(np.float32) / 255.0
mean = np.array([0.485, 0.456, 0.406])
std = np.array([0.229, 0.224, 0.225])
normalized = (image_float - mean) / std
print(normalized.dtype, normalized.shape) # float32 (224, 224, 3)
常见误区:一个高频错误是视图与副本的混淆。使用reshape返回的是视图(View),修改会影响原数组;而flatten通常返回副本。在深度学习数据加载中,应明确是否需要保留原始数据。另外,np.dot与*的区别:*是逐元素乘法,np.dot是矩阵乘法。
torch.from_numpy()共享内存(仅限CPU张量),避免不必要的复制开销。2.5 矩阵运算:从NumPy到PyTorch张量的桥梁
核心原理:深度学习中的全连接层、卷积操作、注意力机制本质上都是矩阵乘法与张量变形。NumPy的np.matmul(或@运算符)与PyTorch的torch.matmul在数学语义上完全一致,但PyTorch额外支持自动求导。理解矩阵的维度匹配规则(前一个矩阵的列数必须等于后一个矩阵的行数)是避免维度错误的基础。
判断方法:在编写矩阵运算前,先手动计算输出形状。例如,输入x形状为(batch, in_features),权重W形状为(in_features, out_features),则输出形状为(batch, out_features)。这一规则在构建多层感知机(MLP)时至关重要。
案例拆解:以下代码对比了NumPy与PyTorch在矩阵乘法上的相似性,并展示了如何将NumPy数组无缝转换为PyTorch张量:
import numpy as np
import torch
# NumPy矩阵乘法
A_np = np.random.rand(3, 4)
B_np = np.random.rand(4, 5)
C_np = A_np @ B_np # 或 np.matmul(A_np, B_np)
# 转换为PyTorch张量并计算
A_torch = torch.from_numpy(A_np) # 共享内存
B_torch = torch.from_numpy(B_np)
C_torch = A_torch @ B_torch # 或 torch.matmul(A_torch, B_torch)
# 验证结果一致性
print(np.allclose(C_np, C_torch.numpy())) # True
常见误区:混淆np.dot与np.matmul在多维数组上的行为。np.dot对于二维数组执行矩阵乘法,但对于高维数组执行的是“点积和”操作,而np.matmul始终执行矩阵乘法(广播语义)。在PyTorch中,torch.mm仅用于二维矩阵,torch.matmul支持高维广播,推荐使用后者。
| 运算 | NumPy | PyTorch | 语义 |
|---|---|---|---|
| 逐元素乘法 | np.multiply 或 * | torch.mul 或 * | 对应位置相乘 |
| 矩阵乘法 | np.matmul 或 @ | torch.matmul 或 @ | 标准矩阵乘法 |
| 批量矩阵乘法 | np.matmul (广播) | torch.matmul (广播) | 支持Batch维度 |