资讯动态

SemEval情感分析数据集解析与Baseline构建实战

发布时间:2026/10/13 7:30:52 来源:尧图企业网站定制
简介semeval数据集.zip是一份面向自然语言处理研究与教学的标准情感分析语料包专门服务情感极性识别、属性抽取、目标识别等细粒度意见挖掘任务。包体小巧共包含14个文件其中12个为XML标注文件按训练集、测试集组织覆盖2014年任务4、2015年任务14和2016年任务12三届赛题设定另有2个PDF为标注指南说明任务规则与标注规范。XML文件以结构化方式记录评论文本、讨论目标、属性词及情感极性层次清晰配合指南可快速理解字段含义。资源覆盖笔记本电脑与餐厅两大真实领域既有单目标情感判断样本也有跨域迁移和多层面情感强度标注适合作为基准数据集用来开发或验证分类模型。压缩包整体约1.76MB解析负担轻已有766人学习对于刚进入情感分析领域的学习者可配合指南理解XML结构并用于模型训练对于进阶研究者则可用于评估不同模型在跨域、多层面情感分析上的表现推动算法迭代。1. SemEval数据集为什么让人又爱又恨下载易、解析难、跑通更看运气任何一个做过文本情感分析的人第一次打开 SemEval 压缩包的时候心里多半都会咯噔一下。这份数据不像 Kaggle 上那种整理得干干净净的 CSV里面是五花八门的 XML、TSV 和 README嵌套目录一层又一层。我见过不少同事卡在第一步压缩包解压了但不知道哪个文件是训练集哪个是测试集标签文件里混着多任务的数据冒号后面跟的是年份还是情感类别都得猜。这些 SemEval 数据集本质上托管的是历届评测任务的原始资料真实、有学术底子但使用门槛被格式问题抬得极高。这份资源能解决的问题很具体把历史任务的原始分发包变成你能直接拿来做实验的训练语料。它适合要做情感分析、文本分类、多语言评测的研究生和企业算法工程师也适合用公开数据集给模型做 baseline 验证的人。先说结论别急着写模型先花一晚上把它彻底拆开揉碎后面会省你一周的调试时间。2. 拆包之前先读结构从目录树看懂今年在测什么2.1 典型的SemEval压缩包长什么样目录树与文件命名规则我把这份 zip 解压之后第一件事永远是用tree或者图形界面的目录预览把整体结构看一遍。常见做法是先用一段命令把文件类型和大小盘一下避免后面在操作时对着几十个文件发懵。unzip semeval数据集.zip -d semeval_data find semeval_data -type f | head -100unzip -d的执行逻辑是把所有文件解压到单独的目录避免散落各处。find后面接的head -100负责节流只看前 100 行路径因为有些包里面套了三层子目录全打出来刷屏。大部分压缩包含有 README、data、tools 三个顶层目录其中 data 下面往往还有 train、test 和 gold 子目录。这里的命名规则比较混乱train 是带标签的test 是不带标签的gold 是官方在评测结束后发布的正确标签单看文件名经常搞混后缀为.txt的文件也不一定是训练文本反而常是标签说明。要把目录树展开到二级甚至三级看每层下面放了什么。留意有没有.py或.jar文件那些是官方评估脚本后面跑分要用的就是它。普洱这种脚本一旦缺失你自己写的评估函数很可能跟官方判定标准不一致。2.2 标签体系决定任务定义从三分类到五档情感强度标签体系是整个数据集里最容易被低估的部分。多数初入手者会把注意力放在文本上但实际建模之前必须先搞明白情感标签的粒度定义。以常见的情感分类任务为例有的年份采用positive/negative/neutral三分类有的采用-1/0/1三值回归还有的要求预测 1 到 5 的整数档位甚至区分“情感强度”和“情感极性”两个维度。下载的这份压缩包里应该有一个labels.txt或LABELS.md里面写清楚了每个标签对应的情绪含义。我的建议是把标签定义单独摘抄出来放到实验笔记里随时对照。编写一个读取标签的脚本也是一个不错的习惯像下面这段代码一样把每个任务用到的标签统计一遍import os from collections import Counter label_dir semeval_data/data tag_counter Counter() for root, dirs, files in os.walk(label_dir): for fname in files: if fname.endswith(.tsv): with open(os.path.join(root, fname), r, encodingutf-8) as f: for line in f: parts line.rstrip(\n).split(\t) if len(parts) 3: tag_counter[parts[2]] 1 for tag, cnt in tag_counter.most_common(): print(f{tag}: {cnt})上面的脚本对目录下的所有 TSV 文件做了遍历读第三列作为标签并计数。参数上要注意os.walk默认按目录递归不会漏子目录TSV 文件如果带 BOM 头utf-8编码会读成\ufeff导致标签认不出这点后面会谈到这里可以先留意。看统计结果如果某个标签占比超过 80%那么这个数据集有极度不均衡的问题如果只有一两个标签出现那说明你读错了列需要重新对字段。2.3 官方README是唯一真地图别急着写代码一份合格的 SemEval 数据包README 会把任务背景、数据字段定义、引用要求写清楚。很多人跳过这一步直接写解析代码翻车率极高。我在实践中的习惯是先打开 README 用编辑器做一次全文速读重点找几个字段。第一数据列的起始位置。TSV 或 XML 的列是什么多数是id、text、label但部分任务把id写在第二列把text写在第一列。第二情感强度的取值范围是离散整数还是连续值。第三是否有需要拼接的文件比如训练集拆分成多个分片。README 里甚至可能说明 Twitter 文本里的url标签要去掉这直接影响后面的数据清洗策略。还有一些包内含有一个官方样例输出的文件比如sample_submission.txt。这个文件告诉你“提交上去的预测结果长什么样”。格式不对跑再好的模型也白搭。提醒一句代码永远在 README 之后写这个顺序不要反。3. 把XML和TSV喂进模型解析脚本与字段对齐的三道坎3.1 用ElementTree解析XML命名空间与CDATA的坑SemEval 历史上不少任务是采用 XML 作为载体尤其是涉及观点挖掘和篇章级情感分析的时候。解析这类文件的常见做法是使用 Python 标准库的xml.etree.ElementTree只读场景下它够用也不引入额外依赖。抛开网上很多人推荐的 lxml那个虽然速度快但安装时需要编译或下载二进制包入门阶段没有必要。看一个典型的 XML 片段根节点下每个review带id和label属性内部有text子节点Reviews Review rid1 labelpositive textthis movie is fantastic !/text /Review Review rid2 labelnegative textwhat a waste of time ./text /Review /Reviews下面是我常用的解析函数import xml.etree.ElementTree as ET def parse_review_xml(xml_path): tree ET.parse(xml_path) root tree.getroot() records [] for review in root.findall(Review): rid review.get(rid) label review.get(label) text_node review.find(text) text text_node.text if text_node is not None and text_node.text else records.append({id: rid, label: label, text: text}) return records records parse_review_xml(semeval_data/data/train.xml) print(records[0])findall(Review)只会找直接子节点不会嵌套搜所以如果 XML 结构里有多层包装比如rootDocumentsReview需要先逐层find再findall。字段取用方面rid是字符串后面当索引键用没问题text_node.text遇到空节点会返回None所以做了空字符串兜底避免后面在预处理时报AttributeError。实际上会把标签嵌在节点属性值里的做法很常见需要注意 CDATA 段的内容不会被 ElementTree 转义它会原样把![CDATA[之后的内容截出来此时需要自己正则清理掉 CDATA 标记。这一节里如果不做后面特征提取时模型会看到一串奇怪的CDATA。3.2 用csv模块读TSV分隔符、引号与缺失字段TSV 相对好处理但绕开一个隐藏的坑。官方 TSV 多数没有表头直接是数据行。用 Python 内置的csv模块读最稳妥因为csv.writer在输出时自动处理引号而pandas.read_csv(sep\t)在遇到纯转义字符时会比较友好。实际场景里有的 TSV 只有三列如id text label而有的带上了额外的证据跨度字段。处理方法是每次读的时候先 dump 一行看长度然后按实际列数做 index 对齐切勿硬编码第三列是标签。import csv def load_tsv(filepath, delimiter\t): rows [] with open(filepath, r, encodingutf-8) as fh: reader csv.reader(fh, delimiterdelimiter, quotingcsv.QUOTE_NONE) for line_no, row in enumerate(reader, start1): if not row: continue rows.append(row) return rows data load_tsv(semeval_data/data/train.tsv) print(len(data), data[0])这里说明两个重要参数quotingcsv.QUOTE_NONE是让逗号不会被当成引号处理因为文本里可能包含引号字符delimiter\t指定了列分隔符。行尾若带\r在 Windows 上打开文件时要留意建议newline写入时配合使用读完以后做一个line.rstrip(\r)不然字段尾部会挂着奇怪的\r路径或 id 匹配时容易出错。列数不齐时reader不会报错只会静默填None所以需要手动检查len(row)是否一致。3.3 输出格式对齐把解析结果做成统一的DataFrameXML 和 TSV 混在一个 pack 里后面训练需要统一的数据流。我一般会在解析后合并成一个pandas.DataFrame统一列名为id / text / label并且给数据来源打上标记方便后续溯源。import pandas as pd df_xml pd.DataFrame(records) df_tsv pd.DataFrame([ {id: r[0], text: r[1], label: r[2]} for r in data if len(r) 3 ]) df_all pd.concat([df_xml, df_tsv], ignore_indexTrue) df_all[label] df_all[label].astype(str).str.strip().str.lower() print(df_all[label].value_counts())重点是最后一行的链式清理astype(str)避免某些标签被读成数值类型导致后面label 1判断失败strip()去掉首尾空格lower()统一大小写。不统一大小写会导致Positive和positive被当成两类分类任务多出一个垃圾类别准确率直接掉几个点。构建统一 DataFrame 之后再抽样看几条文本确认解析过程没有把 HTML 实体或 URL 弄碎。4. 用TF-IDF加逻辑回归跑通基线五步从原始文本到F14.1 数据清洗策略URL、emoji和转义不是噪声很多文本分类的教程一上来就甩给你一个简单的re.sub(r[^\w], , text)这在 SemEval 数据上不成立。官方 Twitter 或测评文本中URL、mention、emoji 往往是带有情感倾向的线索。比如一条负面推文里带sad :(如果只保留单词表情符号消失情感信息就减弱了。我最常做的处理方式是先把 URL 替换成占位符__URL__把用户替换成__MENTION__emoji 则保留原字符但切分为 token 时注意处理。具体看这段清洗函数import re def clean_text(raw): # 统一转小写 text raw.lower() # URL 转占位符 text re.sub(rhttps?://\S|www\.\S, __url__ , text) # 用户名 转占位符 text re.sub(r\w, __mention__ , text) # HTML 实体还原 text text.replace(amp;, ).replace(lt;, ) # 压缩空格 text re.sub(r\s, , text).strip() return text参数解释https?://匹配 http 与 https\S匹配非空白把整个链接吃掉\w只匹配普通字符开头的用户名对带下划线的昵称也能覆盖HTML 实体用连续 replace 还原能做 Web 文本的简单反转义。占位符的意义在于保留“这里原本有社交元素”的信号模型能从__url__出现的频率学到隐含的转发或营销特征。这一步很多人踩过坑分词的 tokenizer 会把和’当作词的一部分导致dont和dont变成两个特征。这时可以在清洗中不做标点去除而在分词语料构建时再决定是否按标点切分。4.2 特征工程TF-IDF的ngram_range怎么设在 SemEval 情感分类这种短文本场景下词袋模型加 TF-IDF 是一个稳定可靠的起点。不夸张地说一个调好参数的 TF-IDF 加线性模型在很多历史任务上可以逼近当年的中游水平。关键参数有两个ngram_range和min_df。from sklearn.feature_extraction.text import TfidfVectorizer vectorizer TfidfVectorizer( ngram_range(1, 2), min_df3, max_df0.8, sublinear_tfTrue, token_patternr(?u)\b\w\w\b ) X_vec vectorizer.fit_transform(df_all[text_clean])ngram_range(1, 2)表示同时使用单词和相邻二元词组作为特征短文场景下二元词组对“not good”这类否定结构特别有用min_df3过滤掉在少于 3 篇文档中出现的低频词削减噪声max_df0.8剔除超过 80% 文档都出现的词比如冠词这种无区分度的词sublinear_tfTrue用1 log(tf)替代原始词频削弱高频词的主导地位。对于短文本数据集原始词频不 log 会导致the、a这种词权重过大所以这个参数基本是一开就有提升。注意token_pattern这里设置的是\w\w确保单个字符不会被当作单词这在处理表情符号时可以把:) 过滤掉它含符号字符如果要保留 emoji需要另行处理。4.3 训练脚本与评估sklearn的Pipeline和官方F1基线的核心是快速训练、稳定评估。我习惯把TfidfVectorizer和LogisticRegression打包进一个Pipeline这样交叉验证时不会出现 特征泄漏 的风险。同时注意SemEval 的官方指标多数是 macro-F1多分类时sklearn的默认averageNone不能直接拿来用需要显式指定。from sklearn.pipeline import Pipeline from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split from sklearn.metrics import f1_score, classification_report pipeline Pipeline([ (tfidf, TfidfVectorizer(ngram_range(1, 2), min_df3, sublinear_tfTrue)), (clf, LogisticRegression(max_iter1000, C1.0, solverliblinear)) ]) X_train, X_val, y_train, y_val train_test_split( df_all[text_clean], df_all[label], test_size0.2, random_state42, stratifydf_all[label] ) pipeline.fit(X_train, y_train) y_pred pipeline.predict(X_val) print(classification_report(y_val, y_pred)) print(Macro F1:, f1_score(y_val, y_pred, averagemacro))LogisticRegression的几个参数值得解释solverliblinear适合中小规模数据收敛快C1.0是正则化强度的倒数值越小正则越强防止在小数据集上过拟合先设 1.0 跑一遍作为基准再调 0.1 和 10 做对比max_iter1000保证收敛尤其在 TF-IDF 特征维度较高时默认的 100 不够。stratifydf_all[label]做的分层采样在标签不均衡时非常关键不分层会导致验证集里某些类彻底消失。整个 pipeline 的好处是交叉验证时向量器只在训练折上拟合验证折上的特征严格来自训练集的词表也就是说避免了数据泄露。5. 避坑手册五个让我翻过车的SemEval细节5.1 标签文件混在训练集里先看文件名后缀现象解压后发现train.txt与train_label.txt同时存在前者是纯文本后者是标签下意识地认为前者就是完整训练数据直接用read_csv加载结果读取后只有一列文本标签全是 NaN。原因部分官方任务把文本和标签拆成两个文件标签文件不接在文本后面。解决查看 README 里的文件说明train.txt的每一行可能是与标签文件按行号对齐的。合并时用pd.concat([df_text, df_label], axis1)不要用merge因为多数时候没有公共 id 列。从那以后我每次解压都会先做一次head -5看文件头再确认行数是否一致。5.2 三种“脏”文本转义、乱码、半角全角现象预处理之后打印文本发现quot;、amp;还挂在语料里或者中文数据出现\u2019字符最离谱的是全角冒号被当成和半角冒号不同的特征。原因官方爬取的语料带着 HTML 实体UTF-8 里还有各种 Unicode 变体。解决清洗函数里做一轮html.unescape()解码再做一次unicodedata.normalize(NFKC, text)把全角字符转成半角。中文场景建议加上繁简转换的库否则分类器会把同样的意思当成两个特征。而这里需要特别提醒的是如果只用了str.replace做几个实体替换很快会碰到漏网之鱼不如直接用标准库的html模块一步到位。5.3 验证集分布偏移别拿随机抽样的结果直接交现象本地验证集 Macro-F1 有 0.71等到提交到测试集时只剩 0.58。原因官方训练集和测试集在时间跨度或来源上不一致本地 随机抽样 的验证分布不对。解决SemEval 有些年份训练集和测试集来自不同的采集周期直接用train_test_split会导致验证集分布和测试集相差很大的情况。把数据按时间或 id 前缀排序取前面 80% 做训练、后面 20% 做验证更能模拟真实评估分布。我在处理这份压缩包时按 id 序号做了排序再切分验证分数与最终测试分数差距就缩到 2 个百分点以内。5.4 多任务文件互相污染变量作用域引发的血案现象训练好的模型预测出来的标签全是同一个值排查后发现是标签映射字典被多次任务的文件覆盖最后映射表只剩一个键值对。原因解析多个任务文件时共用一个全局 dict没有在循环里清空。解决每个任务单独写一个解析函数在函数内部定义映射字典不要做全局复用。调试时用id反查源文件看看标签映射是从哪个文件载入的。这种问题最难查因为它不是语法错误而是运行期数据污染。5.5 评估脚本的删除操作它的意见才是最终判决现象官方评估脚本运行时一直报错提示标签值不合法。原因官方要求的标签字符串跟本地 pad 的短字符串不一致比如官方要求positive你本地是pos。解决在没有看评估脚本之前永远不要自定义标签缩写。脚本里如果出现删除或调整映射的逻辑要把该逻辑完整复制到预处理阶段而不是仅做一次手工映射。我在复现时发现官方脚本对预测结果有一层过滤去掉与 gold 标签不一致的额外标签这层过滤会影响最终分数所以要提前把预测输出里的nan行删掉并保持行的顺序和官方要求一致。代码层面用dropna()处理即可。6. 把官方评估脚本当尺子三个验证进阶技巧这份资源里如果有tools或scorer目录那里的评估脚本比任何自己写的衡量标准都可靠。我的进阶流程是先用官方脚本跑通一个随机预测拿到最低分下限再用简单 baseline 跑一遍看看差距是否合理。如果 baseline 分数比随机预测还低那问题基本在前面的解析流程而不是模型太差。官方评估脚本多数是用 Python 2 写的。直接python eval.py -h可能报语法错误因为print语句没有括号。可以先跑python --version确认环境若只有 Python 3直接运行大概率报错解决办法是用 2to3 工具转换或者手动把脚本里的print改掉。这一步不算难但很多人把它当成优先级很低的事直到提交前才发现脚本跑不起来。另一个技巧是拿官方分布做参照。评估脚本会输出 per-class 的召回率与精确率把模型在这些类的分数与当年的官方 baseline 做对比。如果某个少数类别分数低基本可以判断是训练样本不足这时候我一般会做一个小规模的 EDA统计每类样本的字符长度分布看看模型是不是把长文本全部判成了多数类。最后说一个我自己的习惯。每次下载 SemEval 压缩包我都会强制走一遍固定的验证三步先跑官方评估脚本的随机 baseline再做 TF-IDF 逻辑回归最后打印 classification_report 的每一类指标。这三步跑完我对数据的信任度才有底。那些一上来就微调大型模型的人往往会在几小时之后陷入两难是你的模型问题还是数据解析问题希望这篇拆解帮你在同样的坑里少走几步。祝顺利。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