AI模型调优路上那些让我想砸电脑的坑
去年十月底,我们实验室接了个和某电商平台合作的推荐系统优化项目。导师说“这个项目挺适合你”,我一琢磨,也对——毕竟在上家公司干了三年多后端开发,天天跟高并发、低延迟死磕,现在读研二正好想换个方向试试水。再加上我对性能优化一直有点执念(谁让我当年被一个 Redis 缓存穿透问题折磨到凌晨三点呢),于是欣然入坑。
结果没多久我就后悔了。
不是因为技术难,而是因为……AI模型训练这玩意儿,真·玄学!你以为调个 learning rate 就能起飞?Too young。尤其是在 deadline 压顶、GPU 资源紧张、产品经理还在群里问“能不能明天上线”的时候,那种无力感,真的会让人怀疑人生。
不过好在我有个“秘密武器”:重度依赖 ChatGPT 和 Claude。别笑,这年头不会用 AI 编程的研究生,就像不会用 Git 的程序员——寸步难行。而且最近我还试了 Google 的 Gemini,虽然它在国内访问不太稳,但某些场景下确实比 GPT 更懂工程细节。
今天这篇就聊聊我在做这个推荐模型调优过程中踩过的几个大坑,以及怎么用 AI 编程工具把它们一个个填平的。希望你看到最后,能少走点弯路,多睡几小时觉。
数据清洗:你以为的“干净数据”,其实全是坑
项目一开始,我们拿到的是平台提供的用户行为日志,号称“已脱敏、结构化、可直接用于训练”。信了这句话的我,天真得像个刚入职的实习生。
跑第一轮 baseline 时,模型 loss 降得飞快,准确率却卡在 58% 死活上不去。我一度以为是模型架构有问题,疯狂换 Transformer、Wide&Deep、甚至试了 LightGCN,结果全都扑街。
直到某天深夜,我灵机一动,用 Pandas 看了眼原始数据分布:
import pandas as pd
df = pd.read_parquet("user_behavior.parquet")
print(df['item_id'].value_counts().head(10))
输出吓我一跳:前三个 item_id 的点击量占了总样本的 63%!再一看时间戳,大量记录集中在凌晨 2-4 点,IP 地址高度重复……这哪是真实用户行为?分明是爬虫刷的数据!
这时候,我让 Claude 帮我写了个异常行为过滤脚本:
“帮我用 Python 写一个过滤函数,剔除以下样本:1)单用户单日点击超过 500 次;2)item_id 出现频率 top 0.1%;3)时间戳在非活跃时段(0:00-5:00)且无其他行为。”
它秒回了一段带详细注释的代码,还提醒我:“建议加入 session 间隔阈值,避免把真实夜猫子用户误杀。”——这细节,比我组里某些 PhD 还专业。
清理完数据后,模型 AUC 直接从 0.61 提到了 0.73。那一刻我悟了:垃圾进,垃圾出,AI 再强也救不了脏数据。
学习率调度:别再盲目用 ReduceLROnPlateau 了
数据搞定了,模型开始收敛。但我发现一个问题:训练后期 loss 震荡严重,验证集指标忽高忽低,像极了我的股票账户。
一开始我用了 Keras 默认的 ReduceLROnPlateau,monitor='val_loss',patience=3。结果呢?学习率在 1e-4 和 1e-5 之间反复横跳,模型根本没法稳定。
后来我翻了篇 ICML 的论文,里面提到 cosine annealing with warm restarts 在推荐场景效果更好。但手动实现太麻烦,我又不想重写训练循环。
这时候我打开了 Gemini(当时 GCP 送了点免费额度),输入:
“PyTorch 中如何用 cosine annealing with warm restarts 做学习率调度?给出完整训练 loop 示例,包含 optimizer 和 scheduler 的配合。”
它不仅给出了标准写法,还贴心地加了一句:“注意:scheduler.step() 应放在 optimizer.step() 之后,否则第一个 epoch 的 lr 会错误。”
from torch.optim.lr_scheduler import CosineAnnealingWarmRestarts
optimizer = Adam(model.parameters(), lr=1e-3)
scheduler = CosineAnnealingWarmRestarts(optimizer, T_0=10, T_mult=2)
for epoch in range(num_epochs):
for batch in dataloader:
loss = model(batch)
loss.backward()
optimizer.step()
optimizer.zero_grad()
scheduler.step() # 关键!别放错位置
改完之后,loss 曲线终于平滑如德芙。验证集 AUC 又涨了 0.02。别小看这 0.02,在推荐系统里可能就是百万级 GMV 的差距。
特征工程:AI 编程让我少写了三天代码
我们的模型用了 user_id、item_id、category、hour_of_day 等稀疏特征,外加一些统计类 dense 特征(比如用户过去 7 天点击率)。
但产品经理临时加需求:“能不能加上‘用户最近一次购买距今小时数’?” 我心里一万只羊驼奔腾——这意味着要重新跑一遍特征 pipeline,还得处理冷启动用户(从未购买过)的缺失值。
正愁着,我突然想到:为什么不直接让 AI 帮我生成特征工程代码?
我给 ChatGPT 发了原始 schema 和目标逻辑:
“用户行为表有 user_id, event_time, event_type(click/buy)。请用 Spark SQL 写一段代码,计算每个 user 最近一次 buy 距当前时间的小时数,若从未购买则设为 -1。”
它返回的 SQL 不仅正确,还自动处理了时区问题,并建议:“可以用 COALESCE(buy_time, '1970-01-01') 避免 NULL。”
更绝的是,它还顺手给了 PySpark DataFrame 版本,方便我集成到现有 pipeline:
from pyspark.sql import functions as F
from pyspark.sql.window import Window
window = Window.partitionBy("user_id").orderBy(F.desc("event_time"))
df_buy = df.filter(F.col("event_type") == "buy") \
.withColumn("rn", F.row_number().over(window)) \
.filter(F.col("rn") == 1)
df_final = df.join(df_buy.select("user_id", "event_time"), on="user_id", how="left") \
.withColumn("hours_since_last_buy",
F.when(F.col("event_time").isNull(), -1)
.otherwise((F.unix_timestamp(F.current_timestamp()) -
F.unix_timestamp(F.col("event_time"))) / 3600))
这段代码我几乎没改,直接跑通。省下的时间够我多调两组超参了。
超参搜索:别再 grid search 了,试试贝叶斯优化
早期我用网格搜索调参,learning_rate ∈ [1e-4, 5e-4, 1e-3],batch_size ∈ [512, 1024, 2048]……结果跑了 9 次实验,最好的那组居然不是角落里的组合,而是中间某个“不起眼”的配置。
导师看了直摇头:“你这是 brute force,不是调参。”
后来我改用 Optuna 做贝叶斯优化。但写 objective function 时又卡住了——怎么把模型训练封装成可复用的函数?又怕内存泄漏。
这次我同时问了 GPT 和 Gemini,对比它们的回答:
| 工具 | 优点 | 缺点 |
|---|---|---|
| ChatGPT | 代码简洁,注释清晰 | 忽略了 GPU 内存释放 |
| Gemini | 主动提醒 torch.cuda.empty_cache() |
代码稍冗长 |
最后我融合两家之长,写出如下 objective:
def objective(trial):
lr = trial.suggest_float("lr", 1e-4, 1e-2, log=True)
batch_size = trial.suggest_categorical("batch_size", [512, 1024, 2048])
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
model = MyRecModel(...)
optimizer = Adam(model.parameters(), lr=lr)
for epoch in range(5): # 小 epoch 快速评估
train_one_epoch(model, train_loader, optimizer)
val_score = evaluate(model, val_loader)
# 关键:释放显存,避免 trial 累积爆显存
del model, optimizer
torch.cuda.empty_cache()
return val_score
跑了 30 次 trials 后,找到了一组超参,AUC 达到 0.792,比之前手动调的高了 0.03+。而且整个过程只用了两天 GPU 时间,效率提升明显。
模型部署:别让线上 inference 成为惊喜盲盒
好不容易模型调好了,准备上线。结果运维同学反馈:“QPS 上不去,P99 延迟 800ms,用户都点烂了!”
我本地测试明明只有 20ms 啊?一查才发现:训练时用了 float32,推理时没开 TensorRT 或 ONNX Runtime,还傻乎乎地每次 forward 都重新构建 embedding。
这时候我又祭出 AI 编程大法。让 Claude 帮我写 ONNX 导出 + 推理脚本:
“PyTorch 模型包含 Embedding 层和 MLP,如何正确导出为 ONNX 并用 onnxruntime 推理?注意处理动态 batch size。”
它不仅给出了导出代码,还警告:“Embedding lookup 在 ONNX 中可能被优化掉,建议固定 vocab size 并使用 dynamic_axes。”
最终我们用 ONNX Runtime + CPU AVX-512 加速,P99 延迟降到 45ms,QPS 提升 5 倍。运维大哥终于对我笑了。
总结:AI 编程不是银弹,但能让你少当冤种
回头看这几个月,从数据清洗到线上部署,每一个环节都踩过坑。但不得不说,熟练使用 AI 编程工具(GPT/Claude/Gemini)真的大幅提升了我的开发效率。
尤其是 Gemini,在工程细节和 PyTorch 生态的理解上,有时候比 GPT 更靠谱。当然,它也有短板——比如对中文业务场景的理解不如国产大模型,但在纯技术问题上,表现相当稳。
几点血泪经验送给你:
- 数据质量永远是第一位的。再 fancy 的模型也扛不住垃圾数据。
- 别迷信默认配置。ReduceLROnPlateau 不一定适合你的任务。
- 超参搜索用贝叶斯,别穷举。时间就是生命,GPU 就是金钱。
- 线上推理要专门优化。训练快 ≠ 推理快。
- 善用 AI 编程,但别 blind trust。它写出来的代码,一定要自己 review,尤其涉及资源管理和边界条件。
最后说句实在话:我现在之所以敢考虑跳槽换环境,很大程度上是因为掌握了这套“AI+工程”的组合拳。毕竟在这个卷成麻花的时代,会调模型的人很多,但能把模型从实验室搬到线上、还能压住延迟的人,才是真正的稀缺品。
哦对了,上周五晚上我终于搞定所有线上监控告警,走出实验室时已经凌晨一点。抬头看见月亮,突然觉得——这破模型,好像也没那么讨厌了。
(完)

评论 0