【零基础学AI】第 23 章——用神经网络识别图片

第 23 章 用神经网络识别图片

文章目录

上一章的模型只接收一个数字。本章把输入换成一张手写数字图,输出换成 0 至 9 中的一个类别。训练循环依然包含计算损失、求梯度和更新参数,但图片怎样装进张量、怎样分批,以及训练完怎样单独使用模型,都需要交代清楚。

我们选一组很小的公开图片。普通电脑的 CPU 就能完成训练,读者可以把时间花在看清每一步。本章随附训练脚本、固定数据快照、模型参数和单图预测入口。执行代码见 23-image-classification。

🌈 关于《AI 零基础 36 讲》

🧠 零基础也能开始学 AI

《AI 零基础 36 讲》是一套面向初学者的 AI 入门教程。课程从认识和使用 AI 开始,逐步学习

Python、数据处理、机器学习与深度学习,再进入大模型应用开发,最后完成个人知识库助手的综合实践。

📖 本篇是正式讲解文章

通过具体例子、原创图示和操作结果讲清概念。涉及编程的章节提供可运行代码,方便你边读边试。

🎯 读完以后,动手检验

建议按专栏顺序学习,并完成对应的课后训练,检查自己能否独立运用本章知识。

博客专栏:《AI 零基础 36 讲》

先认清图片和答案

本章使用 scikit-learn 1.7 的 load_digits 数据。它包含 1797 张手写数字图,每张只有 8×8 个像素,原像素值是 0 至 16 的整数。标签是书写者所写的数字,取值为 0 至 9。它是 UCI 原数据测试部分的一个副本,数据来源见数据说明及 官方数据文档。

我们已把图片、标签和划分编号保存到 data/digits.npz,日常运行不用下载。随机种子固定为 42,按类别分层划分,训练集 1437 张,测试集 360 张。每一张图的原始编号都写进划分清单。本章单独示范预测的图片来自这 360 张测试图,真实标签是 5。

先看数组,再做转换。

python 复制代码
from examples.image_core import load_snapshot, pixels_to_tensor
images, labels, train, test = load_snapshot()
x = pixels_to_tensor(images)
print(images.shape, labels.shape)
print(len(train), len(test))
print(tuple(x.shape), x.dtype)
print(float(x.min()), float(x.max()))

输出为 (1797, 8, 8)、(1797,),训练与测试数量为 1437 360。转换后的张量形状是 (1797, 1, 8, 8),类型为 torch.float32,像素范围是 0 至 1。

这四个维度依次表示图片数量、通道、高和宽。灰度图只有一个通道,彩色图常见的红、绿、蓝三个通道在这里没有出现。原像素除以 16 后,最亮的像素变成 1。这条规则要贯穿训练和预测。如果训练时除以 16,预测时却直接输入 255,模型看到的数值范围就变了。

一批和一轮怎样数

DataLoader 负责按批取数据。本章每批最多 64 张,训练时打乱顺序,最后不足一批的图片也保留。1437 除以 64 等于 22 批完整的图片,再剩 29 张,因此一轮共有 23 批。

python 复制代码
import torch
from torch.utils.data import DataLoader, TensorDataset
from examples.image_core import load_snapshot, pixels_to_tensor
images, labels, train, _ = load_snapshot()
dataset = TensorDataset(pixels_to_tensor(images)[train], torch.tensor(labels[train], dtype=torch.long))
loader = DataLoader(dataset, batch_size=64, shuffle=False, drop_last=False)
sizes = [len(batch_y) for _, batch_y in loader]
print(len(loader), sizes[-1], sum(sizes))

结果是 23 29 1437。这里为了便于核对,用固定顺序查看批次。正式训练使用 shuffle=True 和固定随机生成器,每一轮重新排列训练图片。

一批是一次参与计算和更新的图片。一轮是完整走过训练集一次。本章训练 30 轮,每批更新一次,所以共更新 690 次。把轮次写成 30,不代表只更新 30 次;把批量改成 128,更新次数也会随之变化。

