疫情期间网民情绪识别实战:数据清洗、特征工程与基线模型解析
2026/9/15 17:50:47 网站建设 项目流程

开门见山说一句:这个项目是我做过最有“真实世界感”的机器学习案例之一。题目叫“疫情期间网民情绪识别”,听起来高大上,拆开看就是一个标准的中文文本分类任务:输入一段网民评论,模型输出情绪类别,比如积极、消极、中性,再细一点可以分成担忧、愤怒、乐观、感动、理性讨论等。这次先发系列的第(一)部分,侧重数据处理、特征工程和基线模型,这部分是整套流程里最繁琐也最影响上限的环节,值得单独拿出来复盘。

这个项目适合谁?如果你正在学机器学习,刷完了吴恩达或者李宏毅的课,做过波士顿房价这类回归题,但还没完整走完一个中文NLP分类项目,那这篇很对胃口。课程作业一般把数据清洗好、特征处理好,你只管训练模型就行,真实项目里往往反着来:数据又脏又乱,标签还不均衡,特征怎么提、模型怎么选全是坑。写这篇就是想把从裸数据到能跑出可用模型的过程完完整整记录下来,顺便把踩过的坑和判断逻辑讲清楚,方便后来人直接抄作业。

1. 项目定位与整体思路

1.1 为什么选“网民情绪识别”这个题目

选这个题目的理由很实在。首先,情绪识别和文本分类、情感分析高度重合,属于NLP里最成熟、最不容易跑偏的方向,不像问答、摘要那种任务需要太多工程支撑,个人开发者在有限算力下也能做出效果。其次,网民评论这种短文本数据天然带有口语化、噪声大、表达碎片化的特点,做出来的模型泛化能力更有说服力。

更关键的是,这类任务贴近真实诉求。公共卫生事件期间,网民情绪是信息传播效果的直观反映:是恐慌扩散,还是信心占主导,通过评论情绪分布能看得很清楚。从技术角度说,情绪识别比简单的二分类积极/消极更有难度,因为“担心”和“愤怒”可能同时出现在同一条评论里,怎么定标签、怎么处理这种重叠,本身就是很有意思的问题。

另外,这个任务把入门阶段需要掌握的知识点全串起来了:中文分词、停用词过滤、文本向量化、模型选择、类别不平衡处理、评估指标权衡。一次性做完,等于把机器学习从数据到评估的完整闭环走了一遍,比零散地刷课后作业效率高得多。

1.2 业务目标与标签体系怎么定

做任何机器学习项目,第一步都不是上模型,而是把业务问题翻译成数学问题。这个项目里最关键的决定是标签体系。

最初我按最省事的方案,只分积极、消极、中性三类,跑了一版发现信息量太低。拿评论“终于又看到新增病例下降了,但大家别放松啊”举例,这句话既包含积极情绪又包含消极的警示意味,硬归到某一类里会丢失大量信息。后来我把任务改成了更细的多分类:积极、消极、中性之外,再加一个“焦虑/担忧”和一个“理性/中立讨论”。焦虑是疫情期间最有代表性的情绪信号,跟普通消极还不一样。

标签体系一旦定错,后面所有工作都得推翻重来。我见过不少项目在建模上花了很多功夫,最后发现因为标签定义模糊,模型再优化也上不去,问题就出在标签定义阶段。建议在动手前先随机抽500条评论,人工过一遍,看看哪些情绪类型真正高频出现、哪些边界模糊难分,再确定最终标签。有些词表面看着像两个不同类别,实际标注时很难区分,这种就直接合并。

1.3 技术路线选型与可行性分析

当时我列了三条技术路线做比对。第一条是传统机器学习线:TF-IDF特征加朴素贝叶斯或SVM分类器,优点是训练快、解释性强,缺点是对语义理解浅。第二条是深度学习线:Word2Vec预训练词向量加TextCNN或LSTM,能捕捉上下文信息,但需要自己处理词表、padding、训练轮次这些工程细节。第三条是预训练模型线:直接用中文BERT或RoBERTa做分类,效果通常最好,但显存占用大,训练时间长。

