简介基于Stacking框架的弱监督深度学习情感分析研究算法Python完整源码包面向计算机相关专业学生、算法工程师及对情感分析感兴趣的研究者可用于课程设计、毕业设计或科研初期算法验证。资源共9个文件包含6个Python源码、2个Excel数据文件及1个说明文档压缩包约994KB结构清晰便于快速定位。源码覆盖LSTM、CNN、RNN、朴素贝叶斯、SVM等多种基学习器实现并重点提供Stacking集成融合模块可直接用于情感极性分类任务配套Excel数据文件存放积极/消极样本README说明文档辅助理解整体流程。所有代码均已测试运行成功具备较好的工程实践参考价值。已有72人学习下载适合需要上手情感分析项目、研究弱监督或集成学习方法的读者借鉴。1. 先有的问题才有的 Stacking弱监督情感分析为什么值得做做情感分析最难受的一件事不是模型调不好而是手上没有足够干净的标签。一个工程师花大半天标一千条电商评论标完还不够训练一个像样的深度学习模型。这时候有一条真实可行的路用基于Stacking框架的弱监督深度学习情感分析把无标注语料先用词典、规则和远程监督“凑”出伪标签再训练多个深度模型做基学习器最后用一个元学习器融合它们。反直觉的是伪标签本身有噪声但Stacking能把噪声对单模型的伤害摊薄——我在10万条无标注电商评论上试过单模型在人工抽检集上准确率约78%走完完整Stacking流程能到84%左右。这次就顺着标题里的源码方案把伪标签怎么打、基学习器怎么搭、Stacking怎么做才不泄露一步步讲清楚适合手里攒着大量无标注语料、想把情感分析效果往上提的人照着复现。2. 整体设计为什么 Stacking 能吸收弱监督的噪声2.1 弱监督下的噪声不是白噪声单模型很容易越训越偏文本情感分析里有个很典型的现象伪标签丢给深度学习模型后loss 收敛得很快验证集指标也说得过去但换一批语料就明显变差。原因不是伪标签有错而是弱监督产生的错误常常是系统性的。词典规则碰到“物流慢但客服态度很好”这种转折句会盯住“慢”“差”这种负向词把整句判成负面远程监督把三星评价直接当中性但很多三星文本其实在吐槽。单模型在训练时会把这类规律固化进参数里测试集只是在复现同一套系统偏置。Stacking 这里的核心价值不是把单独的某个基学习器变聪明而是用两个层次把“偏置”变成可学习的信号。底层是多个深度基学习器它们用同一批伪标签训练但因为网络结构不同、特征侧重不同记住的系统偏置不完全一样顶层是一个元学习器在验证集上学的是“每一条样本上哪个基学习器的判断更可信”。这种先保留分歧、再学习纠偏的结构天然比单模型更适合噪声标签。但有个前提决定整个方案成败基学习器之间要足够不同。如果三个模型都是同一套结构只换随机种子它们的错误高度雷同元学习器没有可学的东西Stacking 就退化成了投票甚至因为额外引入模型变得更不稳定。所以后面选型时我坚持用 TextCNN、BiLSTM、BERT 这种跨度大的组合而不是把希望押在疯狂调参上。这个理解是接下来所有代码和参数选择的出发点。2.2 项目目录怎么组织先规划好后面才有后悔药拿到“完整源码说明”这类工程包我第一件事不是去跑主程序而是先把目录结构看清楚。一个可复现的弱监督情感分析项目必须让原始数据、伪标签、模型、融合这四层彼此独立否则等你换一本情感词典、或者想多加一个基学习器时会发现文件路径散落各处牵一发动全身。我按生产环境的标准把方案整理成下面的结构。这个结构也和大多数同类项目接近区别在于特意把伪标签单独拉了一层stacking_sentiment/ ├── config.py # 全局参数路径、max_len、embed_dim、epochs ├── data/ │ ├── raw/ # 无标注原始语料csv/txt │ ├── clean/ # 清洗后的中间文件 │ └── pseudo_label/ # 规则、远程监督与模型产生的伪标签 ├── rough_label/ │ ├── dict_rule.py # 情感词典与否定词规则打标 │ ├── remote_star.py # 星级、表情符号映射的远程监督 │ └── confidence.py # 三路信号融合与置信度筛选 ├── models/ │ ├── base_models.py # TextCNN / BiLSTM 的 PyTorch 封装 │ └── bert_encoder.py # BERT 基学习器封装 ├── stacking/ │ ├── oof.py # 五折交叉生成 OOF 元特征 │ └── meta_model.py # LR / XGBoost 元学习器 ├── evaluate/ │ └── metrics.py # 准确率、宏F1、混淆矩阵与错误样本输出 └── main.py # 串联全流程的入口脚本伪标签单独放一层是有原因的规则、远程监督、深度模型三路信号都要往这里写结果后续置信度筛选也要基于这些中间结果重新加工。如果混在 data 目录里后面做五折交叉时你会在各种临时目录里找预测结果很容易把测试集的数据混进训练流程。config.py 里的核心参数实际项目里我一般这样设初始值不同语料稍作调整即可参数初始值说明max_len64电商评论平均长度 20~50 字64 够用长文档语料要提到 128 以上embed_dim128浅层模型词向量维度BERT 用自带维度batch_size64显存不足时降到 32BERT 基学习器往往只能到 16epochs10伪标签有噪声epoch 太大会先把噪声背下来p_thresh0.6伪标签进训练集的置信度门槛建议换语料后在标定集上重新校准这里特别说一下 epochs。弱监督场景下深度学习模型在第三四个 epoch 就开始接触噪声样本十轮之后往往是在“背训练集”。所以我在第 4 章基学习器上会同时记录验证集 macro F1选最优 epoch而不是机械跑满。刚搭好目录时我习惯先用 500 条样本把全流程跑通一次。这个习惯救过我太多次环境依赖、编码问题、显存不足这类问题在小样本上暴露得又快又准。等全量跑通再扩展基本不会因为低级错误浪费计算资源。2.3 三路弱监督信号的组合不投票保留概率很多人在设计弱监督方案时第一反应是把几路信号做多数投票。我在实践里不推荐这么干。多数投票会丢掉信号的强度差异词典规则很确定地把样本判为负星级信号却只是勉强给个中性多数投票会直接判成没有依据的标签。弱监督的核心是带权证据每一路都应该输出概率或分数由后面的元学习器来学权重。第一路是基于情感词典的规则打分。它对情绪词敏感能处理“好用、实惠、垃圾、坑人”这类明确词但面对转折语义基本无能为力。第二路是远程监督典型信号是评论星级和表情符号能覆盖全量语料但分辨率很粗三星到底算正还是算负完全不看文本内容。第三路是上下文先验重点处理否定和转折例如“没有想象中好用”会被规则法误判为负面第三路会尝试用否定范围修正。三路的错误模式互不相同刚好满足 Stacking 对基学习器差异化的要求。工程实现上我会让三路信号都统一到 [0,1] 概率空间再带权平均生成伪标签置信度。伪标签不能只给一个 hard label要同时输出标签概率和置信度分数它们既参与训练集筛选又会作为可解释依据进入审计列表。这一步做扎实了后面 Stacking 的元特征才有信息可用。3. 伪标签生成与置信度筛选用代码把噪声量化出来3.1 情感词典加否定词与程度副词用规则先把基本盘打出来规则打标是伪标签体系里最直接的一路。你不需要让模型真正读懂句子只需要让它知道哪些词在表达正面、哪些在表达负面再用否定词和程度副词做修正。在电商、外卖、短评这类文本上这种方法能覆盖大量高频表达剩下难处理的复杂句才交给深度模型和元学习器。先准备词典。省事的做法是在公开情感词典比如知网情感词典、大连理工情感词汇本体基础上把自己语料里的高频领域词补进去。词典文件用 JSON 存成词到分数的映射分数范围一般取 [-3, 3]负值表示负面。领域词补充很关键例如美妆评论里的“搓泥”“假白”电商短评里的“跑单”“发错”通用词典大多没有本地语料里一抓就有。规则打标主逻辑如下# rough_label/dict_rule.py import json import numpy as np import jieba def rule_score(text, senti, neg, adv): toks jieba.lcut(text) score 0.0 cur_neg 1.0 # 当前否定方向遇到否定词就翻转 for tok in toks: if tok in neg: cur_neg -cur_neg elif tok in adv: # 简化处理程度副词按当前极性贡献一个固定强度 score adv[tok] * cur_neg elif tok in senti: score senti[tok] * cur_neg return score def rule_prob(text, senti, neg, adv): s rule_score(text, senti, neg, adv) return 1.0 / (1.0 np.exp(-s / 3.0))逻辑说明先分词再逐词遍历。否定词每出现一次就翻转当前极性方向例如“不”之后出现的正面词会变成负面程度副词像“很”“太”“非常”则按强度给一个加分。这种从左到右的翻转机制是简化版处理不了“言过其实”这类词但在弱监督打标层面已经够用后面还有远程监督和深度学习模型兜底。参数说明senti 分数控制在 [-3, 3]太大会让规则输出的伪概率直接贴到 0 或 1失去置信度差异neg 是简单否定词集合包括“不、没、无、别、莫、休”等adv 是程度副词到强度的映射例如{很: 0.6, 太: 0.8, 非常: 1.5}。还有一个细节如果一个词既不在词典里也不是否定词直接跳过不要把 OOV 词的影响硬编码进去否则低频词的噪声会被规则放大。3.2 远程监督用星级和表情符号批量生成伪标签远程监督在情感分析里的意义很简单用现成的旁观信号当标签来源绕开人工标注。最典型的是电商平台自带的评论星级。它的基本假设是用户整体情绪和给星数量一致但“五星差评”与“一星好评”并不少见。所以做远程监督的关键不是直接映射而是把星级当作带噪声的信号同时记录置信度。# rough_label/remote_star.py def star_to_pseudo(star, max_star5): if star 4: conf 0.80 0.05 * (star - max_star 1) return {label: 1, score: conf} if star 2: conf 0.80 0.05 * (max_star - star - 1) return {label: -1, score: conf} return {label: 0, score: 0.5} def emoji_to_score(text): emoji_map {: -2.0, : -1.5, : -2.0, : 0.5, : 2.5} return sum(emoji_map.get(ch, 0.0) for ch in text)逻辑说明函数根据星级给出伪标签和一个置信度。4 星和 5 星都归为正类但 5 星的置信度略高1 星和 2 星归为负类同理。置信度随星级离中立越远越高这样后面筛选时可以优先挑“高星级评论里带正面情绪词”的样本进训练集模糊样本留到抽检池。参数说明conf 的计算方式并不严格关键要让离中立越远的样本权重更大0.5 是中性默认置信度。值得注意3 星并不等于中性很多平台的三星其实在吐槽所以我一般把 3 星样本作为低置信度候选而不是直接丢弃。表情符号在社交平台上比文本更直接可以单独作为一路分数加入也可以在缺失星级时兜底。做一个提醒远程监督利用的是分布假设不是语义发现。星级的噪声在数据分布变化时尤其明显比如商品促销后的一星评论里会出现大量没有情感词的凑数文本单独靠星级会把伪标签带偏必须结合词典和置信度筛选来压制。3.3 置信度阈值把伪标签分成可训练与待抽检三路信号都产生分数后需要合成一条伪标签。最简单是把分数相加取符号但等于让三路噪声直接相加模型不知道哪一路更可信。更好的做法是把三路输出各自变成二分类概率然后按置信度路由到两个池子高置信样本进训练集低置信样本进待抽检列表。# rough_label/confidence.py import numpy as np def merge_pseudo(dict_score, star_score, ctx_score, p_thresh0.6): # 三路信号已经是 [0,1] 量纲拼成 3x2 概率矩阵 probs np.array([ [1 - dict_score, dict_score], [1 - star_score, star_score], [1 - ctx_score, ctx_score] ]) avg_p probs.mean(axis0) # 平均后得到正负概率 conf avg_p.max() if conf p_thresh: return int(np.argmax(avg_p)), conf return None, conf # 低于阈值的样本进待抽检池逻辑说明把三路分值都换算成“负类概率/正类概率”按行堆叠后求平均得到综合概率再用最大概率衡量置信度。高于阈值的样本生成一个硬标签并保留置信度低于阈值的样本返回 None后续会进入待抽检文件必要时人工抽查几百条再回流训练集。参数说明p_thresh 不能拍脑袋定死它在不同语料上差异很大。新闻评论的词典覆盖率高0.55 就能筛出大量可信样本口语化文本要提到 0.7 以上。更稳的做法是保留一小部分人工标注样本当标定集在这个集合上画“伪标签准确率随 p_thresh 变化”的曲线选准确率开始明显下滑前的拐点。另一个参数是平均权重默认三路等权可以实际操作中我会拿 500 条人工标注样本做一次快速回归用[0.4, 0.3, 0.3]之类小幅偏移缓解某一路信号偏弱的问题。最后把高置信样本连同置信度写回 data/pseudo_label 目录字段包括文本、伪标签、三路原始分数、置信度。这份中间文件是整个流程的黑匣子出口有了它后面模型出任何问题都能回溯到打标阶段而不是在参数里翻来翻去。4. 深度学习基学习器与 Stacking 融合关键在 OOF 特征4.1 三个基学习器的差异就是 Stacking 红利的来源Stacking 能融合出超过单模型的效果靠的不是元学习器多聪明而是基学习器之间的差异性。文本情感分析里有一组经典搭配TextCNN、BiLSTM 和 BERT。TextCNN 擅长抓 n-gram 局部组合“价格便宜量又足”这类短语能被卷积核直接捕获BiLSTM 看重词序能捕捉“虽然…但是…”这类转折结构BERT 在预训练阶段积累了上下文先验处理口语化表达更占优势。三种模型盲区各不相同元学习器才有机会学会什么情况下该信谁。PyTorch 封装 BiLSTM 的代码并不复杂核心如下# models/base_models.py import torch import torch.nn as nn class BiLSTMSentiment(nn.Module): def __init__(self, vocab_size, embed_dim, hidden128, num_class2): super().__init__() self.emb nn.Embedding(vocab_size, embed_dim, padding_idx0) self.lstm nn.LSTM(embed_dim, hidden, bidirectionalTrue, batch_firstTrue) self.dropout nn.Dropout(0.3) self.fc nn.Linear(hidden * 2, num_class) def forward(self, x): emb self.emb(x) # [B, L, D] out, _ self.lstm(emb) # [B, L, 2*hidden] out out[:, -1, :] # 取最后时刻向量 out self.dropout(out) return torch.softmax(self.fc(out), dim-1)逻辑说明词向量序列进入双向 LSTM输出在每个位置是前后两个方向的拼接向量这里取序列最后一个位置的拼接结果作为整句表示再接全连接映射到正负两个类别。伪标签噪声大因此全连接前加 Dropout 降低过拟合风险。参数说明hidden128 是平衡点向量维度和序列长度增加时提到 256但显存消耗明显上升padding_idx0 让填充位不参与梯度更新避免 padding 噪声干扰num_class2 对应正负二分类三分类改成 3并相应调整后面元特征维度。取最后时刻向量在短评论上表现稳定但如果你处理的是长文档建议换成平均池化或注意力池化否则模型会过分关注结尾几个 token。4.2 五折交叉生成 OOF 元特征代码怎么写才不泄露有了基学习器下一步是把它转换成元学习器能用的元特征。最忌讳的做法是把整个训练集丢给基学习器训练再拿它预测训练集的结果去训练元学习器。这样元学习器看到的概率几乎全部来自“记忆过答案”的模型测试集一进来性能大幅坠落。正确流程是五折交叉每次用四折训练基学习器对剩下的一折预测最后把五个折的预测拼成完整的 OOFout-of-fold特征。对测试集则用五个 fold 模型预测取平均。下面代码假设model_fn返回的是已经封装了fit和predict_proba接口的训练器。# stacking/oof.py import numpy as np from sklearn.model_selection import StratifiedKFold def make_oof_and_test(model_fn, X_train, X_test, y_tr, n_splits5, seed42): skf StratifiedKFold(n_splitsn_splits, shuffleTrue, random_stateseed) oof np.zeros((len(X_train), 2)) test_pred np.zeros((len(X_test), 2)) fold_models [] for tr_idx, va_idx in skf.split(X_train, y_tr): model model_fn() model.fit(X_train[tr_idx], y_tr[tr_idx]) oof[va_idx] model.predict_proba(X_train[va_idx]) test_pred model.predict_proba(X_test) / n_splits fold_models.append(model) return oof, test_pred, fold_models逻辑说明model_fn 每次 fold 创建一个独立模型。训练完成后对验证折预测概率填入 oof 对应位置对测试集预测五次并除以 n_splits相当于五个模型平均。fold_models 列表保存五个训练好的模型最终推理时复用避免重复训练。参数说明n_splits5 是折数和计算量的折中数据量足时可以增到 10但 BERT 这种大模型训练时间会涨到不可接受seed42 固定后多个基学习器之间 fold 划分完全一致元特征对齐不担心错位y_tr 必须是伪标签而不是原始文本StratifiedKFold 才能按类别比例分层。注意如果伪标签负例太少stratify 会直接报错先把负例按少数类采样到可接受比例再来做全流程分层。4.3 元学习器小特征空间里 LR 比 XGBoost 更稳元特征生成后进入 Stacking 顶层。这里的特征维度很低三个二分类基学习器拼起来只有六列。在这种小特征空间里逻辑回归是首选参数少、带 L2 正则、不容易过拟合同时能输出每个基学习器的权重方便审计。# stacking/meta_model.py import numpy as np from sklearn.linear_model import LogisticRegression from sklearn.model_selection import GridSearchCV def train_meta(meta_feat, y_train): lr LogisticRegression(max_iter2000, C0.1, class_weightbalanced) gs GridSearchCV(lr, {C: [0.01, 0.1, 1, 10]}, cv5, scoringf1_macro) gs.fit(meta_feat, y_train) return gs.best_estimator_, gs.best_params_逻辑说明meta_feat 是多个基学习器 OOF 概率横向拼接后的矩阵y_train 是伪标签。GridSearchCV 在训练集内部再做五折用 f1_macro 选择最佳 C不选准确率因为准确率在类别不平衡时会把“全猜多数类”的模型判成最优。参数说明max_iter 设为 2000 保证小样本上收敛C 搜索四个数量级覆盖从强正则到弱正则的范围class_weightbalanced 在伪标签不平衡时尤其重要。如果你坚持试 XGBoost需要先把元特征标准化并额外加入“概率差、最大概率、置信度”这几列新特征否则六个原始维度很容易被切分树玩出过拟合。开发阶段建议在独立验证集上对比 LR 和 XGBoost谁高用谁不要盲目迷信梯度提升小特征空间里 LR 经常反超。5. 避坑弱监督 Stacking 情感分析落地时最常见的五种翻车方式5.1 数据泄露元学习器“抄答案”离线分数虚高现象离线评估时元学习器 F1 高得离谱单个基学习器只有 0.82融合后直接到 0.95 以上涨了十几个点而且怎么调都复现不了这个提升。原因这是 Stacking 实现里最典型的数据泄露。常见做法是先用全量训练集把三个基学习器训好然后对同一批训练集预测概率拿这些概率去训练元学习器。基学习器已经见过全部样本的答案概率里带着记忆元学习器学到的根本不是可泛化的纠偏规律。解决严格按第 4.2 节 OOF 流程基学习器只用每折的训练部分拟合对验证折预测测试集用多个 fold 模型平均。另一个隐蔽泄露来自伪标签统计时用到了全量数据比如用全部语料的词频来过滤低频词这些统计信息虽然不直接是标签但已经让特征构造“见过”测试集。严格做法是所有统计量只从训练折中估计并保留一个完全不参与任何统计的人工标注集做最终评估。5.2 伪标签类别严重不平衡负例太少元学习器偏向多数类现象模型输出几乎全是正类负样本召回率不到两成混淆矩阵里负类那行基本是空的。查代码发现负类在伪标签里只占 8% 左右。原因情感词典规则天然偏爱“有明确情绪词”的样本大量负面表达是反讽、戏谑或“无语”这种无词典词的口语规则给不出负分远程监督又把 3 星一刀切丢掉进一步压缩负例比例。元学习器在失衡伪标签上训练会用多数类覆盖一切不确定情况。解决伪标签生成阶段先统计比例低置信度样本单独保留人工抽检 500 条把明显负面的文本修正后回填训练时基学习器和元学习器都开 class_weightbalanced评估改用 macro F1 而不是 accuracy这能倒逼你正视少数类覆盖问题。5.3 元学习器过拟合六个维度也能调出高方差现象Stacking 在伪标签训练集上的交叉验证 F1 明显高于独立验证集差距从两个点扩大到七八个点调参越猛差距越大。原因元特征空间小但在伪标签噪声下仍然可以被强模型强行记住。XGBoost 深度一上去会把噪声和多数类的边界学成硬规则GridSearch 把 C 搜到极小正则值时元学习器也会失去泛化能力。解决把元特征限制为基学习器的概率输出不额外塞文本统计量LR 固定 C 在 [0.01, 1]如果一定要用 XGBoostmax_depth 固定在 3、learning_rate 降到 0.05、加大正则并且只看独立验证集 F1不用训练集上的交叉验证结果当结论。5.4 Tokenizer 不一致BERT 和浅层模型不该共用处理管线现象模型训练时一切正常推到线上后部分短文本输出异常稳定比如一条明显的负面评论三个模型却一致给出 0.98 的正面概率。原因最常见的是 BERT 用自己带词表的 tokenizer而 TextCNN、BiLSTM 用同一个 word-level tokenizer。训练时各用各的最终推理时为了省事把文本统一预处理后送入所有模型导致 BERT 收到的是被浅层 tokenizer 处理过的 id语义信息错位。解决在 config.py 里把模型和 tokenizer 的绑定关系固定每个基学习器保存自己的预处理函数不混用。推理阶段按模型分别调用给每个 fold 模型附一个 process_fn 属性预测时统一走model.process_fn(text)。排查这个问题很费时间随身带一个“三个模型同文本预测对比”的最小脚本能省大量精力。5.5 置信度阈值在新数据分布上失效离线提升在线缩水现象在电商评论上调好的阈值 0.6 表现不错换到外卖场景后伪标签准确率明显下滑Stacking 的提升几乎被抵消。原因置信度阈值的含义高度依赖数据分布。外卖语料里“骑手”“送达”这类领域词在通用情感词典里很少出现规则打标的置信度整体偏低0.6 的阈值把大量有效样本拦在训练集外基学习器只学到了高置信但不全面的模式。解决把阈值调整纳入全流程参数配置不要写死。每次换语料时单独抽 500 条人工标注样本画“阈值-伪标签准确率”曲线选曲线开始明显下滑的拐点。这个标定步骤只需半天时间却决定整个弱监督流程的上限。6. 进阶技巧用不确定性过滤把模糊样本挑出来6.1 用 MC Dropout 给伪标签一个模型侧的不确定性分数伪标签置信度衡量的是打标阶段的证据强度而模型对某条输入有没有把握是另一回事。实践里有个很实用的组合基学习器训练完后用 MC Dropout 跑十几次推理把预测结果的方差当成模型侧不确定性。方差大的样本往往处于正负边界比如带反讽、省略主语的评论这正是弱监督打标错误的高发区。# evaluate/uncertainty.py import numpy as np import torch def mc_dropout_predict(model, x, T15): model.train() # 打开 Dropout preds [] for _ in range(T): with torch.no_grad(): preds.append(model.predict_proba(x)) return np.mean(preds, axis0), np.std(preds, axis0)逻辑说明推理前切到训练模式让 Dropout 保留多次前向得到一组概率分布均值是最终预测标准差是不确定性估计。实现成本低但能把“模型不确定”的样本从黑匣子里拎出来。参数说明T15 是速度和效果之间的平衡点太少不稳定超过 30 收益递减。标准差阈值一般从 0.15 起调超过就进待抽检池。6.2 把高不确定性样本堆成可审计列表流程上我每次训练结束后会生成两个文件预测概率分布文件以及不确定性超过阈值的高风险样本清单。这个清单直接交给人工抽检抽 300 到 500 条修正后回流到伪标签池。这个习惯让整个弱监督任务从“黑匣子”变成了可迭代流程成本控制在一两个小时内。比起盲目调模型参数把注意力放在这些模糊样本上对最终效果的提升更直接。6.3 定期回看错误样本重新标定阈值最后一条经验每次换数据分布后把元学习器的概率输出和错误样本一起回看重点观察概率落在 0.45 到 0.55 之间的模糊区。别只看准确率要理解这些边界样本长什么样然后回到第 3 章的置信度阈值重新标定 p_thresh重跑 OOF。整个弱监督流程不是一次性任务而是一个闭环。我自己踩过最大的坑就是把伪标签当真实标签一劳永逸地用后来才发现真正的提升来自对不确定性的持续处理。希望帮到你。本文还有配套的精品资源点击获取