简介:源自国际语义评测SemEval的SemEval-2014 Task 4数据集,是自然语言处理中细粒度情感分析与方面级情感分析的经典基准,面向研究者、算法工程师及高校学生,可用于训练和评估识别评论中具体方面及其情感极性的模型。压缩包共11个文件,以10份XML数据文件为主,涵盖餐厅与笔记本电脑两大领域的训练集、验证集及ABSA测试数据PhaseA/PhaseB;其中训练数据对食物、服务、价格等具体方面进行了情感极性标注,另含1份PDF版标注指南,便于理解标注规范和评测标准。目前已有1217人学习下载。该资源省去了逐条下载的麻烦,使用者可直接用于情感词典构建、情感目标抽取、情感极性判断及深度学习模型(如RNN、LSTM、BERT)的实验对比,也可作为统一基准与其他团队公平比较算法性能。 做情感分析的人,早晚都会撞上SemEval-2014 Task 4这份数据集。它是一份zip压缩包,十多年前发布的评测数据,装的是Laptop和Restaurant两个领域的评论文本和方面级情感标注。到今天,只要你想跑基于方面的情感分析(ABSA)的baseline,或者想验证一下LLM在细粒度情感识别上到底行不行,这份数据依旧是绕不开的试金石。这篇文章不聊综述,也不复述论文,就从一个实际使用者的角度,把这份zip从解压到训练的全过程拆一遍:里面到底有什么、标注是什么逻辑、解析时会踩什么坑、怎么把它变成模型能吃的格式。
1. 十年前的情感分析benchmark,为什么今天还要专门聊它
先说清楚一个很多人忽略的事实:SemEval-2014 Task 4并不是一个普通的分类数据集,它是国际语义评测大会SemEval在2014年组织的“基于方面的情感分析”(Aspect Based Sentiment Analysis,ABSA)评测任务。当年参赛队伍拿到的就是我现在说的这个zip,赛方提供的训练集和测试集都是XML格式的评论文本,覆盖两个领域:笔记本电脑评论和餐厅评论。这份数据后来成了ABSA方向引用率最高的公开基准之一,很多经典论文和开源框架都用它做验证。
1.1 四个子任务,一份数据全包含
SemEval-2014 Task 4的核心在于,它把情感分析从“整句正面/负面”这种粗粒度判断,精细到了“句子里的哪个方面、属于什么类别、表达什么情感”。具体拆成四个子任务:
- Aspect Term Extraction(AT,方面词抽取):从句子里抽出明确写出来的方面词,比如“battery life”、“the food”、“service”。
- Aspect Term Polarity(AP,方面词倾向判断):判断抽取出的每个方面词对应的情感极性,通常有positive、negative、neutral、conflict四类。
- Aspect Category Detection(AC,方面类别识别):为句子里的隐含主题分配预定义类别,比如Restaurant域里的FOOD#QUALITY、SERVICE#GENERAL。
- Aspect Category Polarity(AC上极性判断):判断每个方面类别对应的情感极性。
如果不做抽取,只做AC和AC极性判断,数据集中同样提供了预标注好的category和polarity,所以你可以把AT/AP当作一个序列标注加分类任务,也可以把AC相关当作多标签分类任务来研究。这个“一份数据四个玩法”的结构,是它这么多年还能持续产出论文的重要原因。
1.2 两个领域的差异决定了适用场景
Laptop和Restaurant虽然都来自真实用户评论,但语言风格完全不同。Laptop评论句子偏长、技术词密集,比如“the keyboard is comfortable but the screen is too reflective”;Restaurant评论更口语化,常见“the pizza was amazing, but the waiter was rude”这类转折明显的句子。这意味着同一个模型在两个领域上的表现可能差很多,所以评测时通常要分开报告结果,而不是混合计算。很多人第一次跑实验时图省事把两个域合并,最后指标虚高,审稿人一眼就能看出来。
2. 数据标注体系拆解:从句子文本到类别极性,一层层剥开
打开zip之后,训练目录下通常是Laptop_Train.xml、Laptop_Test_Gold.xml、Restaurants_Train.xml、Restaurants_Test_Gold.xml四个主文件,外加一个README。XML名字里的“Gold”表示测试集带标注,对应文件就是官方用来评测的黄金标注。理解这份XML结构,是整个预处理的核心。
<?xml version="1.0" encoding="UTF-8"?> <Reviews> <Review rid="1004293"> <sentences> <sentence id="1004293:0"> <text>I charged the battery overnight, and the battery still died quickly.</text> <aspectTerms> <aspectTerm term="battery" polarity="negative" from="7" to="14"/> <aspectTerm term="battery" polarity="negative" from="43" to="50"/> </aspectTerms> <aspectCategories> <aspectCategory category="LAPTOP#BATTERY" polarity="negative"/> </aspectCategories> </sentence> <sentence id="1004293:1"> <text>After a week the keyboard started to rattle.</text> <aspectTerms> <aspectTerm term="keyboard" polarity="negative" from="12" to="20"/> </aspectTerms> <aspectCategories> <aspectCategory category="LAPTOP#KEYBOARD" polarity="negative"/> </aspectCategories> </sentence> </sentences> </Review> </Reviews>2.1 aspectTerm和aspectCategory到底有什么区别
这是新手最容易混淆的地方。aspectTerm是句子中明确出现的词,它的from/to属性是字符偏移,指向text里的起止位置;aspectCategory是抽象主题,可能没有对应的具体词出现。比如服务员没时间理人,句子里可能完全没出现“service”这个词,但模型必须判断出属于SERVICE#GENERAL且情感是negative。所以AP任务依赖抽取结果,AC任务更像是文本分类。
从训练角度理解两者的关系:aspectTerm是一个短语级别的实体识别问题,aspectCategory是句子级的分类问题。很多进阶工作会把两者联合建模,比如先抽取term,再根据term的上下文推断category,这就是后来ASTE(Aspect Sentiment Triplet Extraction)的雏形。你如果从零搭模型,建议先分别跑通单任务,再考虑联合。
2.2 类别体系和标签分布
Restaurant域的aspectCategory是一个两段式结构,前半是实体(FOOD、SERVICE、AMBIENCE、PRICE、ANECDOTES),后半是属性(QUALITY、STYLE_OPTIONS、PRICE、MISCELLANEOUS),中间用#连接。Laptop域则直接用LAPTOP作为实体前缀,搭配BATTERY、DISPLAY、KEYBOARD、SOFTWARE等属性。两套体系的设计思路略有差异,实际处理时不要跨域套用。
| 域 | 常见类别示例 | 是否包含ANECDOTES |
|---|---|---|
| Laptop | LAPTOP#BATTERY, LAPTOP#DISPLAY, LAPTOP#KEYBOARD, LAPTOP#SOFTWARE, LAPTOP#PRICE | 否 |
| Restaurant | FOOD#QUALITY, FOOD#STYLE_OPTIONS, SERVICE#GENERAL, AMBIENCE#GENERAL, PRICE#GENERAL | 是 |
很多复现实验会忽略一个细节:Restaurant域的ANECDOTES#GENERAL类别的样本量很小,但有些论文统计时把它算进去,有些直接去掉,导致不同论文之间的类别数对不上。你写实验设置时最好明确说自己用哪个版本、去掉还是保留,否则评测结果缺少可比性。
2.3 polarity里的conflict,处理方式影响结果
四分类polarity中有个特殊标签conflict,表示一个方面同时包含正面和负面信息。比如“battery life is great, but it still drains fast”里的battery,很可能被标注为conflict。不同论文对conflict的处理极不统一:有人保留为第四类;有人直接丢弃;有人映射成negative。你会看到同一个模型在不同论文里F1差一两个点,往往就是conflict处理方式不同造成的。建议在你的代码里把conflict单独处理,并且明确写进文档。
3. 拿到zip之后:解压、字符编码、XML解析里的那些坑
这份zip下载下来通常几十MB,压得非常小,解压后也没多大。但真正折磨人的不是体积,而是解析过程中的细节。我第一次处理这份数据时,光是把XML洗干净就折腾了半天,现在把关键问题一次性说清楚。
3.1 用Python标准库解析,避免引入额外依赖
解析这份XML,不需要高大上的工具,Python自带的xml.etree.ElementTree就够用。需要注意的是,原XML里text节点的内容会有HTML实体,比如"、&、'。用ElementTree的text属性拿到的字符串已经做过实体解码,但文件里的from/to偏移仍然是基于原始文本的。在大多数情况下两者能对上,但如果句子包含被实体化的引号或尖括号,直接用字符串切片就可能错位。
import xml.etree.ElementTree as ET def parse_semeval2014(xml_path): tree = ET.parse(xml_path) root = tree.getroot() data = [] for review in root.findall("Review"): for sent in review.findall(".//sentence"): text = sent.find("text").text aspects = [] for at in sent.findall(".//aspectTerm"): aspects.append({ "term": at.attrib.get("term"), "polarity": at.attrib.get("polarity"), "from": int(at.attrib.get("from")), "to": int(at.attrib.get("to")), }) cats = [] for ac in sent.findall(".//aspectCategory"): cats.append({ "category": ac.attrib.get("category"), "polarity": ac.attrib.get("polarity"), }) data.append({"text": text, "aspectTerms": aspects, "aspectCategories": cats}) return data如果你需要准确按偏移切词,建议先对text做一次实体解码记录映射表,再计算真正的字符位置。严谨的做法是写一个自定义的offset mapping函数,把XML实体解码前后的位置对应关系存下来,这样无论文本里有几个转义符都不会切错。
3.2 常见报错和排查办法
很多人卡在解析这一步是因为简单的问题:把训练集文件和测试集Gold文件的文件名搞混;XML根节点不是<Reviews>;或者Windows下打开文件时默认编码不对。我自己遇到最多的是Python 2时代遗留的编码问题,但现在用Python 3,encoding="utf-8"基本能解决。
如果ElementTree报“parse error”,先别急着改代码,用记事本或VS Code打开XML文件看前几行,确认<?xml version="1.0" encoding="UTF-8"?>是不是完整。有些从非官方渠道下载的副本会丢失这行声明,导致解析失败。遇到这种情况,补上声明或者用ET.parse时传入容错方式都能绕过去。
3.3 建议先转成中间格式再喂模型
我现在的习惯是,先把XML解析后统一转成一份中间JSON存起来,再写数据加载器。这样后续实验只需要读JSON,不用每次启动都解析一遍XML。尤其当你做数据增强或者多组实验时,节省的时间非常可观。简单的中间格式可以设计成:
{ "text": "The screen is crisp and bright, but the speakers are weak.", "aspectTerms": [ {"term": "screen", "polarity": "positive", "from": 4, "to": 10}, {"term": "speakers", "polarity": "negative", "from": 39, "to": 47} ], "aspectCategories": [ {"category": "LAPTOP#DISPLAY", "polarity": "positive"}, {"category": "LAPTOP#SPEAKERS", "polarity": "negative"} ] }4. 从XML到模型输入:一条能直接落地的Baseline链路
把数据处理成模型输入之前,你得先明确自己在做四个子任务中的哪一个。不同子任务的标签构造方式完全不一样,下面分开说。
4.1 Aspect Term Extraction:走BIO序列标注
抽取任务通常转换成BIO标注,把句子里的每个token标记为B-term、I-term或O。这一步的关键是tokenization要对齐偏移。如果用BERT的WordPiece分词,“battery”可能拆成“battery”一个token,但“keyboard”也可能拆成“key”、“board”,所以你不能简单按空格打BIO,而是要用tokenizer返回的offset mapping,把aspectTerm的from/to位置映射到token索引上。
from transformers import AutoTokenizer tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased") def encode_for_extraction(sample, max_len=128): encoding = tokenizer(sample["text"], max_length=max_len, truncation=True, return_offsets_mapping=True, return_tensors="pt") offsets = encoding["offset_mapping"][0] labels = ["O"] * len(offsets) for aspect in sample["aspectTerms"]: start, end = aspect["from"], aspect["to"] token_start = token_end = None for idx, (s, e) in enumerate(offsets): if s == start and token_start is None: token_start = idx if e == end: token_end = idx if token_start is not None and token_end is not None: labels[token_start] = "B-term" for idx in range(token_start + 1, token_end + 1): labels[idx] = "I-term" return encoding["input_ids"][0], encoding["attention_mask"][0], labels这段代码的思路是:先拿到offset mapping,也就是每个token对应原文本的起止位置,再拿aspectTerm的偏移去匹配token边界。注意匹配的是位置而不是字符串,这样做更稳。测试下来,BERT-based的序列标注模型在两个域上的AT任务F1能到80以上,但前提是你把特殊token的位置([CLS]、[SEP])排除在loss计算之外。
4.2 Aspect Term Polarity:把aspect信息拼接进句子
极性判断的输入构造相对简单,常见做法是把方面词和句子拼起来,让模型判断这个方面在上下文里的情感。比如“screen is crisp”加上“The screen is crisp and bright, but the speakers are weak.”,模型要能捕捉到but后面带来的转折影响。标准输入格式是这样的:
def encode_for_polarity(sample, aspect_term): encoded = tokenizer( aspect_term, sample["text"], truncation="only_second", max_length=128, padding="max_length", return_tensors="pt" ) return encoded["input_ids"][0], encoded["attention_mask"][0]这里我用truncation="only_second",保证句子过长时优先保留第二段,也就是完整上下文,同时方面词不被截断。别小看这个参数,默认的截断策略可能把句尾的转折信息截掉,导致极性判断错误。我自己跑Restaurant域的实验时,仅仅改了对齐截断方式,AP任务的准确率就涨了一个百分点。
4.3 评测:用官方脚本还是自己写
官方评测用的指标是ACC(准确率)和F1,AT任务重点看Aspect Term的F1,AC任务看精确率和召回率。你可以去SemEval仓库找官方评测脚本,也可以用sklearn的classification_report自己算。需要注意官方对AT的评测是按aspectTerm整体匹配算的,也就是说如果模型抽出了“screen”而标准是“led screen”,即使部分重叠也算错误。这个严格匹配标准让抽取任务的F1普遍偏低,别一看数值没到90就觉得模型废了。
5. 这份数据还够用吗,怎么和现在的方法对接
经常有人问,SemEval-2014的数据这么老了,现在还用会不会被审稿人质疑。我的看法是,它不是万能的,但它仍然是验证ABSA方法最稳妥的starting point。
5.1 它的局限:领域少、样本量小
Laptop训练集大概三千多句,Restaurant也差不多三千多句,和动辄百万条的预训练语料比,它确实很小。这导致直接在这份数据上从头训练深度模型几乎必过拟合,所以现在主流做法都是加载预训练模型后微调,或者用prompt-based方法做few-shot。另一个局限是只有2014年的标注规范,类别体系相对粗糙,比如Laptop域没有“ANECDOTES”这种杂项类别,遇到样本外话题模型容易分错。
5.2 和后续数据集的搭配使用
后续SemEval-2015、SemEval-2016在Task 4基础上扩展了数据规模和新类别,MAMS、ACOS、ASTE等数据集也陆续出现。如果你要做学术论文,建议在SemEval-2014上做主要验证,再选一两个后续数据集测迁移能力。这样既能说明方法在经典基准上的表现,又能证明它对数据分布变化有一定鲁棒性。我见过不少工作只用这份zip跑几个表格就投出去,结果rebuttal时被问泛化性问题,非常被动。
5.3 LLM时代它反而更值钱
大模型流行之后,这份数据的作用反而被放大了。现在很多LLM评测会直接从这份zip里取几百条样本作为few-shot示例,让模型在“给定aspect和文本,输出polarity”的任务设定下做推理。因为它的标注粒度细、类别体系清晰,非常适合用来评估LLM是否真能理解某个方面在不同上下文中的情感。你可以把前文的代码稍加改造,把aspect terms、categories直接拼成prompt,送进GPT或开源模型,输出的结构化文本再解析回标签,一套LLM评测链路几分钟就能搭起来。
我自己现在做实验的习惯是:先在这份数据上跑一个BERT baseline确定合理指标范围,再用同一套预清洗流程去测LLM,最后才谈新方法。如果不先把数据链路跑通,后面所有的性能对比都是空中楼阁。
最后再分享一个排查数据加载的小技巧:解析完XML后,先打印统计信息,比如句子总数、aspectTerm总数、aspectCategory总数、polarity分布,然后和README里给的官方统计数字对一下。如果对不上,说明你的解析逻辑或者过滤条件写错了。这个习惯帮我避免过无数个“模型结果异常”但其实数据早就处理错了的尴尬局面。SemEval-2014 Task 4这份zip虽然老,但它的数据结构设计得相当扎实,你把它彻底吃透,后面接MAMS、ASTE、ACOS这些数据集都会轻松很多。
本文还有配套的精品资源,点击获取