一直有人问我《动手学深度学习》(D2L)这本书到底该怎么上手,我自己作为学习者,可以很负责任地讲:大多数人还没碰到模型训练那一步,就卡在安装环境和数据处理上了。
我当初也是这样。书是好书,内容体系也完整,但安装 d2l 依赖包、初始化张量、预处理数据这些环节,如果没踩过几个坑,真的会非常挫败。这篇笔记不是教科书式的复述,而是把我自己从环境搭建到张量操作、再到数据处理整个链路里那些最磨人、最容易被忽略的细节全部记录下来,希望能给正在啃这本书的朋友一份可以直接照着走的实操参考。
1. 环境不是装好就行,而是要为"少折腾"服务
D2L的书和代码本身跨平台,但真正打开代码开始跑的时候,环境不一致带来的问题比想象中多。很多初学者上来就 pip install d2l,然后以为万事大吉,结果跑 d2l.load_array 的时候发现数据集下载失败,或者是PyTorch版本和CUDA版本对不上,最后整个环境一塌糊涂。
1.1 为什么我建议从 Miniconda 而不是直接装 Python
我见过不少朋友是直接从官网下载Python,然后一路点下一步完成安装,后面再慢慢往里头 pip 塞包。这种做法在普通脚本开发里没什么大问题,但在D2L这种需要频繁切换版本、甚至需要隔离不同项目依赖的场景下,后面一定会后悔。
我自己坚持用Miniconda,理由很简单:它能给你一个相对独立、可重建、可回滚的运行环境。D2L这本书的代码更新比较频繁,而且网上能找到的各种配套代码版本也不完全一致,如果没有环境隔离,不同项目之间的包版本冲突会让人崩溃。
具体操作流程是:
bash复制# 创建一个专门用于 D2L 学习的虚拟环境
conda create -n d2l python=3.9 -y
# 激活环境
conda activate d2l
# 安装 PyTorch(CPU版,适合没独立显卡或不想折腾CUDA的朋友)
pip install torch torchvision
这里多说一句,为什么推荐Python 3.9而不是最新版本?因为D2L的代码基于PyTorch,而PyTorch对新版本Python的支持往往有一定滞后。不求最新,但求稳定,等你把书里代码跑通了,再考虑新特性和新版本也不迟。
注意:如果你不太确定PyTorch安装命令该怎么选,可以直接去PyTorch官网根据操作系统和CUDA版本生成对应命令。CPU版就是学深度学习初期最稳妥的选择,不用一上来就碰CUDA和cuDNN这些环境变量。
1.2 安装 d2l 包的正确姿势
d2l 这个包在PyPI上有,所以常规安装方法一句话就能搞定:
bash复制pip install d2l
但很多国内用户在装的时候会遇到一个非常磨人的问题——子进程报错。后面我单独用一章来讲这个坑,这里先给一个安全进入后续学习的方案:优先从国内镜像源安装,并在安装时加长超时时间。
bash复制pip install d2l -i https://pypi.tuna.tsinghua.edu.cn/simple --timeout 60
装完之后建议立刻验证一下能否正常导入:
python复制import d2l
print(d2l.__version__)
如果能正常输出版本号,说明环境基础已经通了。这时候千万别高兴得太早,你真正在D2L代码中接触最多的其实是 torch 和 torchvision,d2l 只是提供了画图、计时、数据集下载等辅助函数。
1.3 选择对应的内核和代码运行方式
我用的是Jupyter Notebook,因为D2L原版代码基本都是Notebook格式写出来的,直接照着跑能省不少事。但如果你是在VS Code里运行,需要保证VS Code当前选择的Python解释器就是你刚刚创建的那个conda环境。
这一步不设置好,经常会出现"明明我已经 pip install 了,但VS Code里 import d2l 还是报ModuleNotFoundError"的诡异问题。原因很简单:VS Code默认用的Python解释器不是当前环境。
给一个最简单的检查方法:
python复制import sys
print(sys.executable)
如果输出的路径不是你conda环境下的python,那就要在VS Code左下角重新选择解释器,或者手动指定kernel。这个细节虽小,但卡住的人真不少。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. d2l包下载子进程报错:完整排查链路与根治方案
这是我在多个交流群里被问得最多的问题,也是当时我自己卡住了最久的问题。先还原一下现场的报错信息,如果你也遇到过,应该会非常有共鸣。
2.1 报错的真实面目
我在干净的conda环境里直接跑:
bash复制pip install d2l
一开始进度条走得挺顺利,几分钟后莫名其妙地出现了类似这样的错误:
code复制ERROR: Command errored out with exit status 1
command: /home/user/anaconda3/envs/d2l/bin/python -c ...
cwd: /tmp/pip-install-xxxx/
...
ERROR: Subprocess output is empty or failed to decode
或者是:
code复制Downloading d2l-xxxx.tar.gz
...
ERROR: Subprocess-created file does not exist
如果你网速不好或者镜像源不稳定,还可能看到一堆网络相关的报错:
code复制ReadTimeoutError: HTTPSConnectionPool(host='files.pythonhosted.org', port=443): Read timed out.
这些报错虽然最后提示的都不太一样,但本质上是同一类问题——pip在下载后解压包或者构建元数据时,因为网络/源/依赖解析等多方面原因,子进程退出了。
2.2 我当时的排查思路(一步一步来)
我第一次遇到的时候也慌,以为是自己电脑坏了,或者是版本不兼容。后来强迫自己冷静下来,按照下面的路径一步步排查,才真正定位到问题根源。
第一步,确认网络连通性。直接在终端里 pip download d2l -d /tmp/d2l_test --no-deps,看能不能顺利把这个包拉下来。如果这一步就超时或中断,说明问题在网络层,跟包本身没有关系。
第二步,检查pip版本。pip --version,如果pip版本太老,会对某些新版包的构建方式不兼容。直接升级:
bash复制pip install --upgrade pip
第三步,看完整日志,不要只看最后三行报错。pip支持 -v 参数,可以把详细执行过程打出来:
bash复制pip install d2l -v
通过完整日志大概率能看到它在尝试访问哪个源、卡在哪个环节、子进程的退出码是什么。退出码比最后提示的英文更直白。
第四步,换源。把默认的官方PyPI改成国内镜像源,速度提升非常明显:
bash复制pip install d2l -i https://pypi.tuna.tsinghua.edu.cn/simple
第五步,给pip加超时和重试参数。如果网络环境确实不稳定,可以这样:
bash复制pip install d2l --default-timeout=120 --retries=5
2.3 我最终是怎么解决的
在一次完整的日志输出里,我发现它其实是卡在从GitHub下载某个数据文件上。d2l 这个包在构建时有些元数据需要从外部资源获取,所以国内网络环境一波动,子进程就会"假死"或者直接退出。
最终我的方案是:
- 先单独手动下载
d2l的源码包或wheel包; - 然后本地安装:
bash复制pip install /path/to/d2l-xx.whl
如果没有提前下载好,也可以直接从国内镜像源安装,然后不要使用最新版的 d2l,指定安装某个我测试过稳定的版本。比如:
bash复制pip install d2l==0.17.6 -i https://pypi.tuna.tsinghua.edu.cn/simple
版本号不一定要完全一样,但核心思路是:不要永远追最新,D2L书里的代码很多时候是基于某个历史版本写的,新包改动可能带来函数签名或行为上的细微差异,反而会让学习受阻。
2.4 事后防坑措施
这个问题根治之后,我给自己定了三条规矩:
- 每次新建环境,先
pip install --upgrade pip; - 固定用国内镜像源安装第三方库,不裸连官方源;
- 装包前先用
pip download测试连通性,再做正式安装。
这三条真的能帮你省下大量排查环境问题的时间。
3. 张量操作:从创建到广播,把概念落到代码上
张量是D2L全书最核心的数据结构,说白了就是多维数组。但和NumPy数组相比,PyTorch的张量多了一些特性,比如自动求梯度、能在GPU上加速计算等。一开始别想得太复杂,你就把它当作"带GPU加速能力的多维数组"来理解。
3.1 创建张量的几种方式
我是从最基础的 torch.arange 和 torch.zeros 入手的。具体来说,常用的创建方式有这些:
python复制import torch
# 从列表创建
a = torch.tensor([[1, 2, 3], [4, 5, 6]])
# 全0张量
b = torch.zeros((2, 3))
# 全1张量
c = torch.ones((3, 2))
# 从范围创建
d = torch.arange(10)
# 随机张量(均匀分布)
e = torch.rand(3, 4)
# 正态分布随机张量
f = torch.randn(3, 4)
我在学习过程中最大的体会是:动手前先确认张量的 shape 和 dtype。D2L里的代码会频繁用到矩阵乘法、reshape、广播等操作,形状不匹配是报错率最高的地方。
3.2 dtype、device 和形状转换的细节
这是新手最容易被绊倒的地方。我自己也曾经因为把 torch.float64 和 torch.float32 混用,导致后续模型训练时Loss值怎么都不收敛。
PyTorch的默认浮点类型是 torch.float32,这一点和NumPy的默认 float64 不一样。如果你从NumPy转过来,很容易踩中这个暗坑。
python复制# 查看dtype
print(a.dtype)
# 显式转换
a_float32 = a.float()
a_float64 = a.double()
设备(device)也要注意。你创建一个张量,默认是在CPU上。想做GPU加速,要把张量显式移到GPU:
python复制if torch.cuda.is_available():
a_gpu = a.to('cuda')
但在早期学习阶段,CPU上跑就完全够用了,没必要为了"体验GPU"而去配一个自己都不熟悉的环境。把基础概念用CPU跑通,某种意义上比强行上GPU更扎实。
关于形状转换,最常用的是 view 和 reshape。两者的区别是很多初学者混淆的重灾区:
view:要求张量在内存中是连续的,它共享底层数据,不会额外复制;reshape:更灵活,当原张量不连续时,它会自动复制数据以保证结果正确。
python复制x = torch.arange(12)
# 两种情况结果看起来一样,但底层逻辑不同
y1 = x.view(3, 4)
y2 = x.reshape(3, 4)
提示:如果你不确定原张量是否连续,用
reshape一定不会出错,代价是可能多一次内存拷贝。在写D2L的练习代码时,统一用reshape能减少很多不必要的认知负担。
3.3 索引、切片和赋值的坑
张量的索引和切片使用方式和Python列表很像,但有一个需要注意的地方:切片产生的张量是原张量数据的视图,不是副本。这意味着你对切片后的张量做修改,原张量也会跟着变。
python复制x = torch.arange(12).reshape(3, 4)
sub = x[0:2, :]
sub[:] = 0
# 此时x也会被修改
这一点在数据预处理时特别容易出问题。如果你不希望修改原数据,记得显式用 .clone():
python复制sub = x[0:2, :].clone()
3.4 广播机制:一种"自动扩展"的规则
广播机制是理解张量运算的关键。简单来说,当两个张量形状不完全相同但满足一定条件时,PyTorch会自动扩展它们,使它们能够进行逐元素运算。
规则可以归结为两条:
- 从最后一个维度往前对齐,如果维度大小相同,或者其中一个为1,或者一个维度不存在,则可以广播;
- 如果一个维度的大小为1,则它会被拉伸到和另一个张量对应的维度相同大小。
看一个D2L里几乎立刻就会遇到的例子:
python复制a = torch.ones((3, 2))
b = torch.tensor([10, 20])
c = a + b
# c的每个元素会自动加上[10, 20]
这个例子里,b 的形状是 (2,),会先变成 (1, 2),然后广播成 (3, 2),再与 a 相加。
我做过的另一个更直观的例子:
python复制x = torch.arange(6).reshape(2, 3)
y = torch.tensor([100, 200, 300])
z = x + y
# 每行都加上了[100, 200, 300]
我理解广播机制的方式是:把维度对齐,从右往左看,要么一样,要么其中一个是1,否则就不能广播。其实这个特性和NumPy完全一致,如果你之前熟悉NumPy,学起来会非常快。
3.5 自动求梯度时的张量注意事项
张量的另一个核心特性是 requires_grad。在D2L前几章就会接触反向传播,那时候你会发现,对张量原地修改(in-place)非常危险。
python复制w = torch.randn(3, requires_grad=True)
# 如果之后做了反向传播,再对w做原地修改
w.add_(1)
# 梯度计算会报错
因为原地操作会破坏PyTorch构建的计算图,导致反向传播时找不到原始数据。所以我的习惯是:对于需要求梯度的参数,永远不要做原地修改,而是用 w = w + 1 这种创建新张量的方式。
4. 数据处理流水线:从原始文件到可训练样本
D2L里不管是线性回归还是softmax回归,示例数据一般都已经处理成可以直接喂给模型的张量。但现实中你接触到的数据很可能是一堆Excel表格、图片文件或者文本文件,不可能直接送到神经网络里。所以数据处理那段内容虽然看起来不起眼,实际上是你自己开始做项目时的救命工具。
4.1 文本表格数据的处理
D2L这本书在数据处理这一节用了一个真实的例子:加载一个CSV格式的数据集,然后处理缺失值。这种场景在真实业务中太常见了。
我的处理套路如下:
python复制import pandas as pd
import torch
# 1. 读取原始数据
data = pd.read_csv('data.csv')
# 2. 分开特征和标签
x_features = data.iloc[:, 0:2]
y_labels = data.iloc[:, 2]
# 3. 处理缺失值(数值列用均值填充)
x_features = x_features.fillna(x_features.mean())
# 4. 把类别列转换成数值列(get_dummies)
x_features = pd.get_dummies(x_features, dummy_na=True)
# 5. 转换为张量
x_tensor = torch.tensor(x_features.values, dtype=torch.float32)
y_tensor = torch.tensor(y_labels.values, dtype=torch.float32)
这套流程里最容易被忽略的是第三步。如果你只把缺失值删掉,样本量会越来越少,而且在真实数据里,某些列缺失比例高达30%,直接删除会让模型学不到足够的信息。用均值填充是有一定统计学基础的简单处理方式,适合入门阶段使用。
提示:D2L原书在这一节用的是
pandas和torch的组合,用的是torch.tensor直接转换。数据量小的时候没问题,但数据量大时建议torch.from_numpy(...)性能更好一点。
4.2 图像数据的预处理链路
图像数据比表格数据多一层"预处理尺寸调整、像素归一化、张量转换"的流程。我在D2L里跑图像分类任务时,每次加载图片前都会这样做:
python复制from torchvision import transforms
transform = transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])
])
这段代码做了三件事:
Resize:把所有图像统一调整到相同尺寸。不统一尺寸,后面进入卷积层时输出的特征图尺寸对不上,全连接层也没法计算;ToTensor:把PIL图像或NumPy数组变成张量,同时把像素值从[0, 255]缩放到[0.0, 1.0],并且把通道维度提前到第一维(C, H, W);Normalize:用ImageNet数据集的均值和标准差做标准化,让每个通道的分布更接近标准正态分布。
这套流程看起来简单,但每一步都有它存在的理由。比如 ToTensor 为什么要把像素从0到255缩放到0到1?因为神经网络的激活函数(如Sigmoid、ReLU)对输入范围比较敏感,过大的输入值可能导致梯度消失或者梯度爆炸。归一化之后的0到1范围对模型训练是最友好的。
4.3 Dataset 与 DataLoader 的协作关系
在D2L的代码里你会经常看到自定义Dataset类,尤其是做图像分类时。很多初学者不理解为什么需要这个封装。
我当时理解的方式是这样的:Dataset负责"怎么取一个样本",DataLoader负责"怎么把一堆样本打包送进模型"。
python复制from torch.utils.data import Dataset, DataLoader
class MyDataset(Dataset):
def __init__(self, features, labels):
self.features = torch.tensor(features, dtype=torch.float32)
self.labels = torch.tensor(labels, dtype=torch.float32)
def __len__(self):
return len(self.labels)
def __getitem__(self, idx):
return self.features[idx], self.labels[idx]
dataset = MyDataset(x_features.values, y_labels.values)
dataloader = DataLoader(dataset, batch_size=32, shuffle=True)
这里的核心接口就是 __len__ 和 __getitem__。一旦你习惯了这种写法,后面的数据载入、小批量训练就顺理成章了。
我在D2L里用得最多的其实还是 d2l.load_array,它内部做了类似的事情。但理解它背后的原理,才能在遇到内存不足、batch size需要调整、shuffle逻辑需要定制这些问题时,自己动手解决。
4.4 从单条样本到批量训练
为什么D2L书中到处强调 batch_size?因为它决定了每一次梯度更新使用多少条样本。如果太小,梯度噪声大,训练不稳定;如果太大,内存可能不够,而且训练速度并不会一直提升。
我自己的经验是:先把batch size设成一个常见的值(比如32、64),跑通了再根据显存或内存情况调整。不要一上来就用超大batch size,因为那你可能先去解决内存分配问题,而不是理解模型本身。
当你的数据是从DataLoader里取出来时,每次拿到的其实是一个批次(batch)的数据,形状是 (batch_size, 特征维度)。这个观念一定要在早期建立起来,因为后续所有的网络设计、损失函数计算,都是基于"张量的第一个维度是batch"这个习惯来写的。
4.5 重现实操:从零构造一个微型数据Pipeline
我自己在学数据处理时写过一个极简Pipeline,每次想验证某个模型时都会复用,贴在这里供你参考:
python复制import torch
import numpy as np
from torch.utils.data import DataLoader, TensorDataset
# 生成模拟数据
num_samples = 1000
num_features = 10
X = np.random.randn(num_samples, num_features).astype('float32')
y = np.random.randn(num_samples, 1).astype('float32')
# 包装成TensorDataset
dataset = TensorDataset(torch.from_numpy(X), torch.from_numpy(y))
dataloader = DataLoader(dataset, batch_size=32, shuffle=True)
# 验证一个batch
for features, labels in dataloader:
print(features.shape, labels.shape)
break
这段代码的好处是:不依赖外部数据集,能快速验证模型是否能跑通、shape是否正确、loss是否下降。每次换模型前,我会先跑一遍这个微型Pipeline,确保环境没问题,再去接真实数据。
5. 新手最容易忽略的几个"坏习惯"与应对
最后这部分更像是我踩过坑之后的复盘。环境装好了,张量会建了,数据也能加载了,但如果你在编码习惯上栽跟头,前面所有的准备都白搭。
5.1 常看 shape,不要靠猜
这不是玩笑话。我见过很多新手,包括我自己早期,写一个计算"错误地"拼错了维度,花了一晚上都找不到原因。后来我养成了一个近乎强迫症的习惯:每操作一个张量,就打印一下它的 shape。
python复制print(x.shape)
print(y.shape)
print((x + y).shape)
几个 shape 打印下去,绝大多数shape不匹配的问题都能一眼看出来。
5.2 不要轻易修改默认的 dtype 和 device
很多人在数据转换环节喜欢用 torch.float64,因为觉得"精度越高越好"。但在PyTorch里,默认的浮点类型是 torch.float32,如果你把一个 float64 的张量送进模型,可能会出现类型不匹配的错误,或者模型训练变慢,因为GPU对 float64 的计算支持远不如 float32。
同样的,设备(device)也要保持一致。你选CPU就都用CPU,选CUDA就统一 .to('cuda'),不要混着来。
5.3 固定随机种子
看D2L代码的时候,你会发现他们经常设置随机种子,但不会反复强调。我自己跑的实验多了之后才意识到,深度学习训练本身有随机性,如果不固定种子,你复现出来的结果可能和别人的完全不同,甚至自己跑两次结果都不一样。
python复制torch.manual_seed(42)
np.random.seed(42)
虽然这不能百分之百保证结果完全一致(因为PyTorch某些底层操作在GPU上还是有非确定性),但至少能把环境差异降到最低。
5.4 环境复现清单
最后,我在跑完D2L前半部分之后,给自己整理了一份"环境复现清单":
bash复制conda create -n d2l python=3.9 -y
conda activate d2l
pip install --upgrade pip
pip install torch torchvision -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install d2l==0.17.6 -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install pandas matplotlib ipykernel jupyter
这个清单只解决了一件事:在任何新电脑上重建一个可用的D2L学习环境,不超过10分钟。
我还习惯把环境的依赖导出成一个文件,方便之后恢复:
bash复制pip freeze > requirements.txt
这样即使某天环境被弄坏了,也能快速重建,而不是重新踩一遍所有坑。
