我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

深度学习入门必学:Python核心语法与PyTorch实战指南

深度学习入门必学:Python核心语法与PyTorch实战指南 你可能已经刷过不少深度学习的公开课也看过那种“从零开始”的保姆级教程。但说实话很多人一开始没倒在大模型理论上而是死在了第一行代码上。我见过太多朋友拿着PyTorch教程想跑个CNN跑到数据预处理那一步就卡住了原因是连Python的基本数据结构都没搞利索。这篇笔记就是系列的第3篇专门用来补齐深度学习真正用得到的Python基础省得你在代码海洋里溺水。这篇内容适合两类人一类是刚学完Python语法、但不知道深度学习要重点用哪些特性的新人另一类是能看懂教程、但自己动手写代码就发懵的“半吊子”。我不会按教科书顺序把所有语法过一遍而是围绕深度学习里出现频率最高的几个场景——数据切分、张量变换、批量读取、模型搭建、训练循环——讲清楚你必须掌握的那部分Python。你会发现深度学习里绝大部分代码其实就集中在几个固定模式里。1. 深度学习用到的Python到底比普通教程多了什么1.1 从“学语法”到“跑模型”之间的鸿沟普通Python教程教的是字典、元组、集合、while循环这些基础概念但深度学习的代码风格完全是另一副面孔。你去GitHub上随便下一个开源的训练脚本看到的往往是各种列表推导式、lambda函数、嵌套切片、yield生成器甚至还有大量你不认识的库调用。很多新手卡在这里明明每个语法点都看懂了合起来就看不懂了。差距的核心在于“组合运用”。比如[x.unsqueeze(0) for x in batch]这一行就涉及了列表推导式、方法调用、循环变量三个概念。你单独拎出来都懂但在代码里一压就懵。所以这篇笔记我不会让你从头遍历语法而是直接拿深度学习里最常见的代码片段来解剖你需要的是建立“场景到语法”的映射能力。还有个特点是深度学习代码对性能有要求。你写的Python如果跑在CPU上做循环3万张图片可能得处理到天亮。所以必须掌握向量化思维学会用NumPy和PyTorch这类底层C实现的库来替代Python原生循环。这不是一门选修课而是必修课因为模型训练本身就是在做大量线性代数运算Python原生列表在这些运算面前根本不中用。1.2 深度学习代码的标准骨架长什么样任何一个深度学习项目代码基本都围绕下面几个模块展开数据加载与预处理读文件、缩放、归一化、切batch、打乱顺序模型定义继承基类、搭层、定义前向传播训练循环多个epoch迭代、清零梯度、算loss、反向传播、更新参数验证与测试切换评估模式、计算准确率、保存模型这四件事看起来简单但每一件都有Python基础在里面。比如数据加载要会用文件路径拼接要懂得怎么把列表切成训练集和验证集模型定义要理解类的继承和方法重写训练循环要掌握range、enumerate、zip这类迭代工具的使用。所以这篇笔记的最终目标是让你能把Python的语法点主动映射到深度学习的代码结构上而不是被动地“逐个查函数”。2. 动手前先磨刀Python环境与开发工具准备2.1 Python安装与版本选择的现实考量深度学习相关的所有主流框架像PyTorch、TensorFlow、JAX现在都完全支持Python 3.8以上的版本。我的建议是直接安装最新的稳定版Python 3.10或3.11因为新版本在性能上有明显优化官方文档和社区讨论也都围绕新版本展开遇到问题更容易找到答案。什么情况下要关心版本细节就是当你必须用一个非常老的代码库时。有些课程项目是几年之前写的那时候可能还在用Python 3.6你拿3.11去跑大概率会遇到依赖库不兼容的问题。我踩过这种坑某个老项目里的pytorch0.4.1在新Python环境里根本装不上最后只能新建一个老版本环境来跑。所以刚开始动手就装新版本遇到老项目再单独应对。安装方式我推荐用Anaconda而不是直接去python.org下载。原因是Anaconda自带的conda不仅能管理Python版本还能快速创建隔离的环境这对深度学习来说太重要了。你想试PyTorch但A项目需要PyTorch 2.0B项目需要1.13用conda各建一个环境互不打扰。否则你在系统Python里装了2.0再装1.13分分钟把依赖搞成一锅粥。2.2 用conda建一个打不烂的深度学习环境我自己搭建环境的固定流程如下conda create -n dl python3.10 conda activate dl然后装基础库pip install numpy matplotlib jupyter pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118最后一条是装带CUDA支持的PyTorch。如果你是N卡且已经装了CUDA驱动可以用这个命令装上GPU版本如果你还没有GPU把--index-url这部分的参数去掉直接装CPU版本也一样能学。很多新手在这步会犯一个错误直接pip install torch装了CPU版之后跑模型发现慢得离谱过了一段时间才知道自己没装GPU版。建议装完跑一下python -c import torch; print(torch.cuda.is_available())验证一下。另外我推荐装个ipython和rich库。前者是增强版的Python交互环境带高亮自动补全调试小段逻辑特别好用后者能把训练日志打印得漂漂亮亮方便你观察loss变化。这类“体验优化”的工具虽然不影响核心功能但对长期学习的心态影响很大——看着整齐的输出你会更愿意多跑几组实验。3. 深度学习代码里出现频率最高的Python核心特性3.1 列表推导式把一坨循环压缩成一行列表推导式是深度学习代码里最显眼的Python特性。本质就是一行代码完成“对序列中每个元素做操作”这件事。比如把数据集中所有图片的路径加上前缀paths [os.path.join(data/train, name) for name in os.listdir(data/train)]这比传统的for循环加append写法简洁得多。别小看这种语法读开源代码时到处都会碰到。比如数据增强之后取前几项看看长啥样samples [dataset[i] for i in range(5)]跟你写五遍dataset[0]、dataset[1]比起来这行代码的意图更清晰取前5个样本。我个人总结了一个很简单的心法当你发现自己正在写“建一个空列表然后for循环往里塞东西”这种代码时停下来想一想列表推导式是不是能一行搞定。大概写两周就能养成习惯之后你看老式的循环代码反而会觉得很啰嗦。3.2 切片与索引玩转高维张量的前提深度学习里你几乎每天都和“第几个维度”“取哪一段”打交道。Python的切片语法[start:stop:step]在这方面是绝对主力。一维列表切片很多人会但到了高维数据就乱了。比如一个形状为(batch, channel, height, width)的图片张量你想取第一张图的红色通道怎么写image tensor[0, 0, :, :]通道数取法、全量取法、间隔取法都得烂熟于心。我建议新手专门花半小时把下面的切片操作在ipython里跑一遍全部理解透彻a list(range(10)) a[:] # 全部 a[2:5] # 2到4 a[::2] # 每隔一个取 a[::-1] # 倒序有这个基础之后再去学NumPy和PyTorch的张量切片就有了一个统的类比对象。注意PyTorch还支持一种更狠的写法——用逗号分隔每一维度的索引区间。所以你看不懂x[:, None, :]怎么办先把它拆开看第一维全要第二维插入一个维度第三维全要。这不是什么高深公式就是逗号切分维度的思路。3.3 字典与哈希思想模型参数和配置管理的基石字典在深度学习代码里的存在感极强。绝大多数模型配置文件就是一大坨嵌套字典比如config { batch_size: 32, learning_rate: 1e-3, model: { type: ResNet18, num_classes: 10 } }读取的时候靠config[learning_rate]判断有没有某个key靠model in config。这些操作太基础了但我见过很多新手把字典和列表搞混报错KeyError的时候完全不知道从哪里开始查。我的建议是看到一个字典心里就有个念头——“这是不是一张小表格key是行名value是值”所有操作都围绕这张小表格展开。另一个高频率操作是用字典计数。比如统计一个数据文件里每类样本有多少个counts {} for label in all_labels: counts[label] counts.get(label, 0) 1这里的get方法是关键——如果key不存在就返回默认值0避免手动写一堆if判断。这种写法在数据清洗时极其好用而且更Pythonic。3.4 函数与lambda把逻辑打包把代码变短深度学习代码里函数不仅仅是为了避免重复更是一种“接口抽象”。你最常遇到的场景是要把一个自定义的数据增强操作传给某个库比如PyTorch的torchvision.transforms.Compose期望接收多个“可调用对象”。这里你就可以用lambda快速创建from torchvision import transforms transform transforms.Compose([ transforms.Resize((224, 224)), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), lambda x: x / 255.0 ])lambda写起来爽但我提醒一句命名函数永远比匿名函数更清晰。如果一个lambda逻辑超过一行或者你看名字完全猜不出它要干嘛就别硬撑了老老实实定义一个带名字的函数def denormalize(x): return x * 0.5 0.5这种函数在深度学习里还有个职业叫“钩子函数”比如在模型训练过程的某个阶段打印日志或者调整学习率。理解了“函数也是对象、函数可以传来传去”这个Python核心思想之后你再去看PyTorch的很多回调接口就不会懵圈了。4. 面向深度学习的文件与数据操作实战4.1 路径拼接与文件遍历在训练脚本里你会反复做一件事拼接路径。很多人习惯写data/ folder /img.jpg这在Windows上经常炸因为Windows路径分隔符是反斜杠。正确做法是用os.path.join或者pathlibfrom pathlib import Path data_dir Path(data) train_dir data_dir / train image_path train_dir / cat / 006.jpgpathlib是Python 3时代主推的路径处理方案代码比os更简洁也更跨平台。我强烈建议你顺手把它学了后面写数据加载器时会明显感觉顺手。如果你要遍历文件夹下的所有图片可以这样images list(Path(data).rglob(*.jpg))一行代码拿到所有jpg文件路径还要什么os.walk呢这就是学习路径“场景导向”的意义——你知道要解决什么问题顺便就记住了API。4.2 读CSV、写JSON时那些躲不开的小细节做深度学习经常要处理标签文件最常见的就是CSV和JSON两种格式。读CSV有一个经典坑文件里如果包含表头直接按行读取会把表头也当成数据。用csv.DictReader可以把每一行直接变成字典按列名取值非常方便import csv with open(labels.csv, r) as f: reader csv.DictReader(f) rows [row for row in reader]JSON就相对简单json.load和json.dump一对组合搞定。但有一个容易犯的错Python的元组tuple不能被JSON序列化如果你数据里用了元组写JSON时会报TypeError。解决思路是把所有元组转成列表或者用自定义序列化器。我通常直接统一用列表省得折腾。另外提一嘴很多Python初学者以为读文件都是把整个文件load进内存这是不对的思维。当你的数据量达到几个G时正确的做法是逐行读取with open(huge.csv, r) as f: for line in f: process(line.strip())这个with上下文管理器的好处是即使中间报错文件也能自动关闭不用手写close()。这也是日常写脚本必须具备的职业习惯别把资源泄漏问题留给以后。4.3 数据分批处理生成器为什么是深度学习的恩人训练数据不可能一次性全部装进内存。解决方案就是生成器——用一个能按需生成数据的对象每次迭代取一批。Python里定义一个生成器函数只需要用yield替代returndef batch_generator(data, labels, batch_size32): n len(data) indices list(range(n)) random.shuffle(indices) for i in range(0, n, batch_size): batch_idx indices[i:ibatch_size] yield data[batch_idx], labels[batch_idx]这个函数返回的不是一个列表而是一个生成器对象。它的特点是每次next()才执行一次循环体内存里永远只保留一批数据。你可以对这个生成器做无限次迭代只要你不主动销毁它。我在实际项目里一直用这个模式做数据供给。它配合PyTorch的DataLoader几乎可以处理任意规模的数据集。很多新手刚开始觉得“一次性读进来多省事”直到跑10万张图片的数据集时内存爆掉才意识到生成器的价值。5. 深度学习主角登场NumPy数组与PyTorch张量基础5.1 NumPy数组是所有运算的地基在深度学习里Python的基本类型只是脚手架真正干活的是多维数组。NumPy的ndarray是第一个正式登场的多维数据结构。建议你把以下几组核心操作练熟import numpy as np a np.zeros((3, 4)) # 全0矩阵 b np.ones((2, 2)) # 全1矩阵 c np.random.rand(3, 3) # 0~1均匀分布的随机矩阵 d a.reshape(4, 3) # 改变形状 e d.T # 转置 f a.sum(axis0) # 按列求和其中最常见的操作是reshape和transpose。因为模型的不同层要求输入形状不同你要在“展开成一维向量”和“重新组织成图片形状”之间反复横跳。我提醒新手注意reshape和transpose的区别reshape是按内存顺序重新切割不会改变底层数据的排列语义而transpose是交换维度顺序改变了数据在逻辑上的排列。如果你搞混了这两个操作数据喂进模型后可能得到完全错误但是不报错的结果这种bug极难排查。5.2 广播机制让两个形状不一样的数组也能做运算广播机制是NumPy和PyTorch中最容易让新手迷惑的概念。简单说如果两个数组的形状在某个维度上一个为1、一个大于1较小的那个会被自动扩展。举个例子a np.array([[1, 2, 3], [4, 5, 6]]) # shape (2, 3) b np.array([10, 20, 30]) # shape (3,) print(a b)结果会把b当成[[10,20,30], [10,20,30]]来加。这看起来像魔法其实规则很简单从后往前比如果两个维度相等、其中一个为1、或者其中一个缺失就能广播。我在实际经验中总结出一句话先把两个数组的形状写出来对齐到同一个长度缺失的补1然后逐位对比凡是不等且没有1的一定会报ValueError。好好理解这个机制是因为深度学习里的归一化操作大量利用广播。比如你要给每个通道的像素做均值减除数据形状是(batch, channel, h, w)均值形状是(channel, 1, 1)直接相减就是广播操作。如果你强行手动写循环不但慢而且代码极其难看。5.3 为什么有了NumPy还要PyTorch张量很多新手的疑问是NumPy已经能做各种数组运算了深度学习为什么还要引入Tensor答案有两个层面第一Tensor支持自动求导你定义好计算图之后反向传播的事情框架自动搞定而NumPy没有这个能力第二Tensor可以无缝在CPU和GPU之间切换把数据放到显卡上做并行计算速度提升一两个数量级。PyTorch的Tensor用法和NumPy数组在80%的场景下几乎一模一样所以学会NumPy之后上手PyTorch非常快。比如import torch x torch.randn(3, 3) # 标准正态分布 y x x.T # 矩阵乘法 z x.sum(dim0) # 按列求和注意是dim不是axisTensor和NumPy数组互相转换x_np x.numpy() x_back torch.from_numpy(x_np)这个转换在保存数据、读取数据时特别常用。但要注意在GPU上的Tensor不能直接转NumPy要先用x.cpu()把它移动到CPU上再调用.numpy()。这个坑我踩过好几次报错也还算明确但新手容易被卡住半天。6. 模型搭建与训练循环里必须会的Python模式6.1 继承与重写PyTorch模型类为什么长这样在PyTorch里定义一个模型核心就是继承torch.nn.Module并重写两个函数__init__和forward。这种继承模式要求你对Python面向对象有基本理解__init__里先调用super().__init__()做基类的初始化然后注册你需要的层forward里定义数据从输入到输出的流动过程。import torch.nn as nn class MLP(nn.Module): def __init__(self, input_size, hidden_size, num_classes): super().__init__() self.fc1 nn.Linear(input_size, hidden_size) self.relu nn.ReLU() self.fc2 nn.Linear(hidden_size, num_classes) def forward(self, x): x self.fc1(x) x self.relu(x) x self.fc2(x) return x新手最难理解的一点是为什么不用直接写个普通函数原因在于PyTorch需要追踪模型的参数、记录计算图必须借助类的特殊方法。基类nn.Module内部处理了大量参数注册、梯度管理、设备切换的工作你如果不用继承这些全都得自己手动搞那才叫万劫不复。6.2 训练循环里的迭代魔法enumerate、zip与tqdm训练循环看起来长但核心模式十几年没变。拿一个典型的epoch做例子for epoch in range(10): for step, (images, labels) in enumerate(train_loader): outputs model(images) loss criterion(outputs, labels) optimizer.zero_grad() loss.backward() optimizer.step() if step % 100 0: print(fEpoch {epoch}, Step {step}, Loss: {loss.item():.4f})里面的enumerate用来同时拿索引和数据zip用来同时遍历多个列表这两个都是Python基础里近乎顶级频率的迭代函数。如果你看到tqdm库出现from tqdm import tqdm for epoch in range(10): for images, labels in tqdm(train_loader, descfEpoch {epoch}): pass它只是给循环加了个进度条代码逻辑本身没改变。我建议你为了视觉体验把所有长循环都加上tqdm看着进度条走比盯着黑屏等结果要舒服得多也方便你估算每轮训练大概要多久。6.3 保存与加载模型时Python序列化给我们留下的坑训练完了要把模型保存下来。常见的做法有两种终止之前必须搞清楚# 只保存参数推荐 torch.save(model.state_dict(), model.pth) # 保存整个模型不推荐因为依赖类定义的位置 torch.save(model, model_full.pth)加载参数时必须先定义好模型的实例再调用load_state_dictmodel MLP(28*28, 128, 10) model.load_state_dict(torch.load(model.pth, weights_onlyTrue)) model.eval()这里有个新手很容易踩的坑训练时模型里有些层比如Dropout、BatchNorm的行为跟测试时不一样所以加载之后要调用model.eval()切换模式否则预测结果可能跟你预期差很多。这不算Python基础本身的内容但它体现了“你知道一个操作背后的原因”才不容易翻车。顺带一提保存模型时如果带了优化器状态你还能在中断训练后接着训练这对超长训练任务特别实用。7. 深度学习Python入门最常见问题与排查7.1 报错ModuleNotFoundError第一反应不是百度而是这个这是最常见的报错本质是“这个Python环境里没有你要的库”。绝大多数新手问题都出在环境搞混了你明明装了PyTorch怎么一运行就提示找不到大概率是你在base环境里跑脚本而PyTorch装在dl环境里。排查方式很简单which python python -c import sys; print(sys.executable)确认当前用的是哪个Python解释器以及它属于哪个conda环境。如果发现不对就conda activate dl切过去再跑。我之前遇到过一种诡异场景明明在同个shell里激活了环境第三方库还是装到了别的环境。这是因为在激活环境之前你已经用pip装过库而那个环境的Python路径被缓存了。此时重启终端或者用hash -r清理shell缓存就能解决。7.2 数据处理时IndexError与KeyError到底谁在说谎IndexError说的是你用一个超出范围的数字去取列表KeyError说的是你用了一个不存在的键去取字典。出现这些报错时不要急着改代码先打印一下数据的长度或字典的keys看看能不能对上print(len(data)) print(list(config.keys()))我在写数据加载器时最常遇到的场景是模型要求的输入形状和实际数据形状对不上比如模型期待(1, 3, 224, 224)而数据是(3, 224, 224)。这时候第一反应应该是用unsqueeze(0)在batch维度上补一维x torch.randn(3, 224, 224).unsqueeze(0) # 变成 1×3×224×224这是一个极小但至关重要的操作。很多新手的困惑其实就来自高维形状我建议每次报错涉及shape时都养成一个好习惯在每个关键步骤后面打印一下张量形状print(x.shape)亲眼看着形状演变比自己脑补靠谱得多。7.3 训练慢得像蜗牛真是代码写得烂吗有时候代码没问题但是训练特别慢一眼看上去像是被某个循环卡住了。这时候先别急着优化代码去看看是不是数据加载变成了瓶颈。我遇到过一次数据集放在机械硬盘上每个batch都要随机读取几千张小图片结果GPU在等待数据利用率上不去。解决方案是用DataLoader的num_workers参数异步预加载train_loader DataLoader(dataset, batch_size32, shuffleTrue, num_workers4)这对新手来说是个超实用技巧。但你也要注意在Windows上num_workers大于0时如果把代码放在if __name__ __main__:之外运行容易报多进程相关的错误。这是一种常见适配问题建议直接搜索解决方案不用深究到底层的进程细节。另一个性能问题是Python写大量循环——千万别让你的核心计算发生在Python的for循环里应该交给NumPy或PyTorch的向量化函数。这两个库底层用C/C实现1000万次运算可能几毫秒就搞定而Python的for循环可能要几秒钟。这个差距就是“向量化思维”的价值所在。7.4 新手最容易忽略的版本依赖顺序问题装库的时候新手习惯一把梭pip install torch numpy pandas matplotlib sklearn这个习惯在本地的干净环境里没问题但如果你是在已有环境里补装很容易出现“装A库把B库版本顶掉”的连锁反应。比如NumPy从1.x升到2.x很多旧版本库就不兼容了。我的建议是新项目一定要新建虚拟环境每次只安装这个项目需要的库不要一上来把网上的“全家桶”全装上。用pip freeze requirements.txt把当前环境依赖导出来然后在另一个环境里pip install -r requirements.txt这种操作对复现实验结果极其重要。如果实在搞坏了环境也不必紧张conda可以随时克隆或删除环境重装一个也就几分钟的事。我犯过最严重的错误是图省事直接在base环境里乱装导致系统Python被搞坏最后只能重装。现在我的建议只有两个字隔离隔离还是隔离。8. 一遍带过一个小型手写数字识别训练实战这一节我们把前面所有内容串起来跑一个最简单的完整例子。我用的是MNIST数据集PyTorch自带下载接口不需要额外准备数据。import torch import torch.nn as nn from torch.utils.data import DataLoader from torchvision import datasets, transforms # 1. 定义数据预处理 transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) # 2. 加载数据 train_dataset datasets.MNIST(root./data, trainTrue, downloadTrue, transformtransform) train_loader DataLoader(train_dataset, batch_size64, shuffleTrue, num_workers0) # 3. 定义模型 class SimpleNet(nn.Module): def __init__(self): super().__init__() self.fc1 nn.Linear(28*28, 128) self.relu nn.ReLU() self.fc2 nn.Linear(128, 10) def forward(self, x): x x.view(x.size(0), -1) x self.relu(self.fc1(x)) x self.fc2(x) return x # 4. 训练 model SimpleNet() criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr0.001) model.train() for epoch in range(3): for images, labels in train_loader: outputs model(images) loss criterion(outputs, labels) optimizer.zero_grad() loss.backward() optimizer.step() print(fEpoch {epoch}, last loss: {loss.item():.4f})实践完这个例子你会发现前面讲的所有东西都在里面了。你要理解每一步在做什么而不是背下来。尤其注意x.view(x.size(0), -1)这句它就是把(64, 1, 28, 28)的形状自动展平成(64, 784)方便喂进全连接层。这是一种“不关心具体大小、自动适配”的写法深度学习里特别常见。跑完如果报错对照第7节的内容排查。搞定之后换一下网络结构加一个卷积层或者把学习率调大调小你会对前面这些基础概念有更深一层的感觉。我个人实际做项目时的体会是Python基础压根不需要你达到刷算法题的水平但必须是“肌肉记忆”级别——看到列表推导式条件反射能读明白看到view、reshape、unsqueeze这些操作心里马上有张量形状的画面。这些东西不熟深度学习代码读起来就会像天书训练起来全是玄学。磨刀不误砍柴工环境准备好之后花一周时间把这些基础练熟后面跑模型的效率能高出一倍。如果这篇笔记里的代码你都能不依赖教程独立敲出来就可以直接进入下一课了。说实话深度学习本身的门槛不在数学也不在全英文文档而在你手里那门编程语言够不够顺手。Python不是你的终点只是你的刻刀。刻刀趁手了后面的雕塑才能做出来。
返回列表