尧图精选

人工智能导论大作业实战:看图说话+微表情识别全流程解析

🕒 发布时间:2026/10/1 3:45:23 📁 来源:尧图网络
简介这是一份面向人工智能导论课程实验的配套资源包主要实现“看图说话”与“微表情识别”两个任务前者利用深度学习模型为图片生成文字描述后者在人脸检测基础上进一步判断情绪。资源包含TensorFlow/Keras编写的Python源码与完整的课程论文报告适合人工智能初学者、高校学生完成课程设计或复现基础视觉模型时参考。压缩包共9个文件其中包括3个Jupyter Notebook覆盖数据预处理、模型构建与训练评估、2个Markdown说明文档、1个Word课程论文报告、1个用于人脸检测的haarcascade XML分类器及License文件等整体仅5.78MB结构清晰易查找。目前已有338人学习下载可作为相关实验的路线参考。读者可对照代码与报告快速理解图像描述和人脸表情识别的基本流程也能直接复用人脸检测XML与模型代码节省调试和撰写报告的时间。1. 人工智能导论大作业为什么“看图说话微表情识别”是性价比最高的选题如果你正在上《人工智能导论》这门课期末大作业通常有两种做法交一篇3000字的综述或者交一个能跑通的demo。前者稳妥但不出彩后者一旦通了答辩时能直接现场演示老师基本不会追问原理。我今天要拆的这个选题——看图说话Image Captioning配微表情识别Micro-Expression Recognition——正是很多同学选过的“人工智能大作业”经典组合一份代码里同时覆盖了CNN、RNN、注意力机制、多分类、时序特征五个考点写完就是一份拿得出手的“人工智能项目实战”案例。这个方向的核心价值在于它不需要多模态大模型也不需要几十G的预训练权重。用一张消费级显卡甚至CPU都能把训练流程跑完模型原理也完全落在导论课本范围内。看图说话解决的是“给定一张图输出一句自然语言描述”微表情识别解决的是“给定一段人脸视频或一张人脸图判断表情类别”。两件事一个生成一个判别正好凑成一份完整的技术闭环。适合的人群很明确《人工智能导论》课程大作业、人工智能相关专业毕业设计前期预研、想用“人工智能学习路径”里最经典的视觉任务练手的新手工程师。2. 看图说话用预训练CNN榨干图像特征再交给LSTM生成句子2.1 为什么选VGG16LSTM而不是直接上Transformer看图说话主流的实现框架是编码器-解码器架构。编码器负责把图像像素压缩成一个固定维度的向量解码器负责把这个向量展开成句子。在导论课大作业这个场景下编码器我一般选VGG16而不是ResNet或EfficientNet。原因很简单VGG16的最后一个全连接层输出4096维这个维度跟LSTM的输入维度对齐时参数换算非常直观ResNet的2048维也很好用但ResNet有残差连接和BatchNorm调参时多出好几个变量对第一次做这个任务的人来说不够友好。解码器用LSTM不用GRU也不是因为LSTM一定更好而是因为大部分教材和公开课里都拿LSTM当例子答辩时老师问起来你能说得清“遗忘门、输入门、输出门分别干什么”。Transformer在长文本生成上更强但导论课的机器跑不起大规模预训练而且Transformer的收敛速度对数据和超参更敏感大作业没必要冒这个险。这里还有一个关键细节VGG16用哪一层做特征输出。我见过有人直接取fc2层最后一个全连接层的输出也有人取pool5层。区别在于fc2层已经做了非线性变换特征更“抽象”但也丢失了空间结构pool5层保留了7×7的空间布局后续想加注意力机制时还有余地。我的做法是取pool5层输出展平后过一个自己加的全连接层把维度压到embedding_size256或512。这样后面接注意力模块不用改骨架。2.2 数据集准备用Flickr8k跑通流程别一上来就啃COCO看图说话最常用的数据集是Flickr8k、Flickr30k和MS COCO。我强烈建议导论大作业用Flickr8k它有8000张图片每张配5句人工描述。FLickr30k是3万张COCO是12万张单是下载和预处理就够折腾半天训练一轮的时间更是成倍上涨。Flickr8k的缺点是词汇量不大生成出来的句子会有点套路化但这恰恰说明模型训练充分答辩演示效果反而好。数据准备流程分三步下载图片、解析每张图的5句描述、建立词表。词表构建时要把所有句子转成小写按空格分词滤掉标点然后统计词频。一般取词频大于等于2的词进词表其余全部映射到UNK。Flickr8k的词表大概在2000到3000个词之间。数据划分按官方给的建议6000张训练1000张验证1000张测试。如果你需要更细的划分代码常见做法是直接读官方的txt划分文件而不是自己随机切这样后续跟别人论文里的指标对比时数据口径一致。2.3 最小可跑通的训练代码Dataset、collate_fn和训练循环这里给出一个精简到能跑通、又保留了核心细节的PyTorch实现。模型部分用VGG16做编码器LSTM做解码器。import torch import torch.nn as nn import torchvision.models as models from torch.utils.data import Dataset, DataLoader from PIL import Image import numpy as np class CaptionDataset(Dataset): def __init__(self, img_dir, captions, word2idx, transformNone): self.img_dir img_dir # 图片根目录 self.captions captions # dict: {image_id: [句子1, 句子2, ...]} self.word2idx word2idx # 词到索引的映射表 self.transform transform self.image_ids list(captions.keys()) def __len__(self): return len(self.image_ids) def __getitem__(self, idx): img_id self.image_ids[idx] # 随机选一句描述避免模型死记某一句 cap np.random.choice(self.captions[img_id]) # 句子转索引序列开头加start结尾加end tokens [self.word2idx[start]] tokens [self.word2idx.get(w, self.word2idx[unk]) for w in cap.lower().split()] tokens.append(self.word2idx[end]) # 加载图片并做统一缩放 img Image.open(f{self.img_dir}/{img_id}.jpg).convert(RGB) if self.transform: img self.transform(img) return img, torch.tensor(tokens, dtypetorch.long) def collate_fn(batch): # 按句子长度降序排序方便后面pack_padded_sequence imgs, caps zip(*batch) cap_lens [len(c) for c in caps] sorted_idx sorted(range(len(cap_lens)), keylambda i: cap_lens[i], reverseTrue) imgs torch.stack([imgs[i] for i in sorted_idx]) caps [caps[i] for i in sorted_idx] cap_lens [cap_lens[i] for i in sorted_idx] # 补齐到batch内最长句子的长度 targets torch.zeros(len(caps), max(cap_lens), dtypetorch.long) for i, c in enumerate(caps): targets[i, :len(c)] c return imgs, targets, cap_lens代码里有三个关键决策点。第一每张图每次训练随机选一句描述而不是固定取第一句等于把数据量乘以5能明显缓解过拟合。第二collate_fn里做排序是为了后用pack_padded_sequence它能跳过PAD位置的计算既省显存又避免模型去预测无效的PAD。第三词表里必须留出start、end、unk三个特殊token否则解码时无法判断句子何时结束遇到词表外单词会直接崩溃。编码器和训练循环的核心代码更短class EncoderCNN(nn.Module): def __init__(self, embed_size): super().__init__() vgg models.vgg16(weightsmodels.VGG16_Weights.IMAGENET1K_V1) # 取到pool5层输出7x7x512的特征图 self.features vgg.features # 自定义一个映射层把7x7x512压成embed_size维 self.linear nn.Linear(7*7*512, embed_size) # 冻结前13层卷积只训练最后几层和linear层 for i, p in enumerate(self.features.parameters()): p.requires_grad i 20 def forward(self, images): feat self.features(images) # [B,512,7,7] feat feat.view(feat.size(0), -1) # [B,512*7*7] feat torch.relu(self.linear(feat)) # [B, embed_size] return feat class DecoderLSTM(nn.Module): def __init__(self, embed_size, hidden_size, vocab_size): super().__init__() self.embed nn.Embedding(vocab_size, embed_size) self.lstm nn.LSTM(embed_size, hidden_size, batch_firstTrue) self.fc nn.Linear(hidden_size, vocab_size) def forward(self, features, captions, lengths): cap_embed self.embed(captions) # [B, L, embed] # 把图片特征拼到句子的起始位置 cap_embed torch.cat((features.unsqueeze(1), cap_embed), dim1) cap_embed nn.utils.rnn.pack_padded_sequence( cap_embed, lengths, batch_firstTrue, enforce_sortedTrue) out, _ self.lstm(cap_embed) out, _ nn.utils.rnn.pad_packed_sequence( out, batch_firstTrue) return self.fc(out)这段代码里的参数需要解释清楚。embed_size是图片特征和词嵌入的统一维度我设256hidden_size设512这两个值在Flickr8k上是我试出来训练速度和生成质量最平衡的组合。embed_size小于200会生成大量语法不通的句子大于512则训练时间涨一截但BLEU并没有显著提升。冻结前20层卷积参数是有讲究的——VGG16前面几层学的是边缘、纹理这类通用特征微调它们既费时间又容易破坏预训练权重只需要训练后面的高层语义特征和最后新增的linear层就够了。enforce_sortedTrue必须配合collate_fn里的排序操作如果排序逻辑改了这里要改成False否则会直接报错。训练循环里的关键超参batch_size设64学习率设3e-4优化器用Adam训练25到30个epoch。loss用交叉熵但记得把targets里的PAD位置ignore掉PyTorch里直接传ignore_index0就行。每两个epoch在验证集上跑一次贪心解码肉眼看一下生成的句子是否通顺比只看loss值直观得多。2.4 推理阶段从贪心解码换成Beam Search生成质量立刻上一个台阶训练完成后推理时的默认做法是贪心解码——每步取当前概率最大的词作为下一个词。但贪心的问题在于它只看局部最优容易生成“the the the”这种重复文本。一个实现成本极低、效果立竿见影的改进是Beam Search。它每步维护K条候选序列K一般取3到5最终选择整体概率最高的那一条。Beam Search的代码核心并不长在已有训练好的模型上直接替换推理函数即可def beam_search_decode(model, image_feature, word2idx, idx2word, beam_width3, max_len20): # 候选序列用 (概率对数, [token列表]) 表示 start_token word2idx[start] end_token word2idx[end] seqs [(0.0, [start_token], model.init_hidden())] for _ in range(max_len): all_candidates [] for score, tokens, hidden in seqs: if tokens[-1] end_token: all_candidates.append((score, tokens, hidden)) continue lstm_out model.decode_step(image_feature, tokens[-1], hidden) log_probs torch.log_softmax(lstm_out, dim-1) top_k torch.topk(log_probs, beam_width) for i in range(beam_width): new_token top_k.indices[i].item() new_score score top_k.values[i].item() new_tokens tokens [new_token] new_hidden (lstm_out, hidden) # 简写实际传LSTM状态 all_candidates.append((new_score, new_tokens, new_hidden)) # 只保留全局得分最高的beam_width条 seqs sorted(all_candidates, keylambda x: x[0], reverseTrue)[:beam_width] if all(s[-1] end_token for s in seqs[0][1]): break best_seq seqs[0][1] return [idx2word[t] for t in best_seq if t not in (start_token, end_token)]这段代码里的得分用的是对数概率累加原因是概率连乘会数值下溢对数化之后变成加法数值稳定且代码更简洁。topk取beam_width个候选是每步扩展一次的常见做法。真正的坑在hidden状态的处理——LSTM的hidden和cell是两个张量推理时每个候选分支的hidden必须独立维护如果所有候选共享同一个hidden生成的K条句子会逐渐趋同最终退化成贪心解码。我见过好几次有人在这上面翻车代码看起来没问题但Beam Search的BLEU反而比贪心还低就是这个原因。K值的选择也有讲究。K1就是贪心解码K2效果提升最明显K3到5提升趋缓且耗时增加。Flickr8k这种小词表场景K3是性价比最高的设置。在答辩演示时可以用K1和K3各跑一张图对比两段描述这个差异本身就是很好的展示素材。3. 微表情识别两手方案一套保底一套加分3.1 微表情和普通表情的差别决定了你不能直接套用表情识别模型微表情识别是表情识别的一个特殊分支。普通表情持续时间在500毫秒到4秒幅度明显肉眼容易分辨微表情持续时间只有1/25到1/5秒强度低往往是压抑真实情绪时露出的马脚。这给模型带来了两个直接的麻烦帧间差异太小特征不明显公开数据集太少且标注成本极高。CASME II是目前微表情识别最常用的数据集但它的样本量只有不到300个视频片段而且下载需要申请。如果是课程大作业不一定能及时拿到审批。更现实的做法是“两步走”用CK这个静态表情数据集来跑通一个CNN分类模型作为保底方案再在这个基础上做微表情时序增强——比如把视频帧序列输入LSTM或者对帧间差分图做处理——把这个作为“微表情”的加分项。先说明白CK是刻意表演出来的表情和真正的微表情有本质差别。但课程大作业的评分标准看的是“技术方案是否完整”而不是“研究结论是否有突破”。你用CK证明了方案可行再用一个微型自采数据集比如录自己几段面部视频截取片段展示模型能对上微表情的时间特性这在答辩里已经是一个完整的故事线了。3.2 人脸检测与对齐这个步骤决定了你的准确率上限微表情识别的第一个坑不是模型而是人脸预处理。原图上的人脸位置、角度、光照各不相同直接输给CNN会引入大量与表情无关的噪声。我通常在dlib和OpenCV DNN人脸检测器之间选OpenCV DNN因为dlib的HOG检测器在侧脸和大角度旋转时鲁棒性差而OpenCV DNN模型可以拿到边界框置信度方便过滤误检。人脸对齐的经典做法是用OpenCV内置的Eyes Cascade detector检测双眼中心然后算一个仿射变换矩阵把眼睛旋转到水平位置再统一裁剪到224×224。对微表情任务来说对齐比检测更关键——因为表情的肌肉运动幅度小如果眼睛位置偏移几个像素CNN看到的可能是眉毛和眼睑的差异而不是表情本身的差异。import cv2 import numpy as np def align_face(image, detector_path, eye_cascade_path): # 加载模型 detector cv2.FaceDetectorYN_create( detector_path, , (224, 224), score_threshold0.7) face, detections detector.detect(image) if detections is None: return None # 取置信度最高的人脸框 x, y, w, h detections[0][:4].astype(int) face_img image[y:yh, x:xw] gray cv2.cvtColor(face_img, cv2.COLOR_BGR2GRAY) eye_cascade cv2.CascadeClassifier(eye_cascade_path) eyes eye_cascade.detectMultiScale(gray, 1.1, 5) if len(eyes) 2: return cv2.resize(face_img, (224, 224)) # 按x坐标排序取出左右眼中心 eyes sorted(eyes, keylambda e: e[0]) left_center (eyes[0][0] eyes[0][2]//2, eyes[0][1] eyes[0][3]//2) right_center (eyes[1][0] eyes[1][2]//2, eyes[1][1] eyes[1][3]//2) # 计算旋转角度并做仿射变换 angle np.degrees(np.arctan2( right_center[1] - left_center[1], right_center[0] - left_center[0])) M cv2.getRotationMatrix2D(left_center, angle, 1.0) aligned cv2.warpAffine(face_img, M, (face_img.shape[1], face_img.shape[0])) return cv2.resize(aligned, (224, 224))这段代码的行为逻辑是先用OpenCV的深度学习人脸检测器定位人脸再检测双眼位置计算两眼连线与水平线的夹角旋转人脸使得眼睛对齐到水平。这是一种最朴素的人脸对齐但足够解决大多数大作业场景的问题。score_threshold设0.7可以过滤掉大量背景误检如果发现漏检严重再调低到0.5。如果眼睛检测失败闭眼、侧脸等情况代码会退化成直接缩放原人脸框这个fallback逻辑保证了流程不中断。3.3 模型实现用ResNet18迁移学习改最后一层表情分类的模型选型我一般用ResNet18。ResNet18在ImageNet上有预训练权重迁移到表情这种小数据集上收敛快同时模型参数只有1100万左右CPU推理一张图也就100毫秒上下。对比VGG16ResNet18的参数量只有它的四分之一却因为残差连接在精度上不落下风现场演示时不掉链子。import torch.nn as nn import torchvision.models as models class MicroExpressionNet(nn.Module): def __init__(self, num_classes7, freeze_until3): super().__init__() self.backbone models.resnet18( weightsmodels.ResNet18_Weights.IMAGENET1K_V1) # 冻结前3个stage保留低层通用特征 layers list(self.backbone.children()) for i, p in enumerate(self.backbone.parameters()): if i freeze_until * 10: # 经验值粗略按层数冻结 p.requires_grad False # 替换最后一层全连接 in_features self.backbone.fc.in_features self.backbone.fc nn.Sequential( nn.Dropout(0.5), nn.Linear(in_features, num_classes) ) def forward(self, x): return self.backbone(x)这里的关键参数有两个。freeze_until决定从第几个stage开始解冻我一般设3即只训练最后一个stage和新增的fc层。数据集更小的时候比如只有几百张设5让前面全冻住只训fc层。Dropout(0.5)放在fc层之前对表情这种数据量小的任务很关键——我在CK上做过对比加了Dropout的验证集准确率能高5到8个百分点。损失函数用交叉熵优化器还是Adam学习率从1e-4起步每10个epoch衰减0.1倍。训练时数据增强至少要加随机水平翻转、随机旋转±10度、随机亮度对比度扰动。表情识别里还有一个特殊的增强手段——随机擦除随机遮挡人脸的一个小方块强迫模型学会利用局部特征而不是只盯着一块区域。如果你想让方案更接近“微表情”而不是“表情”有一个轻量级改进对视频片段做帧间差分把差分图后一帧减前一帧的绝对值和原始帧拼接成双通道输入。这样模型能够捕捉到肌肉运动的幅度信息而微表情的动静恰恰是“幅度小但方向明确”。代价是训练数据需要按视频组织CK正好有一小段视频所以这条路是可复现的。3.4 评估指标别看准确率看混淆矩阵表情识别有一个典型陷阱CK数据集的类别分布很不均衡开心Happy类样本多、厌恶Disgust类样本少。如果只盯着总准确率模型会倾向于把所有样本都预测成数量多的类别得到看似不错的准确率但实际没有学会识别少数类。我一般训练结束后必须跑三件事绘制混淆矩阵、计算每类召回率、打印加权F1。混淆矩阵能直观看到哪些类别容易互相混淆——在CK上恐惧Fear和惊讶Surprise的混淆率极高生化上有解释这两个表情的眼部和嘴部运动模式确实接近。如果少数类别效果实在太差优先用类别权重而不是简单的过采样。PyTorch里给CrossEntropyLoss传一个weight参数即可数值设为1/类别样本数。这比复制少数类样本的做法更稳因为过采样容易让模型对少数类的具体某几张图过拟合。4. 把两个任务塞进一个zip导论大作业的代码组织、报告结构和答辩演示4.1 一个能直接解压运行的目录结构长什么样大作业最后是要提交一个zip包的评审老师在解压后第一眼看到的就是目录结构。如果里面全是没头没尾的.ipynb和一团乱麻的final_v2_最终版.py还没跑代码印象分就掉了一半。我一般会用下面的结构组织ai_intro_project/ ├── README.md # 怎么装依赖、怎么跑、结果长什么样 ├── requirements.txt # torch, torchvision, opencv-python 等 ├── code/ │ ├── caption/ │ │ ├── dataset.py # 看图说话的数据集类 │ │ ├── model.py # 编码器和解码器 │ │ ├── train.py # 训练入口 │ │ └── inference.py # Beam Search推理 │ └── expression/ │ ├── preprocess.py # 人脸检测对齐 │ ├── train.py # 微表情分类训练 │ └── evaluate.py # 混淆矩阵和F1 ├── data/ │ ├── flickr8k/ # 图片和标注小样本演示版 │ └── ckplus/ # CK裁剪后的人脸图 ├── models/ # 训练好的权重文件说明 └── report/ ├── 实验报告.md └── 答辩演示PPT大纲.mdREADME是很多人会忽略但我觉得最应该认真写的地方。不需要写长作文就写三件事用什么Python版本和显卡环境、每条命令按什么顺序执行、预期输出是什么。比如python code/caption/train.py --epochs 30跑完会生成models/caption_30epochs.pth再跑inference.py会输出测试集BLEU分数。有了这个评审老师复现起来顺畅自己也免得答辩前忘记怎么跑。data/目录我只建议放一个子集比如Flickr8k里挑20张图做冒烟测试完整数据集压缩包太大上传到课程平台常被限制。README里写清楚完整数据集的下载方式和放到哪个路径即可。4.2 把训练流程串起来一个脚本搞定全部课程作业经常给你一台没有图形界面的服务器所有操作都要在命令行完成。我会在code/下放一个run_all.sh把全流程串起来#!/bin/bash set -e # 任何一步失败就终止避免带错往下跑 # 第1步验证数据集目录是否存在 if [ ! -d data/flickr8k ]; then echo ERROR: 请先下载Flickr8k数据集到data/flickr8k exit 1 fi # 第2步训练看图说话模型30个epoch约2小时 python code/caption/train.py --epochs 30 --batch_size 64 --lr 3e-4 # 第3步用测试集评估BLEU python code/caption/inference.py --split test --beam_width 3 # 第4步训练微表情分类模型 python code/expression/train.py --lr 1e-4 --freeze_until 3 # 第5步生成混淆矩阵图 python code/expression/evaluate.py --output report/confusion_matrix.pngset -e这个细节很多人会忽略但非常重要。没有它的话第2步训练中断了脚本还会继续跑第3步然后因为找不到权重文件抛出另一个莫名其妙的错误。加上set -e后任何一步失败都会立即终止日志里能直接看到哪一步出了问题。这在答辩现场调试时能救命。训练过程要留日志。最简单的做法是用Python的logging模块打点记录每个epoch的loss、BLEU或准确率。答辩时老师问“训练到第几个epoch开始收敛”你直接翻日志就能答上来这比“我记得大概10几个epoch吧”这种回答专业得多。4.3 答辩时最能加分的对比实验表和演示顺序答辩PPT里放一张实验对比表比放十张网络结构图都实在。我建议至少做三行对比看图说话贪心解码 vs Beam Search(K3)的BLEU-1/BLEU-4对比微表情识别只训练fc层 vs 解冻最后一整个stage的准确率对比微表情识别不加数据增强 vs 加数据增强的F1对比这三组对比不需要额外做太多实验训练过程中顺手保存一个中间模型就能拿到数据。但它们合在一起能讲出一个完整的故事我在哪些环节做了探索、每个改进带来了多少提升、为什么是这些改进有效。这恰恰是《人工智能导论》这门课想考察的工程素养。现场演示的顺序也值得设计。先演示看图说话因为效果最直观——上传一张没见过的图片等一两秒屏幕打出一句描述。然后切到微表情识别拿手机拍一段自己的视频实时检测并标注表情。如果条件允许两个模型可以在同一个窗口中并排显示一个处理静态图一个处理视频流这个视觉冲击力在答辩现场是很加分的。5. 避坑指南看图说话和微表情识别最常翻车的5个现场5.1 现象看图说话的BLEU跑出来只有0.05几乎是随机水平原因绝大多数是数据预处理出了问题。最常见的是把分词的split()用成了空格分词但原数据集里的句子带英文逗号、句号比如a dog is running , on the grass逗号被split()切出来后单独成词词表里会出现大量逗号、句号这种“伪词”。这些伪词既浪费词表容量又会让LSTM学着生成带奇怪空格的句子。解决分词前统一处理标点。我一般先做re.sub(r[^\w\s], , text)把标点全部删掉再做lower().split()。还有一个隐蔽的问题句子里如果带数字比如2 dogs2会单独成词但测试集里出现2的频率很低。更稳的做法是把所有数字替换成num特殊token。这两个处理做完BLEU一般能翻一倍。5.2 现象微表情模型训练集准确率95%验证集只有30%过拟合非常严重原因数据集太小。CK一共只有593个序列、约980张标注表情图而ResNet18的最后一层有上万个参数完全能把训练集背下来。很多人直接拿预训练模型在冻结层之外全量微调等于用上万参数去拟合一千个样本不炸才怪。解决三个手段组合。一是更激进的冻结策略把freeze_until调到5只训练fc层二是加更强的数据增强随机擦除和光度扰动都用上三是训练时间提前停止用验证集准确率做早停别闷头训满50个epoch。我在CK上实测下来最有用的是第一个手段——只训fc层时验证集准确率反而比全量微调高10个百分点以上。5.3 现象LSTM训练过程中loss死活不降一直是4点几原因学习率过大。LSTM对学习率比CNN敏感得多Adam在3e-3以上时梯度震荡剧烈loss会横跳另一个常见错误是词嵌入维度设太小比如embed_size128时词向量信息容量不足也会导致loss下不去。解决把学习率降到3e-4到1e-4区间embed_size提到256。同时可以给LSTM加梯度裁剪PyTorch里一行代码torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm5)。梯度裁剪不会直接提升精度但能避免训练中途因梯度爆炸被NaN打断——那种情况只能整个模型重训是真正的血泪教训。5.4 现象人脸检测框在视频里跳来跳去导致识别结果抖动原因逐帧独立检测帧间的检测框没有时序关联。人脸轻微移动几像素检测框就会被重新回归导致裁剪出来的人脸区域内容跳变。解决给检测框做一个滑动平均。维护一个全局变量记录最近N帧的检测框坐标每帧的检测结果跟历史值加权平均。更优雅的做法是使用cv2.TrackerKCF——第一帧检测到人脸后交给跟踪器跟踪器丢失目标时再重新检测。在演示场景里这个改进能让识别结果看起来“稳”很多虽然对单帧精度没有本质影响。5.5 现象模型训练完推理时输出全部是unk或者空句子原因词表里unk被赋予的索引是0而在LSTM里索引0是合法的模型学到用UNK乱填充。更麻烦的是训练时PAD用的也是0的话模型会学着把UNK和PAD混在一起。空句子则是Beam Search的终止条件写错了——break条件没有检查候选是否包含end。解决词表构建时固定三个特殊token的索引pad0, start1, end2, unk3。训练时交叉熵的ignore_index设pad的索引0推理时遇到end立即停止该候选序列的扩展。这两处统一后UNK乱填充的问题基本消失。6. 进阶玩法从“跑通”到“能写进简历”的验证与可视化这套大作业如果只做到能跑答辩分数已经有了但如果你想把它沉淀成作品集里的一条项目经历还需要补齐三个细节指标计算的严谨性、可视化解释性、以及一套可复现的实验记录。第一BLEU的计算不能用训练集。很多人为了省事直接在测试集上跑推理但大作业里很容易出现训练集和测试集没切干净的情况导致BLEU虚高。严谨的做法是单独写一个评估脚本只读测试集目录用PyTorch的torch.no_grad()跑Beam Search。BLEU的计算建议用nltk.translate.bleu_score注意corpus_bleu和sentence_bleu的差别——前者对整批句子求总和后者逐句算再平均两者的数值能差出0.05以上。报告里注明用的是哪一种衡量方式否则后续复现的人会产生困惑。第二给CNN做可视化。微表情模型训练完了用GradCAM生成注意力热力图叠在人脸图上——你会看到模型在关注眼睛、眉毛和嘴角区域这跟心理学里微表情的AU定义是吻合的。答辩时放一张热力图对比原图比说十句“我的模型学到了表情特征”都有说服力。PyTorch里用已有的库或手写反向传播钩子都能实现整个代码量不超过30行。这个可视化还有个额外用处如果热力图集中在人脸背景上说明模型学到的是背景区分度而不是表情特征可以据此回头修数据。第三给自己留一套实验记录模板。每跑完一组实验把时间、超参、loss曲线、最终指标记在一个Markdown文件里。这套大作业的调参空间不大但记录本身是让你从“调参玄学”走向“工程方法”的第一步。我自己的习惯是每改一个变量只跑一次完整实验不并行开多个实验因为GPU内存不够两三个实验挤在一起反而全都跑不快。微表情识别方面如果你的余力允许可以试一下把静态CNN的特征换成AU编码——用OpenFace提取脸部动作单元强度序列再送入LSTM分类。这样“微表情”的“微”字就真正落到技术里了因为它捕捉的是肌肉运动单元的微小动态变化而不是静态表情模式。这条路需要多写一个数据转换脚本但工程上它和前面所有代码完全兼容只替换了输入特征。最后想跟你说一个我自己的教训早期做大作业时我总把时间花在换更大更新的模型上面觉得模型越高级分数越高。后来发现课程作业的评分核心是“方案完整度和结果可复现”你用一个链路完整、有对比实验、能稳定复现的VGG16LSTM方案远好于一个训练了三天不出结果的Transformer。先把小方案跑通再在有余力时做改进这才是这门课最值得学会的节奏。希望这篇笔记能帮你少走一点弯路。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →