很多刚开始接触深度学习框架的读者,都会在 PyTorch 和 TensorFlow 之间纠结很久。网上关于“到底选哪一个”的争论也特别多,有人说 PyTorch 更适合研究,代码写起来像原生 Python;也有人说 TensorFlow 生产部署链路完善,工业界用得稳。结果新人越看越焦虑,甚至还没开始写模型,就先被选型问题卡住了。
这篇文章不打算帮你“二选一”。我反而想和你分享一条更务实的路线:把 PyTorch 和 TensorFlow 都装起来,都跑一遍,然后从张量操作、自动求导、训练循环、模型保存这几个维度去对照理解。当你真正同时用过两个框架之后,会发现它们之间的 API 差异只是表面,底层的数据流思路、损失函数设计、梯度更新逻辑几乎是一致的。这个时候,选型就不再是一个信仰问题,而是一个根据业务场景做判断的工程问题。
文章会从核心概念对比开始,接着给出完整的 Anaconda 环境搭建方案,再分别用 PyTorch 和 TensorFlow 实现一个手写数字识别案例,最后整理高频报错的排查思路和工程化建议。内容偏实操,所有代码我都会标注清楚,你可以直接复制运行。
1. 深度学习框架二选一?成年人选择都要
1.1 PyTorch 与 TensorFlow 各自解决了什么问题
先看 PyTorch。PyTorch 由 Facebook AI Research 团队开发,它的核心设计理念是“命令式优先”,也就是动态计算图。什么叫动态计算图?简单来说,你在写模型的时候,每一行张量运算都会立即执行,遇到问题可以直接用 print 打印中间结果,调试体验非常接近普通 Python 程序。这种特性让它在学术研究、快速原型验证、论文复现等场景里特别受欢迎。我们经常看到的 Transformer、Stable Diffusion、LLaMA 等开源模型的官方实现,很多都基于 PyTorch。
再看 TensorFlow。TensorFlow 由 Google 团队开发,早期以静态计算图为主,先定义整张计算图,再送入会话执行。这种设计在分布式训练和线上部署方面有天然优势,因为图结构一旦确定,就可以做大量编译优化。不过静态图也让很多初学者觉得难上手,所以 TensorFlow 2.x 之后默认开启了 Eager Execution,也就是动态执行模式,同时保留了 tf.function 和 SavedModel 这类面向生产环境的能力。再叠加 Keras 高层 API,TensorFlow 现在也能让新手用十几行代码快速训练一个模型。
所以要回答“谁更好”这个问题,其实没有标准答案。PyTorch 在研究和灵活调试上有优势,TensorFlow 在端到端生产链路和旧系统维护上有积累。两者都不是银弹,但两者都值得你掌握基础用法。
1.2 为什么建议两个框架都掌握
第一,开源社区和论文生态不会只围绕某一个框架转。你读论文时会发现,有的作者开源 PyTorch 代码,有的作者开源 TensorFlow 代码。如果你只会其中一个,遇到另一个框架的优质项目就只能干瞪眼。
第二,实际工作中你可能会同时维护多个项目。很多公司早几年用 TensorFlow 搭建了推荐系统或 CV 服务,现在新项目又切到 PyTorch,作为开发人员,你不能要求公司为了你的技术偏好把老项目全部重写。这时候“两个都会”就是刚需。
第三,框架之间的迁移能力本身就是一种核心竞争力。当你理解了张量、自动求导、优化器、数据管道这些概念之后,从一个框架切到另一个框架,只是换一套 API 的问题,核心思想是可以平移的。下面我们就先来拆解这些核心概念。
2. 三大核心概念对比:张量、动态图与自动求导
2.1 张量:两个框架的共同基石
不管 PyTorch 还是 TensorFlow,最基础的数据结构都是张量。你可以把张量理解成“可以放到 GPU 上加速计算的 N 维数组”。0 维张量是标量,1 维张量是向量,2 维张量是矩阵,3 维及以上通常代表批量数据或序列数据。
PyTorch 中张量类型是torch.Tensor,TensorFlow 中张量类型是tf.Tensor。先看一段创建张量的对比代码:
# 文件路径:tensor_demo.py import torch import tensorflow as tf # PyTorch 创建张量 a_torch = torch.tensor([[1.0, 2.0], [3.0, 4.0]]) print("PyTorch 张量:\n", a_torch) print("设备:", a_torch.device) # TensorFlow 创建张量 a_tf = tf.constant([[1.0, 2.0], [3.0, 4.0]]) print("\nTensorFlow 张量:\n", a_tf) print("设备:", a_tf.device)这里有个容易混淆的点:PyTorch 用torch.tensor创建张量,TensorFlow 用tf.constant创建常量张量。两者都支持shape、dtype、device这些属性。你还会看到 PyTorch 经常提到.to(device),TensorFlow 则通过tf.device或者自动分配策略管理设备。
2.2 动态图与静态图:调试体验差异的来源
PyTorch 默认就是动态图模式。它每执行一行代码就计算一行,所以你可以非常自然地在训练循环里打断点、打印中间张量。这也是为什么很多新手觉得 PyTorch 的代码“读起来像普通程序”。
TensorFlow 2.x 默认也是 Eager 模式,也就是动态执行。但 TensorFlow 完整保留了静态图能力,你可以用@tf.function装饰器把一段 Python 函数编译成静态计算图,从而获得更好的执行性能。这种“既能动态调试,又能静态加速”的设计是 TensorFlow 的特色。
有一点需要提醒:不要看到@tf.function就盲目给所有函数加上。静态图编译对代码有约束,比如不能随意使用 Python 的if和while依赖动态张量值,否则很容易踩坑。刚开始学习时,直接用默认的 Eager 模式就够了。
2.3 自动求导:训练模型的发动机
深度学习的核心是反向传播,而反向传播依赖自动求导。
PyTorch 的自动求导基于autograd。默认情况下,只有requires_grad=True的张量会记录梯度。每次调用loss.backward()后,梯度会累积到对应张量的.grad属性中。TensorFlow 的自动求导则通过tf.GradientTape()实现,在with上下文中记录前向计算过程,退出上下文后调用tape.gradient(loss, model.trainable_variables)拿到梯度。
简单对比一下:
# 文件路径:autograd_demo.py import torch import tensorflow as tf # PyTorch 自动求导 x = torch.tensor(3.0, requires_grad=True) y = x ** 2 y.backward() print("PyTorch 梯度:", x.grad) # TensorFlow 自动求导 x_tf = tf.Variable(3.0) with tf.GradientTape() as tape: y_tf = x_tf ** 2 grad = tape.gradient(y_tf, x_tf) print("TensorFlow 梯度:", grad.numpy())这里可以明显看出两套 API 的差异:PyTorch 关注“张量是否记录梯度”,TensorFlow 关注“用上下文管理器捕获哪些计算”。但本质都是链式法则的实现,你只需要理解一次,两边都能迁移。
3. 环境准备:从 Anaconda 到双框架安装
3.1 安装 Anaconda 并创建虚拟环境
推荐使用 Anaconda 管理 Python 环境,因为它能很方便地创建独立虚拟环境,避免不同项目之间的包冲突。
打开终端,输入下面的命令创建深度学习基础环境:
conda create -n dl python=3.10 -y conda activate dl这里我用了 Python 3.10,具体版本可以根据你的项目要求调整。如果项目依赖较老,可能需要 Python 3.8 或 3.9;如果跑最新的 Transformer 代码,可以考虑 3.10 或 3.11。注意,不同版本对依赖包的兼容性不同,没有绝对最优,按实际需求来。
3.2 PyTorch 环境搭建与 GPU 安装思路
安装 PyTorch 最准确的方式是打开 PyTorch 官网,选择你的操作系统、包管理工具和 CUDA 版本,官网会生成对应的安装命令。我把 CPU 版和 GPU 版都列一下,方便你对比:
# CPU 版 pip install torch torchvision torchaudio # GPU 版(示例命令,具体 CUDA 版本以官网为准) pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121安装 GPU 版之前,强烈建议先用nvidia-smi查看显卡驱动支持的最高 CUDA 版本,再根据 PyTorch 官方安装矩阵选择匹配的 CUDA 版本号。如果驱动版本较老,强行装新版 CUDA 依赖可能会在运行时报“找不到 cudart64_*.dll”或“CUDA driver version is insufficient”之类的错误。
如果你在国内网络环境下安装速度很慢,可以使用清华 PyPI 镜像:
pip install torch torchvision torchaudio -i https://pypi.tuna.tsinghua.edu.cn/simple安装完成后,用下面这段代码验证 PyTorch 是否能正常调用 GPU:
import torch print(torch.__version__) print(torch.cuda.is_available())如果torch.cuda.is_available()输出 True,说明 GPU 环境可用。
3.3 TensorFlow 环境搭建与版本差异
TensorFlow 的安装命令相对简单,CPU 版和 GPU 版在 2.x 中通常使用同一个包:
pip install tensorflowTensorFlow 在 Windows 上的 GPU 支持需要注意版本匹配。比如 TensorFlow 2.10 是最后一个原生支持 Windows GPU 的常规版本,之后的 Windows GPU 支持方案发生了变化。如果你用的是较新的 TensorFlow 2.16 或更高版本,建议先查看官方安装文档,确认 CUDA 和 cuDNN 的版本要求。为了避免和 PyTorch 的依赖冲突,我强烈建议你为 TensorFlow 单独创建一个虚拟环境:
conda create -n tf python=3.10 -y conda activate tf pip install tensorflow创建一个独立环境并不是小题大做。PyTorch 和 TensorFlow 都依赖一些底层库,比如numpy、protobuf、absl-py,两者对版本的要求不完全一致。放在同一个环境里,装完一个再装另一个,很容易出现“装好了 A,B 却启动报错”的情况。用虚拟环境隔离是最省心的方案。
3.4 两个框架装在同一台机器上的协调方案
如果你电脑上只有一个 GPU,也不需要同时在一段代码里导入两个框架,那么分开建两个虚拟环境是最稳妥的。日常使用时,先conda activate dl就进入 PyTorch 环境,再conda activate tf就切到 TensorFlow 环境。两个环境的 pip 包互不干扰,这是我在多台机器上验证过最推荐的方案。
如果你确实需要在同一个 Python 进程中同时使用两个框架,也不是完全不可能,但你需要非常小心依赖版本冲突。实际工程里这种需求并不多见,通常只有做框架对比实验或者写迁移工具时才会遇到。因此,不要为了省一点磁盘空间强行合并环境。
4. 实战案例:用 PyTorch 完成手写数字识别
环境准备好之后,我们来跑第一个完整案例。手写数字识别是深度学习的“Hello World”,数据集是 MNIST,包含 60000 张训练图片和 10000 张测试图片,每张图片是 28×28 的灰度图。
4.1 数据准备与 DataLoader
PyTorch 中,我们通常使用torchvision下载 MNIST,并用DataLoader把数据打包成 batch。完整代码如下:
# 文件路径:mnist_pytorch.py import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader from torchvision import datasets, transforms # 1. 数据预处理:转张量 + 归一化 transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) # 2. 下载并加载 MNIST 数据集 train_dataset = datasets.MNIST( root='./data', train=True, transform=transform, download=True ) test_dataset = datasets.MNIST( root='./data', train=False, transform=transform, download=True ) train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True) test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False) print(f"训练集大小: {len(train_dataset)}") print(f"测试集大小: {len(test_dataset)}") print(f"每个 batch 大小: {64}")这里有两个关键点。第一,ToTensor()会把 PIL 图片转换成 0 到 1 之间的张量,然后再用Normalize减去均值 0.1307、除以标准差 0.3081,这是 MNIST 数据集的常用标准化参数。第二,DataLoader会自动把数据切分成 batch,训练时shuffle=True能打乱样本顺序,避免模型学习到样本顺序带来的偏差。
4.2 定义神经网络与训练流程
MNIST 图片是 28×28 的灰度图,我们可以先用一个简单的全连接网络来理解训练流程。这里我把图片拉平成 784 维向量,经过两个隐藏层,最后输出 10 个类别的分数。
完整代码如下:
# 文件路径:mnist_pytorch.py(继续) # 3. 定义简单的全连接神经网络 class SimpleNN(nn.Module): def __init__(self): super(SimpleNN, self).__init__() self.fc1 = nn.Linear(28 * 28, 256) self.fc2 = nn.Linear(256, 128) self.fc3 = nn.Linear(128, 10) self.relu = nn.ReLU() def forward(self, x): x = x.view(x.size(0), -1) # 展平图片 x = self.relu(self.fc1(x)) x = self.relu(self.fc2(x)) x = self.fc3(x) return x # 4. 初始化模型、损失函数、优化器 device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = SimpleNN().to(device) criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=0.001) # 5. 训练循环 epochs = 5 for epoch in range(epochs): model.train() running_loss = 0.0 for images, labels in 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() avg_loss = running_loss / len(train_loader) print(f"Epoch [{epoch + 1}/{epochs}], Loss: {avg_loss:.4f}") # 6. 测试集评估 model.eval() correct = 0 total = 0 with torch.no_grad(): for images, labels in test_loader: images, labels = images.to(device), labels.to(device) outputs = model(images) _, predicted = torch.max(outputs, 1) total += labels.size(0) correct += (predicted == labels).sum().item() print(f"测试集准确率: {100 * correct / total:.2f}%")这段代码里有几个值得强调的习惯:
- 训练前调用
model.train(),测试前调用model.eval()。这会切换 BatchNorm 和 Dropout 等层的行为。 optimizer.zero_grad()必须放在每次反向传播之前,否则梯度会累加。- 测试时用
torch.no_grad()包裹,表示不需要计算梯度,能减少内存占用和加速推理。
4.3 运行结果说明
正常情况下,训练 5 个 epoch 后,测试集准确率可以达到 97% 左右。如果使用 CNN 和更多训练轮次,准确率可以轻松超过 99%。输出大概是这样:
Epoch [1/5], Loss: 0.3241 Epoch [2/5], Loss: 0.1453 Epoch [3/5], Loss: 0.1037 Epoch [4/5], Loss: 0.0785 Epoch [5/5], Loss: 0.0622 测试集准确率: 97.32%如果你在自己机器上跑出来的准确率略低或略高,都很正常,因为初始化权重具有随机性。只要损失在逐步下降,说明模型正在正常学习。
5. 实战案例:用 TensorFlow/Keras 完成手写数字识别
接下来我们用 TensorFlow 再实现一次完全相同的任务。你会发现 TensorFlow 的 Keras 高层 API 写起来更加“声明式”,很多流程都被封装好了。
5.1 加载与预处理数据
TensorFlow 内置了 MNIST 数据集,不需要额外下载脚本:
# 文件路径:mnist_tensorflow.py import tensorflow as tf from tensorflow.keras import layers, models # 1. 加载 MNIST 数据 (x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data() # 2. 归一化到 [0, 1] x_train = x_train.astype("float32") / 255.0 x_test = x_test.astype("float32") / 255.0 # 3. 展平 28x28 图片为 784 维向量 x_train = x_train.reshape(-1, 28 * 28) x_test = x_test.reshape(-1, 28 * 28) print(f"训练集形状: {x_train.shape}") print(f"测试集形状: {x_test.shape}")注意 TensorFlow 的tf.keras.datasets.mnist.load_data()返回的是 NumPy 数组,不需要显式转换成张量,模型训练时会自动转换。
5.2 构建模型并训练
使用 Keras Sequential 模型,结构与 PyTorch 示例保持一致:输入 784 维,两个隐藏层分别是 256 和 128,输出 10 个类别。
# 文件路径:mnist_tensorflow.py(继续) # 4. 构建模型 model = models.Sequential([ layers.Dense(256, activation="relu", input_shape=(784,)), layers.Dense(128, activation="relu"), layers.Dense(10, activation="softmax") ]) # 5. 编译模型 model.compile( optimizer="adam", loss="sparse_categorical_crossentropy", metrics=["accuracy"] ) # 6. 训练模型 history = model.fit( x_train, y_train, batch_size=64, epochs=5, validation_data=(x_test, y_test) ) # 7. 测试集评估 test_loss, test_acc = model.evaluate(x_test, y_test, verbose=0) print(f"测试集准确率: {test_acc * 100:.2f}%")这里有一个容易让新手混淆的地方:loss="sparse_categorical_crossentropy"和loss="categorical_crossentropy"有什么区别?简单来说,如果你的标签是整数形式,比如 0、1、2、3……,就使用sparse_categorical_crossentropy;如果你的标签是 One-Hot 编码,比如[0, 0, 1, 0],就使用categorical_crossentropy。MNIST 默认标签是整数,所以要用sparse版本。
5.3 模型结构与训练输出
model.fit()会在每个 epoch 结束后打印训练损失、训练准确率、验证损失和验证准确率:
Epoch 1/5 938/938 [==============================] - 3s 3ms/step - loss: 0.2205 - accuracy: 0.9349 - val_loss: 0.1070 - val_accuracy: 0.9672 Epoch 2/5 938/938 [==============================] - 3s 3ms/step - loss: 0.0867 - accuracy: 0.9733 - val_loss: 0.0768 - val_accuracy: 0.9761 ... 测试集准确率: 97.50%938/938表示每个 epoch 有 938 个 batch。因为训练集有 60000 张图片,除以 batch_size 64 得到的 batch 数量就是 938 左右。
6. 双框架迁移思路与高频坑位排查
6.1 从 PyTorch 到 TensorFlow 如何迁移
当你明白一个训练流程包含“数据加载、模型定义、损失函数、优化器、训练循环”五个部分后,两个框架的映射关系就很清晰了。我把对应关系整理成一张表:
| 功能模块 | PyTorch | TensorFlow / Keras |
|---|---|---|
| 数据加载 | DataLoader+Dataset | tf.data.Dataset或 NumPy 数组 |
| 模型定义 | 继承nn.Module实现forward | Sequential或继承keras.Model |
| 损失函数 | nn.CrossEntropyLoss等 | losses.SparseCategoricalCrossentropy等 |
| 优化器 | optim.Adam | optimizers.Adam |
| 训练循环 | 手动写for循环 | model.fit()或自定义train_step |
| 梯度计算 | loss.backward() | tf.GradientTape |
PyTorch 的优势是流程透明,每一步都手动控制,适合学习和调试;TensorFlow/Keras 的优势是封装度高,你用几行代码就能完成同样的训练,适合快速搭建和工程部署。两者没有绝对的优劣,理解底层机制之后,切换成本会很低。
6.2 常见问题排查表
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
torch.cuda.is_available()返回 False | PyTorch 安装了 CPU 版,或 CUDA 版本不匹配 | 重新根据官网选择对应 CUDA 版本安装 GPU 版 |
| TensorFlow 找不到 GPU | CUDA/cuDNN 版本与 TensorFlow 不匹配 | 查看官方“Software requirements”文档,确认驱动和 CUDA 版本 |
运行时报CUDA driver version is insufficient | 显卡驱动过旧 | 升级显卡驱动,或回退到低版本 CUDA 的框架包 |
| pip 安装超时 | 网络不稳定 | 使用国内镜像源,比如清华、阿里云镜像 |
| 同时安装两个框架后 import 报错 | protobuf、numpy 等底层依赖冲突 | 拆分成独立 conda 虚拟环境,不要混装 |
PyTorch 加载模型报WeightsOnlyUnpickler错误 | PyTorch 2.6 后torch.load默认weights_only=True | 保存模型时改用state_dict,加载时用load_state_dict;或显式设置weights_only=False |
这里我想单独展开说一下 PyTorch 2.6 的weights_only变化。从前很多教程习惯用torch.save(model, "model.pt")保存整个模型对象,再用torch.load("model.pt")直接加载。但在 PyTorch 2.6 中,官方把torch.load的weights_only参数默认值改成了True,这意味着加载时必须更多限制,原本包含自定义类对象的 checkpoint 可能报错。更稳妥的做法永远是只保存模型参数:
# 保存 torch.save(model.state_dict(), "model_weights.pt") # 加载 model = SimpleNN() model.load_state_dict(torch.load("model_weights.pt")) model.eval()如果你一定要兼容旧代码,需要显式设置weights_only=False,但要了解这会引入潜在的安全风险。因此,在处理训练好的模型时,尽量遵循官方推荐的新方式。
6.3 项目中的选型判断清单
如果你负责一个新项目,可以从下面几个维度判断:
- 研发团队对哪个框架更熟悉。框架只是工具,团队能快速交付更重要。
- 项目是否需要快速原型验证、频繁改模型结构。如果是,PyTorch 的调试体验更好。
- 项目是否要部署到 TF Serving、Android 或旧版生产系统。如果是,TensorFlow 的生态更成熟。
- 项目是否需要复现最新论文方法。大多数最新论文开源代码是 PyTorch。
- 项目是否已经有大量历史代码资产。不要为了追新而忽略维护成本。
在团队没有明确技术栈约束时,我一般建议:科研实验、算法验证优先考虑 PyTorch;端到端生产服务、已有 TensorFlow 体系优先考虑 TensorFlow。两边都装了,两边都会写,这才是“我全都要”的正确姿势。
7. 学习路线与工程化最佳实践
7.1 新手进阶路线
如果你刚开始学,不要一上来就追各种花哨的模型。建议按下面这条主线走:
- 掌握张量操作,包括创建、切片、广播、矩阵乘法。
- 掌握自动求导和梯度更新,理解反向传播的代码表达。
- 用全连接网络跑通 MNIST 或 CIFAR-10。
- 学习卷积神经网络,理解卷积核、池化、感受野。
- 学习循环神经网络或 Transformer,理解序列建模。
- 尝试目标检测、图像分割、文本分类等实际任务。
每到一个阶段,最好都用 PyTorch 和 TensorFlow 各实现一遍。同一份模型结构,两个框架各写一次,你对框架的理解会立刻上升到“迁移”层面。
7.2 工程化建议
开发调试阶段可以随意探索,但一旦要进入项目交付,有几个习惯值得养成:
- 锁版本。用
pip freeze > requirements.txt锁住当前环境依赖,避免未来升级导致结果不可复现。 - 数据管道独立。不要把数据下载、预处理和模型训练耦合在同一个脚本里,尽量拆成可复用的模块。
- 定期保存 checkpoint。训练时间较长时,建议每个 epoch 保存一次权重,方便中断后从断点恢复。
- 日志要结构化。除了打印 loss,还要记录学习率、当前 epoch、GPU 显存占用、数据加载耗时等信息,方便定位瓶颈。
- 不要在训练代码里混入测试代码。用
model.train()和model.eval()明确区分模式,避免 BatchNorm 和 Dropout 行为异常。 - GPU 资源要做好监控。训练大模型时,显存溢出是常态,可以先降低 batch_size 验证代码逻辑,再逐步增大。
7.3 最后的建议
框架之争在深度学习领域会一直存在,但真正能提升你竞争力的,不是“我选对了框架”,而是“我能根据问题选择合适的工具”。如果你刚开始入门,就按照这篇文章的路线,把 PyTorch 和 TensorFlow 都装上,都跑一遍 MNIST。当你发现两套代码只是在 API 层面不同,底层逻辑完全相通时,你就已经跨过了新手期最大的门槛。
如果这篇文章对你有帮助,建议先收藏备用。遇到安装报错或者训练异常时,可以对照本文第 6 章的表格快速排查。后续我还会更新更多关于模型部署、Transformer 实战和 GPU 调优的内容,欢迎持续关注。