做商品评论的实体情感分析,最初是因为一个运营同事拿着报表来找我:“我们店铺整体评分有4.9,但评论区里到底差在哪,能不能拆细一点?”他说这话之前,我已经跑了整段评论的情感分类,准确率看着还不错,但一落到“为什么评分下跌”、“哪个部件被骂最多”这种问题上,整个模型就像拳头打在棉花上,根本使不上劲。那段时间我啃了不少论文,才发现自己真正需要的是“实体情感分析”——不光判断一句评论是正向还是负向,还要把评论中提到的“实体对象”和对应的“情感态度”拆出来。这篇文章就围绕“商品评论中的实体情感分析”这个主题,把我从数据标注、模型选型到落地踩坑的完整过程整理出来,重点聊聊为什么这么做、代码怎么组织、以及哪些坑是别人不会写进文档里的。
1. 为什么非要做实体级情感分析,而不是整体跑个正负面
1.1 一个运营同事抛过来的问题
回到最开始的场景。运营给我看的那条差评到现在我还记得:“手机挺好用的,屏幕显示非常细腻,就是电池一天三充,续航真的拉垮。”
如果交给整段情感分类模型,这句话大概率会陷入两难——它既包含明确的正面表述“挺好用”“非常细腻”,又包含明确的负面表述“一天三充”“拉垮”。很多模型为了强行给出一个整句标签,最后会把这句判成中性,甚至因为负面词权重更大而判成负面。这就有问题了:这句话的真实意图不是“这个商品整体好不好”,而是“手机整体可以,但电池需要改进”。
整体情感分析在这种场景下只能给出一个“水位线”,告诉你评论区总体偏正还是偏负,但没法回答“哪个部件被抱怨最多”“哪个卖点被夸得最狠”。实体情感分析要解决的,恰恰就是把同一句话里的不同对象拆开,分别判断态度。它输出的是“评价对象 + 情感极性”这样的结构:屏幕——正面,电池续航——负面。只有当数据拆到这个颗粒度,运营才可能知道下一版产品究竟该找屏幕供应商聊聊,还是该换电池方案。
1.2 实体情感分析在技术层面到底是什么
在NLP里,实体情感分析一般对应英文的Aspect-Based Sentiment Analysis,或者叫Target-Level Sentiment Analysis。它跟普通文本分类的区别在于,任务被拆成了两个相互关联的子问题。
第一个子问题是实体抽取,学术上常叫Aspect Extraction,简单说就是从评论里找出“用户在评价什么”。这个“实体”不一定是品牌或产品名,更多时候是商品的具体维度,比如手机评论里的“屏幕”“电池”“手感”,餐厅评论里的“上菜速度”“环境”“服务态度”。抽取结果一般用序列标注来表示,BiO标签把句子里的每个字或词标成实体的开始、中间或非实体部分。
第二个子问题是情感分类,也就是针对抽取出来的每个实体,判断用户表达的态度是正向、负向还是中性。这里有个很容易被忽视的细节:同一个实体在同一句话里可能被不同的观点词修饰。比如“这款耳机降噪不错,但戴着夹头”,这里的“降噪”和“佩戴舒适度”其实是两个实体,对应相反的情感极性。如果只用一轮抽取后直接扔给分类器,经常会漏掉第二个实体,导致信息丢失。
所以实体情感分析不是一个简单的“先抽取再接分类”就能完美解决的任务,它需要在上下文中理解实体和观点词之间的语义关联。这也是后面我们选择BERT这类预训练模型而不是传统词典规则的重要原因。
1.3 方案选型:为什么我没有一上来就上深度学习
在动手之前,我对整个技术路线做了个简单的评估。同类型的项目通常有三条路可以走。
第一条路是纯规则匹配。先建一个商品属性词典,扫描评论中是否出现“屏幕”“续航”这些词,再在词附近找“清晰”“拉垮”这类情感词,通过词性和距离规则来判断极性。这条路在小样本上见效极快,而且可解释性好——你能够明确告诉运营“这句话是因为出现了‘电池’和‘拉垮’,所以判断为负面”。但缺点是词典覆盖有限,遇到“掉电飞快”“用一天就没电”这种没有直接出现属性词的表达,规则就会失效。
第二条路是传统机器学习。把实体抽取作为序列标注问题用CRF或BiLSTM+CRF来解,把情感分类作为短文本分类问题交给SVM或TextCNN。这条路的优势是结构清晰,对算力要求低,在数据量只有几千条时也能跑出不错的效果。缺点是特征工程耗时,跨品类时特征泛化能力很差。
第三条路是基于预训练模型的端到端方案。用BERT及其变体把抽取和分类合并成一个多任务目标,或者干脆用生成模型输出结构化的标签。这条路效果通常是最好的——尤其在商品评论这种句式随意、口语化严重的文本上,预训练模型对表达变体的鲁棒性明显更强。缺点是对显存和数据量都有更高要求,推理速度也比前两条慢。
我最终的选择是“先搭规则老基线,再上BERT做主力”,原因很简单:规则基线能在项目第一天就出结果,给后续对照组提供参考;而BERT方案虽然效果最好,但需要更多的时间调试数据标注和质量。两条腿走路,比一上来就追求SOTA要稳得多。
2. 数据准备:实体标注规范比模型更决定上限
2.1 实体类别怎么定才不漏不重
很多人以为实体情感分析的重点在模型,但我在这个项目里最大的体会是:数据标注规范做得好不好,直接决定模型能学到什么边界。
第一步是把实体类别定清楚。以3C数码商品评论为例,我最初整理了下面这组实体类别:
| 实体类别 | 说明 | 示例 |
|---|---|---|
| 外观设计 | 颜值、配色、做工、材质 | “手感圆润”“塑料感很强” |
| 屏幕显示 | 清晰度、亮度、色彩 | “屏幕颗粒感太重” |
| 性能 | 运行速度、卡顿、发热 | “打游戏会发热降频” |
| 电池续航 | 待机时间、充电速度 | “半天就没电了” |
| 系统操作 | 界面流畅度、交互逻辑 | “系统广告太多” |
| 摄像头 | 拍照清晰度、夜景效果 | “夜景噪点明显” |
| 售后物流 | 客服态度、发货速度 | “退货客服半天不回复” |
| 性价比 | 价格、折扣、值不值 | “这个价位还要什么自行车” |
这里有个关键原则:类的划分颗粒度要跟业务问题对齐。运营关心的是“哪个功能模块出了问题”,不是“哪个词被吐槽”。如果你把“电池”和“充电速度”拆成两个独立实体,统计时虽然更精细,但样本分布会变得很稀疏,模型在训练时反而学不好。我建议先粗后细,第一版把所有实体控制在8到12个类别以内,跑通之后再用模块归因。反过来,类别之间也不能出现明显的语义重叠。比如“外观设计”和“性价比”如果定义不清,标注员面对“这个价位长得这么好看”就不知道到底标哪个,最终会制造大量标注噪音。
2.2 观点词与评价对象的关系怎么标注
实体类别只是第一步,更关键的是要把“实体”和“观点”的关系标出来。这里我踩过一个不小的坑:最初标注只标了实体的边界和情感极性,没有标观点词,导致模型在训练时并不知道判断依据是什么。
比如“信号有点拉胯”和“信号勉强能用”,实体都是“信号”,极性却完全相反。如果标注数据只给出“信号——负面”,模型必须自己学会从上下文找证据,这在小样本情况下往往学不准确。后来我把标注规范改成三元组结构:实体边界 + 情感极性 + 触发观点词。每标注一条评论,标注员都要指出是哪个词让判断成立。比如“信号有点拉胯”,就要记录实体是“信号”,观点词是“拉胯”,极性为负面。
这样做有三个好处。第一,标注过程会强制标注员理解语义,减少凭感觉乱标的情况。第二,训练时可以把观点词位置作为额外特征或输入片段,让模型更聚焦。第三,上线后做错误分析时,能快速定位是实体抽错了还是观点判断错了,排查效率提升非常明显。
标注工具方面,我用的是开源标注平台加自定义模板,字段包括评论ID、实体起始位置、实体结束位置、实体类别、观点词起始位置、观点词结束位置、情感极性。每批数据至少安排两个人独立标注,再用Kappa系数做一致性检验。第一批数据如果Kappa低于0.7就别急着训练,大概率是规范有歧义,需要先调整标注细则。
2.3 数据清洗与扩增的实操细节
数据清洗在评论类项目里有一个极容易被忽略的现象:电商评论里充满了“默认好评”和“无意义灌水”。像“好评!”“发货快”“666”这种短文本,虽然可能是真实购买行为,但对实体情感分析而言几乎没有信息量。我的做法是先统计评论长度和关键词命中情况,做一轮初筛,把长度小于3个字、不含任何实体词或观点词的评论直接过滤掉。
另一个需要注意的问题是平台自带的“追评”和“标签式评论”会污染统计口径。比如系统生成的“此用户没有填写评价”,以及一行一个卖点的格式,都会让实体抽取对“实体密度”失真。我的处理方式是单独保存原始评论,只在建模时使用清洗后的子集,避免后续做归因分析时无法追溯。
数据扩增这块,我试过简单回译、同义词替换和句法扰动。对于商品评论,同义词替换效果反而一般,因为商品属性词非常固定,替换“屏幕”成“显示器”在业务上并不可接受。真正有效的是基于预训练模型的生成式数据增强——把评论中的实体和观点词挖空,重新生成同语义句子。这个办法能把原始数据量扩到两倍左右,但也要人工过一遍,防止生成出业务上不存在的组合。
3. 从规则基线到深度学习:核心建模环节逐段拆解
3.1 先搭一个基于规则的Baseline,保证当天能出结果
在正式训练深度学习模型之前,我建议先做一个规则基线。这样做不是为了追求精度,而是为了建立“参照系”——后续模型效果有没有提升,得和这个基线比,而不是凭感觉说“看起来不错”。
规则基线的逻辑很简单,分为三步。第一步是实体识别,用一个业务词典加上少量正则规则,在评论中匹配已经出现过的实体词。第二步是观点词定位,在实体词周围一个窗口范围内寻找情感词。我用的窗口大小是前后各5个字符,超出窗口的候选词不再参与判断,避免“续航”被远处的“好看”干扰。第三步是极性判断,用一个正负情感词表加上否定词反转规则,比如“不清晰”“没什么问题”都需要通过否定词表做语义翻转。
用Python实现这么一个基线很快,核心部分大概长这样:
import re # 简化版规则引擎 entity_dict = ["屏幕", "电池", "续航", "手感", "信号", "拍照", "运行速度", "性价比"] pos_words = {"清晰": 1, "细腻": 1, "流畅": 1, "耐用": 1, "满意": 1, "快": 1, "好": 1} neg_words = {"拉垮": -1, "卡顿": -1, "模糊": -1, "慢": -1, "差": -1, "垃圾": -1, "失望": -1} neg_prefix = ["不", "无", "没", "别", "不用"] def rule_predict(text): results = [] for entity in entity_dict: for m in re.finditer(entity, text): start, end = m.span() window = text[max(0, start-5): min(len(text), end+5)] score = 0 for word, val in {**pos_words, **neg_words}.items(): if word in window: polarity = val # 检查否定词 if any(neg in window[max(0, window.find(word)-2): window.find(word)] for neg in neg_prefix): polarity = -polarity score += polarity if score != 0: results.append({"entity": entity, "sentiment": "pos" if score > 0 else "neg"}) return results这个基线在第一批500条测试数据上,实体抽取的准确率接近60%,情感分类的准确率大概70%,不算好,但足以说明三条关键结论:第一,数据里的实体密度确实很高,值得做细粒度分析;第二,规则方法对“漏匹配”和“上下文歧义”的无能为力,为引入BERT提供了充分的动机;第三,基线暴露了业务指标中最需要关心的样本——比如“续航”这个词在评论里高频出现,应当作为模型评估的一个重点类别单独看。
3.2 用BERT做实体抽取与情感判断的关键实现
规则基线验证完数据可行性之后,我把主力模型换成了BERT。具体做法是把它组织成一个多任务联合模型,共享同一个BERT编码器,同时输出实体边界、实体类别和情感极性三组标签。
我用的是Transformers库微调一个中文预训练模型,输出层挂三个分类头。实体边界和实体类别合并成序列标注任务,标签空间是“BI-O + 实体类别”,比如B-电池、I-电池、O。情感极性则只在实体位置计算,对每个实体token判断正负中性。这种设计的核心原因在于:实体抽取需要依赖上下文判断边界,而情感判断又依赖实体已经无误地抽出来,两个任务共享编码器可以互相补充信息。
下面是训练配置的核心代码,框架上只是一个普通的多任务Fine-tuning脚本,但在标签处理和损失函数上有不少细节需要注意:
from transformers import BertTokenizerFast, BertForTokenClassification from torch.utils.data import Dataset, DataLoader import torch tokenizer = BertTokenizerFast.from_pretrained("hfl/chinese-macbert-base") model = BertForTokenClassification.from_pretrained( "hfl/chinese-macbert-base", num_labels=len(id2label) # id2label里包含实体和情感标签 ) class ReviewDataset(Dataset): def __init__(self, texts, labels): self.texts = texts self.labels = labels def __len__(self): return len(self.texts) def __getitem__(self, idx): text = self.texts[idx] label = self.labels[idx] enc = tokenizer(text, truncation=True, max_length=128, return_offsets_mapping=True) labels_aligned = [] for i, offset in enumerate(enc["offset_mapping"]): if offset == (0, 0): labels_aligned.append(-100) # 特殊token不计算损失 else: # 根据字符偏移映射到BI标注 labels_aligned.append(align_label(offset, label)) enc["labels"] = labels_aligned return {k: torch.tensor(v) for k, v in enc.items() if k != "offset_mapping"}这里最值得注意的,是标签对齐。中文BERT通常用字粒度的tokenizer,但标注规范是按“实体词”记录的。我前几次训练出现loss降不下去的情况,排查到最后发现就是标签对齐错了——有的实体跨过多个token,SimpleMapping会把标签对齐到[CLS]或者偏移一位。解决方案是在embedding前记录offset_mapping,逐个token映射回原始字符位置,再落到标注标签上。
3.3 参数选择与阈值:别让准确率数字骗了你
模型训练的参数选择上,我做了一组对比实验,最后选定的组合是:学习率2e-5、Batch Size 32、最大序列长度128、训练轮数5轮。这组参数并不是网上抄来的,而是根据早停机制验证后选出来的——第5轮之后验证集loss明显回升,说明模型已经开始记训练集的噪音了。
更多时候,真正需要费心思的是评估指标的拆解方式。我用三套口径同时看模型效果:
第一套是实体抽取级别的指标,计算实体边界和类别都完全正确的准确率、召回率、F1。第二套是情感分类级别的指标,只有在实体抽取正确的前提下,才判断情感极性是否正确,这样能端到端反映业务关心的“这个实体被识别出来,且情绪判断正确”。第三套是类别粒度汇总,把每个实体的正负占比按评论出现频次做加权,形成一份“属性口碑表”。
这里特别想提醒一句:别看到总体准确率95%就觉得可以上线。我的模型在“性能”“屏幕”这类高频类别上能到96%,但在“售后物流”和“价格”这两个低频类别上只有七成。如果不分细类去看,这些明显偏低的口碑会在均值里被掩盖。后来我在模型评估报告里强制要求按类别给出明细,凡是低于整体F1超过10个百分点的类别,一律标红重新检查。
3.4 CPU/无GPU环境的替代方案
不是所有项目都有可用的GPU。我在一台只有CPU的服务器上跑过这个需求,两种替代方案都可以考虑。
一种是换轻量级模型,比如用ALBERT或DistilBERT变体,把参数压到可以用CPU在可接受时间内跑完一轮推理。另一种是干脆退回到第2节提到的规则基线,加上更完整的领域词典和句法规则。我在实际项目里发现,如果商品品类非常聚焦,比如只分析某品牌手机,规则基线的F1也能冲到80%左右,配合人工抽检完全够用。深度学习模型真正的优势,体现在跨品类、多表述场景下的泛化能力,但如果你的样本只有几千条且品类固定,把标注质量做上去可能比上大模型收益更高。
4. 踩坑实录:常见问题与排查方法
4.1 训练不收敛与过拟合的排查实录
我先遇到的第一个问题是训练loss抖动不下降。刚开始我怀疑是学习率过大,调低到1e-5之后依然没有明显改观。后来逐步排查发现是数据里存在大量标签噪音——有将近15%的标注样本把情感极性标反了,尤其是“中性”类,标注员普遍觉得“没有明显夸也没有明显骂”的评论很难归类,于是随手标了中性。
解决方案不是疯狂清洗重标,而是先做一遍噪音样本检测。我用训练好的模型对训练集做预测,找出预测置信度高但和人工标注不一致的样本,再让第二个标注员复核。这批样本复核后,有超过一半确实是被标错了。把错误标签修正后,train loss和val loss都在一轮内恢复了正常收敛趋势。
另一个常见问题是过拟合,验证集F1在第3轮后就停滞,而训练集F1还在涨。除了早停之外,我发现对评论数据最有效的策略是加大Dropout和做对抗训练。单纯增加数据量的效果反而一般,因为电商评论的语义模式高度重复,几千条高质量数据已经能覆盖大部分表达。
4.2 实体边界漂移与多观点冲突
实体抽取最怕的问题之一是“边界漂移”。模型会把“电池续航”抽成“电”“电池”“池续”等各种残缺片段。这个问题在短文本上特别明显,比如“续航不错”,模型时常把“航”丢了,只抽到“续”。我排查后发现原因是训练数据里“续航”这种双字词偏多,而BERT的字向量没有见过“航”单独出现在这个语境里。最简单的修复方式是给每个实体类别准备一份标准词表,在模型输出后做后处理,把不完整实体匹配到词表上。这个后处理虽然朴素,但对实体抽取F1的提升通常在5个点左右。
多观点冲突是更隐蔽的问题。比如“屏幕清晰,但颜色太艳,看久了眼睛累”这句话里,“屏幕清晰”和“颜色太艳”虽然都跟屏幕相关,但一个夸一个怼,规则模型很难分清楚。我最后的处理办法是把“实体+观点词”作为依存对进行建模,输入BERT时把观点词位置用特殊标记包裹起来,相当于告诉模型“请重点关注这两个位置的语义关系”。这个技巧在BERT模型上效果立竿见影,多情感极性冲突的准确率提升非常明显。
4.3 跨品类迁移与冷启动
项目刚开始只覆盖3C数码,后来扩展到美妆个护,直接把旧模型应用过去,效果只能用惨不忍睹来形容。原因倒也简单:旧模型见过的实体类别里没有“保湿”“质地”“痘痘肌”这些词,观点词的分布也不同,“敷完闷痘”这种表达在3C语料里根本不可能出现。
应对冷启动,我的经验是“三层递进”。第一步,把旧模型的参数作为初始化,冻结BERT层,只用新品类数据微调输出层,这样能在几百条新数据下快速收敛。第二步,从线上评论里用规则基线跑一轮粗糙标注,筛选出高置信样本补充到训练集,形成伪标注数据。第三步,等精确标注数据积累到一定量后,再全参数微调一轮。这套流程下来,美妆品类的实体抽取F1从不到50%冲到了80%以上,而训练数据量只用了3C项目的一半。
品类迁移还有一个常被忽略的坑:同一个词在不同品类里可能表达完全不同。比如“轻薄”在笔记本评论里是明显的正向卖点,但在羽绒服评论里可能意味着“不暖和”。所以跨品类时不能复用旧模型的整体决策边界,至少要把每类实体的情感极性分布重新验证一遍。
5. 从分析报告到业务联动:落地输出与后续扩展
5.1 从离线批量到实时流式
模型稳定之后,我开始搭建一个每周定期运行的离线分析任务。数据源是平台后台导出的评论,流程是:读取原始评论、规则初筛、BERT推理、结果入库、生成报表。早期的报表是一张静态Excel表,运营用了两周后提了两个优化需求:一是希望按商品SKU维度汇总实体口碑,而不是只看全店铺;二是希望能看到趋势,比如某实体口碑从上周到这周是变好还是变差。
为此我把结果表设计成了“评论ID + SKU + 实体类别 + 情感极性 + 观点词 + 置信度”这种宽表结构,下游可以任意聚合。对“商品评论中的实体情感分析”来说,这张宽表才是整个项目的真正价值资产。模型是一时的,但清洗和预测后的结构化数据可以反复被BI、用户调研和产品推荐逻辑复用。
实时流式是后来的事。当评论量上涨到每天数万条之后,我开始用消息队列接上游评论流,再用消费进程调用模型推理接口,结果直接写入OLAP库。这一步在技术上并不复杂,真正需要花时间的是推理性能压测和降级方案。目前看来,遇到大促评论峰值时,先把高优先级SKU的评论送进模型,其他评论走队列缓冲是最稳妥的策略。
5.2 与销量、客诉数据的联动分析
实体情感分析如果只停留在“评论区口碑统计”层面,价值容易被人质疑。后来我们把结果和销量、退款率、客服投诉数据做了关联分析。有两条发现非常有说服力:第一条是某型号手机的“电池续航”实体负面占比和退货率呈现明显的同涨同跌关系,二者的Spearman相关系数超过0.7;第二条是“摄像头”实体口碑和活动页转化率之间呈现出非线性关系,只有在口碑分低于某个阈值时才显著影响转化。
这类联动分析用到的方法并不高深,关键是把实体情感分析结果当成一个独立变量,而不是只当作描述统计。实际操作时要注意对齐时间口径,评论的发表时间会影响分析结论,尤其是新品首发那几周评论量激增,口碑分布会有明显偏移。我建议在做相关性分析之前先对评论时间做滚动平均,降低短周期波动带来的误判。
5.3 对运营和产品的实际输出形态
项目做到最后,真正被业务团队高频使用的不是模型、不是代码,而是一张“实体口碑趋势看板”。这张看板上有一张主图表,展示各实体情感极性的周度变化;还有一个明细表,点击任意一个实体类别,就能看到被模型判为负面的原始评论和观点词。运营看到“电池续航”负面占比升高后,可以直接点进评论详情,快速确认是产品问题还是同行刷评,再决定是否反馈给产品团队。
个人在交付时最大的体会是:给业务方展示的时候,一定要带着一个能解释的case。不要只说“我们的模型F1有89%”,而要打开一条具体评论,指着标注结果说:“你看,这句里的屏幕和电池被分别识别出来了,屏幕判定正面、电池判定负面,就是因为‘细腻’和‘拉垮’这两个触发词。”这种带案例的输出,比抽象指标更容易建立信任,也会让后续的优化需求更具体可落地。
最后再分享一个判断项目是否成功的标准,是我自己用来复盘这个项目的尺子:模型上线三个月后,运营是否已经离不开这张口碑看板;质检团队是否能用它替代一部分人工抽检;产品经理是否能从评论数据中找到至少一个改进产品的明确动作。如果这三个问题都答“是”,那么实体情感分析这个项目才算是真正完成了从技术到业务的闭环。