1. 为什么用 Python 做深度学习入门,而不是其他语言
1.1 从“能跑起来”到“看得懂原理”的距离
很多人第一次接触深度学习,脑子里冒出来的第一个问题不是“什么是反向传播”,而是“我该用什么写”。这个问题看起来像是工具选择,实际上决定了你后面三个月到半年的学习曲线陡不陡。我见过不少朋友一上来就啃 C++ 或者硬着头皮读某框架的底层源码,结果两周不到就放弃了。不是他们不够聪明,而是路径选错了。
Python 在这个领域的位置,有点像做木工时的电动螺丝刀——它不是唯一能拧螺丝的工具,但它是让你把精力放在“做什么家具”而不是“怎么拧螺丝”上的那个选择。深度学习的核心工作是设计网络结构、准备数据、调参、观察结果,这些环节需要大量试错。Python 的语法足够简洁,让你写一个卷积层的前向传播时,注意力能放在张量形状的变化上,而不是内存管理和指针运算上。
这个速成课程的第一篇,我想先把“为什么是 Python”这件事讲透。因为如果你不理解这个选择背后的逻辑,后面遇到性能瓶颈或者想换语言时,你会反复动摇。理解了,你就能安心把基础打牢。
1.2 Python 在深度学习生态里的真实位置
深度学习框架的格局这些年变化很快,但有一个事实没变:几乎所有主流框架的首选接口都是 Python。这不是偶然。框架开发者需要一种既能快速表达数学运算、又能方便调用底层高性能库的语言。Python 通过 NumPy 这样的数组库,把“写起来像数学公式”和“跑起来接近 C 的速度”这两件事捏在了一起。
你可以这样理解:Python 是方向盘和仪表盘,真正干重活的发动机是底层的 C/C++ 和 CUDA 代码。你转动方向盘的时候,不需要知道发动机里活塞怎么运动。这个分层设计让 Python 在深度学习里站稳了脚跟。
具体到学习路径,我建议的顺序是:先用 NumPy 手写一遍简单的神经网络,理解前向和反向的数学过程;然后再用高层框架搭同样的网络,感受封装带来的效率提升。这个顺序不能反。反过来做,你会变成“调包侠”,遇到问题不知道从哪查起。
提示:不要一上来就装一堆框架。先把 NumPy 和 Matplotlib 玩熟,这两个库能覆盖你入门阶段百分之八十的需求。
1.3 这门速成课程适合谁,不适合谁
适合的人:有基本 Python 语法基础,知道列表、字典、循环、函数怎么写;对微积分和线性代数有一点印象,哪怕忘了也能快速捡起来;想在一个月内跑通第一个能识别手写数字或简单图像的模型。
不适合的人:完全没写过代码的纯小白,建议先花一周把 Python 基础过一遍;指望看完就能发论文或者做产品级部署的人,这个课程是入门,不是进阶;对数学公式极度抗拒、只想拖拽控件的人,深度学习目前还没有那么傻瓜化的工具。
我个人的经验是,入门阶段最大的障碍不是数学,而是“环境配好了但跑不通”的挫败感。所以这篇会花不少篇幅讲环境搭建和第一个可运行示例,让你先看到结果,再回头补理论。
2. 环境搭建:把第一块砖放稳
2.1 Python 版本和包管理器的选择逻辑
Python 版本这件事,我的建议很直接:用 3.9 到 3.11 之间的版本。太老的版本很多新库不支持,太新的版本可能遇到某些库还没适配。3.10 是我个人用得最顺的,兼容性和新特性平衡得比较好。
包管理器方面,pip 是标配,但如果你要做深度学习项目,我强烈建议了解一下 conda 或者更轻量的 venv。原因很简单:深度学习依赖的库之间版本关系很微妙,比如某个版本的 NumPy 和某个版本的框架可能不兼容。用虚拟环境把每个项目的依赖隔离开,能省掉大量“昨天还能跑今天怎么就报错”的时间。
我自己的习惯是每个项目建一个独立环境,命名带上日期或者项目缩写。比如dl_course_01。这样半年后回头看,知道哪个环境对应哪份代码。
# 创建虚拟环境(以 venv 为例) python -m venv dl_course_01 # 激活环境 # Windows: dl_course_01\Scripts\activate # macOS/Linux: source dl_course_01/bin/activate # 确认 Python 版本 python --version2.2 核心库的安装顺序与避坑
安装顺序有讲究。我踩过的坑是:先装了框架,再装 NumPy,结果框架自带的 NumPy 版本被覆盖,导致底层计算出错。正确的顺序是先装基础科学计算库,再装框架。
第一步,装 NumPy 和 Matplotlib:
pip install numpy matplotlib第二步,装一个深度学习框架。入门阶段我推荐从 PyTorch 开始,它的动态图机制对新手更友好,报错信息也相对容易读懂。安装时注意选择对应你操作系统的命令,官网有生成器,不要凭记忆手写。
# 以 CPU 版本为例,具体命令请根据你的系统选择 pip install torch torchvision第三步,装 Jupyter Notebook 或者 JupyterLab。深度学习入门阶段,交互式环境能让你一段一段地执行代码,看到中间结果。这对理解张量形状变化特别有帮助。
pip install jupyterlab注意:如果你有独立显卡并且想用 GPU 加速,安装命令会不一样。但入门阶段我建议先用 CPU 版本,把流程跑通再说。GPU 环境配置的复杂度足够单独写一篇,不要让它成为你入门的拦路虎。
2.3 验证环境是否就绪的完整检查清单
装完之后不要急着写模型,先做一轮验证。我整理了一个检查清单,逐条过一遍,能避免后面很多莫名其妙的错误。
| 检查项 | 验证方法 | 预期结果 |
|---|---|---|
| Python 版本 | python --version | 3.9 到 3.11 |
| NumPy 可用 | python -c "import numpy; print(numpy.__version__)" | 打印出版本号 |
| Matplotlib 可用 | python -c "import matplotlib; print(matplotlib.__version__)" | 打印出版本号 |
| 框架可用 | python -c "import torch; print(torch.__version__)" | 打印出版本号 |
| Jupyter 可用 | jupyter lab | 浏览器打开界面 |
| 虚拟环境激活 | 命令行前缀显示环境名 | 显示(dl_course_01) |
全部通过之后,你就可以开始写第一行深度学习代码了。如果某一条没过,先解决它,不要跳过。环境问题拖到后面解决,成本会翻倍。
3. 从零理解张量:深度学习的“乐高积木”
3.1 张量到底是什么,用生活场景讲清楚
张量这个词听起来吓人,其实它就是多维数组的学名。你可以这样理解:
- 一个数字,比如 5,是零维张量,也叫标量。
- 一排数字,比如 [1, 2, 3],是一维张量,也叫向量。
- 一个表格,比如 [[1, 2], [3, 4]],是二维张量,也叫矩阵。
- 再往上叠,比如多张表格摞在一起,就是三维张量。
深度学习里的一切数据——图片、文字、声音——最终都要变成张量才能送进网络。一张彩色图片可以表示成三维张量:高度、宽度、颜色通道数。一批图片就是四维张量:批次大小、高度、宽度、通道数。
我习惯把张量想象成乐高积木。不同形状的积木可以拼在一起,但接口必须对得上。深度学习调试的大部分时间,其实是在检查“这块积木和那块积木能不能拼”。
3.2 用 NumPy 创建和操作张量的实操
先不碰框架,用 NumPy 感受一下张量的基本操作。这样你能看清底层发生了什么。
import numpy as np # 创建零维张量(标量) scalar = np.array(5) print(scalar.shape) # 输出 () # 创建一维张量(向量) vector = np.array([1, 2, 3, 4]) print(vector.shape) # 输出 (4,) # 创建二维张量(矩阵) matrix = np.array([[1, 2, 3], [4, 5, 6]]) print(matrix.shape) # 输出 (2, 3) # 创建三维张量 tensor_3d = np.random.rand(2, 3, 4) print(tensor_3d.shape) # 输出 (2, 3, 4)形状(shape)是张量最重要的属性。每次操作后,养成打印形状的习惯。我见过太多错误是因为形状不匹配导致的,比如把一个 (32, 784) 的矩阵和一个 (784, 10) 的矩阵相乘是对的,但反过来就错了。
3.3 形状变换:深度学习里最容易出错的地方
形状变换是入门阶段的高频错误来源。常见的操作有 reshape、transpose、squeeze、unsqueeze。我用一个具体例子说明。
假设你有一批 28x28 的手写数字图片,共 100 张。在 NumPy 里它的形状是 (100, 28, 28)。但全连接网络需要输入是 (100, 784) 的形状。这时候就要用 reshape。
# 模拟一批图片数据 images = np.random.rand(100, 28, 28) print(images.shape) # (100, 28, 28) # 展平成 (100, 784) flattened = images.reshape(100, 28 * 28) print(flattened.shape) # (100, 784) # 注意:reshape 的顺序很重要 # 如果写成 images.reshape(100, 784) 效果一样,因为 28*28=784 # 但如果是多维转置,就要小心了提示:reshape 的时候,-1 是个好用的占位符。比如
images.reshape(100, -1),NumPy 会自动算出第二维是 784。这样改代码时不容易出错。
另一个高频操作是增加或删除维度。比如模型要求输入是 (批次, 通道, 高, 宽),但你的数据是 (批次, 高, 宽),就需要在通道维度上插入一个 1。
# 假设 data 形状是 (100, 28, 28) data = np.random.rand(100, 28, 28) # 在第 1 个维度(索引从 0 开始)插入通道维度 data_with_channel = np.expand_dims(data, axis=1) print(data_with_channel.shape) # (100, 1, 28, 28) # 删除维度 data_squeezed = np.squeeze(data_with_channel, axis=1) print(data_squeezed.shape) # (100, 28, 28)这些操作看起来简单,但组合起来就能表达复杂的张量变换。我的建议是:每写一行涉及形状的代码,就在旁边注释写上操作后的形状。这个习惯能帮你省下大量调试时间。
4. 第一个神经网络:手写数字识别实战
4.1 为什么选手写数字识别作为入门项目
手写数字识别(通常叫 MNIST 任务)是深度学习界的“Hello World”。它有几个好处:数据集小,下载快,CPU 上几分钟就能跑完一轮;问题直观,你能看到输入图片和输出数字的对应关系;准确率容易做到 95% 以上,给你正反馈。
但我要提醒一点:不要因为 MNIST 简单就轻视它。它包含了深度学习的完整流程——数据加载、预处理、模型定义、前向传播、损失计算、反向传播、参数更新、评估。把这个流程走通一遍,后面换更复杂的数据集,只是替换中间几个环节的事。
4.2 数据加载与预处理的完整代码
我用 PyTorch 来演示,因为它的数据加载接口设计得很清晰。
import torch from torchvision import datasets, transforms from torch.utils.data import DataLoader # 定义预处理:把图片转成张量,并做归一化 transform = transforms.Compose([ transforms.ToTensor(), # 把 PIL 图片转成形状为 (C, H, W) 的张量,数值缩放到 [0, 1] transforms.Normalize((0.5,), (0.5,)) # 均值 0.5,标准差 0.5,把数值范围变成 [-1, 1] ]) # 下载并加载训练集 train_dataset = datasets.MNIST( root='./data', train=True, download=True, transform=transform ) # 下载并加载测试集 test_dataset = datasets.MNIST( root='./data', train=False, download=True, transform=transform ) # 创建数据加载器 train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True) test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False) # 检查一下数据形状 images, labels = next(iter(train_loader)) print(f"图片批次形状: {images.shape}") # (64, 1, 28, 28) print(f"标签批次形状: {labels.shape}") # (64,)这里有几个细节值得说。归一化那一步,均值和标准差都取 0.5,是因为 MNIST 是灰度图,像素值在 [0, 1] 之间,减去 0.5 再除以 0.5,就把范围映射到了 [-1, 1]。这个操作能让训练更稳定。batch_size 取 64 是个经验值,太小了训练慢,太大了内存吃紧。你可以根据自己机器的情况调整。
4.3 定义网络结构:从输入到输出的数据流
我定义一个简单的全连接网络,两层隐藏层。结构是:784 输入 -> 256 隐藏 -> 128 隐藏 -> 10 输出。
import torch.nn as nn import torch.nn.functional as F class SimpleNet(nn.Module): def __init__(self): super(SimpleNet, self).__init__() self.fc1 = nn.Linear(28 * 28, 256) self.fc2 = nn.Linear(256, 128) self.fc3 = nn.Linear(128, 10) def forward(self, x): # x 的形状是 (batch_size, 1, 28, 28) x = x.view(x.size(0), -1) # 展平成 (batch_size, 784) x = F.relu(self.fc1(x)) x = F.relu(self.fc2(x)) x = self.fc3(x) # 输出层不加激活函数,因为后面用交叉熵损失 return x model = SimpleNet() print(model)x.view(x.size(0), -1)这一行是关键。它把每张图片从 (1, 28, 28) 展平成 784 个数字。x.size(0)是批次大小,-1 让 PyTorch 自动算出剩下的维度。
输出层不加 softmax,是因为 PyTorch 的交叉熵损失函数内部会做 softmax。如果你手动加了 softmax,再传给交叉熵损失,就重复了,会导致训练不稳定。这是个常见的坑。
4.4 训练循环:让网络从错误中学习
训练循环是深度学习的核心。它的逻辑是:前向传播算预测,算损失,反向传播算梯度,更新参数。重复这个过程。
import torch.optim as optim # 定义损失函数和优化器 criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=0.001) # 训练轮数 epochs = 5 for epoch in range(epochs): model.train() # 切换到训练模式 running_loss = 0.0 for batch_idx, (images, labels) in enumerate(train_loader): # 1. 清零梯度 optimizer.zero_grad() # 2. 前向传播 outputs = model(images) # 3. 计算损失 loss = criterion(outputs, labels) # 4. 反向传播 loss.backward() # 5. 更新参数 optimizer.step() running_loss += loss.item() # 每 100 个批次打印一次 if (batch_idx + 1) % 100 == 0: print(f"Epoch [{epoch+1}/{epochs}], " f"Step [{batch_idx+1}/{len(train_loader)}], " f"Loss: {running_loss / 100:.4f}") running_loss = 0.0 print("训练完成")optimizer.zero_grad()这一步不能省。PyTorch 默认会累积梯度,如果不清零,上一次的梯度会和这一次的叠加,导致更新方向错误。这是新手最容易犯的错误之一。
model.train()和后面的model.eval()是切换模式。训练模式下,dropout 和 batch normalization 的行为和评估模式不同。虽然这个简单网络没用到这些层,但养成习惯是好的。
4.5 评估模型:看看它到底学会了没有
训练完之后,用测试集评估一下。测试集的数据是模型没见过的,能反映真实的泛化能力。
model.eval() # 切换到评估模式 correct = 0 total = 0 with torch.no_grad(): # 评估时不需要计算梯度,省内存 for images, labels in test_loader: outputs = model(images) _, predicted = torch.max(outputs.data, 1) total += labels.size(0) correct += (predicted == labels).sum().item() print(f"测试集准确率: {100 * correct / total:.2f}%")torch.no_grad()是个好习惯。评估阶段不需要反向传播,关掉梯度计算能省不少内存,速度也更快。torch.max(outputs, 1)返回每一行最大值和对应的索引,索引就是预测的数字。
跑完这五轮,准确率应该在 97% 左右。如果只有 90% 出头,检查一下归一化有没有做,学习率是不是太大。如果低于 90%,大概率是某个环节写错了。
5. 常见问题与排查技巧实录
5.1 形状不匹配报错:最常见的三类场景
形状错误是入门阶段出现频率最高的问题。我把它分成三类,每类给出排查思路。
第一类:矩阵乘法维度对不上。报错信息通常是mat1 and mat2 shapes cannot be multiplied。这时候检查两个矩阵的最后一维和倒数第二维是否相等。比如 (64, 784) 和 (784, 10) 可以乘,但 (64, 784) 和 (128, 10) 就不行。
第二类:卷积层输入通道数不对。报错信息类似expected input to have 3 channels, but got 1 channel。检查你的数据是灰度图还是彩色图,卷积层定义的输入通道数要和数据一致。
第三类:展平操作后维度算错。比如图片是 28x28,你写成了x.view(x.size(0), 28*28*1),结果对了,但如果图片是 32x32,就错了。建议用-1让框架自动算。
| 报错关键词 | 可能原因 | 排查方法 |
|---|---|---|
| shapes cannot be multiplied | 矩阵维度不匹配 | 打印两个矩阵的 shape |
| expected input to have X channels | 通道数不一致 | 检查数据通道和层定义 |
| invalid shape | reshape 参数错误 | 用 -1 代替手算 |
| size mismatch | 全连接层输入维度错 | 打印展平后的 shape |
5.2 损失不下降:从学习率到数据预处理的排查链
损失不下降或者震荡,原因可能有很多。我按排查成本从低到高列一个顺序。
先看学习率。学习率太大会震荡,太小会下降极慢。Adam 优化器下,0.001 是个安全的起点。如果损失完全不降,试试调到 0.0001。
再看数据预处理。如果输入数据没有归一化,数值范围很大,梯度也会很大,训练容易发散。确保做了归一化。
然后看标签对不对。我遇到过一次,数据加载时标签和图片错位了,导致模型在学随机噪声。检查方法是打印几张图片和对应的标签,肉眼确认。
最后看模型结构。如果网络太深或者太浅,也可能不收敛。入门阶段用两三层全连接就够了,不要一上来就堆十几层。
提示:损失不下降时,先用一个极小的数据集(比如 10 张图片)过拟合一下。如果连 10 张都过拟合不了,说明代码有 bug,不是模型容量问题。
5.3 显存不够用:CPU 入门阶段的替代方案
入门阶段用 CPU 一般不会遇到显存问题,但如果你尝试了 GPU 版本,可能会遇到CUDA out of memory。解决办法有几个:减小 batch_size,从 64 降到 32 或 16;用torch.no_grad()包住评估代码;及时删除不用的中间变量。
如果 CPU 内存也不够,检查是不是数据加载器开了太多 worker。DataLoader的num_workers参数在 Windows 上有时会出问题,设成 0 用主进程加载最稳。
我个人的建议是:入门阶段就用 CPU,把精力放在理解流程上。等你能独立调通三个不同任务之后,再折腾 GPU 环境。那时候你对整个流程有感觉了,遇到环境问题也知道从哪查。
5.4 训练速度慢:哪些优化值得做,哪些不值得
CPU 上训练 MNIST,一轮大概几十秒到几分钟,取决于机器性能。如果你觉得慢,可以做的优化有:减小模型规模,把隐藏层从 256 降到 128;增大 batch_size,但注意不要太大导致内存溢出;用torch.set_num_threads()设置线程数。
不值得做的优化:为了速度去学 CUDA 编程,入门阶段完全没必要;用多进程数据加载,在 CPU 训练时收益不大;把数据转成半精度浮点数,CPU 上支持不好。
我见过有人为了“加速”把代码改得面目全非,结果引入了一堆新 bug。入门阶段,代码清晰比运行速度重要得多。慢一点没关系,能跑通、能理解,才是目的。
6. 从这篇到下一篇:你现在的起点在哪里
走到这里,你已经完成了一个完整的深度学习项目:从环境搭建,到张量操作,到定义网络,到训练评估,再到问题排查。这个流程走一遍,比看十篇理论文章都有用。
我个人的体会是,入门阶段最怕的不是数学难,而是“不知道下一步该干什么”。你现在有了一个可运行的基线,接下来可以做的扩展有很多:换一个数据集,比如 FashionMNIST,看看同样的代码表现如何;改改网络结构,加一层或者减一层,观察准确率变化;把全连接换成卷积,感受一下参数量的差异。
每一个扩展都是一次小实验。实验做多了,你对超参数、网络结构、数据预处理这些概念的理解,会从“书上说的”变成“我试过的”。这个转变,才是真正入门的标志。
最后分享一个小技巧:把你每次实验的配置和结果记在一个表格里。不用很正式,一个简单的 Markdown 文件就行。记录内容包括:数据集、模型结构、学习率、batch_size、最终准确率、遇到的报错。三个月后回头看,这份记录比任何教程都值钱,因为它是你自己踩出来的路。