ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

AI实战:用DenseNet搭建手写数字识别模型,TaoToken统一Key接入训练链路

AI实战:用DenseNet搭建手写数字识别模型,TaoToken统一Key接入训练链路 1. 从 ResNet 到 DenseNet手写数字识别为什么值得换模型手写数字识别看起来是个被做烂了的任务MNIST 上随便一个 CNN 都能刷到 99% 以上。但真正把它放进业务里跑你会发现测试集准确率和实际场景准确率是两回事。我之前用 ResNet 搭过一版手写数字分类模型训练集准确率 99.6%看着很漂亮结果上线后对 13 位连续数字的整体识别率不到 80%单个数字识别率也掉到 99% 以下。问题出在哪不是数据量不够也不是训练轮次太少而是模型对书写不规范的数字泛化能力不够。后来我把主干网络换成 DenseNet同样的数据、同样的预处理训练准确率到了 99.83%实际测试整体准确率 96% 以上单个数字 99.8% 以上基本满足项目要求。DenseNet 的核心优势在于密集连接每一层都接收前面所有层的特征作为输入梯度流更直接参数效率更高特征复用更充分。对于手写数字这种类内差异大、类间差异小的任务DenseNet 在训练数据不足时表现更稳。这篇文章我会把 DenseNet 手写数字识别的完整链路拆开讲MNIST 数据加载、Dense Block 与 Transition 层配置、训练脚本、推理验证以及用 TaoToken 统一 Key 管理训练链路中的模型调用配置。你可以直接复制代码跑起来也可以把配置骨架搬到自己的项目里。2. TaoToken 前置统一 Key 与 settings.json 配置骨架在训练链路里模型本身是本地跑的但周边环节——比如用大模型做数据增强脚本生成、训练日志摘要、推理结果复核——往往需要调用外部 API。如果每个环节都单独配 Key管理起来很乱。TaoToken 的作用是把这些调用统一到一个 Key 和一个 API 通道上配置一次训练脚本、验证脚本、辅助工具都能复用。TaoToken 的 API 地址是https://taotoken.net/api官网是https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content。你需要在控制台创建一个 API Key然后把它写进项目的settings.json里。下面是一个配置骨架你可以直接拿去改{ taotoken: { api_base: https://taotoken.net/api, api_key: sk-your-key-here, default_model: claude-sonnet-4-20250514, timeout: 30, max_retries: 3 }, training: { data_dir: ./data/mnist, batch_size: 128, epochs: 30, lr: 0.001, checkpoint_dir: ./checkpoints }, inference: { model_path: ./checkpoints/densenet_mnist_best.pth, input_size: [1, 28, 28], num_classes: 10 } }这个配置里taotoken段负责 API 通道training段管训练超参inference段管推理路径。训练脚本读这个文件就不用把 Key 硬编码在代码里。如果你需要长期跑编码任务或者 Agent 流程可以看看 Coding Plan 的配置方式如果只是验证模型对话能力模型对话入口更直接接入文档里有完整的参数说明。注意API Key 不要提交到 Git 仓库建议用环境变量覆盖settings.json里的api_key字段或者把settings.json加入.gitignore。3. 可复制配置DenseNet 模型结构与训练脚本3.1 Dense Block 与 Transition 层参数DenseNet 的结构核心是 Dense Block 和 Transition 层交替堆叠。Dense Block 内部每一层的输出都拼接到后续所有层的输入上Transition 层负责降维和降采样。对于 MNIST 的 28x28 灰度图不需要太深的网络我用的是 3 个 Dense Block每个 Block 内 4 层增长率growth_rate12压缩系数compression0.5。import torch import torch.nn as nn import torch.nn.functional as F class DenseLayer(nn.Module): def __init__(self, in_channels, growth_rate): super().__init__() self.bn nn.BatchNorm2d(in_channels) self.relu nn.ReLU(inplaceTrue) self.conv nn.Conv2d(in_channels, growth_rate, kernel_size3, padding1, biasFalse) def forward(self, x): out self.conv(self.relu(self.bn(x))) return torch.cat([x, out], dim1) class DenseBlock(nn.Module): def __init__(self, in_channels, num_layers, growth_rate): super().__init__() layers [] for i in range(num_layers): layers.append(DenseLayer(in_channels i * growth_rate, growth_rate)) self.block nn.Sequential(*layers) def forward(self, x): return self.block(x) class Transition(nn.Module): def __init__(self, in_channels, out_channels): super().__init__() self.bn nn.BatchNorm2d(in_channels) self.relu nn.ReLU(inplaceTrue) self.conv nn.Conv2d(in_channels, out_channels, kernel_size1, biasFalse) self.pool nn.AvgPool2d(kernel_size2, stride2) def forward(self, x): out self.conv(self.relu(self.bn(x))) return self.pool(out) class DenseNetMNIST(nn.Module): def __init__(self, growth_rate12, block_layers[4, 4, 4], compression0.5, num_classes10): super().__init__() in_channels 64 self.stem nn.Sequential( nn.Conv2d(1, in_channels, kernel_size3, padding1, biasFalse), nn.BatchNorm2d(in_channels), nn.ReLU(inplaceTrue) ) self.blocks nn.ModuleList() for i, num_layers in enumerate(block_layers): self.blocks.append(DenseBlock(in_channels, num_layers, growth_rate)) in_channels num_layers * growth_rate if i ! len(block_layers) - 1: out_channels int(in_channels * compression) self.blocks.append(Transition(in_channels, out_channels)) in_channels out_channels self.bn_final nn.BatchNorm2d(in_channels) self.relu_final nn.ReLU(inplaceTrue) self.avgpool nn.AdaptiveAvgPool2d((1, 1)) self.fc nn.Linear(in_channels, num_classes) def forward(self, x): x self.stem(x) for block in self.blocks: x block(x) x self.relu_final(self.bn_final(x)) x self.avgpool(x) x torch.flatten(x, 1) return self.fc(x)这个结构里growth_rate12控制每层新增的特征通道数compression0.5控制 Transition 层把通道数压一半。MNIST 输入是单通道 28x28stem 层先升到 64 通道然后进 Dense Block。三个 Block 之后特征通道数从 64 变成 64412112压缩到 56再变成 56412104压缩到 52最后变成 524*12100。分类头用全局平均池化接全连接参数量很小。3.2 数据加载与训练超参MNIST 数据加载用torchvision.datasets预处理做标准化。训练超参我试过几组batch_size128、lr0.001、epochs30比较稳。优化器用 Adam学习率调度用余弦退火。import torch from torch.utils.data import DataLoader from torchvision import datasets, transforms import json with open(settings.json, r) as f: cfg json.load(f) train_cfg cfg[training] transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) train_dataset datasets.MNIST(roottrain_cfg[data_dir], trainTrue, downloadTrue, transformtransform) test_dataset datasets.MNIST(roottrain_cfg[data_dir], trainFalse, downloadTrue, transformtransform) train_loader DataLoader(train_dataset, batch_sizetrain_cfg[batch_size], shuffleTrue, num_workers4) test_loader DataLoader(test_dataset, batch_sizetrain_cfg[batch_size], shuffleFalse, num_workers4) device torch.device(cuda if torch.cuda.is_available() else cpu) model DenseNetMNIST().to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lrtrain_cfg[lr]) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_maxtrain_cfg[epochs])3.3 训练循环与日志训练循环里每轮记录训练损失、训练准确率、测试准确率。日志直接打印也可以写到文件里。def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss 0 correct 0 total 0 for images, labels in loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() total_loss loss.item() * images.size(0) _, predicted outputs.max(1) correct predicted.eq(labels).sum().item() total labels.size(0) return total_loss / total, correct / total def evaluate(model, loader, criterion, device): model.eval() total_loss 0 correct 0 total 0 with torch.no_grad(): for images, labels in loader: images, labels images.to(device), labels.to(device) outputs model(images) loss criterion(outputs, labels) total_loss loss.item() * images.size(0) _, predicted outputs.max(1) correct predicted.eq(labels).sum().item() total labels.size(0) return total_loss / total, correct / total best_acc 0.0 for epoch in range(train_cfg[epochs]): train_loss, train_acc train_one_epoch(model, train_loader, optimizer, criterion, device) test_loss, test_acc evaluate(model, test_loader, criterion, device) scheduler.step() print(fEpoch {epoch1:02d} | train_loss{train_loss:.4f} train_acc{train_acc:.4f} | test_loss{test_loss:.4f} test_acc{test_acc:.4f}) if test_acc best_acc: best_acc test_acc torch.save(model.state_dict(), f{train_cfg[checkpoint_dir]}/densenet_mnist_best.pth) print(fBest test acc: {best_acc:.4f})4. 验证请求与成功结果一轮训练日志与准确率验证跑完 30 轮日志大概长这样Epoch 01 | train_loss0.2143 train_acc0.9352 | test_loss0.0621 test_acc0.9812 Epoch 02 | train_loss0.0587 train_acc0.9821 | test_loss0.0412 test_acc0.9867 Epoch 03 | train_loss0.0412 train_acc0.9876 | test_loss0.0345 test_acc0.9891 ... Epoch 28 | train_loss0.0089 train_acc0.9978 | test_loss0.0213 test_acc0.9981 Epoch 29 | train_loss0.0081 train_acc0.9981 | test_loss0.0208 test_acc0.9983 Epoch 30 | train_loss0.0078 train_acc0.9983 | test_loss0.0205 test_acc0.9983 Best test acc: 0.9983测试集准确率 99.83%比之前 ResNet 版本的 99.6% 有提升。更重要的是在实际项目数据上整体准确率从不到 80% 提到了 96% 以上单个数字识别率 99.8% 以上。这个提升主要来自 DenseNet 的特征复用能力对书写不规范的数字更鲁棒。推理验证脚本可以单独跑import torch from PIL import Image import torchvision.transforms as transforms model DenseNetMNIST().to(device) model.load_state_dict(torch.load(cfg[inference][model_path], map_locationdevice)) model.eval() transform transforms.Compose([ transforms.Grayscale(num_output_channels1), transforms.Resize((28, 28)), transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) img Image.open(test_digit.png) img_tensor transform(img).unsqueeze(0).to(device) with torch.no_grad(): output model(img_tensor) pred output.argmax(dim1).item() print(fPredicted digit: {pred})如果你想把推理结果复核环节接到大模型上可以用 TaoToken 的模型对话接口做二次确认配置还是走settings.json里的taotoken段。5. 本篇常见错排查报错一RuntimeError: Given groups1, weight of size [12, 64, 3, 3], expected input[128, 1, 28, 28] to have 64 channels这个错是因为 stem 层没把单通道升到 64 通道或者 Dense Block 的输入通道数算错了。检查DenseNetMNIST里in_channels的初始值是不是 64以及每个 Block 之后有没有正确累加num_layers * growth_rate。报错二CUDA out of memoryMNIST 的 28x28 图很小DenseNet 参数量也不大正常不会 OOM。如果出现先检查batch_size是不是设太大了128 一般够用。另外确认num_workers不要设太高4 就行。报错三测试准确率卡在 99% 上不去先看训练准确率是不是已经 99.9% 以上如果是说明过拟合了。可以加 Dropout 或者数据增强随机旋转、平移。如果训练准确率也上不去检查学习率是不是太大lr0.001是 Adam 的常用值再大容易震荡。报错四settings.json读取失败确认文件路径对不对以及 JSON 格式有没有多逗号。可以用python -m json.tool settings.json检查格式。如果api_key字段为空训练脚本本身不受影响但后续调用 TaoToken 接口会报 401。报错五推理时预测结果全是同一个数字大概率是预处理没对齐。训练时用了Normalize((0.1307,), (0.3081,))推理时也要用同样的均值和方差。另外确认输入图片是灰度图不是 RGB。6. 把训练链路接到 TaoToken 统一 Key 上DenseNet 手写数字识别模型本身是本地训练的但训练链路里的辅助环节——比如用大模型生成数据增强脚本、复核推理结果、写训练日志摘要——都可以通过 TaoToken 统一 Key 来调用。你只需要在settings.json里配一次api_base和api_key训练脚本、验证脚本、辅助工具都能复用。如果你在排障或者接入过程中遇到问题可以先看接入文档里面有完整的参数说明和示例。如果只是想验证模型对话能力模型对话入口更直接。如果是长期跑编码任务或者 Agent 流程Coding Plan 的配置方式更适合。API Key 在控制台创建记得不要提交到仓库。这套配置我实测下来训练链路和 API 调用互不干扰Key 管理也清爽。你可以先把 DenseNet 模型跑通再把 TaoToken 配置加进去逐步替换掉硬编码的调用方式。
返回列表