让 64 个像素产生 10 个分数

第一版图片分类器使用全连接网络。Flatten 把每张 8×8 图片展平为 64 个数,第一层把它们映射到 32 个隐藏节点,ReLU 加入非线性,最后一层给出 10 个类别分数。

python 复制代码
import torch
from examples.image_core import make_model
model = make_model()
scores = model(torch.zeros(64, 1, 8, 8))
print(tuple(scores.shape))
print(sum(p.numel() for p in model.parameters()))

输出形状为 (64, 10),参数总数为 2410。每行对应一张图,每列按 0 至 9 的顺序对应一个数字。分数最高的列给出预测类别。这些原始分数常叫 logits,可以为负,也不需要加起来等于 1。

训练使用交叉熵损失。真实标签为 5 时,损失会根据这一行所有类别分数,衡量模型给正确类别的支持程度。它比只问"猜对没有"提供更细的训练信息。对于本章的类别编号标签,CrossEntropyLoss 接收原始分数和 long 整数标签,函数内部会处理所需的归一化计算。不要先对分数做 Softmax 再交给它。

下面用一张虚构教学图片的三个类别分数缩小计算规模。真实类别为第 0 类,损失约为 0.407606。

python 复制代码
import torch
scores = torch.tensor([[1.0, 0.0, -1.0]])
answer = torch.tensor([0], dtype=torch.long)
loss = torch.nn.CrossEntropyLoss()(scores, answer)
print(f"{loss.item():.6f}")

把训练循环完整走一遍

在第 22 章的顺序上,加上"每轮取多批图片"即可。下面用一小批真实训练图片运行一次更新,便于逐行观察。完整 30 轮训练在主程序中。

python 复制代码
import torch
from examples.image_core import load_snapshot, pixels_to_tensor, make_model
torch.manual_seed(42)
images, labels, train, _ = load_snapshot()
batch_x = pixels_to_tensor(images[train[:64]])
batch_y = torch.tensor(labels[train[:64]], dtype=torch.long)
model = make_model()
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
model.train()
optimizer.zero_grad(set_to_none=True)
loss = torch.nn.CrossEntropyLoss()(model(batch_x), batch_y)
loss.backward()
optimizer.step()
print(tuple(batch_x.shape), len(batch_y), bool(torch.isfinite(loss)))

输出是 (64, 1, 8, 8) 64 True。清空梯度、前向计算、计算损失、反向传播、更新参数,都在这一批内完成。下一批开始前再次清空梯度,避免意外把上批梯度叠加进来。

本章使用 Adam,学习率 0.01。轮次、学习率和网络规模在运行前固定,没有看完测试分数再挑一组更好看的设置。每轮结束后,用当前模型重新计算整个训练集的损失和准确率。这条曲线记录的是更新后的整批评估值,和逐小批损失的简单平均需要区分。

评估时切换到 eval(),并在 torch.no_grad() 内计算。当前网络没有 Dropout 或 BatchNorm,切换模式不会改变这两类层的行为,因为它们根本没有出现在网络里。但保持完整的训练和评估流程,后续换模型时更容易正确使用。测试集仅在固定 30 轮结束后评估一次。

损失降下来以后,看看错在什么地方

图来自本章 CPU 实际运行。初始训练损失约 2.297286,末轮约 0.017907,训练准确率由约 8.21% 升到约 99.86%。测试集答对 347/360 张,准确率约 96.39%,损失约 0.095715。完整精度保存在结果 JSON。

训练准确率比测试准确率高。这意味着模型对见过的数据拟合得更好,对未参与更新的图片仍会出错。测试类别数量为每类 35 至 37 张,没有出现某一个类别占据绝大多数的情况。不过一个总百分比仍然藏住了各类差异。

纵轴是真实数字,横轴是预测数字,主对角线表示答对的数量。真实数字 8 的 35 张图中,31 张预测正确,4 张被当成 1。于是本次数字 8 的召回率是 31/35,约 88.57%。数字 2 和 5 在这份测试划分里全部答对,也不能推出以后所有写法都能答对。

