裸辞半年后我靠AI调优拿到了新Offer

DevNinja
2026-07-16 01:54
阅读 904

坐标北京,前大厂前端,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

最热最新
暂无评论
DevNinjaLv.1
0
影响力
0
文章
0
粉丝