资讯动态

目标意图识别毕设:数据集处理与CNN、LSTM、Transformer多算法对比实战

发布时间:2026/10/8 16:51:39 来源:尧图企业网站定制
简介这份资源是面向计算机相关专业在校学生、高校教师及初级程序员的目标意图识别毕业设计项目包基于多种深度学习算法实现涵盖非预训练Bi-LSTM与预训练ERNIE等模型方案适合作为毕设、课程设计或期末大作业的完整参考也便于有钻研需求的读者进行二次开发。压缩包共305个文件约30.9MB以110个Python源码为核心辅以43个txt说明、33个csv数据集、18组out/label/in标注文件以及json、pkl、vocab等模型配置与词表资源另含md文档、sh脚本和少量图片目录结构清晰便于按模块检索。资源内附运行说明文档按文档操作即可跑通需注意项目路径避免中文解压后重命名为英文再运行。目前已有199人学习下载读者可获得完整算法实现、数据集与训练配置并参考错误与正确结果对比文件理解模型差异快速完成实验复现与排错。1. 目标意图识别毕设从数据集到多算法对比的完整落地路径目标意图识别说白了就是让模型看完一段输入文本指令、行为序列、传感器时序甚至图像里的目标关系判断它背后想干什么。放到毕设场景里它通常被包装成「多分类任务」给定一条样本输出它属于哪个意图类别。这个标题里最值钱的部分不是「深度学习」四个字而是「多种算法」和「数据集」——它意味着你要在同一份数据上把 CNN、LSTM、Transformer 这类结构跑一遍做出可对比的结果。适合谁适合正在做毕设、手里有 Python 基础、但不知道如何把「算法对比」做成一篇能过答辩的工程的人。我见过太多人卡在数据格式不统一、标签泄漏、训练集测试集划分随意这三件事上最后跑出来的准确率自己都不敢信。这篇笔记就按「数据怎么进 → 模型怎么搭 → 结果怎么比 → 坑怎么躲」的顺序把这条链路讲透。2. 数据集处理把原始样本变成模型能吃的张量2.1 先搞清楚你的意图识别属于哪一类任务目标意图识别在不同场景下数据形态完全不同。文本指令类比如「打开空调」「查询余额」是序列标注或句分类行为序列类用户点击流、操作日志是变长时序分类如果标题里的「目标」指的是图像中的对象那它更接近「场景意图」——从检测框和类别组合推断整体行为。毕设里最常见的是前两种。你拿到数据集后第一件事不是写模型而是确认三件事样本长度分布、类别是否均衡、有没有重复样本跨集合出现。我一般会先跑一段统计脚本把长度直方图和类别计数打出来心里有数再动手。import pandas as pd import matplotlib.pyplot as plt # 假设数据是 csv两列text, label df pd.read_csv(intent_data.csv) print(类别分布\n, df[label].value_counts()) print(文本长度统计\n, df[text].str.len().describe()) # 画长度直方图决定 padding 长度 df[text].str.len().hist(bins50) plt.xlabel(sequence length) plt.ylabel(count) plt.show()这段代码的作用是让你在写 Dataset 类之前先确定max_len设多少。如果 95% 的样本长度在 40 以内你设 128 就是浪费显存如果长尾到 300设 64 就会截断关键信息。类别分布那行输出如果最大类和最小类差 10 倍以上后面训练必须加权重或做重采样否则模型会偏向多数类准确率虚高但召回惨不忍睹。2.2 构建可复用的 Dataset 与 DataLoaderPyTorch 的Dataset是毕设里最值得写规范的一层。很多人把预处理塞进训练循环导致每次 epoch 都重复分词慢得离谱。正确做法是把分词、转 id、padding 全部放在__getitem__里或者更彻底一点离线预处理成 id 序列存下来。下面是一个文本意图识别的标准写法词表用训练集构建避免验证集词汇泄漏。import torch from torch.utils.data import Dataset, DataLoader from collections import Counter class IntentDataset(Dataset): def __init__(self, texts, labels, vocabNone, max_len64): self.texts texts self.labels labels self.max_len max_len # 只用训练集构建词表 if vocab is None: counter Counter() for t in texts: counter.update(t.split()) self.vocab {w: i2 for i, (w, _) in enumerate(counter.most_common(20000))} self.vocab[pad] 0 self.vocab[unk] 1 else: self.vocab vocab def __len__(self): return len(self.texts) def __getitem__(self, idx): tokens self.texts[idx].split()[:self.max_len] ids [self.vocab.get(w, 1) for w in tokens] ids [0] * (self.max_len - len(ids)) return torch.tensor(ids), torch.tensor(self.labels[idx]) # 划分时先固定随机种子 train_texts, val_texts, train_labels, val_labels train_test_split( df[text].tolist(), df[label].tolist(), test_size0.2, random_state42, stratifydf[label] ) train_ds IntentDataset(train_texts, train_labels) val_ds IntentDataset(val_texts, val_labels, vocabtrain_ds.vocab) train_loader DataLoader(train_ds, batch_size32, shuffleTrue)关键点在vocabtrain_ds.vocab这一句验证集和测试集必须复用训练集的词表否则同一个词在不同集合里 id 不一致模型学到的 embedding 就乱了。stratify参数保证划分后类别比例一致这对小数据集尤其重要。max_len截断策略是保留前 64 个 token如果你的关键意图词常出现在句尾得改成后截断或双向截断这个细节答辩时老师一问就露馅。2.3 标签编码与类别权重的计算标签从字符串转成 0 到 N-1 的整数看似简单但顺序必须固定。我习惯用sklearn的LabelEncoder存一份映射表推理时反查用。类别不均衡时CrossEntropyLoss的weight参数直接传各类别频率的倒数。from sklearn.preprocessing import LabelEncoder import numpy as np le LabelEncoder() all_labels le.fit_transform(df[label]) class_counts np.bincount(all_labels) weights 1.0 / class_counts weights weights / weights.sum() * len(class_counts) # 归一化 class_weights torch.tensor(weights, dtypetorch.float32) criterion torch.nn.CrossEntropyLoss(weightclass_weights)weights归一化后均值约为 1避免整体 loss 尺度变化太大影响学习率。如果某个类只有个位数样本权重会非常大这时候更稳的做法是过采样而不是硬加权否则模型对那几个样本过拟合。这一步做完数据侧基本就干净了。3. 多种深度学习算法的搭建与训练CNN、LSTM、Transformer 怎么选3.1 三个基线模型的代码结构与参数含义毕设要求「多种算法」不是让你堆十几个而是选三到四个有代表性的结构做对比。文本意图识别里TextCNN 抓局部 n-gram 特征BiLSTM 抓时序依赖Transformer 抓全局注意力这三者刚好覆盖不同归纳偏置。下面给出三个模型的精简实现共用同一个 embedding 维度。import torch.nn as nn class TextCNN(nn.Module): def __init__(self, vocab_size, embed_dim128, num_classes10): super().__init__() self.embed nn.Embedding(vocab_size, embed_dim, padding_idx0) # 三个不同尺寸的卷积核捕捉 2/3/4 元词组 self.convs nn.ModuleList([ nn.Conv1d(embed_dim, 128, k) for k in (2, 3, 4) ]) self.fc nn.Linear(128 * 3, num_classes) self.dropout nn.Dropout(0.3) def forward(self, x): x self.embed(x).transpose(1, 2) # [B, E, L] x [torch.relu(c(x)).max(dim2)[0] for c in self.convs] x torch.cat(x, dim1) return self.fc(self.dropout(x)) class BiLSTM(nn.Module): def __init__(self, vocab_size, embed_dim128, hidden128, num_classes10): super().__init__() self.embed nn.Embedding(vocab_size, embed_dim, padding_idx0) self.lstm nn.LSTM(embed_dim, hidden, batch_firstTrue, bidirectionalTrue) self.fc nn.Linear(hidden * 2, num_classes) def forward(self, x): x self.embed(x) out, _ self.lstm(x) # 取最后一个时间步配合 padding 应改用 mask 池化 return self.fc(out[:, -1, :])TextCNN 的padding_idx0让 pad 的 embedding 不参与梯度更新Conv1d的输入通道是 embedding 维度输出通道 128 是超参可以调成 64 或 256。BiLSTM 里out[:, -1, :]取最后一步在变长序列里其实取到了 pad 位置正确做法是用 mask 做平均池化这个坑后面避坑章节会细说。Transformer 直接用nn.TransformerEncoderLayer注意加src_key_padding_mask。3.2 训练循环里必须记录的指标与早停策略训练循环不是只打印 loss。毕设答辩时老师会问「你怎么判断模型收敛了」你需要有验证集准确率、F1、以及早停的耐心值。下面这个循环把关键逻辑都写清楚了。def train_model(model, train_loader, val_loader, epochs30, lr1e-3, patience5): optimizer torch.optim.Adam(model.parameters(), lrlr) criterion nn.CrossEntropyLoss(weightclass_weights) best_f1, wait 0, 0 for epoch in range(epochs): model.train() for x, y in train_loader: optimizer.zero_grad() loss criterion(model(x), y) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 5.0) optimizer.step() # 验证 model.eval() preds, trues [], [] with torch.no_grad(): for x, y in val_loader: preds.extend(model(x).argmax(1).tolist()) trues.extend(y.tolist()) f1 f1_score(trues, preds, averagemacro) print(fepoch {epoch} macro-F1 {f1:.4f}) if f1 best_f1: best_f1, wait f1, 0 torch.save(model.state_dict(), best.pt) else: wait 1 if wait patience: print(early stop) break return best_f1clip_grad_norm_对 LSTM 和 Transformer 几乎是必加项梯度爆炸在时序模型里太常见。早停 patience 设 5 意味着连续 5 个 epoch 验证 F1 不升就停避免过拟合。保存best.pt而不是最后一个 epoch是因为最后几个 epoch 往往已经在过拟合。学习率 1e-3 对 Adam 是安全起点如果 loss 震荡就降到 5e-4。3.3 多算法对比实验的组织方式三个模型不能各跑各的然后手工抄数字。我一般写一个统一的实验入口用配置字典控制模型类型把结果写进同一个 csv。configs { textcnn: {lr: 1e-3, batch: 32, max_len: 64}, bilstm: {lr: 5e-4, batch: 32, max_len: 64}, transformer: {lr: 1e-4, batch: 16, max_len: 64}, } results [] for name, cfg in configs.items(): model build_model(name, vocab_sizelen(train_ds.vocab)) f1 train_model(model, train_loader, val_loader, lrcfg[lr]) results.append({model: name, macro_f1: f1, **cfg}) pd.DataFrame(results).to_csv(compare.csv, indexFalse)这样跑完你手里就有一张对比表答辩时直接展示。注意每个模型的最优学习率不同Transformer 通常需要更小的 lr 和更小的 batch这不是玄学是注意力机制对 batch 内统计量更敏感。如果时间允许每个模型跑三次不同随机种子取均值方差大的模型说明稳定性差这也是一个可写的结论。4. 避坑与排查意图识别毕设里最容易翻车的五件事4.1 验证集准确率比训练集还高现象训练 loss 正常下降但验证准确率始终高于训练准确率差距还不小。原因通常有两个一是 Dropout 只在训练时开启验证时关闭模型在验证集上「发挥更稳」二是数据划分时验证集太简单或者训练集里混入了噪声标签。解决先确认model.eval()在验证前调用了再检查划分是否用了 stratify。如果还高把训练集和验证集对调跑一次如果结论反转说明数据分布有问题需要重新清洗。4.2 模型只预测多数类现象不管输入什么输出永远是样本最多的那个类别准确率看着有 60% 但 F1 很低。原因就是类别不均衡加上没加权。解决按 2.3 节算class_weights传给 loss同时把评估指标从 accuracy 换成 macro-F1。如果加权后还是偏向多数类检查权重是否被归一化抹平了差异必要时直接用过采样把少数类补到和多数类同量级。4.3 LSTM 取最后时间步取到了 padding现象BiLSTM 验证 F1 比 TextCNN 低一截但理论上时序模型不该差这么多。原因out[:, -1, :]在 batch 内序列长度不一时短句后面全是 pad最后一步取到的是 pad 的隐状态。解决用 mask 做加权平均池化或者取每个样本真实长度的最后一步。下面这段是修正写法。def masked_mean(out, mask): # out: [B, L, H], mask: [B, L] mask mask.unsqueeze(-1).float() return (out * mask).sum(1) / mask.sum(1).clamp(min1e-9)mask从 DataLoader 里额外返回pad 位置为 0真实 token 为 1。这个改动通常能让 LSTM 的 F1 提升好几个点属于血泪经验。4.4 词表构建时把验证集词汇也统计进去现象线下验证 F1 很高换一批新数据推理时效果暴跌。原因构建词表时用了全量数据验证集里的词在训练时已经见过 id造成信息泄漏。解决严格按 2.2 节的写法词表只用训练集构建验证和测试复用。这个坑在毕设里极其常见因为很多人图省事直接对df[text]做Counter。4.5 随机种子没固定导致结果不可复现现象同一份代码跑两次F1 差 3 个点答辩时老师让你现场复现结果对不上。原因Python、NumPy、PyTorch 三处随机源都没固定。解决在入口脚本最前面统一设置。import random, numpy as np, torch def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic Truecudnn.deterministic True会稍微降低训练速度但换来确定性毕设场景完全值得。设完之后 DataLoader 的shuffle也要用带种子的 generator否则 batch 顺序仍然随机。5. 把毕设做成可交付推理封装、结果可视化与答辩技巧5.1 写一个能直接调用的推理函数训练完保存best.pt只是半成品答辩演示时你需要一个输入一句话就输出意图的接口。下面这个封装把词表、标签映射、模型权重一起加载避免现场手忙脚乱。class IntentPredictor: def __init__(self, model_path, vocab, label_encoder, max_len64): self.vocab vocab self.le label_encoder self.max_len max_len self.model build_model(textcnn, vocab_sizelen(vocab)) self.model.load_state_dict(torch.load(model_path, map_locationcpu)) self.model.eval() def predict(self, text): ids [self.vocab.get(w, 1) for w in text.split()[:self.max_len]] ids [0] * (self.max_len - len(ids)) x torch.tensor([ids]) with torch.no_grad(): logits self.model(x) prob torch.softmax(logits, dim1) idx prob.argmax(1).item() return self.le.inverse_transform([idx])[0], prob[0][idx].item()map_locationcpu保证在没有 GPU 的演示机上也能跑。返回意图和置信度两个值置信度低于 0.6 时可以提示「不确定」这在答辩时是个加分项说明你考虑了实际部署的边界情况。5.2 用混淆矩阵和对比表撑起答辩结果页答辩 PPT 里最忌讳只放一个准确率数字。你需要三样东西多模型对比表、最优模型的混淆矩阵、以及两三个错误案例分析。混淆矩阵用sklearn一行出图重点看哪些类别互相混淆这往往能引出「为什么这两个意图在语义上接近」的讨论比干巴巴念数字强得多。模型准确率macro-F1训练耗时TextCNN0.910.892minBiLSTM0.900.886minTransformer0.920.9015min这张表里的数字要来自同一份划分、同一个评估脚本。耗时也写上体现你考虑了工程成本。如果 Transformer 只高一个点但慢三倍结论就可以写「在毕设数据规模下 TextCNN 性价比最高」这种有取舍的结论老师最爱听。5.3 我踩过的坑与给你的习惯建议最后说一个我自己的教训。第一次做这类毕设时我把所有预处理都写在训练脚本里每次调参都要重新分词一个实验等二十分钟。后来改成离线预处理加缓存同样的实验两分钟出结果迭代速度完全不是一个量级。另一个习惯是每个实验都存一份配置和结果到 csv不要相信自己的记忆三天前跑的 lr 是多少你肯定记不住。还有答辩前一定用全新环境跑一遍推理脚本确认没有隐藏的路径依赖。这些事看起来琐碎但决定了你是从容演示还是现场翻车。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