news 2026/10/14 6:23:05

深度学习零基础实战:用PyTorch从零实现回归预测

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
深度学习零基础实战:用PyTorch从零实现回归预测

看到“深度学习零基础-回归实战1”这个标题,我猜你很可能和我当年刚入门时一样:神经网络、反向传播、梯度下降这些名词听了八百遍,但真要自己动手跑通一个项目,却不知道该从哪一行代码写起。这篇就是写给完全没碰过深度学习的人,用回归这个最直观、最基础的实战任务,把从数据准备、模型搭建、训练调参到结果评估的完整链路走一遍。回归解决的是“预测连续数值”的问题,比如房价、气温、销量,它也是理解深度学习最核心机制的最佳入口。内容适合只懂一点点Python、不怕装环境、想把理论知识真正跑成代码的同学。

1. 回归问题到底在解决什么

1.1 回归和分类,别一上来就混着学

很多零基础同学第一个接触的深度学习项目往往是图像分类,比如手写数字识别,于是会下意识地以为深度学习就是做“识别”的。但“分类”和“回归”是两类性质不同的任务,新手如果混着学,很容易在输出层、损失函数这些地方栽跟头。

分类任务的输出是离散的类别标签。比如判断一张图里是猫还是狗,输出是“猫”或者“狗”这样的类别。回归任务的输出则是一个连续的数值,比如根据房屋面积、房龄、周边配套来预测房价,输出是“××万元”这样具体的数。一个侧重“归类”,一个侧重“估值”。

这个区别直接决定了模型结构的设计。分类问题在输出层之后通常要接一个激活函数,把输出压缩到0到1之间当成概率;回归问题则恰恰相反,输出层一般不做任何激活,因为预测值可以是任意范围内的实数,你用一个范围固定的函数去限制它,等于亲手把预测能力砍掉一大截。这一点后面实战时会反复遇到。

回归的数学本质是拟合一个函数关系:给定输入 (x),找到一个映射 (f(x)),使得输出 (y) 尽可能接近真实值。在只有两个特征的最简单场景里,你要拟合的就是一个平面,而深度神经网络可以理解为对“线性组合+非线性激活”做了很多层堆叠,从而能拟合出非常复杂的曲面。

1.2 为什么第一个实战选回归

我见过太多人一上来就挑战图像分类、目标检测这类项目,结果被数据集、预处理、网络结构、训练技巧一大堆问题淹没。相比之下,回归是深度学习入门性价比最高的项目,原因有三点。

第一,数据容易搞定。分类任务经常需要下载乱七八糟的公开数据集,有时候光处理格式就要花半天。回归实战完全可以自己用一行随机函数生成一份可控的模拟数据,把精力全部集中到“模型是怎么学”这件事上。

第二,评价标准非常直观。分类任务看准确率,但准确率有欺骗性,比如样本不均衡时会虚高。回归任务看预测值和真实值的差距,小了就是准,大了就是不准,不需要绕弯子。

第三,网络结构最简单。回归实战只需要一个输入层、一个隐藏层、一个输出层的极简结构就能跑出效果,方便把每个模块拆开看清楚。你先在回归里理解“前向传播算损失、反向传播算梯度、优化器更新参数”这条主线,之后学卷积、注意力、Transformer这些高级结构时就会发现,万变不离其宗。

2. 环境准备与模拟数据生成

2.1 零基础环境清单:装什么、怎么装

先别想着一步到位搭建超级深度学习平台,你只需要一个能跑Python的环境,加上三个库。

  • Python 3.9以上,推荐直接用Anaconda管理环境,省去一堆依赖冲突的麻烦。
  • NumPy,用来生成和操作数据。
  • Matplotlib,用来画损失曲线和预测结果。
  • PyTorch,作为主要的深度学习框架,它采用动态图机制,对新手非常友好,调试时能看到每一步张量的变化,逻辑更贴近“写普通Python代码”的感觉。

安装命令很简单,打开终端执行:

pip install numpy matplotlib torch

如果下载速度太慢,可以临时换用国内的镜像源,例如在pip命令后面加上-i参数指定镜像地址,很多同学靠这一招省下大把时间。

装完务必验证一下环境,否则后面代码报错时你根本分不清是环境问题还是代码问题。新建一个Python文件,运行:

import torch import numpy as np import matplotlib.pyplot as plt print(torch.__version__) print(np.__version__)

能正常输出版本号,说明环境基本没问题。如果import torch这个地方卡住了,大概率是安装时的版本和你的Python版本不匹配,建议去框架官网按自己的系统选择对应的安装命令重新装一遍。

