简介:这是一份基于循环神经网络的情感分类系统项目实战资源,主要面向计算机毕业设计、课程设计,也适合希望掌握网络开发与深度学习结合的 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_length=10, blank=True, verbose_name='自动分类') manual_label = models.CharField(max_length=10, blank=True, verbose_name='人工校对') created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') class Meta: ordering = ['-created_at'] class CorrectionLog(models.Model): message = models.ForeignKey(Message, on_delete=models.CASCADE, verbose_name='留言') old_label = models.CharField(max_length=10, verbose_name='原分类') new_label = models.CharField(max_length=10, verbose_name='新分类') operator = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, verbose_name='操作人') updated_at = models.DateTimeField(auto_now=True, 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容易过拟合。训练前先对中文做分词,这里用jieba:
import 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_words=5000) tokenizer.fit_on_texts(df['cut']) sequences = tokenizer.texts_to_sequences(df['cut']) X = pad_sequences(sequences, maxlen=50) y = pd.get_dummies(df['label']).values代码先把每条留言用jieba切词,分词之间用空格拼接,这是Keras的Tokenizer默认的输入格式。num_words=5000表示词典最大保留5000个词,超过部分会被忽略,避免生僻字把词表撑得过大。pad_sequences的maxlen=50是序列的固定长度,超过50个词截断,不足50补0,LSTM输入必须是定长矩阵。
然后构建三层结构的RNN模型:
from keras.models import Sequential from keras.layers import Embedding, LSTM, Dense, Dropout model = Sequential() model.add(Embedding(5000, 128, input_length=50)) model.add(LSTM(64, return_sequences=False)) model.add(Dropout(0.3)) model.add(Dense(3, activation='softmax')) model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy']) model.fit(X, y, validation_split=0.2, epochs=10, batch_size=64)Embedding层把每个词的索引映射成128维向量,LSTM层是核心循环结构。return_sequences=False表示只返回最后一个时间步的输出,这个向量被当成整句话的语义表示。Dropout随机丢弃30%的神经元,降低过拟合风险。训练完成后执行model.save('rnn_emotion.h5'),同时用pickle保存tokenizer对象,后面Django预测时需要用到。
训练参数可以按下面的表格调整,不同场景下的推荐值不完全一样:
| 参数 | 默认值 | 说明 |
|---|---|---|
| embedding_dim | 128 | 词向量维度,小语料可以降到64 |
| lstm_units | 64 | LSTM单元数,越大拟合能力越强但越容易过拟合 |
| maxlen | 50 | 序列最大长度,短留言场景设为30即可 |
| batch_size | 64 | 批次大小,显存或内存不足时减半 |
| epochs | 10 | 训练轮数,配合EarlyStopping观察val_loss |
添加一个ModelCheckpoint回调,可以把每轮最好的模型权重存下来,而不是最后一轮的权重。常见做法是:
from keras.callbacks import ModelCheckpoint, EarlyStopping checkpoint = ModelCheckpoint('best_model.h5', monitor='val_acc', mode='max', save_best_only=True) early_stop = EarlyStopping(monitor='val_loss', patience=3) model.fit(X, y, validation_split=0.2, epochs=20, batch_size=64, callbacks=[checkpoint, early_stop])save_best_only只在验证集准确率提升时保存权重,patience=3表示连续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, maxlen=50) 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( content=content, auto_label=label, 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 class="result-box"> <p>你的留言:{{ content }}</p> <p>系统判断:<span class="badge">{{ label }}</span></p> <p>置信度:{{ confidence }}</p> </div>3.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(id=msg_id) old_label = msg.auto_label if new_label != old_label: CorrectionLog.objects.create( message=msg, old_label=old_label, new_label=new_label, operator=request.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(days=i) day_start = timezone.make_aware(timezone.datetime.combine(day, timezone.datetime.min.time())) day_end = day_start + timedelta(days=1) 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_TZ=True时存储的UTC时间比较会出错。如果对性能有要求,可以使用SQL的GROUP BY DATE(created_at),但项目数据量不大时,上面这个写法更直白。
前端用fetch拉到数据后交给ECharts:
fetch('/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_active=True).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 'root'@'localhost' 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改成64,LSTM单元数从64改成32,准确率损失不大,但推理速度会明显提升。如果训练时用的是maxlen=50,预测时也必须显式指定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', binary=False) 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_length=maxlen, trainable=True))weights参数直接指定初始化矩阵,trainable=True表示让词向量在训练中继续微调。如果训练语料够大,微调效果好;如果语料只有几千条,建议设成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(name='att_weight', shape=(input_shape[-1], 1), initializer='uniform', trainable=True) self.b = self.add_weight(name='att_bias', shape=(input_shape[1], 1), initializer='uniform', trainable=True) super(Attention, self).build(input_shape) def call(self, x): e = K.tanh(K.dot(x, self.W) + self.b) a = K.softmax(e, axis=1) output = x * a return K.sum(output, axis=1)这个层做的事情是:对LSTM输出的每个时间步向量打分,经过softmax得到权重,加权求和整个序列。使用时需要注意LSTM要返回所有时间步的隐藏状态,也就是return_sequences=True:
model.add(Embedding(num_words, embed_dim, input_length=maxlen)) model.add(LSTM(64, return_sequences=True)) model.add(Attention()) model.add(Dropout(0.3)) model.add(Dense(3, activation='softmax'))加入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。提升不是巨大的,因为短留言本身信息密度高,结构相对简单。但如果你把训练数据中“中性”样本随机删除部分,让三类样本数量接近,整个模型的分数还会再往上走。记住:情感分类的瓶颈通常不在网络结构,而在训练数据的噪声和分布,清洗规则、标注一致性这些环节比花哨的模型更容易带来稳定收益。当你想复用这份源码时,优先替换训练数据和清洗规则,而不是一上来就调模型结构。
本文还有配套的精品资源,点击获取