☰
赛题数据预处理与baseline搭建:从压缩包到可提交结果的关键流程
2026/10/17 14:42:35 网站建设 项目流程

简介:这是2021年安徽省大数据与人工智能应用竞赛人工智能网络赛本科组的完整赛题数据包,面向参加同类竞赛的高校本科生、备赛团队及人工智能课程实训者。数据围绕两项任务:一是基于1000张人脸图片及其年龄标签完成年龄估计;二是利用电梯情况、楼层、户型、区域、装修情况、面积、建筑时间等房源信息预测房屋价格,并提供train、val、test三个CSV文件,划分比例约17000:3000:3000,便于直接开展训练、验证与测试。压缩包共1009个文件,以jpg图像和csv表格为主体,另含3个Python脚本和2个txt说明文件,整体约13.47MB,结构清晰、上手成本低。目前已有1068人学习使用。下载后即可获得规范划分的赛题数据、基础脚本和字段说明,既能复现竞赛流程,也适合练习特征工程、模型调参与算法对比,可作为课程项目或毕业设计的可靠数据支撑。

1. 赛题数据不是题库:先理解它怎么决定你的排名

拿到这份2021年某省大数据与人工智能应用竞赛人工智能(网络赛)本科组赛题数据,很多人的第一反应是赶紧翻代码、找模型、赶第一版提交。但真正能让人拉开差距的,往往是打开压缩包后的头三个小时:目录结构、标签字段、脏数据,这些东西决定你后面所有训练是否白费。

赛题数据不是拿来背的题库,它是一个带约束条件的工程输入。文件命名、编码、目录层级、标签分布、缺失样本,每一项都直接影响模型的训练与最终得分。这篇笔记按我处理类似竞赛数据的一线习惯,从检查数据包开始,讲到预处理、baseline搭建和最值得注意的四个坑。新手能跟着命令一步步走,熟手也能拿这套流程对照自己的项目查漏补缺。

2. 解压后先重建目录结构:数据包里的文件,各是什么角色

2.1 解压前摸清数据包:三条命令避免解开后懵

我一般不会直接双击解压,而是先在终端里看一眼压缩包结构。训练集几十万张图片或者一堆嵌套目录时,盲解压容易把磁盘撑爆,也容易在一堆文件里找不到重点。用命令先读它,成本低得多。

ls -lh 赛题数据.zip # 看压缩包体积,预估是图片还是结构化数据 unzip -l 赛题数据.zip | head -30 # 只查看压缩包内文件清单,不解压 du -sh 赛题数据.zip # 再次确认磁盘占用 unzip -q 赛题数据.zip -d data_dir # 静默解压到指定目录

前三条命令都不改动文件,unzip -l会把包内的路径和文件名列出来,配合head -30可以快速判断数据是平铺图片、子目录分类、还是几个 CSV。du -sh看的是整体体积,如果压缩包解压后会膨胀很多,需要提前留磁盘空间。最后一条的-q表示静默,-d data_dir指定解压目标目录,我习惯让所有数据都放到单独目录里,后续写相对路径遍历时不会被其它文件干扰。解压后如果还看不清层级,再跑一句tree -L 2 data_dir看两层就够了。

2.2 赛题数据目录的常见骨架:训练集、测试集与提交样例

这类网络赛的数据包大多遵循同一套约定,哪怕具体赛题不同,核心角色也就那么几类。我遇到比较多的结构是下面这样的表格。

路径或文件角色常见注意点
train/训练样本子目录可能是类别,也可能是拍摄批次,先确认再使用
test/需要预测的样本一般没有标签,不要花时间去找测试集答案
labels.csv训练标签编码、字段名、样本顺序都可能埋雷
sample_submission.csv提交格式样例列名、行顺序、小数精度都必须严格对齐

如果赛题是图像分类,train/和test/下通常是图片文件;如果训练集按类别建了子目录,那么标签其实是隐含在路径里的,可以直接用目录名作为标签,不需要再读 CSV。如果train/是一条平铺目录、没有子目录,那就需要靠labels.csv把文件名和标签关联起来。

结构化数据的赛题则常见train.csv、test.csv和label字段,目录层级相对简单,但字段类型和缺失值更需要注意。网络赛的数据包通常只给训练标签和测试特征,这是比赛的公平边界,别指望用某些技巧翻到测试集答案。你所需要的一切验证集,都应该从训练集里自己切。

