
简介压缩包提供了一套基于新浪微博评论的SVM情感分析完整实现面向有一定Python和机器学习基础的学习者适用于市场舆情监测、公众情绪预测等场景内容覆盖从数据采集、清洗、特征工程到模型评估的完整链路。项目共47个文件以9个Python脚本为核心包含微博评论抓取、中文分词、去停用词、TF-IDF向量化与SVM分类器训练等步骤另有8张分析截图、1个mp4操作录屏、docx论文原稿、csv数据集、pkl模型文件等共约139.62MBcsv与npy文件保存了中间特征及结果方便对照调试。目前已有10620人学习热度较高。读者可按源码顺序运行参考录屏核对关键输出通过调整SVM的核函数、惩罚参数C等观察情感分类准确率变化并借助论文原稿理解实验设计与误差分析适合课程设计或毕业设计直接参考。1. 基于新浪微博评论的情感分析先搞清楚这套流程在解决什么问题拿到“基于新浪微博评论的情感分析.zip”这类压缩包意味着你面对的不是一键出结果的演示程序而是一整套从数据到模型再到评估的流水线。微博评论是中文 NLP 里最难的短文本长度通常不到 30 个字网络新词、表情符号、反讽和否定式表达混在一起常规分词和词典几乎全部失效。这篇文章不讨论某个具体压缩包的内部实现而是把这个标题拆成一条可复现的工程路径评论怎么采集、文本怎么清洗、情感模型如何从词典法一路升级到深度学习以及五个真实踩坑记录。适合准备做舆情分析、电商评论分类或课程设计的人新手能跟着跑通熟手能直接对照参数和边界。2. 数据是地基评论采集与中文短文本清洗2.1 评论采集低频、单线程、带身份信息要分析微博评论前提是先拿到数据。常见做法是直接请求移动端接口带上浏览器里复制出来的 Cookie模拟正常用户访问。注意这不是让你去对抗反爬而是避免因为请求太密触发平台限制。我一般会维护一个 requests.Session固定 UA 和 Cookie每个请求之间随机休眠 2 到 5 秒。import requests import time import random session requests.Session() session.headers.update({ User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X), Cookie: 这里粘贴你的登录Cookie }) def fetch_comments(weibo_id, max_page5): all_comments [] for page in range(1, max_page 1): url fhttps://m.weibo.cn/api/comments/show?id{weibo_id}page{page} try: resp session.get(url, timeout10) data resp.json() for item in data.get(data, []): all_comments.append(item.get(text, )) except Exception as e: print(f第{page}页失败: {e}) time.sleep(random.uniform(2, 5)) return all_comments这段代码的作用是翻页抓取某条微博的评论正文。session复用 TCP 连接和 Cookie比每次新建请求更不容易触发风控random.uniform(2, 5)是刻意制造的随机间隔防止出现固定节奏的访问特征接口返回的是 JSONdata字段里的每条记录都有一个text字段里面是带 HTML 标签的评论正文。参数上max_page通常 3 到 5 页就足够生成一个实验数据集再多收益会明显下降因为热门微博靠前评论的情感分布和整体差异很大抓得越深反而引入更多广告和无关回复。如果你的目标是训练模型而不是复现某条微博的舆情更推荐按关键词搜索接口抓多条微博下的评论这样样本多样性更好。2.2 文本清洗流水线URL、用户、话题和 emoji 的取舍微博评论的脏主要在五个地方URL、用户名、话题标签、HTML 实体、emoji。很多项目一上来就用正则把 emoji 删干净这是我在后面避坑章里会专门说的错误做法。正确的流水线是URL、用户和话题标签直接删除因为它们不携带情感HTML 实体反转义emoji 用emoji库转成语义等价的文字描述简繁体统一成简体。import re import emoji from opencc import OpenCC cc OpenCC(t2s) # 繁体转简体 def clean_weibo_text(raw: str) - str: text re.sub(rhttps?://\S, , raw) # 删除URL text re.sub(r[\w\u4e00-\u9fa5\-], , text) # 删除用户 text re.sub(r#.*?#, , text) # 删除#话题# text re.sub(r[^], , text) # 去掉HTML标签 text emoji.demojize(text, languagezh) # 笑脸符号 - :咧嘴笑: text cc.convert(text) # 繁体转简体 return text.strip()每一行的作用都写在注释里需要单独解释的是emoji.demojize。它会把表情符号转成类似:咧嘴笑:的文本标记这样“今天天气真好:太阳:”就被保留成了可被分词识别的语义单元而不是被正则无情删掉。情感分析任务里emoji 往往是情绪最强的信号删除它等于自断一臂。OpenCC(t2s)解决的是繁体评论问题。微博里大量用户习惯打繁体如果不在清洗阶段统一同一个词在词典匹配时会因为字形差异被漏掉。注意t2s是“繁体到简体”的映射别传反了否则会把正常简体转成繁体后面所有匹配全部失效。2.3 分词与自定义词典让 jieba 认识 yyds 和绝绝子清洗完的文本进入分词环节。jieba 是中文 NLP 的默认选择但它的默认词典来自新闻语料对微博网络新词几乎无解。yyds会被切成 “yyd” 和 “s”“绝绝子”会被切成 “绝/绝/子”。解决方法是加载一个针对微博语料的自定义词典格式是三个字段词、词频、词性。词频越大越优先被切分出来。yyds 10 n 绝绝子 10 n 无语子 10 n 栓Q 10 n 芭比Q 10 n把这个文件保存为weibo_dict.txt然后在代码里加载import jieba jieba.load_userdict(weibo_dict.txt) jieba.enable_paddle() # 可选使用Paddle模式对新词更友好 words jieba.lcut(绝绝子yyds今天真的栓Q了) print(words) # [绝绝子, , yyds, , 今天, 真的, 栓Q, 了]load_userdict是让 jieba 在原有词典基础上追加自定义词不需要重建词典词频填 10 表示这个词在切分时被优先考虑具体数值不是越大越好填 10000 会导致它在所有语境下都被强行剖成一个词反而可能切错一般句子。enable_paddle是 jieba 的可选模式需要额外安装 paddle 相关依赖如果只是做实验可以不启用。它的好处是对未登录词有更好的识别效果代价是推理速度变慢。对情感分析来说分词的正确性直接影响后面词典法的匹配覆盖率这一步值得多花时间迭代词典——每发现一个被切开的新词就把它加进weibo_dict.txt。3. 情感判别三件套词典基线、TF-IDF 线性模型与 TextCNN3.1 先跑通词典法用情感词典打一个可解释的底拿到清洗和分词后的数据第一个能跑通的情感判别方法是词典法。常用的是 BosonNLP 情感词典每行格式为“词\t权重”正数表示正向程度负数表示负向程度。词典法不需要标注数据只需要把文本分词后逐词查表累加权重最后按分数符号判断正负。import jieba NEG_WORDS {不, 没, 别, 莫, 无, 未, 甭} def load_sentiment_dict(path): word_score {} with open(path, encodingutf-8) as f: for line in f: word, score line.strip().split(\t) word_score[word] float(score) return word_score def dict_sentiment(text, word_score, window3): tokens jieba.lcut(text) score 0.0 neg_flag False countdown 0 for token in tokens: if token in NEG_WORDS: neg_flag True countdown window continue if token in word_score: s word_score[token] score -s if neg_flag else s if countdown 0: countdown - 1 if countdown 0: neg_flag False return score这个打分的核心不是简单累加而是加了一个否定翻转窗口。当遇到“不/没/别”这类否定词后接下来window个词里的情感词权重全部取反所以“不好看”里的“好看”贡献的是负分。窗口设为 3是因为中文里否定词通常在离它最近的两三个词内发挥作用比如“不是很满意”的“满意”就在窗口内。词典法的优点是可解释、零标注成本、单机秒级跑完几十万条评论缺点是覆盖不全网络新词和反讽它完全无能为力。它的作用也不是最终模型而是给你一个分数分布作为后续伪标签的参照系以及在模型翻车时回去定位原因的工具。3.2 TF-IDF 加线性模型把伪标签变成常规分类任务词典法能给每条评论一个正负倾向但直接拿它当最终结果准确率通常在 70% 上下还达不到可用标准。常见做法是把它产生的打分和 SnowNLP 概率交叉过滤出置信度高的样本打上伪标签然后用 TF-IDF 特征训练一个有监督分类器。这样标签虽然是带噪声的但模型能学到的特征交互远超词典线性累加。from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.pipeline import Pipeline from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report # df 来自采集清洗后的数据pseudo_label 是伪标签列打标细节见第4章 texts df[clean_text].tolist() labels df[pseudo_label].tolist() vectorizer TfidfVectorizer( tokenizerlambda s: jieba.lcut(s), ngram_range(1, 2), min_df2, max_df0.9, sublinear_tfTrue ) model Pipeline([ (tfidf, vectorizer), (clf, LogisticRegression(C1.0, class_weightbalanced, max_iter200)) ]) X_train, X_test, y_train, y_test train_test_split( texts, labels, test_size0.2, stratifylabels, random_state42 ) model.fit(X_train, y_train) print(classification_report(y_test, model.predict(X_test)))ngram_range(1, 2)同时保留单字词和相邻两词组合比如“不好”会作为一个整体特征出现这比单纯靠 TF-IDF 捕捉否定表达更直接min_df2过滤只在一条评论里出现过的词降低噪声max_df0.9去掉在 90% 以上评论里都出现的词这些词通常是语气助词没有区分度。sublinear_tfTrue对词频做对数压缩避免长篇评论天然拥有更高权重。LogisticRegression的C1.0是正则强度倒数伪标签噪声大C 不建议比 1 更小否则模型过于保守学不到特征class_weightbalanced针对正负样本不平衡自动给少数类更高惩罚。这里需要特别说明的是tokenizer参数sklearn 会逐个调用它对原文分词不能用jieba.cut这种生成器必须用返回列表的lcut。3.3 TextCNN 做增量小数据规模下成本与收益的平衡点TF-IDF 加线性的 F1 通常能到 0.8 左右但要再往上走就得引入词向量和卷积网络。TextCNN 是短文本分类性价比最高的深度模型比 BERT 轻得多在一张无 GPU 的笔记本上也能跑。核心思想是用多个不同宽度的卷积核并行提取 n-gram 特征再对每个特征图做全局池化。import torch import torch.nn as nn import torch.nn.functional as F class TextCNN(nn.Module): def __init__(self, vocab_size, embed_dim100, num_filters128, kernel_sizes(2, 3, 4), num_classes2): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim, padding_idx0) self.convs nn.ModuleList([ nn.Conv1d(embed_dim, num_filters, k, paddingk // 2) for k in kernel_sizes ]) self.dropout nn.Dropout(0.3) self.fc nn.Linear(num_filters * len(kernel_sizes), num_classes) def forward(self, x): emb self.embedding(x) # (B, L, embed_dim) emb emb.transpose(1, 2) # (B, embed_dim, L) pooled [] for conv in self.convs: c F.relu(conv(emb)) # (B, num_filters, L) m torch.max(c, dim2).values # 每通道取最大值 pooled.append(m) out torch.cat(pooled, dim1) return self.fc(self.dropout(out))paddingk // 2是为了让卷积输出长度与输入一致这样torch.max池化时不会因为长度塌缩丢失边界信息。kernel_sizes(2, 3, 4)等价于同时看 2-gram、3-gram、4-gram覆盖微博短文本里最常见的短语长度。num_filters128是三组卷积共享的通道数对这个数据规模128 到 256 之间效果差异不大但 512 会明显变慢且容易过拟合。padding_idx0要求构造训练数据时用 0 代表填充位否则 Embedding 会把填充也算进语义。模型定义之后训练循环用最简单的写法optimizer torch.optim.AdamW(model.parameters(), lr1e-3) criterion nn.CrossEntropyLoss() for epoch in range(8): model.train() for batch_ids, batch_labels in train_loader: optimizer.zero_grad() loss criterion(model(batch_ids), batch_labels) loss.backward() optimizer.step() # 验证集上算F1连续两轮不升就保存当前模型并早停 print(fepoch {epoch 1} loss{loss.item():.4f})AdamW对权重衰减的处理比原始 Adam 更稳定短文本分类任务里默认用它学习率 1e-3 在几百条到几千条样本量下基本不用调epoch 设置 8配合验证集上的早停逻辑已经足够。如果数据量少于 2000 条不建议上深度学习TextCNN 的优势需要一定样本量才能体现强行使用只会过拟合。4. 训练闭环伪标签策略、验证集设计与阈值调优4.1 SnowNLP 批量打伪标签的两个缺陷没有人工标注时最省事的打伪标签工具是 SnowNLP它的sentiments属性输出一个 0 到 1 的积极性分数。但直接用它的裸分数划分正负会踩两个坑第一SnowNLP 的训练语料偏电商评论对微博短文本的整体打分偏保守第二它对“不怎么样”“绝了”这类口语化表达会给出反直觉的高分。解决办法是不直接信任它的分数绝对值而是用高低两个阈值夹出一个置信区间只取区间之外的样本。from snownlp import SnowNLP def pseudo_label_by_snownlp(text, low0.3, high0.7): try: p SnowNLP(text).sentiments except Exception: return None if p high: return 1 if p low: return 0 return Nonelow0.3, high0.7意味着只有模型最自信的 30% 和 70% 两侧样本才进入训练集中间 40% 直接丢弃。乍看浪费数据实际是明智的取舍伪标签训练最怕的就是把噪声标签当真理过滤掉模糊样本能显著降低训练崩溃概率。可以用词典法的分数过滤做第二层校验比如词典法打出强负分但 SnowNLP 给出正向的样本直接丢弃而不是勉强归类。两条互相矛盾的信号说明这条文本大概率带反讽或生僻表达留着只会增加噪声。4.2 验证集划分与类别不平衡准确率是陷阱伪标签完成后验证集的划分方式直接决定你对模型真实水平的判断。很多入门项目用train_test_split默认参数不做分层采样结果测试集里 95% 都是正类模型全预测正类也有 95% 准确率看起来漂亮实际毫无价值。微博评论的正负比例往往高度倾斜正确做法是分层采样加 F1 评估。from sklearn.model_selection import train_test_split train_texts, val_texts, train_labels, val_labels train_test_split( df[clean_text], df[pseudo_label], test_size0.2, stratifydf[pseudo_label], # 按标签比例分层 random_state42 )stratifydf[pseudo_label]保证训练集和验证集的类别比例一致避免极端失衡。评估指标建议只看 F1-score 和混淆矩阵不要单看准确率。正负样本如果差距悬殊两条路可以并行一是在模型里设置class_weight二是在训练集中对少数类做上采样。上采样的倍数建议控制在 2 倍以内超过 3 倍容易让模型对少数类特征过拟合在真实分布上反而掉点。这里还要警惕伪标签带来的分布偏移。SnowNLP 置信度高的样本通常是情感表达直白的评论真正困难的样本隐晦表达、反讽集中在被过滤掉的中间带。所以验证集上 F1 0.85不代表真实微博评论流上有 0.85需要保留一部分人工标注的真实测试集做最终把关。4.3 概率阈值与置信度过滤少而精胜过全而糙分类器默认以 0.5 为阈值划分正负但对情感分析这种类别模糊的任务0.5 往往次优。更好的做法是在验证集上扫描阈值找到让 F1 最大的那个点。你会发现预测概率分布呈现中间多、两端少的形态提高阈值会牺牲召回降低阈值会牺牲精确扫描的意义就是根据业务容忍度移动这个平衡点。import numpy as np from sklearn.metrics import f1_score proba model.predict_proba(val_texts)[:, 1] best_th 0.5 best_f1 0.0 for th in np.arange(0.30, 0.76, 0.02): preds (proba th).astype(int) f1 f1_score(val_labels, preds, pos_label1) if f1 best_f1: best_f1 f1 best_th th print(fbest threshold{best_th:.2f}, f1{best_f1:.4f})np.arange(0.30, 0.76, 0.02)以 0.02 步长扫描 0.30 到 0.75 的阈值这个区间覆盖大部分任务的最优点。如果扫描出的最优阈值偏向 0.7说明模型在正类上不够自信宁可少判也不要乱判偏向 0.3 则说明多数负类概率被高估。阈值确定后还有一个容易被忽视的操作对完全落在中间概率区间的预测结果不硬给标签而是标为“待人工复核”。在真实舆情系统里这个策略叫“少而精胜过全而糙”把模型没把握的样本筛给人工成本远低于在全部样本上追求 100% 正确率。5. 避坑手册微博情感分析里我踩过的 5 个坑5.1 采集频率过高触发平台限流一夜之间接口全 418现象采集脚本前几百条评论正常某个时间点后所有请求返回 418 状态码不再有 JSON 数据看起来是账号被限制。原因很直接请求频率太高加上 UA 是默认的 Python requests 字符串接口的风控策略识别出了非浏览器行为。解决分两层请求侧把 UA 换成手机端浏览器的标志每次请求之间加 2 到 5 秒随机延迟单线程跑数据侧不要把一次采集任务全押在单一 Cookie 上每套 Cookie 限量抓取触发限流后立即停止等一小时以上再继续。把 418 当作信号而不是异常遇到就退避而不是加大强度重试。5.2 新词分词被拆yyds 变成 yy 和 ds现象词典法给“yyds”打分始终为 0这个词明明看着就是正向情感。原因是 jieba 默认词典把这个词拆成多个无意义片段每个片段都不在情感词典里。解决就是前面说的自定义词典但要补一个细节词频不能乱填。填 10 会被部分语境忽略填 10000 又会在“yyds 这个缩写到底代表什么”这类句子里强行切错。我一般先填 10 跑一遍分词观察所有测试句子中这个词是否稳定成词不稳定再逐级提高。这个习惯比一次性填大词频更可控。5.3 emoji 被当作标点删除情感信号白白丢掉现象测试集中所有带大哭表情、生气表情的评论模型几乎全部判错。原因是预处理阶段我用正则删除了所有非中文字符emoji 和标点一起没了。微博用户大量靠表情符号表达情绪文字是中性大道理表情才是真实立场。解决改用emoji.demojize把它转成文字标记参与后面的 TF-IDF 和词典匹配。我在一次对比里发现从删除 emoji 改为转换 emojiF1 在验证集上直接涨了 3 个点这是投入产出比最高的一项修正。5.4 否定词翻转被忽略模型把“不好看”学成好看现象词典法评测里“不好看”“不推荐”全部落在正类。原因是简单累加权重时“好看”“推荐”的权重为正否定词“不”被分词切出来但没有参与计算。解决就是 3.1 节里的窗口翻转逻辑但在线性模型里还需要配合 n-gram 特征。我见过很多人只在词典法里处理否定却忘了在TfidfVectorizer里开ngram_range(1, 2)导致深度模型一样学不会否定。这两个位置必须同时处理词典法靠窗口翻转线性模型靠 bigram 特征模型才能理解“不好坏”。5.5 反讽识别是无底洞准确率卡在 0.85 的元凶现象模型把“这部电影太好看了好看到我全程玩手机”判为正类人工却认为是负向嘲讽。原因反讽依赖上下文和常识单纯看词面特征几乎无法识别属于当前小模型的能力边界。我的处理原则是承认边界不去硬解。通用做法是在持续迭代的数据里专门筛反讽样本用规则先粗筛正向情感词加高密度感叹号加转折词再人工确认后放入训练集让模型学到部分模式剩余无法识别的部分依靠置信度过滤交给人工。反讽识别是独立的研究课题一个 2000 条数据的小模型解决不了别在这上面过度投入。6. 往生产走一步把模型封装成命令行工具与上线前抽检6.1 用 argparse 把训练好的模型变成可交互工具模型调优完成后最顺手的落地形式是一个命令行工具输入一句微博评论输出正负判断和置信度。我习惯用joblib保存整个 pipeline包括 TF-IDF 和分类器而不是只保存模型权重这样新文本进来不用重新拼装预处理链。训练侧只需要一行joblib.dump(model, weibo_sentiment.joblib)预测侧这样写import argparse import joblib import jieba parser argparse.ArgumentParser(description微博评论情感预测) parser.add_argument(--text, typestr, requiredTrue) parser.add_argument(--model, typestr, defaultweibo_sentiment.joblib) args parser.parse_args() pipeline joblib.load(args.model) proba pipeline.predict_proba([args.text])[0, 1] label 1 if proba 0.5 else 0 print(f情感: {正向 if label else 负向} 置信度: {proba:.3f})加载后直接调用predict_proba拿到概率阈值也可以改成从命令行参数传入方便线上随时调。注意TfidfVectorizer如果使用了自定义tokenizer保存和加载时对 jieba 的依赖要在运行环境里保持一致否则加载后分词语料不同预测结果会和训练时不一致。6.2 上线前抽检 100 条记录错在哪每次拿到一批新数据我保留一个固定习惯随机抽 100 条模型预测结果逐条标注模型对错和错误类型按“反讽误判、否定误判、新词漏判、标签噪声”四类计数。这个过程能直观暴露模型在当前数据分布上的短板。比如某次抽检发现一半错误来自“差评里的正向词”说明否定窗口或 bigram 特征还需加强。这个习惯比盯着 F1 看更能指导下一步投入。教训留给后来者我最早做这个方向时删 emoji、瞎填词频、忽略分层采样三件事全撞上了改一圈下来 F1 从 0.72 涨到 0.83。踩坑不可怕可怕的是每一步都觉得模型不行其实数据预处理没到位。现在你拿到“基于新浪微博评论的情感分析”这类项目先别急着调参把数据管线打磨干净模型自然会给回报。希望帮到你。本文还有配套的精品资源点击获取