news 2026/10/6 16:51:45

基于Matlab的CNN图像分类实战:从原理到代码解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于Matlab的CNN图像分类实战:从原理到代码解析

这篇笔记是系列第三篇,前两篇我分别梳理了CNN的基本原理和网络结构怎么画、怎么理解,到了这一篇,我猜很多人跟我当初一样,卡在了同一个地方:博客和PPT看了不少,卷积、池化、步长、填充这些概念都能背了,但一打开Matlab,面对trainNetwork、convolution2dLayer、trainingOptions这些函数还是不知道从哪里下手,更别说把代码和脑子里那套“特征提取”的图对应起来。

先说结论:用Matlab学CNN,尤其是想快速验证想法或者把深度学习用在自己课题里的人,真没有网上说的那么“不专业”。Deep Learning Toolbox里的高层API封装得相当干净,写起来比Python+Keras更像在写数学公式,而且数据预览、中间层可视化、断点调试这些体验,对刚入门的人来说实在太友好了。这篇笔记会以一段能直接运行的CNN训练代码作为主线,逐段拆解每行代码背后的原理和调用逻辑,再把我实际训练时踩过的几个坑完整复盘一遍,包括损失变成NaN、验证集准确率突然归零这类问题。适合正在用Matlab做图像识别、信号分类,或者毕业论文里需要跑深度学习的读者参考。

1. 从“背函数”到“看代码”:我为什么用Matlab学CNN

1.1 我的学习路线和Matlab的位置

我在前两篇笔记里走过一条弯路:先花大量时间啃数学推导,再想一把梭直接上Python和PyTorch,结果卡在环境配置和数据预处理上,一个周末什么都没跑出来。后来回到Matlab,反而两天就把一个手写数字识别的CNN跑通了。原因很简单:Matlab里图像读取、矩阵操作、标签管理、绘图这些基础能力是天然自带的,不需要像Python那样拼一堆库,深度学习工具箱又把这些能力直接串成了流水线。

所以我现在的建议是:如果你已经有Matlab基础,不要有“深度学习必须用Python”的心结。Matlab的Deep Learning Toolbox完全可以用来入门CNN,而且由于它把底层计算封装得比较彻底,你反而能更早地把注意力集中在“网络结构怎么搭、参数为什么这么设”这些更关键的问题上。

1.2 从“会用函数”到“看懂代码”的转变

很多教程会直接给你一段trainNetwork的完整代码,然后说“你运行一下看看效果”。这种教法的问题在于,你跑通了也不知道发生了什么,改一个参数也不会调,换个任务直接懵。真正有效的学习方式,是把这段代码当成一条链路来读:数据怎么进去的、每一层做了什么事、训练循环在优化什么、最后输出的是什么。

Matlab的CNN代码之所以适合做这种“逐行拆解”,是因为它的API设计逻辑和CNN的物理结构几乎是一一对应的。imageInputLayer对应输入图像,convolution2dLayer就是卷积操作,maxPooling2dLayer就是下采样,fullyConnectedLayer就是把特征图展平做分类,你写的代码和网络结构图是能互相印证的,这一点比看抽象的框架源码要直观得多。

2. 一个能直接运行的CNN代码:数字识别全流程

2.1 数据准备:imageDatastore到底帮你做了什么

我用的数据集是Matlab自带的DigitDataset,路径为matlabroot/toolbox/nnet/nndemos/nndatasets/DigitDataset,里面是10000张28x28的灰度手写数字图(0到9各1000张),文件按类别分文件夹存放。加载代码就三行:

digitDatasetPath = fullfile(matlabroot, 'toolbox', 'nnet', 'nndemos', ... 'nndatasets', 'DigitDataset'); imds = imageDatastore(digitDatasetPath, ... 'IncludeSubfolders', true, ... 'LabelSource', 'foldernames');

这段代码的核心是imageDatastore,它的作用有两个:一是自动遍历子文件夹,把每张图片的路径读进来;二是按文件夹名字自动生成标签,也就是foldernames这个参数的效果。换句话说,只要你的数据按类别放在不同文件夹里,Matlab会帮你把“图片”和“标签”的对应关系建立好,这是后面训练的基础。

