ARTICLE DETAIL

资讯详情

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

聊天机器人+情绪检测:基于Seq2seq、LSTM与Attention的毕设级全栈实现

聊天机器人+情绪检测:基于Seq2seq、LSTM与Attention的毕设级全栈实现 简介这是一份基于Seq2seq框架、LSTM和注意力机制实现的聊天机器人及情绪检测毕业设计项目面向需要完成NLP毕设或者想学习对话系统落地的开发者。资源围绕文本预处理、改进Seq2seq模型训练、抑郁文本分类模型构建以及网页交互设计了完整流程先通过带标签语料训练情绪分类模型再在聊天过程中实时调用该模型初步判断用户是否存在抑郁倾向。压缩包共45个文件大小约66.81MB包含Python脚本、Jupyter Notebook训练/推理文件、训练好的h5权重、pkl词表、npy数组、HTML页面、配置文件和说明文档等覆盖数据整理、模型训练、服务推理到前端展示全链路。其中提供了基于TensorFlow2.0Keras与Vue/Ajax的Web示例读者可在浏览器中直接对话并观察情绪检测结果适合作为毕业设计参考或二次开发基础。目前已有191人学习浏览资源结构清晰按模型、数据处理、页面和配置分模块组织便于快速定位和复用。1. 毕业设计级的聊天机器人加情绪检测这次不只看它会不会聊天如果你也是把「聊天机器人 情绪检测」当毕业设计题目大概率会卡在同一个地方模型跑通了对话却像个复读机情绪检测训出来了又不知道该怎么跟聊天流程接上。这套基于 Seq2seq 框架、LSTM 和 Attention 机制、用 TensorFlow 2.0 Keras 实现前端用 HTML Vue Ajax 的完整工程正好把这两条线都打通了——既能跟用户实时聊天又能在对话过程中对用户输入的文本做抑郁情绪倾向判断并把结果回显到页面上。它不是那种只在控制台里 print 一句「情绪正常」的演示品而是有语料清洗、词向量、对话模型训练、情绪分类模型训练、Flask 服务和 Vue 前端整条链路的毕设级项目。适合三类人正在做 NLP 方向毕业设计的学生、想快速复现一个完整「对话 情感分类」项目的初学者、以及需要给已有对话系统补一个情绪检测模块的开发者。2. 核心选型为什么是 Seq2seq LSTM Attention以及这套代码怎么组织起来的2.1 为什么用 Seq2seq LSTM Attention而不是直接上 Transformer拿到这套工程的代码结构时第一反应可能会想现在大家都在聊 flash attention、Coordination Attention 这些改良结构甚至「Attention is all you need」之后 Transformer 几乎成了对话系统默认底座为什么毕业设计还要用 Seq2seq LSTM 这套组合答案很实际毕设场景下你手头通常只有一两个 GPU 甚至只有 CPU数据量撑不起 Transformer 的参数量而 LSTM 在短对话上的表现足够稳定训练时间和显存开销都可控。Seq2seq 的原理可以用一句话讲明白一个 Encoder LSTM 把用户输入的整句话读成一个上下文向量然后 Decoder LSTM 从这个向量开始逐字生成回复。Attention 机制补的是 Encoder 的短板——没有 Attention 时Decoder 只能依赖最后那个上下文向量句子一长信息就丢加了 Attention 之后Decoder 在生成每一个字时都会回头去看 Encoder 所有时间步的隐藏状态按权重挑重点。这套组合里的 LSTM 负责解决梯度消失Attention 负责解决信息瓶颈两者是互补关系。在答辩时这也是一个很好讲的点你可以直接说明「改进的 Seq2seq 模型」到底改在哪。这套工程里甚至同时保留了不带 Attention 和带 Attention 两个版本的推理脚本chatbot_inference_non-Attention.ipynb 和 chatbot2_inference_Attention.ipynb训练时跑两组对比实验把生成结果摆出来比单纯贴准确率有说服力得多。2.2 从文件拓扑反推工程架构两条训练线解压资源包后先不要急着跑代码花十分钟对着文件清单捋一遍结构能省下后面大量排查时间。我把关键文件按职责归了一下类见下表。职责文件作用原始语料qingyun.tsv青云语料问答对喂给训练流程的原始数据文本清洗language/zh_wiki.py、language/langconv.py繁简转换等清洗工具语料预处理阶段使用词表word_to_index.pkl、index_to_word.pkl、vocab_bag.pkl字/词到索引的映射训练和推理共用词向量code/word2vec.py、Word2Vec_java.pkl训练词向量初始化 Embedding 层训练脚本get_data.ipynb、chatbot_train.ipynb、code/train.py、code/lstm.py、code/dataset.py数据组装、模型定义、训练主流程对话模型权重model/lstm_java_total.h5、model/lstm_java.ymlKeras 权重 模型结构注意是两个文件配合使用情绪检测模型models/W--184-0.5949-.h5带标签对话数据训练出的分类模型命名里能看到训练轮数与 loss推理与测试infer.py、test.py、chatbot.py单条测试、批量测试、对话逻辑封装服务端server.pyFlask 服务加载两个模型并提供 HTTP 接口前端templates/chat.html、newchat.html、wechat.html、example.htmlstatic/Vue Ajax 页面仿微信聊天样式其他simkai.ttf楷体字体文件用于 matplotlib 绘图时中文显示看到这个拓扑就会明白整个工程其实是两条独立的训练线一条是 Seq2seq 对话模型从 qingyun.tsv 出发经过 get_data.ipynb 组装成 padding 后的张量在 chatbot_train.ipynb 或 code/train.py 里完成训练产出 lstm_java_total.h5 和 lstm_java.yml另一条是情绪检测模型用带标签的对话文本训练一个分类模型产出 W--184-0.5949-.h5 这类权重。两条线的产物在 server.py 里汇合对话模型的输出作为回复文案情绪模型的输出作为用户情绪倾向的判断结果。2.3 模型结构定义与参数边界在 code/lstm.py 里能看到模型的核心定义。常见做法是 Encoder 和 Decoder 各用一个两层 LSTM隐藏单元数取 128 到 256 之间。Embedding 维度一般跟随词向量训练时的设置比如 100 或 200。Attention 实现上用的是加性注意力把 Decoder 当前隐藏状态和 Encoder 所有隐藏状态做加性打分后 softmax得到上下文向量再和 Decoder 输入拼接。我一般会建议把 hidden_size 控制在 256 以内因为这个数量级下单卡 GPU 训练一个晚上能出可用效果超过 512 后收益很小但训练时间翻倍。词表方面word_to_index.pkl 对应的是字级别词表中文按字切分而非按词切分这样可以控制词表规模也能减少 OOV未登录词问题。vocab_bag.pkl 则是词袋文件用于词向量训练。这里有一个很容易翻车的地方如果按词切分词表轻松破万Embedding 层参数量随之飙升而按字切分通常只有两三千个字符训练压力小很多。3. 从 qingyun.tsv 到 h5 权重数据预处理与训练全流程复现3.1 语料清洗与样本过滤这应该是全流程里最枯燥但最不能跳的一步。qingyun.tsv 是青云语料里抽取的问答对格式是标准的 TSV每一行是 question 和 answer 两列。放到这个工程里我能看到的清洗策略是先用 language 目录下的繁简转换工具把繁体文本统一转成简体再做标点归一化和空白字符清理最后按长度过滤——太短的问答对没有训练价值太长的对话在 LSTM 里也学不出有效表示截断比保留更划算。这里用一段常见的数据读取与过滤逻辑做示范import pandas as pd df pd.read_csv(qingyun.tsv, sep\t, headerNone, names[question, answer]) df df.dropna() # 按字符数过滤常见做法是保留 4~60 个字符的问答对 df df[(df[question].str.len() 4) (df[question].str.len() 60)] df df[(df[answer].str.len() 2) (df[answer].str.len() 60)] # 去掉完全重复的样本避免训练集里同一句话出现几百次 df df.drop_duplicates(subset[question, answer]) print(f清洗后剩余样本数: {len(df)})这段代码里最关键的是长度上下限的选择下限 4 是为了过滤「嗯」「好」这类无信息量的单字回复上限 60 是因为 Seq2seq 模型的输入是固定长度 padding 后的张量超过 60 的句子会被截断强行保留只会让信息丢失更严重。比例上如果语料里长句占比很高可以适当把上限调到 80但要同步加大模型 hidden_size否则长句依然学不动。dataset.py 里组装训练样本时就是按这个过滤结果把 question 和 answer 转成索引序列再统一 padding 成固定长度。3.2 词表与词向量三个 pkl 各管什么在进入训练之前词表相关文件的作用必须搞清楚否则后面推理阶段很容易出现「词表不一致」的玄学错误。word_to_index.pkl 管的是「字到索引」的字典比如「你」对应 12 号索引index_to_word.pkl 是反向的「索引到字」vocab_bag.pkl 则是词向量训练用的词袋记录的是训练词向量时见过的全部字集合。一句话记住区别word_to_index 是训练对话模型时用的vocab_bag 是训练词向量时用的两者覆盖的集合可以不一样但在推理时必须加载与对话模型同一份 word_to_index否则解码出来的就是乱码。词向量训练用 code/word2vec.py 完成常见做法是直接用 gensim 的 Word2Vec在 qingyun.tsv 的问答文本上训练字向量。训练完成后把向量矩阵导出成 Word2Vec_java.pkl在 lstm.py 里初始化 Embedding 层。这样做的好处是让模型在训练开始前就具备一定的语义先验比如「难过」和「伤心」的向量距离更近而不是从随机初始化开始摸索。from gensim.models import Word2Vec sentences [list(q) for q in df[question]] [list(a) for a in df[answer]] w2v_model Word2Vec(sentences, vector_size200, window5, min_count1, workers4) # 导出为后续训练使用的 pkl import pickle with open(Word2Vec_java.pkl, wb) as f: pickle.dump({vectors: w2v_model.wv.vectors, index_to_key: w2v_model.wv.index_to_key}, f)vector_size 取 200 是这个量级语料的合理选择太小如 50语义表达能力不足太大如 500会让 Embedding 层的参数量膨胀训练速度明显下降。window 取 5 是因为中文的语境依赖通常落在相邻的几个字内。注意到这里用list(q)按字切分这和前面说到的字级别词表保持一致训练脚本里加载词表、初始化 Embedding 的路径也严格依赖这个切分逻辑。3.3 训练主流程与模型保存策略训练部分chatbot_train.ipynb 和 code/train.py 是同一套流程的两份载体notebook 版本适合逐步调试train.py 适合直接后台跑批。训练的核心配置我一般会这样设置batch_size 取 64epoch 设 30 左右embedding_dim 用 200hidden_size 取 256优化器用 Adam学习率初始 0.001每训练 5 轮衰减一次。如果显存不足优先把 batch_size 降到 32而不是动 hidden_size。模型保存是最容易被忽略的环节。这个工程里出现的是 lstm_java_total.h5 lstm_java.yml 的组合前者是权重文件后者是模型结构描述。这种保存方式意味着加载时必须先读 yml 重建结构再加载 h5 权重from tensorflow.keras.models import model_from_yaml with open(model/lstm_java.yml, r) as f: model_structure f.read() model model_from_yaml(model_structure) model.load_weights(model/lstm_java_total.h5)如果直接load_model(model/lstm_java_total.h5)会报结构不匹配错误因为 h5 里只有权重没有结构。这是 Keras 常见的「model.save_weights model.to_yaml」保存模式和model.save单文件保存是两回事。训练过程中建议配合 ModelCheckpoint 回调每个 epoch 结束保存一次这样即使中途崩了也有后悔药可吃。情绪检测模型的训练线类似只是最后一层从 Decoder 换成 Dense sigmoid 二分类。带标签的对话数据作为输入标签表示该文本是否带有抑郁情绪倾向。训练完成后同样导出成 h5 权重即 models 目录下的 W--184-0.5949-.h5。看这个命名按常见工程习惯推测是训练到第 184 轮保存的 checkpointval_loss 在 0.5949 左右新版本读者可以用情绪分类的验证集指标来理解这个文件。4. 系统搭建与联调避坑server.py 到 Vue 页面之间的五个坑4.1 server.py 如何把对话模型和情绪检测模型包成一个 HTTP 接口整个系统能跑起来的关键在 server.py。它要做的事有两件启动时把两个模型都加载进内存运行时接收前端传来的用户消息先送给对话模型生成回复再把用户消息送给情绪检测模型算一个倾向分数最后把回复和情绪结果一起打包成 JSON 返回。Flask 是这里最常见的服务端框架因为足够轻量加载两个模型后内存占用依然可控。from flask import Flask, request, jsonify from chatbot import ChatBot app Flask(__name__) chatbot ChatBot() emotion_model load_emotion_model(models/W--184-0.5949-.h5) app.route(/chat, methods[POST]) def chat(): data request.get_json() user_input data.get(message, ) reply chatbot.generate(user_input) emotion emotion_model.predict_text(user_input) return jsonify({reply: reply, emotion: emotion}) if __name__ __main__: app.run(host0.0.0.0, port5000)代码里chatbot.generate()封装了对话模型的推理逻辑包括输入文本转索引、padding、Decoder 逐步生成、索引转回文本。emotion_model.predict_text()则是把输入文本经过同样的预处理后送入分类模型输出一个 0 到 1 之间的分数阈值一般取 0.5。这里有个容易被忽视的细节两个模型各自的预处理必须和训练时一致如果对话模型是按字切分情绪模型也必须是按字切分差一个空格结果都会偏。4.2 前端 Vue Ajax 接入与中文显示前端页面在 templates 目录下chat.html 和 newchat.html 是两套可用的聊天界面wechat.html 是仿微信风格的样式example.html 是最简示例。前端通过 Ajax 把用户输入 POST 到后端的 /chat 接口拿到 reply 和 emotion 两个字段后渲染到聊天窗口里。Vue 在这里的作用主要是数据绑定——把消息列表绑到界面上的一个数组新增消息只需要 push 一条记录DOM 自动更新。axios.post(/chat, { message: this.inputText }) .then(response { this.messages.push( { role: user, content: this.inputText }, { role: bot, content: response.data.reply } ); this.emotionTag response.data.emotion; this.inputText ; }) .catch(error console.error(请求失败:, error));这段代码里 emotion 字段在界面上表现为一个标签比如「情绪倾向低落」或「情绪倾向稳定」。注意这里返回的是文本分类倾向不是任何医疗结论工程里也刻意没有写成诊断语气。中文显示上HTML 页面本身用 meta charset 声明 UTF-8 就能避免乱码工程里的 simkai.ttf 是给训练过程绘图用的字体和前端无关不要在页面里试图加载它。4.3 联调避坑清单下面这五条是从这套工程的实际使用场景里整理出来的高频问题每一条都是「现象 → 原因 → 解决」的完整链路。坑一推理时输入的每一个字都变成unk。现象是生成的回复全是未知字符标记模型看起来完全没训练过。原因几乎可以断定是推理时加载的 word_to_index.pkl 和训练时用的不是同一份两个词表的索引映射对不上。解决方法是训练结束后立刻把词表文件备份一份推理脚本里硬编码加载这份备份不要依赖「默认路径下恰好存在某个 pkl」这种运气。坑二服务器启动时报端口被占用。现象是app.run(port5000)时抛 OSError原因是上一次运行的服务没有正常退出或者有其他进程占了 5000 端口。解决方式是先查占用进程并停掉或者换一个端口。更稳的做法是把端口号提到配置里前端请求地址同步修改避免在代码里到处硬编码。坑三前端直接双击打开 html 文件时 Ajax 全失败。现象是页面能打开但一发送消息就报跨域错误。原因是浏览器直接访问 file:// 协议时向 http://localhost:5000 发请求属于跨域被浏览器拦截。解决方式是统一通过http://localhost:5000访问页面也就是先启动 server.py再在浏览器里访问 Flask 提供的页面地址不走文件系统。坑四TF2.0 环境下运行旧代码报 Session 相关错误。现象是tf.Session()或session.run()直接抛错。原因是工程里如果混用了 TF1.x 的会话式 API而当前环境是 TF2.0 默认开启 eager execution两者天然冲突。解决方式是把所有tf.Session相关代码统一替换成 Keras 的model.predict()或者在调用处用tf.compat.v1.disable_eager_execution()做兼容处理。个人更推荐前者因为 predict 接口在推理阶段反而更简洁。坑五启动时内存直接被打满。现象是加载模型后内存占用飙升页面响应极慢甚至进程被杀。原因是工程把 pad_question.npy、pad_answer.npy 这类大型 numpy 数组一次性全量加载同时两个模型都在内存里叠加后超过机器上限。解决方式是按需加载推理只需要对话模型和情绪模型训练用的 pad 数组不需要在 server.py 里加载如果仍需加载词向量矩阵用np.load(..., mmap_moder)以内存映射方式读取可以显著降低常驻内存。5. 进阶验证与改进Attention 权重可视化与情绪检测效果评估模型能跑通只是第一步毕业设计答辩时真正拉开差距的是你能不能拿出除「能聊天」之外的验证手段。这里给两个可以直接落地的改进方向都基于工程里已有的文件不需要另起炉灶。第一个方向是 Attention 权重可视化。工程里 chatbot2_inference_Attention.ipynb 是带 Attention 的推理脚本Decoder 在生成每个字时都会产生一组对所有输入位置的注意力权重。把这组权重保存成矩阵用 matplotlib 画成热力图就能直观看到回复里的每个字到底对齐了用户输入里的哪些字。import matplotlib.pyplot as plt import matplotlib matplotlib.rcParams[font.sans-serif] [SimKai] # 使用 simkai.ttf 对应字体 plt.imshow(attention_matrix, cmapBlues, aspectauto) plt.xlabel(用户输入位置) plt.ylabel(回复生成位置) plt.colorbar() plt.show()这就是工程里 simkai.ttf 字体真正的使用场景——没有这个中文字体热力图的坐标轴标签会显示成一排方块。答辩时把这张图投出来配合说明「这一步生成『难过』时模型重点看了用户的『累』和『撑不住』」比任何指标数字都直观。第二个方向是情绪检测模型的效果评估。分类模型不能只看准确率因为带抑郁倾向的文本在真实对话里占比不高类别不平衡下准确率会虚高。正确的做法是把测试集单独留出计算 precision、recall 和 F1 值并画混淆矩阵。如果 recall 偏低说明模型把大量风险文本漏判为正常这时可以降低判定阈值到 0.3 或 0.4牺牲一部分误报来换取更少的漏报。情绪检测这件事里漏报的代价远高于误报这是做这个功能时必须想清楚的取舍。对话模型本身的改进最值得加的是 beam search。工程里推理脚本大概率用的是贪心解码——每一步只取概率最大的字这在短对话里够用但容易出现「你叫什么名字」回答「我叫你」这种重复问题。改成 beam search 后解码器每一步保留概率最高的前几个候选序列最后再整体打分回复质量会有明显提升。实现上只需要改 infer.py 里的 generate 函数加入 beam width 参数一般取 3 到 5。这套工程我前前后后复现过两遍第一遍翻车就翻在词表不一致上生成的回复全是乱码浪费了整整一个晚上。从那以后我每次拿到这种「模型 词表 预处理」三件套项目都会先做一次最小冒烟测试拿一条训练集里的原样输入喂给推理脚本确认输出和训练时看到的某个历史输出对得上再谈调参和部署的事。先验证链路完整性再优化效果这个顺序希望帮到你。本文还有配套的精品资源点击获取
返回列表