ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

TensorFlow与PyTorch实战:回归模型从零构建与对比

TensorFlow与PyTorch实战:回归模型从零构建与对比 从猜价格这类最朴素的AI需求开始回归模型几乎是每个新手接触人工智能时绕不开的第一课。它不像图像分类那样复杂也不像自然语言处理那样需要海量语料却完整覆盖了数据准备、模型定义、训练、评估和预测这条全链路。本文我会用TensorFlow和PyTorch这两套当前最主流的框架分别从零构建一个可运行的AI回归模型对比它们在写法、调试、部署上的真实差异并把我在实战中踩过的坑一并交代清楚。如果你正打算入门AI或者想从其中一个框架切换到另一个这篇实战记录应该能省掉你不少试错时间。1. 回归模型为什么值得你第一个上手1.1 回归模型到底在解决什么问题回归模型的核心任务是预测一个连续数值比如明天的气温、二手的房价、店铺的销售额。它和分类模型最直观的区别就在输出层分类输出的是离散的类别概率回归输出的是一个或多个连续实数。这看似只差了一个激活函数但背后的数据假设、评估指标、调参侧重点完全不同。很多新手一上来就学图像识别、目标检测结果被卷积、池化、anchor这些概念砸晕。其实回归模型反而是最接近传统统计分析的一种神经网络形态。它只用全连接层就能工作数学原理清晰透彻——神经网络本质上是拟合一个函数 f(x) 到 y 的映射而回归就是最直接的函数拟合。你先理解这个过程后面再看复杂的NLP和CV模型会觉得它们的底层逻辑其实是一样的只是特征工程和网络结构更复杂了。1.2 为什么一定要同时碰TensorFlow和PyTorch日常社区里经常能看到TensorFlow党和PyTorch党互相瞧不上但真实工业界这两者你都得会一点。TensorFlow生态成熟尤其在生产部署、移动端支持方面非常稳定很多大厂存量项目都跑在它上面PyTorch在学术研究圈子里更吃香动态图带来的调试便利性让论文复现和快速迭代效率高很多。作为个人开发者我现在的经验是做快速原型和项目测试时首选PyTorch因为写起来更接近原生Python思维做需要长期维护、上线服务的系统时倾向TensorFlow因为从模型到服务这条链路更顺畅。两个都会你才能横向比较不至于被某个特定框架的写法规训了思维方式。这篇文章里我会用同一份模拟数据让两个框架各跑一遍这样对比起来也公平。2. 环境准备给两个框架安个舒服的窝2.1 先想清楚硬件和版本否则后面全是泪我在刚接触AI时有过一段血泪史直接拿pip装CPU版TensorFlow后来装了PyTorch版本全是默认结果在MNIST分类中遇到了各种报错。所以环境准备这一步别偷懒先确认几个关键决策你用的是NVIDIA GPU还是纯CPU。有GPU就优先装GPU版本训练速度差几倍到几十倍没有GPU就用CPU版本小规模回归模型其实也够用。选Python 3.9还是3.10不用追求最新版但最好支持两个框架。目前TensorFlow 2.10以上和PyTorch 2.x对Python 3.10都支持得不错。包管理器建议直接上Anaconda省去后面大量依赖冲突的麻烦。用Anaconda管理环境是我强烈推荐的做法。你永远不想因为一个项目的依赖更新把另一个项目的环境搞坏。下面是我在Windows和Linux上都验证过的创建命令conda create -n ai_regression python3.9 -y conda activate ai_regression创建好独立环境后再分框架安装。2.2 TensorFlow环境配置一条命令有玄机TensorFlow现在统一的安装包是tensorflow它同时包含CPU和GPU支持在2.x版本中如果TensorFlow检测不到GPU会自动退到CPU。Windows上直接pip安装即可pip install tensorflow如果你需要显卡加速要额外确认CUDA和cuDNN版本是否匹配。TensorFlow 2.10针对Python 3.9和CUDA 11.2是一个比较稳的组合。建议不要用conda直接装cudatoolkit因为TensorFlow的官方预编译包对CUDA版本有自己的预期混装反而容易引发找不到动态库的怪问题。每次装完我都习惯跑一小段探测代码import tensorflow as tf print(tf.__version__) print(tf.config.list_physical_devices(GPU))看到GPU列表为空也不用慌CPU环境也能跑只是训练时会在终端里看到等待时间拉长。但如果连TensorFlow本身都import报DLL错误那八成是VC运行库或者CUDA依赖缺失去官网装对应的运行库就好。2.3 PyTorch环境配置官网那行复制粘贴命令PyTorch的安装比TensorFlow友好不少官方首页会根据你的操作系统和CUDA版本生成一个install命令这是我见过最省心的向导式配置conda install pytorch torchvision torchaudio pytorch-cuda11.8 -c pytorch -c nvidia如果不需要GPU可以去掉最后一个cuda的包CPU版体积小很多。我的习惯是CPU版用pip安装更干净pip install torch。但注意PyTorch的CPU版和GPU版包名相同都是从PyPI下载对应wheel。用conda从官方channel安装会额外拉取一堆依赖速度不一定更快。装好后照例验证import torch print(torch.__version__) print(torch.cuda.is_available())这里print输出的False不代表装错了只是当前机器没有GPU资源。2.4 两个框架装进同一个环境会有冲突吗很多人在同一个conda环境里同时装TensorFlow和PyTorch两者会依赖同一个kernelspec和numpy等底层库。目前来看两个框架对第三方库的要求已经趋于一致同时安装并不冲突。我自己的经验是用同一个环境省心只要别同时混装老版本就行。真遇到莫名其妙的问题比如导入TensorFlow后PyTorch报错优先检查numpy版本然后重启内核。另外千万别直接用conda install tensorflow和pip install torch混着来conda和pip的依赖解析器会互相覆盖容易出现所谓包已安装但import失败的诡异问题。我的原则是环境创建用conda包尽量全部用pip安装这样依赖关系最清晰。3. TensorFlow实战用Keras快速构建回归模型3.1 生成有规律的模拟数据造数是门技术为了让两个框架的对比公平我先造一份带线性规律的数据。回归模型最怕数据量太小导致训练不稳定这里我用numpy生成1000个样本特征是单变量标签是特征乘以一个权重加上噪声模拟真实场景中有规律但干扰大的情况import numpy as np import matplotlib.pyplot as plt np.random.seed(42) X np.linspace(-3, 3, 1000).reshape(-1, 1) y 2.5 * X 1.2 np.random.randn(1000, 1) * 0.8 plt.scatter(X, y, s4, alpha0.5) plt.xlabel(X) plt.ylabel(y) plt.show()这里的randn噪声模拟的是现实中不可避免的随机波动。如果你的数据是完美的线性关系任何模型都能轻松拟合那就没有练习意义了。添加高斯噪声后回归模型的目标就变成了在干扰中尽量捕捉潜在的趋势。你还应该把数据分成训练集和测试集比如前800个样本做训练后200个样本做验证评估时才不会既当运动员又当裁判from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.2, random_state42)3.2 Keras Sequential一行一行搭积木TensorFlow的Keras接口非常直观用Sequential模型可以把每一层像积木一样垒起来。回归任务不需要太深一两个全连接层足够了但为了演示多层网络我特意加了隐藏层import tensorflow as tf from tensorflow.keras import layers model tf.keras.Sequential([ layers.Dense(units16, activationrelu, input_shape(1,)), layers.Dense(units16, activationrelu), layers.Dense(units1) ])注意隐藏层的激活函数用了relu它的非线性表达能力让两个全连接层可以拟合更复杂的曲线变化而最后一层没有激活函数这是回归任务的关键——输出层必须是线性的范围不受限制。如果你在输出层误用sigmoid或tanh输出会被压缩在有限区间预测值永远无法超出某个范围数据超出这个范围时就废了。Keras的模型编译也很直白loss用均方误差MSE优化器用adammetrics用来额外记录平均绝对误差MAE方便人眼观察model.compile(optimizeradam, lossmse, metrics[mae])这里为什么选MSE而不是别的损失因为MSE损失可导且对大误差惩罚更大适合数值型预测。它唯一的缺点是离群点会造成梯度爆炸不过在我们这种模拟数据里不是问题。3.3 训练与评估fit起来很快Keras的fit方法封装了训练循环一个函数搞定所有history model.fit(X_train, y_train, epochs100, batch_size32, validation_data(X_test, y_test), verbose1)训练过程会打印每个epoch的loss和mae。你会发现前几十轮loss急剧下降后面逐渐平缓。如果训练集上的loss一直降验证集loss却不再降甚至升高那就是过拟合的苗头。此时可以加正则化、Dropout或者干脆减少epoch数。我通常会把history.history里的loss画出来直观判断plt.plot(history.history[loss], labeltrain_loss) plt.plot(history.history[val_loss], labelval_loss) plt.legend() plt.show()验证曲线上扬就是要提前停了如果两个loss都在降就说明还没训练充分可以加epoch。评估阶段使用model.evaluatetest_loss, test_mae model.evaluate(X_test, y_test, verbose0) print(Test Loss:, test_loss, Test MAE:, test_mae)我们还可以直接拿模型预测并把预测线画出来和真实数据对照视觉上就能看出拟合得如何。3.4 TensorFlow实战中的几个隐藏细节第一个细节是输入维度。Keras里第一层必须指定input_shape如果形状不对fit时会直接报shape mismatch。第二个细节是批量大小batch_size设太大会导致内存占用高太小则梯度震荡。回归任务通常32或64就够。第三个细节是numpy数组的dtypeKeras一般会自动转成float32但也有Exception的场景训练前最好统一用X_train X_train.astype(np.float32)避免一些莫名其妙的警告。另外如果你在团队协作中发现两次训练结果不一致通常和随机种子有关。前面我设置了np.random.seed但TensorFlow也有自己的随机初始化可以再加一句tf.random.set_seed(42)这样才能让复现实验成为可能。4. PyTorch实战把控制权握在自己手里4.1 准备数据从Dataset到DataLoader的固定套路PyTorch不像Keras那样直接用numpy数组就能训练它更倾向于让我们自己定义Dataset和DataLoader。这套流程初看繁琐但好处是灵活尤其在处理图片、文本等复杂数据时你会体会到它的威力。对我们的回归问题写一个最小版本import torch from torch.utils.data import Dataset, DataLoader class RegressionDataset(Dataset): def __init__(self, X, y): self.X torch.tensor(X, dtypetorch.float32) self.y torch.tensor(y, dtypetorch.float32) def __len__(self): return len(self.X) def __getitem__(self, idx): return self.X[idx], self.y[idx] train_dataset RegressionDataset(X_train, y_train) test_dataset RegressionDataset(X_test, y_test) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue) test_loader DataLoader(test_dataset, batch_size32, shuffleFalse)这里dtypetorch.float32是必须的PyTorch默认会用float32初始化模型参数。如果你喂给模型的张量是float64它不会自动转换而是会直接抛出类型不匹配的异常。另外DataLoader里的shuffle开在训练集上、关在测试集上是一个默认习惯——训练集打乱顺序能避免模型学到样本顺序上的伪规律测试集完全不需要。4.2 定义模型面向对象的结构PyTorch的模型定义通过继承nn.Module完成这比Keras的列表式结构更符合传统面向对象思维import torch.nn as nn class RegressionModel(nn.Module): def __init__(self): super().__init__() self.hidden1 nn.Linear(1, 16) self.hidden2 nn.Linear(16, 16) self.output nn.Linear(16, 1) def forward(self, x): x torch.relu(self.hidden1(x)) x torch.relu(self.hidden2(x)) x self.output(x) return x model RegressionModel()这里有个新手常问的点为什么forward里不能自己调用self.hidden2(self.hidden1(x))后直接赋值给输出层其实可以但最好保持层与激活的清晰分离。nn.Linear初始化自带随机权重和偏置不需要你再单独初始化。优化器和损失函数在PyTorch里也是独立定义的import torch.optim as optim criterion nn.MSELoss() optimizer optim.Adam(model.parameters(), lr0.001)4.3 手动训练循环沟通每一步在干什么PyTorch的训练循环需要你手写这是它和Keras最不一样的地方但也是我为什么推荐新手至少手写一遍的原因。你会彻底明白模型在训练时到底发生了哪些步骤num_epochs 100 for epoch in range(num_epochs): model.train() running_loss 0.0 for X_batch, y_batch in train_loader: optimizer.zero_grad() # 清空上一步的梯度 y_pred model(X_batch) # 前向传播 loss criterion(y_pred, y_batch) # 计算损失 loss.backward() # 反向传播 optimizer.step() # 更新参数 running_loss loss.item() * X_batch.size(0) epoch_loss running_loss / len(train_loader.dataset) if (epoch1) % 10 0: print(fEpoch {epoch1:3d} Loss: {epoch_loss:.4f})这里每一步都不能省。尤其optimizer.zero_grad()如果不调用梯度会在backward时累加导致参数更新异常loss可能会一飞冲天。我第一次把zero_grad()注释掉后精确复现了loss爆炸现象那一刻真的解开了我对框架底层运行的疑惑。4.4 用验证集观察过拟合训练过程中不光要看训练loss还要周期性评估验证集效果。我习惯在每个epoch末尾写一个独立的评估段model.eval() val_loss 0.0 with torch.no_grad(): for X_batch, y_batch in test_loader: y_pred model(X_batch) loss criterion(y_pred, y_batch) val_loss loss.item() * X_batch.size(0) val_loss / len(test_loader.dataset)model.eval()和torch.no_grad()这两个搭档极其重要。前者让Dropout和BatchNorm层切换到推理模式后者关闭梯度计算大大降低内存占用并加速计算。很多新手手册不会强调这点实际项目中一旦在测试阶段开了梯度不仅慢还可能因为你无意间改了requires_grad而污染模型状态。输出测试报告print(fValidation Loss: {val_loss:.4f})到这里一个完整的PyTorch回归模型已经能跑起来了。相比TensorFlow的fit这个循环看起来麻烦但是每个环节你都可以随时插入print、打断点看某一步张量的形状和数值。这种透明的控制感是PyTorch在科研圈子里受欢迎的原因。5. 两个框架实战对比从代码到体验的硬核测评5.1 代码量与易用性Keras完胜但要看场景单从回归模型的实现来看TensorFlow Keras的代码量几乎只有PyTorch的三分之二。Keras用model.fit把训练循环藏了起来你不需要关心zero_grad和optimizer.step这对快速验证想法非常友好。PyTorch则需要你手动写循环但换来的是对每一步的控制力。以我个人的使用习惯来说当我想快速验证一个模型在数据集上是否有效时我会优选Keras但当我需要实现一个自定义训练算法比如对抗训练、梯度惩罚时Keras的封装反而成了阻碍这个时候PyTorch手写的循环改起来没有任何魔法。5.2 调试体验动态图和静态图的时代差PyTorch默认的即时执行模式eager execution让你可以像写普通Python代码一样执行张量操作随时print张量值。早年的TensorFlow是静态图模式你需要先定义好一张计算图放到Session里跑中间变量很难直接看到调试非常痛苦。不过TensorFlow 2.x已经默认开启eager execution两者的调试差距其实已经大幅缩小。但这中间仍有一个核心区别PyTorch的forward函数就是用Python写的原生逻辑你可以随意加条件判断、循环甚至强制调用另一个Python库的APITensorFlow的Keras层在底层编译成tf.Graph时会经历一定的code transformation少数Python语法不能直接用。虽然正常场景很难踩到这种边界但在研究型项目中PyTorch的灵活性依然有不可替代的价值。5.3 模型保存与部署两边的生态各有侧重训练好的模型总得上线才创造价值。TensorFlow整条生产链路成熟度极高我常用的保存方式是model.save(regression_model.keras)然后可以用tf.keras.models.load_model加载或者用tf.lite.TFLiteConverter转换到移动端。TensorFlow Serving也可以很轻松地发布为HTTP服务和Kubernetes等基础设施的集成文档比较完整。PyTorch的保存方式稍微多样化一点torch.save(model.state_dict(), regression_model.pth)注意state_dict只保存参数不保存结构加载时需要先实例化模型再load_dict。如果你希望整个模型结构和参数都圈在一起可以torch.save(model, model_full.pth)但这种保存方式对模型类定义的位置有依赖不推荐在生产环境长期使用。PyTorch也提供torch.jit.trace和torchscript来做序列化配合LibTorch或ONNX也能部署到服务端但整体生态相较TensorFlow还是略微分散一些。5.4 性能实测小模型看不出差距用刚才那份模拟数据两个框架在100个epoch内训练时间在CPU上跑都几秒钟内结束。如果你也用CPU跑没必要把时间浪费在选择谁更快上。绝大多数回归场景下性能瓶颈在数据读取和预处理而非模型计算。只有当你的数据量达到几十万行、特征成百上千时GPU的并行优势才会显性化而这时两个框架在GPU加速上差距也很小。所以我的建议是别因为某个论坛帖子说谁快就选谁要看你之后要做的项目更依赖那个生态。搞研究、频繁改模型结构选PyTorch做工业部署、和已有Java/C服务整合选TensorFlow更省心。6. 回归模型不只是跑通数据预处理和调参细节6.1 特征标准化一个容易被忽略但决定性很强的步骤玩模拟数据时特征范围如果很小即使不做标准化也能正常收敛。但真实项目里的特征尺度可能差异巨大房价预测里有面积几十到几百平方米和地理位置经纬度以及房龄几年)混在一起如果直接输入数值大的特征很容易让梯度更新方向被它独占收敛缓慢且不稳定。最经典的处理是Z-Score标准化from sklearn.preprocessing import StandardScaler scaler_X StandardScaler() X_train_scaled scaler_X.fit_transform(X_train) X_test_scaled scaler_X.transform(X_test)注意只能用训练集来fit再用同一套参数去transform测试集绝不能把测试集一起丢进fit里否则会造成信息泄露模型的评估结果会过于乐观。这一点在正经的机器学习项目中会经常考到也是入门进阶的一道分水岭。6.2 学习率调参里最值钱的一个旋钮两个框架里我都用了Adam和默认的0.001学习率。这个默认值在不少场景下都好用但它不等于万能。你把学习率调成0.1再跑一遍大概率看到loss先降几轮然后直接变成NaN——这是学习率过大导致梯度更新步长太大跨越了损失函数的凹谷冲到数值异常区域。反之把学习率调成0.00001loss下降就像蜗牛一样100个epoch内可能还没进入平坦区。我更推荐一种省力的做法先设大学习率快速看一个大概比如0.01再每次缩小10倍观察loss曲线。等到曲线稳定平滑就是合适的量级了。6.3 过拟合不是只有分类模型才有的问题很多人觉得回归模型简单容易忽略过拟合。其实样本量一少噪声被学习进模型回归曲线就会出现明显的抖动弹跳。判断过拟合最直接的手段还是我在前面反复强调的分开训练loss和验证loss。训练loss一路降验证loss降到某个点后上升就是过拟合的明确信号。解决办法包括增加样本量真实数据很难但可以通过数据增强模拟降低模型复杂度减少隐藏层数或神经元数使用正则化如L1/L2在隐藏层加入Dropout单纯为了压缩训练loss而加层数在回归任务里不但没啥用反而容易把噪声也当趋势学进去。6.4 回归损失函数MSE、MAE和Huber怎么选我一直用MSE但真实场景下MSE对离群点极其敏感。比如预测房价如果数据里混入几个特殊豪宅同一个面积下价格异常高MSE会因为误差平方而给离群点非常高的梯度权重导致模型为了拟合这些极端点而牺牲大部分普通样本。MAE则对离群点更稳健因为它对误差绝对值求平均不会因为某个样本误差大而惩罚翻倍。MAE的缺点是梯度在误差接近0时依然恒定低调优阶段收敛不够精细。折中的选择是Huber损失它在小误差时表现为平方损失大误差时表现为线性损失兼具两者的优点。Keras和PyTorch都直接提供HuberLoss如果是真实业务数据且离群点不少我建议直接换Huber试一下效果往往有明显提升。6.5 数据泄露最容易在预测任务中被忽略的严重Bug在回归项目里数据泄露可能藏在很多不起眼的处理中。前面提到的标准化是常见泄露点另一个典型是时序预测中直接随机划分训练测试集导致未来信息泄漏到过去。如果回归任务面向时间序列卖量预测我通常按时间切分而不是随机切分不然预测的是被偷看过试卷的成绩上线后一落千丈。我遇到过最隐蔽的泄露是特征里包含了另一个目标变量的滞后值。比如要预测明天的销量却把今天的实际销量当成特征。今天销量和明天销量强相关模型看上去效果极好实际到了部署时今天的销量作为特征是可以获取的但如果业务上还没来得及统计今天销量你的特征在推理时根本拿不到这个模型上线必翻车。所以做任何一个回归项目前先问一句推理时我能拿到这些特征吗7. 从回归模型走向真实项目我的几点填坑体会7.1 从模拟数据到脏数据最大的落差在数据清洗模拟数据是我们自己造的类型干净没有缺失值。真实数据里至少会有这些情况某些行直接是空值某些列是字符串编码需要做类别特征编码某些数值列出现明显的录入错误比如年龄写成了150我的习惯是先info()看列类型再describe()看数值分布接着对缺失值做可视化检查。回归模型的精度很大程度上取决于数据和特征工程而不是框架。很多时候你把数据多清洗一轮比换一个模型更有效。这一点无论用TensorFlow还是PyTorch都一样。7.2 训练遇到NaN从学习率开始排查有一次我在实际任务里训练一个多变量回归模型跑到第10轮loss直接NaN。当时我以为是数据里有NaN检查了半天都没有。后来把学习率从默认的0.001调低一个数量级到0.0001问题立刻消失。这类问题在深度网络里很常见梯度过大导致数值溢出。排查顺序建议是检查数据里有没有NaN、Inf检查学习率是否过大检查损失函数是否适合当前输出范围检查激活函数是否在深层产生了梯度爆炸这些在Keras里和PyTorch里都可能发生处理思路完全一致。如果你用PyTorch可以在每个梯度更新后加一句if not torch.isfinite(loss): print(loss is NaN, stopping) break提前捕获异常能省下后面几十分钟盯着跑完的空白时间。7.3 模型保存后再次加载最容易绕晕的环节两个框架的保存加载不互通转换格式时尤其小心。TensorFlow的.keras或.h5文件不能在PyTorch里直接使用需要先转成ONNX格式。PyTorch把state_dict保存到一个文件里加载时要求你当前环境下有完全一样的模型类定义否则报错。我在项目中一直坚持既保存state_dict又导出一份ONNX作为通用交换格式。ONNX可以被ONNX Runtime直接加载也可以被TensorRT优化这在跨框架协作时省了很多麻烦。7.4 一个技巧画出一条好的预测线很多人建模只管打印loss从不把预测结果画出来。但实际业务方对你模型提交的mae0.2完全没有概念你直接甩一张拟合曲线图人家一眼就看出趋势对不对。回归模型的最终交付物不只是模型文件还要有一份可解释的预测分布图。我自己通常会在测试集中随机抽100个点画真实值和预测值的散点再加一条yx对角线如果点都贴着对角线说明模型预测很准如果偏离明显那你就能从散点形状里看出模型在哪个区间容易被低估或高估这对下一步优化非常有指导意义。7.5 到底先学TensorFlow还是先学PyTorch不少入门者问过我这个问题我的回答永远是先跑通一个再说。这篇博文之前如果你完全没接触过框架我建议你先跟着TensorFlow的部分把流程跑通因为Keras的封装让你少碰很多底层障碍能快速建立起建模-训练-评估的全局认知。接着再用PyTorch手写一遍训练循环这个过程中你既理解了底层原理又把两个框架的API都见了一遍。两个框架并非二选一的关系它们是你工具箱里的两把扳手修不同车的时候各有顺手之处。回归模型是一切深度学习的起点它很小但五脏俱全。你现在花在这个小模型上的理解和调试功夫都会在之后做大规模分类、生成式模型时返回来回报你。这篇实战记录里提到的数据标准化、学习率调参、过拟合判断、模型保存等步骤几乎是可以平移到你未来任何一个有监督学习项目里的通用能力。如果你正在调试时卡住了或者跑出的结果和想象中不太一样欢迎照着上面几个排查思路捋一遍——大概率能省下不少搜索报错的时间。
返回列表