☰
人工智能网络赛实战:从数据清洗到交叉验证的竞赛全流程指南
2026/10/12 14:37:58 网站建设 项目流程

简介:2021年安徽省大数据与人工智能应用竞赛人工智能(网络赛)本科组赛题数据包,面向备战省级AI竞赛的高校本科生与指导教师,覆盖人脸年龄预测和房价回归预测两大典型任务。压缩包共1009个文件,以1000张jpg人脸图像、4个csv表格为核心,另附3个py脚本和2个txt说明,整体仅13.47MB,便于下载与本地复现。csv数据中train/val/test按17000:3000:3000划分,房源字段含电梯、楼层、户型、区域、装修、面积、建筑时间且存在缺失,适合练习数据清洗、特征工程与模型调参;py脚本可用于快速搭建基线,txt文档说明数据格式与赛题要求。包内数据完整呈现赛题真实分布,可直接用于模型训练、方案验证和赛前冲刺,为参赛团队省去大量数据收集与整理时间。目前已有1068人学习使用,适合具备一定机器学习基础、希望通过真题实战提升能力的读者。

1. 人工智能网络赛的核心不是模型,而是那份赛题数据

人工智能竞赛里,网络赛和线下赛最明显的差别是:你拿到一份赛题数据后,需要在有限的提交次数内完成清洗、建模、预测和上传,而线上评测分数不会告诉你具体错在哪。2021年某省大数据与人工智能应用竞赛人工智能(网络赛)本科组赛题数据,就是一套典型的竞赛级数据包:训练集、测试集、提交样例和评分说明放在一起,能完整体验从读数据到提交评分的全过程。它适合三类人:准备参加下一届竞赛的学生,想找一个有真实赛题背景来练手的数据分析师,以及需要模拟赛题环境的导师。拿到这种资源,先别急着跑模型,先花一小时把数据结构、评测口径和提交格式吃透。

2. 赛题数据结构拆解:从文件清单到字段理解

2.1 网络赛的评分口径:先看清楚线上要什么

网络赛和平时自己拿数据集练习有一个本质区别:本地验证分数和线上评分经常不一致。原因不在模型,而在数据划分和评测指标。网络赛通常的做法是主办方从全量数据里划分出训练集和测试集,参赛者只能用训练集训练,对测试集做预测,然后提交预测结果。线上评测时,测试集的标签不公开,评分脚本用指定指标计算得分。

常见评测指标有准确率、F1、AUC,有时候是多个指标的加权组合。为什么要先看这个指标?因为指标决定了你在本地验证时怎么切数据。如果评测指标是宏观 F1,那么你本地验证时也要算宏观 F1,不能只看准确率。如果评测指标是 AUC,那么分类概率的排序比阈值更重要。我见过很多人在提交前才发现自己本地一直用准确率做早停,线上却算 F1,最后分数对不上,只能在最后几次提交里瞎试。

拿到赛题数据后,我的固定顺序是:先读说明文档,再列文件清单,然后看训练集字段类型,最后才写第一行代码。说明文档里通常会写清楚以下几点:目标列是哪一列,类别标签取值范围,提交文件需要的列名和行数,以及评测脚本的具体公式。这份数据如果没附单独文档,至少会有一个 README 或赛题 pdf。不要跳过这一步,它比后面任何一个调参技巧都重要。

2.2 一个数据包的典型构成:训练集、测试集、提交样例

网络赛数据包解压后,常见的文件构成如下表所示。实际竞赛里的命名可能不同,但类型基本一致。

文件类型典型文件名作用
训练集train.csv字段完整,包含目标列,用于训练和本地验证
测试集test.csv不含目标列,需要预测并提交
提交样例sample_submission.csv告诉系统提交文件的列名和行数格式
说明文档README.pdf / 赛题说明.txt评测指标、字段含义、提交规则

拿到压缩包第一步,我会在命令行里看目录结构,确认文件大小和行数预期。比如训练集有几十万行,测试集只有几万行,说明测试集可能是全量数据的一部分,数据分布可能和训练集有差异。这个差异后面会直接影响特征构造方式。

