首页 / 资讯中心 / 文章详情

PyTorch实战核心指南:动态计算图、模型训练与工程优化

PyTorch实战核心指南:动态计算图、模型训练与工程优化 ★ FEATURED ARTICLE
写代码这几年深度学习框架换了一波又一波最后真正让我觉得“用起来顺手、debug不闹心、生产也扛得住”的还是PyTorch。不管是刚入门的同学还是已经在跑实验的工程师只要碰过深度学习大概率绕不开它。这篇就来聊聊我实际使用PyTorch的经验从核心设计思路到踩坑记录一次性讲透。PyTorch能做什么一句话概括它是目前最主流的深度学习框架之一用来搭建、训练、部署神经网络模型。图像分类、目标检测、自然语言处理、推荐系统、强化学习几乎都能在PyTorch生态里找到成熟方案。适合谁看刚接触深度学习的新手、正在做课程设计或比赛的学生、需要把模型落地到业务中的算法工程师都能从这篇文章里拿走一些有用的东西。1. 上手PyTorch的第一道坎动态计算图到底在说什么很多人第一次接触PyTorch最困惑的概念就是“动态计算图”。我在带新人入门的时候发现如果这个点没想明白后面写模型永远都是靠复制粘贴。1.1 静态图与动态图的本质区别先说说TensorFlow早期的静态图模式。静态图的逻辑是你先定义好整个计算流程画好一张图然后把数据“喂”进去执行。好处是图结构固定部署时可以做很多编译优化性能上限确实高坏处是调试非常痛苦你没法在中间某个节点打印一下变量看看值是多少因为执行顺序和图定义是分离的。PyTorch走的是另一条路——动态图。它的思路是“定义即执行”你写一行x torch.matmul(a, b)这行代码马上就真的计算了并且同时把操作记录到计算图里。图的构建是随着代码运行逐步进行的所以你在中间任何一个位置都可以打印、断点、修改逻辑。用生活类比来理解静态图像提前画好一张完整的地图按固定路线走中途不能改道动态图像开车用导航每走一段路就能根据实时路况调整方向灵活得多。1.2 动态图给调试和实验带来的实际便利这个特性对科研和实验场景太重要了。我在调试模型的时候经常在loss计算后直接加一行print(loss_grad)或者用pdb在任意位置打断点查看中间张量的shape和数值。这在动态图下是自然而然的事情因为Python的执行栈就在那里你可以像调试普通Python程序一样调试神经网络。另外动态图让“条件分支”变得极其简单。比如在实现某些需要根据输入长度变化网络结构的模型时直接在forward函数里写if/else就行完全不用担心图结构不匹配的问题。而静态图框架遇到这种情况需要借助专门的控制流API写法绕且难排错。注意PyTorch 2.0之后引入了torch.compile可以把你写的动态图模型自动编译优化成静态图执行性能和部署体验都上了大台阶。这说明动态图和静态图的边界正在模糊但PyTorch的核心理念——Python原生体验优先——没有变。2. 核心机制拆解三个你必须吃透的模块PyTorch之所以好用不是因为某个单一特性而是几个核心机制设计得足够优雅。这三个模块我认为是重中之重张量与自动求导、nn.Module体系、数据加载管线。2.1 张量、自动求导与反向传播的精髓张量Tensor是PyTorch的基本数据结构可以理解成“支持GPU加速且带梯度记录的NumPy数组”。它和NumPy数组最大的区别在于PyTorch张量可以追踪自身参与的所有运算并自动计算梯度。自动求导autograd是PyTorch的灵魂。核心机制是每个张量都有requires_grad属性当你在requires_gradTrue的张量上做运算时PyTorch会自动构建计算图并在调用loss.backward()时通过链式法则从输出端逐层向后计算每个参数的梯度。我在讲这个机制时喜欢让新人做一个实验定义x torch.tensor([2.0], requires_gradTrue)然后计算y x**2 3*x 1再执行y.backward()最后看一下x.grad。结果是2*x 3 7这就是微积分里求导的答案。你会直观地明白——所谓“深度学习框架帮你自动求导”本质上就是把链式法则这个机械过程程序化而backward()这个函数就是触发整个反向传播的开关。2.2 nn.Module体系与nn.Sequential的正确使用姿势nn.Module是所有神经网络层的基类。你自定义的任何网络都必须继承它并在__init__中定义子层在forward中定义前向计算逻辑。继承nn.Module带来的底层好处是它会自动收集你定义的所有子模块的参数parameters并统一管理它们的设备转移.to(device)、训练/评估切换.train()/.eval()、状态保存与加载。我见过很多新手踩坑把网络里的层定义成普通的nn.Linear但没把它们赋值给self属性结果训练时参数完全不更新。原因就是nn.Module的__init__里有元编程逻辑只有赋值给self.xxx的子模块才会被注册到参数列表里。nn.Sequential适合层与层之间没有复杂分支的线性堆叠结构比如简单卷积网络、MLP。写法直观import torch.nn as nn model nn.Sequential( nn.Linear(784, 256), nn.ReLU(), nn.Dropout(0.2), nn.Linear(256, 10) )但有条件分支、多输入输出、残差连接的网络结构老老实实自己写forward别硬套Sequential否则代码会越来越拧巴。2.3 Dataset与DataLoader的数据流水线设计数据加载是工程上最容易被忽视的环节。PyTorch的标准做法是自定义一个继承torch.utils.data.Dataset的类实现__len__和__getitem__两个方法然后用DataLoader包装输出。from torch.utils.data import Dataset, DataLoader class MyDataset(Dataset): def __init__(self, data, labels): self.data data self.labels labels def __len__(self): return len(self.data) def __getitem__(self, idx): return self.data[idx], self.labels[idx] dataset MyDataset(data, labels) dataloader DataLoader(dataset, batch_size64, shuffleTrue, num_workers4)你可能会问这和自己写for循环取数据有什么区别区别大了。DataLoader内置了多进程加载num_workers、自动打乱shuffle、批处理batch_size以及内存固定pin_memory等功能。训练一次epoch数据加载的效率直接影响整个训练时长。实操心得num_workers不是越大越好。我之前在某机器上把num_workers调成16结果每个epoch开始前系统卡顿严重反而比num_workers4慢了一倍。原因是CPU核数和磁盘IO能力不匹配进程切换开销超过了并行收益。推荐从CPU物理核心数的一半开始尝试观察任务管理器中的CPU占用率再逐步调整。3. 完整实战从零训练一个图像分类模型理论讲再多不如完整走一遍流程。我用一个经典的CIFAR-10图像分类任务带你走过从数据准备到模型训练的全部环节每行代码都有存在的理由。3.1 环境准备与数据获取假设你已经装好了PyTorch安装方式后面单独说。先准备数据CIFAR-10是10类物体的小型图像数据集3通道彩色图尺寸32x32非常适合新手练习和快速验证代码正确性。import torch import torch.nn as nn import torch.optim as optim import torchvision import torchvision.transforms as transforms transform transforms.Compose([ transforms.RandomCrop(32, padding4), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)), ]) trainset torchvision.datasets.CIFAR10(root./data, trainTrue, downloadTrue, transformtransform) trainloader torch.utils.data.DataLoader(trainset, batch_size128, shuffleTrue, num_workers4) testset torchvision.datasets.CIFAR10(root./data, trainFalse, downloadTrue, transformtransform) testloader torch.utils.data.DataLoader(testset, batch_size100, shuffleFalse, num_workers4)注意这里的transformsRandomCrop和RandomHorizontalFlip是数据增强作用是通过随机裁剪和水平翻转人为扩充训练样本多样性防止过拟合。Normalize里的均值(0.4914, 0.4822, 0.4465)和标准差(0.2023, 0.1994, 0.2010)是CIFAR-10数据集在所有训练图片上统计得到的标准化参数。这个标准化非常重要它能把像素值分布拉平让模型训练更稳定。提示downloadTrue会把数据集下载到你指定的root目录下国内网络可能比较慢。如果下载受阻建议先用浏览器或下载工具手动下载压缩包放到 ./data 目录再运行代码时它会自动检测并解压。3.2 模型搭建与参数选择背后的逻辑搭建一个经典的卷积神经网络——一个简化版的VGG风格网络。为了让新手能直接跑通又不过度简化设计成3组卷积块加全连接分类头class SimpleCNN(nn.Module): def __init__(self): super(SimpleCNN, self).__init__() self.conv1 nn.Conv2d(3, 32, 3, padding1) self.bn1 nn.BatchNorm2d(32) self.conv2 nn.Conv2d(32, 64, 3, padding1) self.bn2 nn.BatchNorm2d(64) self.conv3 nn.Conv2d(64, 128, 3, padding1) self.bn3 nn.BatchNorm2d(128) self.pool nn.MaxPool2d(2, 2) self.fc1 nn.Linear(128 * 4 * 4, 256) self.fc2 nn.Linear(256, 10) self.dropout nn.Dropout(0.5) self.relu nn.ReLU() def forward(self, x): x self.pool(self.relu(self.bn1(self.conv1(x)))) x self.pool(self.relu(self.bn2(self.conv2(x)))) x self.pool(self.relu(self.bn3(self.conv3(x)))) x x.view(x.size(0), -1) x self.dropout(self.relu(self.fc1(x))) x self.fc2(x) return x几个关键点解释一下第一个是BatchNorm2d的使用。BatchNorm对每个batch的数据做归一化把卷积层输出的分布拉回到均值为0、方差为1的标准状态。好处有两个一是梯度传播更顺畅可以放心用较大的学习率二是对初始化不敏感训练稳定性大幅提升。早期没有BatchNorm的网络训练一个不小心就会梯度爆炸或梯度消失很多时间都花在调参上。第二个是最大池化MaxPool2d。每经过一次池化特征图的宽高减半通道数翻倍。从32x32的图像开始经过三次池化后变成4x4的特征图通道是128展平后就是128 * 4 * 4 2048。这个数字不是随便写的你在设计网络时每一步都要算清楚feature map的尺寸变化否则在全连接层拼接时会出现维度不匹配的报错。第三个是Dropout。训练时按概率随机丢弃一部分神经元强迫网络学习冗余特征是防止过拟合的经典手段。注意Dropout只在训练时生效模型推理前必须调用model.eval()切换状态。3.3 训练循环与梯度下降的完整实现训练循环本身并不复杂但每一行都有明确职责。把训练函数封装好方便后续扩展def train_one_epoch(model, trainloader, criterion, optimizer, device): model.train() running_loss 0.0 correct 0 total 0 for inputs, labels in trainloader: inputs, labels inputs.to(device), labels.to(device) optimizer.zero_grad() outputs model(inputs) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() * inputs.size(0) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() epoch_loss running_loss / total epoch_acc correct / total return epoch_loss, epoch_acc这段代码是训练的核心循环顺序千万别乱。我在指导新人时最常看到的问题就是把optimizer.zero_grad()漏掉或者放错位置。它的作用是在每个batch开始前把上一轮累积的梯度清零。PyTorch的梯度是累积的不清零的话多个batch的梯度会叠加模型更新方向就会错。执行顺序有个简单记忆方法关、算、返、更。先zero_grad()关掉旧梯度再forward算出loss然后backward()反向传播计算梯度最后step()用优化器更新参数。这个顺序是无数人验证过的最稳定写法不要自作聪明调换。主循环代码device torch.device(cuda if torch.cuda.is_available() else cpu) model SimpleCNN().to(device) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr0.001) scheduler optim.lr_scheduler.StepLR(optimizer, step_size10, gamma0.1) num_epochs 30 for epoch in range(num_epochs): loss, acc train_one_epoch(model, trainloader, criterion, optimizer, device) scheduler.step() print(fEpoch {epoch1:02d} | Loss: {loss:.4f} | Acc: {acc:.2f}%)这里的学习率lr0.001选的是Adam优化器的常用默认值。Adam在训练早期收敛速度快对学习率不那么敏感适合新手起步。学习率调度器StepLR的作用是每10个epoch把学习率乘以0.1让模型在训练后期用小学习率做精细化参数调整让loss降到更低的位置。损失函数选择nn.CrossEntropyLoss这里有个隐藏细节CrossEntropyLoss内部包含了Softmax操作所以你模型最后一层不需要再额外加Softmax。如果手贱在forward里加了torch.softmax再配合CrossEntropyLoss出来的loss会偏高且收敛变慢。3.4 模型保存、推理与完整评估流程训练完成之后保存模型是必须的。PyTorch有两种保存方式区别很大# 方式一保存整个模型不推荐 torch.save(model, model_full.pth) # 方式二保存状态字典推荐 torch.save(model.state_dict(), model_weights.pth)推荐方式二。state_dict只保存了模型的权重参数占空间小而且加载时对模型结构改动更灵活。加载方式# 推理前先加载权重 model SimpleCNN() # 确保网络结构一致 model.load_state_dict(torch.load(model_weights.pth)) model.eval()模型评估时注意设置torch.no_grad()。推理阶段不需要计算梯度如果不关梯度每个batch都会额外构建计算图内存占用和耗时会翻倍。with torch.no_grad(): correct 0 total 0 for inputs, labels in testloader: inputs, labels inputs.to(device), labels.to(device) outputs model(inputs) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() print(fTest Accuracy: {100 * correct / total:.2f}%)用这套简化CNN在CIFAR-10上跑30个epoch测试准确率大约能到80%左右。这个数字看起来不算惊艳但要注意我们用的只是几层卷积的基础网络没有预训练权重也没有复杂的训练策略。作为新手练手项目能跑通整个流程并且理解了每一行代码的作用比盲目追求高准确率更有意义。4. 常踩的坑与排查技巧实录PyTorch虽然好用但坑也不少。我把这几年实际碰到的高频问题整理成速查表每一个都是血泪教训换来的。4.1 维度不匹配与设备不一致的报错维度不匹配是最常见的报错类型。比如你输入给nn.Linear(128 * 4 * 4, 256)的数据是1维的而不是2维的就会报mat1 and mat2 shapes cannot be multiplied。排查这类问题有个我每次都用的固定套路在forward函数里加打印观察每一层的输入输出shape变化def forward(self, x): print(fInput shape: {x.shape}) x self.conv1(x) print(fAfter conv1: {x.shape}) # ... 以此类推把print带上跑一个batch一眼就能看到shape是在哪一步变得不对。排查完记得删掉print。设备不一致的报错信息是Expected all tensors to be on the same device。新手写模型时经常忘了把数据和模型都放到GPU上或者只把模型放上去了数据还留在CPU上。我的经验是在训练脚本开头就统一定义device变量所有关键位置都xxx.to(device)这个报错基本就不会出现了。4.2 显存不足与训练中断的应急处理显存不足CUDA Out of Memory在跑大模型时几乎避不开。第一次遇到别慌按优先级排查第一是检查是不是训练过程中计算图没有释放。如果你在循环里写了loss torch.nn.functional.mse_loss(...)但没有把loss赋值给中间变量或者保留了所有batch的loss用于可视化这些张量会一直占用显存。第二是batch_size太大。固定的显存容量下batch_size与输入分辨率直接决定显存占用。遇到OOM第一反应就是把batch_size减半这通常立刻解决问题。第三是混合精度训练。PyTorch自带自动混合精度AMP功能用float16代替float32存储中间张量显存占用直接减半。scaler torch.cuda.amp.GradScaler() with torch.cuda.amp.autocast(): outputs model(inputs) loss criterion(outputs, labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()这段代码里的GradScaler是梯度缩放器因为float16的数值范围比float32窄很多梯度可能下溢所以先放大再缩放。AMP在训练速度上也有明显提升现在已经是我的默认配置。4.3 模型不收敛的通用诊断路径遇到loss不下降或者直接变成NaN这是训练中最磨人的场景。我的排查顺序是第一步检查学习率。学习率过大会导致loss震荡甚至NaN最简单的方法是把学习率除以10重新跑。如果还不收敛再考虑别的问题。第二步检查数据和标签是否对齐。我自己就干过这种事数据做了随机打乱但标签没跟着打乱模型从头到尾在学无效映射。在Dataset的__getitem__里print几组样式测试一下是最快的验证方式。第三步检查网络结构是否正确。用单条样本过一遍前向传播观察输出是否正常。如果输出也是NaN那大概率是网络结构有问题比如初始化不当、层连接错误。避坑技巧训练开始前用固定随机种子跑一个batch记录初始loss。如果初始loss是NaN后面必然全乱。对于分类问题初始loss应该在ln(类别数)附近才对比如10类任务的初始交叉熵应该是ln(10)2.3左右。差太远说明网络结构或初始化有硬伤。4.4 DataLoader多进程加载的坑DataLoader的num_workers是个看起来简单但实际很坑的参数。新手在Windows系统上跑num_workers0时脚本会报BrokenPipeError或者无限卡在加载阶段。这个问题的根因是Windows的进程启动机制和Linux不同主进程与子进程的交互方式有差异。解决方法是把主训练逻辑包在一个判断里if __name__ __main__: # 训练代码Linux系统基本不受这个问题困扰但Windows下这是必须的。另外部分自定义的数据集类如果__getitem__里包含了不可序列化的对象比如lambda函数也会导致多进程加载失败。遇到这种情况要么把lambda改成普通函数要么老老实实num_workers0。5. 工程化优化从能跑到跑得快的进阶路径模型能跑通只是第一步真正在业务里使用模型还有大量工程化的问题需要考虑。这里分享几个我认为性价比极高的优化方向。5.1 GPU利用率分析找训练瓶颈的近视镜法很多同学训练时看着GPU利用率波动就焦虑其实要先用数据说话。我建议训练时同时运行以下命令nvidia-smi观察GPU-Util列。如果GPU利用率长时间低于50%说明数据加载或者CPU预处理成了瓶颈。一般调整办法增大num_workers、用pin_memoryTrue、预处理步骤减少重复计算。如果GPU利用率已经稳在90%以上那就别瞎折腾数据管线的参数了专心调模型结构或batch_size。还有个小技巧训练过程中经常按watch -n 0.5 nvidia-smi实时刷新观察显存、功率和利用率的变化趋势比只盯着loss曲线更能定位性能瓶颈。5.2 混合精度训练的正确打开姿势前面在显存不足部分提到了AMP这里展开说下实战效果。在我近期用BERT做文本分类的任务中开启混合精度后显存占用降了将近40%训练速度提升了约1.5倍准确率几乎无损。具体操作已经展示过这里补充三个容易忽略的细节。首先是精度溢出问题。float16的表示范围大约在5e-5到65504之间如果loss超过65504就变成inf。GradScaler先把你loss放大若干倍等backward之后再缩回来规避了这个问题。其次混合精度不是所有层都用float16BN层和部分归一化层习惯上还在float32下计算AMP自动处理了这些细节。最后是模型保存AMP训练的模型state_dict里的权重可能混有float16和float32两种精度加载到普通float32模型上会报类型不匹配错误。解决办法是加载后统一转换model.load_state_dict(torch.load(model_weights.pth)) model model.float()5.3 用torch.compile无损加速你的模型PyTorch 2.0发布的torch.compile是我近期试过最惊喜的功能。用法简单到令人发指model SimpleCNN() model_compiled torch.compile(model)编译后的模型在A100上训练速度能提升20%-30%某些计算密集型网络甚至能到50%。它原理是把Python代码里的计算模式提取出来编译成底层的CUDA kernel绕过了Python解释器的开销。但注意两点一是torch.compile需要额外的编译时间第一次调用明显卡顿是正常的二是它和某些动态控制流的兼容性还不完美如果编译后报错直接把torch.compile去掉就行代码逻辑完全不受影响。5.4 多卡训练的基础配置与踩坑当单卡无法满足需求就需要上多卡训练。PyTorch最常用的多卡方案是DistributedDataParallelDDP它的核心思路是把模型复制到多张GPU上每个GPU处理一部分batch每步训练时同步各卡的梯度。基本配置分为四步初始化进程组、指定当前GPU、包裹模型、调整DataLoader。# 命令行启动 torchrun --nproc_per_node2 train.pyimport torch.distributed as dist import torch.multiprocessing as mp from torch.nn.parallel import DistributedDataParallel as DDP def train(rank, world_size): dist.init_process_group(nccl, rankrank, world_sizeworld_size) torch.cuda.set_device(rank) model SimpleCNN().to(rank) model DDP(model, device_ids[rank]) # 后续训练流程不变 if __name__ __main__: world_size 2 mp.spawn(train, args(world_size,), nprocsworld_size)多卡训练有几个让人抓狂的隐形坑。第一个是数据与标签在不同GPU上的分布DDP每个进程独立加载数据必须确保每个batch的数据分布规律一致否则会得到完全不同的训练结果。第二个是批量大小变化多卡训练时总batch_size是每张卡的batch_size乘以卡数学习率一般也要相应放大。第三个大坑是BN层的状态同步。DDP训练时每张卡上的BN层统计量是独立的导致推理时BN的均值方差不准。解决办法是在训练后期冻结BN层参数或者用SyncBN替代普通BN。我在多卡训练一个分割模型时最初测试准确率比单卡还低后来排查发现就是BN不同步的问题。6. PyTorch生态与选型建议框架本身再强大生态跟不上一样让人头疼。PyTorch在这几年积累起来的生态体系是它保持领先地位的重要筹码。6.1 图像、文本、时间序列三大方向的生态盘点图像方向最出名的是torchvision里面内置了数据集的自动下载接口、常用的数据增强函数、以及几十种预训练模型ResNet、VGG、MobileNet、EfficientNet等。做目标检测可以用torchvision.detection里现成的Faster R-CNN、Mask R-CNN实现做分割有DeepLabV3。文本方向的核心是HuggingFace Transformers库它是当下NLP事实上的标准工具箱。你几乎不需要自己实现任何Transformer模块只需要调用from transformers import AutoTokenizer, AutoModelForSequenceClassification tokenizer AutoTokenizer.from_pretrained(bert-base-chinese) model AutoModelForSequenceClassification.from_pretrained(bert-base-chinese, num_labels2)这个过程就是加载预训练权重省去了从头训练的全部时间和数据需求。在绝大多数实际项目中用预训练模型微调的效果会远远好于自己从零训练。时间序列方向相对分散PyTorch Forecasting与Darts是目前比较活跃的选择适合风电预测、电力负荷预测、金融时序建模等场景。不过说实话这个方向的生态成熟度远不如前两个。6.2 开发环境与依赖管理实践建议我给新手的建议是别用系统Python直接安装PyTorch万分建议用虚拟环境隔离。用conda或venv创建独立的项目环境用一个环境一套依赖避免版本冲突。安装PyTorch的核心注意点是选择与CUDA版本匹配的安装命令。装错了会出现AssertionError: Torch not compiled with CUDA enabled最稳妥的方法是到PyTorch官网的安装向导页面根据自己的CUDA版本复制对应命令。显卡驱动里的CUDA版本不是你的运行时版本是PyTorch需要调用的CUDA库版本两者有区别。实操心得我电脑上用CUDA 11.8和CUDA 12.1各建了一个PyTorch环境因为不同项目依赖的cuda版本不同。遇到GPU驱动可以兼容多个CUDA版本的时候这种多环境并存很管用别怕麻烦。6.3 什么时候应该放弃PyTorch选别的框架说了这么多PyTorch的好话也得客观说下它的局限。如果你的需求是大规模工业部署特别是需要服务端高并发推理PyTorch的原始推理性能不如ONNX Runtime或TensorRT。但这两者都只是推理引擎不支持训练通常的做法仍然是PyTorch训练导出为ONNX后部署。所以实际上不存在真正的“替代关系”。如果你是量化交易等低延迟行业对推理延迟极度敏感那么直接上TensorRT把PyTorch模型导出为TensorRT的engine格式性能能高出数倍。如果你是嵌入式或移动端部署可以考虑适配TensorFlow Lite或Core MLPyTorch也有对应的torchvision转换工具但生态成熟度确实差一些。还有一个场景是研究新型模型结构需要自定义大量反向传播逻辑这时你可能要考虑JAX。JAX的函数式编程风格更接近数学表达式在纯学术研究中有独特的优势。但从行业实际使用比例来看PyTorch仍然是绝对主流。最后说说我个人在实际使用中的体会。框架这东西你用久了就会形成肌肉记忆。PyTorch最让我欣赏的不是某个具体功能而是它始终保持了“让Python成为一等公民”的理念——你不用为了用框架去学一套新的编程范式把你已有的Python功力直接用起来就好了。这种设计哲学上的纯粹才是它长久保持生命力的根源。如果你还在犹豫选哪个框架入门直接选PyTorch不会错。动手写一个模型跑通一个epoch你就能感受到我说的这种顺畅感了。
阅读完成 · 觉得有帮助?
咨询建站