NOAI 2024 复赛题解|第1题:8个神经元,能学会投篮吗?

NOAI 2024 复赛题解|第1题:8个神经元,能学会投篮吗?

本文核心观点
NOAI 2024复赛第1题要求用极小的神经网络预测投篮命中率,考查对神经网络基础组件的理解。核心思路:用满3层约束,选LeakyReLU避免神经元死亡,注意损失函数不要重复套Sigmoid。

NOAI 2024 复赛题解|第1题:8个神经元,能学会投篮吗?

NOAI复赛 机器学习

需要真题、资料,请拉到文末添加艾斯老师微信。

题目回顾

给一份球星投篮数据(csv),包含 loc_x、loc_y、minutes_remaining、shot_distance、shot_made_flag 五个字段。数据已归一化。训练集 20,000 条,测试集约 5,000 条。

任务:用 PyTorch 搭一个 MLP,根据投篮位置(loc_x, loc_y)预测是否命中。

约束条件

输入 2 个特征(loc_x, loc_y),输出 1 个标签

最多 3 个线性层,每层最多 8 个神经元

激活函数只能从 ReLU、Sigmoid、Tanh、ELU、LeakyReLU、PReLU 中选

不得使用 nn.Sequential() 嵌套

先想清楚:这道题在干什么

别急着写代码 ✋ 先把题目翻译成几何语言:

loc_x 和 loc_y 构成一个二维平面,也就是球场的俯视图。每个投篮落在这个平面上的一个点,标注命中或未命中。你的 MLP 要在这个平面上画出一组曲线,把"容易命中的区域"和"不容易命中的区域"分开。

这组曲线就是决策边界

约束条件把网络压到了极限:最多 3 个线性层,每层 8 个神经元,总参数不超过 100 个。在这个尺度下,每一个设计选择都会直接影响结果。

思路一:先跑通

目标不是拿高分,是确认代码没 bug、数据加载正确、训练流程能走完。

网络结构:2 → 8 → 1

class MLP(nn.Module): def __init__(self): super().__init__() self.fc1 = nn.Linear(2, 8) self.fc2 = nn.Linear(8, 1) def forward(self, x): x = torch.relu(self.fc1(x)) x = self.fc2(x) return x

损失函数用 nn.BCEWithLogitsLoss(),优化器 Adam,学习率 0.001。

这个版本跑出来 accuracy 不会太好。两层网络在二维空间上能画出的决策边界比较简单——基本就是几条折线围出来的凸区域。投篮数据的分布大概率不是这种形状。

但这一步的意义是:拿到一个基础分,同时确认整条流程没问题。

思路二:用满约束

题目允许 3 层、每层 8 个,那就用满。

网络结构:2 → 8 → 8 → 1

class MLP(nn.Module): def __init__(self): super().__init__() self.fc1 = nn.Linear(2, 8) self.fc2 = nn.Linear(8, 8) self.fc3 = nn.Linear(8, 1) def forward(self, x): x = F.leaky_relu(self.fc1(x)) x = F.leaky_relu(self.fc2(x)) x = self.fc3(x) return x

多了一层,表达能力强不少。但更关键的变化是激活函数从 ReLU 换成了 LeakyReLU

为什么?因为网络太小。

ReLU 把所有负值输出为 0。8 个神经元里如果有几个"死掉"了(输出永远为 0,梯度也为 0,再也更新不了),剩下能用的就更少。LeakyReLU 给负值保留一个小斜率(默认 0.01),神经元不会彻底死掉。

大网络里死几个神经元无所谓。8 个神经元的网络里,死一个就少了 12.5% 的容量。

这种"小网络要避免死神经元"的意识来自平时调模型的经验,比赛时能帮你少走一轮弯路。

思路三:细节决定上限

到这一步,代码结构已经定型了,区分度在细节。

激活函数怎么选

6 个候选函数分两类:

类型 函数 特点
有界 Sigmoid、Tanh 输出有上下限,深层容易梯度消失
无界 ReLU、ELU、LeakyReLU、PReLU 正半轴无上限,梯度传导更顺畅

隐藏层用无界的。Sigmoid 和 Tanh 放在隐藏层,网络层数一多梯度就衰减,小网络训练本来就不稳定,不要再加负担。

PReLU 比 LeakyReLU 多一个可学习参数——负半轴的斜率不是固定的 0.01,而是训练出来的。8 个神经元的网络里多几个参数不算什么,但多一点自适应能力可能有帮助。值得试。

损失函数的坑

