被产品经理逼着调参的那两周:一个普通CS本科生的AI训练实战手记
大家好,我是一名普通一本 CS 专业的大四狗,坐标成都,刚拿了个本地二线大厂的 offer 正在等入职。最近在家闲得发慌(其实是被催着提前熟悉项目),干脆把上个月帮实习团队搞的一个小模型调优项目复盘写下来。毕竟,谁让我重度依赖 ChatGPT 和 Claude 写代码呢?不输出点东西感觉对不起每天白嫖的 token。
事情的起因特别“互联网”——我们组负责公司内容推荐系统的冷启动模块,产品经理在双11前两周突然拍脑袋说:“能不能让新用户首屏推荐点击率再提 5%?”
我:???
要知道这模块跑的是轻量级 DNN + 特征交叉,线上已经稳定跑了半年,数据 pipeline、特征工程、AB 实验都跑通了。现在要临时压榨性能,还卡 deadline,这不是拿算法工程师当炼丹童子使吗?
但没办法,PM 手里握着 KPI,我们手里只有 GPU 显存。于是,我这个即将入职的“准员工”就被拉来当壮丁,和正式同事一起卷这个项目。说是卷,其实大部分时间都在和 OOM 报错、梯度爆炸、验证集 loss 不下降斗智斗勇。
别看是小模型,照样能把你搞崩溃
我们的原始模型其实挺朴素:输入层接 embedding,中间三层全连接,最后 sigmoid 输出点击概率。训练数据是过去一个月的新用户行为日志,大约 200w 条样本,正负样本比 1:4(还算健康)。用 PyTorch Lightning 搭的,训练脚本也就 200 行出头。
但问题就出在这“朴素”上。一开始我天真地以为,调参无非就是改改 learning rate、batch size、加个 dropout。结果第一轮跑完,验证 AUC 只有 0.68,离线上基线 0.72 差了一大截。更离谱的是,训练 loss 稳稳下降,验证 loss 却在第 3 个 epoch 开始反弹——典型的过拟合。
当时真的想砸电脑。还好 Claude 提醒我:“你有没有检查特征分布漂移?” 我一查,好家伙,训练集里“用户设备类型”这个特征,iOS 占 60%,但线上新流量里 Android 占 70%。这不就相当于拿 iPhone 用户的数据去预测华为用户的行为?难怪效果差。
教训一:模型没毛病,数据先背锅。
我们赶紧和数据运营同学对齐,重新切分训练/验证集,按天滚动采样,确保分布对齐。同时加了特征重要性分析(SHAP 值),砍掉几个高 cardinality 但贡献低的 categorical 特征。这一波操作后,AUC 直接拉到 0.71,差点泪目。
调参不是玄学,但确实有点“看天吃饭”
解决了数据问题,真正的调优才开始。这里分享几个我觉得真有用的技巧,不是网上抄的那种“调大学习率”废话。
1. Learning Rate 别瞎试,用 LR Finder
以前我都是凭感觉设 0.001,后来被同事安利了 PyTorch 的 lr_finder(或者 fastai 里的版本)。原理很简单:从很小的 lr 开始,每个 batch 线性增大,记录 loss 变化。理想情况下 loss 会先快速下降,然后突然上升,拐点附近就是最佳 lr。
# 伪代码示意
lr_finder = LRFinder(model, optimizer, criterion)
lr_finder.range_test(train_loader, end_lr=1, num_iter=100)
lr_finder.plot() # 找 loss 最陡降处
我们跑出来最佳 lr 在 3e-4 左右,比默认的 1e-3 效果好不少。而且配合 cosine annealing with warm restarts,收敛更快,还不容易卡局部最优。
2. Batch Size 不是越大越好,尤其显存紧张时
我们用的是 2080Ti(别笑,成都这边很多公司还在用这卡),batch size 超过 1024 就 OOM。但小 batch 又导致梯度噪声大。后来用了 梯度累积(gradient accumulation):
accum_steps = 4
for i, (x, y) in enumerate(dataloader):
loss = model(x, y)
loss = loss / accum_steps # scale loss
loss.backward()
if (i + 1) % accum_steps == 0:
optimizer.step()
optimizer.zero_grad()
相当于逻辑上 batch size = 4096,物理上只占 1024 的显存。训练稳定性提升明显,AUC 又涨了 0.005。
3. 别忽略“无聊”的工程细节
比如 随机种子固定。有次我两次跑同一套参数,结果 AUC 差了 0.01,差点以为代码有 bug。后来发现是没 fix seed,PyTorch/CUDA/dataloader 的随机性叠加起来影响不小。
torch.manual_seed(42)
np.random.seed(42)
random.seed(42)
torch.backends.cudnn.deterministic = True
还有 早停(early stopping)策略。我们设了 patience=5,监控验证集 AUC 而不是 loss——因为业务目标是点击率,AUC 更贴近实际效果。有一次模型在第 8 轮就停了,省了 2 小时训练时间,运维大哥直夸我会过日子。
算法选择:有时候简单就是美
最初 PM 还提议上 GNN 或者 Transformer,说“现在不都流行 Attention 吗”。我和 mentor 对视一眼,默默打开了 FLOPs 计算器……结论很现实:在线服务延迟要求 <50ms,模型推理必须 <20ms。那些 fancy 的结构根本塞不进线上 pipeline。
最后我们回归朴实:在原始 DNN 上加了 Feature-wise Linear Modulation (FiLM) —— 一种轻量级的特征条件归一化。原理是对每个特征通道做 affine transform,参数由另一个小网络生成。计算开销几乎可忽略,但 AUC 提升了 0.008。
class FiLM(nn.Module):
def __init__(self, feat_dim, cond_dim):
super().__init__()
self.gamma_net = nn.Linear(cond_dim, feat_dim)
self.beta_net = nn.Linear(cond_dim, feat_dim)
def forward(self, x, cond):
gamma = self.gamma_net(cond)
beta = self.beta_net(cond)
return gamma * x + beta
上线后 AB 实验显示 CTR +4.2%,虽然没到 PM 要的 5%,但也够交差了。毕竟算法不是万能的,运营和产品预期管理也很关键——这话是 mentor 教我的,深以为然。
性能对比:调优前后到底差多少?
为了说服 PM 和运维接受新模型,我们做了详细对比。数据如下(测试环境:2080Ti, CPU 8核, batch=1024):
| 指标 | 原始模型 | 调优后模型 | 提升 |
|---|---|---|---|
| AUC | 0.720 | 0.741 | +2.9% |
| 训练时间 / epoch | 8m 12s | 9m 05s | +11% |
| 推理延迟 (p99) | 18ms | 21ms | +17% |
| 显存占用 | 5.2GB | 5.8GB | +12% |
| 线上 CTR (AB实验) | 基线 | +4.2% | 达标 ✅ |
虽然推理慢了点,但运维看了数据后也没多说什么——毕竟 CTR 提升直接关联 GMV,老板开心就行。
开发心得:工具链比算法更重要
这次项目让我深刻体会到,在工业界,模型只是拼图的一小块。真正决定成败的,往往是那些“非算法”环节:
- 数据 pipeline 是否健壮:我们用 Airflow 调度,某次上游表延迟导致特征缺失,模型直接崩了。
- 监控是否到位:接入了 Prometheus + Grafana,实时看 loss、AUC、QPS,出问题秒定位。
- AB 实验框架是否可靠:运营同学反馈“怎么新模型反而点击少了?”,结果发现是分流逻辑写错了……
最搞笑的是上周五晚上,我刚提交代码准备下班,CI 突然报错:
AssertionError: Negative sampling ratio must be > 0
翻了半天才发现是测试用例里 mock 数据没更新。当时真想给测试同学发个“下次请用生产数据格式”。
写在最后:调参如修行
说实话,作为一个还没正式入职的菜鸟,能参与这种贴近业务的项目,真的赚到了。以前在学校总觉得“调参=调学习率+batch size”,现在才知道,模型训练是一个系统工程,需要算法、开发、运营、产品多方拉扯妥协的结果。
我也终于理解为什么大厂要分 MLE(Machine Learning Engineer)和 Researcher——我们不是在追求 SOTA,而是在有限资源下找到性价比最高的解。
对了,入职前 HR 问我有什么期待,我说:“希望能配一张 4090。”
HR 笑了:“我们还在用 1080Ti 呢。”
我:???成都的二线厂,原来这么 hardcore 吗……
Anyway,这篇水文就当是给自己留个笔记。如果你也在被 PM 逼着调参,不妨试试 LR Finder + 梯度累积 + 数据对齐三件套。不行的话……就祭出程序员终极奥义:
重启一下,说不定就好了。
(完)

评论 0