嘿,朋友!我知道你现在的感觉。看着GitHub上那些几千行的代码,或者听到“卷积神经网络”、“反向传播”、“梯度下降”这些词,心里是不是有点发毛?觉得这好像是物理学博士才玩的东西?
别急,先深呼吸。
我是Agnes。我想跟你分享一段我亲身经历过的路。几年前,我也是一个对着 import numpy as np 发呆的初学者。那时候,我以为深度学习是黑魔法。但当我真正坐下来,一行一行敲代码,看着那个最初只会说“Hello World”的程序,逐渐学会“看”图片时,我才发现:这其实就和学骑自行车一样,只是一个技巧问题。
今天,我们不讲复杂的数学推导,不堆砌晦涩的术语。我们就像两个朋友在咖啡馆聊天,我会牵着你的手,带你从最基础的Python环境搭建,一步步走到能亲手训练一个图像识别模型的那一天。准备好了吗?让我们开始这段冒险。
第一章:别急着开车,先学会系安全带
在踏入深度学习的大门之前,我们需要先装备好我们的“工具包”。很多人(包括曾经的我)最容易在这里放弃,不是因为代码太难,而是因为环境配置报错报得怀疑人生。
1.1 为什么是 Python?
你一定会问:“为什么不用 C++ 快一点?或者用 Java?”
想象一下,你要盖一栋摩天大楼(深度学习模型)。
- C++ 就像是你自己烧砖、炼钢、砍木头。虽然最后房子可能更结实,但你大部分时间都在做原材料,而不是设计房子。
- Python 则是直接给你递来了预制好的楼板、门窗和电梯。你只需要负责怎么把它们拼起来。
在深度学习领域,Python 就是那个“预制成型的材料”最全的语言。TensorFlow、PyTorch、Keras,所有这些强大的框架,第一优先支持的都是 Python。所以,选它,就对了。
1.2 你的第一个“ Hello World ”:环境搭建
别被“环境搭建”这个词吓到。对于深度学习,我强烈推荐使用 Anaconda。它就像一个万能工具箱,里面已经预装了 Python、NumPy(做数学计算的)、Pandas(处理数据表格)等常用库。你只需要安装它,然后开一个“虚拟环境”,世界就清净了。
如果你还没装,去 Anaconda 官网下载个免费版的安装就行。安装完后,打开你的终端(Mac/Linux)或 Anaconda Prompt(Windows),输入以下命令,创建一个专门给深度学习用的环境,名字叫 deep_learning:
conda create --name deep_learning python=3.9
接着,激活它:
conda activate deep_learning
现在,这个环境里还是空的。我们需要安装深度学习的心脏——PyTorch。为什么选 PyTorch?因为它现在是最流行的,而且它的代码风格非常像 Python 原生代码,读起来就像读故事一样自然。
去 PyTorch 官网 复制那个安装命令。通常,对于初学者,我们推荐安装 CPU 版本(哪怕你没显卡),这样最稳定,出错最少。
pip install torch torchvision torchaudio
1.3 验证:Hello, Deep Learning
环境装好了?来,我们写一个真正的“Hello World”。这次,我们要让它不仅能说话,还能“思考”。
打开你的编辑器(VS Code 或者 PyCharm 都可以),新建一个文件叫 hello_dl.py,输入这段代码:
import torch
# 看看 PyTorch 能不能正常工作
print("PyTorch 版本:", torch.__version__)
# 创建一个简单的张量(Tensor),你可以把它理解为多维数组
x = torch.tensor([1.0, 2.0, 3.0])
print("我的第一个张量:", x)
# 做个简单的加法,感受张量的威力
y = x + 10
print("张量加法结果:", y)
运行它。如果你看到了版本号,以及 tensor([11., 12., 13.]),那么恭喜你!你已经跨过了新手村的第一道门槛。这台“机器”已经准备好为你工作了。
第二章:数据是燃料,没有燃料,再好的引擎也跑不起来
很多教程一上来就讲模型结构,这是错误的。在深度学习里,数据(Data)比模型(Model)更重要。一个好模型配上一堆垃圾数据,结果也是垃圾;而一个简单模型配上高质量数据,往往能取得意想不到的效果。
2.1 认识我们的“学生”:MNIST 手写数字数据集
我们要做的第一个任务,是手写数字识别。 想象一下,你教一个小孩认字。你会给他看很多张图,上面写着“0”,跟他说“这是0”;再看很多张写着“1”的图,跟他说“这是1”。反复多次后,小孩就学会了。
MNIST 数据集就是那个“教材”。它包含:
- 60,000 张训练图片(供模型学习)
- 10,000 张测试图片(供考试,看模型学得怎么样)
- 每张图都是 28x28 像素 的黑白图片,代表一个 0-9 之间的数字。
别看 28x28 很小,这对于计算机来说,就是一张由 784 个数字组成的表格(每个像素值从 0 到 255)。
2.2 加载数据:让计算机“看见”
我们来写代码,把这套教材下载并加载到我们的程序中。
import torch
import torchvision
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
import numpy as np
# 定义数据预处理步骤
# ToTensor(): 把图片从 0-255 的整数,变成 0.0-1.0 的小数,方便计算
# 这就像把“像素值”标准化,让模型更容易学习规律
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.5,), (0.5,)) # 归一化,把数据缩放到 -1 到 1 之间
])
# 下载并加载训练数据
trainset = torchvision.datasets.MNIST(root='./data', train=True,
download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=64,
shuffle=True) # batch_size=64 表示一次喂给模型64张图片,shuffle=True 表示打乱顺序
# 下载并加载测试数据
testset = torchvision.datasets.MNIST(root='./data', train=False,
download=True, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=64,
shuffle=False)
# 随便取出一张图片,让我们看看“教材”长什么样
def imshow(img):
img = img / 2 + 0.5 # 反转归一化,把 -1~1 变回 0~1
npimg = img.numpy()
plt.imshow(np.transpose(npimg, (1, 2, 0)))
plt.show()
# 获取一批图片
images, labels = next(iter(trainloader))
# 显示前几张图片
imshow(torchvision.utils.make_grid(images[:8]))
print(' '.join(f'{labels[j]:5s}' for j in range(8)))
当你运行这段代码时,你会看到 8 张灰扑扑的小方格图片。有的像 0,有的像 1,有的像 5。
关键点解释:
- DataLoader:这就像是一个“传送带”。它不会一次把 60,000 张图都塞进内存(那样电脑会爆),而是每次只抓 64 张(batch_size)给模型。
- Normalize:为什么要把像素从 0-255 变成 -1 到 1?因为这样可以让数学计算更稳定,模型收敛(找到最优解)的速度更快。这就好比做菜时,把食材切得大小均匀,受热才均匀。
第三章:搭建神经网络——给计算机一个“大脑”
现在,数据准备好了,我们得给计算机装一个“大脑”来处理这些图片。
在最开始,你可能会想:“我们能不能直接写一堆 if 语句?比如:如果是圆形的,就是0或9;如果是直的,就是1或7?”
试试就知道了,你会发现这几乎不可能。 因为每个人写“7”的写法都不一样,有的带横杠,有的不带。写规则的成本太高了。
所以,我们换一个思路:我们不告诉计算机规则,我们让计算机自己从数据里“猜”出规则。
3.1 什么是神经网络?
想象一个工厂的流水线:
- 输入层:原料进来(28x28=784 个像素值)。
- 隐藏层:工人干活。第一层工人把像素两两组合,看有没有“横线”;第二层工人把横线组合起来,看有没有“圆圈”;第三层工人把圆圈和横线组合,看像不像数字“8”。
- 输出层:成品出厂。最终输出 10 个概率,分别代表“这是0的可能性”、“这是1的可能性”……“这是9的可能性”。
这就是全连接神经网络(Fully Connected Network, FCN)。
3.2 代码实现:构建我们的第一个模型
在 PyTorch 中,定义模型非常简单,就像搭积木一样。
import torch.nn as nn
import torch.optim as optim
class SimpleNN(nn.Module):
def __init__(self):
super(SimpleNN, self).__init__()
# 我们只需要两层全连接层
self.fc1 = nn.Linear(784, 128) # 第一层:784个输入,变成128个特征
self.fc2 = nn.Linear(128, 10) # 第二层:128个特征,变成10个输出(0-9)
def forward(self, x):
# 数据流通过去
x = torch.relu(self.fc1(x)) # 加一个激活函数 ReLU,让它学会“非线性”的东西(比如弯曲的线)
x = self.fc2(x)
return x
# 实例化模型
model = SimpleNN()
print(model)
运行后,你会看到模型的结构图。这就是我们的“大脑”雏形。
3.3 训练模型:让大脑自己学习
模型建好了,但它现在是个“傻瓜”,它什么都不会。我们需要“训练”它。
训练的过程,就是不断地:
- 喂给它一张图片。
- 让它猜一个数字。
- 看看它猜得对不对(计算损失 Loss)。
- 如果错了,就调整它内部神经元之间的连接强度(反向传播)。
- 重复几万次。
# 定义损失函数:衡量预测值和真实值之间的距离
criterion = nn.CrossEntropyLoss()
# 定义优化器:用来调整模型参数,让损失变小
optimizer = optim.SGD(model.parameters(), lr=0.01) # 随机梯度下降
# 开始训练 5 个轮次(Epoch)
for epoch in range(5):
running_loss = 0.0
for i, data in enumerate(trainloader, 0):
# 获取一批数据
inputs, labels = data
# 清空上一次的梯度
optimizer.zero_grad()
# 前向传播:把图片喂进去,得到预测结果
outputs = model(inputs)
# 计算损失:预测结果和真实标签之间的差距
loss = criterion(outputs, labels)
# 反向传播:计算梯度,并更新参数
loss.backward()
optimizer.step()
# 打印进度
running_loss += loss.item()
if i % 200 == 199: # 每200步打印一次
print(f'[{epoch + 1}, {i + 1:5d}] loss: {running_loss / 200:.3f}')
running_loss = 0.0
print('Finished Training.')
当你看到 Loss(损失)从 2.0 降到 0.1 左右,你就知道,模型正在“学会”看数字了。
3.4 测试模型:期末考试
训练完了,总得考考它吧?
# 计算准确率
correct = 0
total = 0
with torch.no_grad(): # 测试时不需要计算梯度,节省内存
for data in testloader:
images, labels = data
outputs = model(images)
_, predicted = torch.max(outputs.data, 1) # 取概率最大的那个类别
total += labels.size(0)
correct += (predicted == labels).sum().item()
print(f'Accuracy of the network on the 10000 test images: {100 * correct // total} %')
如果你运气好,全连接神经网络在 MNIST 上大概能跑到 97% 左右的准确率。对于一个只用了两层全连接层的“小学生”模型来说,这已经很棒了!
但是,你有没有想过,为什么它只能跑到 97%?剩下的 3% 错在哪里?
第四章:进阶!为什么我们需要 CNN(卷积神经网络)?
让我们回到那个 28x28 的图片。
在全连接网络里,我们把 784 个像素直接拍扁了喂进去。这意味着,计算机在判断“这是7”的时候,它把左上角的像素和右下角的像素放在同等重要的位置。
但这不符合人类的视觉逻辑。
人类看图,是先找局部特征的:
- 我先看到几条线。
- 线组合成角。
- 角组合成边。
- 边组合成数字。
卷积神经网络(CNN) 就是模仿这个过程设计的。它有一个超能力:局部感知和参数共享。
4.1 什么是“卷积”?
想象你手里有一个小窗户(叫卷积核或滤波器),你在整张图片上慢慢滑动。
- 这个窗户只让你看一小块区域。
- 当你滑动时,它在寻找特定的图案,比如“竖直的线”或者“水平的线”。
如果图片里真的有这条线,窗户那里就会“亮”起来,输出一个高数值。如果没有任何线,就输出低数值。
这就是特征提取。CNN 的厉害之处在于,它不需要你告诉它“找线”,它自己会学出哪些卷积核最好用。
4.2 搭建 CNN 模型
现在,我们来搭建一个真正的“专业人士”模型。我们会用到三个核心组件:
- Conv2d:卷积层,用来提取特征(找线、找角、找形状)。
- MaxPool2d:池化层,用来缩小图片尺寸,减少计算量,同时保留最显著的特征(就像把高清照片压缩成缩略图,但保留主要轮廓)。
- Linear:全连接层,用来根据提取到的特征,做出最终判断。
import torch.nn.functional as F
class CNN(nn.Module):
def __init__(self):
super(CNN, self).__init__()
# 第一层卷积:输入1个通道(黑白图),输出16个特征图
# kernel_size=3 表示窗户是3x3的大小
self.conv1 = nn.Conv2d(1, 16, kernel_size=3, stride=1, padding=1)
# 第二层卷积:输入16个特征图,输出32个特征图
self.conv2 = nn.Conv2d(16, 32, kernel_size=3, stride=1, padding=1)
# 池化层:把图片尺寸缩小一半
self.pool = nn.MaxPool2d(2, 2)
# 全连接层
# 经过两次卷积和两次池化,28x28的图片会变成 7x7
# 32个特征图,所以输入维度是 32 * 7 * 7 = 1568
self.fc1 = nn.Linear(32 * 7 * 7, 128)
self.fc2 = nn.Linear(128, 10)
def forward(self, x):
# 经过第一层卷积 -> ReLU激活 -> 池化
x = self.pool(F.relu(self.conv1(x)))
# 经过第二层卷积 -> ReLU激活 -> 池化
x = self.pool(F.relu(self.conv2(x)))
# 把特征图拍扁,变成一维向量,准备送进全连接层
x = x.view(-1, 32 * 7 * 7)
# 全连接层
x = F.relu(self.fc1(x))
x = self.fc2(x)
return x
# 实例化CNN模型
cnn_model = CNN()
print(cnn_model)
4.3 训练 CNN
训练代码和前面差不多,只是把模型换成了 cnn_model。
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(cnn_model.parameters(), lr=0.001) # 这里换个优化器 Adam,通常收敛更快
for epoch in range(10): # 多训练几个轮次,CNN通常能跑得更好
running_loss = 0.0
for i, data in enumerate(trainloader, 0):
inputs, labels = data
optimizer.zero_grad()
outputs = cnn_model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
running_loss += loss.item()
if i % 200 == 199:
print(f'[{epoch + 1}, {i + 1:5d}] loss: {running_loss / 200:.3f}')
running_loss = 0.0
print('Finished Training.')
4.4 结果对比
当你跑