PyTorch 有两个二分类损失函数,搞混了训练直接崩 💥

损失函数 要求 说明
nn.BCELoss() 输入必须是 0~1 之间的概率 forward 最后要加 sigmoid
nn.BCEWithLogitsLoss() 输入是原始 logits 内部自动做 sigmoid,数值更稳定

推荐用 BCEWithLogitsLoss,forward 里不加 sigmoid。预测时再手动套:

# 训练 criterion = nn.BCEWithLogitsLoss() loss = criterion(model(x).squeeze(), y) # 预测 preds = (torch.sigmoid(model(x)).squeeze() > 0.5).float()

如果你 forward 最后加了 sigmoid,又用了 BCEWithLogitsLoss——相当于 sigmoid 套了两次,输出全部被压到 0.5 附近,模型什么都学不到。这是复赛最常见的 bug 之一。

学习率

小网络对学习率很敏感。太大(> 0.01)loss 剧烈震荡,太小(< 0.0001)epoch 用完了还没学好。从 0.001 起步,配一个学习率调度器:

scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, patience=10, factor=0.5 )

loss 连续 10 个 epoch 不下降,学习率减半。简单有效。

Batch size

直觉上 batch 大一点更稳定。但小网络容量有限,太大的 batch 让梯度过于平滑,反而不容易跳出局部最优。建议 64 ~ 256。

隐藏层宽度不一定要拉满

3 个线性层不意味着每层都要 8 个神经元。可以试试:

2 → 8 → 4 → 1(先宽后窄,像漏斗)

2 → 4 → 8 → 1(先窄后宽,先提取再组合)

哪种更好取决于数据分布,但尝试不同结构、用验证集比较,本身就是拉开差距的动作

容易踩的坑

坑 1只能用 loc_x 和 loc_y,不是全部 4 个特征

数据里有 4 个特征字段,但题目写得很明确:"输入为 2 个特征(loc_x,loc_y)"。用了 minutes_remaining 或 shot_distance,网络结构不满足要求,0 分

坑 2nn.Sequential 写了就是 0 分

# 这样写直接判 0 self.layers = nn.Sequential( nn.Linear(2, 8), nn.ReLU(), nn.Linear(8, 1) )

评分系统检测不到 nn.Sequential 内部的网络结构,会判定不满足要求。必须在 __init__ 里逐层定义,在 forward 里手动连接。

坑 3A 榜不是最终成绩

A 榜用测试集的 50%,B 榜用另外 50%。如果反复根据 A 榜分数微调——你拟合的是 A 榜那 50% 的数据,不是模型的真实能力。B 榜出来可能更低。

正确做法:自己从训练集里划出 10%~20% 做验证集,在本地调好了再提交。把 A 榜当成最终检验,不要当成调参工具。

坑 4注意输出维度

nn.Linear(8, 1) 的输出是 (batch_size, 1),而标签 y 通常是 (batch_size,)。维度不匹配会导致 loss 计算出错但不报错——PyTorch 的广播机制会"帮"你算,结果是错的。记得用 .squeeze() 对齐。

调试技巧:把决策边界画出来

这道题只有 2 个输入特征,意味着你可以直接可视化决策边界:

# 生成网格点 xx, yy = torch.meshgrid( torch.linspace(X[:,0].min(), X[:,0].max(), 200), torch.linspace(X[:,1].min(), X[:,1].max(), 200), indexing='ij' ) grid = torch.stack([xx.flatten(), yy.flatten()], dim=1) # 预测并画图 with torch.no_grad(): probs = torch.sigmoid(model(grid)).reshape(200, 200) plt.contourf(xx, yy, probs, levels=20, cmap='RdBu_r') plt.scatter(X[:,0], X[:,1], c=y, s=1, cmap='RdBu_r')

如果决策边界看起来合理(比如靠近篮筐的区域命中率更高),说明模型学到了东西。如果边界乱七八糟或者是一条直线,说明哪里有问题。

二维输入是可视化的天然优势,不用白不用 👀

区分度在哪

这道题的天花板不高(约束太紧),但下限差异大。

跑通基础 MLP、没犯硬伤,就已经超过一部分人了

用满 3 层、选对激活函数和损失函数,分数会有明显提升

试过不同结构组合、用了学习率调度器、画了决策边界来调试——这些不是高级技巧,是对每个组件都真正理解后自然会做的事

这道题考的不是谁的模型花哨。8 个神经元的网络里,没有地方藏拙,也没有地方炫技。

微信二维码

扫码备注【NOAI】加交流群