简介:这是一份面向计算机相关专业学生与企业研发人员的弱监督深度学习情感分析研究算法项目,基于Stacking框架完成多模型融合与情感分类实战。资源定位在项目实战与学习借鉴,适配课程设计、毕业设计及初期项目立项演示等场景。压缩包共9个文件,包含6个Python脚本、2个Excel数据表格和1个Markdown说明文档,脚本覆盖LSTM、CNN、RNN、贝叶斯、SVM及Stacking融合分析等核心模型,数据表格提供正负样本情绪标注,说明文档梳理了项目结构、运行流程与实验结果,包体仅994KB,轻量易用。目前已有72人学习下载。下载后可从数据预处理、单模型基线到Stacking集成逐步对照源码理解弱监督情感分析算法流程,并直接扩展用于其他文本分类任务,适合希望快速上手深度学习项目或复现对比实验的读者。
1. 弱监督情感分析为什么绕不开Stacking:先搞清楚这套源码解决什么问题
做情感分析项目时,最卡人的往往不是模型选型,而是标注数据不够。人工标注一条评论的情感倾向,成本高、周期长,尤其在电商、舆情、金融评论这类垂直场景里,标注一致性还会被领域词带偏。弱监督在这个背景下被反复拿出来用:用情感词典、规则或者远程监督自动生成一批带标签数据,让深度学习模型先跑起来。但弱监督有个天生的毛病——标签有噪声,同一个模型在噪声标签上训久了,很容易把错误也背下来。这时用Stacking框架把多个深度学习模型堆在一起,让第二层模型学会如何信任不同基学习器的输出,能明显压住单模型的抖动。
这套源码本质上就是把"弱监督生成伪标签"和"Stacking集成"这两件事串成一条完整Python链路:先造出可用训练集,再训练多个深度学习基模型,最后用交叉验证生成元特征并训练融合模型。适合手里有大量无标注文本、又没有预算做人工标注的团队或个人;也适合想复现论文中Stacking弱监督流程、但又不想从零拼代码的同学。读完这篇,你能照着把环境配起来、把伪标签生成、基模型训练、融合层搭建这条链路跑通,并知道哪些参数值得动、哪些坑会反复踩。
2. 先从原理上把两件事拆清:弱监督标签从哪来,Stacking为什么压得住噪声
2.1 弱监督情感分析的三种常用标签来源:词典、规则与远程监督
弱监督最省事的做法是用情感词典给每条文本打分。常见的是基于通用情感词典(如知网Hownet情感词典、大连理工大学情感词汇本体库),统计句子中正向词和负向词的个数与权重,加权得到一个情感分数,分数大于阈值标为正向,否则为负向。这种方法对电商评论这种表达直白的文本比较有效,但对"难吃到我想给差评但服务挽回了一切"这种转折句,词典判断基本会错。
第二种是规则增强。在词典打分的基础上,加入否定词反转、程度副词加权、转折词切分等规则。比如"不好吃"在词典里可能只看到"好"的正向分,规则里就必须识别"不"把分数翻转。实现上一般先分词,然后扫描否定词和程度副词的位置,对局部窗口的分值做调整。这套方法能吃掉一部分词典的误判,但规则要维护,换个领域就失灵。
第三种是远程监督,也叫远监督:用关键词、评分、话题标签作为弱标签来源。比如把某个平台的好评标签直接当作正向样本,把差评标签当作负向样本。这个思路和基于规则的词典方法可以混用——先远程监督粗分,再用词典规则修正。无论用哪种方式,得到的标签准确率通常在70%到85%之间,远低于人工标注。但优势是无标注成本,能轻松凑出几十万条训练样本,这就给后续的Stacking集成留足了空间。
2.2 Stacking的本质:用第二层模型学会融合第一层的错误
Stacking并不是一个具体的算法,而是一种集成策略。它把若干个基学习器(level-0模型)的预测结果作为新的特征,交给一个元学习器(level-1模型)去做最终预测。和简单平均投票不同,Stacking不是等权对待每个基模型,而是让元模型自己去学:在哪些样本上哪个基模型更可靠,给谁更大的权重。
实现时有个最容易掉进去的坑:不能直接用全部训练集训练基模型,再用同一批数据预测出"特征"训练元模型。因为基模型已经见过这些样本的标签,预测结果会偏乐观,元模型学到的是既得答案而不是泛化规律。正确做法是交叉验证生成OOF(out-of-fold)特征:把训练集切成KFold,每折轮流作为验证集,基模型在剩下K-1折训练,只对当前验证折做预测,最终把K折预测拼接起来作为该基模型的完整元特征。这样得到的是"模型没见过该样本时"的预测,才能代表真实泛化表现。
这和bagging、boosting有明显边界:bagging对同一模型的多个副本做平均,boosting是串行纠错,而Stacking是让另一个模型做裁决。为什么它特别适合弱监督?因为弱监督标签噪声大,不同基模型会用不同方式过拟合这些噪声——有的记住了词典假象,有的被规则带偏。Stacking的元模型可以学会"当这两个基模型出现分歧时,更信谁",从而抵消单模型的系统误差。
2.3 为什么要用弱监督 + Stacking组合,而不是直接上大型预训练模型
很多人会问:现在有BERT、有大模型,情感分析直接微调不就行了吗?对,如果手里有几百条高质量人工标注数据,微调BERT是性价比最高的路线。但在弱监督场景下,你有的是几十万条噪声标签,BERT的参数量大,很容易把这批噪声死记硬背下来,在真实测试集上的泛化能力反而不如结构简单的TextCNN。这不是模型不行,而是数据信噪比太低。
把Stacking加进来,本质是在"噪声标签训练"这个约束下,尽可能保住模型的鲁棒性。一套常见的落地配置是:三个异构基学习器——TextCNN(捕捉局部短语)、BiLSTM(捕捉上下文依赖)、再加一个浅层模型比如SVM或逻辑回归(特征来自情感词典),最后用一个带正则的逻辑回归作为元学习器。这样即使每个基模型在弱标签上都各自犯错,错误之间的相关性也有限,元模型有纠错空间。
这也是这套源码最值得参考的地方:它没有盲目追求大模型,而是用集成思路消化弱监督噪声。后面所有步骤,都围绕"如何把这三个基模型的OOF预测拼起来并训练出稳健的元模型"展开。
3. 先把基础跑通:环境准备、数据构造与弱标签生成
3.1 环境与依赖:Python版本、深度学习框架怎么选
在实际跑这个方案前,先把环境固定住。我一般用Python 3.9或3.10,依赖最省心的是深度学习相关库在3.9上的轮子最全。如果你装了Python 3.12,某些库的预编译包还没跟上,编译起来容易翻车。
# Python 3.9/3.10 + pip 安装核心依赖 pip install numpy pandas scikit-learn pip install tensorflow-cpu==2.12.0 # 或换成 pytorch: pip install torch torchvision pip install jieba # 中文分词,情感分析几乎都要用到这里的逻辑是:sklearn提供Stacking的元学习器和交叉验证工具;深度学习框架负责训练基模型,CPU版足够处理万级样本的弱监督实验,不需要一开始就上GPU。注意tensorflow的版本不要追最新,2.12或2.13在这套流程里最稳定,太新的版本常遇到模型保存和兼容性报错。如果后续要用BERT做特征,再单独装transformers,不影响这套主体流程。
3.2 用情感词典得分给无标注文本生成弱标签
假设你已经有一份原始文本,放在corpus.txt里一行一条。先做清洗、分词,然后计算每条文本的情感词典得分。这里用一个小型词典示例,真实场景建议用完整的情感词典文件。
import jieba import numpy as np # 简化情感词典:positive_words和negative_words是list,实际可换成词典文件 positive_words = set(['好', '赞', '喜欢', '划算', '快', '满意']) negative_words = set(['差', '烂', '慢', '贵', '坑', '失望']) def sentiment_score(text): words = jieba.lcut(text) score = 0.0 for w in words: if w in positive_words: score += 1 elif w in negative_words: score -= 1 return score texts = [line.strip() for line in open('corpus.txt', encoding='utf-8') if line.strip()] scores = [sentiment_score(t) for t in texts] # 用中位数而不是0作为阈值,避免正负样本比例失衡 threshold = np.median(scores) labels = [1 if s > threshold else 0 for s in scores]这段代码有几个参数值得调:词权重默认是1,你可以把强情感词权重设为2;阈值用中位数能保证正负样本各约50%,这是弱监督里常用的做法。如果你希望负样本更纯,可以把阈值上调,让只有得分明显为负的样本才标为负向。这里用np.median的原因是为了应对词典得分整体偏高或偏低的情况,比硬编码0更稳。
距离"能用"还差一步:很多句子没有命中任何情感词,得分正好是0,这类样本会被归到标签1或0,属于噪声来源。处理办法是把得分等于0的样本单独挑出来,要么丢掉,要么单独设一个"中性"类。在二分类情感分析里,通常直接去掉它们更干净。
3.3 划分训练集与验证集,并统计弱标签的分布质量
标签生成后不要急着喂给模型。先做一次分布检查,并且人工抽几十条看一看标签和原文是否对得上。这一步能帮你提前发现词典适配性问题。
import pandas as pd df = pd.DataFrame({'text': texts, 'label': labels}) print(df['label'].value_counts()) print('正向样本率:', df['label'].mean()) # 随机抽30条正向和30条负向,肉眼检查 sample = df.groupby('label').sample(30, random_state=42) sample.to_csv('weak_label_sample.csv', index=False)value_counts主要看正负样本数量差是否过大。如果某类只有另一类的1/10,后面训练出来的模型几乎只会输出多数类,F1会很难看。抽样人工检查是弱监督流程里的关键一步,即使只看30条,也能快速发现该领域词典是否缺词,比如"客服已读不回"里的"已读不回"并不在通用词典里,但明显是负向。
如果发现标签质量太差,有个省力的办法:把几个不同来源的弱标签做交集或投票。比如词典打分、情感规则、远程监督三个来源一致才保留,这样能把噪声压下去,代价是有效样本变少。这一步在源码里对应数据预处理模块,别嫌麻烦直接跳过。
3.4 用弱标签训练一个基础分类器(TF-IDF + LogisticRegression作为baseline)
在跑深度学习之前,先用一个传统模型做baseline,能快速验证弱标签是否具备可学习性。如果传统模型在验证集上的F1连0.6都不到,说明标签质量问题很大,先回去调标签生成,而不是继续上深度模型。
from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split from sklearn.metrics import f1_score # 注意stratify保留正负比例 train_df, val_df = train_test_split(df, test_size=0.2, random_state=42, stratify=df['label']) vectorizer = TfidfVectorizer(max_features=20000, ngram_range=(1,2)) X_train = vectorizer.fit_transform(train_df['text']) X_val = vectorizer.transform(val_df['text']) model = LogisticRegression(max_iter=1000, C=0.1) model.fit(X_train, train_df['label']) val_pred = model.predict(X_val) print('Baseline F1:', f1_score(val_df['label'], val_pred))max_features=20000用来限制词表规模,避免弱标签噪声被词表放大;ngram_range=(1,2)让模型能捕捉到"不/好吃"这类连续词序特征,对短文本很有用;C=0.1表示较强正则,这是针对噪声标签的防御性设置。LogisticRegression在sklearn里自带L2正则,正好承担baseline角色。
这一步稳定跑通后,整个弱监督链路就算验证过了。注意这里验证集的标签同样来自弱监督,所以F1只能证明模型拟合了弱标签,不代表真实泛化能力。要看到真实水平,事后需要单独标一份纯人工验证集——这是很多人在项目后半段才补的课,建议提前备好。
4. 搭建Stacking层:把多个深度学习基模型集成起来
4.1 基学习器怎么选差异才够大:TextCNN、BiLSTM、再加一个词典特征模型
Stacking的效果好坏,关键看基学习器之间的"差异度"。如果三个模型结构几乎相同,他们的预测错误也会高度相关,融合收益很小。我在项目中常用组合是:TextCNN(捕捉局部n-gram特征)、BiLSTM(捕捉顺序和长距离依赖)、以及一个基于情感词典特征训练的SVM或逻辑回归(实践中的常见做法,结构上和深度模型差异最大)。
TextCNN是情感分析经典模型,卷积核能扫描连续词的组合,对"价格便宜"这类短语很敏感。下面给出简洁实现(用TensorFlow/Keras):
from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Embedding, Conv1D, GlobalMaxPooling1D, Dense, Dropout def build_textcnn(vocab_size, embed_dim=100, max_len=100): model = Sequential([ Embedding(vocab_size, embed_dim, input_length=max_len), Conv1D(128, 3, activation='relu'), # 卷积核大小3,捕捉3-gram GlobalMaxPooling1D(), Dropout(0.3), Dense(1, activation='sigmoid') ]) model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['AUC']) return modelConv1D的核大小是超参数,常见取3、4、5各一组然后拼接,能得到更丰富的n-gram特征。但在Stacking里我们不追求单个模型极致,一个核大小3就够,差异化交给别的模型去补。Dropout(0.3)是特意加的,噪声标签训练很需要。
BiLSTM则适合捕捉"虽然……但是……"这种转折结构。它的双向结构让每个位置既看到前文又看到后文,处理弱监督下的长文本更稳。
from tensorflow.keras.layers import LSTM, Bidirectional def build_bilstm(vocab_size, embed_dim=100, max_len=100): model = Sequential([ Embedding(vocab_size, embed_dim, input_length=max_len), Bidirectional(LSTM(64, dropout=0.2, recurrent_dropout=0.2)), Dense(1, activation='sigmoid') ]) model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['AUC']) return model这里LSTM(64)的输出向量维度是128(双向拼接),足以携带上下文信息。recurrent_dropout防止弱标签噪声被记忆,训练会更慢但更稳。第三个基模型我常用词典特征+SVM:将每条文本的情感词命中数、情感得分、否定词个数拼成特征向量,喂给sklearn的SVC或LinearSVC。这种手写特征和深度模型的表征方式差异最大,Stacking融合时第二层能捕捉到的互补信息也最明显。
4.2 交叉验证生成OOF特征:这是Stacking的命门
现在进入Stacking实现里最容易出错的一步。你需要对每个基模型做KFold交叉验证,收集它在每一折验证集上的预测概率,把K折预测按样本顺序拼接起来。下面这段代码演示如何对单个基模型生成OOF预测,并保存下来:
import numpy as np from sklearn.model_selection import StratifiedKFold def generate_oof(model_fn, X, y, n_splits=5): skf = StratifiedKFold(n_splits=n_splits, shuffle=True, random_state=42) oof = np.zeros(len(X)) for fold, (train_idx, val_idx) in enumerate(skf.split(X, y)): model = model_fn(vocab_size, max_len=max_len) train_sequences = X[train_idx] val_sequences = X[val_idx] # 训练基模型 model.fit(train_sequences, y[train_idx], epochs=10, batch_size=64, verbose=0) # 只预测当前验证折,保存概率 oof[val_idx] = model.predict(val_sequences).flatten() return oof这段代码第一个关键点是StratifiedKFold,它保证每一折训练集和验证集的正负比例都和整体一致,弱标签数据往往有偏,不用分层抽样会让某些折缺少正向样本。第二个关键点是oof[val_idx],每次只填充当前折的预测,下一折训练新模型再填入,五个模型之间相互独立。这样拼接出的OOF预测,每个样本对应的都是"模型没见过该样本"的概率,元模型用它训练才不会虚高。
epochs=10、batch_size=64这两个参数是弱监督实验的起步值。噪声标签下,模型在3到5轮就可能过拟合,所以后面还需要配早停。如果你发现训练损失降了但验证集F1不动,说明模型已经开始记噪声,早停就是在这里止损的。
4.3 第二层用LR还是XGBoost?给元模型的选择建议
OOF特征拼好后,每个样本有三个(或更多)预测概率值,这就是新的特征矩阵。第二层模型不需要太复杂,我常用带L2正则的逻辑回归,偶尔用XGBoost但必须调低树深度。逻辑回归更稳、更容易解释,还能看到每个基模型的权重——这个权重直接告诉你哪个模型在最终融合里说话分量更重。
from sklearn.linear_model import LogisticRegression from sklearn.metrics import f1_score # 假设三个基模型的OOF预测分别为oof_cnn, oof_lstm, oof_svm X_stack = np.column_stack([oof_cnn, oof_lstm, oof_svm]) meta_model = LogisticRegression(C=1.0, max_iter=500) meta_model.fit(X_stack, y_train) # 在验证集上做同样预测并融合 stack_val = np.column_stack([val_cnn, val_lstm, val_svm]) final_pred = meta_model.predict(stack_val) print('Stacking F1:', f1_score(y_val, final_pred))C=1.0是逻辑回归默认的正则强度,若基模型之间相关性偏高,可以降到0.1,强迫元模型减少对某个单一模型的依赖。第二个重要点是,这里的y_train和y_val都来自弱标签。单独看这个数字只是相对比较;要想得到可信的绝对值,需要单独拿人工标注的测试集来评。完整链路跑一遍后,你会发现Stacking的F1通常会比最好的单模型高2到5个百分点,但不会奇迹般翻倍——如果提升很大,先确认是不是OOF特征泄漏了。
生成验证集上的融合特征时,还需要注意:验证集的三个预测也必须来自"各自在训练集上训练好的基模型"。也就是说,基模型在KFold折外训练完毕后,用整个训练集重训一遍(或者保留最好的折模型),然后再对独立验证集预测,这样才模拟真实推理流程。
5. 避坑指南:弱监督 + Stacking最常见的五个翻车点
5.1 现象:OOF特征用了全量训练集预测,元模型在训练集上F1近0.99,验证集却一落千丈
原因:这是Stacking实现里最典型的数据泄漏。基模型已经在整个训练集上训练过,再用整个训练集的预测结果去训练元模型,等于把答案抄进特征里。元模型看到的特征里同时包含真实标签信息和特征信息,训练时表现极好,一旦遇到没见过的新样本就失效。
解决:严格按KFold交叉验证流程,永远只保留模型对各验证折的预测。每个基模型在每个Fold里都重新训练一个副本,训练完立即丢弃,只把oof[val_idx]存下来。如果你用sklearn的cross_val_predict,记得设置method='predict_proba',并且确认返回的顺序和原始样本顺序一致。
5.2 现象:弱标签正负比严重失衡,一个类别占了90%以上
原因:情感词典得分阈值定得太随意,或者数据来源本身以中性文本为主。比如从新闻标题里抽句子做情感分析,绝大多数样本没有明显情感词,得分都落在0附近,用0做阈值几乎全变负向。
解决:改用分布分位数定阈值,保底让正负比接近1:1到1:3之间。更严重的失衡可以考虑SMOTE过采样,但这在弱监督下会放大噪声,我一般优先去掉中性样本,把样本量降下来换取纯度。
5.3 现象:三个基模型融合后F1只提升了不到1个百分点,感觉白做了
原因:基模型之间相关性太高。比如三个模型都用同样的随机种子、同样的词向量初始化,甚至都用了同一种训练数据采样方式,它们的预测结果高度趋同,Stacking学不到互补信息。
解决:从三处制造差异。第一是模型结构:TextCNN+BILSTM+词典SVM已经足够异构。第二是训练数据:给不同基模型用不同比例的弱标签子集,比如一个用全量、一个用去除了中性样本的子集、一个用重采样后的平衡集,这能显著降低相关性。第三是随机种子:给每个基模型分配不同的random_seed,看似不起眼,但对LSTM这类随机初始化的模型影响很大。
5.4 现象:深度学习模型在弱标签上训练几个epoch后,验证F1开始剧烈抖动
原因:噪声标签让模型在梯度下降时不断"修正"自己,把某些偶然出现的词和标签关联起来跨步学习。训练损失还在降,但预测概率已经偏向极端值,导致验证指标震荡。
解决:加早停回调,并加入标签平滑。Keras里可以用EarlyStopping(monitor='val_loss', patience=2, restore_best_weights=True),一般2到3个epoch内无改善就停。标签平滑是把硬标签1和0替换成0.9和0.1,给模型留出错的空间,弱监督场景下几乎必加。
from tensorflow.keras.callbacks import EarlyStopping early_stop = EarlyStopping(monitor='val_loss', patience=2, restore_best_weights=True) # 训练时传入 callbacks=[early_stop]5.5 现象:拼接OOF特征时出现样本错位,F1低到零点几
原因:在拆KFold时,train_idx和val_idx是原数组的下标,而如果之前对DataFrame做过reset_index或过滤操作,索引和位置对不上了,把预测存进oof[val_idx]时就会张冠李戴。
解决:在进入KFold之前,先对样本统一做reset_index(drop=True),并且保证所有基模型都用同一个X、y数组传入。我在项目里习惯一开始就把文本序列和标签保存为numpy数组,后面所有fold操作都用它,避免DataFrame索引引起的问题。
6. 最后让结果可信任:Stacking效果的验证方法与一个调参技巧
做完融合后,第一件事不是看F1,而是确认这个F1是"在人工标注集上"算出来的,还是在弱标签集上算出来的。弱监督项目里,最骗人的就是弱标签集上的漂亮分数。所以真正落地前,请单独抽出200到500条文本,人工标注一遍,作为测试集。这套源码里的训练过程可以不管它,但测试集的样本必须从原始语料里随机抽取、且不能出现在基模型的训练K折里。然后在测试集上同时评估最好的单模型和Stacking模型,看提升是否仍然存在。如果提升只在弱标签集上出现,说明元模型学的其实是噪声模式,需要回到基模型差异化这一步重做。
调参方面,一个值得优先尝试的技巧是把元模型的特征从"概率值"换成"概率值的排序信息"或"概率的logits"。具体做法是:不需要对基模型的预测做任何归一化,直接把原始概率值输入逻辑回归。当某个基模型在正负样本上都输出0.9和0.8这种接近值,原始概率保留了很多粒度信息,比先转成0/1再送进去信息量大得多。另外,如果元模型用逻辑回归,训练前对OOF特征做标准化,效果会稳定一些,虽然逻辑回归不强制要求标准化,但特征尺度差异大时权重解释会别扭。
还有个小经验:基模型训练时没必要用同样的epoch预算。弱监督噪声多的那个模型(比如词典特征SVM)几乎没有过拟合风险,而BiLSTM很容易过拟合,所以要单独给每个基模型设早停。如果最后Stacking提升还是不大,去检查一下各基模型的OOF之间的相关系数,如果都超过0.8,说明你的数据在强特征面前已经"简单到"模型们结论一致,这时候与其堆模型,不如回头做伪标签清洗。我在类似项目里踩过最多的坑,不是模型写不出来,而是迭代到后面才发现,最能让Stacking发挥作用的其实是基模型之间的合理差异。希望这篇能把你在弱监督情感分析这条路上常见的坎提前避掉,帮到你。
本文还有配套的精品资源,点击获取