紧接着要把数据分成训练集和验证集:

[imdsTrain, imdsValidation] = splitEachLabel(imds, 0.8, 'randomized');

splitEachLabel(imds, 0.8)表示每个类别的样本按80%和20%的比例拆分,'randomized'表示先随机打乱再拆,避免同一个类别的图片连在一起造成训练分布偏差。

再往后是数据增强。这一步对数字识别来说很有必要,因为手写数字的写法千变万化,如果不做任何变换,网络很容易过拟合。我用了imageDataAugmenter:

imageAugmenter = imageDataAugmenter( ... 'RandRotation', [-15 15], ... 'RandXTranslation', [-3 3], ... 'RandYTranslation', [-3 3]); augimdsTrain = augmentedImageDatastore([28 28], imdsTrain, ... 'DataAugmentation', imageAugmenter); augimdsValidation = augmentedImageDatastore([28 28], imdsValidation);

augmentedImageDatastore的作用是:在训练过程中,每次取一个mini-batch时,对图片做随机旋转和平移,相当于免费扩充了训练样本量。'RandRotation'的范围我后来从±180度改到了±15度,原因后面会讲。注意验证集没有加数据增强,因为验证集要尽量反映真实分布。

2.2 搭建网络:从输入层到分类层的每一层含义

我用的网络结构不算深,但对理解CNN的流程已经足够:

layers = [ imageInputLayer([28 28 1], 'Name', 'input') convolution2dLayer(3, 8, 'Padding', 'same', 'Name', 'conv1') batchNormalizationLayer('Name', 'bn1') reluLayer('Name', 'relu1') maxPooling2dLayer(2, 'Stride', 2, 'Name', 'pool1') convolution2dLayer(3, 16, 'Padding', 'same', 'Name', 'conv2') batchNormalizationLayer('Name', 'bn2') reluLayer('Name', 'relu2') maxPooling2dLayer(2, 'Stride', 2, 'Name', 'pool2') fullyConnectedLayer(10, 'Name', 'fc') softmaxLayer('Name', 'softmax') classificationLayer('Name', 'output') ];

逐个说关键层:

imageInputLayer([28 28 1]):输入层,28 28是图像宽高,1是通道数。灰度图是1通道,RGB彩色图是3通道,这个必须和你的数据一致。

convolution2dLayer(3, 8, 'Padding', 'same'):3是卷积核大小(3x3),8是卷积核个数,也就是输出特征图的深度。'Padding', 'same'表示保持输出尺寸和输入一致,等会儿会算给你看。

batchNormalizationLayer:批归一化层,作用是把每一批数据的分布拉回均值为0、方差为1附近,让训练更稳定、收敛更快。不少新手会默认“CNN就是卷积+池化+全连接”,把BN漏掉,实测下来加不加BN,收敛速度和最终准确率差别还是挺大的。

reluLayer:激活函数,把负值置零,引入非线性。没有它,多层卷积叠加起来还是一个线性变换,网络表达能力会大打折扣。

maxPooling2dLayer(2, 'Stride', 2):最大池化,核是2x2,步长也是2。作用是把特征图尺寸缩小一半,同时保留局部最明显的特征。池化没有需要学习的参数,它的作用是减少计算量、扩大感受野。

fullyConnectedLayer(10):全连接层,10对应10个数字类别。这一层会把前面得到的特征图“展平”成一维向量,然后做线性变换,输出每个类别的得分。

softmaxLayer和classificationLayer:softmax把得分转成概率,分类层再根据概率输出最终类别标签。这两个是配套使用的,不能只留一个。

2.3 训练选项配置:这些参数别只抄答案

训练网络之前还要设置trainingOptions,我常用的配置如下:

options = trainingOptions('sgdm', ... 'InitialLearnRate', 0.01, ... 'MiniBatchSize', 128, ... 'MaxEpochs', 12, ... 'Shuffle', 'every-epoch', ... 'ValidationData', augimdsValidation, ... 'ValidationFrequency', 30, ... 'Verbose', true, ... 'Plots', 'training-progress');