2.2 自己动手造一份可复现的练习数据

很多教程一上来就丢给你一个公开数据集,下载、预处理、理解字段含义要花半天。这里我们换一种思路:用NumPy自己生成一份模拟房价数据,真实项目里你当然要用真实数据,但学习阶段自制数据有两个明显优势——可控和可复现。

设计一个简单但不失真实的生成逻辑:用两个特征预测房屋价格,特征分别是房屋面积和房龄。假设真实规律是“每平米价值50万元,每多一年房龄折损20万元”,然后加上一点随机噪声来模拟真实世界的不确定性。

import numpy as np np.random.seed(42) n_samples = 200 area = np.random.uniform(40, 150, n_samples) # 面积 40-150 平 age = np.random.uniform(0, 50, n_samples) # 房龄 0-50 年 # 真实关系:价格 = 50 * area - 20 * age + 噪声 price = 50 * area - 20 * age + np.random.normal(0, 80, n_samples) X = np.stack([area, age], axis=1) # 特征矩阵,形状 (200, 2) y = price # 标签,形状 (200,)

这里有几个容易忽略的细节。

np.random.seed(42)是固定随机种子,保证你不管跑多少次,生成的数据都完全一样。这个习惯在复现实验时极其重要,不然你换台电脑换个时间跑,数据全变了,后面所有结果都无法对比。

生成的特征X形状是(200, 2),每一行是一条样本,每一列是一个特征;标签y形状是(200,),它和X的行数一一对应。深度学习框架强制要求输入是二维矩阵,这一点新手经常会搞错。

最后做一次训练集和测试集的切分。不要把所有数据都拿去训练,否则你无法判断模型是真正学到了规律,还是死记硬背住了训练数据。这里简单按8:2切分:

from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) print(X_train.shape, X_test.shape)

如果不想用scikit-learn,纯手写切分也就两行:

idx = np.random.permutation(len(X)) train_idx, test_idx = idx[:160], idx[160:]

模拟数据还有一个好处:你知道真实答案是“面积权重50,房龄权重-20”。训练完成后把学到的权重拿出来和真实值对比,就能直观感受到模型学得准不准。这种可验证性,是真实数据集很难提供的。

3. 从零搭建第一个回归模型

3.1 模型结构:输入、隐藏层、输出

我们要搭建的模型在深度学习里算“迷你型”:输入层、一个隐藏层、输出层。它的核心逻辑可以概括成两个线性变换中间夹一个激活函数。

输入层接收两个特征,也就是面积和房龄,数学上对应一个形状为(2,)的向量。隐藏层是模型真正“思考”的地方,这里设定8个神经元。你可以把每个神经元理解成从输入特征中提取局部信息的加工单元,8个神经元意味着用8种不同的角度去观察这条数据。隐藏层后面接ReLU激活函数,作用是给模型注入非线性能力。

为什么要非线性?如果所有层都是线性变换,那堆多少层本质上都等价于一层线性模型,表达能力非常有限。ReLU的规则很粗暴:大于0就原样输出,小于等于0就清成0。它实现简单,计算快,梯度消失问题也比别的激活函数轻,所以是回归任务里隐藏层的默认选择。

输出层只需要1个神经元,输出价格。这里有个关键点:输出层之后不要再加激活函数。因为价格可以是任何正数,也可以变化很大,一旦加了Sigmoid之类的函数,输出会被强行限制在0到1之间,模型就废了。这一点和分类任务的差异非常大,我在实际教学中见过不少同学把分类项目的习惯带过来,导致回归模型怎么训练loss都不下降。

用PyTorch定义这个模型非常简洁:

import torch import torch.nn as nn model = nn.Sequential( nn.Linear(2, 8), nn.ReLU(), nn.Linear(8, 1) ) print(model)

nn.Linear(2, 8)表示一个从2个输入到8个输出的线性层,它内部会自己初始化一个(8, 2)的权重矩阵和一个(8,)的偏置向量。你不用关心初始化细节,但要知道这些参数就是后续训练过程中不断被更新的对象。

3.2 损失函数与优化器怎么选

模型初始状态下的预测完全是瞎猜,我们需要一个量化“猜得有多不准”的指标,这就是损失函数。回归任务最常用的损失函数是均方误差(MSE),公式可以理解为“预测值和真实值差值的平方,再取平均”。

举个例子,某条样本的真实价格是100万,模型预测出94万,那么这条样本的误差就是(100-94)²=36。把所有样本的误差取平均,就是我们每轮训练要努力降低的那个数。

