简介:这是一套基于Python的情感分析系统完整源码,主要面向具备基础Python知识、希望入门自然语言处理或需要完成课程设计/毕业设计的开发者。系统围绕数据清洗、模型训练、情感分类三大核心展开,并借助Flask框架提供Web接口,可直接对输入文本输出正向、负向或中性的情感判断,适合用于舆情分析、评论情感识别等场景。资源包共包含34个文件,以Python脚本为主(22个py文件),同时配有训练用文本语料(txt)、需求说明、验证数据表(xlsx)以及配置文件,压缩包仅1.6MB,结构轻量且便于快速部署。目前已有77人学习下载,读者可获得从原始语料到模型应用的全流程源码,包括数据预处理、情感分类、模型训练与验证、Flask接口部署等模块,能帮助理解情感分析系统的工程实现,并便于二次开发或课程设计参考。
1. 基于Python的情感分析系统:先别急着跑,看清单张判废
拿到一个名为“基于Python的情感分析系统”的源码压缩包,第一反应别是双击解压然后立刻运行主程序。我在某公司接过一个评论情感分析的项目包,第一眼看上去结构完整、依赖齐全,结果直接跑训练脚本卡了半小时也不出结果,后来才发现是模型文件缺失、数据编码不匹配。从那以后我养成了习惯:任何源码包到手,先花十分钟看三样东西——README 或说明文档、requirements.txt、源码目录结构,再决定往哪个方向使劲。这三样能让你对“这个系统到底能不能跑”有个基本判断,避免白踩坑。
这个系统解决的是典型的短文本情感分类问题:把电商评论、外卖评价、新闻评论判成正向、负向或中性,并输出一个情感分数,为舆情监控、客服分流、差评预警这类业务提供依据。适合谁?适合已经有点 Python 基础、想拿现成源码改造成自己数据的开发者。下面按这类项目最常见的落地方案拆开讲:先让它转起来,再读核心代码,再谈调参、避坑和换数据迁移。
2. 解压与首跑:让源码先转起来的最小命令与依赖核对
看到“源码”两个字,大多数人的第一反应是直接运行 main.py。这一步踩坑率最高,因为情感分析项目对环境依赖极其敏感:jieba 的版本差异能改动分词边界,scikit-learn 的版本跨度能让加载模型直接报错。先把目录结构认清楚,再装依赖,再决定先跑哪条命令,顺序反了就会陷入“报错—搜错—乱改”的循环。
2.1 目录结构:这本源码包常见的三层布局
一个标准的基于 Python 的情感分析项目,不管压缩包叫什么名字,内部布局通常可以归成三层:数据层、模型层、源码层。数据层放原始评论和标注好的训练集,一般是 csv 或 txt;模型层放训练产物,通常是 pkl 或 joblib 后缀,也可能因为体积过大没被打进包里;源码层放训练脚本、预测脚本、公共工具函数。先用 tree 看一眼最直观:
# 在解压后的项目根目录执行 tree -L 2. ├── data │ ├── train.csv │ └── test.csv ├── model │ └── sentiment_model.pkl ├── src │ ├── train.py │ ├── predict.py │ └── utils.py ├── requirements.txt └── README.mddata 目录存放训练集和测试集。model 目录里的 pkl 文件是训练产物,如果解压后发现 model 目录是空的,别慌,这大概率是“只给源码不附带模型”的包,需要先训练再预测。源码目录里通常有训练和预测两个入口,utils.py 承担清洗、分词这类公共逻辑。这个布局不是唯一标准,但多数项目能对号入座。
还需要留意一点:有些作者习惯把训练入口叫 train.py,预测入口叫 predict.py,也有叫 run.py、demo.py 的。不要因为只看到一个 run.py 就以为没有预测入口,很多包把训练和预测写在同一个文件里,靠命令行参数区分。判断方法是打开文件看函数定义和if __name__ == "__main__"分支,这比猜文件名更可靠。
2.2 用 requirements.txt 先锁定依赖,别用全局环境裸装
依赖安装是最容易翻车的环节。源码包会用 requirements.txt 列出依赖,但列出的版本号往往是作者当时的环境快照。装得太新可能接口变了,装得太旧可能和当前 Python 版本冲突。我一般做两件事:先建虚拟环境,再按 requirements 安装,避免把全局环境里已装的包搅进来。
# 创建并激活虚拟环境 python -m venv .venv # Windows 下激活命令是 .venv/Scripts/activate # Linux / macOS 下是 source .venv/bin/activate source .venv/bin/activate # 安装依赖 pip install -r requirements.txtrequirements.txt 里常见的是 jieba、scikit-learn、pandas、numpy,偶尔会有 snownlp 或 textblob。一个典型的文件长这样:
jieba==0.42.1 scikit-learn==1.2.2 pandas==1.5.3 numpy==1.23.5版本锁死就一定安全吗?不完全是。scikit-learn 1.2.2 在 Python 3.11 下正常,在 Python 3.12 下就可能安装失败,需要换成 1.3 以上版本。如果 requirements 里没写版本号,更要注意,直接装最新版不一定能跑通。装完依赖后,建议跑一句验证命令,提前暴露“包装了但 Python 解释器不是同一个”的问题,这类问题在虚拟环境里尤其隐蔽。
python -c "import jieba, sklearn, pandas; print('deps ok')"依赖全部就绪后,再确认当前解释器路径确实是虚拟环境里的 python。有些开发工具会自动切回全局解释器,导致明明装了包却找不到。这个检查几十秒,能省掉后面大量的奇怪报错。
2.3 跑通最小可执行路径:先预测,后训练
依赖装好以后,最省力的路径是先跑预测,再跑训练,不要一上来就执行训练脚本。预测脚本一般只需要一个模型文件和一段测试文本,依赖资源最少,跑通它说明环境没问题、代码能执行、数据格式能被读取。训练脚本要读全量数据、做向量化,耗时更长,出错面也大得多。
# 先看预测脚本暴露出的参数 python src/predict.py --help # 常见用法是传入一句文本,输出情感标签和分数 python src/predict.py --text "这家店的配送很快,包装也很严实"如果这条命令能输出“正向”或一个 0 到 1 之间的情感分数,环境基本就通了。有的项目把预测入口做成交互式,直接输入文本回车出结果;有的做成命令行参数。不管哪种形态,最小验证逻辑都一样:让一段短文本完整走一遍加载模型、清洗、分词、向量化、分类的流程。
如果预测报错,最常见的两个原因是模型文件缺失、模型特征维度与当前代码不一致。这类报错一般比较直白,比如 FileNotFoundError 或 ValueError。先别急着查代码逻辑,按第五章的排查清单顺序过一遍,多数能定位到问题源头。
预测跑通后,再跑训练脚本就从容很多。训练命令一般长这样:
# 训练并保存模型 python src/train.py --data data/train.csv --save model/sentiment_model.pkl训练顺利跑完后,会用新模型文件覆盖旧文件。这时再执行一次预测脚本,验证新模型效果是否正常。整个过程走完,你对这份源码的骨架就建立了:它本质上就是一个“训练—保存—加载—预测”的闭环。后面几章就围绕这个闭环拆开讲代码实现。
3. 核心代码走读:从原始文本到情感分数的四步流水线
把系统跑起来之后,最重要的事情是读懂数据从输入到输出的路径。情感分析系统的核心不是某个高深的算法,而是四步流水线:清洗、分词、特征化、分类。每一步都影响最终情感分数,任何一步在训练与预测时行为不一致,结果就会漂。下面按这类项目最常见的代码写法逐段拆开。
3.1 数据清洗:把评论里的噪声和空值处理干净
拿到的原始标注数据,不太可能整整齐齐。训练数据一般是爬下来的评论或表单里填的内容,噪声很杂:URL、Emoji、@用户、HTML 标签、重复标点。这些噪声如果不清理,它们会作为特征进入模型,造成两个后果:一是词汇表膨胀、训练变慢;二是预测时遇到训练时没见过的噪声,分类结果随机飘。
import re import pandas as pd def clean_text(text: str) -> str: """清洗单条文本,返回干净的中文短文本""" if not isinstance(text, str): return "" # 去掉 HTML 标签 text = re.sub(r"<[^>]+>", "", text) # 去掉 URL text = re.sub(r"http[s]?://\S+", "", text) # 去掉 @ 用户名 text = re.sub(r"@\w+", "", text) # 用白名单保留中文、英文、数字和常用标点,其余删掉 text = re.sub(r"[^\u4e00-\u9fa5a-zA-Z0-9,。!?、]", "", text) # 折叠连续重复标点 text = re.sub(r"([,。!?、])\1+", r"\1", text) return text.strip() df = pd.read_csv("data/train.csv") df["clean_comment"] = df["comment"].apply(clean_text) print(df.head())clean_text 里做的五步操作,每步都可能对模型效果产生具体影响。去标签和去 URL 是常规操作;去 @ 是为避免用户名成为高权重词;去表情要谨慎,如果数据显示“笑哭”“狗头”这类表达情感丰富,一刀切删掉反而丢信息,更好的做法是映射成[哈哈]这类文本标记,但初版先删掉能加快迭代。
需要重点解释的是正则中间那一步:[^\u4e00-\u9fa5a-zA-Z0-9,。!?、]是白名单思路,不是黑名单。白名单的好处是好理解,坏处是你必须手动维护允许的字符范围,比如遇到破折号和引号,就要往里加。这类项目里最常见的坑是清洗函数只在训练集上用,预测脚本里没有复用,导致两边特征不一致,这个问题在第五章会专门展开。
3.2 中文分词与停用词:分词结果决定模型的天花板
情感分析和英文按空格切分不同,中文没有天然分隔符。分词做得好不好,直接影响“惊喜”和“惊吓”能不能被切成一个整体词。“不错”和“不 错”是两种完全不同的特征表示。这类项目里最常见的分词工具是 jieba,搭配停用词表一起使用。
import jieba # 加载用户词典,比如行业术语、品牌名、网络新词 jieba.load_userdict("data/userdict.txt") def cut_words(text: str) -> list: """分词并过滤停用词""" # 停用词表,每行一个词 stopwords = set() with open("data/stopwords.txt", encoding="utf-8") as f: for line in f: stopwords.add(line.strip()) words = jieba.cut(text, cut_all=False) result = [] for w in words: w = w.strip() if not w: continue # 过滤停用词和单字 if w in stopwords or len(w) == 1: continue result.append(w) return result sample = "这家店的配送速度真是没话说" print("/".join(cut_words(sample)))jieba.cut 的 cut_all 参数值得单独说。常见错误是把它设成 True 拿全模式结果。全模式适合搜索引擎召回,不适合做分类特征,因为同一个词会被切出多个组合,特征空间会变得很脏,模型很难学到稳定模式。这里用 False 精确模式,每个词只保留一种切法。
停用词表别直接套网上的通用词表。通用词表往往把“不”“没”这种否定词过滤掉,但情感分析里“不好”“不行”恰恰是强负向信号。我一般会先用自己的训练集跑一遍词频,把前 100 个高频且无语义区分的词挑出来加进停用词表,而不是靠猜。分词这一步的效果直接决定后面特征的质量,是整条流水线里最值得花时间打磨的环节。
3.3 特征表示与模型选择:从词频向量到分类器
分词完成后,文本变成了词列表,但模型吃的是数值矩阵。这里要做特征向量化。这类情感分析源码里最常见的两套方案:一是 TfidfVectorizer 做加权词频向量,配逻辑回归或朴素贝叶斯;二是直接用 snownlp 这种自带模型的黑匣子。前者可控性强,效果和速度均衡,推荐先用它。
from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression corpus = [ "这家店的配送速度真是没话说", "等了一个小时还没送到,差评", "包装很严实,没有破损", "味道一般,但价格便宜" ] # 预处理流水线:分词后用空格连接,形成模型可读的语料格式 def preprocess(texts): return [" ".join(cut_words(t)) for t in texts] X_train = preprocess(corpus) vectorizer = TfidfVectorizer(ngram_range=(1, 2), max_features=5000) X_vec = vectorizer.fit_transform(X_train) print(X_vec.shape) y_train = [1, 0, 1, 1] # 1 正向,0 负向 model = LogisticRegression(C=1.0, max_iter=1000) model.fit(X_vec, y_train)这里 ngram_range=(1, 2) 让特征既包含单个词也包含相邻词组合,“不 好”这种两词组合能作为一个特征被保留,对情感判断很有价值。max_features 把词表规模限制在 5000,防止低频词把模型带偏。C 值是逻辑回归的正则化强度,C 越小惩罚越大,这个参数在下一章调参部分会继续展开。
选 TF-IDF 而不是 CountVectorizer,是因为情感倾向强的词往往集中在少数文本里,TF-IDF 正好把这种词的权重放大。再往上一层可以用 word2vec 把词映射成稠密向量,但初版没必要,逻辑回归配 TF-IDF 在几千条短文本上已经能打出不错的基准线。对这份源码来说,读懂上面这条流水线比看懂任何一个模型算法的公式更重要,后续调参和换数据都围着它转。
4. 训练与调参:用测试集卡准准确率,再谈上线
训练脚本能跑通,只是起点。真正决定系统能不能用的是数据切分对不对、参数会不会调、评估指标会不会看。这三个问题处理不好,再新的模型也白搭。调参在情感分析这类短文本任务上,有时候确实有点玄学,但至少有几个参数是每次都要优先盯住的。
4.1 训练/验证/测试的切分比例与随机种子问题
训练脚本里最常见的错误是拿全量数据训练,然后用同一批样本看准确率。这是典型的信息泄漏,模型记的是训练样本,不是泛化能力。这类项目的正确做法是把数据切分成训练集、验证集、测试集。数据量小时可以不单独留验证集,用交叉验证来调参,但测试集必须锁死不动,直到最后一刻才允许碰。
from sklearn.model_selection import train_test_split # 原始文本和标签,假设已经清洗并分词完成 X_text = df["clean_comment"].tolist() y = df["label"].tolist() # 先切测试集,并保证标签在训练和测试中分布一致 X_train, X_test, y_train, y_test = train_test_split( X_text, y, test_size=0.2, random_state=42, stratify=y )stratify=y 是这里最容易忽略的参数。如果原始数据里 90% 是好评、10% 是差评,不分层抽样会导致切出来的某个子集里没有差评样本,模型学不到负向模式。random_state=42 固定随机种子,保证每次跑出来的切分结果一致,这样调参时才能确定准确率变化是参数引起的,而不是样本换了一批。
在情感分析这类短文本任务里,我的默认切分习惯是训练 70%、验证 10%、测试 20%。数据量只有几百条时,测试集缩到 10% 也可以,但最少不能低于 50 条,否则测试集上的指标波动会很大,改了参数也看不出真实差异。数据量不够的时候,先把数据攒够再谈调参更务实。
4.2 三个优先调的参数:C 值、n-gram 范围、特征数量
训练脚本跑通后,真正耗时间的调参才开始。不要同时调一堆参数,先抓住影响最大的三个:TfidfVectorizer 的 ngram_range、max_features,以及逻辑回归的 C 值。用网格搜索能一次看趋势,但样本量大时网格搜索很慢,我一般手动固定两个,只扫一个。
from sklearn.pipeline import Pipeline from sklearn.model_selection import GridSearchCV pipeline = Pipeline([ ("vec", TfidfVectorizer()), ("clf", LogisticRegression(max_iter=2000)) ]) # 重点调这三个维度 param_grid = { "vec__ngram_range": [(1, 1), (1, 2), (1, 3)], "vec__max_features": [2000, 5000, 10000], "clf__C": [0.1, 1.0, 10.0] } # cv=5 交叉验证,用 f1_macro 防止类别不平衡对评估的干扰 search = GridSearchCV(pipeline, param_grid, cv=5, scoring="f1_macro", n_jobs=-1) X_train_vec = [" ".join(cut_words(t)) for t in X_train] search.fit(X_train_vec, y_train) print(search.best_params_) print(search.best_score_)这三个参数的调法有规律,不是背数字。ngram_range 从 (1, 1) 放宽到 (1, 2) 通常能让准确率上升,因为“不好”“太差”这类词组合有明确情感含义;再到 (1, 3) 不一定继续上升,短文本里三连词组出现次数不够,特征太稀疏。max_features 限制字典大小,太大把低频噪声纳入,太小丢掉有效词。C 值控制正则化强度,情感数据量一般不大,C 取 0.1 到 1 区间表现稳定,过大容易过拟合,过小则欠拟合。
需要注意网格搜索这套流程要放在预处理之后。分词不能塞进 Pipeline 里做 fit,因为向量化需要等全部文本完成分词后才能建词表,Pipeline 只吃字符串数组,jieba 的输出是非定长列表。分词放在管道外面先拼成带空格的语料,这既是代码可读性的问题,也是避免潜在 bug 的工程习惯。
4.3 评估指标:只看准确率,情感分析容易自欺欺人
情感数据天然不平衡。电商评论里好评占七八成,模型无脑全判正向,准确率都能到 70%。这一点在评估时特别容易翻车。看指标时不能只看 accuracy,要看分类报告里的 precision、recall、f1-score,以及混淆矩阵。
from sklearn.metrics import classification_report, confusion_matrix y_pred = search.predict([" ".join(cut_words(t)) for t in X_test]) print(classification_report(y_test, y_pred, target_names=["负向", "正向"])) print(confusion_matrix(y_test, y_pred))混淆矩阵对角线越大越好。负向这一类的 recall 如果很低,说明大量差评被当成了好评。这在情感分析场景里是危险的,宁可漏一些好评,也不能漏掉差评,因为差评才是舆情预警最需要抓住的信号。如果发现负向 recall 过低,第一个要调的不是模型参数,而是标签——确认标注的人是不是把“一般”“还行”这种中性评语也标成了正向。
用 F1 而不是用准确率作为选择模型的依据。网格搜索里 scoring 设为 f1_macro,能公平地把两个类别放在同等权重上比较。如果业务上更关心负向,把 scoring 改成 f1 或 recall 并指定正例标签,这个选择比调整后续任何参数都更直接地影响系统行为。调参调到最后,你会发现情感分析项目里“怎么评估”比“怎么调参”更决定实战效果。
5. 情感分析系统避坑手册:五条让你翻车的常见问题
环境跑通了、代码也读完了,接下来才是真正考验工程经验的部分。情感分析系统里有一批高度重复的坑,几乎每次接手新数据、换新环境都会遇到。下面五条是最常见也最有代表性的,按“现象→原因→解决”写清楚。
5.1 编码错了:训练集一读进来就崩
现象:运行训练脚本时,读到数据文件直接抛 UnicodeDecodeError,或者模型训练完成但预测出来的文本全是乱码。
原因:csv 或 txt 文件是从某个老系统里导出的,编码是 GBK 或 GB2312,而代码里 open 默认用了 utf-8,或者反过来。Windows 和 Linux/macOS 混用环境里这类问题最频繁,源码包里给的数据文件很容易踩中。
解决:不猜编码,先探测再指定。用file data/train.csv命令或 Python 的 chardet 库查看实际编码,然后在代码里显式指定。常见的还有open(path, encoding="utf-8-sig")的方式去掉 BOM 头部。更彻底的方案是统一口径:代码里一律指定 encoding,数据文件统一重新导出为 UTF-8,两边对齐。
5.2 模型文件加载失败:pkl 里存的东西和代码对不上
现象:预测脚本加载模型时报错,ValueError 提示特征数量不一致,或者 AttributeError 说模型对象没有 predict 属性。
原因:模型文件是旧版 scikit-learn 训练的,现在用新版加载,pickle 的二进制结构不兼容;或者模型文件里存的是向量化器,但代码把它当成分类器用,接口对不上。
解决:先跑python -c "import sklearn; print(sklearn.__version__)"看当前版本,再去源码里找训练环境的版本痕迹。最稳妥的办法是不要依赖包里自带的 pkl,直接用源码和给定数据重新训练一版模型。修改预测脚本时,load 出来先print(type(model))确认对象类型,再调接口,避免在类型错误上反复试。
5.3 分词结果在训练和预测时不一致
现象:模型在测试集上效果不错,但投入预测时准确率明显掉下来。仔细检查发现“没话说”在训练时被切成“没话/说”,预测时被切成“没/话说”。
原因:jieba 的词典是全局状态。如果训练脚本和预测脚本不是共用同一份用户词典,或者加载顺序不同,同一个词的分词结果会不一样。另外 jieba 版本升级也会改变分词行为,旧模型配新 jieba 就会出现静默漂移。
解决:把分词逻辑收敛到一个函数里,训练和预测共用同一个模块,用户词典和停用词表路径统一从配置文件读取。然后在预测脚本里加一个冒烟测试:断言“这家店不错”必须输出既定词序列,防止 jieba 升级后悄悄改变分词行为。这个问题排查起来很隐蔽,但修复方式很简单,难在意识到“分词不一致”这个根因。
5.4 类别不平衡:好评太多,差评被模型无视
现象:分类报告显示准确率 80% 多,模型看起来没问题,但实际用起来差评几乎漏光,自动标注把很多负面反馈标成正向。
原因:训练集本身严重不平衡,好评占 85% 以上。逻辑回归在无加权的情况下学习了先验概率,为了总体准确率牺牲了差评分类的召回。
解决:训练时给少数类加权重,逻辑回归里设置class_weight="balanced",或者只对负向和中性样本做二分类。数据切分时必须要用 stratify。评估层面把“不漏差评”作为硬指标,用混淆矩阵盯负向列。很多时候补数据比调模型更有效,差评样本少就人工补标注,至少补到总样本的 20%,模型的负向召回才会有实质改善。
5.5 预测脚本没有复用清洗逻辑
现象:训练和离线评测都正常,一接线上数据就出现大量“ ”“转发微博”乱码被当成特征,预测结果开始失控。
原因:数据清洗函数只在训练脚本里定义了一份,预测脚本复制粘贴了一段,或者预测脚本直接跳过了清洗步骤。两边正则不一致,导致线上文本进入模型前走了完全不同的预处理链路。
解决:把 clean_text 和 cut_words 抽到 utils.py 统一导入,训练和预测的预处理顺序严格一致:清洗→分词→停用词过滤→向量化→分类。如果线上问题已经发生,先比较训练集与线上数据在词表上的覆盖率,把相差较大的脏词模式加进清洗逻辑。这条是从 demo 走向可用最难迈过的一道门槛,看起来是代码组织问题,本质是工程规范问题。
6. 换数据进阶:把旧模型迁移到新领域的三个技巧
6.1 先校准标签口径,再谈迁移
把已经跑通的外卖评论模型迁移到商品评论,别直接拿旧模型预测新数据。领域之间的表达差异很大:外卖评论说“快”,商品评论说“实物与图片不符”,旧模型完全没有见过这类表达。迁移前先抽 10% 的新数据让旧模型预测,人工校验错误样本,统计高频错词。你会发现“还行”“一般”这类泛化词在新领域里的标签口径可能不同,之前标记成“正向”的口径,到商品评论里就得改成“中性”。这一步不做,后面调参数都是白费。
6.2 用自定义词典和否定词增强找回领域表达
领域迁移里最有效的低成本手段,不是换模型,而是改用户词典和预处理规则。
import re import jieba # 载入新领域专有词,比如“掉漆”“续航”“质感” jieba.load_userdict("data/product_userdict.txt") # 简单否定增强:把“不好”转成“不_好”,让它成为独立特征 def negate(text: str) -> str: for item in ["不", "没", "无", "别"]: text = re.sub(item + "([\u4e00-\u9fa5])", item + "_" + r"\1", text) return text这个 negate 函数在“不”和后面第一个字之间加下划线,使“不_好”“没_用”变成有区分度的独立特征,而不是让“不”和“好”在 n-gram 里被切散。配合自定义词典使用,通常能让负向样本的召回提升几个点,成本只是几十行代码。这个技巧不依赖大模型,也不依赖 GPU,在任何一套基于词向量的情感分析系统里都能复用。
我在这类项目上吃过亏:某次直接把外卖情感模型迁移到二手商品评论上,准确率掉了十几个点,起初以为是模型不行,后来才发现是新领域里“一般”“还行”的标签口径完全变了,又赶上否定词没处理,差评几乎全漏。后来抽出 500 条新样本增量训练,配合自定义词典和否定增强才把效果救回来。从那以后我养成了习惯:换领域先换数据和词表,不要一上来就换模型。希望这些经验帮你在自己的情感分析系统上少走一段弯路。
本文还有配套的精品资源,点击获取