# 解压并查看数据包结构,不要直接双击打开大型CSV tar -tzf data_network_2021.tar.gz mkdir -p data && tar -xzf data_network_2021.tar.gz -C data ls -lh data/

这段命令的作用是先把压缩包内容列出来,确认有没有说明文档或额外的 schema 文件,再解压到单独目录,避免把原始文件散落在当前目录。用tar -tzf列目录而不是直接解压,是因为有些赛题数据包体积超过 1GB,直接解压可能把磁盘撑爆。-C data指定了解压目标目录,方便后续统一管理。解压之后,用ls -lh看每个文件大小,能大概判断训练集规模。

然后进入 Python 环境,用 pandas 快速读入训练集和测试集:

import pandas as pd # 读取训练集和测试集,先不做任何处理 train = pd.read_csv("data/train.csv", sep=",", encoding="utf-8") test = pd.read_csv("data/test.csv", sep=",", encoding="utf-8") # 快速检查形状、列名和缺失情况 print("train shape:", train.shape) print("test shape:", test.shape) print("train columns:", train.columns.tolist()) print("train dtypes:\n", train.dtypes) print("missing ratio:\n", train.isna().mean().sort_values(ascending=False))

这里sep=","是因为 CSV 默认逗号分隔,如果字段内容里本身包含逗号,读出来会错位,后面会讲怎么排查。encoding="utf-8"是最常见编码,遇到中文乱码可以换成gbk或gb18030。train.shape用来确认行数和列数,如果和说明文档对不上,说明分隔符或表头有问题。isna().mean()计算每一列缺失比例,按降序排列后,能一眼看出哪些列需要优先处理。

2.3 从字段里读出任务类型:分类还是回归

赛题数据的目标列决定了整个技术路线。训练集里如果目标列是整型且取值数量很少,通常是分类任务;如果目标列是连续浮点数,就是回归任务。还有一类是多标签分类,目标列包含多个标签,或者需要预测多条记录,这种情况在 NLP 赛题中出现较多。

看一眼目标列分布就能确认任务类型:

import numpy as np # 假设目标列名是 label,需要根据实际数据调整 if "label" in train.columns: print("target unique values:", train["label"].nunique()) print("target distribution:\n", train["label"].value_counts(normalize=True))

nunique()统计目标列有多少个不同取值。如果是 2,就是二分类;如果是 5 到 20,是多分类;如果取值特别多比如上千,要确认是不是连续值回归任务。value_counts(normalize=True)输出各类别占比,帮助判断类别是否不平衡。本科组网络赛常见二分类,但也会出现多分类或回归,不要假设赛题一定是二分类。

还要注意目标列的数据类型。有时候标签看起来是0和1,但 pandas 读进来可能是 float,原因是有缺失值或某个异常字符。这种情况下要把目标列先转成字符串检查,而不是直接建模。网络赛的数据一般不会太脏,但“一般不会”四个字往往是翻车的开始。

3. 把原始数据变成特征:清洗、编码与构造的落地做法

3.1 数据清洗顺序:先处理缺失和重复,再谈特征

数据清洗最怕的不是脏数据,而是没有顺序。我的习惯是固定流程:先查重复行,再处理缺失值,然后处理异常值,最后才做特征编码和构造。顺序不能反,否则去重之后缺失统计会变,导致前面白做。

先看重复行:

# 按全部列查重复行 dup_rows = train[train.duplicated(keep=False)] print("duplicated rows:", len(dup_rows)) # 按ID列查重复,防止出现同一个样本被复制多次 id_col = "id" # 实际以数据为准 if id_col in train.columns: dup_ids = train[id_col].duplicated().sum() print("duplicated IDs:", dup_ids)

duplicated()返回布尔序列,keep=False表示把所有重复行都标出来,方便查看。查找重复 ID 的原因是:有些赛题数据在拼接时可能混入重复样本,如果只按全列查可能查不出来,因为同一样本在不同批次里某些列数值会有微小差异。发现重复 ID 后,一般保留第一次出现的行,删除后面重复的,除非说明文档明确要求保留全部样本。

