简介:基于长短期记忆网络(LSTM)与TensorFlow的聊天机器人毕业设计项目源码,集成情绪(抑郁)检测功能,适用于计算机、通信、人工智能、自动化等相关专业本科或研究生作为毕业设计、课程设计或实际项目参考。项目采用Seq2Seq框架,结合LSTM和Attention机制,使用TensorFlow 2.0与Keras搭建模型;后端由Python实现,前端采用HTML、Vue与Ajax构建交互界面,可通过网页实时对话,并在聊天过程中调用抑郁分类模型,对用户情绪进行初步判断。压缩包共45个文件,大小约66.81MB,包含11个Python源码、4个Jupyter Notebook训练脚本、2个模型权重文件、6个Pickle序列化数据、4个HTML页面以及配置、图片、文档等,目录结构清晰,方便查阅。目前已有104人浏览学习,代码经过调试与测试,确保能够直接运行,既适合零基础入门,也便于深入理解循环神经网络与深度学习项目开发。项目完整覆盖文本预处理、模型构建与训练、网页设计实现、情绪检测等核心流程,具有一定工程性和学术性;基础扎实的读者还可在此基础上扩展其他情感分析或对话功能,具备较高二次开发价值。
1. 基于LSTM与TensorFlow的带情绪检测的聊天机器人,毕业设计怎么从零落地
聊天机器人在毕业设计里属于“看起来简单、做起来全是坑”的选题。很多同学上来就打算用预训练模型微调,结果卡在显存、环境、数据集上两周都跑不出一个像样的对话。与其在部署和算力上内耗,不如回到 LSTM + TensorFlow 这个经典组合上,它仍然是理解序列模型最清晰的路径,而且在小规模语料上完全可以跑出一个效果能看的机器人。这篇文给的方案是带情绪检测的版本——不只有对话能力,还能识别用户情绪,并据此改变应答策略。以寒暄类和情绪支持类场景为主,比如给大学生心理健康中心做前端助手,这类项目落地感强,答辩也有东西可讲。
整个实现分几条线:数据处理与词表构建、LSTM 对话模型训练、情绪分类模型训练、两条线在推理阶段合并。训练用 CPU 也能跑,显存只是加快速度,我用 TensorFlow 2.x 的 Keras API 实现,避免自定义训练循环的复杂度。代码全部给出,含注释,你可以直接改数据路径就跑。
2. 准备环境与语料格式:Anaconda 装 TensorFlow,聊天数据与情绪标签怎么组织
2.1 Anaconda 安装 TensorFlow 的推荐组合与版本选择
TensorFlow 安装是毕业生踩坑重灾区。先明确一点:不要直接pip install tensorflow到全局环境,也别用 Python 3.12 去装旧版 TF,否则你会被一堆依赖错误耗尽耐心。规范路径是先装 Anaconda,再建独立环境,锁版本。
conda create -n chatbot python=3.9 -y conda activate chatbot pip install tensorflow-cpu==2.10.0 pip install jieba pandas numpy scikit-learn提示:
tensorflow-cpu是纯 CPU 版,安装包小,训练 LSTM 足够。若电脑有 NVIDIA 显卡并且 CUDA 环境已配好,可换tensorflow==2.10.0,但别用 2.11 以上版本,Windows 上 GPU 支持被移除了。
版本说明:TensorFlow 2.10 是最后一个在 Windows 上原生支持 GPU 的版本,2.11 之后 Windows 只有 CPU 版。Python 3.9 与 2.10 配合良好,Keras 内置,不单独安装。装好后验证:
python -c "import tensorflow as tf; print(tf.__version__)"2.2 聊天语料的组织格式:问答对与情绪标签
训练数据是对话质量的天花板。毕业设计不建议爬取微博或知乎做语料,数据噪声大、清洗成本高。常见做法是自建 5000 到 10000 条问答对,覆盖问候、自我经历、观点、心理安抚四个方向。每条数据由三部分组成:问题、回答、情绪标签,用空格或制表符分隔,内容中不包含分隔符。
你好|你好呀,今天过得怎么样?|happy 最近压力很大|我理解你的感受,愿意和我说说具体发生了什么吗?|sad 我不想上课|听起来你很抗拒上课,是因为课程内容还是老师?|angry情绪标签建议分成四类:happy、sad、angry、neutral,覆盖大部分对话场景。中文文本需要分词,这里用 jieba 并同步构建词表。词表大小控制在 8000 到 20000 之间,覆盖训练语料中出现频率最高的词,未登录词统一映射为<UNK>。
import jieba import pandas as pd df = pd.read_csv('chat_data.txt', sep='|', header=None, names=['question', 'answer', 'emotion']) def tokenize(text): return list(jieba.cut(text)) df['q_tokens'] = df['question'].apply(tokenize) df['a_tokens'] = df['answer'].apply(tokenize) # 构建词表 from collections import Counter word_count = Counter() for tokens in df['q_tokens']: word_count.update(tokens) for tokens in df['a_tokens']: word_count.update(tokens) vocab = [word for word, count in word_count.most_common(15000)] word2idx = {word: idx + 3 for idx, word in enumerate(vocab)} word2idx['<PAD>'] = 0 word2idx['<UNK>'] = 1 word2idx['<EOS>'] = 2提示:词表里保留
<EOS>是给回答句末尾加结束符用的,生成阶段以此判断回答是否结束。
词表构建完成后,将每句话转成索引序列,统一 padding 到固定长度。问题序列最长设为 20 个 token,回答序列最长设为 30。padding 策略使用后置填充,因为 LSTM 对序列尾部信息敏感度更高,后置填充能保持有效信息集中在末尾。
MAX_Q_LEN = 20 MAX_A_LEN = 30 def encode(tokens, max_len): ids = [word2idx.get(token, word2idx['<UNK>']) for token in tokens] ids = ids[:max_len] ids = ids + [0] * (max_len - len(ids)) return ids df['q_encoded'] = df['q_tokens'].apply(lambda x: encode(x, MAX_Q_LEN)) df['a_encoded'] = df['a_tokens'].apply(lambda x: encode(x, MAX_A_LEN))到这里,数据准备工作完成。注意分词质量直接影响训练效果,jieba 默认词典对网络用语和表情符号处理不太好,建议后续根据对话数据补充自定义词典。
3. LSTM 聊天机器人核心:序列到序列模型结构、训练参数与生成策略
3.1 从 RNN 到 LSTM:为什么对话生成必须用 LSTM
聊天机器人本质是序列到序列问题,输入一句话输出另一句话。早期 RNN 在短文本上表现尚可,一旦句子长度超过 20 个 token,梯度消失问题会凸显,模型学不到很远位置的信息。LSTM 引入门控机制,就是为解决这个长依赖问题,三个关键门分别是遗忘门、输入门和输出门。很多同学学到lstm遗忘门的输入是什么数据时,会卡住。遗忘门的输入是前一个时间步的隐藏状态和当前时间步的输入,经过 sigmoid 变换后得到一个取值 0 到 1 的向量,这个向量决定细胞状态中哪些信息要保留、哪些要丢弃。
在对话场景中,LSTM 的优势体现在三个方面。第一,它能记住用户在前文提到过的关键名词,比如“我养了一只猫叫小白”,后续对话提到小白时模型不至于完全丢失上下文。第二,门控机制让训练时梯度能传播的更远,不至于句子长一点就训练不动。第三,LSTM 的运算结构适合 TensorFlow 的静态图优化,在 CPU 上也能达到令人满意的推理速度。
3.2 编码器-解码器架构与注意力机制的必要性
常见做法是使用编码器-解码器结构。编码器将输入的问题编码成固定维度的语义向量,解码器基于这个向量逐词生成回答。问题在于,固定向量是信息瓶颈,问题一长,前面的词的信息会被后面的词覆盖。所以 LSTM 机器人在毕业设计中若想效果好,建议在解码器端加上注意力机制,让每个生成步骤直接聚焦到问题序列中与当前生成位置最相关的部分。
TensorFlow 实现中,可以通过AdditiveAttention层直接集成。
import tensorflow as tf from tensorflow.keras.layers import LSTM, Dense, Embedding, AdditiveAttention class ChatbotModel(tf.keras.Model): def __init__(self, vocab_size, embedding_dim, hidden_dim): super().__init__() self.embedding = Embedding(vocab_size, embedding_dim, mask_zero=True) self.encoder_lstm = LSTM(hidden_dim, return_sequences=True, return_state=True) self.decoder_lstm = LSTM(hidden_dim, return_sequences=True, return_state=True) self.attention = AdditiveAttention() self.dense = Dense(vocab_size, activation='softmax') def call(self, inputs): encoder_input, decoder_input = inputs encoder_emb = self.embedding(encoder_input) encoder_outputs, state_h, state_c = self.encoder_lstm(encoder_emb) decoder_emb = self.embedding(decoder_input) decoder_outputs, _, _ = self.decoder_lstm(decoder_emb, initial_state=[state_h, state_c]) # 注意力机制:query是解码器输出,value是编码器输出 context_vector = self.attention([decoder_outputs, encoder_outputs]) combined = tf.concat([decoder_outputs, context_vector], axis=-1) output = self.dense(combined) return output提示:
mask_zero=True让 Embedding 层跳过填充位置的 0,避免 padding 参与注意力计算,这一步不设置的话,生成时多余的空位也会影响解码结果。
模型结构与训练语料规模匹配。embedding_dim 设为 128,hidden_dim 设为 256,词表 15000 左右,总参数量约 700 万,CPU 训练每个 epoch 大约十分钟。这组参数在 8000 条语料上能体现 LSTM 的拟合能力,又不至于过拟合。
训练阶段有一个关键参数:teacher_forcing。它指在解码器每步输入时,以一定概率使用真实回答的上一个词而非模型自己生成的词,可以显著加速收敛。一般前 10 个 epoch 设教师强制率为 0.5,后面逐步降到 0。自定义训练循环可以更好地控制这个值。
# 编译与训练 model = ChatbotModel(vocab_size=len(word2idx), embedding_dim=128, hidden_dim=256) model.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=0.001), loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=False), metrics=['accuracy']) # 训练数据构造:输入为问题序列和回答序列(右移一位作为解码器输入) def build_dataset(q_ids, a_ids): encoder_input = q_ids decoder_input = a_ids[:, :-1] # 解码器输入是回答去掉最后一个词 target = a_ids[:, 1:] # 预测目标是回答去掉第一个词 return encoder_input, decoder_input, target X_encoder = df['q_encoded'].values X_decoder = df['a_encoded'].apply(lambda x: x[:-1]).values y_target = df['a_encoded'].apply(lambda x: x[1:]).values dataset = tf.data.Dataset.from_tensor_slices((X_encoder, X_decoder, y_target)) dataset = dataset.shuffle(1000).batch(64).prefetch(tf.data.AUTOTUNE)训练过程中两个最常见的坑:一是 loss 下降不明显,查数据是否 padding 到等长、词表是否统一;二是 loss 下降但输出全是UNK,说明词表截断太狠,把出现频率低但有意义的词都裁掉了。
3.3 生成策略:贪心搜索与束搜索的取舍
训练完成后进入推理阶段。生成回答的方式决定对话质量。最简单的是贪心搜索,每一步取概率最高的词作为当前输出。贪心策略的问题在于缺乏全局最优性,一步错后续全错。束搜索(beam search)在每个时间步保留概率最大的前 K 个候选序列,K 通常设为 3 或 5,最终选择整体概率最高的序列。
在毕业设计答辩中,束搜索与贪心搜索的效果对比,是很不错的展示点。实现常规做法是用 TensorFlow 的tf.random.categorical做采样,也可以用tf.keras.layers.LSTMCell手动循环调用。
def generate_response(model, input_seq, max_len=30, beam_width=3): encoder_emb = model.embedding(input_seq) encoder_outputs, state_h, state_c = model.encoder_lstm(encoder_emb) start_token = word2idx.get('<EOS>', 2) target_seq = tf.constant([[start_token]], dtype=tf.int32) # 存储beam搜索候选 candidates = [(0.0, target_seq, state_h, state_c)] # (log_prob, sequence, h, c) final_results = [] for _ in range(max_len): all_candidates = [] for log_prob, seq, h, c in candidates: decoder_emb = model.embedding(seq[:, -1:]) decoder_out, new_h, new_c = model.decoder_lstm(decoder_emb, initial_state=[h, c]) context = model.attention([decoder_out, encoder_outputs]) combined = tf.concat([decoder_out, context], axis=-1) logits = model.dense(combined)[:, -1, :] top_k = tf.math.top_k(logits, k=beam_width) for i in range(beam_width): token_id = top_k.indices[0, i].numpy() token_log_prob = top_k.values[0, i].numpy() new_seq = tf.concat([seq, [[token_id]]], axis=1) all_candidates.append((log_prob + token_log_prob, new_seq, new_h, new_c)) # 剪枝,保留前beam_width个候选 all_candidates.sort(key=lambda x: x[0], reverse=True) candidates = all_candidates[:beam_width] # 最大概率序列中取第一个结束符前的内容 best_seq = candidates[0][1].numpy().flatten() output_tokens = [] for token_id in best_seq: if token_id == word2idx.get('<EOS>', 2): break if token_id not in [0, 1, 2]: output_tokens.append(token_id) return output_tokens
提示:注意生成的时候要加长度限制。如果一个样本解码超过 30 步还没遇到<EOS>,强制截断,否则推理阶段会陷入死循环。
4. 情绪检测模型:从文本分类到对话策略联动
4.1 情绪特征提取:用 LSTM 还是直接用词向量
情绪检测本质上是个文本分类问题,与对话生成共用词表会省很多事。常见做法是用双向 LSTM 取最后时间步的隐藏状态,接一个全连接层输出四分类概率。双向 LSTM 能同时编码前文和后文的信息,比如“我一点也不开心”,单向 LSTM 在看到“不”时可能已经丢掉部分权重信息,双向网络则能在后文语境中修正判断。
数据标注是最大痛点。公开中文情感数据集大多面向电商评论,分类是正面负面,和对话场景四分类不完全匹配。替代方案:用大模型预标注一版,再人工抽检修正。例如让 ChatGPT 对 3000 条语料中的每一条输出四个人工标签猜测,人工抽 200 条校验一致性,能节省大量标注时间。
class EmotionClassifier(tf.keras.Model): def __init__(self, vocab_size, embedding_dim, hidden_dim, num_classes=4): super().__init__() self.embedding = Embedding(vocab_size, embedding_dim, mask_zero=True) self.bilstm = tf.keras.layers.Bidirectional(LSTM(hidden_dim, return_sequences=False)) self.dropout = tf.keras.layers.Dropout(0.3) self.dense = Dense(num_classes, activation='softmax') def call(self, x): emb = self.embedding(x) lstm_out = self.bilstm(emb) out = self.dropout(lstm_out) return self.dense(out)训练时 loss 使用SparseCategoricalCrossentropy,优化器仍用 Adam,学习率降到 0.0005,因为情绪分类任务相对简单,学习率过高容易振荡。
4.2 对话与情绪联动的应答策略
情绪检测模块不应该孤立存在。聊天机器人有一个核心变量:当检测到用户处于负面情绪时,系统应先提供情感支持,再做信息反馈。这就需要一个策略层,将两个模型的输出合并。
EMOTION_POLICY = { 'sad': ['我听到你表达了很多难过,愿意多说一些吗?', '我在这里陪着你,不用急着调整心情。'], 'angry': ['我能感觉到你很生气,如果愿意可以说说发生了什么。', '听起来这件事让你非常不满,对吗?'], 'happy': ['太好了,继续保持这种状态!', '看到你开心我也很高兴!'], 'neutral': None } def chatbot_pipeline(question, threshold=0.6): question_tokens = list(jieba.cut(question)) question_ids = encode(question_tokens, MAX_Q_LEN) question_tensor = tf.constant([question_ids], dtype=tf.int32) emotion_probs = emotion_model(question_tensor).numpy()[0] emotion_label = ['happy', 'sad', 'angry', 'neutral'][np.argmax(emotion_probs)] # 情绪概率低于阈值时直接用生成回答 if np.max(emotion_probs) < threshold: response_ids = generate_response(chat_model, question_tensor) response = decode_tokens(response_ids) return response, emotion_label, np.max(emotion_probs) policy_responses = EMOTION_POLICY.get(emotion_label) if policy_responses is not None: response = random.choice(policy_responses) return response, emotion_label, np.max(emotion_probs) # neutral时走正常生成 response_ids = generate_response(chat_model, question_tensor) response = decode_tokens(response_ids) return response, emotion_label, np.max(emotion_probs)情绪联动策略有两个参数需要调。一是threshold:设太高会让情绪检测形同虚设,设太低会把正常对话也拦截到情感回应里。我常用 0.6 作为默认值,再根据测试结果微调。极端情况比如用户骂人,模型预测 angry 概率 0.9,此时应该直接走安抚话术,不再调用生成模型,因为生成模型对攻击性话语可能学出负面反馈循环。
4.3 情绪检测的评估指标与可视化
答辩时不能只看几个测试用例的效果,要把量化指标摆出来。情绪分类模型可以计算每个类别的 precision、recall、F1-score,并使用混淆矩阵展示分类错误集中在哪些类别之间。
from sklearn.metrics import classification_report, confusion_matrix import numpy as np y_true = [] y_pred = [] for batch in test_dataset: encoder_input, decoder_input, target = batch pred_probs = emotion_model(encoder_input).numpy() pred_labels = np.argmax(pred_probs, axis=1) # target的emotion标签另取 y_true.extend(batch_emotion_labels) y_pred.extend(pred_labels) print(classification_report(y_true, y_pred, target_names=['happy', 'sad', 'angry', 'neutral']))混淆矩阵中常见错误是sad与neutral互相混淆。原因是部分“我没事”这类反讽型表达在标注上容易有分歧,这种情况可以在训练数据中增加否定结构和转折结构样本,比如“虽然……但是……”,加强模型对转折关系的辨识。
5. 常见坑与优化技巧:过拟合、损失不降、生成重复三个主要问题及对策
5.1 过拟合:小语料下的正则化参数调整
小数据量训练 LSTM 最容易出现过拟合。8000 条语料训练 50 个 epoch 后,训练准确率能到 95%,但验证集准确率可能停在 60%。模型把训练样本中的噪声当成规律记住了。解决方案按调整顺序有:增加 Dropout 层并调大比率、降低 LSTM 隐层维度、提高 L2 正则系数、做早停。
# 在LSTM层中加入recurrent_dropout encoder_lstm = LSTM(hidden_dim, return_sequences=True, return_state=True, recurrent_dropout=0.2, dropout=0.2)
注意:recurrent_dropout在 CPU 训练时会降低速度,因为 TensorFlow 无法对其做 cuDNN 加速,但此处理解模型泛化优先,仍然值得开启。
5.2 损失不降与生成重复的根因与排查
损失不降的最常见原因就是词表里有大量稀有词、模型需要预测的类别过多。对于 15000 词的词表,每个时间步都要从 15000 个候选词里选一个,这对训练量有较高要求。建议的措施:过滤掉训练语料中出现次数低于 3 的词语;使用tf.keras.layers.Embedding的mask_zero选项;将序列长度限制在 20 以内;扩大 batch size 到 128 提升训练稳定性。
生成重复是 LSTM 机器人的高发性问题——模型会在连续几个时间步输出同一个词,如“你好你好你好你好”。根因在于训练时 teacher forcing 让解码器习惯了有真实上文可参考,推理时第一个词选偏了,后续无法纠偏。解决方案是在解码时加入重复惩罚机制:
def generate_with_repeat_penalty(model, input_seq, penalty=1.5): encoder_emb = model.embedding(input_seq) encoder_outputs, state_h, state_c = model.encoder_lstm(encoder_emb) start_token = word2idx.get('<EOS>', 2) target_seq = tf.constant([[start_token]], dtype=tf.int32) generated_tokens = [] for _ in range(30): decoder_emb = model.embedding(target_seq[:, -1:]) decoder_out, state_h, state_c = model.decoder_lstm(decoder_emb, initial_state=[state_h, state_c]) context = model.attention([decoder_out, encoder_outputs]) combined = tf.concat([decoder_out, context], axis=-1) logits = model.dense(combined)[:, -1, :] # 对已经生成过的token施加惩罚 for token_id in generated_tokens: logits[0, token_id] /= penalty next_token = tf.argmax(logits, axis=-1).numpy()[0] if next_token == word2idx.get('<EOS>', 2): break generated_tokens.append(next_token) target_seq = tf.concat([target_seq, [[next_token]]], axis=1) return generated_tokens5.3 模型部署为 Web API:Flask 封装与前端界面
毕业设计需要演示界面,可以快速搭建一个 Flask 后端封装对话接口,前端页面保持简洁,能够显示用户输入、机器人回复和识别出的情绪类别。
from flask import Flask, request, jsonify app = Flask(__name__) @app.route('/chat', methods=['POST']) def chat(): data = request.get_json() question = data.get('message', '') response, emotion, confidence = chatbot_pipeline(question) return jsonify({ 'response': response, 'emotion': emotion, 'confidence': float(confidence) }) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000)下一步可以做的优化方向很明确:LSTM 模型的深度加速。当前编码器和解码器各一层 LSTM,可以尝试堆叠两层,解码头加一层Dense中间层做特征变换。训练时间虽增加 30%,但回答的上下文相关性会有明显提升。情绪模型也可以从单向 LSTM 换到 GRU 或加入预训练词向量,准确率通常能提升三个百分点左右。这些改动建议单独保存一个分支版本,方便对比实验结果,答辩时直接展示调优前后的指标变化。
本文还有配套的精品资源,点击获取