2.3 标签文件先读字段:别把列名和顺序搞反

拿到labels.csv时,我建议先只做一件事:读出来,看前几行,看列类型,看类别分布。代码量很小,但它能挡掉一大批后续问题。

import pandas as pd train = pd.read_csv("labels.csv") print(train.head(10)) # 先看字段名和前几行内容 print(train.dtypes) # 看列类型,有没有把数值读成字符串 print(train["label"].value_counts(normalize=True)) # 看类别占比,初步判断是否均衡

这三个输出回答三个问题:字段名到底叫什么、数值列有没有被当成字符串、类别样本数是否均衡到值得做特殊处理。dtypes观察object类型往往意味着数字被读成了文本,后面排序或映射标签时会出错。value_counts(normalize=True)直接显示占比,比看绝对数量更直观。

如果标签文件是 JSON 而不是 CSV,就用json.load打开再print(type(data)),先看最外层的结构是列表还是字典,再决定怎么转 DataFrame。不要一上来就把 label 转成 0、1、2,很多框架需要从字符串标签生成自己的映射表,提前转数字反而容易丢原始信息。再做一步检查:assert train["id"].is_unique,确认 id 能唯一标识样本,否则后面合并提交时会一对多,直接导致零分。

3. 预处理赛题数据:查泄漏、平衡性与图片质量

3.1 查数据泄漏:测试集与训练集出现同一张图的判定

网络赛时间紧,数据包制作过程可能不太严谨,训练集和测试集偶尔会出现完全一样的文件。这种情况一旦出现,等于测试集答案外泄,但反过来如果你误把测试集特征拿去训练,线上分数会假性偏高,真实应用时直接翻车。

先做一层最简单的 MD5 去重检查,用哈希集合比对所有图片。

import os, hashlib from pathlib import Path def file_md5(path, chunk=8192): m = hashlib.md5() with open(path, "rb") as f: while data := f.read(chunk): m.update(data) return m.hexdigest() train_dir = Path("train") test_dir = Path("test") exts = {".jpg", ".jpeg", ".png", ".bmp"} train_hashes = {file_md5(p) for p in train_dir.rglob("*") if p.suffix.lower() in exts} dups = [p for p in test_dir.rglob("*") if p.suffix.lower() in exts and file_md5(p) in train_hashes] print("测试集与训练集重复文件数:", len(dups))

chunk=8192表示每次读 8KB,避免一次性载入大图把内存打满;rglob("*")会递归遍历所有子目录,也就是不关心数据是按类别还是按批次组织的。如果重复文件数量不是 0,先别急着删除,把那些重复路径打印出来,核对它们是否同时出现在两个集合里。MD5 只对完全相同的字节有效,如果同一张图被重新压缩过,MD5 会不同,这时需要借助感知哈希或图像尺寸对比做二次确认。竞赛数据包里最常见的还是原样复制,MD5 已经足够报警。

3.2 统计类别分布:决定采样策略前先量化不均衡

图像分类赛题经常出现类别不均衡,少数类可能只有几十张,多数类却有上万张。如果不量化,靠肉眼根本看不出来。用 Counter 把各类别数量和占比列出来,数据说话。

from collections import Counter counter = Counter(train["label"]) total = sum(counter.values()) for cls, cnt in sorted(counter.items(), key=lambda x: -x[1]): print(f"{cls}: {cnt} ({cnt/total:.2%})") minority = min(counter.values()) majority = max(counter.values()) print(f"max/min: {majority / minority:.1f}x")

当最大类别和最小类别相差 10 倍以上,就要认真考虑采样策略了。但决策不能只看比例,还要看比赛用的是什么评估指标。如果线上指标是 accuracy,训练集本身是从真实分布里抽出来的,强行过采样少数类未必能涨点;如果指标是 macro F1 或 balanced accuracy,那么少数类权重会被放大,重采样或加权损失基本是必须的。

常见做法是先用类别权重给小类别更多梯度,比如sklearn.utils.class_weight.compute_class_weight或 PyTorch 的CrossEntropyLoss(weight=...)。不要一看到不均衡就盲目欠采样,欠采样会让数据量骤减,小模型根本学不够。先把分布打印出来,和评估指标一起决定策略,这一步能省下很多试错时间。

