资讯详情

细粒度情感分析实战:SemEval-2014 Task 4数据集解析与BERT预处理

📅 2026/10/20 7:07:00 | 华诺云谱 👁 阅读
细粒度情感分析实战:SemEval-2014 Task 4数据集解析与BERT预处理
简介这是一份用于细粒度情感分析与方面级情感分析的标准评测数据集面向NLP研究者、算法工程师及高校相关专业学生可支撑情感词典构建、情感目标抽取、情感极性判断以及深度学习模型的训练与评估。压缩包共11个文件以10个XML标注数据和1份PDF标注指南为主整体仅1.31MB包含餐厅与笔记本电脑两个领域的训练集、验证集及PhaseA/PhaseB两阶段测试数据其中训练与验证数据分别用于模型构建和参数调优两阶段测试数据则递进检验模型的泛化能力XML文件负责存储带方面级情感极性的评论标注PDF详细解释标注规则与评测标准。已有1220人学习下载适合作为学术实验、课程设计或竞赛复现的基准资源。读者可借此直接开展跨领域细粒度情感分析研究在统一任务上客观对比不同模型的表现同时借助配套标注指南快速理解数据结构并校验算法输出。1. 别拿 SemEval-2014 Task 4 当普通情感分类数据这是细粒度 ABSA 的起点情感分析和自然语言处理方向的同学迟早会撞上这个数据集。它全名 Aspect-Based Sentiment Analysis基于方面的情感分析是 SemEval-2014 第四个子任务Task 4的官方标注数据。很多人第一眼看到它以为是 SST-2 那种句子级正负情绪分类直接拿一个句子一个标签喂给模型跑 Softmax结果指标刷得好看一上自己的业务场景就完全失灵——因为它的标注单位是句子里的评价对象不是整个句子。这份数据集解决的是“这家餐厅的寿司好吃但服务慢”这种复杂句里机器能不能分开判断寿司和服务的情感倾向。它面向的目标人群很明确正在做细粒度情感分析、需要标准评测基准的算法工程师、NLP 方向的研究生、以及想用 ABSA 思路改造自己评论分析系统的从业者。理解了这一点这个 zip 包的解压才是有意义的动作。2. 数据包内部结构XML 标注体系与 train/test 划分逻辑2.1 两个领域、四层标注字段先搞清楚每个文件是什么解压 SemEval-2014 Task 4数据集.zip 之后目录层级很规整。根目录下是 Laptop 和 Restaurant 两个领域的文件夹每个文件夹里各有 train 和 test 两个 XML 文件。Laptop 领域源自笔记本产品的用户评论样本量偏小但句子表达更零散Restaurant 领域是餐饮评论句子结构更完整也是后来绝大多数 ABSA 论文跑实验的主力数据。两个领域共享同一套标注规范这点特别关键——你在餐饮数据上调好的代码直接换路径就能在笔记本数据上跑不需要改解析逻辑。每个 XML 文件里的基本单元是sentence标签一个 sentence 对应一条原始评论句子。sentence 内嵌两个核心块text存原文aspectTerms和aspectCategories存标注结果。aspectTerms 处理的是“评价对象在原文里出现过的词”比如“寿司”和“服务”这种实体词aspectCategories 处理的是“评价对象在原文里没直接出现但语义明确”的类别比如“价格”“食品质量”。抓重点一个句子可以同时有多个 aspect term、多个 aspect category每个 aspect 都有自己的情感极性标签——这就是细粒度这个说法的来源。2.2 解析 XML 的正确姿势用 ElementTree 而不是正则外部看是 XML内部看是带坐标的标注格式。aspectTerm 节点长这样aspectTerm termsushi fromx toy polaritypositive/其中 from 和 to 是这个词在 sentence text 里的字符级偏移量。第一次解析这个文件的人最容易踩进去的坑是在text里用find()去找词的位置然后把索引当作 from/to。事实证明因为 XML 特殊字符会被转义比如amp;还原成后长度发生变化这个索引经常和标注对不上。我一般直接用 xml.etree.ElementTree 解析代码不长也不需要引入额外的第三方库。import xml.etree.ElementTree as ET def parse_semeval2014(xml_path): tree ET.parse(xml_path) root tree.getroot() samples [] for sentence in root.iter(sentence): text sentence.find(text).text or aspects [] terms sentence.find(aspectTerms) if terms is not None: for term in terms.findall(aspectTerm): aspects.append({ target: term.get(term), from: int(term.get(from)), to: int(term.get(to)), polarity: term.get(polarity) }) categories sentence.find(aspectCategories) if categories is not None: for cat in categories.findall(aspectCategory): aspects.append({ category: cat.get(category), polarity: cat.get(polarity) }) if aspects: samples.append({text: text, aspects: aspects}) return samples这段代码的核心逻辑是遍历每个 sentence先取原文再分别处理 aspectTerms 和 aspectCategories 两个子节点。需要注意root.iter(sentence)的写法对嵌套结构更鲁棒比直接root.findall(sentence)稳——因为有些官方文件的 sentence 节点外面还包了一层 review 节点。from 和 to 是字符串记得转成 int后面做词片段截取时会用到。polarity 字段的取值只有四种positive、negative、neutral、conflictconflict 表示标注者意见不一致官方评测时一般不计入准确率计算但在训练时不能直接丢否则模型学不到这种模糊样本的表示。2.3 数据规模与类别分布餐厅和笔记本差异不小解析完两个领域的训练集做个简单统计就能发现重要差异。Restaurant 训练集约 3000 条句子标注了约 3700 个 aspect term 和 4700 个 aspect categoryLaptop 训练集约 3000 条句子但 aspect term 数量只有约 2300aspect category 约 2500。什么意思呢餐厅评论里一个句子往往同时评价氛围、价格、食物多个维度而笔记本评论更聚焦于单一部件。这直接影响你做实验时的观察视角Restaurant 上多任务学习比单任务涨点明显因为多个 aspect 共享同一个句子上下文Laptop 上则不一定aspect 密度低了共享上下文带来的收益会被噪声抵消。再说 polarity 分布。两个领域的 positive 占比都超过一半negative 次之neutral 最少conflict 大约只有几十条。这种天然不平衡意味着你用 accuracy 做评估无脑预测 positive 也能拿到一个不低的下限。这就是为什么我后面在验证模型效果的时候强制要求代码里同时输出 macro-F1 和类别各自的 F1而不是只看总准确率——先看下限在哪不然你以为自己模型在涨实际是样本比例在起作用。3. 四个子任务怎么选从 Aspect Term Extraction 到 Polarity 分类3.1 子任务 1 和 3做抽取出身还是做分类出身SemEval-2014 Task 4 官方把任务拆成了四个子任务很多初学者容易看岔。子任务 1 是 Aspect Term ExtractionATE输入是句子输出是句子中出现过的评价对象词比如“sushi”“service”子任务 2 是 Aspect Term PolarityATP在给定评价对象词本身及其位置的前提下输出该词的情感极性子任务 3 是 Aspect Category DetectionACD不依赖显式词只给句子预测评价对象所属的预定义类别如 FOOD、SERVICE、PRICE子任务 4 是 Aspect Category PolarityACP在给定类别的前提下判断情感倾向。选择哪个子任务作为切入点取决于你的业务场景。如果你的线上系统已经有商家维度的结构化数据知道用户在评论哪个商品那做 ATP 就够了输入是商品名加评论句子输出情感倾向如果你想都是从非结构化文本里先找出大家在聊什么维度那就得做 ATE ATP 串联或者 ACD ACP 串联。两条路线的模型架构差异很大ATE/ACD 是标注或分类问题输出是词元序列或离散类别ATP/ACP 是给定 aspect 的条件情感分类。我实测下来的经验是先做 ATE再做 ATP 的流水线式方案工程上更可控因为 ATE 的结果可以直接可视化错了也好定位——你给业务方展示的时候先证明“找对了对象”再证明“判对了情绪”说服力比端到端黑匣子强得多。3.2 数据读取和格式转换把 XML 变成模型能吃的训练样本不管选哪个子任务第一步都是把原始 XML 转成统一的训练样本结构。这里有一个细节容易被忽略aspectTerm 的 from/to 是字符偏移但对中文或空格分词的语言来说这个偏移不能直接切词。还好该数据集是英文切词器用 spaCy 或 BERT tokenizer 时偏移对齐要格外小心。通常做法是先把 sentence text 按字符偏移切出目标词再连同整句一起交给 tokenizer 处理然后在 token 级重新对齐目标位置。from transformers import AutoTokenizer tokenizer AutoTokenizer.from_pretrained(bert-base-uncased) def prepare_for_ate(sample, tokenizer, max_len128): text sample[text] enc tokenizer(text, max_lengthmax_len, truncationTrue, return_offsets_mappingTrue) labels [-100] * len(enc[input_ids]) # -100 是 PyTorch CrossEntropyLoss 的默认忽略索引 for asp in sample[aspects]: if from in asp: start_char asp[from] end_char asp[to] for idx, (start, end) in enumerate(enc[offset_mapping]): if start start_char and end end_char and end start: labels[idx] 1 # 标记为 B-Aspect return enc[input_ids], enc[attention_mask], labels代码里有几个关键点需要展开。return_offsets_mappingTrue让 tokenizer 返回每个 token 在原文中的字符级位置这是把偏移量从字符空间转换到 token 空间的标准手段。-100不是随便选的它正好是 PyTorch 里CrossEntropyLoss默认的ignore_index可以在计算损失的时候自动跳过 padding 和特殊 token这部分不参与反向传播梯度不会受影响。循环里判断的是 token 的字符区间是否完全落在 aspect 的字符区间内这种包含式匹配比“起始位置相等才算命中”要鲁棒因为 BERT tokenizer 会把一个词拆成多个子词只有子词整体都落在目标范围内才能全标上。顺手提醒一句aspectTerms 的 to 是排除末尾的也就是text[from:to]才是目标词切片时要小心别把它当成闭区间给多切一个字符。对要做 ATE 的人这个转换就已经够了接下来套一个 BiLSTM-CRF 或 BertForTokenClassification 就能训。对要做 ATP/ACP 的人则要换个格式输入是句子加一个查询aspect term 或 category输出是情感极性标签。这种建模方式更接近 natural language inference把 aspect 串成句子让模型判断两个句子之间的蕴含/矛盾关系从而推断情感。实际操作我一般用[CLS] text [SEP] aspect [SEP]作为输入从[CLS]的池化输出接一个三分类头。3.3 分别在四个子任务上的评估视角别把 Task 2 当成 Task 1我在给别人讲这份数据的时候最常纠正的一个错误就是把子任务理解混。ATE 的评测指标是标注级别的精确率、召回率和 F1只有预测出的字符边界和 ground truth 完全一致才算命中ACD 的评测是类别级别的 macro-F1ATP/ACP 则算分类准确率。之所以不推荐把这四个任务的指标混在一个表里比较是因为它们的难度基准完全不同。ATE 要求你同时解决边界识别和词性判断precision 很难刷上 80ACP 是给定 aspect 条件做三分类随机揣测 baseline 都能到 50 多。有了这四个任务各自独立的结果你组一个流水线系统就顺手了先 ATE 找出所有评论对象再对每个对象做 ATP 判断情感然后如果想按类别聚合就再做一次 ACD把提取出的对象映射到四五个预定义的类别上得到每个类别的整体情感倾向。这条链路在真实的餐饮评论分析系统里验证过能直接支撑“好评率最高的菜系是什么”“服务差评集中在哪些时段”这类业务问题的回答。4. 把 XML 转成 BERT 输入预处理流程、长度截断与类别映射4.1 数据清洗和类别映射的规范化处理跑 BERT 之前数据往往要先过一道清洗。这份数据集里有一种特有现象同一个句子会同时有 aspectTerm 和 aspectCategory 标注但是 order 是错的比如 aspectTerm 是“price”aspectCategory 里又给了一个“PRICE”两者情感极性是一致的。这时候要做的是去重和合并不然训练样本里会出现同一条 aspect 被当成两个样本间接增加正样本的占比影响模型学到的类别先验。另一种情况是 aspectTerm 自带的大小写不统一比如 “Sushi” 和 “sushi”标记的时候先统一成小写再做匹配可以少很多麻烦。类别标签也建议统一编码成整数。polarity 有四个取值分别对应两分类、三分类还是四分类完全看你的下游需求。做三分类任务的时候conflict 样本可以直接丢掉也可以像官方一样在评测时忽略。我个人的做法是保留 conflict 但单独算一个标签到了统计时会额外输出一个四分类的混淆矩阵这样能直观看到模型把冲突样本误判到哪边多一些——通常会被推去 neutral因为 neutral 和 conflict 在语义上本来就分界模糊。POLARITY_MAP {positive: 0, neutral: 1, negative: 2, conflict: 3} def convert_to_cls_format(sample, aspect, tokenizer, max_len128): text sample[text] if target in aspect: aspect_text aspect[target] elif category in aspect: aspect_text aspect[category].replace(_, ) enc tokenizer( texttext, text_pairaspect_text, max_lengthmax_len, truncationTrue, paddingmax_length, return_tensorspt) label POLARITY_MAP[aspect[polarity]] return enc[input_ids], enc[attention_mask], enc[token_type_ids], label这里用text_pair把 aspect 文本拼到第二个句子位置tokenizer 会自动用[SEP]分隔开这样极端情况下 aspect 内容越长模型也能在第二段里得到完整上下文。对含target的 term 做输入构造时第一个token在第二句的位置对齐是第一坑——因为token_type_ids已经区分了句子 A 和句子 B模型分得清你输入里哪部分是原评论哪部分是查询对象。paddingmax_length直接把训练批量固定成定长张量不用再手动做动态 paddingDataLoader 里拿到的 batch 大小恒定省不少调试时间。4.2 长句子截断策略截原文不截查询词该数据集里不少原句子超过 128 token尤其餐厅评论里用户经常一口气写一大段。BERT 类模型对max_length的处理方式是直接截断传统做法是保留前 128 个 token但如果最后 50 个 token 里藏着一个 aspect 词模型就永远看不到那个评价对象预测结果自然变成瞎猜。更稳妥的策略是“保留开头 截断中间”把句子第一个 token 和各 aspect 词附近的内容保留下中间部分做截断或直接用滑窗覆盖。在实际数据处理脚本里我一般不重写tokenizer的截断逻辑而是先判断 aspect 目标词的字符偏移位置和整句长度如果 aspect 词落在尾部、快要被截断就调整句子切分窗口让 aspect 词处于保留区间内。简单实现是左移 windowdef adaptive_truncate(text, aspect_start, max_len128): tokens tokenizer.tokenize(text) if len(tokens) max_len: return text # aspect_start 是字符偏移粗略按比例换算成 token 位置 approx_pos int(aspect_start / len(text) * len(tokens)) if approx_pos max_len - 20: return tokenizer.convert_tokens_to_string(tokens[:max_len]) start max(0, min(approx_pos - 60, len(tokens) - max_len)) return tokenizer.convert_tokens_to_string(tokens[start:start max_len])这个函数是简易版的自适应截断核心思想是确保目标 aspect 的上下文窗口不会在截断时被丢弃。approx_pos用字符偏移除以文本长度再映射到 token 位置误差在英文数据集上大概几个 token 以内够用了。截断后的文本重新传回tokenizer时会重新切分偏移信息只用来决定窗口位置不直接参与最终 token 对齐所以误差不会放大。这种写法比粗暴的truncationTrue右截断更适合标注数据——因为 aspect 词往往不是一个句子的开头而是夹在中间或靠后的位置。4.3 训练时的 Batch 构造与随机采样顺序很多人在这个数据集上直接拿 DataLoader 默认顺序跑不 shuffle训练进度曲线一路飘损失忽高忽低。原因在于原始 XML 文件里句子是一条条连续排的同一个领域的相近句子主题高度相似不 shuffle 会导致每个 batch 里的样本高度同质。稳妥做法是训练集和验证集都设shuffleTrue测试集设为False保持顺序一致。另外一个小 trick 是把 polarity 和 aspect term 的标签拼在一起做多任务用共享的 BERT encoder 同时输出 term 的位置和极性这种做法在 Restaurant 领域尤其有用因为同一句子里的多个 aspect 语义互相关联。5. 常见问题与避坑记录五个亲测踩过的坑5.1 从偏移量到 token 的映射凭空多了几个字符现象用 BERT 的offset_mapping结合 from/to 做 ATE 标注训练出来的模型在测试集上 F1 偏低检查训练数据时发现少量样本的标签位置错了一位。原因原始 XML 中text里有 “” 这类 XML 转义符ElementTree 解析时自动还原成 “”原文字符长度发生偏移原先的 from/to 是按原始字符串计算的位置对不上。解决解析 XML 时不要用解析后的find(text).text直接用要拿到原始未经解析的文本再算。代码上先raw re.findall(rtext(.*?)/text, xml_string, re.S)手动把 XML 转义符还原成目标字符再计算偏移量。从那以后我每个数据集都先做一次偏移量随机校验抽几条人工比对不通过不训练。5.2 训练集和测试集的 domain 版本不一致现象Restaurant 训练集跑出 F1 有 78到测试集只剩 66差一大截。原因官方把 Restaurant 测试集里一半样本换成了 OOD 句子——标注的 aspect category 在训练集中从未出现过比如训练集只有 FOOD、SERVICE、PRICE测试集却突然冒出来 AMBIENCE 相关的句子。模型没见过的类别当然预测不出来。解决训练之前先检查训练集和测试集里 aspectCategory 标签的集合用集合差集找出来回出现的新类别提前想好这些样本是直接丢掉还是做特殊的 label smoothing。5.3 conflict 样本被无脑丢掉导致预测偏好漂移现象丢弃了所有 conflict 样本之后模型对不确定的句子强制归到 neutral导致 neutral 的 F1 上升positive 的 F1 下降。原因conflict 和 neutral 在语义上其实有重叠模型本来可以用它们之间的模糊地带做更好的边界区分。直接删掉相当于把中立样本的判别边界交给噪声。解决保留 conflict但训练时把它单独归一个类计算指标时再忽略。如果框架不方便改标签集合可以用 one-hot 三分类加上一个权重乘子让模型知道冲突样本的损失权重低一些。5.4 中文评论误用英文数据集现象有同学拿这个数据集的标注规范直接套中文评论数据结果 from/to 偏移在中文上完全对不上。原因英文的字符偏移对中文不成立中文分词粒度不同BERT 的 tokenizer 行为也不一样。偏移量必须按 token 级重新映射不能复用英文数据集的预处理。解决中文 ABSA 任务建议先做字级别的 offset 映射再用BertTokenizerFast的return_offsets_mapping拿到中文每个 token 覆盖的字符位置流程参考第 4.2 节但要注意一个中文汉字在 tokenizer 里可能被拆成“字 标点符号”等多 token。5.5 评测时用错标签集合导致正确率偏高现象subtask 2 的测试集准确率跑到 91但看混淆矩阵发现 negative 一类全是 0只剩 positive 和 neutral 在起作用准确率虚高。原因有些框架里label 的 id 没有和POLARITY_MAP对齐模型把所有样本预测到同一个类上了。准确率这种宏观指标对类别不平衡不敏感虚高很容易骗人。解决强制每个实验跑完输出 macro-F1 和每类 recall不看单一 accuracy。如果某类 recall 为 0说明模型塌了这个实验无效。6. 端到端验证写一个官方评估脚本跑通任务二并自查数据质量这个数据集最适合的“第一次完整训练”路径我建议直接从子任务 2ATP开始因为输入输出都好建模且指标含义直观。验证方式就是重写官方评测逻辑用训练好的模型对测试集每条 sentence 的每个 aspectTerm 做预测得到 polarity 后和真值对比输出分类准确率和每个类别的 recall。为了让验证不流于表面我会额外做一次数据还原——从模型的输出 JSON 中随机抽 20 条句子把原文、预测的 polarity 和 ground truth 并排打到控制台上眼睛确认一遍。python evaluate_atp.py --test_data data/Restaurant/Test.xml \ --checkpoint output/best_model.bin \ --output predictions.jsonevaluate_atp.py的内部逻辑不算复杂对测试集每一条句子跑一次 batch 推理将预测结果按[CLS]的 logits 取 argmax 映射到标签名然后和 JSON 里的真值比对。需要额外注意的一点是要把预测为正类但 target 词不在原句中出现的样本记录下来——这类样本是模型猜测出来的不是真实识别出来的在盘点上要单独过滤掉不然后续分析会被高估的准确率带偏。跑通以后我做的最后一件事是数据自检把 aspectTerms 和 aspectCategories 的预测结果交叉比对看看两个任务在同一个句子上有没有产生矛盾。比如某条句子预测 term “sushi” 是 positive但 category 预测结果为 “FOOD#QUALITY negative”这说明模型的 term 级和 category 级是两套独立的判断逻辑没有共享中间表达。在这个数据集上想拿到一致的端到端结果只做单任务是不够的必须多任务联合训练。从那以后我每次拿到新的标注数据集都强制走一遍“统计标签分布、抽样检查偏移量、跑官方评估脚本”这三个动作起来之后写代码也顺手很多也不用担心在评测集上被数据陷阱坑出无效结论。希望这份 SemEval-2014 Task 4 数据集的拆解对你有帮助。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