2、Python基础回顾:语法、数据结构、函数、面向对象与NumPy矩阵运算

深度学习模型的开发本质上是对高维数据的变换与优化,而PyTorch作为动态计算图的代表框架,其底层张量操作与Python语法、NumPy数组运算有着密不可分的联系。本章并非简单的语法罗列,而是从“为深度学习打基础”这一核心目标出发,梳理Python中必须熟练掌握的五个关键模块:基础语法与容器、函数式编程思维、面向对象设计、NumPy向量化操作以及矩阵运算的工程语义。理解这些内容,将直接决定你后续能否高效阅读PyTorch源码、自定义数据集类(Dataset)、构建网络层(nn.Module)以及调试梯度传播问题。

Python 深度学习基础栈 语法与容器 列表/字典/元组 函数与作用域 lambda/装饰器/闭包 面向对象 OOP 类/继承/多态 NumPy 数组 广播/索引/向量化 矩阵运算与张量 dot/matmul/广播 PyTorch 张量(Tensor)与自动求导 Dataset / nn.Module / 梯度计算

上图展示了本章的知识架构与它们在深度学习中的最终落点。Python的容器与语法是数据加载与预处理的基础;函数式编程帮助理解PyTorch的forward计算逻辑;面向对象是构建网络层与自定义数据集的核心范式;NumPy的向量化思维则是理解GPU并行计算与张量运算的桥梁。下面我们逐一深入。

2.1 核心语法与数据结构:从容器到数据批处理

核心原理:深度学习中的数据通常以批量(Batch)形式组织,而Python的列表(List)、字典(Dict)和元组(Tuple)正是构建数据管道(Data Pipeline)的原始积木。列表用于存储有序的可变序列,例如一个Batch中所有图像的路径;字典用于存储键值对,例如样本ID到标签的映射;元组则用于表示不可变的数据记录,例如坐标点或模型返回的多个输出。

操作流程:在实际项目中,我们通常遵循以下模式:

  1. 使用列表推导式快速生成数据路径列表,避免显式for循环。
  2. 使用字典存储超参数配置,并通过**kwargs解包传递给函数。
  3. 使用元组作为函数多返回值的标准容器,便于解包赋值。

案例拆解:假设我们需要加载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),但哈希开销较大
经验提醒:在PyTorch的DataLoader中,每个Batch通常是一个由张量组成的列表或字典。理解原生容器的特性,有助于你自定义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会降低代码可读性,建议仅在简单表达式时使用。

风险说明:在PyTorch中,forward()函数必须接收输入张量并返回输出张量,不能在其中修改全局变量或执行非张量操作,否则会破坏自动求导的计算图追踪。

2.3 面向对象编程:构建网络层与数据集类

核心原理:PyTorch的核心抽象——nn.Module和Dataset——都是基于面向对象(OOP)设计的。理解类、继承、多态是自定义网络结构的前提。一个网络层类通常继承nn.Module,在__init__中定义子层,在forward中定义计算逻辑。

操作流程:自定义一个网络层或数据集需要遵循以下步骤:

  1. 定义类并继承基类(nn.Module或Dataset)。
  2. 在__init__中调用super().__init__()并初始化参数。
  3. 实现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是矩阵乘法。

性能提示:在GPU训练中,数据从NumPy转换为PyTorch张量时会发生内存拷贝。如果数据量巨大,建议使用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支持高维广播,推荐使用后者。

运算NumPyPyTorch语义
逐元素乘法np.multiply 或 *torch.mul 或 *对应位置相乘
矩阵乘法np.matmul 或 @torch.matmul 或 @标准矩阵乘法
批量矩阵乘法np.matmul (广播)torch.matmul (广播)支持Batch维度
总结复盘:本章所回顾的Python基础并非孤立的语法点,而是深度学习开发的“内功”。容器与语法决定了你组织数据的方式;函数与OOP决定了你构建模型的抽象层次;NumPy与矩阵运算则直接映射到PyTorch的张量操作。建议在学习后续章节时,遇到任何PyTorch代码,都尝试用原生Python或NumPy“翻译”一遍,这将极大加深对计算图与自动求导的理解。