1. 从“选哪个框架”这个老问题说起
如果你在深度学习这个圈子里待过一段时间,大概率会被问到或者问过别人一个问题:“我到底该学哪个框架?”这个问题几乎和“该学Python还是R”一样,属于入门阶段的经典纠结。但有意思的是,问这个问题的人,往往并不是真的需要一份“框架排行榜”,他们真正想知道的是:在什么场景下,用哪个框架最省事、最不容易踩坑、最能让我把精力花在模型本身而不是工具上。
我自己从最早接触Theano和Caffe的时代一路走过来,中间经历了TensorFlow 1.x静态图的折磨,也见证了PyTorch从学术圈小众工具变成工业界主流选择的整个过程。到现在这个时间点,主流深度学习框架的格局其实已经相对清晰了,但“清晰”不等于“简单”——每个框架都有自己的脾气,选错了不是不能用,而是会在某个你意想不到的环节浪费大量时间。
这篇文章不打算给你一个“谁最强”的结论,因为这种结论没有意义。我想做的是把目前主流几个框架——PyTorch、TensorFlow、JAX、PaddlePaddle,以及在某些特定场景下仍然活跃的框架——从设计哲学、实际使用体验、生态成熟度、部署链路这几个维度拆开来讲。核心目标是让你看完之后,能根据自己的项目类型、团队情况、部署目标,做出一个不后悔的选择。不管你是刚入门的学生,还是正在做技术选型的工程师,这些内容都应该能帮你少走一些弯路。
2. PyTorch为什么成了大多数人的默认选项
2.1 动态图不是噱头,是调试效率的质变
PyTorch最大的标签就是“动态计算图”,也叫Define-by-Run。这个概念刚出来的时候,很多人觉得只是写法上的差异,但实际上它带来的是整个开发体验的改变。在静态图框架里,你定义完计算图之后,必须先把图编译好,然后才能喂数据进去跑。这意味着如果你在模型定义中间写了一个条件判断,比如根据某个张量的值决定走哪个分支,静态图是没法直接处理的,你得用特殊的控制流API去绕。
动态图就不一样了,它本质上就是Python代码在逐行执行,你写if就是真的if,你写for就是真的for。这意味着调试的时候你可以直接用Python的断点工具,在任意一行停下来看张量的值。这个看起来很简单的事情,在实际开发中节省的时间是巨大的。我印象很深的一次经历是,当时在做一个序列标注任务,模型中间需要根据上一时刻的输出决定下一步的输入,用静态图框架写的时候,光是搞清楚tf.while_loop的用法就花了大半天,而换成PyTorch之后,直接写个循环就完事了。
当然,动态图也有代价。早期的PyTorch在性能上确实不如静态图框架,因为每次前向传播都要重新构建计算图,有一定的开销。但随着TorchScript和torch.compile的成熟,这个问题已经被大幅缓解了。你可以在开发阶段用纯动态图快速迭代,到了部署阶段再用torch.compile或者导出成TorchScript来获得接近静态图的性能。这种“开发时灵活、部署时优化”的分层策略,是目前最务实的做法。
2.2 生态的滚雪球效应:为什么新模型总是先有PyTorch实现
如果你关注最新的论文,会发现一个很明显的现象:绝大多数新模型的开源实现都是PyTorch版本先出来,TensorFlow版本要么没有,要么滞后很久。这不是偶然的,而是生态滚雪球效应的结果。研究者用PyTorch做实验,因为调试方便;开源的时候自然就放PyTorch代码;后来的人想复现,最省事的方式就是直接用PyTorch;久而久之,PyTorch就成了学术界的通用语言。
这个效应带来的实际好处是,当你想复现某个新模型的时候,大概率能找到现成的PyTorch代码,而且质量通常不错。HuggingFace的Transformers库就是最好的例子,它几乎成了预训练模型的事实标准接口,而它的核心实现就是基于PyTorch的。你想用BERT、GPT、T5这些模型,直接from transformers import ...就完事了,底层是PyTorch还是TensorFlow它都支持,但PyTorch版本的维护通常更及时。
不过这里有一个需要注意的地方:生态好不等于所有东西都该用PyTorch。如果你的团队已经在TensorFlow上投入了大量精力,有成熟的训练流水线和部署方案,那为了追新而全面迁移到PyTorch可能得不偿失。工具是为人服务的,不是反过来。
2.3 从研究到生产的链路:TorchScript和ONNX的实际表现
PyTorch早期被诟病最多的地方就是部署。研究阶段用Python写得好好的,到了线上要集成到C++服务里就傻眼了。TorchScript就是为了解决这个问题而生的,它可以把PyTorch模型序列化成一种中间表示,然后在不依赖Python环境的情况下加载和执行。
实际用下来,TorchScript对大多数标准模型的支持是没问题的,但如果你在模型里用了比较花哨的Python特性,比如动态创建类、复杂的装饰器,转换的时候可能会报错。我的经验是,在写模型的时候就尽量保持“可导出友好”,比如避免在forward里做太复杂的Python逻辑,把配置参数都显式传进去而不是从全局变量里读。这样后面导出的时候会顺利很多。
ONNX是另一个选择,它的好处是跨框架通用,PyTorch训练完可以导出成ONNX,然后用ONNX Runtime或者TensorRT来推理。但ONNX的坑在于算子支持不全,有些PyTorch里的操作ONNX没有对应的实现,导出的时候会失败或者行为不一致。所以如果你打算走ONNX路线,最好在模型设计阶段就查一下目标算子的支持情况,别等到训练完了才发现导不出去。
3. TensorFlow的转身:从被吐槽到重新能打
3.1 Keras的整合到底解决了什么问题
TensorFlow 1.x时代最让人头疼的就是API太乱。你想搭一个简单的全连接网络,得先定义placeholder,再定义变量,再写loss,再写optimizer,最后还要搞session。一套流程下来,代码又长又难读。而且当时有tf.layers、tf.contrib.layers、tf.keras好几套API并存,新手根本分不清该用哪个。
TensorFlow 2.x最大的改变就是把Keras提升为官方推荐的高层API,同时默认开启Eager Execution。这意味着你可以像写PyTorch一样写TensorFlow了,model.fit()一行就能完成训练循环,model.compile()指定优化器和损失函数。对于标准任务来说,开发效率提升非常明显。
但Keras的整合也带来了一些新的困惑。比如你想自定义训练循环的时候,是继续用model.fit()加callbacks,还是自己写tf.GradientTape?两种方式都能用,但适用场景不同。我的建议是:如果标准训练流程能满足需求,就用model.fit(),省事且不容易出错;如果需要做对抗训练、多任务学习、自定义梯度更新这类操作,就老老实实写GradientTape循环。不要为了“看起来高级”而放弃高层API的便利性。
3.2 TFX和TFLite:工业部署的完整拼图
TensorFlow在工业部署方面的积累是它最大的护城河之一。TFX是端到端的机器学习平台,涵盖了数据验证、特征工程、模型训练、模型评估、模型部署整个流水线。如果你在一个需要严格版本管理和可复现性的生产环境里工作,TFX提供的这些组件能帮你省去很多自己造轮子的时间。
TFLite则是面向移动端和嵌入式设备的轻量级推理引擎。它可以把模型量化成8位整数,体积缩小到原来的四分之一,推理速度也能提升两三倍。我做过一个图像分类的项目,原始模型大概100MB,量化之后只有25MB左右,在手机上的推理延迟从200ms降到了60ms左右,效果还是很明显的。
不过TFLite的量化不是没有代价的。训练后量化(Post-training Quantization)虽然简单,但对精度的影响有时候会超出预期,尤其是对那些对数值范围比较敏感的模型。如果精度掉得太多,就得考虑量化感知训练(Quantization-Aware Training),在训练阶段就模拟量化的效果,让模型提前适应。这个流程比训练后量化复杂不少,但精度损失通常能控制在1%以内。
3.3 静态图回归:tf.function的取舍
TensorFlow 2.x虽然默认是Eager模式,但保留了tf.function装饰器,可以把Python函数编译成静态图。这个设计的意图是让你在开发时享受Eager的便利,在部署时通过tf.function获得图执行的性能优势。
实际使用中,tf.function的行为有时候会让人困惑。比如你在被装饰的函数里打印一个张量的值,第一次调用的时候会打印,后面就不打印了,因为图只追踪一次。再比如Python的副作用(比如往列表里append元素)在tf.function里可能不会按预期执行。这些坑在官方文档里都有说明,但新手很容易忽略。
我的经验是:不要一上来就给所有函数加tf.function,先让代码在Eager模式下跑通,确认逻辑没问题之后,再把训练步骤和推理步骤用tf.function包起来。这样既能保证正确性,又能获得性能提升。另外,tf.function的input_signature参数最好显式指定,这样可以避免因为输入形状变化导致重复追踪,影响性能。
4. JAX:为高性能计算而生的新选择
4.1 函数式编程带来的思维转变
JAX的设计哲学和PyTorch、TensorFlow都不一样,它是纯函数式的。这意味着你的函数不能有副作用,同样的输入必须产生同样的输出。这个约束一开始会让人觉得很不习惯,比如你不能在函数里直接修改一个全局变量,也不能用普通的Python列表来累积结果。
但函数式带来的好处是可组合性和可变换性。JAX提供了jit、grad、vmap、pmap这几个核心变换,你可以把它们自由组合。比如jit(grad(f))就得到了一个编译过的高效梯度函数,vmap(grad(f))就得到了一个可以批量计算梯度的函数。这种组合能力在写复杂算法的时候非常强大。
举个例子,你想计算一个函数的二阶导数,在PyTorch里你得用torch.autograd.grad嵌套调用,还要处理create_graph=True的问题。在JAX里就是grad(grad(f)),一行搞定。再比如你想把一个只支持单样本的函数变成支持批量的,PyTorch里你得手动加batch维度,JAX里用vmap自动帮你处理。
4.2 jit、vmap、pmap:三个核心变换的实际用途
jit是最常用的变换,它把Python函数编译成XLA(Accelerated Linear Algebra)可以执行的代码。XLA会做算子融合、内存优化等操作,通常能带来明显的性能提升。但jit也不是万能的,如果你的函数里有依赖数据的控制流(比如根据张量的值决定循环次数),jit要么报错,要么需要你用jax.lax.cond和jax.lax.while_loop来重写。这是JAX使用中最常见的坑之一。
vmap是自动向量化,它可以把一个处理单样本的函数自动变成处理批量的函数。这个在写损失函数的时候特别有用,你只需要关心单个样本怎么算,批量的事情交给vmap。而且vmap和jit可以组合使用,jit(vmap(f))会生成一个既批量又编译的高效函数。
pmap是跨设备并行,它可以把计算自动分配到多个GPU或者TPU上。JAX在TPU上的表现尤其出色,因为XLA本身就是为TPU设计的。如果你有TPU资源,JAX几乎是唯一的选择。但pmap的使用也有门槛,你需要理解SPMD(Single Program Multiple Data)的编程模型,知道怎么用jax.lax.pmean、jax.lax.psum这些集合通信操作。
4.3 生态短板:为什么JAX还不是所有人的首选
JAX在性能和研究灵活性上确实很强,但它的生态成熟度目前还比不上PyTorch和TensorFlow。最明显的差距在数据加载和预处理方面,PyTorch有torchvision、torchaudio、torchtext这些成熟的库,TensorFlow有tf.data,而JAX这边虽然有一些第三方库,但成熟度和文档完善度都还有差距。
另一个问题是预训练模型的丰富程度。HuggingFace的Transformers库虽然也支持JAX,但很多模型的JAX版本并没有经过充分测试,用的时候可能会遇到各种奇怪的问题。如果你打算用JAX做NLP任务,最好做好自己写模型加载和权重转换的准备。
所以我的看法是:JAX适合那些对性能有极致追求、团队有较强工程能力、愿意在生态不完善的地方自己动手的场景。如果你只是想快速做一个标准任务,PyTorch或者TensorFlow会让你省心很多。
5. PaddlePaddle和其他框架的生存空间
5.1 国产框架的差异化路线
PaddlePaddle(飞桨)是国内起步最早、生态最完整的深度学习框架之一。它的定位和PyTorch、TensorFlow不太一样,更强调产业落地和全流程支持。从数据标注工具、模型训练、模型压缩到端侧部署,PaddlePaddle提供了一整套工具链。
在实际使用中,PaddlePaddle的高层API(PaddleNLP、PaddleVision等)对中文任务的支持确实比较友好。比如PaddleNLP里内置了很多中文预训练模型,加载和微调的流程都做了封装,对于做中文NLP的开发者来说省去了不少配置工作。另外PaddlePaddle在国产硬件适配方面也做得比较早,如果你有特定的硬件部署需求,它可能是一个值得考虑的选择。
但PaddlePaddle的国际化程度相对有限,英文文档和社区讨论的丰富度不如PyTorch和TensorFlow。如果你需要频繁查阅英文资料或者参与国际开源项目,可能会觉得有些不便。
5.2 特定场景下的“老框架”仍然在服役
除了上面几个主流框架,还有一些在特定场景下仍然活跃的框架。比如Caffe,虽然已经很久没有大更新了,但在一些嵌入式视觉项目里还能见到,因为它的模型文件格式简单、推理速度快、依赖少。再比如MXNet,虽然Apache已经停止了主要开发,但亚马逊内部和一些遗留系统还在用。
这些框架的共同特点是:它们解决的是特定历史阶段的问题,在当时的场景下是最优解,但随着技术发展,通用性和生态丰富度被后来的框架超越了。如果你接手了一个老项目,发现用的是这些框架,不要急着迁移,先评估一下维护成本和迁移风险。有时候在原有框架上打补丁比全面重写更划算。
5.3 框架选型的决策清单
说了这么多,最后落到实际操作上,怎么选?我整理了一个决策清单,你可以根据自己的情况逐条对照:
| 考量维度 | 优先选PyTorch | 优先选TensorFlow | 优先选JAX | 优先选PaddlePaddle |
|---|---|---|---|---|
| 主要用途 | 学术研究、快速原型 | 工业部署、移动端 | 高性能计算、TPU | 中文NLP、国产硬件 |
| 团队背景 | Python熟练、喜欢灵活 | 有TF1.x经验、需要TFX | 函数式编程经验 | 国内团队、中文任务 |
| 部署目标 | 服务器、ONNX | TFLite、TF Serving | TPU、自定义硬件 | 端侧、国产芯片 |
| 生态需求 | 最新模型、HuggingFace | 成熟流水线、TFX | 自己动手、性能优先 | 中文预训练模型 |
| 学习成本 | 低 | 中 | 高 | 中 |
这个表不是绝对的,只是一个参考框架。实际选型的时候,最重要的还是看你的具体任务和团队情况。比如你团队里所有人都是PyTorch老手,那除非有非常强的理由,否则没必要换。反过来,如果你要做一个需要部署到大量安卓设备上的应用,TensorFlow Lite的成熟度可能比PyTorch Mobile更有优势。
6. 跨框架迁移和混合使用的实战经验
6.1 模型权重转换的常见坑
有时候你不得不做跨框架的事情,比如用PyTorch训练,但部署环境只支持TensorFlow。这时候就需要做权重转换。最常见的方式是通过ONNX作为中间格式:PyTorch导出ONNX,然后TensorFlow用onnx-tf导入。但这个过程坑很多。
第一个坑是算子命名不一致。PyTorch里的Conv2d在ONNX里叫Conv,在TensorFlow里叫Conv2D,转换工具会帮你映射,但有些冷门算子可能没有对应的映射规则。第二个坑是维度顺序。PyTorch默认是NCHW,TensorFlow默认是NHWC,转换的时候需要做transpose,如果转换工具没处理好,推理结果就会完全错误。第三个坑是动态维度。如果你的模型支持变长输入,ONNX导出的时候需要显式指定动态维度,否则转换出来的模型只能处理固定形状的输入。
我的建议是:转换完之后一定要做数值验证。用同一批输入分别跑原始模型和转换后的模型,逐层对比输出,确保误差在可接受范围内。不要只看最后的结果对不对,中间层的偏差可能会在后续层被放大。
6.2 用ONNX作为中间层的利弊
ONNX的好处是提供了一个统一的模型表示,让不同框架之间可以互相转换。但ONNX本身也在演进,不同版本支持的算子集不一样。你用的PyTorch版本导出的ONNX可能是opset 13,但目标推理引擎只支持到opset 11,这时候就得降级导出,而降级可能会导致某些算子不被支持。
另外ONNX对控制流的支持一直是个弱项。虽然从opset 11开始引入了If和Loop算子,但用起来还是比较别扭。如果你的模型里有复杂的控制流,导出ONNX可能会失败,或者导出后的行为跟原始模型不一致。
所以我的经验是:如果只是做推理部署,ONNX是值得尝试的;但如果你的模型需要频繁修改,或者控制流比较复杂,直接用一个框架从头到尾会更省心。跨框架的维护成本往往比想象中高。
6.3 多框架并存的团队协作建议
在一些比较大的团队里,不同组可能用不同的框架。比如研究组用PyTorch,工程组用TensorFlow。这种情况下,最重要的是约定好交接标准。模型导出成什么格式、输入输出的形状和类型是什么、预处理和后处理的逻辑在哪里实现,这些都要提前说清楚。
我见过太多因为交接不清晰导致的问题:研究组说模型输入是[0, 1]范围的浮点数,工程组以为是[0, 255]的整数;研究组说输出是logits,工程组直接当概率用了。这些问题看起来很低级,但在实际协作中非常常见。
一个实用的做法是写一个参考实现,用最简单的代码(哪怕性能很差)把整个流程跑通,包括预处理、推理、后处理。这个参考实现作为“黄金标准”,任何框架的实现都要跟它对齐。这样即使换了框架,只要输出跟参考实现一致,就能保证正确性。
7. 我个人的一些使用体会
说了这么多框架的对比和分析,最后分享几点我自己的真实感受。第一个感受是,框架之间的差距在缩小。早些年PyTorch和TensorFlow的体验差异很大,选错了会很痛苦。但现在两者都在互相借鉴,PyTorch有了torch.compile,TensorFlow有了Eager Execution,基本的开发体验已经趋同了。所以如果你现在才开始学,选哪个都不会太错,重要的是先把一个用熟。
第二个感受是,不要被“最新最好”的心态绑架。每次有新框架或者新版本出来,总有人急着迁移,但迁移是有成本的。如果你的现有方案能解决问题,而且没有明显的性能瓶颈,那就没必要为了追新而折腾。我见过一个团队,两年内换了三次框架,每次都是从头开始,结果项目进度严重滞后。工具是拿来用的,不是拿来炫耀的。
第三个感受是,底层原理比框架API更重要。框架的API会变,今天学的model.fit()明天可能就换了名字,但反向传播的原理、优化器的数学、正则化的作用,这些东西是不变的。把时间花在理解原理上,比花在记忆API上回报率高得多。你理解了原理,换任何框架都能快速上手;你只记了API,换个版本可能就懵了。
最后一个建议是,动手写。看再多的对比文章,不如自己动手跑一个完整的项目。从数据加载、模型定义、训练循环到推理部署,走一遍完整的流程,你会对每个框架的优缺点有切身的体会。这种体会比任何文章都来得真实。