为什么用平方而不是绝对值的差值?因为平方运算会把大误差放得更大。预测偏差10万造成的损失是100,预测偏差1万造成的损失是1,前者是后者的100倍。这个特性让模型在训练时会优先把离谱的预测值拉回正轨,收敛更快。它的代价是loss曲线波动看起来比较大,因为少量异常样本也会对loss产生较大影响,这属于正常现象。

优化器负责根据损失计算出的梯度来更新模型参数。这里用最朴素的随机梯度下降SGD,它就像蒙着眼睛下山:算出当前位置的坡度,往最陡的方向迈一小步,反复迭代。步长就是学习率,太小走得太慢,太大可能一步跨过山脚甚至直接飞出山体。

criterion = nn.MSELoss() optimizer = torch.optim.SGD(model.parameters(), lr=0.01)

如果你用更现代的Adam优化器,通常可以把学习率设置得更小一些,比如0.001,收敛也更平稳。零基础阶段我建议先用SGD,因为它每一步的更新逻辑更直白,方便理解梯度是怎么作用的。

3.3 一个完整的训练循环

把数据变成深度学习框架能识别的张量格式,然后定义训练循环:

X_train_t = torch.tensor(X_train, dtype=torch.float32) y_train_t = torch.tensor(y_train, dtype=torch.float32).reshape(-1, 1) epochs = 500 loss_history = [] for epoch in range(epochs): pred = model(X_train_t) # 前向传播 loss = criterion(pred, y_train_t) # 计算损失 optimizer.zero_grad() # 清空上一轮梯度 loss.backward() # 反向传播,计算梯度 optimizer.step() # 更新参数 loss_history.append(loss.item()) if epoch % 50 == 0: print(f"epoch {epoch}, loss: {loss.item():.2f}")

这里有一个新手最容易踩的坑:y_train_t必须转成(160, 1)的形状,保持和模型输出形状一致,否则计算的loss会出现广播维度不匹配,或者得到一堆莫名其妙的值。

循环内部的顺序是固定的,不要随便换。特别是optimizer.zero_grad()这步,PyTorch默认会累积梯度,如果不清空,上一轮的梯度会叠加到下一轮上,导致参数更新步长越来越大,loss直接发散成NaN。

500个epoch跑完之后,把loss曲线画出来:

import matplotlib.pyplot as plt plt.plot(loss_history) plt.xlabel("epoch") plt.ylabel("MSE loss") plt.title("Training Loss Curve") plt.show()

一个健康的训练过程,loss应该先快速下降,然后逐渐变得平缓。如果看到loss一路上蹿下跳或者完全不动,问题大多出在数据归一化和学习率上,这正是下面要讲的关键内容。

4. 观察训练过程与关键调参细节

4.1 损失曲线怎么看

很多同学跑完训练只关心最后输出了多少数字,完全不看过程中的曲线,这是非常不推荐的做法。损失曲线是整个训练过程最诚实的记录,它能在第一时间暴露出模型有没有学进去。

一张正常的训练曲线通常分两段。第一阶段是前几十个epoch,loss快速下降,对应模型从“完全瞎猜”进入“基本发现规律”的过程。第二阶段是平缓段,loss在小范围内缓慢下降,说明模型开始做精细修正,在噪声背景下尽可能逼近真实关系。

如果曲线是始终高位震荡,跳来跳去不往下走,常见原因是学习率过大。如果曲线缓慢下降但斜率极低,几百个epoch都降不了多少,那基本是学习率过小。如果曲线先降后升,说明模型开始过拟合,即把训练数据里的噪声也背下来了,它在新数据上的表现反而会变差。

开始训练时别一口气关掉输出,正常观察每一步的loss变化,这比事后看评估指标更能快速定位问题。

4.2 学习率踩坑实录

学习率是深度学习里最值得花时间理解的一个超参数。我用同一份数据和同一个模型结构,只修改学习率,实测结果差异非常明显:

学习率训练表现主要原因
0.001下降太慢,500轮后仍有较大误差每次更新步长太小,还没走到最优区域
0.01正常下降,500轮能收敛得不错步长合适,梯度更新平稳
0.5损失直接变成NaN步长太大,参数更新越过合理范围导致数值溢出
1.0一开始就发散,loss狂增更新幅度比权重大几个量级,模型崩坏

为什么学习率大会导致NaN?假设某个参数的梯度是100,学习率是0.5,那么一次更新就把参数挪动了50。如果参数原本在10这个量级,更新后变成-40,再经过下一轮计算,数值可能爆炸到1e38以上,直接溢出浮点数能表达的范围,loss就会显示成inf或nan。