考虑到这个项目的最终目标是跑通流程并拿到可解释性较高的结果,我最终选择了这样的策略:先拿TF-IDF+SVM做一套基线,保证有稳定的分数下限;再用Word2Vec+TextCNN做增强版;最后如果时间充裕再上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(r'http\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_features=40000, min_df=5, sublinear_tf=True ) x_train = vectorizer.fit_transform(train_texts) x_val = vectorizer.transform(val_texts)

sublinear_tf=True这个参数很多人忽略,它把词频做了对数变换,防止某个词在特别长的文本里出现次数过多而主导整个向量。短文本场景里这个参数尤其好使。

Word2Vec则是另一种思路,它不是计算词频和权重,而是把每个词映射到稠密向量,让语义相近的词在向量空间里靠得近。比如“害怕”和“恐惧”的向量距离会很近。我最初的做法是直接平均整条评论的词向量,后来发现平均池化会把关键信息稀释掉,“就是不舒服”里“不舒服”已经足够表达情绪,但平均后它的权重被其他无意义词拉低了。后来改用TextCNN,用卷积核去抓局部关键信息,效果才有明显提升。

3.2 模型对比:朴素贝叶斯、SVM、RNN与BERT

我在同一份数据上对比了四种方案:朴素贝叶斯、SVM、TextCNN、BERT。这里放一个简化版的对比表格,方便直观感受差距。

模型参数量级训练耗时宏F1(5分类)可解释性备注
朴素贝叶斯很小秒级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_weight='balanced',让模型在损失函数层面给少数类更高权重;第二步是对训练集做分层采样,保证每个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_splits=5, shuffle=True, random_state=42) 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-IDF+SVM这条线,重点看C值。C控制正则化强度,太小容易欠拟合,太大容易过拟合。我用了网格搜索在0.1、1、10、100之间试,最终在C=10附近效果最好。核函数用的线性核,因为文本特征维度很高,线性核又快又不容易过拟合,不用刻意试RBF。

Word2Vec+TextCNN这条线,训练细节更琐碎。词向量维度我选了128,窗口选5,最小词频5。TextCNN的卷积核尺寸设置成(2,3,4),每个尺寸128个卷积核,捕捉不同长度的局部模式。训练时学习率初始设成1e-3,batch size 64,训练10个epoch。这里要注意early stopping,耐心参数设为2个epoch,验证集F1不再提升就停止,避免过拟合。

BERT这条线,我用的google-bert/bert-base-chinese,学习率5e-5,batch size 16,max_len 128,训练3个epoch。因为数据集不大,跑3个epoch就能收敛,再多反而过拟合。当时显存是12G的GPU,batch size调到32会显存溢出,所以用梯度累积模拟更大batch,实际效果差别不大。

语言模型的训练有一个技巧对我帮助最大:先用小批量数据跑一个epoch,确认loss在掉,再全量训练。能避免很多低级错误,比如数据预处理把标签和文本对错位,这种问题在全量训练时往往要等半个小时后才能发现。先用100条数据快速验证,几十秒就能暴露问题。

4.3 结果分析与错误样例复盘

模型跑完不是结束,看错误样例才是真正提升的地方。我把验证集里预测错误的样本全部打出来,大概两百多条,一条一条看,发现错误集中在这几类。

第一类是隐含情绪识别不出来。比如“天气不错,但是出不了门”,字面是积极,实际表达的是无聊和无奈。TF-IDF模型完全不认识“出不了门”和“天气不错”之间的转折关系,TextCNN能抓局部但抓不到跨子句的转折,只有BERT这种能建模上下文的模型才能识别出来。

第二类是反讽,这个比较难。“太棒了,又封了”这句话,模型很容易判成积极,因为“太棒了”是强积极词。但结合后文“又封了”才能看出是反讽。第二版改进时我给反讽样本单独做了一部分规则增强训练数据,把“太棒了”“真好”“谢谢啊”这些词在疫情场景下的反讽用法喂给模型,有一定效果,但无法根除。反讽识别本身就是NLP难题,不用强求。

第三类是长短句差异。短文本信息太少,比如“加油”一个人单独发出来,判积极没问题。但“吃饭了吗”这种跟主题无关的中性文本,会被分到其他类别。我的处理办法是加一个“无关/其它”类别,把跟疫情无关的日常表达统一收进去,这个类别对实际应用非常有用。

5. 常见问题与排查技巧实录

5.1 中文编码与乱码问题

处理中文文本,编码问题几乎人人都会遇到。pandas读CSV时指定encoding='utf-8'是常规操作,但数据源不一定全是标准的UTF-8,经常遇到gbk或gb18030编码的文件。我的处理方式是读取时用errors='ignore'忽略无效字符,避免整个文件读不进来。分词环节如果输出乱码,先检查终端编码,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条小数据跑通整套流程,再做全量扩展,别一开始就急着上大模型。先把数据收拾干净,把标签边界定清楚,后面每一步都会顺利很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询