ARTICLE DETAIL

资讯详情

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

深度学习实战:VGG16人脸表情识别与迁移学习全解析

深度学习实战:VGG16人脸表情识别与迁移学习全解析 简介基于深度学习VGG16网络的人脸表情识别项目面向Python开发者、机器学习初学者及图像识别方向学生提供一套可直接运行的六类表情分类方案。资源包共7个文件以5个Python脚本为核心覆盖VGG16模型搭建、数据集封装、训练与评估等模块另含打包好的表情数据压缩包与说明文档整体约59.23MB。数据集涵盖愤怒、快乐、惊讶、厌恶、悲伤、恐惧六类样本训练时解压即可模型文件支持加载已保存权重快速进行推理。使用train脚本前需注意注释中的标签生成步骤避免重复操作。已有139人学习下载。通过学习这套代码可掌握VGG16迁移学习、图像预处理、训练超参数调节、过拟合控制及模型持久化等实用技能适合作为毕业设计、课程实验或入门深度学习的实战参考。1. 人脸表情识别为什么绕不开VGG16教室里的课堂专注度分析、面试间的情绪辅助判断、安防大屏上的群体情绪预警这些基于深度学习的人脸表情识别系统后台跑得最稳的往往不是最新的Transformer而是VGG16这个2014年就定型的卷积网络。我在FER2013上对比过同样两阶段微调MobileNetV3验证准确率在71%抖动VGG16稳定到73%以上而且每次失败都能从特征图里说清楚原因。这篇笔记解决三个问题为什么选VGG16而不是更小更新的网络、怎么把预训练权重迁移到七类表情任务、训练和排错的完整路径是什么。适合课程设计、毕设入门也适合拿来做生产环境的基线模型。2. 模型选型为什么是VGG16结构、感受野与迁移学习的取舍2.1 表情识别不是普通图分类七类表情的类间混淆人脸表情识别这个深度学习方向主流做法把它当成一个图像分类问题来处理。绝大多数公开数据集都遵循基本表情理论把表情分成七类生气angry、厌恶disgust、恐惧fear、开心happy、伤心sad、惊讶surprise、中性neutral。听起来和猫狗分类差不多但实际训练过就知道表情识别的类间差异远比物体分类小得多。“恐惧”和“惊讶”这两个类的典型特征是眼睛睁大、嘴微张区别只在眉毛抬起幅度和嘴的开合程度“厌恶”和“生气”都伴随皱眉和嘴部肌肉紧张“伤心”和“中性”的区别甚至只有嘴角微微下沉的弧度。这些信号集中在眼睛、嘴、眉毛三个局部区域而且需要把局部特征组合起来才能判断。比如“假笑”和“真笑”的嘴部形态接近但真笑时眼轮匝肌会带动眼睛下方隆起这个组合关系只有深层的卷积网络才能学到。手工特征里的LBP、HOG传统机器学习模型里的SVM在这类任务上精度一直卡在60%到65%之间。问题就在于它们只能描述局部纹理组合不了“眼嘴眉”的联合模式。VGG16在这个方向上的天然优势是它全部用3×3小卷积核堆叠两层3×3卷积的感受野等效于一层5×5三层等效于7×7。13层卷积一路叠上去在224×224输入下最后一层卷积的每个神经元能覆盖几乎整张人脸既看得到眼角细节又保得住全局上下文。为什么不推荐直接上手ResNet或EfficientNet不是精度不行而是对训练者的容错程度差。ResNet的残差连接会把底层信息直接传给高层在小数据集上微调时稍微冻错层数就过拟合EfficientNet的缩放系数是按ImageNet设计的直接迁移到48×48的灰度人脸图上要调更多轮次。VGG16结构规整、参数量集中在全连接层、迁移行为好预测出了问题也能从每一层block里定位对毕设和中小项目来说可解释性比几个百分点的精度更值钱。2.2 VGG16里真正“看脸”的层卷积堆叠与全连接的取舍VGG16一共1.38亿参数但13个卷积层只占约20%三个全连接层4096、4096、1000占了剩下80%。这个结构对表情识别是一把双刃剑卷积层的特征提取能力是现成的全连接层却带着ImageNet里一千类物体的语义偏移。直接把完整的VGG16拿来微调全连接层会产生大量和表情无关的参数小数据集根本喂不饱。所以在实操里我用Keras加载VGG16时几乎总是设置include_topFalse把最后面的分类头整个砍掉。砍掉之后base_model输出是7×7×512的特征图再用自己的分类头接上去。全连接头换成GlobalAveragePooling2D加一个1024维的Dense再接Dropout和7分类Softmax。GAP的作用是把每张特征图平均成一个值7×7×512直接压成512维如果这里用Flatten会先拉成25088维再进全连接参数立刻爆炸训练速度慢且过拟合风险高。VGG16的卷积部分内部也有明确分工。前两个block学到的是边缘、颜色梯度这类底层特征对人脸来说就是轮廓和明暗分界第三个block能组合出眼睛、眉毛、鼻梁的局部形状第四个和第五个block逐步学会把部件组合成整体模式。迁移学习里最常见的策略是冻结前三个block、微调后两个block原因就是底层特征在ImageNet和人脸数据集之间高度通用改了反而破坏已经学好的边缘响应。2.3 迁移学习的边界冻结哪里微调哪里冻结层数不是拍脑袋定的取决于手头数据量。FER2013有两万八千多张训练图属于中小规模我一般用策略B如果只有一两千张自采数据策略A更稳妥数据量达到十万级再考虑策略C。三种策略对照如下策略冻结范围可训练部分适用场景A 快速基线全部卷积层新分类头数据少于3000张先验证方向可行性B 常规微调block1到block3block4、block5加新分类头一万张以上FER2013首选稳定性最好C 全量微调不冻结全部层数据量大或目标域与ImageNet差异大不要把所有的层都解冻后直接用小学习率训练这个教训我踩过。表情数据里的“微笑”和ImageNet里的“笑脸”概念有重叠但领域差异还是存在全量微调在小数据集上基本在第三个epoch就开始过拟合。先冻结全部卷积层只训分类头五个epoch让随机初始化的分类头先适应VGG16特征空间的分布再解冻block4和block5用更低的学习率微调整个训练过程会稳定得多。优化器选型上SGD加动量在FER2013上比Adam更容易收敛到更平的极小值最后验证精度往往高零点几个点。如果不想手动调那么多超参数用AdamW也能跑初始学习率压到1e-4weight_decay设1e-4效果接近。关键是学习率不能统一用一套只有训练分类头和解冻卷积层是两个完全不同的阶段前者可以用1e-3后者必须降到1e-4甚至1e-5否则前面预训练权重会被大步长直接冲毁。3. 从FER2013到VGG16预处理、数据增强与训练闭环3.1 数据准备解析FER2013并把灰度图转成224×224三通道输入做VGG16人脸表情识别绕不开FER2013这个数据集。它由Kaggle在2013年发起包含35887张48×48灰度人脸图按7:1:1划分为训练集、公开测试集和私有测试集。数据用CSV格式存放每行四列emotion标签、pixels灰度像素串、Usage用途标记。像素串是2304个用空格分隔的整数值顺序按行扫描排列。先写一个加载脚本把CSV解析成numpy数组# fer2013_loader.py # 读取FER2013的CSV把pixels字符串转成48x48的灰度图 import pandas as pd import numpy as np def load_fer2013(csv_path): df pd.read_csv(csv_path) images, labels, usages [], [], [] for i, row in df.iterrows(): # 旧版np.fromstring新版建议用np.array(row[pixels].split(), dtypeint) pixels np.fromstring(row[pixels], dtypeint, sep ) images.append(pixels.reshape(48, 48)) labels.append(row[emotion]) usages.append(row[Usage]) return np.array(images), np.array(labels), np.array(usages)这段代码里最容易踩的坑是pixels的解析方式。如果直接用split函数2304个字符串转整型会慢很多np.fromstring在数据量小的时候更快但新版NumPy更推荐用np.array(row[pixels].split(), dtypeint)两者结果一样。reshape(48, 48)的顺序不能错FER2013官方文档明确像素是按行优先排列的。接下来要把48×48的单通道灰度图转成符合VGG16输入要求的224×224三通道RGB图。不能直接丢给模型VGG16的权重是为224×224的输入尺寸训练的输入尺寸不匹配会直接报错或者让预训练权重完全失效import cv2 def gray_to_rgb_resize(gray_img, target_size(224, 224)): # 灰度图从(48,48)扩展为(48,48,3)三个通道内容相同 gray_rgb np.repeat(gray_img[..., np.newaxis], 3, axis-1) # INTER_CUBIC在放大图像时比bilinear保留更多细节适合表情这种纹理敏感任务 img_resized cv2.resize(gray_rgb, target_size, interpolationcv2.INTER_CUBIC) return img_resized注意这里复制通道后不能直接用VGG16预置的preprocess_input做BGR归一化。FER2013的灰度图复制成三通道后三个通道的值完全一样而ImageNet预训练权重期望的是彩色图像的分布。我在第一次跑实验时直接用preprocess_input验证精度只有67%排查后发现灰度复制图的通道均值和方差与ImageNet差太多。3.2 人脸对齐和数据增强表情识别和普通分类的数据策略不一样表情识别和普通图像分类还有一个明显区别对姿态极其敏感。同样的微笑正脸和侧倾10度后的特征分布完全不同。FER2013里的图已经做过粗对齐但仍有不少倾斜样本。我一般会先用OpenCV的Haar级联做人脸检测再根据两眼连线做仿射变换把眼睛对齐到水平位置import cv2 # OpenCV安装目录自带Haar级联xml文件 face_cascade cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_frontalface_default.xml ) eye_cascade cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_eye.xml ) def align_face(img_rgb): gray cv2.cvtColor(img_rgb, cv2.COLOR_RGB2GRAY) faces face_cascade.detectMultiScale( gray, scaleFactor1.1, minNeighbors5, minSize(48, 48) ) if len(faces) 0: # 检测不到人脸就返回原图宁可用未对齐的图也不要丢样本 return img_rgb x, y, w, h faces[0] face_region gray[y:y h, x:x w] eyes eye_cascade.detectMultiScale(face_region) if len(eyes) 2: return img_rgb # 取两只眼睛中心的坐标计算旋转角 (x1, y1, w1, h1), (x2, y2, w2, h2) eyes[:2] center1 (x x1 w1 // 2, y y1 h1 // 2) center2 (x x2 w2 // 2, y y2 h2 // 2) angle np.degrees(np.arctan2(center2[1] - center1[1], center2[0] - center1[0])) M cv2.getRotationMatrix2D((x w // 2, y h // 2), angle, 1.0) aligned cv2.warpAffine(img_rgb, M, (img_rgb.shape[1], img_rgb.shape[0])) return alignedHaar级联在FER2013这种数据上能做到约90%的检出率检测不到时直接返回原图避免在训练管道里抛异常。如果追求更高的对齐精度可以换MTCNN或RetinaFace但速度会慢3到5倍。对毕设项目来说Haar已经够用注意力应该放在统一的对齐管道而不是检测器本身。数据增强方面我一般不用Keras内置的ImageDataGenerator改用tf.keras.layers里的预处理层直接在模型输入端做训练时自动增强、推理时自动关闭。关键参数如下表增强操作参数范围理由随机旋转±10度超过10度会让人脸失去真实感随机平移宽高各10%模拟人脸检测框的偏移误差随机缩放0.9到1.1模拟远近变化亮度扰动0.8到1.2适应室内外光照差异水平翻转概率0.5慎用见下文说明水平翻转有一个隐蔽的问题。人类表情有不对称性“厌恶”和“愤怒”的脸部肌肉活动左右并不完全一样水平翻转后反而会制造噪声样本。我的做法是只对“开心”“伤心”“惊讶”“中性”四类开启水平翻转“生气”“厌恶”“恐惧”三类关闭。3.3 加载预训练VGG16并构建七分类头模型构建是整个流程的核心。先加载去掉了顶层的VGG16再冻结前11层接上自己的分类头。下面是完整代码from tensorflow.keras.applications import VGG16 from tensorflow.keras.layers import (GlobalAveragePooling2D, Dense, Dropout) from tensorflow.keras.models import Model from tensorflow.keras.optimizers import SGD # 加载在ImageNet上预训练过的VGG16去掉顶层1000分类 base_model VGG16( weightsimagenet, include_topFalse, input_shape(224, 224, 3) ) # 冻结前11层也就是block1到block3的全部卷积层 for layer in base_model.layers[:11]: layer.trainable False # 自定义分类头 x base_model.output x GlobalAveragePooling2D()(x) # 7x7x512 - 512维特征向量 x Dense(1024, activationrelu)(x) x Dropout(0.5)(x) # 防止全连接层过拟合 predictions Dense(7, activationsoftmax)(x) model Model(inputsbase_model.input, outputspredictions) # 第一阶段用SGD较大学习率只训练分类头 model.compile( optimizerSGD(learning_rate0.001, momentum0.9), losscategorical_crossentropy, metrics[accuracy] ) model.summary()几个参数需要解释。include_topFalse是关键它把VGG16最后一个1000类分类器和第一个4096维全连接层一起去掉避免ImageNet类别语义干扰表情分类。冻结前11层是因为block1到block3学的是通用边缘纹理特征这些特征在人脸和ImageNet物体之间高度共享真正需要针对表情调整的是block4和block5的语义组合层。Dropout(0.5)放在1024维Dense之后它是全连接层防过拟合的主力。FER2013只有两万八千张训练图而VGG16提取的特征维度是512再升到1024如果不加Dropout训练到第15轮左右验证准确率就会开始下降典型过拟合信号。3.4 训练回调与断点续训别让一晚上白跑模型训练不能只写一个fit就完事。VGG16在单卡GPU上跑一个epoch大约要两到三分钟30个epoch就是两个小时起步任何一次中断都意味着重来。我习惯把三类回调写全模型检查点负责保存最优权重早停负责在验证集不再提升时主动终止学习率衰减负责在接近收敛时缩小步长。from tensorflow.keras.callbacks import (ModelCheckpoint, EarlyStopping, ReduceLROnPlateau) callbacks [ ModelCheckpoint( vgg16_fer2013_best.h5, monitorval_accuracy, save_best_onlyTrue, # 只保存验证集最优的权重 verbose1 ), EarlyStopping( monitorval_loss, patience10, # 连续10轮不改善就停止 restore_best_weightsTrue ), ReduceLROnPlateau( monitorval_loss, factor0.2, # 学习率每次乘0.2 patience4, min_lr1e-6 ) ] # 第一阶段固定全部卷积层只训练分类头5个epoch for layer in base_model.layers: layer.trainable False model.compile( optimizerSGD(learning_rate0.001, momentum0.9), losscategorical_crossentropy, metrics[accuracy] ) history_stage1 model.fit( train_dataset, validation_dataval_dataset, epochs5, callbackscallbacks ) # 第二阶段解冻block4和block5学习率降为1e-4 for layer in base_model.layers[:11]: layer.trainable False for layer in base_model.layers[11:]: layer.trainable True model.compile( optimizerSGD(learning_rate0.0001, momentum0.9), losscategorical_crossentropy, metrics[accuracy] ) history_stage2 model.fit( train_dataset, validation_dataval_dataset, epochs30, callbackscallbacks )阶段切换时有一个特别容易忽略的操作修改trainable属性之后必须重新compile因为Keras只在compile时读取各层的可训练状态。我见过不少人在解冻后忘了重新compile结果第二阶段跑了好几个epoch权重一直没更新白白浪费实验时间。EarlyStopping的patience设10是比较宽松的值适合表情识别这种验证曲线波动大、容易在局部极小值停留的任务。ReduceLROnPlateau的factor设0.2意味着每触发一次学习率就降到原来的五分之一最多能从1e-4降到1e-6这是给模型一个重新冲刺的机会。整个训练流程跑完FER2013的验证准确率通常落在71%到74%之间这个区间受随机种子影响会有1%到2%的浮动。4. 避坑手册VGG16表情训练常见的5个坑4.1 坑一验证集卡在70%上不去训练集准确率一路爬到90%验证集却卡在70%附近怎么调都不动。这是新手遇得最多的现象不是模型坏了大概率是两个原因叠加一是分类头太薄1024维Dense加Dropout的表达能力撑不住VGG16深层特征二是block4和block5没有参与微调语义特征仍然停留在ImageNet物体的分布里。解决思路分两步。先确认是不是分类头容量不够把1024维提到2048维Dropout从0.5降到0.3试一轮如果提升不明显再检查冻结范围把冻结层数从11层减少到8层让block3也参与微调。需要留意的是FER2013这个数据集本身的标注噪声很大70%到72%就是它的精度天花板继续堆参数只会在验证集上抖动得更厉害。4.2 坑二损失下降但准确率纹丝不动训练损失从1.9降到1.2看起来一切正常但准确率一直停在40%左右整个训练过程毫无意义。看一眼混淆矩阵会发现模型把几乎所有样本都预测成了“中性”或者“开心”这两个多数类。原因在于FER2013的类别分布并不均匀“开心”和“中性”各占两成多“厌恶”只有不到5%模型发现全预测成多数类就能把损失压得很低。解决办法是给少数类加权。用sklearn提供的compute_class_weight直接算好每个类别的权重再传给fit的class_weight参数。同时把监控指标从accuracy换成f1_macro因为accuracy在类别不均衡时完全不能反映真实性能。算权重的代码很简单from sklearn.utils.class_weight import compute_class_weight # y_train是原始整数标签不是one-hot class_weights compute_class_weight( class_weightbalanced, classesnp.unique(y_train), yy_train ) class_weight_dict dict(enumerate(class_weights))注意class_weight_dict的键必须是整数类别编号和标签值严格对应。模型输入标签是one-hot编码也不影响因为权重是按索引取的。加了类别权重之后“厌恶”和“恐惧”的召回率通常能提升8到10个百分点整体准确率反而会有1%左右的下降但这是健康的下降因为模型不再偷懒了。4.3 坑三灰度图复制三通道后效果变差标题写了VGG16人脸表情识别数据却是灰度图这是绕不开的矛盾。常见的错误做法是直接把48×48灰度图复制成三个通道再resize到224×224然后扔给VGG16。这么做的结果通常是验证准确率比随机初始化还低VGG16的预训练权重完全发挥不出来。原因在于ImageNet预训练权重期望的输入是均值约[123.68, 116.78, 103.94]的彩色图而灰度复制图的三个通道均值完全一样分布完全偏离了预训练时的统计特征。我的解决方案是保留灰度信息但要自己重新初始化第一个卷积层。具体做法是加载预训练权重后把base_model的输入层改成单通道第一层卷积层的权重用随机初始化覆盖其余卷积层保持预训练权重from tensorflow.keras.layers import Input, Conv2D from tensorflow.keras.models import Model # 用单通道输入重新定义VGG16的输入部分 input_tensor Input(shape(224, 224, 1)) conv1 Conv2D(64, (3, 3), paddingsame, activationrelu)(input_tensor) # 这里手动把原VGG16第一个卷积块的后续层接上 rest base_model.layers[1] # 跳过原来的输入层 # 实际使用中推荐直接改input_shape(224,224,1)加载 # 加载后手动为第一层赋值与灰度数据匹配的权重初始化实际落地时更简洁的方案是先用灰度图做标准归一化到[-1,1]区间再去加载VGG16这种情况下要接受第一层卷积和预训练权重之间有偏差。更稳妥的做法是用OpenCV的COLOR_GRAY2BGR转换后归一化这比复制通道效果好因为OpenCV会做一次线性映射把灰度值压缩后叠加到BGR空间分布更接近自然图。4.4 坑四显存不够训练时OOM中断VGG16是个显存大户。224×224输入、batch_size设32在8GB显存的GPU上跑起来会很吃紧batch_size设64基本必炸。现象是训练到第三个epoch左右突然报OOM错误整个进程退出之前的进度全丢。解决思路有三个按优先级排列。第一把batch_size降到8或16之前保存的模型不受影响可以从断点继续训练第二开启混合精度训练把计算精度从float32降到float16显存占用直接减半第三用梯度累积模拟大batch的效果代码实现也很简单# 模拟batch_size32的效果实际每次只前向16张图 accumulation_steps 2 optimizer SGD(learning_rate0.001, momentum0.9) for step, (x_batch, y_batch) in enumerate(train_dataset): with tf.GradientTape() as tape: preds model(x_batch, trainingTrue) loss loss_fn(y_batch, preds) grads tape.gradient(loss, model.trainable_variables) # 先累积梯度每accumulation_steps步再应用一次 if step % accumulation_steps 0: optimizer.apply_gradients(zip(grads, model.trainable_variables)) model.reset_metrics()显存不够时我劝你直接换思路不要硬调网络结构。VGG16的参数量摆在那里剪枝也好、换轻量网络也好对表情识别这种小任务收益不大而且引入新的不确定因素。与其这样不如直接在训练环境上解决租一台带16GB显存GPU的服务器跑深度学习一晚上能出结果的事没必要让本机卡死。4.5 坑五预训练权重加载报错或预测结果明显异常现象分两种一种是加载weightsimagenet时直接报错提示权重文件无法访问或校验失败另一种是权重能加载但预测结果表明模型把大多数样本都判成同一类。这两个问题我都碰到过都和权重文件的来源与匹配方式有关。前者通常是网络环境导致的下载失败或者本机缓存里残留了不完整的权重文件。解决方法是手动下载官方权重文件放在~/.keras/models目录下文件名要和Keras应用模块里定义的哈希一致。每次加载前检查一下文件大小Keras官方vgg16_weights_tf_dim_ordering_tf_kernels_notop.h5大约是57MB文件大小对得上再继续。后者更隐蔽。VGG16在tf.keras.applications和旧版keras里的权重键名不同如果混用版本权重加载时部分层会保持随机初始化。判断方法是打印base_model的summary检查卷积层的可训练参数数量是否异常巨大。如果每个block的参数都是满量说明权重没加载进去。统一用tf.keras.applications加载不要混用keras独立包。5. 从准确率到可用性混淆矩阵、类激活图与时序平滑5.1 用混淆矩阵定位真实瓶颈验证准确率只是一个数字真正能指导下一步调参的是混淆矩阵。七类表情里“恐惧”和“惊讶”互相错分“厌恶”和“生气”互相错分这是FER2013上公认的难点但具体错到什么程度、哪一类拖后腿最严重只有看矩阵才知道from sklearn.metrics import confusion_matrix import seaborn as sns import matplotlib.pyplot as plt y_pred model.predict(test_dataset) y_pred_labels np.argmax(y_pred, axis1) y_true_labels np.argmax(y_test, axis1) cm confusion_matrix(y_true_labels, y_pred_labels) sns.heatmap(cm, annotTrue, fmtd, cmapBlues) plt.xlabel(Predicted) plt.ylabel(True) plt.title(FER2013 Confusion Matrix) plt.show()如果“厌恶”类几乎全被错分到“生气”说明分类头没有学到厌恶特有的嘴部单侧抬升模式优先补充厌恶类样本比调整网络结构更有效。如果“中性”类召回率高但精确率低说明模型把各种过渡表情都归进了中性这种情况要考虑在损失函数里加label smoothing让模型对低置信度样本不那么自信。5.2 用Grad-CAM确认模型在看什么准确率够高不代表模型学到了合理的人脸特征。我的习惯是每次训练完都跑一遍Grad-CAM看模型在做判断时到底关注脸的哪个区域。正常的“开心”表情热力图应该集中在嘴边和脸颊“恐惧”应该集中在眼睛和眉毛中部。如果热力图落在头发、背景或者下巴边缘说明训练数据里存在系统性偏差。Grad-CAM在Keras里的实现不复杂核心是取最后一个卷积层的输出特征图用类别权重的梯度做加权平均。实践里可以直接用tf-explain或者自己写二十行代码实现。看到模型确实在看眼部、嘴部这些关键区域我才敢把模型放进真实场景否则它学到的可能是数据集里的背景纹理换个环境就翻车。5.3 单帧预测的时序平滑技巧人脸表情识别在视频场景下还有一个单帧模型解决不了的问题抖动。连续帧的预测结果经常在“中性”和“伤心”之间来回跳原因不是模型不重要而是相邻帧表情本来就模糊。我在部署时习惯加一个指数移动平均做后处理效果立竿见影# 时序平滑alpha越大对新帧越敏感 alpha 0.6 smoothed_probs None for frame_pred in video_predictions: if smoothed_probs is None: smoothed_probs frame_pred else: smoothed_probs alpha * smoothed_probs (1 - alpha) * frame_pred final_label np.argmax(smoothed_probs)这样处理之后单帧偶尔的误判会在连续几帧内被平滑掉整体决策的稳定性提升明显而延迟只增加一帧的时间。阈值设0.6比较平衡如果场景对响应速度要求高就调到0.4对稳定性要求高就调到0.8。我现在做每个表情识别项目都会把混淆矩阵和Grad-CAM的可视化脚本写进推理流程里每次调参后先看这两张图再决定下一步动作。模型调参经常被当成玄学其实把错题本摊开规律就清楚了。希望帮到你。本文还有配套的精品资源点击获取
返回列表