裸辞半年后我靠AI调优拿到了新Offer
坐标北京,前大厂前端,Gap了整整六个月。这篇文章记录了我从零开始学AI模型训练调优的血泪史,以及那些在深夜里对着GPU发呆的日子。
先说说我这半年
去年九月底,我在某大厂提了离职。
说实话,不是被裁的,是主动提的。当时刚扛完一轮大促,连续三周晚上十点打车回家,到家倒头就睡,第二天早上挤一号线去继续搬砖。有天早上在地铁上,看着周围一张张面无表情的脸,突然就想:我特么才28,怎么活得跟48一样?
于是裸辞了。
前两个月是真的爽,每天睡到自然醒,下午去健身房撸铁,晚上打打游戏。但到了第三个月,看着银行卡余额蹭蹭往下掉,开始慌了。打开招聘软件一看,好家伙,现在招前端的JD里,十条有八条写着"熟悉AI相关技术优先"。
我当时就懵了。我一个写了六年React的,VSCode里插件装了一百多个,从Prettier到ESLint到各种主题皮肤,唯独没有一个是跟AI沾边的。
行吧,学呗。
从前端到AI,这跨界跨得有点大
刚开始学的时候,我是真的一脸懵逼。什么梯度下降、反向传播、损失函数,每个字都认识,连在一起就不知道在说啥。但没办法,为了找工作,硬着头皮上。
我这个人有个习惯,学新技术喜欢直接上手干,看十篇博客不如自己写十行代码。于是我在GitHub上找了个开源的图像分类项目,准备自己跑一遍。
然后,第一道坎就来了——环境配置。
搞过前端的朋友都知道,我们配环境最多就是npm install报个错,查查StackOverflow,删个node_modules重来。但AI这边呢?CUDA版本、cuDNN版本、PyTorch版本、Python版本,这四个东西的版本必须严格对应,差一个小版本都可能报错。
我记得特别清楚,有天晚上我配环境配到凌晨两点,RuntimeError: CUDA out of memory这个报错我看了不下五十遍。当时真的想砸电脑。
后来加了个技术群,群里有个老哥跟我说:"兄弟,你这2080Ti跑大模型,不out of memory才怪。"
得,硬件不行,那就从调优技巧入手,把有限的资源榨干。
数据才是爹
这是我学了三个月AI之后,最深刻的一个体会。
以前写前端,我们总说"组件设计"、"架构模式",觉得代码写得好就能解决一切问题。但在AI这里,数据质量决定了一切。
我刚开始训练一个文本分类模型的时候,准确率死活上不去,一直在60%左右徘徊。我调了学习率、换了模型结构、加了正则化,折腾了一个星期,准确率还是62%。
后来我静下心来,把训练集打开一条一条看。好家伙,数据标注错误率高达30%!有些明显是A类的文本被标成了B类,有些数据甚至是重复的。
我把数据清洗了一遍,去重、纠错、统一格式,重新跑了一遍。准确率直接飙到了85%。
当时那个心情,怎么形容呢,就像你debug了一天的代码,最后发现是少写了一个分号。又气又笑。
数据增强的几个骚操作
数据不够,增强来凑。这里分享几个我实际用过且效果不错的方法:
| 增强方法 | 适用场景 | 效果提升 | 踩坑点 |
|---|---|---|---|
| 同义词替换 | 文本分类 | 3-5% | 别替换专业术语 |
| 回译 | 短文本 | 5-8% | 耗时较长 |
| Mixup | 图像分类 | 2-4% | 混合比例要调 |
| Cutout | 图像分类 | 1-3% | 裁剪大小有讲究 |
| EDA | 通用NLP | 4-6% | 别过度增强 |
有个细节要注意,数据增强不是越多越好。我之前试过把训练集扩大了5倍,结果模型过拟合了,在验证集上表现反而下降。后来控制在2-3倍,效果最好。
学习率,这个磨人的小妖精
如果说数据是爹,那学习率就是妈。
调学习率这件事,说简单也简单,说难也难。简单在于,框架都给你封装好了,一行代码就能改。难在于,你永远不知道当前这个学习率是不是最优的。
我一开始用的是固定学习率,0.001,教科书上写的默认值。结果训练loss震荡得跟心电图似的,上上下下就是降不下来。
后来我试了学习率预热(Warmup),就是训练刚开始的时候用很小的学习率,然后慢慢涨到目标值。这个方法对Transformer类的模型特别有效,我试了一下,训练稳定性提升了不少。
# 我常用的学习率调度策略
from transformers import get_cosine_schedule_with_warmup
optimizer = AdamW(model.parameters(), lr=3e-5, weight_decay=0.01)
scheduler = get_cosine_schedule_with_warmup(
optimizer,
num_warmup_steps=100,
num_training_steps=1000
)
# 每个step之后调用
for step, batch in enumerate(train_dataloader):
outputs = model(**batch)
loss = outputs.loss
loss.backward()
optimizer.step()
scheduler.step() # 关键!别忘了这步
optimizer.zero_grad()
还有一个技巧叫学习率衰减。我一般用余弦退火(Cosine Annealing),效果比阶梯衰减好。具体参数嘛,说实话,全靠玄学。我一般会跑几组实验,画个loss曲线,肉眼判断哪个好看就用哪个。
别笑,很多调参侠都是这么干的。
Batch Size的取舍
这个参数,本质上是个显存和效果的trade-off。
我的2080Ti只有11G显存,跑个BERT-base,batch size开到32就直接OOM。但batch size太小,梯度估计不准,训练不稳定。
怎么办?
梯度累加(Gradient Accumulation),这个技巧救了我。
accumulation_steps = 4
effective_batch_size = 8 * accumulation_steps # 等效batch size = 32
for step, batch in enumerate(train_dataloader):
outputs = model(**batch)
loss = outputs.loss / accumulation_steps # 注意这里要除一下
loss.backward()
if (step + 1) % accumulation_steps == 0:
optimizer.step()
optimizer.zero_grad()
原理很简单,就是多做几次前向传播,把梯度累加起来再更新。这样显存占用跟batch size=8一样,但等效的batch size是32。
不过有个坑要注意,loss要除以累加步数,不然梯度会爆炸。我第一次用这个技巧的时候忘了除,loss直接飙到NaN,还以为模型坏了,排查了半天。
混合精度训练,白嫖性能
这个技巧,怎么说呢,属于不用白不用的类型。
混合精度训练(Mixed Precision Training)就是在训练过程中,一部分计算用FP16,一部分用FP32。好处是显存占用减半,速度提升30%-50%,坏处是可能会有精度损失。
但在实际使用中,我发现精度损失几乎可以忽略不计。至少在我做的文本分类任务上,混合精度和全精度的准确率差异在0.1%以内。
from torch.cuda.amp import autocast, GradScaler
scaler = GradScaler()
for batch in train_dataloader:
optimizer.zero_grad()
with autocast(): # 开启混合精度
outputs = model(**batch)
loss = outputs.loss
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
就这几行代码,训练速度直接提升了40%。我当时看到训练时间从6小时缩短到3.5小时的时候,差点在出租屋里叫出来。
正则化,防止模型想太多
模型这东西,跟学生一样,有时候会"想太多"——也就是过拟合。
训练集上准确率99%,验证集上准确率70%,这就是典型的过拟合。模型把训练集里的噪声都学进去了,泛化能力为零。
我常用的正则化手段有这几个:
1. Dropout
这个最经典,也最无脑。但我发现一个有意思的现象:Dropout率不是越高越好。我试过0.1、0.3、0.5三个值,0.3效果最好,0.5反而让模型学不动了。
2. Weight Decay
就是L2正则化,在优化器里加一行就行。我一般设0.01,这个值比较万能。
3. Early Stopping
这个我特别推荐。就是监控验证集上的指标,如果连续N个epoch没有提升,就提前停止训练。
best_val_acc = 0
patience = 5
wait = 0
for epoch in range(num_epochs):
train(model, train_dataloader)
val_acc = evaluate(model, val_dataloader)
if val_acc > best_val_acc:
best_val_acc = val_acc
torch.save(model.state_dict(), 'best_model.pt')
wait = 0
else:
wait += 1
if wait >= patience:
print("Early stopping!")
break
有了Early Stopping,我再也不用担心训练太久导致过拟合了。而且还能省时间,何乐而不为?
用Cursor写调参脚本,效率翻倍
说到写代码,不得不提一下我最近的新宠——Cursor。
裸辞之后,我以前的同事推荐我用Cursor写代码。说实话,刚开始我是拒绝的。VSCode我用得好好的,插件装了一百多个,快捷键肌肉记忆都形成了,换个编辑器?不可能。
但用了一次之后,真香。
Cursor本质上是个VSCode的fork,界面几乎一样,我的插件和配置直接迁移过来,零学习成本。但它内置了AI辅助编码,而且不是那种智障的自动补全,是真正能理解你意图的AI。
比如我写训练脚本的时候,直接跟它说:"帮我写一个带有学习率预热和余弦退火的学习率调度器",它直接就给我生成了上面那段代码,而且质量还挺高。
更爽的是,我调参的时候经常需要改一堆配置,以前我得手动改,改完还得记得改回来。现在直接跟Cursor说:"把batch size改成16,学习率改成2e-5,跑一下看看效果",它就能帮我改配置、跑实验、记录结果。
# config.yaml - 我一般用yaml管理实验配置
experiment:
name: "text_cls_exp_003"
model:
name: "bert-base-chinese"
dropout: 0.3
training:
batch_size: 16
learning_rate: 2.0e-5
num_epochs: 10
warmup_steps: 100
weight_decay: 0.01
gradient_accumulation_steps: 2
data:
train_path: "./data/train.json"
val_path: "./data/val.json"
max_length: 256
配合Cursor,我一天能跑十几组实验,以前一天最多跑三组。效率提升不是一点半点。
模型选择,别盲目追大
这一点,是我踩了很多坑之后总结出来的。
刚开始学的时候,我总觉得模型越大越好。BERT不行就上RoBERTa,RoBERTa不行就上DeBERTa,DeBERTa不行就上GPT。结果呢?训练时间翻倍,显存爆炸,效果提升微乎其微。
后来我做了个对比实验:
| 模型 | 参数量 | 训练时间 | 准确率 | 显存占用 |
|---|---|---|---|---|
| BERT-base | 110M | 3.5h | 87.2% | 8G |
| RoBERTa-base | 125M | 4.2h | 87.8% | 9G |
| DeBERTa-base | 140M | 5.1h | 88.1% | 10G |
| BERT-large | 340M | 9.5h | 88.5% | OOM |
看到了吗?从BERT-base到DeBERTa-base,准确率只提升了0.9%,但训练时间增加了45%。而BERT-large虽然准确率最高,但我的卡根本跑不起来。
所以我的建议是:先用小模型跑通流程,确认数据没问题,再考虑换大模型。
很多时候,模型之间的差距,远没有数据质量的差距大。
一些反直觉的经验
学了半年AI,我发现有些经验跟直觉是相反的:
1. 更多的数据不一定更好
我试过把训练集从1万条扩充到5万条,结果准确率反而下降了2%。后来分析发现,新增的数据质量不如原来的,把模型带偏了。
2. 训练越久不一定越好
有次我训练了50个epoch,发现第15个epoch的时候验证集准确率最高,后面都在下降。所以Early Stopping真的很重要。
3. 复杂的模型不一定更好
我试过在一个简单的二分类任务上用GPT-2,结果效果还不如BERT-base。杀鸡用牛刀,不仅慢,还可能过拟合。
4. 调参不是玄学,但也不完全是科学
虽然有很多经验法则,但最终效果还是得靠实验。我一般会设计一个网格搜索,把关键参数都试一遍,然后选最好的。
# 简单的网格搜索
from itertools import product
param_grid = {
'learning_rate': [1e-5, 2e-5, 3e-5],
'batch_size': [16, 32],
'dropout': [0.1, 0.3]
}
best_acc = 0
best_params = None
for lr, bs, drop in product(param_grid['learning_rate'],
param_grid['batch_size'],
param_grid['dropout']):
acc = train_and_evaluate(lr=lr, batch_size=bs, dropout=drop)
if acc > best_acc:
best_acc = acc
best_params = {'lr': lr, 'batch_size': bs, 'dropout': drop}
print(f"lr={lr}, bs={bs}, drop={drop} -> acc={acc}")
print(f"Best params: {best_params}, acc={best_acc}")
面试的时候被问到了什么
最后说说面试。
上个月面了三家公司,有两家问到了AI相关的问题。我把记得的问题整理一下:
Q: 你是怎么理解过拟合的?怎么解决?
这个我答得比较好,因为实际遇到过。我举了那个训练集准确率99%、验证集70%的例子,然后说了Dropout、Early Stopping、数据增强这几个方法。面试官点了点头。
Q: 学习率太大或太小分别会有什么问题?
太大:loss震荡,甚至发散。太小:收敛太慢,可能陷入局部最优。
Q: 你在项目中是怎么做模型调优的?
我说了数据清洗、学习率调度、混合精度训练、Early Stopping这些。面试官追问了一个问题:"你怎么判断是数据问题还是模型问题?"
这个问题我准备过。我的回答是:先看训练集和验证集的loss曲线。如果训练集loss降不下去,说明模型学不动,可能是模型容量不够或者学习率有问题。如果训练集loss降下去了但验证集升上去了,说明过拟合了,可能是数据不够或者正则化不够。
面试官说:"不错,有实战经验。"
写在最后
从裸辞到现在,整整半年。
这半年里,我从一个连PyTorch都不会装的纯前端,到现在能独立完成一个文本分类模型的训练和调优。说实话,过程挺痛苦的,但收获也很大。
最大的收获不是技术本身,而是思维方式的变化。
以前写前端,遇到问题就是debug,看报错信息,查文档,改代码。但在AI这里,很多问题不是靠debug能解决的。你需要理解数据、理解模型、理解训练过程,然后做出判断和调整。
这种"全局思维",对我帮助很大。
现在我已经拿到了一家AI公司的offer,岗位是"AI应用工程师",说白了就是用AI技术做产品。薪资比之前涨了不少,最重要的是,不用卷了。
如果你也是一个想转AI方向的前端,或者跟我一样裸辞在家的朋友,希望这篇文章能给你一些帮助。
最后,借用一句我特别喜欢的话:种一棵树最好的时间是十年前,其次是现在。
共勉。
P.S. 如果你也在学AI,推荐几个我用的资源:Hugging Face的课程、李宏毅的机器学习视频、还有fast.ai的实战课程。别光收藏,真的要看。
P.P.S. 我的VSCode插件列表里现在多了个Cursor,如果你还没用过,真的可以试试。不是广告,纯粹是好用。


评论 0