疫情期间网民情绪识别实战:数据清洗、特征工程与基线模型解析

发布时间:2026/9/15 17:50:55
疫情期间网民情绪识别实战:数据清洗、特征工程与基线模型解析 开门见山说一句这个项目是我做过最有“真实世界感”的机器学习案例之一。题目叫“疫情期间网民情绪识别”听起来高大上拆开看就是一个标准的中文文本分类任务输入一段网民评论模型输出情绪类别比如积极、消极、中性再细一点可以分成担忧、愤怒、乐观、感动、理性讨论等。这次先发系列的第一部分侧重数据处理、特征工程和基线模型这部分是整套流程里最繁琐也最影响上限的环节值得单独拿出来复盘。这个项目适合谁如果你正在学机器学习刷完了吴恩达或者李宏毅的课做过波士顿房价这类回归题但还没完整走完一个中文NLP分类项目那这篇很对胃口。课程作业一般把数据清洗好、特征处理好你只管训练模型就行真实项目里往往反着来数据又脏又乱标签还不均衡特征怎么提、模型怎么选全是坑。写这篇就是想把从裸数据到能跑出可用模型的过程完完整整记录下来顺便把踩过的坑和判断逻辑讲清楚方便后来人直接抄作业。1. 项目定位与整体思路1.1 为什么选“网民情绪识别”这个题目选这个题目的理由很实在。首先情绪识别和文本分类、情感分析高度重合属于NLP里最成熟、最不容易跑偏的方向不像问答、摘要那种任务需要太多工程支撑个人开发者在有限算力下也能做出效果。其次网民评论这种短文本数据天然带有口语化、噪声大、表达碎片化的特点做出来的模型泛化能力更有说服力。更关键的是这类任务贴近真实诉求。公共卫生事件期间网民情绪是信息传播效果的直观反映是恐慌扩散还是信心占主导通过评论情绪分布能看得很清楚。从技术角度说情绪识别比简单的二分类积极/消极更有难度因为“担心”和“愤怒”可能同时出现在同一条评论里怎么定标签、怎么处理这种重叠本身就是很有意思的问题。另外这个任务把入门阶段需要掌握的知识点全串起来了中文分词、停用词过滤、文本向量化、模型选择、类别不平衡处理、评估指标权衡。一次性做完等于把机器学习从数据到评估的完整闭环走了一遍比零散地刷课后作业效率高得多。1.2 业务目标与标签体系怎么定做任何机器学习项目第一步都不是上模型而是把业务问题翻译成数学问题。这个项目里最关键的决定是标签体系。最初我按最省事的方案只分积极、消极、中性三类跑了一版发现信息量太低。拿评论“终于又看到新增病例下降了但大家别放松啊”举例这句话既包含积极情绪又包含消极的警示意味硬归到某一类里会丢失大量信息。后来我把任务改成了更细的多分类积极、消极、中性之外再加一个“焦虑/担忧”和一个“理性/中立讨论”。焦虑是疫情期间最有代表性的情绪信号跟普通消极还不一样。标签体系一旦定错后面所有工作都得推翻重来。我见过不少项目在建模上花了很多功夫最后发现因为标签定义模糊模型再优化也上不去问题就出在标签定义阶段。建议在动手前先随机抽500条评论人工过一遍看看哪些情绪类型真正高频出现、哪些边界模糊难分再确定最终标签。有些词表面看着像两个不同类别实际标注时很难区分这种就直接合并。1.3 技术路线选型与可行性分析当时我列了三条技术路线做比对。第一条是传统机器学习线TF-IDF特征加朴素贝叶斯或SVM分类器优点是训练快、解释性强缺点是对语义理解浅。第二条是深度学习线Word2Vec预训练词向量加TextCNN或LSTM能捕捉上下文信息但需要自己处理词表、padding、训练轮次这些工程细节。第三条是预训练模型线直接用中文BERT或RoBERTa做分类效果通常最好但显存占用大训练时间长。考虑到这个项目的最终目标是跑通流程并拿到可解释性较高的结果我最终选择了这样的策略先拿TF-IDFSVM做一套基线保证有稳定的分数下限再用Word2VecTextCNN做增强版最后如果时间充裕再上BERT。这三个方案共用同一套数据预处理和评估代码切换成本低还能横向比较不同技术路线的差距。事实证明这个判断是对的基线模型帮我在数据清洗阶段就发现了不少问题。2. 数据获取与预处理实战2.1 数据来源与合规说明情绪识别要有数据第一步就得解决数据从哪来。我当时用的是公开渠道可以拿到的中文评论数据包括新闻评论区、社交平台的公开讨论文本时间范围聚焦在2020年初到2022年之间。这里我必须多说一句数据合规是底线。所有数据我做了匿名化处理去掉网名、ID这类个人信息只保留纯文本内容而且只用于学术研究不涉及任何个人身份识别。如果你是学生找数据时优先用公开数据集或者自己爬但严格遵守平台条款和隐私规范别碰用户隐私。数据量方面一开始我只标注了3000条做原型验证跑通流程后扩大到2万条左右。实际体验下来情绪分类不是越多的数据就一定越好数据质量远比数量重要。相同数据量下清洗得干净、标签标得准的2万条效果能比标得乱七八糟的5万条高出不少。2.2 文本清洗的核心步骤中文评论数据的脏脏得很有规律。原始文本里常见的噪声包括超链接、HTML标签、用户名、多余空白、全角半角混用、繁体字、表情符号和连续重复的标点。这些噪声不仅占用资源还会直接影响分词效果。清洗时我的处理顺序是有讲究的不是随便写个正则替换就完事。我的清洗流程大致如下import re def clean_text(text): # 1. 去HTML标签 text re.sub(r.*?, , text) # 2. 去URL text re.sub(rhttp\S|www\.\S, , text) # 3. 去用户名 text re.sub(r[\w\u4e00-\u9fa5], , text) # 4. 全角转半角 text text.replace(, ,).replace(。, .).replace(, !).replace(, ?) # 5. 繁体转简体用opencc或zhconv # text OpenCC(t2s).convert(text) # 6. 去控制字符和多余空白 text re.sub(r[\t\r\n], , text) text re.sub(r\s, , text).strip() return text为什么强调顺序比如先转半角再做其他正则匹配效率更高先去掉URL再处理文本内容能避免URL里包含的符号干扰后续规则。我踩过一个坑一开始先做了全角转半角然后去URL结果URL里的标点被转换后正则没匹配上留下了几个残缺的链接字符串导致分词效果很怪。所以顺序要固定下来别今天这样洗明天那样洗。表情符号的处理需要单独说。网上很多人一刀切把所有emoji删掉但情绪识别场景里emoji本身就是情绪信号。“今天又是新增下降的一天”里的笑脸代表的是积极情绪。“这数据看不懂”里的表情明显是焦虑。我的做法是先用emoji库把表情转成文本标签比如把笑脸映射成[开心]、哭脸映射成[难过]再决定是否保留这些标签作为特征。这个操作对模型效果有提升实测能带来1到2个F1点的收益。2.3 分词与去停用词的细节中文NLP绕不开分词。我默认用jieba分词但没用默认的精确模式一把梭而是做了三个额外的配置。第一加载自定义词典。疫情相关文本里有许多专有名词比如“德尔塔”“奥密克戎”“无症状感染者”“流调”等如果不加自定义词典很容易被拆成“无/症状/感染/者”丢失整词语义。自定义词典的格式很简单一行一个词可以顺便指定词频和词性import jieba # 每行格式词 词频 词性比如无症状感染者 100 n jieba.load_userdict(user_dict.txt)第二停用词表不要无脑全删。网上流传的各种中文停用词表动辄几千词但里面往往包含“不”“没”“别”这类否定词。这些词在情绪识别里是强特征“不开心”“不乐观”“没办法”删掉否定词情绪方向直接反转。我当时的做法是维护一份自定义停用词表只去掉“的”“了”“吧”“啊”这类纯语气助词和副词否定词和转折词全部保留。第三对文本做“重复字符压缩”。“哈哈哈哈哈哈哈哈”这种表达在处理时会变成一长串“哈”我统一压缩成“哈”保留一个既保留情绪信号又避免特征维度爆炸。连续感叹号“”也是情绪强度的信号通常表示强烈的愤怒或惊讶我单独保留了一个标记。这些小细节能明显改善特征质量。2.4 标签标注与质量校验标签是整个项目的天花板标注质量直接决定模型上限。我用了两个方法控制质量一是多人标注一致性校验二是失败样本复盘。多人标注一致性校验的做法是让两个标注员独立标注同一批200条数据然后算标注一致性系数。系数低的地方基本都是标签边界模糊的地方要么拆分标签要么写清楚标注规则。比如“理性讨论”和“中性”我就反复纠结了很久后来干脆把“中性”限定为“无明显情绪”把“理性讨论”限定为“有分析、有建议、情绪平稳但信息量大”的评论边界一下子清楚了。失败样本复盘同样重要。模型训练完把预测错的样本打印出来看如果发现把某个标签的样本大量分错到另一个标签往往不是模型问题而是标注规则不一致。我就遇到过一次模型把“焦虑”都预测成“消极”回头看数据发现有一部分标注员把“真担心啊”标成了焦虑另一部分标成了消极标签打架模型当然学不干净。3. 特征工程与模型选择3.1 从TF-IDF到词向量特征方案怎么选文本数据不能直接喂给模型得先变成数值向量。传统方案里最有代表性的就是TF-IDF它衡量的是一个词在文档里的重要程度。TF-IDF的直觉很简单一个词在当前文本出现次数多但在其他文本里很少出现那它对当前文本更有区分度。比如“口罩”在疫情期间的评论里到处都有单独拎出来区分度反而不高而“抢不到”这种词更能标识负面情绪。用sklearn做TF-IDF向量化时几个关键参数需要根据自己的数据调整。我的经验是ngram_range设置成(1,2)把“不/担心”这样相邻的词对组合进来能捕捉一定局部信息。max_features一般设成30000到50000太小会丢词太大容易吃内存。min_df设成5过滤掉只在极少数评论里出现一次的词这些词通常是打错字或者太个人化的表达留着只会增加噪声。from sklearn.feature_extraction.text import TfidfVectorizer vectorizer TfidfVectorizer( ngram_range(1, 2), max_features40000, min_df5, sublinear_tfTrue ) x_train vectorizer.fit_transform(train_texts) x_val vectorizer.transform(val_texts)sublinear_tfTrue这个参数很多人忽略它把词频做了对数变换防止某个词在特别长的文本里出现次数过多而主导整个向量。短文本场景里这个参数尤其好使。Word2Vec则是另一种思路它不是计算词频和权重而是把每个词映射到稠密向量让语义相近的词在向量空间里靠得近。比如“害怕”和“恐惧”的向量距离会很近。我最初的做法是直接平均整条评论的词向量后来发现平均池化会把关键信息稀释掉“就是不舒服”里“不舒服”已经足够表达情绪但平均后它的权重被其他无意义词拉低了。后来改用TextCNN用卷积核去抓局部关键信息效果才有明显提升。3.2 模型对比朴素贝叶斯、SVM、RNN与BERT我在同一份数据上对比了四种方案朴素贝叶斯、SVM、TextCNN、BERT。这里放一个简化版的对比表格方便直观感受差距。模型参数量级训练耗时宏F15分类可解释性备注朴素贝叶斯很小秒级0.55~0.58强适合做baseline独立假设在文本分类里有点过于简单TF-IDF SVM中等分钟级0.63~0.66强稀疏特征下表现稳健适合小数据集Word2Vec TextCNN较小10分钟左右0.70~0.72中等能抓局部特征但依赖词向量质量BERT很大2~3小时0.76~0.78弱效果最好但显存和耗时要求高数字是我在固定验证集上跑出来的近似值不代表绝对水平但趋势是明确的从朴素贝叶斯到BERT效果逐步提升代价是训练时间和资源消耗也逐步增加。写到这里想多讲一句朴素贝叶斯常被当成弱鸡模型但它做文本分类的baseline其实很合适。它假设词与词之间独立这个假设在语言里显然不成立可是对短文本分类任务来说很多情况下即便假设不成立预测结果依然有参考价值。用来做快速验证数据质量、发现标签问题足够了。我在第一天就是靠朴素贝叶斯跑了一遍数据找出好几条没清洗干净的异常样本在跑大模型之前先把这些低级问题消灭掉。3.3 样本不均衡与评估指标评论情绪天然不均衡。疫情初期消极和焦虑的评论数量明显多积极和理性讨论相对少。直接拿原始数据训练模型会倾向于把所有样本都预测成多数类看似准确率很高实际上一点用都没有。这时候不能用准确率当主要指标。准确率在类别不平衡时极具欺骗性比如90%的样本是消极模型全预测成消极准确率就是90%但这种模型毫无价值。我改用了宏平均F1和加权平均F1。宏F1对每个类别单独算F1再取平均不会偏向多数类更真实反映模型在少数类上的表现。处理类别不平衡我按优先级做了三步。第一步是设置class_weightbalanced让模型在损失函数层面给少数类更高权重第二步是对训练集做分层采样保证每个batch里各类别比例相对均衡第三步才是考虑过采样或欠采样。但注意过采样不推荐直接复制少数类样本容易过拟合SMOTE这类生成方法在文本特征空间里效果也存疑。我在实际项目中靠前两步就解决了大部分问题。4. 模型训练与调参实录4.1 数据划分与验证策略划分数据看起来简单其实是个坑。如果用户A的评论进了训练集用户B的评论进了验证集而A和B在讨论同一个话题验证集效果会虚高。更好的做法是尽量按用户维度切分保证同一作者的所有评论都在同一个集合里。我一开始没注意结果验证集F1有0.70换做按用户切分后掉到0.66这才是真实水平。另一个重点是分层划分。直接用train_test_split默认切分可能把某个小类全切到测试集里。我用的是StratifiedKFold保证训练集和验证集里每个类别的比例和原始数据一致。跑五折交叉验证取平均分数作为模型真实水平的估计。from sklearn.model_selection import StratifiedKFold skf StratifiedKFold(n_splits5, shuffleTrue, random_state42) for fold, (train_idx, val_idx) in enumerate(skf.split(texts, labels)): train_texts [texts[i] for i in train_idx] val_texts [texts[i] for i in val_idx] # 训练流程聊一下为什么用五折而不是简单的三七开。短文本情绪识别对数据分布非常敏感万一验证集里抽到一批极端的表达波动会很大。五折交叉验证把训练分成五次每次用不同数据做验证最终结果更稳定。我当时的判断是如果五折平均F1超过0.70就说明模型和数据是可靠的值得上线测试。4.2 训练中的超参数与调参心得不同模型的调参重点差别很大。TF-IDFSVM这条线重点看C值。C控制正则化强度太小容易欠拟合太大容易过拟合。我用了网格搜索在0.1、1、10、100之间试最终在C10附近效果最好。核函数用的线性核因为文本特征维度很高线性核又快又不容易过拟合不用刻意试RBF。Word2VecTextCNN这条线训练细节更琐碎。词向量维度我选了128窗口选5最小词频5。TextCNN的卷积核尺寸设置成(2,3,4)每个尺寸128个卷积核捕捉不同长度的局部模式。训练时学习率初始设成1e-3batch size 64训练10个epoch。这里要注意early stopping耐心参数设为2个epoch验证集F1不再提升就停止避免过拟合。BERT这条线我用的google-bert/bert-base-chinese学习率5e-5batch size 16max_len 128训练3个epoch。因为数据集不大跑3个epoch就能收敛再多反而过拟合。当时显存是12G的GPUbatch size调到32会显存溢出所以用梯度累积模拟更大batch实际效果差别不大。语言模型的训练有一个技巧对我帮助最大先用小批量数据跑一个epoch确认loss在掉再全量训练。能避免很多低级错误比如数据预处理把标签和文本对错位这种问题在全量训练时往往要等半个小时后才能发现。先用100条数据快速验证几十秒就能暴露问题。4.3 结果分析与错误样例复盘模型跑完不是结束看错误样例才是真正提升的地方。我把验证集里预测错误的样本全部打出来大概两百多条一条一条看发现错误集中在这几类。第一类是隐含情绪识别不出来。比如“天气不错但是出不了门”字面是积极实际表达的是无聊和无奈。TF-IDF模型完全不认识“出不了门”和“天气不错”之间的转折关系TextCNN能抓局部但抓不到跨子句的转折只有BERT这种能建模上下文的模型才能识别出来。第二类是反讽这个比较难。“太棒了又封了”这句话模型很容易判成积极因为“太棒了”是强积极词。但结合后文“又封了”才能看出是反讽。第二版改进时我给反讽样本单独做了一部分规则增强训练数据把“太棒了”“真好”“谢谢啊”这些词在疫情场景下的反讽用法喂给模型有一定效果但无法根除。反讽识别本身就是NLP难题不用强求。第三类是长短句差异。短文本信息太少比如“加油”一个人单独发出来判积极没问题。但“吃饭了吗”这种跟主题无关的中性文本会被分到其他类别。我的处理办法是加一个“无关/其它”类别把跟疫情无关的日常表达统一收进去这个类别对实际应用非常有用。5. 常见问题与排查技巧实录5.1 中文编码与乱码问题处理中文文本编码问题几乎人人都会遇到。pandas读CSV时指定encodingutf-8是常规操作但数据源不一定全是标准的UTF-8经常遇到gbk或gb18030编码的文件。我的处理方式是读取时用errorsignore忽略无效字符避免整个文件读不进来。分词环节如果输出乱码先检查终端编码Windows环境有时需要chcp 65001切换到UTF-8代码页。5.2 模型只预测多数类怎么办这是类别不平衡的典型症状。模型训练完打印分类报告发现少数类precision和recall全是0模型直接放弃了少数类。我的排查顺序是这样的先看训练集标签分布确认少数类占比再看class_weight有没有生效最后看验证集是不是没做分层抽样。解决后一般会好很多。如果还不行就考虑把少数类的阈值下调也就是分类时不再取概率最大的类别而是概率超过某个较低阈值就判为少数类。5.3 关键词词表带来的误判做情绪识别时很容易想到直接维护积极词表、消极词表然后看评论命中了哪些词来判断情绪。这个方案起步快但后续很痛苦。“药真的难买”命中了“真”这个字规则可能判积极实际是牢骚。“疫苗出来了吗”命中“疫苗”和“出”两个词规则很容易当成积极信号。文本是上下文组合的艺术诚实地说规则方案做精确匹配的上限很低更适合用来做弱监督预标注而不是最终方案。5.4 训练内存不足与工程优化特征阶段内存容易爆多跑几次发现是max_features设置太大几万维的TF-IDF矩阵在内存里非常可怕。如果仍然不够可以用scipy.sparse存储稀疏矩阵sklearn接口默认支持。BERT训练显存不足的话优先调小max_len很多评论在128字以内没必要用512。其次调小batch size最差情况用梯度累积。我还在训练中加了模型保存策略按验证集F1保存最优权重防止最后几个epoch过拟合覆盖掉最好的模型。问题现象可能原因排查思路解决建议验证集分数虚高数据划分泄漏同一作者跨集合按用户ID分组切分用GroupKFold模型全预测成多数类类别不平衡打印分类报告查看少数类F1class_weight 分层采样分词结果奇怪专有名词未识别查看分词样例自定义词典训练loss不降数据标签错位抽几条人工核对重建训练集BERT显存溢出max_len或batch过大观察显存占用调小max_len或batch最后再分享一个我个人的经验。这个项目做完第一部分我最大的体会是NLP里模型选型固然重要但数据清洗和标签设计的杠杆效应远远超过调参。后面如果有人要复现这个项目我建议先从3000条小数据跑通整套流程再做全量扩展别一开始就急着上大模型。先把数据收拾干净把标签边界定清楚后面每一步都会顺利很多。