深夜在成都用AI编程工具跑深度学习框架的那些事

技术乌托邦
2026-07-18 05:54
阅读 369

凌晨两点半,成都的夜已经深了。窗外偶尔传来几声出租车的喇叭,楼下那家串串香早就关门了。我灌了第三罐可乐,盯着屏幕上PyTorch和TensorFlow的训练loss曲线,顺手在Trae里敲下了一行注释。

说实话,写这篇文章的契机挺有意思的。上个月我们组接了个计算机视觉的项目,要给一家本地餐饮连锁做菜品识别系统。说白了就是让摄像头拍一下盘子,自动算出你吃了啥、多少钱。产品经理说这个需求"很简单"的时候,我差点把手里的盖碗茶泼他脸上。

作为一个白天写业务CRUD、晚上准备考公的打工人,我本来是想用这项目练练手,顺便把最近研究的Rust和深度学习结合一下。结果一上来就面临一个灵魂拷问:用哪个框架?

框架选型这件事,真没想的那么简单

我们团队之前主要用TensorFlow,毕竟历史包袱在那儿,老系统都是TF 1.x时代的产物。但说实话,TF 2.x虽然改进了很多,动态图用起来也顺了,可社区活跃度肉眼可见地在下滑。Stack Overflow上随便搜个问题,排在前面的回答很多都是2019年以前的。

PyTorch这边呢,学术圈基本全面倒戈了。我翻了几篇最新的论文,代码清一色PyTorch。Hugging Face的生态也是围着PyTorch转的。但问题是,我们线上部署环境是TensorFlow Serving,切换框架意味着整个推理链路都要重构。

JAX我也看了,Google亲儿子,函数式编程的风格确实很优雅。但团队里没几个人熟,学习成本太高。而且JAX的调试体验……懂的都懂,一个jit编译错误能让你怀疑人生。

就在我纠结的时候,领导甩了句话:"你先做个技术调研,下周给我结论。"

得,又是个周末要搭进去的活儿。

借助AI工具加速调研过程

既然要对比,那就得真刀真枪地跑。我给自己定了个计划:用同一个数据集、同一个模型结构,分别在三个框架上实现,然后对比开发效率、训练速度、部署便利性。

数据集用的是Food-101,101个菜品分类,每个分类1000张图。模型选了ResNet-50,不算太复杂,但足够说明问题。

这里要说说我最近用的几个AI辅助工具,确实帮了大忙。

先说Trae,这是字节跳动出的IDE,基于VS Code魔改的。我一开始是冲着它的免费额度去的(穷逼程序员的基操),结果用下来发现体验还真不错。它内置的AI对话功能可以直接在编辑器里问问题,不用来回切窗口。我写PyTorch的DataLoader时,有个多进程预取的参数调了半天不对,直接在Trae里问,它给我分析了num_workerspin_memory的配合问题,还顺带提醒了我persistent_workers在较新版本里的变化。省了我至少半小时查文档的时间。

Tabnine我也在用,主要是代码补全。这玩意儿训练数据据说比较干净,补全的代码质量相对稳定。写TensorFlow的tf.data pipeline时,它的补全确实能跟上思路,特别是那些又臭又长的API名字,tf.data.experimental.parallel_interleave这种,手打真的会死。不过它有个问题,对中文注释的理解偶尔会抽风,我写个"这里要做数据增强",它有时候会给我补一段完全不相关的代码。

GitHub Copilot是团队统一采购的,说实话,在深度学习代码这块,它的表现有点两极分化。写一些标准的训练循环、评估逻辑,它补全得很准,基本上我写个def train_epoch(,它就能把整个函数骨架给你填好。但涉及到一些框架特有的骚操作,比如PyTorch的torch.compile或者TensorFlow的自定义训练步骤,它经常给你一些看起来对但实际跑不通的代码。有一次它给我补了段TF的梯度裁剪代码,用了个已经deprecated的API,我debug了二十分钟才发现。

Perplexity是我用来做技术调研的主力。这玩意儿本质上是个AI搜索引擎,但比传统的搜索好用太多了。我想知道"PyTorch 2.0的torch.compile在ResNet上的实际加速比",直接问它,它会给我整理好相关的benchmark数据、论文引用、社区讨论,还会标注信息来源。比起自己在Google上翻十几个网页,效率高了一个量级。

工具 主要用途 优点 槽点
Trae 代码编写+AI对话 免费、集成度高、中文支持好 偶尔响应慢
Tabnine 代码补全 补全质量稳定、本地模型可选 中文注释理解偶尔抽风
GitHub Copilot 代码补全+生成 团队统一采购、长代码生成强 框架新特性支持滞后
Perplexity 技术调研 信息来源可追溯、结构化回答好 深度技术问题分析偶尔不准

三个框架的实际体验

PyTorch:写起来真爽

先说PyTorch。代码写起来确实舒服,动态图的调试体验没得说。我写模型的时候,直接print中间tensor的shape,断点调试想看哪里看哪里,不用像TF 1.x那样搞一堆tf.print

import torch
import torch.nn as nn
from torchvision import models, transforms
from torch.utils.data import DataLoader

# 数据预处理
transform = transforms.Compose([
    transforms.Resize(256),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], 
                         std=[0.229, 0.224, 0.225])
])

