人工智能网络赛实战:从数据清洗到交叉验证的竞赛全流程指南

发布时间:2026/10/12 14:38:11
人工智能网络赛实战:从数据清洗到交叉验证的竞赛全流程指南
简介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,, encodingutf-8) test pd.read_csv(data/test.csv, sep,, encodingutf-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(ascendingFalse))这里sep,是因为 CSV 默认逗号分隔如果字段内容里本身包含逗号读出来会错位后面会讲怎么排查。encodingutf-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(normalizeTrue))nunique()统计目标列有多少个不同取值。如果是 2就是二分类如果是 5 到 20是多分类如果取值特别多比如上千要确认是不是连续值回归任务。value_counts(normalizeTrue)输出各类别占比帮助判断类别是否不平衡。本科组网络赛常见二分类但也会出现多分类或回归不要假设赛题一定是二分类。还要注意目标列的数据类型。有时候标签看起来是0和1但 pandas 读进来可能是 float原因是有缺失值或某个异常字符。这种情况下要把目标列先转成字符串检查而不是直接建模。网络赛的数据一般不会太脏但“一般不会”四个字往往是翻车的开始。3. 把原始数据变成特征清洗、编码与构造的落地做法3.1 数据清洗顺序先处理缺失和重复再谈特征数据清洗最怕的不是脏数据而是没有顺序。我的习惯是固定流程先查重复行再处理缺失值然后处理异常值最后才做特征编码和构造。顺序不能反否则去重之后缺失统计会变导致前面白做。先看重复行# 按全部列查重复行 dup_rows train[train.duplicated(keepFalse)] 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()返回布尔序列keepFalse表示把所有重复行都标出来方便查看。查找重复 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(columnsdrop_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, onuser_id, howleft) # 测试集关联时需要先用训练集生成的统计表做映射 test test.merge(user_stats, onuser_id, howleft)这里有几个坑。第一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_splits5, shuffleTrue, random_state42) 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_estimators2000) 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(ffold {fold} score: {score:.5f}) print(mean score:, np.mean(fold_scores))n_splits5是常用折数数据量较小可以增加到 10 折数据量太大就减到 3 折。shuffleTrue打乱样本顺序避免原始数据按某种时间顺序排列导致同一类样本集中在一个折里。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, verboseFalse)] ) best_iters.append(model.best_iteration_) # 取平均轮数在全量数据上重新训练 mean_iter int(np.mean(best_iters)) final_model lgb.LGBMClassifier(**params, n_estimatorsmean_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, indexFalse)最后一步保存提交文件时列名必须和提交样例完全一致。很多线上评分系统比对的是列名和行数而不是列顺序。indexFalse是必须的否则 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 -Acat -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而是先写一个提交文件校验脚本把列名、行数、数据类型全部卡一遍确认无误再开始花时间调参。希望帮到你。本文还有配套的精品资源点击获取