ARTICLE DETAIL

资讯详情

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

LSTM+Attention中文聊天机器人教学实现

LSTM+Attention中文聊天机器人教学实现 简介这是一份面向机器学习初学者与高校课程设计实践者的中文聊天机器人项目资源聚焦注意力机制在自然语言处理中的落地应用帮助学习者理解并复现端到端对话系统建模流程。资源共22个文件包含3个核心Python脚本训练、推理、数据预处理、4个Jupyter Notebook含带/不带注意力机制的对比实验、3个.pkl词汇映射文件、3个.npy对话数据文件、1个.h5预训练模型及配套字体与图像资源整体压缩包58.86MB结构清晰开箱即用。已有128人学习下载适合NLP入门者快速体验注意力机制对生成质量的提升效果。用户可直接运行预训练模型进行中文对话交互深入分析模型架构差异如chatbot_inference_Attention.ipynb与non-Attention版本对比掌握中文分词、词表构建、padding处理等关键预处理环节并基于tsv格式的qingyun.tsv对话数据集开展二次训练或优化实验。1. 这不是“调个 API 就完事”的聊天机器人它用纯 LSTM Attention 实现中文对话生成不依赖 Transformer、不调用大模型接口模型文件仅 87MBCPU 推理延迟 ≤1.2si5-8250U适合课程设计复现与 NLP 入门者亲手拆解注意力机制的黑匣子你见过那种“下载即运行打开就聊”的中文聊天机器人吗不是调 OpenAI 或千问 API 的壳子也不是用 LangChain 拼凑的 demo而是从数据清洗、词表构建、序列 padding、LSTM 编码器-解码器架构到手写 Attention 权重计算、上下文向量拼接、beam search 解码——全链路可读、可断点、可单步调试的完整实现。这个项目就是一个基于原始 Seq2Seq Bahdanau Attention 的轻量级中文对话系统所有代码在chatbot_train.ipynb和chatbot_inference_Attention.ipynb里摊开写模型权重存于W--184-0.5949-.h5注意后缀是.h5不是.pt或.bin训练数据来自qingyun.tsv青云语料约 3.2 万轮 QA 对连字体都配好了simkai.ttf——为的是在image*.png里渲染中文回复时不乱码。它不是工业级产品但恰恰因此成了目前 GitHub 上少有的、能让本科生三天内跑通并真正看懂“注意力到底在哪算、怎么反向传播、为什么能缓解长程遗忘”的教学级范本。如果你正卡在“Attention 公式会背但不知道score tanh(encoder_out W1 decoder_hidden W2) V这三行矩阵乘法在实际对话中对应哪句话”那这份资源就是你的后悔药。2. 从 qingyun.tsv 到 pad_question.npy中文对话数据预处理全流程含分词陷阱、OOV 处理逻辑与 padding 长度的血泪经验2.1 数据源结构解析qingyun.tsv 不是 raw text而是带显式对话轮次标记的结构化语料qingyun.tsv是典型的 tab-separated QA 对每行格式为问\t答 今天天气怎么样\t还不错阳光很好。 你叫什么名字\t我是小智很高兴认识你注意无空行、无 ID 字段、无多轮上下文即每轮都是独立的单轮问答single-turn。这意味着模型学的是“给定一个问题生成一个合理回答”而非多轮对话状态追踪。项目未使用transformer或stateful LSTM所以get_data.ipynb中的预处理逻辑完全围绕单轮对齐展开——这是理解后续 Attention 计算范围的前提Attention Score 矩阵尺寸是(decoder_step, encoder_seq_len)而encoder_seq_len正是由qingyun.tsv中最长问句决定的。2.2 中文分词与词表构建不用 jieba 默认模式而用字符级切分 手动过滤项目未引入外部分词库如 jieba、pkuseg而是直接采用字符级character-level编码# 来自 get_data.ipynb 片段 def build_vocab(sentences): vocab {PAD: 0, UNK: 1, GO: 2, EOS: 3} idx 4 for sent in sentences: for char in sent: if char not in vocab and char.strip(): # 过滤空格、换行符 vocab[char] idx idx 1 return vocab提示字符级虽简单但导致词表极大vocab_bag.pkl解压后含 4217 个字符且无法识别“苹果手机”这类复合词。但好处是彻底规避了分词歧义如“南京市长江大桥”切分错误对课程设计而言牺牲泛化性换可控性是合理取舍。2.3 Padding 与索引映射pad_word_to_index.pkl 和 pad_index_to_word.pkl 的双向绑定逻辑预处理后生成两个核心 pickle 文件pad_word_to_index.pkl字典{char: int_index}用于将句子转为数字序列pad_index_to_word.pkl字典{int_index: char}用于将模型输出的 logits 转回汉字关键参数在get_data.ipynb中硬编码MAX_Q_LEN 30 # 问句最大长度超长截断 MAX_A_LEN 30 # 答句最大长度同上 PAD_IDX 0 # PAD 对应索引用于后续 mask所有句子经pad_sequences后统一为(batch_size, MAX_Q_LEN)形状注意padding 位置在右侧post-padding这直接影响 Attention 中的 mask 构造——attention_mask是一个(batch_size, MAX_Q_LEN)的 0/1 矩阵其中 0 表示 PAD 位置需在 score 计算后置为-inf再 softmax否则 PAD 会参与权重分配。2.4 生成 numpy 数据文件pad_question.npy / pad_answer.npy / answer_o.npy 的物理含义这三个.npy文件是训练数据的最终形态文件名shape含义关键细节pad_question.npy(N, 30)N 条问句的数字序列dtypeint32值域[0, 4216]pad_answer.npy(N, 30)N 条答句的数字序列含GO开头每条答句首元素恒为2GOanswer_o.npy(N, 30)pad_answer.npy的右移版本即 label首元素为第一个真实字末元素为EOS用于计算 loss注意answer_o.npy是监督信号ground truth其第 t 步预测目标是answer_o[:, t]而 decoder 输入是pad_answer[:, :t]—— 这是 teacher forcing 标准做法也是 Attention 在 decoder step t 时能“看到”整个 encoder 输出的原因。3. 模型架构拆解LSTM Encoder-Decoder Bahdanau Attention 的 Keras 实现逐层解释权重形状与前向逻辑3.1 Encoder双层 Bidirectional LSTM输出 shape(batch, seq_len, units*2)Encoder 定义在chatbot_train.ipynb的build_encoder()函数中def build_encoder(vocab_size, embedding_dim, hidden_units): encoder_inputs Input(shape(MAX_Q_LEN,)) encoder_embedding Embedding(vocab_size, embedding_dim)(encoder_inputs) # 第一层 BiLSTM encoder_lstm1 Bidirectional(LSTM(hidden_units, return_sequencesTrue, return_stateTrue, dropout0.2))(encoder_embedding) # encoder_lstm1[0] 是 output, [1:] 是 (h1, c1, h2, c2) encoder_output, forward_h1, forward_c1, backward_h1, backward_c1 encoder_lstm1 # 第二层 BiLSTM输入为上层 output encoder_lstm2 Bidirectional(LSTM(hidden_units, return_sequencesTrue, return_stateTrue, dropout0.2))(encoder_output) encoder_output2, forward_h2, forward_c2, backward_h2, backward_c2 encoder_lstm2 # 最终 encoder state拼接两层最后时刻的 hidden state encoder_state_h Concatenate()([forward_h2, backward_h2]) encoder_state_c Concatenate()([forward_c2, backward_c2]) return Model(encoder_inputs, [encoder_output2, encoder_state_h, encoder_state_c])hidden_units256故encoder_output2.shape (batch, 30, 512)BiLSTM 双向拼接encoder_state_h.shape (batch, 512)作为 decoder 初始 hidden state关键点encoder_output2是 Attention 的 Key/Value 来源不是最后一层 LSTM 的 final state而是整个序列的隐状态矩阵——这正是 Bahdanau Attention 的设计对每个 decoder step动态加权encoder_output2的所有时间步。3.2 Attention Mechanism手写 Bahdanau Score 函数三组可学习权重 W1/W2/V 的物理意义Attention 层未调用tf.keras.layers.Attention而是手动实现class BahdanauAttention(tf.keras.layers.Layer): def __init__(self, units): super().__init__() self.W1 tf.keras.layers.Dense(units) # (512, units) self.W2 tf.keras.layers.Dense(units) # (512, units) self.V tf.keras.layers.Dense(1) # (units, 1) def call(self, query, values): # query: (batch, hidden_size) - (batch, 1, hidden_size) query_with_time_axis tf.expand_dims(query, 1) # score: (batch, seq_len, 1) score self.V(tf.nn.tanh(self.W1(values) self.W2(query_with_time_axis))) # attention_weights: (batch, seq_len, 1) attention_weights tf.nn.softmax(score, axis1) # context_vector: (batch, hidden_size) context_vector attention_weights * values context_vector tf.reduce_sum(context_vector, axis1) return context_vector, attention_weightsvalues即encoder_output2shape(batch, 30, 512)W1(values)投影为(batch, 30, units)W2(query)投影为(batch, 1, units)相加后 tanh →(batch, 30, units)V(...)压缩为(batch, 30, 1)softmax 后得 attention weights物理意义W1学习如何将 encoder 隐状态映射到 query 相容空间W2将 decoder hidden 映射过去V是最终打分函数。三者联合训练使模型学会“问句中‘天气’二字重要就给 encoder 输出中对应位置高权重”。3.3 Decoder单层 LSTM Attention Context 拼接输出层用 TimeDistributed DenseDecoder 输入为pad_answer.npy含GO输出为answer_o.npy右移版decoder_inputs Input(shape(MAX_A_LEN,)) decoder_embedding Embedding(vocab_size, embedding_dim)(decoder_inputs) # 每个 decoder step 的 input 是 embedding context_vector # context_vector 来自上一步 Attention 计算结果 decoder_lstm LSTM(hidden_units*2, return_sequencesTrue, return_stateTrue, dropout0.2) decoder_outputs, _, _ decoder_lstm(decoder_embedding, initial_stateencoder_state) # 拼接 Attention context vector需在每个 timestep 计算 # 实际代码中用 tf.keras.layers.RNN 自定义 cell 实现 step-wise attention # 最终 decoder_outputs.shape (batch, 30, 512) output TimeDistributed(Dense(vocab_size, activationsoftmax))(decoder_outputs)关键创新decoder_outputs并非直接接 softmax而是先与context_vector拼接Concatenate()([decoder_outputs, context_vectors])再过 Dense 层——这确保每步预测都融合了当前 decoder hidden 与全局 encoder 信息。TimeDistributed保证 Dense 层在时间维度上共享权重输出(batch, 30, vocab_size)与answer_o.npy一一对应。4. 训练与推理分离train.ipynb 与 inference.ipynb 的职责边界以及 .h5 模型文件的加载陷阱4.1 训练脚本 chatbot_train.ipynb 的核心配置与收敛表现训练超参在 notebook 中明确写出BATCH_SIZE 64 EPOCHS 200 LEARNING_RATE 0.001 OPTIMIZER Adam(learning_rateLEARNING_RATE) LOSS SparseCategoricalCrossentropy(from_logitsFalse) # 因 softmax 已激活模型在EPOCH 184达到 val_loss0.5949见模型文件名W--184-0.5949-.h5之后开始过拟合训练耗时约 6 小时GTX 1060 6GB未使用 learning rate scheduler 或 early stopping靠人工观察 loss 曲线停训callbacks仅含ModelCheckpoint保存weights_onlyTrue的.h5文件不含 optimizer state —— 这意味着你无法从中 resume training只能 inference4.2 推理脚本 chatbot_inference_Attention.ipynb 的三步加载法.h5模型不能直接load_model()因含自定义层BahdanauAttention# 正确加载方式必须指定 custom_objects custom_objects {BahdanauAttention: BahdanauAttention} model load_model(models/W--184-0.5949-.h5, custom_objectscustom_objects, compileFalse) # compileFalse 避免找不到 loss 函数compileFalse是关键.h5中无 optimizer 配置强行 compile 会报Unknown loss function加载后需手动model.compile(loss..., optimizer...)仅用于验证inference 时无需 compilechatbot_inference_Attention.ipynb中predict_step()函数实现 greedy decodingdef predict_step(input_sentence): inputs preprocess_sentence(input_sentence) # 字符转 index pad enc_out, enc_h, enc_c encoder_model(inputs) # 得到 encoder 输出 dec_input np.array([[word_to_index[GO]]]) # 起始 token result for t in range(MAX_A_LEN): predictions, dec_h, dec_c decoder_model([dec_input, enc_out, enc_h, enc_c]) predicted_id np.argmax(predictions[0][0]) # 取概率最高字 if predicted_id word_to_index[EOS]: break result index_to_word[predicted_id] dec_input np.array([[predicted_id]]) # 下一 step 输入 return result4.3 Attention 可视化image2.png 中 heatmap 的生成逻辑与解读方法image2.png是chatbot_inference_Attention.ipynb中plot_attention()函数输出X 轴问句字符如“今天天气怎么样”共 8 字Y 轴答句生成过程如“还不错”共 3 字颜色深浅Attention weight 值越深表示 decoder 当前 step 越关注 encoder 对应位置典型 pattern答句首字“还”常高亮问句“天气”第二字“不”高亮“怎么样”证明 Attention 确实建模了语义对齐——这不是玄学是矩阵运算的可解释结果。5. 避坑指南五个真实翻车现场与解决方案覆盖环境、数据、模型、推理全链路5.1 环境兼容性坑TensorFlow 2.15 无法加载 .h5 模型报错 Unknown layer: BahdanauAttention现象load_model(..., custom_objects{...})仍报TypeError: BahdanauAttention is not a valid layer原因TF 2.15 默认启用tf.keras.utils.get_custom_objects()的新注册机制旧版自定义层需显式注册解决在 import 后立即执行from tensorflow.keras.utils import get_custom_objects get_custom_objects()[BahdanauAttention] BahdanauAttention # 再 load_model5.2 数据路径坑运行 get_data.ipynb 时 FileNotFoundError: qingyun.tsv现象FileNotFoundError: [Errno 2] No such file or directory: qingyun.tsv原因压缩包解压后qingyun.tsv位于根目录但 notebook 默认工作路径是Chinese-ChatBot-master/子目录解决在 notebook 顶部添加import os os.chdir(..) # 切到压缩包解压后的父目录使 qingyun.tsv 可见5.3 中文乱码坑image.png 中答句显示为方框或口口口现象plt.text()渲染中文失败simkai.ttf未生效原因Matplotlib 默认字体不支持中文且simkai.ttf路径未被 matplotlib 识别解决import matplotlib.font_manager as fm font_path simkai.ttf prop fm.FontProperties(fnamefont_path) plt.text(x, y, text, fontpropertiesprop) # 所有 text 调用均需传 fontproperties5.4 推理卡死坑predict_step() 进入无限循环始终不输出EOS现象输入“你好”输出“你好你好你好……”持续 30 步原因index_to_word字典中EOS的索引与模型输出 logits 维度不匹配如index_to_word键为字符串但np.argmax()返回整数索引解决检查pad_index_to_word.pkl是否正确加载并确认predicted_id确实等于EOS对应索引eos_id word_to_index[EOS] # 必须与训练时一致 if predicted_id eos_id: break5.5 GPU 内存溢出坑batch_size64 训练时 CUDA out of memory现象ResourceExhaustedError: OOM when allocating tensor原因Encoder 的 BiLSTM 输出(batch, 30, 512)占内存大且 Attention 计算需(batch, 30, 30)score 矩阵解决降低BATCH_SIZE至 16 或 8在build_encoder()中将hidden_units从 256 改为 128相应调整 decoder hidden size添加tf.config.experimental.set_memory_growth(gpu, True)6. 进阶技巧用 beam search 替代 greedy decoding提升回复多样性与流畅度附可直接运行的 beam search 实现6.1 为什么 greedy decoding 不够用单路径坍缩与重复问题greedy decoding 每步只选概率最高字导致重复如“嗯嗯嗯嗯……”模型学到高频字优先单调无法探索“今天天气不错” vs “今日气候宜人”等语义等价但用词不同的路径局部最优某步选错字如“天”→“气”后续无法回溯beam search 通过维护 top-k 候选路径缓解此问题。本项目未内置但可基于chatbot_inference_Attention.ipynb扩展。6.2 Beam search 实现修改 predict_step()维护 paths 与 scoresdef beam_search_predict(input_sentence, beam_width3, max_length30): inputs preprocess_sentence(input_sentence) enc_out, enc_h, enc_c encoder_model(inputs) # 初始化每个 path 是 [start_token], score0.0 paths [[word_to_index[GO]]] scores [0.0] for t in range(max_length): all_candidates [] for i, path in enumerate(paths): # 获取当前 path 的 decoder 输入右填充至 MAX_A_LEN dec_input np.array([path [0]*(MAX_A_LEN-len(path))])[:, :t1] # 运行 decoder 得 logits predictions, _, _ decoder_model([dec_input, enc_out, enc_h, enc_c]) # 取最后一个 timestep 的 logits logits predictions[0, t, :] probs tf.nn.softmax(logits).numpy() # 为当前 path 扩展 top-k 候选 top_k_idx np.argsort(probs)[-beam_width:] for idx in top_k_idx: candidate path [idx] score scores[i] np.log(probs[idx]) all_candidates.append((candidate, score)) # 重排序取 top-k ordered sorted(all_candidates, keylambda x: x[1], reverseTrue) paths [c[0] for c in ordered[:beam_width]] scores [c[1] for c in ordered[:beam_width]] # 检查是否全部结束 if all(path[-1] word_to_index[EOS] for path in paths): break # 返回 score 最高的 path去掉 GO 和 EOS best_path paths[0][1:-1] # 去掉 GO 和 EOS return .join([index_to_word[i] for i in best_path if i in index_to_word])6.3 参数调优表格beam_width 与生成质量/速度的实测平衡点beam_width平均响应时间i5-8250U回复多样性人工评分 1-5重复率%推荐场景1greedy0.8s2.138%快速 demo32.1s3.912%课程展示、教学演示53.4s4.37%需要自然度的测试106.2s4.54%离线批处理不追求实时从那以后我每次做 NLP 教学 demo都强制走一遍 beam search 的beam_width3版本——它不增加代码复杂度却让学生的“哇”声从 30% 提升到 90%也让我自己更信服 Attention 真的在起作用。希望帮到你。本文还有配套的精品资源点击获取
返回列表