首页 / 资讯中心 / 文章详情

Transformer单轮对话机器人落地全攻略:数据、模型与推理

Transformer单轮对话机器人落地全攻略:数据、模型与推理 ★ FEATURED ARTICLE
简介这是基于Transformer架构实现的单轮对话聊天机器人Python项目面向计算机相关专业学生、毕业设计及课程设计场景也适合作为NLP入门进阶的实践范本。代码经作者毕业答辩环境完整测试功能运行稳定可直接在本地配置依赖后启动训练与推理流程。压缩包共16个文件、约80KB其中6个py脚本涵盖模型主体、训练入口、数据处理、对话调用等模块vocab.pkl保存词表txt文件提供依赖清单与配置说明abak备份文件留存模型权重与训练日志便于复现调参另附ipynb交互式分析与README使用文档结构清晰便于查改。目前已有50人学习下载。相比网上零散示例这份资源将数据预处理、词表生成、模型训练、推理对话串联成完整闭环并给出依赖安装与环境配置指引适合在此基础上二次开发完成智能客服、对话系统等扩展功能。1. 直接选型而非从头发明Transformer 单轮对话机器人的落地价值基于 Transformer 的单轮对话机器人落地的核心其实不是模型结构而是“数据、代码、模型使用说明”这三样东西能不能焊成一个闭环。我在接到类似需求时最先做的一定不是调参而是先把训练数据整理成模型能直接吃的样子再选一个轻量 Transformer 骨架跑通最后才去优化生成效果。这篇文章就按这个顺序把单轮对话机器人的数据集格式、Python 代码骨架、模型使用说明和最容易踩的几个坑串起来。适合刚入门的工程师对照复现也适合已经跑过分类任务的熟手快速定位自己的问题。2. 数据集构造把原始问答整理成 Transformer 能吃的 src/tgt 格式2.1 单轮对话数据长什么样两种主流采集渠道单轮对话机器人训练集的本质是一堆成对的(src, tgt)文本src是用户输入tgt是系统回复。数据落盘格式我一般用 JSONL每条一行两个字段{src: 你好, tgt: 你好请问有什么可以帮您} {src: 你们营业时间到几点, tgt: 我们每天营业到晚上十点。}字段越短越干净。单轮任务不需要保存对话历史、用户ID、时间戳这些信息在后续扩展多轮时再补训练阶段只会让模型学到无关噪声。第一类数据来源是真实对话日志。客服后台或智能音箱日志里一次会话可能包含多条消息。配对的规则是取用户说完的最后一条 utterance对应紧接着出现的系统回复。如果用户连发“在吗”“能听到吗”系统才回复“您好”这时请只保留最后一条用户输入“能听到吗”作为src不要合并成“在吗 能听到吗”。合并后的句子词序混乱模型会学到奇怪的表达习惯。还有一种情况是系统一次回复了多句话比如“您稍等”后面又跟了知识库答案。这种多句回复在单轮任务里会严重破坏tgt的结构我的处理方式是截断到第一个句号或第一句话只保留核心应答。第二类来源是人工整理的意图模板。比如“你好”“您好”“在吗”“请问有人吗”统一对应“您好请问有什么可以帮您”人工模板至少要覆盖业务里最常用的 50 个意图。写模板时的关键不是数量多而是同义改写要充分。你会发现用户不太可能照着你写的句式原样提问所以同一个意图最好有 5 条以上不同表达方式。这里有个非常容易翻车的地方如果某个回复模板写得特别多比如“好的呢”出现 500 次其他回复只有几十次模型训练完就会变成只会说“好的呢”的复读机。要在数据组装阶段对tgt做频率统计任何单条回复的占比不要超过 10%超了就降采样。2.2 清洗与去重用 Python 脚本把日志变成可训练的数据集拿到原始 JSONL 后不能直接喂给模型。我得先跑一遍清洗脚本把 URL、电话号码、多余空白、纯重复回声都处理掉。这里给一个可以照着改的版本import json import re def clean_text(text: str) - str: # 用占位 token 替换噪声而不是直接删掉 text re.sub(rhttps?://\S, [URL], text) text re.sub(r\d{11}, [PHONE], text) text re.sub(r\s, , text).strip() return text def build_pairs(raw_lines, max_src_len50, max_tgt_len50): pairs [] for line in raw_lines: item json.loads(line) src clean_text(item[src]) tgt clean_text(item[tgt]) # 过滤过短输入比如只回复了一个嗯 if len(src) 2 or len(tgt) 2: continue # 单轮回复不需要长文本超过长度直接丢弃 if len(src) max_src_len or len(tgt) max_tgt_len: continue # 短回声样本会让模型学会复读 if src tgt and len(src) 4: continue pairs.append({src: src, tgt: tgt}) return pairs def dedupe(pairs): seen set() out [] for p in pairs: key p[src] \u0001 p[tgt] if key not in seen: seen.add(key) out.append(p) return out if __name__ __main__: with open(raw_dialog.jsonl, r, encodingutf-8) as f: raw_lines f.readlines() pairs build_pairs(raw_lines) pairs dedupe(pairs) print(f清洗后剩余 {len(pairs)} 对)这段代码有几个设计点值得说明。第一URL 和电话用[URL]、[PHONE]占位而不是直接删除。原因是如果直接删掉模型会看到“请访问 获取详情”这种语法断裂的句子用占位 token 可以让模型学到“遇到链接时做个标记不要复读链接内容”这件事。第二src tgt且长度小于 4 的样本必须删典型就是用户说“嗯”系统回“嗯”这种回声对会教会模型复读当前输入。第三去重时把src和tgt用\u0001这个不可见字符拼起来做 key避免“src 相同但 tgt 不同”的数据被误删也避免“你好”“您好”和“你好您”“好”这类拼接撞车。清洗完数据后还需要构建词表并划分训练集。中文单轮对话我一般按字切分因为对话里新词和新错别字多按字切词表小、OOV 少模型收敛也快from collections import Counter def build_vocab(pairs, max_vocab30000): counter Counter() for p in pairs: counter.update(p[src]) counter.update(p[tgt]) vocab [[PAD], [UNK], [BOS], [EOS]] for w, _ in counter.most_common(max_vocab - len(vocab)): vocab.append(w) word2idx {w: i for i, w in enumerate(vocab)} return vocab, word2idx def encode(text, word2idx, max_len50): # 单轮对话是短文本超过 max_len 直接截断 chars [word2idx.get(ch, word2idx[[UNK]]) for ch in text[:max_len]] return [word2idx[[BOS]]] chars [word2idx[[EOS]]] def split_pairs(pairs, train_ratio0.8, val_ratio0.1): n len(pairs) train pairs[:int(n * train_ratio)] val pairs[int(n * train_ratio):int(n * (train_ratio val_ratio))] test pairs[int(n * (train_ratio val_ratio)):] return train, val, test[BOS]和[EOS]是两个非常重要的边界 token。src用它们包裹是为了让 Transformer 知道输入序列从哪里开始、到哪里结束tgt用它们包裹是因为解码器在训练时要根据前一个 token 预测下一个 token[BOS]作为起始符[EOS]作为停止符。数据切分建议按 8:1:1 分成 train/val/testval 用来做早停test 只在最终验证时碰一次。你要留意哦很多项目直接把数据全量丢进去训练最后看不出过拟合就是吃了没切 val 的亏。3. 模型实现一个轻量 Transformer 的 PyTorch 骨架3.1 选型为什么不选 LSTM也不用纯 Decoder-Only单轮对话机器人最常用的模型结构是序列到序列Seq2Seq输入一句src解码器逐字生成一句tgt。如果你以前接触过 transformer 分类任务那么换到对话生成时最大的差别在输出端分类任务在序列末尾接一个分类头而生成任务需要一个自回归的解码器每个时间步预测下一个 token。LSTM 编码器一辈子也可以做但有两个落地麻烦。第一LSTM 是串行处理序列的src长度一超过 30 个token前端的语义就容易被长距离遗忘第二训练速度比 Transformer 慢好几倍同样一块显卡Transformer 能跑的 batch 更大。这些特点在单轮任务上尤其突出因为用户输入虽短但表达跳脱注意力机制比循环链路更擅长抓住关键词。Decoder-Only 的 GPT 结构能不能做能。做法是把src和tgt拼成一段文本去训练比如“用户你好\n系统您好”。但落地时有个麻烦生成阶段需要你自己维护“用户”和“系统”这两个分隔符稍不注意模型会学着生成出第二段“用户”把对话角色搞乱。Encoder-Decoder 结构把输入和输出从结构上分开了报错也更直观。所以对于单轮对话这个具体任务我选择用 PyTorch 自带的nn.Transformer搭一个轻量骨架下面这段代码可以直接跑通。3.2 轻量 Transformer 的 PyTorch 实现位置编码与掩码是核心我一般会用 PyTorch 的nn.Transformer组件而不是从零写多头注意力因为nn.MultiheadAttention内部已经处理了key_padding_mask和attn_mask的组合逻辑少踩很多坑。这里给出极简实现import torch import torch.nn as nn import math class PositionalEncoding(nn.Module): def __init__(self, d_model, max_len256): super().__init__() pe torch.zeros(max_len, d_model) position torch.arange(0, max_len).float().unsqueeze(1) div_term torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model)) pe[:, 0::2] torch.sin(position * div_term) pe[:, 1::2] torch.cos(position * div_term) self.register_buffer(pe, pe.unsqueeze(0)) def forward(self, x): return x self.pe[:, :x.size(1)] class TransformerSeq2Seq(nn.Module): def __init__(self, src_vocab_size, tgt_vocab_size, d_model128, nhead4, num_encoder_layers2, num_decoder_layers2, max_len64, dropout0.1): super().__init__() self.src_embed nn.Embedding(src_vocab_size, d_model) self.tgt_embed nn.Embedding(tgt_vocab_size, d_model) self.pos PositionalEncoding(d_model, max_len) self.transformer nn.Transformer( d_modeld_model, nheadnhead, num_encoder_layersnum_encoder_layers, num_decoder_layersnum_decoder_layers, dropoutdropout, batch_firstTrue) self.fc_out nn.Linear(d_model, tgt_vocab_size) def forward(self, src, tgt, tgt_mask, src_key_padding_maskNone, tgt_key_padding_maskNone): src_emb self.pos(self.src_embed(src) * math.sqrt(self.src_embed.embedding_dim)) tgt_emb self.pos(self.tgt_embed(tgt) * math.sqrt(self.tgt_embed.embedding_dim)) memory self.transformer.encoder(src_emb, src_key_padding_masksrc_key_padding_mask) out self.transformer.decoder(tgt_emb, memory, tgt_masktgt_mask, tgt_key_padding_masktgt_key_padding_mask, memory_key_padding_masksrc_key_padding_mask) return self.fc_out(out) def make_tgt_mask(tgt): # 上三角掩码当前位置只能看到左侧的 token device tgt.device seq_len tgt.size(1) return torch.triu(torch.ones(seq_len, seq_len, devicedevice), diagonal1).bool()代码里有三个容易忽视的细节。第一embedding输出后乘了sqrt(d_model)这是 Transformer 原论文里的做法目的是把词嵌入的向量范数放大到与位置编码同一量级否则相加时位置编码会被淹没。第二batch_firstTrue让输入张量形状是(batch, seq_len, d_model)而不是 PyTorch 默认的(seq_len, batch, d_model)新手用默认格式时经常在 reshape 上花两小时。第三make_tgt_mask生成的是 bool 类型的上三角矩阵nn.Transformer会自动把True的位置替换成负无穷这比你自己在注意力分数里做masked_fill更靠谱。参数选择上d_model128、nhead4、num_layers2是我对单轮任务的数据量做的默认估计。业务语料通常只有几万到几十万对模型不需要很大。如果团队语料超过百万对再考虑把d_model提到 256、层数加到 3 或 4。这里给一个实际会用的参数参考表参数推荐范围说明d_model128~256单轮语料小大模型容易过拟合nhead4~8必须能被 d_model 整除num_encoder_layers2~3层数加深对收敛帮助有限num_decoder_layers2~3与 encoder 保持一致即可dropout0.1~0.2主要防高频回复过拟合max_len32~64单轮回复短过长会发散3.3 Batch 封装把变长文本统一成固定形状模型需要的输入是一批定长的整数矩阵所以数据加载时要做 padding 和 mask。我用 PyTorch 的DataLoader配合一个collate_fn来做。不要嫌这段基础很多生成质量不理想问题就出在 padding 位置没有做 maskdef collate_fn(batch, pad_idx0): src_list [item[src] for item in batch] tgt_list [item[tgt] for item in batch] src_lens [len(s) for s in src_list] tgt_lens [len(t) for t in tgt_list] max_src max(src_lens) max_tgt max(tgt_lens) src_ids torch.full((len(batch), max_src), pad_idx, dtypetorch.long) tgt_ids torch.full((len(batch), max_tgt), pad_idx, dtypetorch.long) for i, (s, t) in enumerate(zip(src_list, tgt_list)): src_ids[i, :len(s)] torch.tensor(s) tgt_ids[i, :len(t)] torch.tensor(t) src_mask (src_ids pad_idx) tgt_mask (tgt_ids pad_idx) return src_ids, tgt_ids, make_tgt_mask(tgt_ids), src_mask, tgt_maskcollate_fn返回五个对象src_ids、tgt_ids、tgt_mask、src_key_padding_mask、tgt_key_padding_mask。其中src_mask是True的位置表示 paddingTransformer 内部会在注意力计算时把这里置为负无穷。要特别注意这个 padding mask 不会传给make_tgt_mask因为make_tgt_mask只处理序列内部的因果关系它的形状是(seq_len, seq_len)而key_padding_mask的形状是(batch, seq_len)两者作用完全不同。4. 训练与推理从 loss 曲线到 beam search 的参数怎么调4.1 训练循环batch size、学习率、warmup 和梯度裁剪模型和数据都就绪后下一步是写训练循环。单轮对话生成是一个语言建模任务损失函数用交叉熵但计算时要把tgt做一次错位解码器的输入是tgt[:-1]预测的目标是tgt[1:]。也就是说给模型看“你好 [EOS]”的左边部分让它预测“[BOS] 你好 [EOS]”的右边部分。def train_one_epoch(model, dataloader, optimizer, criterion, clip1.0): model.train() total_loss 0.0 for src_ids, tgt_ids, tgt_mask, src_pad_mask, tgt_pad_mask in dataloader: tgt_input tgt_ids[:, :-1] tgt_label tgt_ids[:, 1:] tgt_mask tgt_mask[:, :tgt_input.size(1), :tgt_input.size(1)] logits model(src_ids, tgt_input, tgt_mask, src_key_padding_masksrc_pad_mask, tgt_key_padding_masktgt_pad_mask[:, :-1]) loss criterion(logits.reshape(-1, logits.size(-1)), tgt_label.reshape(-1)) optimizer.zero_grad() loss.backward() nn.utils.clip_grad_norm_(model.parameters(), max_normclip) optimizer.step() total_loss loss.item() return total_loss / len(dataloader)训练参数里最值得花心思的是学习率和 batch size 的配合。我经常看到有人把学习率设成1e-4batch size 设成 8结果 loss 慢吞吞地降。对d_model128这样的小模型Adam 学习率1e-3是常见起点配合 warmup 前 500 步从 0 线性升到目标值可以避免早期因梯度方向剧烈变化导致的训练震荡。batch size 建议从 32 起步如果你的显存能放下 64 或 128优先用大的因为更大的 batch 能让梯度估计更准训练曲线更顺滑。梯度裁剪max_norm1.0几乎是必选项。Transformer 在短文本上偶尔会出现一个极端的样本让梯度的某个分量变得特别大一次更新就把模型参数推飞loss 直接冲到 NaN。如果发现训练到一半 loss 突然变nan第一反应不是调学习率而是确认是否加了梯度裁剪。训练轮数要克制。单轮对话任务的多样性和数据量远不如开放域对话模型很容易在 10 个 epoch 之内把高频回复背下来。我的习惯是每个 epoch 结束都在验证集上算一次 loss连续 3 个 epoch 验证 loss 不降就早停。如果 train loss 还在降、val loss 已经反弹那就是典型的过拟合信号这时候不需要改模型结构把 dropout 调到 0.2 或削减一层解码器往往就稳住了。4.2 推理贪心解码不够用温度和重复惩罚才关键训练完成后推理阶段的做法和训练完全不同。训练时模型看到的是完整的tgt推理时只能从[BOS]开始逐个生成 token。最简单的是贪心解码每一步取概率最高的词拼接上去但这样生成的回复往往平淡、重复且不自然。更实用的做法是带温度的采样并控制最大生成长度。下面是一个可以直接验证效果的generate函数def generate(model, src_ids, bos_idx2, eos_idx3, max_len32, temperature0.8): model.eval() src_ids src_ids.unsqueeze(0) src_mask torch.zeros_like(src_ids, dtypetorch.bool) memory model.transformer.encoder(model.pos(model.src_embed(src_ids) * math.sqrt(model.src_embed.embedding_dim)), src_key_padding_masksrc_mask) tgt_ids torch.tensor([[bos_idx]], devicesrc_ids.device) with torch.no_grad(): for _ in range(max_len): tgt_mask make_tgt_mask(tgt_ids) logits model.fc_out( model.transformer.decoder( model.pos(model.tgt_embed(tgt_ids) * math.sqrt(model.tgt_embed.embedding_dim)), memory, tgt_masktgt_mask)) next_logits logits[:, -1, :] / temperature next_id torch.multinomial(torch.softmax(next_logits, dim-1), num_samples1) tgt_ids torch.cat([tgt_ids, next_id], dim1) if next_id.item() eos_idx: break return tgt_ids这里面最值得调的两个参数是temperature和max_len。temperature默认 1.0但我会设成 0.7~0.9因为客服或助手场景里回答需要稳定低温度让概率分布变得更尖锐不太容易产生“好的呢呀哈”这样的发散采样。如果你发现回复还是偏随机就继续降到 0.6如果发现回复总是同一句话就升回 0.9。max_len设 32 对单轮回复足够了再长的话模型容易在句子结束后继续编输出“你好你好你好”这种复读。注意我这里用了torch.multinomial而不是argmax这就是采样生成和贪心解码的区别采样会保留一些随机性让同一句输入产生不同表达但温度控制不好时也会牺牲准确性。如果想要更高质量的回复可以接 beam search。但 beam search 在这类短文本任务上的收益没有想象中大而且容易把多条候选都收敛到同一个句式。我用 beam size3 配length_penalty0.8效果优于 beam size5因为单轮回复短过大的束宽只会增加重复。还要加一个简单的重复惩罚如果某个 token 在输出中已经出现了两次就把它的分数乘 0.5否则模型很容易在 beam 里反复横跳同一个词。5. 避坑单轮对话 Transformer 最容易翻车的 5 个问题5.1 数据侧翻车loss 降得很好看线上回复全是“嗯嗯”“好的”现象是训练曲线一路走低验证 loss 也能到 1.5 以下但把模型接到测试页面上随便问什么它都回“嗯嗯”或“好的呢”。原因很简单数据里这类无信息量回复占比太高交叉熵损失被它们主导模型发现只要预测“嗯嗯”就能把 loss 压下去于是放弃了所有语义信息。解决办法是回头统计tgt的频率把占比超过 10% 的短回复要么对高频项降采样要么直接删除再重新训练。我一般会保留训练前后各一条“你好”模板但把“嗯嗯”“好的呢”这类回复数量压到每类不超过 30 条。5.2 模型结构翻车训练到一半 loss 变成 nan或者推理时显存溢出训练 loss 突然变 nan最直接的原因是梯度爆炸尤其是学习率偏大、batch size 偏小的时候。解决方式是加梯度裁剪并降低学习率常见做法是在优化器 step 之前调用clip_grad_norm_同时把 warmup 步数拉长。推理时显存溢出则多半是src输入过长导致的。nn.Transformer的注意力矩阵是输入长度的平方复杂度用户粘贴了一篇文章进来显存肯定受不了。在服务端要做两层限制一是在进入模型前截断src到 64 个 token二是用src_key_padding_mask把 padding 位置挡住避免模型注意到大量无效位置。5.3 数据清洗不彻底模型学会输出 URL 和乱码如果清洗阶段没有把 URL、电话号码、邮箱替换成[URL]这类占位 token模型很容易在生成结果里带出“请访问 https://xxx”这种内容。原因是 URL 属于高频出现的复杂 tokenTransformer 对它的局部模式很敏感却完全不理解它的语义。解决方式其实在数据章节已经做了就是clean_text里的正则替换。在真实日志里还要多一条规则把“【”“】”“★★★★”这类装饰符替换为空格。否则模型会学到在回复前面加一串星号好像这样更像真人客服。5.4 推理翻车beam search 返回 5 个一模一样的句子beam search 在单轮回复上最常见的翻车现场是beam size 设成 5最后返回的候选里 3 个一模一样另外 2 个只是标点不同。原因是 beam search 的多样性不足当某个片段概率突出时它会沿着同一个高分路径走下去。解决方式是加一个长度惩罚和 n-gram 重复惩罚。如果你用的是 HuggingFace 的generate方法直接把num_beams3、no_repeat_ngram_size2、length_penalty0.8三个参数一起设上如果还在重复就退回采样生成用温度 0.8 配top_p0.9通常比 beam 更自然。5.5 词表修改翻车训练完了想加两个词加载权重直接 size mismatch这个问题几乎每个做生成任务的人都会碰到。训练完模型后发现业务方需要加入新的专属词于是直接在词表里加了两个 token结果torch.load时报 embedding 维度对不上。原因是模型输出层fc_out和tgt_embed的维度都绑定在tgt_vocab_size上词表一变权重就无法加载。我的做法是第一训练开始前就把所有会用到的特殊 token 全部放进去包括[URL]、[PHONE]、[TITLE]等一次性定死第二如果非要用新词就重新初始化新行对应的 embedding并只冻结旧 embedding 行训练而不是直接加载旧权重。6. 把模型接进生产一个 40 行验证脚本与三个自检技巧6.1 快速验证脚本加载权重跑通一句话输入训练好的模型要落地上线最好先写一个 40 行左右的独立推理脚本。这个脚本不接 Web 服务只做一件事读入用户输入返回模型回复。它能帮你快速判断模型是否被正确保存、词表是否对齐、推理速度是否达标。import json import torch from model import TransformerSeq2Seq, make_tgt_mask def load_model_and_vocab(model_path, vocab_path, device): word2idx json.load(open(vocab_path, encodingutf-8)) model TransformerSeq2Seq(len(word2idx), len(word2idx)) model.load_state_dict(torch.load(model_path, map_locationdevice)) model.to(device).eval() return model, word2idx def chat(model, word2idx, text, device, max_len32, temperature0.8): ids [word2idx.get(ch, word2idx[[UNK]]) for ch in text[:50]] ids [word2idx[[BOS]]] ids [word2idx[[EOS]]] src_ids torch.tensor(ids, dtypetorch.long).to(device) tgt_ids generate(model, src_ids, temperaturetemperature, max_lenmax_len) out_chars [] for tid in tgt_ids[0].tolist(): if tid in (word2idx[[BOS]], word2idx[[EOS]], word2idx[[PAD]]): continue out_chars.append([k for k, v in word2idx.items() if v tid][0]) return .join(out_chars) if __name__ __main__: device torch.device(cuda if torch.cuda.is_available() else cpu) model, word2idx load_model_and_vocab(model.pt, vocab.json, device) while True: text input(请输入你的问题) if text.strip() in (exit, quit): break print(chat(model, word2idx, text, device))验证脚本里最容易被忽略的细节是把model.eval()放到加载权重之后、循环推理之前。如果不切到 eval 模式dropout层还会随机丢弃信息用户会感觉模型每次回答都不太一样。还有就是vocab.json里存的word2idx要和训练时完全一致我最常踩的坑是在训练后重新 dump 了一次词表顺序变了导致所有 token 错位。建议训练结束后把词表固化到单独文件推理时不再跑build_vocab。6.2 三个自检技巧自问自答、反向匹配、盲测第一个技巧是自问自答。拿训练集里的src让模型生成回复看它是否能复现较合理的答案再拿 20 条从没见过的测试输入让模型生成看语义是否通顺。如果训练集里的输入都被背得很好但新输入一塌糊涂说明过拟合需要加强清洗或降低模型容量。第二个技巧是反向匹配。让模型生成 20 条回复检查每条回复里有没有大量重复 token、有没有 URL、有没有和src完全无关的句子。我写过一个简单脚本统计每条回复里重复出现的字符数超过 30% 就标记为可疑样本。这个指标比 BLEU 更能反映用户实际观感。第三个技巧是人工盲测。找一个纯规则匹配的基线系统把模型回复和基线回复打乱顺序让同事打分维度只有三个是否通顺、是否有用、是否泄露敏感信息。不要在文本编辑器里对比两个输出那样你会不自觉偏向新模型。盲测分数不会骗人如果模型版与规则版差距不大问题多半出在数据质量而不是模型结构。6.3 面向生产的三条扩展路径如果验证通过可以往三个方向扩展。第一个是检索增强把生成模型的回复当作候选和倒排索引召回的语料候选混合再做一个排序层兼顾生成的灵活性和检索的准确性。第二个是外壳模板比如业务要求回复必须以“您好”开头模型只生成中间的槽位内容外层用模板拼接这能保证商业礼仪和品牌话术的稳定性。第三个是向多轮扩展不要一股脑把全部历史拼进src而是取最近两轮对话加当前输入做成带窗口的序列否则模型会学到把用户历史问题也回进去。我最早做单轮对话机器人时把全部精力放在调模型结构上试过加深解码器、换注意力头数效果都不如回头清洗一遍数据来得明显。后来我给自己定了个规矩先花一天时间看数据样本再看 loss 曲线最后才碰模型结构。如果你也在做类似的项目不妨先从数据集构造入手把tgt频率平衡和噪声过滤做扎实你会发现自己省下的调参时间远超预期。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站