3.3 统一分辨率并检查损坏图片:图像赛题最容易翻车的地方

图像赛题里,模型输入尺寸是固定的,而赛题数据里的图片尺寸可能五花八门。最常见翻车方式是直接resize拉伸,导致长宽比变形;另一种是某张损坏图片在训练中途抛异常,整个 epoch 中断。

我习惯先做中心裁剪再缩放,同时把图片统一转成 RGB。

from PIL import Image, ImageFile import numpy as np ImageFile.LOAD_TRUNCATED_IMAGES = True # 允许读取部分截断图片,但别依赖它 def load_centered(path, size=(224, 224)): img = Image.open(path).convert("RGB") w, h = img.size side = min(w, h) left = (w - side) // 2 top = (h - side) // 2 img = img.crop((left, top, left + side, top + side)) img = img.resize(size, Image.Resampling.BILINEAR) return np.asarray(img)

convert("RGB")会把 RGBA 四通道图转成三通道,避免网络输入维度不一致。中心裁剪取的是宽高中较小的一边,裁掉多余的长边,再缩放到目标尺寸,比直接拉伸更接近主流预训练模型的输入习惯。Image.Resampling.BILINEAR是较新版本 Pillow 的写法,旧版本写Image.BILINEAR即可。ImageFile.LOAD_TRUNCATED_IMAGES = True只对部分截断图片有效,真正损坏的图片照样抛异常,所以还是要做一轮损坏检查。

bad = [] for p in Path("train").rglob("*"): if p.suffix.lower() not in {".jpg", ".jpeg", ".png", ".bmp"}: continue try: with Image.open(p) as im: im.load() except Exception: bad.append(p) print("损坏图片数量:", len(bad))

im.load()会真正解码像素数据,只open不load是看不出损坏的。如果bad列表不为空,先把这些路径从训练列表里剔除,或者用一张纯色占位图替换,不要在训练循环里用异常处理去兜底,那样排查起来很麻烦。归一化用的均值和标准差,可以从这个赛题数据里自己统计,也可以直接用预训练模型的推荐值,但验证集不要叠加随机增强,这是另一个容易被忽略的细节。

4. 从赛题数据到第一个可提交结果:最小baseline跑通

4.1 用分层抽样切出验证集:调参前先建立一个可靠标尺

赛题数据只给一份训练集,测试集没有标签,所以必须自己切验证集。如果样本类别不均衡,随机切分会造成某些小类别在验证集里缺失,于是验证指标忽高忽低,完全不可信。用train_test_split加stratify可以按类别比例抽样。

from sklearn.model_selection import train_test_split train_df, val_df = train_test_split( train, test_size=0.2, stratify=train["label"], random_state=42 )

test_size=0.2表示留出 20% 做验证集,stratify=train["label"]表示按标签类别分层,让训练集和验证集每一类的占比都大致相同。random_state=42固定随机种子,保证每次重跑划分结果一致。如果赛题数据带有拍摄地点、时间戳或批次字段,就不能只做随机划分。同一批次或同一来源的图片容易有背景相近、光照相似的问题,随机划分会让模型在验证集里偷看到训练集的环境特征,线上分数就会和本地差距很大。这时用GroupKFold按来源分组划分更稳妥。

4.2 最小训练线路:先用轻量模型打通数据流

我一般不会一上来就训练几千轮的 ResNet。先用一个能跑通数据读取、模型前向、loss 反传的最小脚本,确认整条链路没有问题,再换大模型。这个习惯帮我挡过很多次“代码看着没错,训练一启动就崩”的窘境。

下面是一个最简单的 PyTorch Dataset 和 DataLoader 写法。

import os import torch import numpy as np import torch.nn as nn from torch.utils.data import Dataset, DataLoader from PIL import Image class TrainDS(Dataset): def __init__(self, df, root, size=(224, 224), label_map=None): self.df = df.reset_index(drop=True) self.root = root self.size = size self.label_map = label_map or { lbl: i for i, lbl in enumerate(sorted(df["label"].unique())) } def __len__(self): return len(self.df) def __getitem__(self, idx): row = self.df.iloc[idx] path = os.path.join(self.root, row["path"]) img = Image.open(path).convert("RGB").resize(self.size) img = torch.tensor(np.asarray(img)).permute(2, 0, 1).float() / 255.0 label = torch.tensor(self.label_map[row["label"]]) return img, label train_loader = DataLoader(TrainDS(train_df, "data"), batch_size=32, shuffle=True, num_workers=2)

