ARTICLE DETAIL

资讯详情

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

意图识别双路线实战:传统方法与深度学习Python源码拆解

意图识别双路线实战:传统方法与深度学习Python源码拆解 简介这份资源面向意图识别方向的课程设计、毕业设计与入门研究者提供传统机器学习与深度学习两条技术路线的完整Python实现帮助读者在同一套数据上对比不同建模思路的效果差异。包内共282个文件以96个py源码、39个txt说明、33个csv数据表为主另含pdf与caj参考文献、json与iob标注文件、pkl与vocab词表等压缩包约28.24MB目录按数据集、模型与测试脚本分模块组织。资源覆盖ATIS与SNIPS两个英文数据集前者含4978条训练、888条测试、22个类别后者含13784条训练、700条测试、7个类别并给出SVM、LR、Stack-Propagation、Bi-model with decoder、Bi-LSTM、JointBERT、ERNIE等模型的训练与测试入口可输出训练时间与评测结果。已有166人学习适合希望快速跑通基线、理解传统方法与预训练模型差异并完成实验报告的读者。1. 意图识别双路线实战从传统特征到深度学习一份能跑通的 Python 源码拆解拿到「分别基于传统方法和深度学习方法实现意图识别」这个题目时很多人的第一反应是直接上 BERT 微调但真正在业务里落过地的人都知道意图识别Intent Detection从来不是「哪个模型强就用哪个」这么简单。它本质是一个短文本分类问题用户说一句话你要判断他到底想干什么——查天气、退订单、问价格、还是纯闲聊。传统方法用 TF-IDF 加 SVM 或朴素贝叶斯几十行代码就能跑出一个基线深度学习方法用 TextCNN、BiLSTM 或 BERT精度能往上抬几个点但代价是训练时间和显存。这份源码加数据集的价值恰恰在于把两条路线放在同一个工程里对比让你看清什么场景该用哪条路。如果你手头有几千条标注语料、想快速搭一个能上线的意图分类器或者正在纠结要不要为了两个点的准确率上 GPU这篇笔记会带你从数据格式一路走到模型评估把参数和坑都摊开讲。2. 传统方法路线TF-IDF 加线性分类器的最小闭环2.1 为什么先跑传统方法而不是直接上 BERT我一般会建议团队先花半天时间把传统基线跑出来原因有三个。第一它能给你一个精度下限后面深度学习模型如果只比它高一个点你就要怀疑是不是数据量不够或者标注噪声太大。第二传统方法的训练时间以秒计你可以快速验证数据格式、标签体系、分词方案是否正确不用等 GPU 排队。第三线上推理时 TF-IDF 加线性模型的延迟通常在毫秒级QPS 要求高的场景反而更实用。意图识别的传统做法核心就两步把句子转成向量再喂给分类器。向量化常用 TF-IDF 或 CountVectorizer分类器常用 LinearSVC、LogisticRegression 或 MultinomialNB。中文场景下分词质量直接决定特征好坏jieba 是最常见的选择但要注意自定义词典——业务专有名词比如「花呗」「工单」如果不加进去会被切碎成无意义的字。2.2 数据格式与标签体系怎么定源码里的数据集一般是 CSV 或 JSON 格式两列text 和 label。label 可以是字符串也可以是整数 ID。我习惯在预处理阶段就把 label 映射成整数同时存一份 id2label 的字典方便后面推理时还原。import pandas as pd from sklearn.preprocessing import LabelEncoder # 读取数据假设是 CSV两列 text 和 label df pd.read_csv(data/intent_data.csv, encodingutf-8) print(df.head()) print(类别分布\n, df[label].value_counts()) # 标签编码 le LabelEncoder() df[label_id] le.fit_transform(df[label]) id2label {i: label for i, label in enumerate(le.classes_)} print(标签映射, id2label)这段代码做了三件事读数据、看类别分布、把字符串标签转成整数。类别分布一定要打印出来如果某个意图只有十几条样本后面训练时要么做数据增强要么直接合并到相近意图里否则模型根本学不会。LabelEncoder的fit_transform会按字母序排列标签所以id2label的映射关系要存下来推理时不能搞反。2.3 分词、TF-IDF 与分类器串联中文分词是传统路线的第一个分水岭。jieba 的cut默认精确模式但意图识别里更推荐用lcut配合自定义词典。下面是一个完整的训练脚本骨架import jieba from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.svm import LinearSVC from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report # 加载自定义词典每行一个词 jieba.load_userdict(data/user_dict.txt) def tokenize(text): # 返回空格分隔的词串供 TfidfVectorizer 使用 return .join(jieba.lcut(text)) df[tokens] df[text].apply(tokenize) # 划分训练集和测试集stratify 保证类别比例一致 X_train, X_test, y_train, y_test train_test_split( df[tokens], df[label_id], test_size0.2, random_state42, stratifydf[label_id] ) # TF-IDF 向量化 vectorizer TfidfVectorizer( max_features5000, # 控制特征维度防止过拟合 ngram_range(1, 2), # 加入二元词组捕捉「退 订单」这类组合 min_df2, # 至少出现两次的词才保留 max_df0.9 # 出现在 90% 以上文档的词丢弃 ) X_train_vec vectorizer.fit_transform(X_train) X_test_vec vectorizer.transform(X_test) # 线性 SVM 训练 clf LinearSVC(C1.0, max_iter2000) clf.fit(X_train_vec, y_train) # 评估 y_pred clf.predict(X_test_vec) print(classification_report(y_test, y_pred, target_namesle.classes_))TfidfVectorizer的几个参数值得展开说。max_features5000是经验值语料在几千到几万条时够用太大反而引入噪声。ngram_range(1,2)对意图识别帮助明显因为很多意图靠词组区分比如「查 天气」和「查 订单」。min_df2过滤掉只出现一次的词这些词多半是噪声或拼写错误。max_df0.9去掉过于通用的词类似停用词的效果。LinearSVC的C参数控制正则强度C 越大越容易过拟合C 越小越偏向欠拟合。意图识别数据量不大时C 取 0.5 到 2 之间比较稳。max_iter设 2000 是为了避免收敛警告如果数据量大可以再调高。2.4 传统路线的评估与保存训练完不能只看 accuracy意图识别是不平衡分类要看每个类别的 precision、recall 和 f1-score。classification_report会直接输出这些指标。如果某个类别的 recall 特别低说明模型把这类样本大量误判成其他类需要检查标注质量或增加该类样本。模型保存用 joblib同时要把 vectorizer 和 label encoder 一起存推理时缺一不可import joblib joblib.dump(vectorizer, model/tfidf_vectorizer.pkl) joblib.dump(clf, model/linear_svc.pkl) joblib.dump(le, model/label_encoder.pkl)推理时先分词再用同一个 vectorizer 做 transform注意不是 fit_transform最后 predict 并用 label encoder 还原标签。这一步顺序错了精度会直接崩掉。3. 深度学习方法路线TextCNN 与 BiLSTM 的工程实现3.1 深度学习意图识别的选型逻辑传统方法的天花板通常在 85% 到 90% 之间再往上走就需要模型理解词序和上下文。深度学习路线里TextCNN 和 BiLSTM 是两个最常用的基线。TextCNN 用不同尺寸的卷积核捕捉 n-gram 特征训练快、参数少适合数据量在几千到几万条的场景。BiLSTM 能建模长距离依赖对「先……再……最后……」这类复杂句式更友好但训练慢一些。BERT 类预训练模型精度最高但需要 GPU 和更多数据如果标注语料少于五千条微调效果未必比 TextCNN 好。这份源码里深度学习部分一般会提供 TextCNN 和 BiLSTM 两种实现用 PyTorch 或 TensorFlow 写。我下面以 PyTorch 为例把关键模块拆开讲。3.2 词表构建与序列填充深度学习模型不直接吃文本要先建词表把每个词映射成整数 ID。词表一般保留频率最高的前 N 个词其余用UNK表示。句子长度不一时用PAD填充到固定长度。from collections import Counter import torch from torch.utils.data import Dataset, DataLoader # 构建词表 all_tokens [jieba.lcut(text) for text in df[text]] word_counter Counter([w for tokens in all_tokens for w in tokens]) vocab {PAD: 0, UNK: 1} for word, count in word_counter.most_common(10000): if count 2: vocab[word] len(vocab) print(词表大小, len(vocab)) # 数据集类 class IntentDataset(Dataset): def __init__(self, texts, labels, vocab, max_len32): self.texts texts self.labels labels self.vocab vocab self.max_len max_len def __len__(self): return len(self.texts) def __getitem__(self, idx): tokens jieba.lcut(self.texts[idx]) ids [self.vocab.get(w, self.vocab[UNK]) for w in tokens] # 截断或填充 if len(ids) self.max_len: ids ids[:self.max_len] else: ids ids [self.vocab[PAD]] * (self.max_len - len(ids)) return torch.tensor(ids), torch.tensor(self.labels[idx])词表大小控制在 1 万到 3 万之间比较常见太小会导致大量UNK太大则 embedding 矩阵浪费显存。max_len32对意图识别通常够用因为用户指令一般很短超过 32 个词的句子很少。如果业务里有长文本可以调到 64 或 128但要注意显存占用。3.3 TextCNN 模型结构与关键参数TextCNN 的核心是多个不同尺寸的卷积核并行提取特征然后池化拼接。下面是一个可直接用的实现import torch.nn as nn import torch.nn.functional as F class TextCNN(nn.Module): def __init__(self, vocab_size, embed_dim, num_classes, filter_sizes, num_filters, dropout0.5): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim, padding_idx0) # 每个尺寸的卷积核各 num_filters 个 self.convs nn.ModuleList([ nn.Conv2d(1, num_filters, (fs, embed_dim)) for fs in filter_sizes ]) self.dropout nn.Dropout(dropout) self.fc nn.Linear(len(filter_sizes) * num_filters, num_classes) def forward(self, x): # x: [batch, seq_len] x self.embedding(x) # [batch, seq_len, embed_dim] x x.unsqueeze(1) # [batch, 1, seq_len, embed_dim] # 每个卷积核输出 [batch, num_filters, seq_len - fs 1, 1] x [F.relu(conv(x)).squeeze(3) for conv in self.convs] # 全局最大池化 x [F.max_pool1d(i, i.size(2)).squeeze(2) for i in x] x torch.cat(x, dim1) x self.dropout(x) return self.fc(x)embed_dim一般取 128 或 256数据量大时可以到 300。filter_sizes常用[2,3,4]对应二元、三元、四元词组特征。num_filters每个尺寸取 64 到 128。dropout0.5是防止过拟合的关键如果训练集小于一万条可以调到 0.6 甚至 0.7。padding_idx0让PAD的 embedding 不参与梯度更新。3.4 训练循环与早停策略训练循环里要监控验证集 loss连续几轮不下降就早停避免过拟合。下面是一个精简版训练脚本from torch.optim import Adam from sklearn.metrics import f1_score device torch.device(cuda if torch.cuda.is_available() else cpu) model TextCNN( vocab_sizelen(vocab), embed_dim128, num_classeslen(le.classes_), filter_sizes[2, 3, 4], num_filters64 ).to(device) optimizer Adam(model.parameters(), lr1e-3) criterion nn.CrossEntropyLoss() best_f1 0.0 patience 3 no_improve 0 for epoch in range(20): model.train() total_loss 0 for batch_x, batch_y in train_loader: batch_x, batch_y batch_x.to(device), batch_y.to(device) optimizer.zero_grad() logits model(batch_x) loss criterion(logits, batch_y) loss.backward() optimizer.step() total_loss loss.item() # 验证 model.eval() preds, trues [], [] with torch.no_grad(): for batch_x, batch_y in val_loader: batch_x batch_x.to(device) logits model(batch_x) preds.extend(logits.argmax(dim1).cpu().tolist()) trues.extend(batch_y.tolist()) val_f1 f1_score(trues, preds, averagemacro) print(fEpoch {epoch1}, loss{total_loss:.4f}, val_f1{val_f1:.4f}) if val_f1 best_f1: best_f1 val_f1 torch.save(model.state_dict(), model/textcnn_best.pt) no_improve 0 else: no_improve 1 if no_improve patience: print(早停触发) break学习率1e-3是 Adam 的常用起点如果 loss 震荡可以降到5e-4。patience3表示验证集 F1 连续三轮不提升就停。保存模型时存state_dict而不是整个模型加载时先实例化结构再load_state_dict这样更灵活。3.5 BiLSTM 的适用场景与实现差异BiLSTM 和 TextCNN 的主要区别在于它按顺序处理序列能捕捉词序信息。实现上把卷积层换成 LSTM 层即可class BiLSTM(nn.Module): def __init__(self, vocab_size, embed_dim, hidden_dim, num_classes, dropout0.5): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim, padding_idx0) self.lstm nn.LSTM(embed_dim, hidden_dim, batch_firstTrue, bidirectionalTrue) self.dropout nn.Dropout(dropout) self.fc nn.Linear(hidden_dim * 2, num_classes) def forward(self, x): x self.embedding(x) out, _ self.lstm(x) # 取最后一个时间步的输出 out out[:, -1, :] out self.dropout(out) return self.fc(out)hidden_dim一般取 128 或 256双向拼接后维度翻倍。BiLSTM 训练比 TextCNN 慢但对手续类、多步指令类意图识别更准。如果数据里句子平均长度超过 20 个词BiLSTM 的优势会更明显。4. 两条路线的对比与选型避坑4.1 精度、速度与数据量的三角权衡把两条路线放在一起对比结论会更清晰维度TF-IDF LinearSVCTextCNNBiLSTMBERT 微调训练时间秒级分钟级分钟级小时级推理延迟毫秒级毫秒级毫秒级十毫秒级数据需求千条级五千条以上五千条以上万条以上典型精度85%90%90%93%91%94%94%97%GPU 依赖无可选推荐必须选型的核心不是追高精度而是看你的数据量和延迟要求。数据少于三千条时传统方法加规则兜底往往比硬上深度学习更稳。数据过万且允许 GPU 推理再考虑 BERT。4.2 避坑与常见问题排查现象一传统方法训练集精度 99%测试集只有 70%。原因TF-IDF 的max_features设太大或者min_df1导致稀有词被当成特征模型记住了噪声。 解决把max_features降到 3000 到 5000min_df设为 2 或 3同时检查是否有重复样本同时出现在训练集和测试集。现象二深度学习模型 loss 不下降一直卡在某个值。原因学习率太大导致震荡或者词表里UNK比例过高embedding 学不到有效表示。 解决把学习率降到1e-4试一轮同时统计UNK占比如果超过 10%说明词表太小或分词有问题需要扩大词表或换分词工具。现象三验证集 F1 波动很大每次训练结果差好几个点。原因数据量小划分训练验证集时类别分布不均匀。 解决用stratify分层抽样同时固定随机种子。如果还是波动做 5 折交叉验证取平均。现象四推理时精度比测试时低很多。原因推理时用了fit_transform而不是transform或者分词词典和训练时不一致。 解决把 vectorizer、label encoder、词表都保存成文件推理时严格加载同一份不要重新 fit。现象五某个意图的 recall 始终为 0。原因该类样本太少或者标注时和其他意图边界模糊。 解决先看混淆矩阵确认它被误判成了哪个类然后要么补充样本要么合并意图要么加规则后处理。5. 把模型推到线上推理封装与持续迭代的一个习惯训练完模型只是开始真正让意图识别产生价值的是推理封装和迭代机制。我一般会写一个统一的Predictor类把传统模型和深度学习模型的推理接口对齐这样线上切换模型时不用改业务代码。class IntentPredictor: def __init__(self, model_type, model_dir): self.model_type model_type if model_type traditional: self.vectorizer joblib.load(f{model_dir}/tfidf_vectorizer.pkl) self.clf joblib.load(f{model_dir}/linear_svc.pkl) self.le joblib.load(f{model_dir}/label_encoder.pkl) else: self.vocab torch.load(f{model_dir}/vocab.pt) self.model TextCNN(...) # 按训练时的参数实例化 self.model.load_state_dict(torch.load(f{model_dir}/textcnn_best.pt)) self.model.eval() def predict(self, text): if self.model_type traditional: tokens .join(jieba.lcut(text)) vec self.vectorizer.transform([tokens]) pred_id self.clf.predict(vec)[0] return self.le.inverse_transform([pred_id])[0] else: tokens jieba.lcut(text) ids [self.vocab.get(w, 1) for w in tokens][:32] ids [0] * (32 - len(ids)) with torch.no_grad(): logits self.model(torch.tensor([ids])) pred_id logits.argmax(dim1).item() return self.id2label[pred_id]这个封装的关键点是传统模型和深度学习模型对外都只暴露predict(text)一个方法业务侧不关心底层用的是哪个。模型文件、词表、标签映射全部从同一个目录加载避免版本错配。上线后一定要记录推理日志尤其是低置信度的样本。我习惯把置信度低于 0.6 的请求单独存一份每周人工过一遍把标注正确的补充进训练集标注错误的用来发现新意图。这个习惯坚持三个月意图识别的准确率通常能再涨三到五个点比换模型管用得多。最后说一个我踩过的坑不要等到模型「完美」了才上线。先用传统方法跑一个能用的版本收集真实流量里的 badcase再用这些数据去训练深度学习模型迭代节奏会快很多。希望帮到你。本文还有配套的精品资源点击获取
返回列表