缺失值处理要分类型。数值型列缺失,我一般用中位数填充或直接让树模型自己处理;类别型列缺失,我习惯填一个"missing"字符串,让模型把它当成一个独立类别。填充前要先算缺失比例,如果某一列缺失超过 70%,这列基本没有信息量,该删就删:

missing_ratio = train.isna().mean() # 删除缺失比例超过70%的列 drop_cols = missing_ratio[missing_ratio > 0.7].index.tolist() print("drop columns with high missing ratio:", drop_cols) train = train.drop(columns=drop_cols)

这里missing_ratio > 0.7是一个经验阈值,不是标准答案。我自己会先看这个数据的总规模,如果行数很多,50% 缺失也可以放弃;如果数据量本身很小,缺失 50% 的列反而可能保留有价值的信息,因为剩下的一半样本可能代表一个稀有子集。不要机械地套阈值,要用行数乘以缺失比例计算实际剩余样本数量。

对于数值列,常见做法是fillna(train[col].median()),但要注意中位数计算时不能包含测试集以外的信息。严格来说,先用训练集计算填充值,再应用到训练集和测试集,顺序不能乱。

3.2 类别特征编码:高基数类别别直接 OneHot

网络赛数据里经常出现类别特征,比如城市、设备型号、来源渠道。如果类别取值数量很少,比如只有两个,用OneHotEncoder最直观;但如果类别取值超过几十个,OneHot 会制造大量稀疏列,训练变慢,还容易过拟合。

对高基数类别特征,我一般先用频次编码或标签编码。简单说,标签编码就是把类别值按训练集中出现的频率映射为整数。频次越高的类别,编码数字越大,这种做法对树模型比较友好。

from sklearn.preprocessing import LabelEncoder cat_cols = train.select_dtypes(include=["object"]).columns.tolist() print("categorical columns:", cat_cols) # 对每个类别列做标签编码,同时保证测试集也能映射 for col in cat_cols: le = LabelEncoder() # 先把训练集和测试集两类取值合并,避免测试集出现未知类别时报错 combined = pd.concat([train[col].astype(str), test[col].astype(str)]) le.fit(combined) train[col + "_label"] = le.transform(train[col].astype(str)) test[col + "_label"] = le.transform(test[col].astype(str))

这里有个关键点:LabelEncoder的fit必须同时用训练集和测试集的取值,否则测试集里如果有一个训练集没见过的类别,transform会直接报错。astype(str)是保险做法,把缺失值也变成字符串"nan",避免由于 NaN 导致的映射不一致。

如果是用 CatBoost 这类原生支持类别特征的模型,连编码都不用做,直接声明cat_features参数即可。树模型本身能处理数值型输入,对类别做标签编码后,模型在分裂时能够按编码顺序做切割,效果通常比 OneHot 好,尤其是在类别数量多的情况下。OneHot 也不是完全没用——如果后续要接线性模型或神经网络,OneHot 仍然是必要步骤。

3.3 统计特征和交叉特征:怎么构造才不过拟合

特征构造是竞赛里拉开差距的地方,但也是最容易翻车的地方。常见做法是利用分组统计:对某个 ID 列做分组,统计目标值的均值、方差、频次等,作为新特征。这类特征在用户行为赛题里特别常见,比如一个用户下过多少单、购买金额均值、时间间隔标准差等。

一个基础的分组统计特征:

# 假设有一个用户ID列 user_id,和一个数值列 amount # 构造用户维度的统计特征,并映射回原表 user_stats = train.groupby("user_id")["amount"].agg(["count", "mean", "std", "max"]).reset_index() user_stats.columns = ["user_id", "user_amount_count", "user_amount_mean", "user_amount_std", "user_amount_max"] # 训练集关联 train = train.merge(user_stats, on="user_id", how="left") # 测试集关联时,需要先用训练集生成的统计表做映射 test = test.merge(user_stats, on="user_id", how="left")

这里有几个坑。第一,std在分组只有一个样本时会变成 NaN,需要后续填充;第二,测试集中可能有些用户只在测试集出现,训练集统计表里没有这个用户,merge之后会产生 NaN,需要用fillna填补或用全局均值替代;第三,如果训练集和测试集是同一批用户,这些用户统计特征非常强,但本质上是在用全量信息做预测,如果测试集用户和训练集完全不重叠,这类特征就完全无效。