__getitem__返回的是经过缩放的图片张量和整数标签。permute(2, 0, 1)把 Pillow 读出的 HWC 顺序转成 PyTorch 要求的 CHW 顺序。/ 255.0将像素值归一到 0 到 1 区间。batch_size=32表示每次取 32 张图,shuffle=True只在训练时使用,验证集 DataLoader 必须设置shuffle=False,否则指标会因为顺序不同而无意义。num_workers=2用两个子进程加载图片,如果环境是 Windows,num_workers经常触发多进程错误,直接改成num_workers=0就能避免。这个 Dataset 只做数据流转,不要用它判断模型上限,先确认 loss 能稳定下降再说。

如果不想引入深度学习框架,也可以退一步,把图片缩放成 64×64 后拉成一维向量,直接喂 sklearn 的逻辑回归,同样能验证“标签对得上、路径读得到、提交格式没问题”。这套最小链路跑通后,你已经胜过了不少还在跟路径斗智斗勇的人。

4.3 按样例文件拼提交:列名、顺序和概率形式都要对齐

提交文件是最容易被低估的一环。模型再强,提交格式错位就是零分。我一般不会自己从头构造提交表,而是读取官方给的sample_submission.csv,保持它的列名和行顺序。

sample = pd.read_csv("sample_submission.csv") score_dict = dict(zip(val_ids, val_scores)) sub = pd.DataFrame({ "id": sample["id"].values, "score": [score_dict[x] for x in sample["id"].values] }) assert list(sub.columns) == list(sample.columns) sub.to_csv("submission.csv", index=False)

dict(zip(val_ids, val_scores))把每个 id 对应的预测值做成映射表,再用官方样例的 id 顺序逐行取值,这样即使模型输出的顺序和官方不一致,也能自动对齐。assert list(sub.columns) == list(sample.columns)确保列名完全一致,多一列或少一列都会直接失败。index=False避免写出无意义的行号列。如果比赛要求输出的是每个类别的概率,而不是单个分值,先确认样例里是label字段还是多个概率列;如果模型输出的是 logits,记得先做 softmax 再提交,很多全零分就是这么来的。

5. 赛题数据使用中的四个避坑记录

5.1 中文标签读出来乱码

现象:打印labels.csv时中文字段变成乱码,标签合并时所有类别都对不上。

原因:数据文件是从 Excel 导出的,常用 GBK 或 GB18030 编码,而pandas.read_csv默认按 UTF-8 解析,一旦遇到不认识的字节就直接变乱码。

解决:先用二进制读取,再逐个编码尝试,而不是靠猜。

raw = open("labels.csv", "rb").read() for enc in ["utf-8", "gbk", "gb18030", "latin1"]: try: train = pd.read_csv("labels.csv", encoding=enc) print("成功编码:", enc) break except UnicodeDecodeError: continue

utf-8和gbk是两种最可能的编码,gb18030是 GBK 的超集,能兼容更多生僻字。latin1几乎不会报错,但它不是正确解,只是兜底方案。乱码问题不解决,后续标签映射、分组统计全会受影响,这是数据处理里的头号拦路虎。

5.2 路径对不上打不开图片

现象:代码报FileNotFoundError,但手动打开文件明明存在。

原因:压缩包内部有嵌套目录,或者项目在 Windows 上生成反斜杠路径,而 Linux 环境只认正斜杠;也有可能文件名大小写不一致。

解决:统一用pathlib.Path和相对路径管理,不硬编码绝对路径。

from pathlib import Path root = Path("data") for p in root.rglob("*"): rel = p.relative_to(root) print(rel.as_posix())

rglob("*")递归遍历所有子目录,relative_to(root)把绝对路径转换成相对于根目录的路径,as_posix()把所有分隔符统一成正斜杠。这样在本地跑完,压缩打包到服务器或另一台机器上,只要保持root不变,路径就不会失效。我在这个坑上吃过血泪亏,最冤枉的不是模型不行,而是训练到一半才发现一个子目录没被扫描到。