逐个解释为什么这么设:

  • 'sgdm':带动量的随机梯度下降算法。动量可以理解为给参数更新加了一个“惯性”,能在一定程度上抑制震荡,让训练更平滑。对初学者这个就是最稳妥的默认选择。
  • 'InitialLearnRate', 0.01:初始学习率。学习率决定了每次参数更新的步长,太大了会震荡甚至发散,太小了收敛极慢。0.01对这个小网络是个合理的起点,后文会展示学习率设成0.1时损失直接变NaN的翻车现场。
  • 'MiniBatchSize', 128:每批送入网络训练的样本数。这个值受显存或内存限制,调小了训练更稳但更慢,调大了对梯度估计更准但更占内存。
  • 'MaxEpochs', 12:整个训练集被完整遍历12遍。对数字识别这种简单任务,12轮已经足够了,继续增大收益很小,还可能过拟合。
  • 'Shuffle', 'every-epoch':每一轮训练前都把样本重新打乱,避免网络学到样本顺序带来的假规律。
  • 'ValidationData', augimdsValidation:指定验证集,训练过程中会自动评估验证准确率并在图上显示,方便你观察有没有过拟合。
  • 'ValidationFrequency', 30:每30次迭代验证一次。
  • 'Plots', 'training-progress':画出训练进程图,这个功能对初学者太重要了,能直接看到损失曲线和准确率曲线的变化。

然后执行训练:

net = trainNetwork(augimdsTrain, layers, options);

2.4 训练后评估:光看损失下降还不够

训练完成后,用验证集评估模型:

[YPred, scores] = classify(net, augimdsValidation); YValidation = imdsValidation.Labels; accuracy = mean(YPred == YValidation); fprintf('验证集准确率: %.2f%%\n', accuracy * 100);

这里有个很容易搞错的地方:YValidation不能从splitEachLabel之前的旧变量里拿,而是要直接用imdsValidation.Labels。因为imdsValidation是拆分后的新数据存储,它维护着自己的标签顺序,如果你用旧的YTrain或者别的标签数组去对,顺序对不上,准确率会掉得莫名其妙。这个问题我后面还会详细复盘。

如果想看得更细,可以用confusionchart(YValidation, YPred)画混淆矩阵,能直观地看出哪些类别之间容易混淆。比如7和1、9和4这类手写体,在28x28的分辨率下确实容易搞混,这时你会意识到数据增强、网络深度、训练参数都不是拍脑袋定的,而是要根据这些实际反馈来调。

3. 逐步拆解训练过程:代码背后的计算逻辑

3.1 一张图怎么穿过卷积层:卷积核、步长和填充

很多人对照代码看网络图,最困惑的就是“卷积到底做了什么”。我用一个简单例子解释。假设输入是5x5的灰度图,卷积核是3x3,步长stride=1,padding=0,那么输出尺寸用公式算:

[ H_{out} = \lfloor \frac{H_{in} + 2P - K}{S} \rfloor + 1 ]

其中H_in是输入高度,K是卷积核尺寸,P是填充像素数,S是步长。代进去就是:

[ H_{out} = \lfloor \frac{5 + 0 - 3}{1} \rfloor + 1 = 3 ]

所以输出是3x3的特征图。这个3x3的每个位置,都是原图对应3x3区域和卷积核逐元素相乘再求和得到的。比如原图某局部区域是[1 0 1; 0 1 0; 1 0 1],卷积核是[1 0 -1; 1 0 -1; 1 0 -1],两者点积就是11+00+1*(-1)+01+10+0*(-1)+11+00+1*(-1)=0。这个核是一个典型的垂直边缘检测核,如果图像局部左右对称,响应是0;如果一边亮一边暗,响应会很大。所以卷积层提取到的特征,本质上就是“图像局部和这个核的相似程度”。

那'Padding', 'same'是干嘛的?如果不加padding,卷积后尺寸会缩小,多次卷积后图像就变得特别小,边缘信息也会快速丢失。same的意思是给原图四周补上足够多的0,让输出尺寸和输入一致,这样网络层数就可以堆得更深。