还有一类是交叉特征,比如把两个类别特征拼起来,生成一个新类别列,再用频次编码。交叉特征能够捕捉“单个特征单独看不出来、但组合后有意义”的模式。但不要盲目交叉所有类别列,否则组合数量爆炸,树模型会花大量时间分裂在无效组合上。我的习惯是先在训练集上做简单统计,看一下目标值在不同组合间的差异;差异明显才保留,否则删掉。

4. 建模与本地评分:交叉验证怎么设才能贴合线上

4.1 基准模型选择:树模型还是深度学习

网络赛时间窗口短,通常只有几天,所以我不建议一上来就用深度模型。除非赛题明确是图像分类或文本分类,否则结构化的表格数据优先用提升树模型。LightGBM 和 CatBoost 是主流选择,训练速度快,自带正则化,还能处理缺失值和类别特征。

为什么不用随机森林或逻辑回归做基线?逻辑回归对特征缩放和缺失值敏感,随机森林在高维稀疏特征下表现不如提升树,而且提升树在小数据集上也能快速收敛。深度模型在表格数据上并不总是比树模型好,这是这些年大量竞赛实践得出的结论。

LightGBM 的主要参数设置如下:

import lightgbm as lgb params = { "objective": "binary", # 二分类任务 "metric": "auc", # 线上评测指标是AUC,本地用同一指标 "learning_rate": 0.05, # 学习率,越低越稳但越慢 "num_leaves": 31, # 一棵树的叶子数,越大越容易过拟合 "min_child_samples": 20, # 叶子节点最少样本数,防止过拟合 "feature_fraction": 0.8, # 每棵树随机抽80%特征 "bagging_fraction": 0.8, # 每个样本采样比例 "bagging_freq": 1, # 每轮迭代都做bagging "lambda_l1": 0.1, "lambda_l2": 0.2, "random_state": 2021, }

这里objective要根据任务类型改,回归任务要换成"regression",多分类要换成"multiclass"并设置num_class。metric应该和线上评测指标保持一致,不要图省事只看 log loss 或 accuracy。learning_rate设置成 0.05 是比较保守的选择,配合足够的迭代轮数能够收敛得更好,代价是训练时间变长。num_leaves控制在 31 左右是默认值,如果数据量很大可以适当调大,但过大会让决策树层次变深,容易在局部样本上钻牛角尖。

4.2 交叉验证策略:分类用分层,回归用随机 K 折

交叉验证的目的是让本地验证分数尽可能接近线上分数。二分类任务里,如果训练集的类别分布是 8:2,而线上测试集的分布也是 8:2,那么随机切KFold也能用,但为了保险,我习惯用分层抽样,保证每一折训练集和验证集的类别比例和整体一致。

from sklearn.model_selection import StratifiedKFold # 5折分层交叉验证 skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) fold_scores = [] X = train.drop(columns=["label"]) y = train["label"] for fold, (train_idx, valid_idx) in enumerate(skf(X, y)): X_train, X_valid = X.iloc[train_idx], X.iloc[valid_idx] y_train, y_valid = y.iloc[train_idx], y.iloc[valid_idx] model = lgb.LGBMClassifier(**params, n_estimators=2000) model.fit( X_train, y_train, eval_set=[(X_valid, y_valid)], callbacks=[lgb.early_stopping(100), lgb.log_evaluation(200)] ) # 预测概率,按线上指标计算分数 pred_prob = model.predict_proba(X_valid)[:, 1] # 自定义函数计算AUC,或直接用sklearn from sklearn.metrics import roc_auc_score score = roc_auc_score(y_valid, pred_prob) fold_scores.append(score) print(f"fold {fold} score: {score:.5f}") print("mean score:", np.mean(fold_scores))

n_splits=5是常用折数,数据量较小可以增加到 10 折,数据量太大就减到 3 折。shuffle=True打乱样本顺序,避免原始数据按某种时间顺序排列导致同一类样本集中在一个折里。early_stopping(100)是指验证集分数连续 100 轮不提升就停止训练,防止过拟合,也节省时间。log_evaluation(200)表示每 200 轮打印一次训练日志,不用每轮都输出。

如果赛题是按时间顺序采集的数据,比如预测未来某个时间段的事件,那么随机 K 折就不合适了,要用时间序列切分:用前 70% 时间的样本训练,后 30% 验证。这一点要看说明文档里有没有暗示数据的时间属性。

4.3 最终提交策略:用全量训练集重新训练

交叉验证阶段得到了模型,但最终提交时,是继续用 5 折里最好的模型直接预测,还是合并所有数据重新训练?我一般选择后者。理由是交叉验证模型只用其中一折数据训练,信息丢失较多,而全量训练能充分利用所有样本。

但这里有个前提:全量参数和交叉验证里确定的参数保持一致,迭代轮数可以适当增加。实际操作是在交叉验证里记录验证集中达到最优分数时的迭代轮数,取 5 折的平均值,然后全量训练时用这个轮数继续训练。

# 在交叉验证中记录每一折的最佳迭代轮数 best_iters = [] for fold, (train_idx, valid_idx) in enumerate(skf(X, y)): model = lgb.LGBMClassifier(**params) model.fit( X_train, y_train, eval_set=[(X_valid, y_valid)], callbacks=[lgb.early_stopping(100, verbose=False)] ) best_iters.append(model.best_iteration_) # 取平均轮数,在全量数据上重新训练 mean_iter = int(np.mean(best_iters)) final_model = lgb.LGBMClassifier(**params, n_estimators=mean_iter) final_model.fit(X, y) # 对测试集预测并保存提交文件 test_prob = final_model.predict_proba(test)[:, 1] submission = pd.DataFrame({ "id": test["id"], "pred": test_prob }) submission.to_csv("submission.csv", index=False)

最后一步保存提交文件时,列名必须和提交样例完全一致。很多线上评分系统比对的是列名和行数,而不是列顺序。index=False是必须的,否则 pandas 会把行索引写进 CSV,多出一列,导致在线提交时解析失败。

5. 避坑指南:网络赛中数据与提交常见的五个陷阱

5.1 本地分数和线上分数对不上

现象:本地 5 折交叉验证 AUC 达到 0.88,线上公开榜只有 0.72,差距大得离谱。

原因:最常见的是评测指标不一致,比如本地看 AUC,线上算 F1;其次是数据泄漏,本地验证时不小心把测试集信息用进了训练过程;还有一个低频原因是测试集分布和训练集差异过大。

解决:先核对说明文档里的评分公式。我一般会在本地把 accuracy、F1、AUC、log loss 都算一遍,找和线上最接近的指标。如果指标一致但分数仍对不上,就要检查特征构造过程中是否用到了全量数据统计,尤其是 target encoding 类特征。这类特征在训练集上做拟合时效果很好,但泛化到未知测试集上就会崩。

提示:不要把本地验证分数当成预测线上分数的依据,它只能用来做模型比较和早停,分数绝对值不是真相。

5.2 读入 CSV 后整列数据全是 NaN

现象:用pd.read_csv读训练集,某一列明明在文本编辑器里能看到值,但 pandas 里显示全是 NaN。

原因:这一列里包含特殊字符,比如;、|,或者数据本身用;分隔。默认sep=","会遇到分隔符不匹配,导致列错位。另一种可能是文本列里有英文双引号,但没按规定转义,pandas 解析时把引号内容当成多列。

解决:先用head -n 5看原始文本:

# 查看CSV前两行,确认分隔符 head -n 2 train.csv | cat -A

cat -A会把行尾符和制表符显示出来。如果分隔符是;,读取时改成sep=";"。如果某一行里字段数量比其他行多,说明该行存在多余的逗号或引号,需要quotechar参数调整。

5.3 提交文件行数与测试集不一致

现象:提交后系统提示 “submission has 18900 rows, expected 18901”。

原因:测试集里有一行被误删。我遇到过因为dropna()默认把包含任何 NaN 的行删掉,导致测试集行数减少一行。还有一些人会把测试集做成训练集一部分去填充缺失,合并后忘记去重,导致行数变多。

解决:保存提交文件前,加一个硬性校验:

assert len(test) == len(submission), "行数不一致,禁止提交" assert set(test["id"]) == set(submission["id"]), "ID集合不一致,禁止提交"

这两行断言不花钱,但能救回一次提交次数。从那以后,我每次保存提交文件都会强制走一遍这个检查。

5.4 训练集和测试集的分布明显不同

现象:训练集目标类别比例是 7:3,但线上测试集大概率是 5:5,导致模型整体偏差,线上分数偏低。

原因:网络赛赛题有时候会把测试集从不同时间段采集,或者为了增加难度故意做了样本选择。如果说明文档里没交代,你需要自己通过特征分布判断。常见方法是计算每个数值特征在训练集和测试集中的均值和标准差,差异超过一倍就要警惕。

解决:这种情况下,靠交叉验证调参救不回来。一种做法是减少模型复杂度,降低对训练集特殊分布的拟合;另一种做法是在特征构造上尽量保留稳健的统计特征,比如排序、分位数,而不是精确数值。如果数据里有关联的时间列,让人工判断一下测试集对应的时间点是否超出训练集覆盖范围。

5.5 特征构造时引入数据泄漏

现象:本地验证分数高得离谱,比如 AUC 0.99,排序特征、交叉特征全用上了,但线上分数很低。

原因:数据泄漏往往来自两部分:一是目标编码用了未来信息,比如用全量数据的标签均值做特征;二是特征值本身包含标签信息,比如 ID 列是按照某种规则编码的,能间接推出类别。

解决:在交叉验证过程中测试任何新特征时,必须严格按照“只在训练折内计算统计量,再映射到验证折”的方式。写一个函数封装这个逻辑,避免手动复制粘贴时出错。对 ID 类特征,不要直接当成数值特征用,先看成类别特征,观察它对目标的分类能力是否异常高,如果单个特征就能把数据完美分开,几乎可以断定是泄漏。

6. 进阶用法:用赛题数据搭一套可复用的参赛脚手架

网络赛数据最有价值的地方,不是跑出一个好看的分数,而是能帮你试探出自己整个流程的哪一步最容易出错。所以我会建议把这套赛题数据拿去搭一个可复用的参赛脚手架,以后换任何赛题,数据一换,脚本就能跑。

脚手架至少要有四个模块:数据探查、特征生成、交叉验证、提交校验。数据探查脚本输出所有字段的缺失率、基数、分布直方图,保存成一个 HTML 报告。特征生成脚本只做一件事,输入 train 和 test,输出拼好的特征矩阵。交叉验证脚本固定读特征矩阵,按模型类型跑多折验证,并把每折分数写进日志。提交校验脚本是最后一道防线,检查列名、行数、ID 幂等性。

一个最小化的提交校验函数:

def validate_submission(test, submission): errors = [] if len(test) != len(submission): errors.append(f"行数不一致: test={len(test)}, submission={len(submission)}") if list(submission.columns)[:2] != ["id", "pred"]: errors.append("列名应为 id, pred") if test["id"].duplicated().sum() > 0: errors.append("测试集ID存在重复,需要人工检查") for error in errors: print("ERROR:", error) if not errors: print("submission OK")

这个函数放在本地,每次要提交之前强制运行。它的力量不在于逻辑多复杂,而在于把“我以为没问题”变成“系统告诉我没问题”。网络赛最贵的就是提交次数,多给自己留几次后悔药,比任何调参技巧都管用。

更进一步,可以把交叉验证里记录的特征重要性和分数变化保存下来,作为下一轮特征构造的调整依据。用赛题数据做完这一整套流程,再把同样的脚本换到新赛题上,只需要改数据路径、目标列名和评测指标。从那以后,我每次拿到网络赛数据,第一件事不是跑 baseline,而是先写一个提交文件校验脚本,把列名、行数、数据类型全部卡一遍,确认无误再开始花时间调参。希望帮到你。

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

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

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

立即咨询