# 模型定义,继承nn.Module就完事了
class FoodClassifier(nn.Module):
    def __init__(self, num_classes=101):
        super().__init__()
        self.backbone = models.resnet50(weights='IMAGENET1K_V2')
        self.backbone.fc = nn.Linear(2048, num_classes)
    
    def forward(self, x):
        return self.backbone(x)

model = FoodClassifier()
# 看看模型结构,直接print就行
dummy_input = torch.randn(1, 3, 224, 224)
output = model(dummy_input)
print(f"Output shape: {output.shape}")  # 直接看,不用跑session

训练循环也很直观,没有什么黑盒:

optimizer = torch.optim.Adam(model.parameters(), lr=1e-4)
criterion = nn.CrossEntropyLoss()

for epoch in range(20):
    model.train()
    running_loss = 0.0
    for batch_idx, (images, labels) in enumerate(train_loader):
        images, labels = images.to(device), labels.to(device)
        
        optimizer.zero_grad()
        outputs = model(images)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()
        
        running_loss += loss.item()
        if batch_idx % 50 == 0:
            print(f"Epoch {epoch}, Batch {batch_idx}, Loss: {loss.item():.4f}")

PyTorch 2.0的torch.compile我也试了,加了就一行代码:

model = torch.compile(model, mode="reduce-overhead")

在Food-101上,训练速度大概提升了15%-20%,没吹的那么神,但白嫖的性能不要白不要。

不过PyTorch的部署确实是个痛点。虽然TorchServe能用,但跟TensorFlow Serving比,稳定性和生态还是差一截。我们运维老哥看到TorchServe的文档时,那个表情我至今难忘。

TensorFlow:老当益壮但略显疲态

TF的代码写起来确实啰嗦一些,特别是Keras API和底层API混用的时候。但不得不说,TF的部署生态是真的成熟。TF Serving、TF Lite、TF.js,全家桶安排得明明白白。

import tensorflow as tf
from tensorflow import keras

# 数据pipeline,tf.data确实强大
def preprocess(image, label):
    image = tf.image.resize(image, [256, 256])
    image = tf.image.central_crop(image, 224/256)
    image = tf.keras.applications.resnet.preprocess_input(image)
    return image, label

train_ds = tf.data.Dataset.from_tensor_slices((train_images, train_labels))
train_ds = train_ds.map(preprocess, num_parallel_calls=tf.data.AUTOTUNE)
train_ds = train_ds.batch(32).prefetch(tf.data.AUTOTUNE)

# 模型定义,Keras用起来还是方便的
base_model = keras.applications.ResNet50V2(
    weights='imagenet', 
    include_top=False,
    input_shape=(224, 224, 3)
)
base_model.trainable = False  # 先冻结

inputs = keras.Input(shape=(224, 224, 3))
x = base_model(inputs, training=False)
x = keras.layers.GlobalAveragePooling2D()(x)
outputs = keras.layers.Dense(101, activation='softmax')(x)
model = keras.Model(inputs, outputs)

model.compile(
    optimizer=keras.optimizers.Adam(1e-4),
    loss='sparse_categorical_crossentropy',
    metrics=['accuracy']
)

# 训练,一行搞定
history = model.fit(
    train_ds,
    epochs=20,
    validation_data=val_ds,
    callbacks=[
        keras.callbacks.EarlyStopping(patience=3),
        keras.callbacks.ReduceLROnPlateau(factor=0.5)
    ]
)