步长Stride控制的是卷积核每次滑动多远。步长=1意味着逐像素滑动,步长=2相当于跳着看,输出尺寸减半。步长越大,特征图越小,计算量也越小,但可能会丢失细节,所以实际使用中步长通常设在1或2。

3.2 我这段代码里的特征图尺寸是怎么变化的

把前面网络的尺寸计算全部画出来,就能完整看到一张28x28的图像是怎么“流动”的:

层输入尺寸参数输出尺寸
imageInputLayer--28x28x1
conv128x28x13x3, 8个核, padding=same, stride=128x28x8
pool128x28x82x2, stride=214x14x8
conv214x14x83x3, 16个核, padding=same, stride=114x14x16
pool214x14x162x2, stride=27x7x16
展平7x7x16-1x784
fc1x78410个神经元1x10

注意最后一个池化输出的7x7x16,展平后是7×7×16=784个值,这784个数就是网络提取到的高层特征,全连接层把它们映射到10个类别的得分。

这个尺寸链条非常重要,尤其是当你自己搭网络时,最后一层全连接神经元的输入维度和上一层展平后的维度必须对得上,否则Matlab会直接报维度不匹配的错误。这也是为什么我建议你搭网络时,先在草稿纸上把每一层的输出尺寸算出来,再写代码。

3.3 训练循环在做什么:从损失值到反向传播

trainNetwork封装了前向传播、损失计算、反向传播和参数更新。很多初学者以为options里设了sgdm就完事了,其实你应该知道这几件事在每次迭代里是怎么发生的:

前向传播:一个mini-batch的图像经过所有层,得到预测概率。分类层用的是交叉熵损失,简单理解就是“预测概率分布和真实标签分布有多不一致”,损失越大说明错得越离谱。

反向传播:根据损失对每个参数的梯度,从最后一层往第一层逐层回传。Matlab内部用的是自动微分,你不需要手推梯度公式,但你要知道:梯度计算是逐层的,靠近输出层的层梯度信号强、更新快,靠近输入层的层梯度信号弱、更新慢。这也是为什么网络深了以后需要BN、残差连接这些手段来帮助梯度传播。

参数更新:sgdm在每一步会用当前梯度更新参数,同时考虑上一次更新的方向,公式可以近似理解为:

[ v_{t+1} = m v_t + \text{lr} \cdot \nabla L ]

[ w_{t+1} = w_t - v_{t+1} ]

其中m是动量系数(默认0.9),lr是学习率。v累积了过去梯度的指数衰减平均,相当于给更新方向加了“惯性”。

还有一个关键认知:BN层里的均值和方差是在训练过程里逐步估计出来的,这决定了它在训练和推理(验证)两种模式下的行为略有不同。训练时用当前batch的统计量,验证时用训练阶段累积的移动平均。所以把验证集也做增强,或者让模型在验证集上“见过”数据,都会污染验证效果。

3.4 验证集为什么不能参与训练

这句话听起来像废话,但在实操中太容易踩雷了。我之前见过有人把整个imds既当训练集又当验证集传进ValidationData,结果训练图上的验证准确率接近100%,一换到真实新数据上就拉胯。这就是信息泄露:验证集如果参与了训练,哪怕只是被“观察”过,你调参时就已经在隐式地拟合它了。

正确的做法就是splitEachLabel拆开之后,训练和验证严格分开,验证集只在每个ValidationFrequency周期被评估一次,它的作用只是给你一个“模型目前泛化得怎么样”的读数,辅助你判断该停还是该调参数。

4. 实测中踩过的坑:损失曲线异常和内存不足

4.1 学习率设成0.1,损失直接NaN

我在一次实验里图快,把InitialLearnRate从0.01改成了0.1,心想反正网络小,步子大一点没关系。结果训练不到10次迭代,损失值直接变成了NaN,训练过程图上出现一条断崖式的曲线,准确率也跟着崩了。

