简介:面向Python课程期末大作业的完整人脸表情识别项目,基于ResNet网络实现,包含可直接运行的源码、配套图像数据集与详细说明文档,适合计算机相关专业学生作为课程设计、毕业设计或自学项目参考。项目经过本地编译与严格调试,评审分达到95分以上,难度适中,助教审定,能够满足学习与二次开发需求。压缩包共103个文件、54.11MB,涵盖Python源码与编译产物(.py/.pyc)、预训练模型权重(.hdf5)、人脸表情图片样本(.png/.jpg/.jpeg)、项目配置与说明文件(.xml/.iml/.md/.txt)以及操作演示视频(.mp4)等,目录组织清晰。已有135人学习下载。其中除了完整代码和数据外,还提供了可复用的ResNet/Xception等模型权重,以及happy、surprised等示例图片和演示GIF,便于直观理解表情分类效果;说明文档能帮助快速梳理项目结构与参数调优思路,是期末作业或入门人脸识别方向的实用素材。
1. 期末大作业选ResNet做人脸表情识别:先想清楚这三件事
“python期末大作业基于ResNet的人脸表情识别(源码+数据集+说明文档).zip”这个标题拆开看就是一个经典组合:用ResNet网络做表情分类,配套现成的源码、数据集和说明文档。很多人的期末大作业死在“代码能跑但答辩一问就懵”——数据怎么加载的、resnet的残差结构到底解决了什么问题、表情数据集有哪些坑,光跑通远远不够。这篇按任务定义、源码拆解、调参、避坑、答辩加分的顺序,把整套方案从头讲到能讲清楚,适合要交作业的学生,也适合第一次接触torchvision和预训练模型的入门者。读完你能复现训练,也能把每个参数为什么这么设说明白。
2. 人脸表情识别在做什么:任务定义、ResNet选型与数据集先验
2.1 表情识别本质是图像分类:先定你的标签空间
拿到这个题目,第一件事不是打开代码,而是确认你的标签空间。人脸表情识别在公开数据集里几乎是固定的套路:CK+、FER2013、RAF-DB这些常用数据集,绝大多数按七类标注——愤怒、厌恶、恐惧、开心、悲伤、惊讶、中性;少部分会把“轻蔑”单独拆出来变成八类。不同数据集的表情类别不完全一致,这直接决定你最后的全连接层输出维度是7还是8。
我见过不少翻车案例:说明文档里写着七类分类,代码里num_classes=7,但数据集目录里实际躺着八个文件夹,训练时ImageFolder自动按文件夹名排序生成标签,最后一个类别永远没有样本参与训练,val_acc却显示正常,因为预测时那个类别从来没出现过。所以动手前先把标签分布统计一遍,这是最便宜的事故预防。
import os from collections import Counter data_root = "./data/train" # 以你解压后的实际路径为准 label_count = Counter() for cls_name in os.listdir(data_root): cls_path = os.path.join(data_root, cls_name) if os.path.isdir(cls_path): n = len(os.listdir(cls_path)) label_count[cls_name] = n for cls, n in label_count.most_common(): print(f"{cls}: {n}")这个脚本的逻辑很简单:列出训练目录下每个文件夹,统计每个类别的图片数量。ImageFolder在PyTorch里就是按文件夹名排序后生成从0开始的整数标签,所以这里的cls_name排序和训练时的标签索引是严格对应的。如果发现某个类别图片数远少于其他类,后面调参时必须加权损失或者做采样,否则模型会把稀有表情全部忽略。
2.2 ResNet的残差结构如何解决深层网络退化
为什么期末作业普遍选ResNet而不是VGG或者更早的AlexNet?因为ResNet解决了“越深越差”的反直觉问题。普通卷积网络堆到十几层以上时,训练误差不降反升,这被称为网络退化,注意这不是过拟合,过拟合是训练误差低但验证误差高,退化是训练误差本身就下不去。
原因在于深层网络做反向传播时,梯度要逐层连乘回去,连乘次数越多,梯度越容易消失或爆炸。ResNet的核心改动是引入残差块:让某一层的输出从F(x)变成F(x) + x,其中x是输入本身。这样梯度在反向传播时多了一条“捷径”,可以直接穿过若干层传到前面,相当于给梯度修了一条高速公路。
这条身份映射还有一个工程价值:就算F(x)这一支学不到东西,网络至少能退化成“直接复制输入”,所以深层网络的表现不会比浅层更差。期末答辩被问到“为什么选ResNet”,你抓住“解决退化、梯度能直传、加深网络不掉点”这三点就够用了,不要背教科书里的定义。
2.3 为什么期末场景选ResNet-18而不是ResNet-50或VGG
同一个ResNet家族里也分18层、34层、50层、101层,期末作业最常见的两个选择是ResNet-18和ResNet-50。选型的核心约束是数据量、算力和训练时间,不是“层数越多越高级”。
| 模型 | 参数量 | 单张224x224前向显存占用 | 典型训练耗时(小数据集CPU) | 期末答辩观感 |
|---|---|---|---|---|
| ResNet-18 | 约11M | 较低 | 快 | 结构清楚,好讲 |
| ResNet-34 | 约21M | 中等 | 中等 | 略冷门 |
| ResNet-50 | 约25M | 较高 | 慢 | 容易被追问“为什么用这么重的模型” |
| VGG16 | 约138M | 很高 | 很慢 | 参数巨大,不好解释 |
表情数据集本身图片量级也就几千到几万张,而且普遍是小分辨率灰度图。ResNet-18在数据量不大时已经能把七类表情训到不错的水平,训练速度快,显存占用低,实体机没有独立显卡也能靠CPU熬出来。ResNet-50的优势在ImageNet这种千万级大图数据集上才明显,在小数据集上更容易过拟合。
如果源码里给的是ResNet-50也不用慌,常见做法是加载预训练权重做迁移学习,靠ImageNet上学到的通用特征兜底。但你要想清楚一个问题:表情识别输入经常是48x48的灰度图像,和ImageNet的224x224三通道分布差距很大,迁移学习带来的提升没有想象中那么玄学,有时甚至不如从零训练。这个我放在第5章细讲。
2.4 数据集形态:FER2013灰度单通道与自制目录
拿到zip先看数据集是什么形态,这决定你的数据加载代码。常见有两种形态。一种是FER2013的CSV格式,每一行是emotion,pixel,Usage,pixel列里是48x48=2304个灰度值;另一种是文件夹形态,train/anger/xxx.jpg这种结构,也可能是data/生气/xxx.jpg中文目录。两种加载方式完全不同。
CSV格式要用pandas或csv读取,再把pixel字符串拆成2304个整数转成(48,48)的单通道张量;文件夹形态直接用torchvision.datasets.ImageFolder最省事。问题在于FER2013原版是灰度单通道,而ImageFolder默认按三通道读图,灰度图会被复制成三个通道,虽然能跑但会浪费计算量,而且如果你用了ImageNet预训练权重,模型期望的输入是三通道,灰度单通道会因为维度不匹配直接报错。
from PIL import Image import numpy as np img_path = "./data/train/anger/000001.jpg" img = Image.open(img_path) print(f"mode={img.mode}, size={img.size}") arr = np.array(img) print(f"ndim={arr.ndim}, shape={arr.shape}")这段代码检查单张图片的通道数。mode如果是L说明是灰度单通道,RGB是三通道。arr.ndim为2表示灰度,为3表示彩色。这一步的结论直接决定transforms里要不要加transforms.Grayscale(num_output_channels=3),以及要不要在model.conv1上把输入通道从3改成1。很多人的源码跑不通,卡点根本不是网络结构,而是数据通道数没对齐,属于基础但非常隐蔽的坑。
3. 把源码跑起来:环境、目录与train.py最小可复现
3.1 环境准备:Python版本、PyTorch与CUDA匹配
期末大作业最怕的环境问题是“在自己电脑上跑通,到答辩机器上装环境装了两小时”。先说结论:Python装3.8到3.10这一段最稳,别为了尝鲜装3.13。很多开源源码和依赖库还没跟进新Python版本,装包时容易碰一鼻子灰。PyTorch选2.x系列的稳定版即可,CPU版本也能训练,只是慢。
CUDA这块容易踩坑。安装GPU版PyTorch前,先在命令行跑一句nvidia-smi看显卡驱动支持的CUDA版本,然后去PyTorch官网选对应版本的安装命令。常见错误是显卡驱动是CUDA 11.x,却装了要求CUDA 12.x的PyTorch,运行时提示找不到libcudart或者torch.cuda.is_available()返回False。
conda create -n emo_resnet python=3.9 conda activate emo_resnet pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install numpy pandas matplotlib tqdm这里用conda新建了Python 3.9的环境,隔离系统Python,避免把全局环境搞乱。第二行激活环境后,后续的pip install都装在这个独立环境里。PyTorch的安装地址用了cu118,意思是CUDA 11.8版本,如果你的驱动支持CUDA 12.x,把cu118换成cu121之类的对应标识,拿不准时先跑nvidia-smi看一眼右上角的版本号。如果只想CPU训练,直接pip install torch torchvision即可,但训练时间会成倍拉长。
装完验证一下:在Python里执行import torch; print(torch.cuda.is_available())。输出True才说明GPU可用,输出False就老老实实CPU跑,或回去检查驱动和PyTorch版本匹配问题。
3.2 目录结构:这个压缩包里每个部分负责什么
绝大多数这类项目的目录组织方式是固定套路,解压后大致长这样,具体名称以说明文档为准:
| 目录/文件 | 职责 |
|---|---|
dataset/或data/ | 存放图片数据集,内部按类别分子目录 |
models/或model.py | ResNet结构定义,也可能是从torchvision引入 |
train.py | 训练主脚本,包含数据加载、模型初始化、训练循环、保存权重 |
predict.py或test.py | 加载训练好的权重,对单张图片做推理 |
requirements.txt | 依赖包列表 |
说明文档.docx或README.md | 项目说明、怎么跑、参数含义 |
拿到压缩包第一件事不是直接跑train.py,而是打开说明文档,确认三个信息:数据集放哪个目录、训练入口是哪个脚本、依赖包版本有没有特别要求。缺任何一个,盲跑大概率在import阶段就报错。
训练产物一般是best_model.pth或model_final.pth这个格式的权重文件,里面存的是state_dict,也就是模型的参数张量。predict脚本加载它时需要先构造一个和训练时完全相同的模型结构,再执行load_state_dict,否则会报key不匹配的错误。
3.3 数据加载:直接改造torchvision内置Dataset
这类小项目最常见的实现方式是用torchvision.datasets.ImageFolder,它对文件夹结构的数据集几乎是零成本接入。整个数据加载链路里最容易被忽视的是transforms的搭配:表情识别是细粒度分类,图片分辨率小,直接缩放到224x224会丢失大量纹理细节;但ResNet结构又要输入224x224,所以一套合理的数据预处理是:随机裁剪、水平翻转、归一化。
from torchvision import datasets, transforms from torch.utils.data import DataLoader train_transform = transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomCrop(224), transforms.RandomHorizontalFlip(p=0.5), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) train_dataset = datasets.ImageFolder(root="./data/train", transform=train_transform) train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=0, pin_memory=True) print("类别:", train_dataset.classes) print("样本数:", len(train_dataset))Resize((256, 256))先把图放大到256,RandomCrop(224)再随机裁224,等于做了一个简单的数据增强,让模型每次看到的都是同一张图的不同区域,有一定的抗过拟合作用。RandomHorizontalFlip对表情识别是安全的,因为左右翻转后“开心”还是“开心”,“愤怒”还是“愤怒”,标签不会变。归一化的均值标准差直接沿用ImageNet的数值,这在预训练模型上是标准做法。
num_workers=0在Windows上不会出问题,设大于0在某些机器上会因为多进程和主程序交互报错,这是Windows上PyTorch的老毛病,慢就慢一点,先保证不崩。
3.4 训练主脚本:最小可用的train.py怎么组织
一个能交作业的train.py,结构上必须包含五个部分:数据加载、模型初始化、损失函数与优化器、训练循环、模型保存。老师答辩时一定会问“你的训练主流程是什么”,这个脚本就是用来回答这个问题的。
先看模型初始化这一块的写法,期末项目最常用的路径有两种:一种是从torchvision加载预训练ResNet-18,把最后一层全连接改成7分类;另一种是自己实现一个残差块再拼成ResNet,完全从零训练。两种都合理,前者快,后者更容易展示你对网络结构的理解。
import torch import torch.nn as nn from torchvision import models def build_model(num_classes=7, use_pretrained=True): model = models.resnet18(weights='DEFAULT' if use_pretrained else None) in_features = model.fc.in_features model.fc = nn.Linear(in_features, num_classes) return model model = build_model(num_classes=len(train_dataset.classes)) criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-4)这段代码里weights='DEFAULT'是torchvision新版推荐写法,它会加载ImageNet预训练权重。如果你的torchvision版本较老,也可以写成pretrained=True,二者对应新旧两代API。model.fc.in_features先取出原全连接层的输入维度,再替换成输出为num_classes的新全连接层,这样前面的卷积层参数全部保留,只需要从头训最后的分类头。
损失函数用CrossEntropyLoss,它内部已经包含了softmax,所以模型输出层不需要额外加softmax,直接输出7个类别的logits即可。优化器用Adam还是SGD都有道理:SGD+L2正则训得更稳,但需要手动调学习率;Adam收敛快,对小数据集更友好。期末场景建议用Adam,学习率初始值给1e-4,别上来就1e-3,表情数据集小,学习率太大容易震荡。
训练循环部分,每个epoch结束时打一次平均损失和验证集准确率,并保存验证集上最好的模型。保存的时候别只存model.state_dict(),顺手把epoch和best_acc也存进去,答辩时老师问“你这个模型训到第几个epoch最好”你立刻能答上来。
4. 参数怎么调:五组必调参数与三类监控指标
4.1 batch_size与学习率要一起动
很多人调参只动学习率,忽略了batch_size。这两个参数是绑定的:batch_size越大,梯度越平滑,噪声越小,这时候可以适当加大学习率;batch_size越小,每个batch的梯度越不稳定,学习率必须相应调小,否则loss会像心电图一样上下跳。
期末项目的机器配置差异很大,常见做法是先确定batch_size,再按经验给学习率。batch_size给16到64这段区间比较合适,ResNet-18在224x224输入下,用GTX 1060 6GB显存,batch_size设为32能稳定跑;把输入缩到224后还爆显存,就先降到16,再不行就检查是不是数据加载时把图片尺寸弄错了。
一个实用的经验:换batch_size时,学习率按比例缩放。batch从32改成64,学习率从1e-4上调到2e-4;batch从32降到16,学习率下调到5e-5。这不是什么严格数学推导,是工程上常用的近似,答辩被问到“为什么不改batch只改lr”时,这就是你的理由。
4.2 学习率衰减策略:StepLR还是CosineAnnealing
训练初期用大学习率快速收敛,后期用小学习率精细调整,这比全程固定学习率效果好得多。常见的两种调度器在期末项目里都常出现,区别就在衰减节奏。
StepLR是按固定步长阶梯式衰减,比如每隔15个epoch学习率乘以0.1,适合你能大概判断模型在哪个epoch收敛的情况。CosineAnnealing则让学习率按余弦曲线从初始值平滑降到接近0,中途会经历“学习率低—调不动—又回升”的循环,对小数据集、训练epoch不多时表现不错,而且不需要你去猜衰减节点,属于设置一次就不用管的类型。
from torch.optim.lr_scheduler import StepLR, CosineAnnealingLR # 方案A:每15个epoch学习率降为原来的0.1 scheduler_a = StepLR(optimizer, step_size=15, gamma=0.1) # 方案B:50个epoch内从初始lr余弦衰减到1e-6 scheduler_b = CosineAnnealingLR(optimizer, T_max=50, eta_min=1e-6)两种方案在使用上有一个显著差异:StepLR要配合训练代码里的if epoch % step_size == 0这种判断去打日志,方便你观察每次衰减后loss的变化;CosineAnnealing则不用盯衰减点,但训练结束时学习率已经很低,如果还想继续追加训练,最好重建优化器,否则学习率回升的逻辑会让人迷惑。期末项目总epoch数一般在30到80之间,两种都能用,我更倾向StepLR,因为答辩时能把“在第15个epoch学习率降了10倍,loss从0.8掉到0.5”讲成一个具体的故事。
4.3 类别不平衡的处理:加权损失与采样器
表情数据集的类别分布天然不平衡,FER2013里“开心”和“中性”样本多,“厌恶”“恐惧”样本少。如果直接用普通交叉熵,模型会倾向于把所有样本预测成多数类,因为只要这样做,整体loss就能降低。两个常用解法是加权损失和加权采样。
加权损失的做法是给每个类别一个权重,样本少的类别算loss时乘一个更大的系数,迫使模型重视这些类。权重的计算很简单:总样本数除以每个类别的样本数,再归一化。PyTorch的CrossEntropyLoss直接支持weight参数,把权重张量传进去就行。
import torch from torch.utils.data.sampler import WeightedRandomSampler # 计算每个类别的样本数,然后取倒数作为权重 class_counts = [train_dataset.targets.count(i) for i in range(len(train_dataset.classes))] total = sum(class_counts) class_weights = [total / c for c in class_counts] criterion = nn.CrossEntropyLoss(weight=torch.tensor(class_weights, dtype=torch.float)) # 或者用加权采样器,让每个batch里都能抽到稀有类 sample_weights = [class_weights[target] for target in train_dataset.targets] sampler = WeightedRandomSampler(sample_weights, num_samples=len(sample_weights), replacement=True) train_loader = DataLoader(train_dataset, batch_size=32, sampler=sampler)两种方式可以叠加用,也可以只用一种。加权损失的优势是改动小,一行参数搞定;加权采样器的优势是每个batch里稀有类的出现频率直接提高,模型能真实看到这些样本。如果题目数据集本身均匀,这部分可以跳过;如果答辩老师发现你处理过类别不平衡,这是明显的加分项。
4.4 训练日志怎么判读:loss、acc与过拟合的三条判断线
训练跑起来之后,判断模型状态主要看三条曲线:训练loss、训练acc、验证acc。很多人只看验证acc,这是不够的。
| 现象 | 判断 | 处理 |
|---|---|---|
| 训练loss不降,验证acc在50%左右晃 | 模型没学会,可能是学习率太大或数据加载出错 | 先降学习率看有没有变化 |
| 训练loss降了,训练acc很高,验证acc低 | 过拟合,模型在背训练集 | 加数据增强、加Dropout、减小模型容量 |
| 训练loss和验证acc都在降 | 正常训练,保持当前参数 | 不要动,让它跑完 |
| 验证acc先升后降,训练acc还在升 | 过拟合开始,最佳模型在前面 | 加载之前保存的best_model |
这里有一个常见的误操作:训练完直接拿最后一轮的权重去测试。正确做法是每个epoch记录验证acc,只保留验证acc最高的那一次权重,这叫“early stopping,但用权重回溯代替中断训练”。很多源码里已经写了“保存最优模型”的逻辑,你要确保用的是best_model.pth而不是last_model.pth,别在最后一步把结果搞砸了。
5. 避坑指南:跑这个项目最常见的5个翻车现场
5.1 CUDA out of memory
现象:训练刚开始或跑到第二个epoch,弹出一大段红色报错,核心是CUDA out of memory。
原因:最常见的不是显存不够,而是输入图像尺寸太大或batch_size太大。很多人用ImageNet预训练模型时忘了改输入尺寸,把500x500的原图直接塞给ResNet,前向传播到第3层就爆了。
解决:先把transforms.Resize固定到224或256,再确认batch_size不超过32,还爆就降到16。如果这些都没问题,检查是不是num_workers开太多,数据加载子进程也会占显存。最后的手段是torch.cuda.empty_cache()加在训练循环开头,但治标不治本。
5.2 loss完全不下降,一直在3.0附近
现象:训练了好几轮,loss稳定在3.0附近不动,验证acc也在类别数分之一附近徘徊,比如7分类就卡在14%左右。
原因:3.0这个数值是交叉熵在7分类下的随机猜测起点(ln(7)约等于1.95,但多分类下会更高一点),loss不降通常是两个原因:一是学习率太大或太小,太大loss会震荡,太小loss几乎不动;二是标签和数据没对齐——比如你用了别人的预训练权重,但权重是在不同标签顺序上训的。
解决:先把学习率分别试1e-3、1e-4、1e-5,看哪个区间loss能松动。如果都不动,找一张训练集图片,单独跑一次模型前向,打印预测结果和真实标签,人工核对是不是“愤怒”的图预测成了“中性”且损失巨大。这类问题90%靠打印中间结果能定位到。
5.3 验证acc卡在50%上下,怎么调都上不去
现象:训练loss正常下降,但验证准确率始终在50%左右波动,超过不了60%。
原因:通道数处理错误是隐藏元凶。FER2013是灰度图,被ImageFolder默认按三通道读了之后,模型把同一份灰度数据复制三份当作RGB输入,虽然能跑通,但信息并没有增加,而且用ImageNet预训练权重时,模型统计学到的颜色分布和灰度图差很远。
解决:在transforms里加transforms.Grayscale(num_output_channels=3),把灰度图显式转成三通道但保持灰度内容;或者在模型第一层把卷积输入通道改成1,舍弃预训练权重从零训练。前者能继续用预训练权重,稳定度高,推荐先试这个。
5.4 测试时模型几乎把所有人都预测成“中性”
现象:训练时acc还行,但拿单张图片去测试,无论什么表情,预测结果都是“中性”或者“开心”。
原因:类别不平衡导致模型学习到了捷径——只要预测成多数类就能保证整体loss低。表情数据集里“中性”和“开心”样本最多,模型逐渐学会偷懒。
解决:按4.3节的加权损失处理。还有一个容易忽略的点:测试时的transforms必须和训练时一致,尤其Normalize的均值和标准差,训练时做了归一化,测试时没做,模型看到的分布完全不同,预测就会偏向某一个类。检查predict.py里的transforms是否复制了训练时的完整流程。
5.5 加载预训练权重时提示fc维度不匹配
现象:运行model.load_state_dict(checkpoint)时报错,提示fc.weight size mismatch,或者用旧版API加载新版torchvision模型时直接崩溃。
原因:torchvision版本升级后,预训练权重的存储格式和模型定义有变化。旧代码里写pretrained=True在新版本中已弃用,而新写法weights='DEFAULT'加载的权重,需要模型结构完全一致才能对齐。
解决:最省事的做法是不要手动改模型的fc层后再去load整个权重,而是先加载预训练权重,再替换最后一层,替换后的fc参数不做加载,从零初始化。
import torch from torchvision import models from torchvision.models import ResNet18_Weights # 先加载预训练模型,保留原始fc层的结构 model = models.resnet18(weights=ResNet18_Weights.IMAGENET1K_V1) # 再替换fc层,fc层的参数不包含在预训练权重里 num_classes = 7 model.fc = torch.nn.Linear(model.fc.in_features, num_classes) # 保存权重时也把state_dict结构对应的信息保留 torch.save({"model_state_dict": model.state_dict(), "num_classes": num_classes}, "best_model.pth")这里的关键顺序是:先加载完整预训练权重,再替换fc层。如果反过来,模型结构里fc输出是7,加载ImageNet权重时fc输出是1000,必然报维度不匹配。保存时把num_classes存进字典,predict脚本加载权重时先用这个值构造模型,彻底避免维度对不上。
6. 答辩加分的三个进阶技巧:可视化、困难样本与混淆矩阵
6.1 做一个预测可视化脚本,别只输出类别号
交作业时如果只输出“这个人是开心”,说服力很弱。改进版是在推理脚本里画出图片、真实标签、预测标签、每个类别的置信度概率,让老师一眼看到模型不是瞎猜。用一个matplotlib的柱状图展示概率分布,代码量不大,但展示效果很强。
6.2 针对困难样本做增强,而不是盲目加数据
如果验证集里“恐惧”和“厌恶”老是混淆,单独为这两类增加亮度抖动、随机旋转和遮挡增强,会比全局增强更有效。判断困难样本的方法是打印验证集里被预测错的样本,统计哪两类最容易互相误判,再针对性补数据。这个思路叫困难样本挖掘,说出来比“我用了数据增强”高级很多。
6.3 验证时输出混淆矩阵,量化每个类别的表现
准确率只给一个数字,混淆矩阵能展示模型具体在哪两类上犯错。用sklearn.metrics.confusion_matrix跑一次验证集,把结果打印成表格,答辩时指着“愤怒被误判成中性”那一格说明后续改进方向,这个闭环讲完,作业的完成度就有了。
我自己的习惯是:训练完先打印混淆矩阵,再选三个预测错的样本做可视化,最后才去调参数。这个顺序帮我省了很多重复训练的时间——因为有些“问题”根本不在模型里,而在标签和数据预处理里。希望这篇能帮你把期末项目从“跑通”推到“讲明白”,少走我踩过的那些弯路。
本文还有配套的精品资源,点击获取