资讯详情

基于RNN与Django的中文留言情感分类系统设计与实现

📅 2026/9/10 17:39:19 | 华诺云谱 👁 阅读
基于RNN与Django的中文留言情感分类系统设计与实现
简介这是一份基于循环神经网络的情感分类系统项目实战资源主要面向计算机毕业设计、课程设计也适合希望掌握网络开发与深度学习结合的 Python 学习者。系统基于 Python、Django、MySQL 搭建核心功能是对用户留言进行情感自动分类同时实现数据管理校对、统计结果可视化、公告管理和用户管理等模块覆盖从模型推理到后台管理的完整业务链路。压缩包内有三百五十二个文件以 Python 源码、前端网页样式脚本、图片素材、演示动画、数据库脚本以及训练好的模型权重文件为主整体大小约三百八十兆目录结构清晰便于按模块查阅和二次开发。随包附有说明文档与演示视频目前已有二百二十二人浏览学习可帮助读者快速部署运行并深入理解循环神经网络与 Django 项目融合时的数据处理、模型调用和接口设计思路具有较强的工程参考价值。1. 从留言板到情绪雷达RNN情感分类系统的设计思路用户留言是产品最直接的声音但也是人工运营最头疼的入口。如果每天几千条反馈手动分门别类不仅慢而且标准不统一。这套基于Python Django MySQL的情感分类系统把用户留言自动分成正向、中性、负向三个类别管理员只需要在后台校对那些模型判断不准确的个案同时用图表直观看到情绪走向。项目不是一个孤立的预测脚本而是完整的业务闭环用户提交留言后系统调用循环神经网络模型实时分类管理员可以校对自动分类结果查看统计图表还能通过公告模块发布通知。源码亲测可用附说明文档和演示视频适合做毕业设计、课程设计也是小团队内部运营工具的上手范本。2. 数据流与模型设计从留言到情感标签的完整链路2.1 数据库模型留言、分类、校对的三张核心表数据层是整个系统的基础。情感分类的核心实体是“留言”围绕它还需要“人工校对记录”和“分类统计”的支撑。放在Django的models.py里大致是from django.db import models from django.contrib.auth.models import User class Message(models.Model): content models.TextField(verbose_name留言内容) auto_label models.CharField(max_length10, blankTrue, verbose_name自动分类) manual_label models.CharField(max_length10, blankTrue, verbose_name人工校对) created_at models.DateTimeField(auto_now_addTrue, verbose_name创建时间) class Meta: ordering [-created_at] class CorrectionLog(models.Model): message models.ForeignKey(Message, on_deletemodels.CASCADE, verbose_name留言) old_label models.CharField(max_length10, verbose_name原分类) new_label models.CharField(max_length10, verbose_name新分类) operator models.ForeignKey(User, on_deletemodels.SET_NULL, nullTrue, verbose_name操作人) updated_at models.DateTimeField(auto_nowTrue, verbose_name校对时间)这里auto_label是模型自动输出的情感标签manual_label是人工复核后覆盖的标签CorrectionLog记录每一次校对变化。用外键关联User可以统计每个管理员校对了多少条。为什么要分开存自动和人工两个字段因为在后面的数据分析里可以直接对比这两个字段算出模型的偏离率知道哪些样本是模型容易出错的。在生成初始迁移时需要在项目根目录依次执行python manage.py makemigrations和python manage.py migrate。这两条命令会读取models.py里的类定义自动生成建表SQL并执行。如果你用的是MySQL执行前要确认数据库emotion_db已经建立否则会提示“Unknown database”。2.2 为什么是RNN文本分类里的序列建模选择情感分类属于文本分类任务常规方案包括朴素贝叶斯、TF-IDF加SVM以及深度学习方法。对于中文留言这种长度不固定、语义依赖前后词的场景RNN天然有优势。RNN的隐藏状态会按时间步逐个词处理输入前一个词的信息会传递到后一个词所以能捕捉“不太满意”这种否定结构。虽然LSTM和GRU本质上都是RNN的门控变体但它们解决了长距离依赖时的梯度消失问题实践中通常直接用LSTM单元替代普通RNN训练稳定性和效果都更好。在短文本场景下TextCNN也可以到相似的准确率但RNN的顺序建模思路更符合“人在读句子”的直觉。对毕业设计来说这一点也很关键解释LSTM的三个门遗忘门、输入门、输出门会比解释卷积核更直观。2.3 训练数据的准备分词、序列化和模型训练训练阶段一般在独立脚本里完成不在Django服务中运行。常见做法是把数据整理成两列label和content标签用0、1、2分别对应负向、中性和正向。样本量建议每类不少于2000条不然LSTM容易过拟合。训练前先对中文做分词这里用jiebaimport jieba import pandas as pd from keras.preprocessing.text import Tokenizer from keras.preprocessing.sequence import pad_sequences df pd.read_csv(train_data.csv) df[cut] df[content].apply(lambda x: .join(jieba.cut(str(x)))) tokenizer Tokenizer(num_words5000) tokenizer.fit_on_texts(df[cut]) sequences tokenizer.texts_to_sequences(df[cut]) X pad_sequences(sequences, maxlen50) y pd.get_dummies(df[label]).values代码先把每条留言用jieba切词分词之间用空格拼接这是Keras的Tokenizer默认的输入格式。num_words5000表示词典最大保留5000个词超过部分会被忽略避免生僻字把词表撑得过大。pad_sequences的maxlen50是序列的固定长度超过50个词截断不足50补0LSTM输入必须是定长矩阵。然后构建三层结构的RNN模型from keras.models import Sequential from keras.layers import Embedding, LSTM, Dense, Dropout model Sequential() model.add(Embedding(5000, 128, input_length50)) model.add(LSTM(64, return_sequencesFalse)) model.add(Dropout(0.3)) model.add(Dense(3, activationsoftmax)) model.compile(losscategorical_crossentropy, optimizeradam, metrics[accuracy]) model.fit(X, y, validation_split0.2, epochs10, batch_size64)Embedding层把每个词的索引映射成128维向量LSTM层是核心循环结构。return_sequencesFalse表示只返回最后一个时间步的输出这个向量被当成整句话的语义表示。Dropout随机丢弃30%的神经元降低过拟合风险。训练完成后执行model.save(rnn_emotion.h5)同时用pickle保存tokenizer对象后面Django预测时需要用到。训练参数可以按下面的表格调整不同场景下的推荐值不完全一样参数默认值说明embedding_dim128词向量维度小语料可以降到64lstm_units64LSTM单元数越大拟合能力越强但越容易过拟合maxlen50序列最大长度短留言场景设为30即可batch_size64批次大小显存或内存不足时减半epochs10训练轮数配合EarlyStopping观察val_loss添加一个ModelCheckpoint回调可以把每轮最好的模型权重存下来而不是最后一轮的权重。常见做法是from keras.callbacks import ModelCheckpoint, EarlyStopping checkpoint ModelCheckpoint(best_model.h5, monitorval_acc, modemax, save_best_onlyTrue) early_stop EarlyStopping(monitorval_loss, patience3) model.fit(X, y, validation_split0.2, epochs20, batch_size64, callbacks[checkpoint, early_stop])save_best_only只在验证集准确率提升时保存权重patience3表示连续3轮验证损失不下降就停止训练。这样能避免训练后段过拟合导致模型退化。3. 核心功能实现情感分类、人工校对与统计图表3.1 在Django中加载模型并完成预测把训练好的best_model.h5和tokenizer.pkl放进项目的model/目录。在views.py里要避免每次请求都加载一次模型Keras加载模型是非常耗时的操作通常放在模块级别import os import pickle from keras.models import load_model from keras.preprocessing.sequence import pad_sequences BASE_DIR os.path.dirname(os.path.dirname(os.path.abspath(__file__))) model_path os.path.join(BASE_DIR, model, best_model.h5) tokenizer_path os.path.join(BASE_DIR, model, tokenizer.pkl) _model load_model(model_path) with open(tokenizer_path, rb) as f: _tokenizer pickle.load(f) LABELS [负向, 中性, 正向] def predict_sentiment(text): cut_text .join(jieba.cut(str(text))) seq _tokenizer.texts_to_sequences([cut_text]) padded pad_sequences(seq, maxlen50) proba _model.predict(padded)[0] idx int(proba.argmax()) return LABELS[idx], round(float(proba[idx]), 4)这里先对输入文本做与训练时一致的分词然后转换成序列并填充到相同的长度。_model.predict返回一个二维数组第一维是样本序号由于我们只传入一条样本所以取[0]。输出的是三个类别的概率分布例如[0.1, 0.2, 0.7]用argmax取出最大概率对应的索引再到LABELS列表里映射中文标签。置信度保留4位小数展示给用户时更有说服力。3.2 留言提交与自动分类流程前端通过表单提交留言视图函数做内容校验和模型推理from django.shortcuts import render, redirect from django.contrib import messages from .models import Message def add_message(request): if request.method POST: content request.POST.get(content, ).strip() if not content: messages.warning(request, 内容不能为空) return redirect(add_message) label, confidence predict_sentiment(content) Message.objects.create( contentcontent, auto_labellabel, manual_label ) return render(request, result.html, { content: content, label: label, confidence: confidence }) return render(request, add_message.html)这里把自动分类结果写入auto_label人工校对字段留空表示还没有人复核。strip()去掉首尾空格避免空白内容浪费模型推理时间。如果不想让匿名用户随意提交可以在视图上加login_required但一般场景下留言本身是公开的不需要强制登录。模板里用简单的HTML展示结果div classresult-box p你的留言{{ content }}/p p系统判断span classbadge{{ label }}/span/p p置信度{{ confidence }}/p /div3.3 数据管理模块人工校对与错误反馈管理员在校对页面可以看到留言列表和自动分类并修改为正确的标签。这个模块的核心是记录“模型错了还是对了”因此要同时保存自动标签和人工标签from django.contrib.admin.views.decorators import staff_member_required staff_member_required def correction_list(request): msgs Message.objects.filter(manual_label).order_by(-created_at)[:200] return render(request, correction_list.html, {msgs: msgs}) staff_member_required def save_correction(request, msg_id): if request.method POST: new_label request.POST.get(new_label) msg Message.objects.get(idmsg_id) old_label msg.auto_label if new_label ! old_label: CorrectionLog.objects.create( messagemsg, old_labelold_label, new_labelnew_label, operatorrequest.user ) msg.manual_label new_label msg.save() return redirect(correction_list)staff_member_required保证只有Django后台标为staff的用户才能访问避免普通用户篡改数据。保存时如果人工标签和自动标签不一样就写一条CorrectionLog。这里有个小技巧在校对页面用manual_label过滤出还没被校对的记录这样管理员不用在几千条历史数据里翻找。如果有低置信度的需求也可以对prob字段排序但当前模型并没有存储置信度所以一般直接按时间倒序。3.4 数据分析模块用图表展示情绪分布统计结果用ECharts展示后端提供JSON接口。例如查询最近30天每天的正、负向数量from django.db.models import Count from django.http import JsonResponse from django.utils import timezone from datetime import timedelta def emotion_trend_json(request): days 30 data [] for i in range(days, -1, -1): day timezone.now().date() - timedelta(daysi) day_start timezone.make_aware(timezone.datetime.combine(day, timezone.datetime.min.time())) day_end day_start timedelta(days1) qs Message.objects.filter(created_at__range(day_start, day_end)) data.append({ date: str(day), neg: qs.filter(auto_label负向).count(), neu: qs.filter(auto_label中性).count(), pos: qs.filter(auto_label正向).count() }) return JsonResponse({data: data})这段代码按天遍历created_at__range是Django ORM的区间查询起点和终点分别是当天0点和次日0点。timezone.make_aware会把本地日期时间加上时区信息否则与USE_TZTrue时存储的UTC时间比较会出错。如果对性能有要求可以使用SQL的GROUP BY DATE(created_at)但项目数据量不大时上面这个写法更直白。前端用fetch拉到数据后交给EChartsfetch(/emotion/trend_json/) .then(res res.json()) .then(res { const dates res.data.map(item item.date); const neg res.data.map(item item.neg); const pos res.data.map(item item.pos); const chart echarts.init(document.getElementById(trendChart)); chart.setOption({ tooltip: { trigger: axis }, legend: { data: [负向, 正向] }, xAxis: { type: category, data: dates }, yAxis: { type: value }, series: [ { name: 负向, type: line, data: neg }, { name: 正向, type: line, data: pos } ] }); });这是一个标准的基础配置。trigger: axis让鼠标滑过时显示当天所有分类的数据比默认的item更适合多折线图。如果你不想前端引入ECharts也可以用matplotlib在服务端生成图片然后返回到模板但交互性差一些。4. 用户权限、公告管理与部署排错4.1 用户管理与权限控制系统内置的用户管理直接复用Django的User模型不需要另建用户表。在settings.py里配置LOGIN_URL /accounts/login/ LOGIN_REDIRECT_URL /普通用户只负责提交留言管理员负责校对和数据分析。给视图加权限限制的标准做法是使用装饰器前面已经用了staff_member_required。如果需要对普通用户隐藏某些页面就用login_required。对于注册功能用django.contrib.auth.forms.UserCreationForm即可from django.contrib.auth.forms import UserCreationForm from django.http import HttpResponseRedirect def register(request): if request.method POST: form UserCreationForm(request.POST) if form.is_valid(): form.save() return HttpResponseRedirect(/accounts/login/) else: form UserCreationForm() return render(request, registration/register.html, {form: form})is_staff和is_superuser是Django自带的两个布尔字段管理员账号在admin后台或shell里设置。在项目初始化时可以用python manage.py createsuperuser创建超级用户这条命令会询问用户名、邮箱和密码。4.2 公告管理模块最小可用实现公告不需要单独做复杂的发布界面直接用Django admin管理即可。注册模型之后管理员登录/admin/就能看到公告的增删改查from django.contrib import admin from .models import Announcement class AnnouncementAdmin(admin.ModelAdmin): list_display (title, is_active, created_at) list_filter (is_active,) search_fields (title,) admin.site.register(Announcement, AnnouncementAdmin)在首页取最新三条公告展示给普通用户latest_notices Announcement.objects.filter(is_activeTrue).order_by(-created_at)[:3]is_active字段控制是否在前台显示这样管理员可以提前编辑好多条公告定时切换生效不需要直接删除记录。对于“是否展示”的筛选注册了list_filter之后admin列表页会提供一个下拉选择框方便快速过滤。4.3 Django MySQL 连接配置与中文编码问题数据库使用MySQL时settings.py中的配置是DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: emotion_db, USER: root, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, OPTIONS: {charset: utf8mb4}, } }charset必须写成utf8mb4这是MySQL中完整的UTF-8编码支持汉字以及Emoji。如果写成utf8遇到四字节表情符号时会出现“Incorrect string value”报错。另外MySQL 8.0默认使用caching_sha2_password认证插件Django的mysqlclient可能连不上解决办法是在MySQL命令行中执行ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY your_password;或者使用pymysql作为驱动并在项目的__init__.py里写import pymysql pymysql.install_as_MySQLdb()但我个人更推荐直接安装mysqlclient因为pymysql是纯Python实现的性能略差而且某些Django版本会发出警告。连接成功后执行python manage.py migrate创建系统表再执行python manage.py runserver 0.0.0.0:8080就可以访问了。4.4 模型推理与部署中的常见错误排查运行情感分类时最常见的现象是预测结果全部集中在某一类比如所有留言都被判成“中性”。原因通常是训练样本极度不平衡或者测试环境与训练环境的分词版本不一致。如果不一致留言分词后得到的词序列完全不同模型输出的概率分布会偏向训练集中占比高的类别。解决方法是保证jieba的词典一致尽量使用同一版本。另一个坑是模型加载后在多个进程中被重复加载。Django development server默认单进程但部署到Gunicorn时如果配置了多个worker每个worker都会加载一份best_model.h5内存占用成倍增加。常见做法是把模型加载放到ready()方法中或者用一个单独的推理服务通过HTTP接口调用。在小项目中也可以限制Gunicorn的worker数量为2。内存不够时可以降低Embedding维度比如128改成64LSTM单元数从64改成32准确率损失不大但推理速度会明显提升。如果训练时用的是maxlen50预测时也必须显式指定50否则pad_sequences会默认补到整批数据里的最大长度而这个长度在单条请求里就是这条留言的长度和模型输入维度不匹配直接报错。5. 进阶用预训练词向量与注意力机制提升分类效果5.1 用预训练词向量替换随机Embedding基础模型的Embedding是随机初始化后训练的在小数据集上容易过拟合。更常见的做法是加载公开的中文词向量比如用gensim加载维基百科语料预训练的向量。加载后的矩阵要填进Keras的Embedding层import numpy as np from gensim.models import KeyedVectors wv KeyedVectors.load_word2vec_format(chinese_w2v.vec, binaryFalse) embedding_matrix np.random.uniform(-0.25, 0.25, (num_words, embed_dim)) for word, i in tokenizer.word_index.items(): if i num_words and word in wv: embedding_matrix[i] wv[word]代码里num_words必须和Tokenizer的num_words一致否则索引错位。未匹配到的词保留随机初始化值这样即使词表里有少量新词也不会报错。模型定义时传入预训练矩阵model.add(Embedding(num_words, embed_dim, weights[embedding_matrix], input_lengthmaxlen, trainableTrue))weights参数直接指定初始化矩阵trainableTrue表示让词向量在训练中继续微调。如果训练语料够大微调效果好如果语料只有几千条建议设成False只训练LSTM部分否则微调过程中随机初始化的词向量会拖慢收敛速度。5.2 一个轻量的注意力层实现注意力机制帮助模型聚焦句子里的关键部分。Keras本身没有开箱即用的Attention层可以自己写一个很轻的版本from keras import backend as K from keras.layers import Layer class Attention(Layer): def build(self, input_shape): self.W self.add_weight(nameatt_weight, shape(input_shape[-1], 1), initializeruniform, trainableTrue) self.b self.add_weight(nameatt_bias, shape(input_shape[1], 1), initializeruniform, trainableTrue) super(Attention, self).build(input_shape) def call(self, x): e K.tanh(K.dot(x, self.W) self.b) a K.softmax(e, axis1) output x * a return K.sum(output, axis1)这个层做的事情是对LSTM输出的每个时间步向量打分经过softmax得到权重加权求和整个序列。使用时需要注意LSTM要返回所有时间步的隐藏状态也就是return_sequencesTruemodel.add(Embedding(num_words, embed_dim, input_lengthmaxlen)) model.add(LSTM(64, return_sequencesTrue)) model.add(Attention()) model.add(Dropout(0.3)) model.add(Dense(3, activationsoftmax))加入Attention之后模型的参数变少了吗并没有它多了一组s和b权重但序列中的关键信息会被更有效地利用。调参时不要改动太大保持原有Embedding维度和LSTM单元数只增加Attention层观察验证集准确率是否提升。5.3 效果对比与验证方法为了验证改动是否有效需要用相同的训练集切分和随机种子做对照实验import random import numpy as np np.random.seed(42) random.seed(42)固定随机种子后分别训练基础模型和加了预训练词向量、Attention的模型用混淆矩阵看类别错误分布from sklearn.metrics import classification_report y_pred model.predict_classes(X_test) print(classification_report(y_test, y_pred, target_names[负向, 中性, 正向]))常见的结果是随机Embedding的F1在0.85左右换成预训练词向量后到0.87再加Attention到0.88。提升不是巨大的因为短留言本身信息密度高结构相对简单。但如果你把训练数据中“中性”样本随机删除部分让三类样本数量接近整个模型的分数还会再往上走。记住情感分类的瓶颈通常不在网络结构而在训练数据的噪声和分布清洗规则、标注一致性这些环节比花哨的模型更容易带来稳定收益。当你想复用这份源码时优先替换训练数据和清洗规则而不是一上来就调模型结构。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。