排查思路是这样的:先看是不是数据里有NaN。我检查了输入图像,用ismissing查了标签,数据本身没问题。然后把学习率降到0.01,问题立刻消失。原因其实不复杂:学习率过大时,参数更新步长超出损失曲面允许的范围,梯度爆炸到数值溢出,NaN一旦出现,后续所有参数更新都会受污染,基本救不回来。

这个坑给我的教训是:改动超参数时,最好一次只动一个,而且改完后先跑两三个epoch看损失走向。如果你发现损失在前几个迭代就剧烈震荡或直接变NaN,首选操作是把学习率往小调一个数量级,比如从0.01改到0.001,而不是怀疑网络结构写错了。

4.2 miniBatchSize和内存不足:8GB显存也扛不住

做数字识别时,把MiniBatchSize设成1024之后,训练开始没多久就报Out of memory。这是因为每个mini-batch都需要把全部中间特征图保存在GPU显存里用于反向传播,批越大,特征图占用的显存就越多。

解决办法有两个方向:一是把MiniBatchSize降到128或64,通常能立刻解决问题;二是如果必须用大批次,考虑改用分布式训练或者减小输入图像尺寸。另外提醒一句,Matlab用GPU训练需要Parallel Computing Toolbox,并且对显卡型号和驱动版本有要求,在命令行输入gpuDevice可以查看GPU是否可用。

如果显存还是不够,可以考虑trainNetwork的另一个选项'ExecutionEnvironment','cpu',但CPU训练会慢很多,只建议在调试小模型时用。

4.3 验证集准确率突然归零:一次完整的排查链路

这个坑非常典型。有一次训练过程显示训练准确率稳定上升,最后接近99%,但验证集准确率却变成了10%左右,大约是随机猜的概率。当时第一反应是网络过拟合,但过拟合也不至于验证准确率掉到随机水平。

我按下面的顺序一步步排查:

  1. 先检查YPred和YValidation的长度是否一致。用size一查,发现两者样本数一样,排除长度不匹配问题。
  2. 画混淆矩阵,发现所有验证样本都被预测成同一个数字。这说明网络并没有“部分学会”,而是输出已经完全偏了。
  3. 回头看训练曲线,训练准确率明明很高,验证集却全偏,这不符合过拟合的表现,更像是数据标签错位。
  4. 检查imdsValidation.Labels和augimdsValidation的对应关系。augmentedImageDatastore内部维护了一套自己的样本队列,它和imdsValidation.Labels的顺序在理论上是一致的,但如果我在拆分后重新shuffle过验证集,或者把两个不同数据存储混在一起,就会导致标签顺序对不上。
  5. 最后发现,问题出在我一次清理代码时使用了imdsValidation = imdsValidation.shuffle(),而YValidation还是从imdsValidation.Labels里取的,理论上这样没问题。真正的问题是我在一个子函数里把imdsValidation又当成了全局变量做了二次拆分,导致标签和图片错位。

这个案例给我的教训是:Matlab的数据存储对象是带内部状态的,你调了shuffle之后,它内部的顺序会变,但Labels属性的顺序只和你最后一次操作有关。训练代码里最好保持一个原则:imdsValidation和它的Labels必须在同一个作用域内一起取出、一起使用,不要跨函数传完再回头对标签。

4.4 数据增强过度:数字识别准确率不升反降

数据增强的初衷是好的,但如果幅度过大,反而会让任务变得不真实。我一开始把RandRotation设成±180度,想着数字旋转也能识别,网络应该能学会。结果验证准确率比不增强还低了好几个百分点。原因是6倒过来会像9,7倒过来完全不像7,数据增强把类别之间的边界搞模糊了,网络被迫去学那些现实中不太可能出现的变形,学偏了。

后来把旋转范围缩到±15度,保留小幅平移,验证准确率才恢复并超过基线。这个小实验告诉我:数据增强的幅度一定要结合任务本身来判断,不能只追求“数据多”,更要追求“数据像”。

4.5 工具箱和版本导致的隐藏错误

trainNetwork报错的时候,很多情况不是代码逻辑问题,而是工具箱缺失。Matlab的深度学习功能分布在Deep Learning Toolbox里,GPU训练需要Parallel Computing Toolbox,如果你用的是老版本,可能还不支持某些层,比如batchNormalizationLayer。建议先用ver命令查看已安装工具箱,尤其是这几个:Deep Learning Toolbox、Parallel Computing Toolbox、Computer Vision Toolbox(做图像处理时会用到)。