TF的tf.data API确实强,数据管道的性能优化做得很好。但写起来那个链式调用,有时候看得人眼花。而且Keras的高层API和底层TF操作混在一起时,调试起来很痛苦。你用了个自定义的tf.function,结果梯度传不过去,报错信息又写得跟天书一样,那种感觉真的很崩溃。

上周五晚上我就遇到了这个问题。一个自定义的loss function,在eager模式下跑得好好的,一@tf.function就nan。折腾到凌晨一点,最后发现是tf.where的条件分支里有个梯度断裂的问题。当时真的想砸键盘。

JAX:优雅但劝退

JAX我只做了个简单的demo,没敢上生产。函数式编程的风格确实很纯粹,但心智负担太重了。

import jax
import jax.numpy as jnp
from flax import linen as nn

class ResNetBlock(nn.Module):
    features: int
    strides: tuple = (1, 1)
    
    @nn.compact
    def __call__(self, x, train=True):
        residual = x
        y = nn.Conv(self.features, (3, 3), self.strides, padding='SAME')(x)
        y = nn.BatchNorm(use_running_average=not train)(y)
        y = nn.relu(y)
        y = nn.Conv(self.features, (3, 3), (1, 1), padding='SAME')(y)
        y = nn.BatchNorm(use_running_average=not train)(y)
        
        if self.strides != (1, 1):
            residual = nn.Conv(self.features, (1, 1), self.strides)(x)
        
        return nn.relu(y + residual)

# 初始化和调用,一切皆函数
model = ResNetBlock(features=64)
key = jax.random.PRNGKey(0)
params = model.init(key, jnp.ones((1, 224, 224, 3)))

# JIT编译,性能拉满
@jax.jit
def train_step(params, batch, key):
    def loss_fn(params):
        logits = model.apply(params, batch['image'], train=True, rngs={'dropout': key})
        return jnp.mean(-jnp.sum(batch['label'] * jax.nn.log_softmax(logits), axis=-1))
    
    loss, grads = jax.value_and_grad(loss_fn)(params)
    return loss, grads

JAX的jit编译确实猛,在同样的硬件上,纯计算速度比PyTorch快不少。但问题是,每次修改代码都要重新编译,调试的时候那个等待时间真的让人抓狂。而且vmappmap这些批量化的操作,理解起来有门槛。

对于我们这种要快速迭代的项目,JAX的开发效率还是不够。除非你是做科研的,追求极致的计算性能,否则不建议轻易入坑。

对比总结

跑了两周的benchmark,数据汇总如下:

指标 PyTorch 2.0 TensorFlow 2.15 JAX 0.4
开发效率 ⭐⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐
训练速度(单卡) 1x 0.92x 1.15x
调试体验 ⭐⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐
部署便利性 ⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐
社区活跃度 ⭐⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐
学习曲线 平缓 中等 陡峭

最后给领导的结论是:新项目上PyTorch,老系统维护继续TF,JAX暂时观望。

领导问我:"那部署怎么办?"

我说:"TorchServe我研究过了,文档虽然烂点,但能用。实在不行就ONNX导出来,TensorRT推理也行。"

领导点点头:"行,那你负责把TorchServe的部署流程跑通,写个文档给运维。"

我:"……"

得,又给自己挖了个坑。

考公和写代码,其实没那么冲突

说到这儿,可能有人好奇,我都准备考公了,还折腾这些干嘛?

其实吧,考公是给自己留条后路。程序员这行,35岁的焦虑不是假的。但在职的这段时间,该学的还是得学,该做的項目还是得做好。一方面是为了对得起这份工资,另一方面,这些实战经验不管以后干啥都用得上。

而且说实话,深度学习这块的东西,考公面试有时候也会问到。特别是现在各地都在搞数字政府、智慧城市,懂点AI的技术岗还是吃香的。

成都这边的生活节奏确实舒服,不像北京上海那么卷。晚上下班回家,吃个火锅,写写代码,看看书,日子过得还行。深夜写代码效率确实高,可能是没人打扰的缘故。

最近研究Rust也是类似的想法。Rust的所有权机制、内存安全这些概念,跟考公复习的逻辑推理其实有异曲同工之妙。都是要你把规则吃透,然后在规则内找到最优解。

好了,可乐喝完了,代码也跑完了。训练loss已经降到0.3了,明天看看验证集的效果。

如果这篇文章对你有帮助,点个赞呗。如果没帮助……那你来成都,我请你吃串串。

评论 0

最热最新
暂无评论
技术乌托邦Lv.1
0
影响力
0
文章
0
粉丝