错误图版取 13 个错误中的前 8 个,按固定测试顺序排列,没有按视觉效果挑选。每个小图都写出真实标签、预测标签和数据编号。图像来自公开数据,组合布局和结果图为本章原创。比如编号 1553 的真实数字为 8,模型输出 1。8×8 的图已经丢失了很多细节,人眼也可能觉得模糊,但数据标签不会因此自动变成模型的判断。保留这些失败图片,才能看清结果的适用范围。

训练脚本关掉以后,模型怎样继续用

训练程序将参数写到 results/digits-state.pt。这个文件同时保存结构标记、类别顺序、输入形状和像素除数。预测程序先建立同样的网络,再装入参数,随后按同一条预处理规则读取图片。

python 复制代码
from examples.image_core import load_model, load_single_image, predict_image
model = load_model("results/digits-state.pt")
image = load_single_image("data/sample-digit.png")
result = predict_image(model, image)
print(result["label"])
print(f"{result['probabilities'][5]:.4f}")

结果为 5 和 0.9973。这里的十个输出经过 Softmax,按数字 0 至 9 排列。它们是模型在这些类别之间分配的数值,0.9973 也不能当作"现实中有 99.73% 的保证"。输入与训练图片差异很大时,模型依然可能给一个很高的分数。

PNG 显示时使用 0 至 255 的灰度值,加载后先恢复到原来的 0 至 16,再除以 16。配套 NPY 保存原始像素矩阵,两个入口产生完全相同的张量。这一点已有测试。程序还在 360 张测试图上比较保存前后的原始分数,结果逐值一致。单图脚本独立启动,没有重新训练,也没有读取真实标签来生成答案。

两个很容易遇到的报错

把标签写成 0.0 会生成浮点类型。若本意是类别编号,就应生成 torch.long 标签。随附错误程序会出现要求 Long 类型的报错,修复后输出损失 0.407606。只有在明确使用概率分布标签的另一种接口时,浮点目标才有对应含义。

把一张 28×28 图交给本章入口也会失败。修复示例使用配套 8×8 灰度图,得到 (1, 1, 8, 8)。不要只改一个 reshape 把 784 个像素塞进 64 个位置。尺寸、背景明暗、数字位置和笔画分布都可能影响预测。

你现在可以自己运行训练,再关掉训练程序,用单图入口加载文件。记下这一轮的损失、准确率和分错图片。完成【零基础学AI】第 23 章课后练习与答案后,下一章会保留图片的二维位置,用卷积观察局部图案,并比较从头训练和迁移学习。

相关推荐
sunny066038 分钟前
推理 INFRA 方法论(2):分布式推理:TP、DP、EP、CP 与 PP 的取舍
人工智能·分布式
axe_hu42 分钟前
AI产品经理的职业规划与学习路径
人工智能·职场和发展·产品经理·学习方法
武子康43 分钟前
固定时间步已经开了,为什么游戏回放还会走出不同位置?
人工智能·后端·agent
ManageEngineITSM1 小时前
工单分类怎么设计?类别、子类别与自动分派的实用方法
大数据·服务器·数据库·人工智能·分类·数据挖掘
听我哔哔1 小时前
口播视频唇形总对不上,Seedance 2.5在zeekeAI里同步
人工智能·音视频
Raas1001 小时前
MAI Gateway实战部署:AI网关私有化部署?从0到1上手指南
人工智能·gateway·mai gateway·企业级ai产品
专注仿真1 小时前
卷积神经网络(CNN)算法详解
人工智能·python·算法·代码·大模型训练
FII工业富联科技服务1 小时前
多智能体如何进入制造现场?从业务协同到数字孪生验证
大数据·人工智能·算法·机器人·制造·数字孪生
golang学习记1 小时前
IDEA 开始换芯,发布全新AI版终端
java·人工智能·intellij-idea