另外提醒一句:尽量用正版授权或学校提供的正版版本。我见过有人在奇怪版本上装各种“密钥补丁”,训练到一半报出一堆看不懂的底层错误,其实只是版本破解不完全。为了跑通代码,花一整晚在这种事情上,非常不值得。

5. 把CNN概念和代码一块块对应起来

5.1 特征图可视化:用activations看网络学到了什么

训练结束后,最直观的验证方式是看看每一层到底提取了什么。Matlab用activations函数就可以做到:

testImg = imread(fullfile(digitDatasetPath, '0', 'img_1.jpg')); testImg = imresize(testImg, [28 28]); if size(testImg, 3) == 3 testImg = rgb2gray(testImg); end act1 = activations(net, testImg, 'conv1');

act1的形状是28x28x8,对应conv1层输出的8个特征图。把8个特征图分别画出来,你会看到有的特征图高亮区域集中在笔画边缘,有的集中在角落,这说明第一层卷积核学到的是不同朝向的边缘、拐角这类局部纹理。再把conv2层的特征图也画出来,会看到响应越来越抽象,开始倾向于组合低级特征,比如某些小结构或笔画模式。

用montage函数可以把所有特征图拼在一起显示:

figure; for i = 1:size(act1, 3) subplot(2,4,i); imshow(act1(:,:,i), []); title(sprintf('conv1 特征图 %d', i)); end

这种可视化对理解CNN特别有帮助,它能让你直观看到“卷积核是在找什么”、“第几层开始变得抽象”。很多教程用抽象的示意图,代码一跑可视化,你才算真正建立感觉。

5.2 感受野、展平和全连接:为什么最后一层是784到10

很多人不理解全连接层为什么能把二维特征图变成一维分类得分。其实fullyConnectedLayer在Matlab内部会自动把输入展平,也就是把7x7x16的多维数组reshape成一维的784个值,再做矩阵乘法。这个展平过程就是fc层做的第一件事。

感受野这个概念也能在这个例子里体会:第一层卷积核是3x3,所以每个输出像素只能看到原图像的一个3x3局部。经过第一次池化后,第二层卷积的3x3区域映射回原图,大约是7x7的区域。层数越深,单个输出单元能“看到”的原始图像范围越大,这就是感受野的扩大。理解了这一点,你才会明白为什么CNN不需要每个人都看整幅图,而是从局部到全局逐层抽象。

5.3 这套代码怎么迁移到自己的项目

数字识别代码跑通之后,大多数人要做的第一件事就是换成自己的数据集。你只需要注意几个改动点:

如果是彩色图像,imageInputLayer的第三个维度要改成3,比如[224 224 3]。同时augmentedImageDatastore的第一个参数也要改成对应尺寸,比如[224 224 3],它会自动把不同分辨率的图像resize到统一尺寸。

如果你的分类问题不是10类,fullyConnectedLayer的神经元数量要改成你的类别数。比如二分类就改成2,softmaxLayer不需要改。

如果你的数据量特别大,不要一次性把所有图片加载进内存,请继续使用imageDatastore的方式,它本身是“惰性加载”的,只在训练取batch时读取图片,内存友好得多。

另外,如果你的任务和通用图像分类比较接近,比如区分猫狗、花卉、工业零件缺陷,还有个更快的方案:用预训练网络做迁移学习。Matlab内置了resnet18、googlenet等模型,把最后几层替换成你自己的分类层,然后只训练后面几层。这种做法在小数据集上效果很好,训练时间也短。我写另一篇笔记时会专门展开迁移学习,这篇先把从零搭建的理解打扎实。

5.4 老生常谈但真的有用:先跑通,再调参

如果让我给刚接触Matlab CNN的人一个最浓缩的建议,我会说:先把这份数字识别代码原样跑通,再去动任何参数。跑通的意思是你能看到训练进度图、能得到一个80%以上的验证准确率,并且能画出几张预测结果图。这个流程走完,你对数据接口、网络定义、训练流程的基本盘就有数了,之后调参、换数据集才不会像无头苍蝇。