我踩过最深的坑是损失出现NaN之后第一反应是“数据有问题”,折腾半天才发现只是学习率设成了0.5。后来养成了习惯:任何训练跑飞,先把学习率降到原来的十分之一再重跑,这个操作能解决一多半的“玄学问题”。

这里还有一个经验之谈:不要一上来就用各种自动调参工具搜索最优学习率。先在1e-2、1e-3、1e-4这三个数量级上各跑一遍,只看loss曲线的下降形态,就能快速圈定大致方向,之后再微调即可。

4.3 归一化为什么能救回训练

如果你直接把面积和房龄的原始数值喂给模型,很可能发现loss曲线像只受伤的猴子在峭壁上乱蹦,怎么调学习率都不太听话。原因出在两个特征的数值量级差异太大:面积是40到150,房龄是0到50,权重初始化后对不同尺度特征的敏感度完全不同。

用一个生活类比来解释:两个人并排跑步,A的步长是1米,B的步长是0.5米,现在让两个人都跑“10步”,A能跑10米,B只能跑5米,你根本没法用同一个“步数”来衡量两个人。梯度下降面对不同尺度特征时,也会出现类似的失衡,表现为某个维度的权重更新过快,另一个维度又更新过慢。

解决办法是对特征做标准化,让它们都落在以0为中心、标准差为1的范围内:

mean = X_train.mean(axis=0) std = X_train.std(axis=0) X_train_norm = (X_train - mean) / std X_test_norm = (X_test - mean) / std

注意计算均值方差时只用训练集的数据,然后把同一组均值方差应用到测试集上。千万不要用全部数据一起算、再一起切分,那样等于把测试集的信息泄漏给了训练过程,得到的效果会被高估。这属于实践中最常见、也最容易忽略的细节。

做了归一化之后,即使不改变模型结构,只把学习率保持在0.01,训练曲线通常也会变得平滑很多,收敛速度显著加快。归一化不是锦上添花的技巧,而是神经网络训练的前提条件之一。

5. 常见问题与排查技巧实录

5.1 损失不降、NaN、预测值相同排查

零基础阶段跑回归,最容易撞见三类事故,我都逐一遇到过,这里整理成一张速查表,方便你按图索骥:

现象可能原因排查思路
损失一直不下降学习率过小;特征未归一化把学习率调大一个数量级,检查特征是否标准化
损失变成NaN学习率过大;参数数值溢出学习率除以10,检查是否有极端异常值
预测值几乎相同隐藏层神经元太少;网络表达能力不足增加隐藏层神经元数量,尝试加大网络深度
loss计算结果很奇怪标签形状没转成(样本数, 1)打印pred和y的shape对比,统一为二维
训练和测试表现差异极大测试集归一化用了不同参数统一用训练集的mean/std处理测试集

其中“预测值几乎相同”这一点非常隐蔽。有种情况是隐藏层只有两个神经元,模型学到的信息过少,所有样本输出都落在同一个均值附近。另外一种可能是模型根本没训练起来,参数还停留在初始状态。

排查时我建议先跑一个最简单的自查脚本:固定随机种子、不分割数据集,用全部数据训练50轮,打印每轮loss。如果这时候loss都不下降,说明问题出在模型配置或数据上;如果loss降了但测试集表现差,问题才转移到过拟合或数据划分上。

5.2 回归评估指标选哪个

训练结束后,你需要回答一个关键问题:模型到底预测得准不准?这个问题不能靠肉眼观察,得用数值指标。

回归任务不能用分类准确率,因为预测值几乎不可能和真实值完全相等,只要差一点点就算“错误”,准确率会趋近于零,没有参考价值。常用的指标有三个:

  • 均方根误差(RMSE):先对MSE开根号,让误差量纲回到原始数据的单位。比如价格误差是“万元”,RMSE也是“万元”,非常直观。它保留了MSE对大误差的惩罚特性。
  • 平均绝对误差(MAE):计算预测值和真实值差值的绝对值,再取平均。它不会把大误差放得那么大,更贴近直观感受。
  • (R^2)(决定系数):表示模型解释了数据中多少比例的方差。最大是1,越大越好;如果接近0,说明模型还不如直接用平均值当预测结果。

在实战中,我通常先用RMSE看量级是否合理,再用R2看拟合程度。它们的计算方式如下:

from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score pred_test = model(torch.tensor(X_test_norm, dtype=torch.float32)).detach().numpy() rmse = mean_squared_error(y_test, pred_test, squared=False) mae = mean_absolute_error(y_test, pred_test) r2 = r2_score(y_test, pred_test) print(f"RMSE: {rmse:.2f}") print(f"MAE: {mae:.2f}") print(f"R2: {r2:.4f}")

这里面有一个隐藏的坑:如果用PyTorch模型预测,结果是一个带梯度的张量,需要先调用detach()再转成NumPy数组,否则做sklearn评估时会出现类型不兼容的报错。我在这里卡过好几次。

5.3 训练后的可视化自检法

指标只能告诉你“好不好”,不能告诉你“哪里不好”。想要真正理解模型的预测行为,一定要把预测结果画出来,这一步很多人会偷懒,但它恰恰是最有价值的调试手段。

最常见、最有效的画法是以真实值为横轴、预测值为纵轴画散点图。如果模型表现良好,所有点会紧密地落在一条y=x的对角线附近。如果点都聚集在某个水平带里,说明模型存在系统性偏差,很可能学习不充分;如果训练集上效果不错但测试集上点明显散开,那基本可以断定过拟合。

plt.scatter(y_test, pred_test, alpha=0.6) plt.plot([y_test.min(), y_test.max()], [y_test.min(), y_test.max()], "r--") plt.xlabel("true price") plt.ylabel("predicted price") plt.show()

此外,还可以把训练收敛后的权重直接打印出来:

for name, param in model.named_parameters(): print(name, param)

虽然数值已经过非线性变换叠加,不能直接和真实规律的“50、-20”对比,但观察权重大小可以发现异常信号,比如某个权重的量级比其他权重大了好几个数量级,说明可能存在梯度爆炸或数据尺度问题。

这个小习惯我一直保留到现在,项目里排查模型bug时,先画图看分布,再回头查数据和代码,往往比凭空猜要快得多。

如果你能把这份代码完整跑通,并且能清楚地解释“为什么输出层不加激活函数”“为什么算均方误差”“为什么需要归一化”这三个问题,那么深度学习这条路上最关键的一扇门已经打开了。之后再遇到更复杂的网络,本质上还是这套流程的延伸:数据进模型,损失做裁判,梯度当向导,优化器负责走。最后再分享一个我在教学过程中反复验证过的小技巧:训练结束后,把预测曲线和真实点一起画出来,盯上十秒钟,比打印十行日志都更能帮助你理解模型到底学到了什么。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 6:22:56

hyperframes 帧索引方案:高密度帧数据处理与查询优化实践

1. 拆解 hyperframes:它到底是什么,能解决什么问题第一次看到 hyperframes 这个词,很多人会下意识地把它和“超高速”“超大规模”这类形容词联系起来,但真正落到项目层面,它指的是一套围绕高密度帧数据处理构建的轻量…

作者头像 李华
网站建设 2026/10/14 6:22:17

Linux编译安装三步曲:configure、make、make install全解析

如果你第一次在 Linux 下装软件,看到终端里一长串./configure && make && make install,八成会心里犯嘀咕:这是在搞什么鬼?为什么装个程序跟做饭似的,还得分步骤来?我把这套流程前前后后折…

作者头像 李华
网站建设 2026/10/14 6:22:16

投资222

产业逻辑

作者头像 李华
网站建设 2026/10/14 6:22:02

HanLP 实战:从通用到垂直领域的 NLP 工程化落地策略

1. 从零散笔记到系统认知:NLP 工程落地的真实面貌做 NLP 这行有个很有意思的现象:网上教程一抓一大把,从分词、词性标注到命名实体识别、依存句法分析,每个任务都有现成的开源工具和预训练模型,看起来门槛低得不行。但…

作者头像 李华
网站建设 2026/10/14 6:22:00

Scrapy分布式爬虫改造实战:从单机到多节点部署全解析

搞爬虫的人,谁没在单机 Scrapy 上吃过扩展性的亏?跑一个中型站点,动辄几十个并发,抓个几十万页面,先是 CPU 打满,再是内存告急,最后连数据库写入都开始排队。我最早接触分布式爬虫就是这个场景&…

作者头像 李华
网站建设 2026/10/14 6:21:43

VS Code AI 编程扩展连接第三方 API 的两种配置方式

2026 年了,VS Code 里的 AI 编程扩展早就不是新鲜东西。我平时主力就是 Claude 相关的那几款扩展,写代码、补注释、解释报错都靠它。最开始图省事,直接在扩展设置里填官方 Key,后来因为团队调整,官方额度收回、统一走内…

作者头像 李华