说实话,写这篇笔记之前我犹豫了一下。深度学习这个系列走到第3篇,按理说应该开始碰模型了,我却停下来补Python基础——而且标题里那个“Pyhton”我都没改。这恰恰说明一个问题:基础里那些看似“很简单”的细节,才是真正卡住新手的东西。环境装完、教程打开,满屏的transpose、broadcast、view、tensor.shape,每一条都认识,但放一起就是不知道怎么用。
这篇不是给你背语法的Python教程,而是专门为深度学习准备的那部分Python基础:环境怎么搭不踩坑、语法里哪些写法在模型代码里出现频率最高、NumPy要从什么角度去学才能无缝过渡到张量操作,以及最常见的报错和误区到底怎么排查。写给我的目标读者只有两类人:一是跑完了环境配置教程但一打开模型源码就发懵的新手,二是会用Python写爬虫、写脚本,但一进深度学习就总觉得哪里不对的同学。你只要跟着这篇把思维扭过来,后面啃PyTorch源码会轻松一大截。
1. 先把框架搭起来:这份Python基础笔记到底学什么
1.1 一条主线:从“写Python”到“搭网络”的思维切换
我见过太多人学Python学得很努力,速成课刷了好几遍,列表、字典、循环都用得很溜,结果一打开深度学习的开源项目还是看不懂。问题不在于他Python差,而在于他学的Python和深度学习需要的Python根本是两个方向。
普通Python开发的核心是“业务逻辑”:处理文件、写接口、爬数据、操作数据库。深度学习的核心则是“数据张量流”:数据进来是什么形状,经过一层层变换变成什么形状,最后输出什么形状。你在深度学习代码里看到的绝大多数操作都可以归到两个词上:一个是reshape,一个是变换。所以你的Python基础必须围绕一个核心主线来学——从数据结构思维转向张量形状思维。
拿最常见的例子来说。一个新手指着一行代码问“为什么这里要加x = x.view(-1, 784)?”因为它要把三维或四维的图片数据摊平成二维矩阵,才能喂给全连接层。这在普通Python开发里你永远碰不到,但在深度学习里到处都是。所以这篇笔记所有内容都会朝着这个方向去组织,不是罗列知识点,而是告诉你哪个知识点在读模型代码、训练脚本、数据预处理时真的会反复出现。
1.2 Python凭什么成为深度学习的事实标准
这个问题值得想清楚,因为想清楚了,你就知道为什么“Python基础”不是可学可不学,而是必学。市面上做深度学习的语言不少,C++、Java、Julia都有人用,但最后大家默认选的还是Python,原因不只是“简单”。
最核心的原因有两个。第一,生态。PyTorch、TensorFlow、JAX、scikit-learn这些库全是Python优先,深度学习领域的新论文,基本当天或一周内就会放出Python版本的复现代码。你想复现别人的模型,不用Python等于把自己排除在整个学术社区之外。第二,开发效率。深度学习的核心运算其实不归Python管,而是C++和CUDA在底层执行,Python只是一个“指挥者”。你用Python写一行model.train()、loss.backward(),底层是几千行高性能代码在跑。Python本身慢不慢根本不重要,重要的是它能让你快速把想法组装起来。
为了让你有个直观对比,我把主流深度学习开发方案的差异列一下:
| 方案 | 开发效率 | 性能 | 生态成熟度 | 适合人群 |
|---|---|---|---|---|
| Python + PyTorch/TensorFlow | 极高 | 底层C++/CUDA优化,性能无损 | 最成熟,资料最多 | 绝大多数研究者、工程师、学生 |
| C++ + TensorFlow C++ API | 低 | 优 | 中等 | 需要极致的部署性能,或嵌入式场景 |
| Java + DJL/Deeplearning4j | 中等 | 中等 | 较弱 | 后端整合到Java系统的场景 |
| MATLAB | 中等 | 中等 | 较弱 | 传统工科实验教学、信号处理 |
现在大家应该明白了,Python在深度学习里的地位不是因为它“容易学”,而是因为它站在了正确的位置上——既能让你快速表达想法,又把高性能计算外包给了底层语言。所以接下来环境搭建、语法速通、NumPy思维,全部奔着一个目的去:让你尽早写出第一行能跑起来的深度学习代码,并且每写一行都知道它在干什么。
2. 环境搭建别偷懒:Anaconda与虚拟环境的正确打开方式
2.1 别从官网裸装Python,直接用Miniconda
我见过很多小白第一步就走错了:去python.org下载一个Python装好,然后开始pip install。你要是在深度学习这条路上也这么干,后面大概率会踩到两个坑:一是Python版本和某个库不兼容,二是不同项目依赖的库版本互相打架。这两个坑都极其难排查,浪费的时间绝对超过你省下的那几分钟安装时间。
正确的做法是直接用Miniconda。Miniconda和Anaconda是同一个东西的两个版本,Anaconda自带了一堆科学计算包,装完即用但体积巨大;Miniconda只带一个conda包管理器和最基本的Python,需要什么自己装。我更推荐Miniconda,因为干净、可控,你能清楚地知道自己装了什么,而不是装了一堆用不上的库。
安装完之后不要急着用默认的base环境。我的习惯是单独建一个专门跑深度学习的虚拟环境,命令是这样:
conda create -n dl python=3.10 numpy matplotlib jupyter ipykernel这条命令做了三件事:创建一个叫dl的虚拟环境、指定Python 3.10版本、顺手装好numpy和matplotlib这两个后面一定会用到的库。等它跑完,激活环境:
conda activate dl你会看到命令行前面多了个(dl),这就说明当前在虚拟环境里了。后面所有pip install、跑代码、启动Jupyter,都要确保在这个环境下进行。激活环境这个动作,说真的,我见过太多人忘了,然后明明装好的库,运行脚本时却报ModuleNotFoundError,就是这个原因。
2.2 每个项目一个虚拟环境,这个习惯能救你一命
虚拟环境的好处用一句话说就是:不同项目之间依赖隔离。
举个例子。你项目A用的是PyTorch 1.13,项目B需要PyTorch 2.1,两个版本的API有差异。如果你把两个都装到同一个环境里,轻则每次运行都要小心翼翼看版本,重则直接互相覆盖导致环境崩溃。用虚拟环境就很简单:项目A建一个env_a,项目B建一个env_b,各装各的,互不干扰。
我在实际中建议再进一步,把一个环境里的依赖关系导出成文件,方便换机器或分享给你同学时一键复现:
conda env export > environment.yml conda env create -f environment.yml这个习惯在刚入门时可能感觉不到价值,等你训练到一半要换GPU服务器、或者实验室新同学要复现你的环境时,你会感谢自己当初多敲了这一行。
还有一个小细节:装包时优先用conda装,conda装不到再用pip。原因是conda不仅管Python包,还会帮你处理一些非Python的底层依赖,比如某些库需要C库支持,conda能一并搞定,而pip只管Python层面的东西。混用的时候也不可怕,记住“先conda后pip”的顺序就好。
2.3 Notebook做探索、VSCode写工程
环境搭好了,还得有趁手的工具。我见过一些新手只用Jupyter Notebook写代码,也有人只用VSCode,其实这两个工具不是二选一,而是各司其职。
Jupyter Notebook适合做探索性实验。你会想加载数据、看形状、试几个变换、画个图看看效果,这种“写一行看一行”的场景Notebook是最舒服的。它还能直接内嵌显示图像和训练曲线,做数据可视化探索非常直观。我通常在Notebook里完成数据探索、模型原型验证、可视化这几个步骤。
VSCode适合写正式的可复用代码。比如你开始把数据处理封装成函数、把模型定义写到单独的.py文件里、要训练一个完整的脚本,这时就需要一个正经的编辑器。VSCode装好Python插件后,调试、代码提示、查看变量都很好用。它对新人非常友好的一点是集成了可视化断点调试,你可以在某一行设个断点,代码跑到这里就会停下来,然后左侧面板能看到当前所有变量的值——这对排查bug的理解效率,比print高一个量级。
顺带说一个画图的实际小坑。训练完画曲线时,横坐标是几百个epoch就会挤成一团,看得眼晕。解决方案很简单:
import matplotlib.pyplot as plt plt.plot(range(1, 101), losses) plt.xticks(rotation=45) # 横坐标旋转45度,避免标签重叠 plt.show()或者用plt.locator_params(nbins=20)让横坐标只显示20个刻度。这些都是实战里经常要用到的小技巧,教程里一般不教你,但自己跑起来一定会碰到。
3. Python语法速通:深度学习里真正高频的那些写法
3.1 变量是引用,不是盒子:先懂这一条,能省一半调试点
很多新手对Python变量有一个错误的理解:变量就像一个盒子,往里装值。比如b = a会以为把a复制了一份给b。但这个理解是错的,而且如果在深度学习代码里抱着这个想法,你会遇到一些非常诡异的问题。
Python里变量更像是贴在对象上的标签。执行b = a时,并没有复制对象,而是给同一个对象又贴了一张标签。你通过b修改内容,a看到的也跟着变,因为本就是同一个对象。看这个例子:
a = [1, 2, 3] b = a b.append(4) print(a) # 输出 [1, 2, 3, 4],a 也被改了这在深度学习代码里特别危险。比如你加载了一个数据集列表,不小心写了new_data = old_data,然后对new_data做了一些预处理(比如归一化、打乱顺序),结果old_data也被一并改了,训练集和验证集就混在一起了。要让复制真正发生,得显式写b = a.copy(),或者import copy; b = copy.deepcopy(a)。
还要区分可变和不可变类型。整数、浮点数、字符串、元组是不可变的,你的操作会生成新对象;列表、字典、集合、NumPy数组、PyTorch张量是可变的,原地操作会直接影响原对象。我在调试里遇到最多的问题之一,就是有人无意中共享了同一个list对象,改了这里影响了那里。入门期就把“变量是引用”这条刻在脑子里,后面能省掉一半的调试点。
3.2 列表、元组、字典:选对容器,代码自然就顺了
Python常用的容器就是列表、元组、字典这三样,深度学习里各有各的用途。
列表用来装同类的样本集合,比如一个训练集就是一堆图像的列表,每个元素是一张图。列表可以增删改查,天然适合“动态收集”的场景。元组和列表长得很像,但它创建后就不能修改,通常用来表示“固定结构的数据”:比如数据集里每一条样本是(image, label)这样的二元组,前半张图片后半标签,这个结构不会变,用元组就比用列表更合适。PyTorch的Dataset类里,重写__getitem__方法时返回的基本都是元组。
字典专门用来做“名称到值”的映射。在深度学习代码里,字典最常见的用法是保存超参数配置,然后在训练脚本里读取:
config = { "batch_size": 32, "learning_rate": 0.001, "epochs": 50, "optimizer": "adam", }这种写法比你写一堆散落的变量要清晰得多。更高级一点的做法是把所有参数集中在一个.py文件里,后面想复现实验时一键读取。还有一类代码里经常会看到**kwargs,它本质上就是帮你把字典里的键值对拆开作为函数参数传入,理解了字典,这个语法也顺理成章了。
选对容器的判断标准其实很简单:如果你需要“一对一”的键值映射就选字典,如果是“一串东西”且中途会增删就选列表,如果是“固定结构的一包数据”就选元组。顺着这个标准走,代码会自然变得可读、可维护。
3.3 切片是深度学习的“第一语法”
这一小节我想起强调再强调都不为过。切片就是类似a[1:3]这种写法,看起来只是取数据的一部分,但在深度学习里,它是你操作张量数据的核心手段。
先说最基础的形态。切片的完整语法是start:stop:step,start是起始位置(包含),stop是结束位置(不包含),step是步长。三个都可以省略:a[:]取全部,a[::2]隔一个取一个,a[::-1]倒序。字符串、列表、元组、NumPy数组、PyTorch张量全都支持这套规则。
但真正体现切片威力的是在多维数据上的应用。深度学习里图像数据通常是四维张量,形状是(batch, channel, height, width),代表“这一批有多少张图、每张图几个通道、高多少、宽多少”。你想取出批里的第一张图,就写x[0, :, :, :];你想把所有图的R通道抽出来,就写x[:, 0, :, :];你想取前8张图、每张图只取左上角64x64区域,就写x[:8, :, :64, :64]。
这一点和普通Python列表的切片没本质区别,但因为它作用在多维数据上,覆盖了多个维度,很多人一开始会懵。我的建议是别空想,直接在Notebook里创建一个形状为(4, 3, 32, 32)的随机数组,比如np.random.rand(4, 3, 32, 32),然后一步一步用切片切它,每切一次就打印结果的shape,亲手操作几次你就彻底懂了。
3.4 函数与类:PyTorch模型就是挂着类的外衣的函数流
函数在Python里很好理解,一段能重复调用的逻辑块。深度学习里你到处都会遇到函数:定义数据预处理函数、定义损失计算函数、定义训练循环函数、定义评估函数。把这些逻辑拆成函数,是让代码可读、可调试的关键。
类在深度学习里的地位更特殊,因为PyTorch的模型就是这样定义的。给你看一个最典型的模型定义代码:
import torch.nn as nn class MyModel(nn.Module): def __init__(self): super().__init__() self.fc1 = nn.Linear(784, 256) self.relu = nn.ReLU() self.fc2 = nn.Linear(256, 10) def forward(self, x): x = self.fc1(x) x = self.relu(x) x = self.fc2(x) return x看不懂细节没关系,你已经能看出骨架:__init__里“搭架子”,把网络各层定义好;forward里“传数据”,把输入一层层传下去,最后返回输出。这个类做的事情,本质上就是把一串函数串联起来,所以我说它“挂着类的外衣的函数流”。
当你打印一个模型实例时,print(model)会把每层的信息展示出来,这是什么原理?因为类的__repr__方法被nn.Module实现了,所以打印时会输出易读的层次结构。这类底层机制你现在不需要深究,但理解“类=属性 + 方法”,会帮助你后面看懂面向对象的深度学习代码。
4. 用NumPy建立张量思维:形状、轴与广播是核心
4.1 为什么必须先过NumPy这关:Tensor的底层就是ndarray
如果你直接跳进PyTorch,虽然也能跑,但很多操作你会知其然不知其所以然。PyTorch的Tensor底层设计思路和NumPy的ndarray一脉相承,你在NumPy里学的形状操作、广播机制、轴的概念,几乎都能平移到Tensor上。
而且TensorFlow和PyTorch的数据预处理阶段,用NumPy的地方比用深度学习库本身还多。读图片、做归一化、拼接batch、数据增强前后的形状管理,这些全都跑在NumPy上。换句话说,NumPy就是深度学习中“数据操作”这门课本身。
NumPy的入门关键又是一个核心概念:万物皆数组,数组皆有形状。创建一个数组:
import numpy as np a = np.array([[1, 2, 3], [4, 5, 6]]) print(a.shape) # (2, 3) print(a.ndim) # 2 print(a.dtype) # int64shape返回一个元组,描述了每个维度上有几个元素。ndim告诉你有几个维度。这组数字就是你接下来要每天盯着看的核心线索。在调试时,我几乎默认在任何可疑操作后都打印一下.shape。
4.2 shape、axis、广播:三个最容易懵的概念
很多新手的困惑集中在三个概念上:shape、axis和广播。我逐个说透。
shape就是一个元组,描述数组各维度大小。(3, 4)表示3行4列的二维数组,(2, 3, 4)表示由2个(3, 4)矩阵组成的三维数组。你可以把多维数组想象成俄罗斯套娃:维度每增加一层,就在上一层套娃里面多套一层。
axis是指操作沿哪个方向进行。这条真的要好好理解,因为你会在各种函数的参数里看到它。拿二维数组来说,axis=0表示沿行方向(从上到下)操作,axis=1表示沿列方向(从左到右)操作。np.sum(a, axis=0)得到的结果是对每一列求和,形状从(2, 3)变成(3,);np.sum(a, axis=1)则是对每一行求和,形状变成(2,)。
广播是让不同形状的数组能直接做运算的规则,也是初学者最懵的地方。最简单的理解:NumPy会从尾部开始对比两个数组的维度,如果两个维度相等,或其中一个为1,就可以对齐并广播。看个实际例子:
a = np.zeros((3, 1)) # 形状 (3, 1) b = np.ones((1, 4)) # 形状 (1, 4) c = a + b # 结果形状 (3, 4)这里a的(3,1)和b的(1,4)都不完全相等,但通过广播,a被隐式扩展成了对每一列重复,b也被隐式扩展成了对每一行重复,最后加出来一个(3, 4)的矩阵。这个机制在深度学习里到处都是,比如给所有样本加上同一个偏置向量、批量归一化(BatchNorm)之类的操作,底层全是广播。
4.3 一个手写数字的例子,帮你把形状变化刻进直觉里
理论说再多都抽象,直接看一个实际例子。假设你加载了一批手写数字图片,形状是(32, 1, 28, 28),意思是32张图、每张单通道、28x28像素。
第一件事,很多模型要求输入是一维向量,所以要把每张图“摊平”:
x_flat = x.reshape(32, 784) # 28*28 = 784这里reshape(32, 784)把四维数据变成了二维:32行,每行是784个像素值。-1这个参数在任何维度上都可以用,表示“这一维大小由系统自动推导”。比如写x.reshape(-1, 784),系统会根据总元素数自动算出第一维是32。我特别推荐你养成用-1的习惯,因为代码里常会出现你不确定具体批大小的情况。
但有时候你又会遇到反向的问题。图片已经是一维了,但卷积神经网络需要二维的矩阵形式,于是:
x_2d = x_flat.reshape(32, 1, 28, 28) # 变回四维再比如,某模型要求通道在最后一维(很多深度学习库的数据格式是(batch, height, width, channel)),但你的数据是(batch, channel, height, width),这时候要用transpose:
x_transposed = x.transpose(0, 2, 3, 1)这行代码的意思是把原数组的第0维放在新数组第0维,原第2维放在新第1维,原第3维放在新第2维,原第1维放在新第3维。
如果你只做一次可能会绕晕,我的建议是完整跑一遍下面的流程:创建形状(32, 1, 28, 28)的随机数组→reshape(32, 784)→打印形状→reshape(32, 1, 28, 28)→打印形状→transpose(0, 2, 3, 1)→打印形状。每一步的shape变化亲手打出来,胜过背十遍规则。我当时就是靠这一套操作,把“形状”这个概念彻底焊死在脑子里了。
5. 调试与避坑:别人踩过的坑,你最好别踩第二次
5.1 新手最容易踩的六个坑速查表
我在带新人和自己学习的过程中,见过、踩过的坑其实蛮有共性的。整理成一个速查表,比只说一个具体报错更有价值:
| 坑点 | 表现 | 原因 | 解决 |
|---|---|---|---|
| 环境没激活 | 明明装好了库,运行却提示ModuleNotFoundError | 当前终端不在虚拟环境里,或Notebook内核选错 | conda activate dl,确认命令行前缀;Notebook里检查kernel是否对应dl |
| pip装错版本 | 装PyTorch后import torch报错 | 装了不匹配的版本或CPU版当GPU版用 | 按照官网对应CUDA版本的命令安装,用nvidia-smi确认驱动版本 |
| shape搞错 | 报错信息里出现“size mismatch”之类 | 数据维度不是模型期望的输入维度 | 逐层打印x.shape,定位到具体哪一层不匹配 |
| 广播规则破防 | 两个形状完全不兼容的数组直接相加报错 | 对广播规则理解不到位 | 回到(3,1)与(1,4)的例子,重学广播三条规则 |
| 数据没归一化 | 训练loss不降或非常大 | 输入值域差距过大(比如0-255和0-1) | 除以255或标准化,检查数据范围 |
| matplotlib显示问题 | 中文变方块、横坐标挤成一团、图不显示 | 字体配置、刻度重叠、Notebook缺魔法命令 | plt.rcParams['font.sans-serif'] = ['SimHei'];加rotation=45;Notebook里加%matplotlib inline |
第3个坑我单独多说几句:shape不匹配是新手报错里出现频率最高的一类。它会让你盯着屏幕怀疑人生,但排查思路其实很清晰。比如报错信息写mat1 and mat2 shapes cannot be multiplied (32x784 and 256x10),就是在提示你:左边矩阵是32x784,右边是256x10,矩阵乘法要求左边列数=右边行数,这里784不等于256,所以炸了。定位方法就是在出错的层前打印一下输入形状:print(x.shape),一路打印过去,总能在第一处对不上的地方发现问题的根源。
5.2 关于“parameter是不是mb”:一句话说清参数量纲
这个问题是我从搜索引擎里看到很多人在问的,确实值得专门讲一下。有人看到模型描述里写“参数量7B”,有人看到模型文件大小是4GB,就开始疑惑:parameter到底是不是MB?
一句话回答:parameter的数量单位是“个”,不是MB;MB是存储大小单位。
一个线性层nn.Linear(784, 256)的参数个数是784 * 256 + 256 = 200960,大约20万个。多出来的256是偏置项。这个参数数量跟字节数怎么换算?要看权重存成什么精度。用float32表示,每个参数占4字节,那么这20万个参数占200960 * 4 = 803840字节,约786KB。如果一个模型有70亿个参数(比如大型语言模型的“7B”),用float16表示,每个参数占2字节,那么光权重就需要70e9 * 2 = 140e9字节,也就是约140GB。所以你看,参数数量和文件大小是两个维度的事,别把它们混在一起。
在深度学习讨论中,大家说“模型的参数量”就是指有多少个可学习的权重,说“模型大小”通常是指这些权重的存储字节数。这两个指标一个看模型“规模有多大”,一个看“跑起来要占多大空间”,各有各的用途。理解了这一点,你在看模型规格时就不会再把“7B”直接当成“7GB”了。
5.3 三个调试习惯,比任何技巧都管用
最后分享三个我从实践中总结出来的调试习惯,不花哨,但比任何奇技淫巧都管用。
第一个习惯,打印形状。我在调试深度学习代码时,最常用的语句就是print(x.shape)。输入数据出来打一次、第一层后打一次、reshape之后打一次、输出之前打一次。四个print打下去,整个数据流向就清楚了,问题往往马上自己现形。这个习惯土,但极其有效。
第二个习惯,断点调试而不是print满天飞。print能看变量值但看不到代码执行到哪一步时的完整上下文。VSCode里在行号左侧点一下就能设断点,运行到断点时程序暂停,左侧面板可以看到所有当前的变量值,还能单步执行。你只需要在出错那一行之前设个断点,看一眼变量们的形状和值,很多时候一眼就能定位问题。我的经验是,print用来快速确认,断点用来深入排查。
第三个习惯,最小复现。遇到一个诡异的bug,别在完整的训练脚本里埋头查。把报错的这部分代码抽出来,用一小段最简数据单独测试。比如你怀疑广播有问题,就构造两个形状最简单的数组,在一个新的Notebook单元格里做同样的操作,看结果对不对。这样能帮你快速排除大量无关因素,精准锁定问题源头。这个习惯不仅适用于深度学习,任何工程问题都适用——把问题逼到最小规模,它就没地方躲了。
这篇笔记写到这里,其实内容本身没什么“高深”的东西,但我们这行就是这样:模型结构再复杂,最后决定你能不能把代码跑起来、能不能定位到问题根源的,永远是这些基础环节。我自己回头翻过往的学习记录,惊觉那些困扰了我一整晚的问题,最终几乎都回到了环境、形状、依赖、语法习惯这几个点上。你如果能在入门阶段把这些基础打踏实,后面读论文复现代码的路会顺非常多。