调参时也遵循“一次只改一个”的原则,改完记录结果。我建议准备一个小本子,专门记下每个实验的学习率、miniBatchSize、网络层数、数据增强参数和对应准确率。别看这个习惯简单,它比任何调参技巧都重要,因为深度学习实验属于典型的“没有记录就没有经验积累”的领域。

我在实际使用中还有个体会:Matlab的调试能力确实是学CNN的一大利器。你可以直接在trainNetwork之前加断点,查看layers里每一层是否按顺序排列,可以随时disp(size(XXX))查看数据尺寸,甚至在自定义训练循环里逐行检查梯度值。我后来遇到过很多网络维度不匹配的报错,几乎全是靠断点和size命令定位的。所以不要嫌麻烦,该打断点就打,该打印就打印,这个习惯能帮你省下大量排错时间。

最后再分享一个我自己一直在用的小技巧:训练完不要只盯着准确率,花两分钟把预测错的样本挑出来看。Matlab里可以用find(YPred ~= YValidation)找到预测错误的样本索引,然后逐个显示原图、真实标签和预测标签。很多时候你会发现,模型犯的错误往往也是人眼容易混淆的情况,比如潦草的7和1、边缘残缺的4和9。这时候你就知道下一步该补什么数据、加强哪种数据增强,而不是盲目地加深网络或者加大学习率。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/6 16:51:36

Oracle与崖山数据库排序性能对比:内存到磁盘的实测分析

老规矩,先说这次测试的来由。最近团队在评估国产数据库替换的可行性,Oracle那边一堆存量业务,崖山(YashanDB)是重点考察对象之一。替换评估不能光看兼容性清单,更不能信厂商的PPT,最靠谱的方式就…

作者头像 李华
网站建设 2026/10/6 16:51:01

AI编程超能力:Superpowers技术链原理与中文支持实战

1. “Superpowers”不是功能开关,而是新一代AI编程工具链的统称最近在开发者社区里,“superpowers”这个词出现频率高得有点反常——它既不像传统软件功能那样有明确按钮,也不像某个具体插件那样能直接在扩展市场搜到。我第一次看到是在 Curs…

作者头像 李华
网站建设 2026/10/6 16:47:33

从零实现JavaScript K线图:Canvas绘制与交互实战

简介:这是一份利用JavaScript与HTML5 Canvas实现的K线图组件,面向需要在网页或移动端加入蜡烛图走势展示的前端开发者,覆盖从基础绘制到移动端手势交互的完整实现思路。整体压缩包仅11KB,共3个文件,其中两个JavaScript…

作者头像 李华
网站建设 2026/10/6 16:45:53

MySQL 8.4 Docker 自定义镜像构建:Ubuntu与CentOS双版本实战

不少团队折腾 Docker 镜像时,都有一个绕不开的痛点:MySQL 官方镜像确实一行docker pull mysql:8.4就能用,可真到了生产环境,时区不对、字符集不合规、插件版本不匹配、内网拉不到镜像、安全审计要求逐层追溯,官方镜像那…

作者头像 李华
网站建设 2026/10/6 16:45:48

浮点运算工程实践:可复现性、误差控制与混合精度优化

如果前面的七篇你都跟下来了,我相信你对float和double的底细已经比大多数同事清楚:符号位、指数、尾数、舍入模式、特殊值、ulp,这些概念现在应该能脱口而出。但真到工程里,还是会碰到很多“纸面上讲不通”的问题:为什…

作者头像 李华
网站建设 2026/10/6 16:41:53

MySQL慢SQL优化实战:慢查询日志、复合索引与索引失效全解析

1. 从业务现象到优化目标:一张慢 SQL 引发的血案做后端开发的,多少都有过这种经历:线上系统毫无征兆地开始卡顿,接口响应从几十毫秒变成几秒甚至几十秒,用户投诉电话一个接一个,运维盯着监控大屏一脸惊慌&a…

作者头像 李华