5.3 提交即零分

现象:训练正常,验证集指标不错,提交后线上分数为 0。

原因:先检查文件格式。最常见的是输出行顺序和官方样例不一致,或者列名被改掉;其次是把多分类 logits 直接当概率提交,没有做 softmax。

解决:读取官方样例的行顺序,按 id 映射预测结果,并在保存前做一致性断言。

sample = pd.read_csv("sample_submission.csv") pred = pd.DataFrame({ "id": sample["id"].values, "score": pred_by_id[sample["id"].values] }) assert pred.shape[0] == sample.shape[0] assert list(pred.columns) == list(sample.columns) assert pred["score"].notna().all()

assert pred.shape[0] == sample.shape[0]保证行数一致,notna().all()保证没有缺失值。如果要求的是概率,再检查输出范围是否在 0 到 1 之间。零分问题 90% 是格式问题,剩下 10% 才是模型问题,先按这个顺序排查。

5.4 本地验证集和线上成绩脱节

现象:本地验证集准确率 0.95,线上成绩 0.55,差距大到离谱。

原因:随机划分的验证集和官方测试集分布不一致。赛题数据里如果存在同一来源的批次,随机划分会让验证集和训练集共享相似样本,模型本质上是靠记忆背景或拍摄参数做题,换到全新测试集就露馅。

解决:用分组交叉验证代替随机划分。

from sklearn.model_selection import GroupKFold gkf = GroupKFold(n_splits=5) for fold, (train_idx, val_idx) in enumerate(gkf.split(X, y, groups=sample_group)): print(f"fold {fold}: train {len(train_idx)}, val {len(val_idx)}")

GroupKFold的groups参数传入样本的分组标识,可以是批次 ID、拍摄时间或来源字段。它保证同一组的所有样本不会同时出现在训练集和验证集,这样验证结果才更接近线上。如果赛题数据没有明确的组字段,也可以从文件名前缀或目录名推断,多花这一步,比事后怀疑“玄学”有效得多。

6. 调模型前,先做两个动作:过拟合小样本和提交自检

6.1 用 64 张图过拟合,验证数据链路不是玄学

我拿到赛题数据并切好验证集之后,不会直接开训完整模型,而是先取 64 张训练样本,把模型硬跑过拟合。如果模型在这么小的样本上都没办法把训练 loss 压下去,大概率不是网络容量不够,而是标签错位、增强太强、或者 Dataset 返回了错误的数据。

mini_df = train_df.sample(64, random_state=42) mini_loader = DataLoader(TrainDS(mini_df, "data", label_map=label_map), batch_size=8, shuffle=True, num_workers=0)

label_map必须和完整训练时保持一致,否则后面切回完整训练会重新生成映射,导致前几轮标签错乱。小样本过拟合跑通后,再换三个不同的random_state切验证集,对比指标标准差。如果标准差极大,说明验证集量太少或分布不稳定,这时不要急着调模型超参数,先调整划分策略。这个习惯看似多花半小时,实则能省掉好几个下午的无效调参。

6.2 提交前做一次空跑自检

线上提交次数通常有限,不应该拿正式提交去试错。我会在每次提交前,把要上传的文件用代码做一轮极值检查。

sub = pd.read_csv("submission.csv") sample = pd.read_csv("sample_submission.csv") assert sub.shape == sample.shape assert list(sub.columns) == list(sample.columns) assert sub["score"].notna().all() assert sub["score"].between(0, 1).all()

如果输出的是概率,between(0, 1)能拦截 logits 直接提交的错误;如果输出的是类别标签,这个断言就要改成检查是否属于合法类别集合。每次修改模型后都要重新跑一遍,别嫌麻烦。格式正确是上线的基础,很多翻车不是赛题数据不行,而是倒在最后一步。

我自己的习惯是,拿到任何一份赛题数据,先花半天时间把目录、编码、标签分布和划分逻辑全部确认清楚,再开始训练。以前我也图快直接开训,结果整个下午都在排查路径和乱码,提交分数一直是零。后来老老实实走完上面这套流程,反而跑得更顺,模型选型反而成了最不费脑的一部分。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询