ARTICLE DETAIL

资讯详情

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

从线性回归到深度学习:梯度下降与损失函数核心解析

从线性回归到深度学习:梯度下降与损失函数核心解析 线性回归这块内容可以说是机器学习里最基础也最容易被低估的一块。很多朋友一开始接触机器学习都是从线性回归开始觉得就是一个y wx b没什么好学的。但真到了后面接触深度学习你会发现那些看起来很复杂的神经网络本质上还是在做类似的事情——只不过把线性变换、非线性激活一层层堆叠起来。我自己从入门到做实际项目中间踩了不少坑也重新梳理过这条路线今天就顺着“从基础到深度学习”的线索把线性回归彻底讲透顺便说说它跟深度学习那些剪不断理还乱的关系。这篇文章适合三类人刚准备入门机器学习的初学者想把手写线性回归和框架实现串起来理解的人以及已经会用深度学习但回头想把基础概念补扎实的工程师。我会尽量把原理、代码、实操经验放在一起讲也把我在调参和排查时碰到过的问题列出来争取让你看完之后能自己动手复现一遍也能理解为什么深度学习框架里的某些做法跟线性回归的套路一脉相承。1. 线性回归到底在解决什么问题1.1 从预测房价讲起什么是“回归”我刚学机器学习的时候老师拿房价预测举例。假设面积越大房价越高我就想找一个函数输入面积输出房价。这不就是初中一次函数吗把点画在坐标系里用一条直线去拟合这些点让直线尽量靠近所有点这就是最简单的线性回归。但“回归”这个词不光是用在房价上。凡是“根据已知数据预测一个连续数值”的问题都可以称作回归问题。比如预测明天气温是25度还是30度预测一件衣服的销量是100件还是200件预测一辆车的油耗是6.5升还是7.8升。这些输出都是连续数值不像“这张图里有猫还是没猫”那种分类问题。线性回归就是回归问题里最直接的一种建模方式它假设目标值和特征之间满足线性关系。那句y wx b里w叫权重或者回归系数b叫偏置或者截距。x是输入特征y是预测结果。如果输入特征不止一个比如预测房价时既看面积又看房龄那公式就变成y w1*x1 w2*x2 b。更高维度也一样写成矩阵形式就是y Xw b。这里面的核心思想就是通过已知数据把w和b给“学”出来。1.2 损失函数怎么衡量“拟合得好不好”有了模型怎么判断w和b选得好不好最自然的想法是把预测值和真实值放在一起比较看差了多少。差得越小模型越好。常用的误差度量是均方误差Mean Squared Error简称MSE。假设真实值是y_true预测值是y_pred对每个样本算出(y_true - y_pred)^2然后求平均。平方的好处是避免正负误差抵消同时也放大了大误差的影响。这个函数在机器学习里就叫损失函数Loss Function或者叫代价函数。线性回归的优化目标就是找到一组w, b让均方误差最小。用数学语言表达就是minimize (1/n) * sum((y_true - (w*x b))^2)这里面有一个很关键的点均方误差函数是一个凸函数也就是说它只有一个全局最小值点。这跟后来深度学习里的非凸损失函数不一样深度学习的损失函数有很多局部极小值训练的时候可能卡在某个坑里出不来。而线性回归的损失函数是“碗形”的只要优化方法得当就一定能找到全局最优解。这也是线性回归作为入门内容特别好的原因它把优化最核心的思想暴露给你看却没有那些干扰复杂度。1.3 最小二乘法与梯度下降两种找最优解的思路找最优解有两条主流路线。一条是解析法学名叫最小二乘法Ordinary Least Squares。它的思路是直接通过数学推导把损失函数对w和b求导令导数为零解方程得到唯一解。公式写出来是w (X^T X)^{-1} X^T y。这个方法在小数据量、特征维度不高时很管用计算一遍就出结果不需要迭代。但它的局限性也很明显当特征数量非常多甚至比样本数量还多的时候X^T X可能不可逆计算复杂度也高得吓人。另一条路线是梯度下降Gradient Descent。它的思路更“笨”也更通用随便初始化一组w, b然后计算损失函数对它们的偏导数也就是梯度。梯度指向损失上升最快的方向那我们沿着反方向迈一小步就能让损失下降。重复这个步骤直到损失不再明显变化。梯度下降是深度学习训练的基石。你在深度学习框架里跑到optimizer.step()本质上就是在做这件事用小批量数据的梯度来更新模型参数。之所以不用最小二乘法直接求解是因为神经网络太复杂损失函数根本解不出解析解只能靠迭代一点点逼近。所以从线性回归到深度学习的一条重要脉络就是“最小二乘法只适用于简单线性模型梯度下降适合一切可导模型”。2. 从手算到Python实现把线性回归跑起来2.1 数据准备与特征处理纸上谈兵没意思真正动手写代码才算数。我习惯从模拟数据开始因为数据是已知生成的能对照验证模型的正确性。下面我用Python生成一组带噪声的线性数据import numpy as np np.random.seed(42) X np.random.rand(100, 1) * 10 # 特征0到10之间 true_w 2.5 true_b 1.0 y true_w * X true_b np.random.randn(100, 1) * 2.0这里生成的数据真实关系就是y 2.5*x 1额外加上高斯噪声来模拟现实中的不完美观测。我们的目标就是从X和y中把2.5和1.0尽量准确地恢复出来。在开始训练之前通常需要做特征缩放。为什么如果有多个特征而且它们的取值范围差异很大比如一个特征在0~1之间另一个在0~100000之间那梯度下降时更新的步伐会被大范围特征主导导致收敛很慢。简单有效的做法就是标准化让数据均值为0标准差为1。X_mean X.mean() X_std X.std() X_norm (X - X_mean) / X_std实操经验是即使只有一个特征做标准化也能让梯度下降的收敛更加平稳尤其是学习率稍大的时候。后面在深度学习中有一个常见的Batch Normalization层思想源头也能追溯到这种简单的数据归一化。2.2 手写梯度下降全过程这里我用手写方式实现线性回归训练不调用任何现成库的LinearRegression完全从零开始这样才能彻底理解每一步。基本流程是初始化参数w和b我习惯初始化为0。计算预测值y_pred w * X b。计算损失 MSE。计算梯度对w的梯度(2/n) * sum((y_pred - y_true) * X)对b的梯度(2/n) * sum(y_pred - y_true)更新参数w w - lr * grad_wb b - lr * grad_b重复2~5步若干轮。写出来就是def linear_regression_gd(X, y, lr0.01, epochs1000): n len(y) w 0.0 b 0.0 history [] for epoch in range(epochs): y_pred w * X b loss np.mean((y_pred - y) ** 2) grad_w (2 / n) * np.sum((y_pred - y) * X) grad_b (2 / n) * np.sum(y_pred - y) w - lr * grad_w b - lr * grad_b if epoch % 100 0: history.append((epoch, loss, w, b)) return w, b, history w_learned, b_learned, _ linear_regression_gd(X_norm, y, lr0.05, epochs1000)这里有两个细节容易出错。第一如果数据没有标准化lr0.05很可能导致梯度爆炸损失变成无穷大。我推荐第一次跑模拟时先给数据标准化或者更稳妥地使用lr0.01多看几次损失曲线再调。第二梯度公式里的2/n是从MSE求导得来的不要漏掉。如果忽略了相当于每一步的梯度都放大了可能导致震荡。2.3 超参数选择学习率与迭代次数学习率是调参里最重要的一个超参数。我见过不少初学者数据没问题公式没问题就是损失一直不降最后发现是学习率设得太小跑几百轮都挪不动。反之学习率太大损失曲线像过山车甚至直接变成NaN。怎么判断学习率合不合适最简单的方式是打印每轮的损失值观察它的变化趋势如果损失一直平稳下降最后趋于平缓说明学习率合适或略偏小可以加大一点加速收敛。如果损失震荡或者上升说明学习率太大需要调低。如果损失下降非常慢几百轮都没什么变化说明学习率太小。迭代次数设多少没有一个固定答案。更科学的做法是用早停Early Stopping思路每次记录验证集表现当验证损失不再下降时停止训练。线性回归里没有验证集也能用训练损失判断但工程上养成分训练集、验证集的好习惯很重要。我自己的经验是先用少量轮数比如200轮快速看梯度方向是否正常再根据损失曲线决定要不要加轮数。比起固定跑一万轮这样更省时间也更能看出问题。3. 线性回归到深度学习的桥梁3.1 为什么说线性回归是神经网络的神经元如果把线性回归的计算过程画成一个结构图你会发现它长得就像一个神经元输入x乘以权重w加上偏置b输出y。深度学习中一个最基本的神经网络单元就是这样组成的。只不过有个关键区别神经网络单元在线性计算之后通常会加一个非线性激活函数比如ReLU、Sigmoid。为什么必须加非线性想象一下如果每一层神经元的计算都是线性的那不管叠加多少层最终效果还是线性变换。多层就没有意义了不如直接用一层。所以深度学习的模型中每个神经元往往先做wx b再套一个激活函数这样才能让网络逼近任意复杂的函数。这个视角特别重要。线性回归本质上是“单层、线性、无激活函数”的神经网络。深度学习中很多模型比如全连接神经网络的第一层在对输入数据做线性变换时跟你手写的线性回归完全一样只是后面接了什么操作不同。3.2 从线性到非线性激活函数的出现当我们要处理的问题不是“一条直线”能解决的比如预测房价时面积增长到一定程度后价格增幅变缓那纯线性拟合就会偏差很大。这时候就需要非线性。深度学习的做法是堆叠多层线性变换层间插入非线性激活函数。拿ReLU举例它的公式是max(0, x)特别简单。如果网络中有几百个神经元每个神经元都做“线性加权 ReLU”整个网络就能实现分段线性拟合只要神经元的数量足够多它可以逼近任意形状的函数。这也是为什么深度学习表达能力强大而线性回归只能拟合直线。但反过来看深度学习模型往往更难训练参数更多损失函数更复杂非常容易过拟合和陷入局部最优。从线性回归转换到深度学习的思路可以一步一步来先在一个隐藏层里加几个神经元和ReLU实现一个简单MLP再把隐藏层增加到多层就真正进入深度学习领域。我建议所有学深度学习的人都用“线性回归—单层感知机—多层感知机—深度网络”这条路线走一遍不要一上来就搭ResNet。把每一步中“线性变换”和“非线性激活”的角色搞清楚后面学CNN、RNN、Transformer都会顺利很多。3.3 深度学习中依然存在的“回归思维”虽然深度学习通常和图像分类、自然语言处理绑在一起但回归任务在深度学习里同样常见比如自动驾驶预测距离、电商预测点击率、气象预测气温。深度学习模型的最后一个输出层如果不要激活函数直接输出一个数值那就是在做回归任务。它的损失函数也经常用MSE跟线性回归一模一样。还有一个很有意思的地方线性回归的损失函数的梯度形式和神经网络中反向传播计算梯度的基本单元是一致的。在反向传播中每一层都会计算损失对输出的梯度然后利用链式法则往前传。而链式法则的最基础应用就是对线性函数求导。如果你能熟练推导线性回归的梯度公式那么理解反向传播的数学原理就不会太吃力。4. 动手玩转从玩具数据到深度学习框架4.1 用PyTorch实现线性回归学深度学习的人迟早会接触到PyTorch或TensorFlow。在这些框架里实现线性回归比手写要简单得多但正因为简单新手反而容易迷迷糊糊。我就用PyTorch实现一遍重点在于让你看清框架的每一行对应手写版本的哪一步。先安装并导入框架import torch import torch.nn as nn import torch.optim as optim定义模型nn.Linear(1, 1)表示输入1维、输出1维它内部就含有一个权重和偏置跟我们手写的w和b等价class LinearRegressionModel(nn.Module): def __init__(self): super().__init__() self.linear nn.Linear(1, 1) def forward(self, x): return self.linear(x) model LinearRegressionModel()然后定义损失函数和优化器criterion nn.MSELoss() optimizer optim.SGD(model.parameters(), lr0.01)训练循环也很简单for epoch in range(1000): model.train() optimizer.zero_grad() y_pred model(X_tensor) loss criterion(y_pred, y_tensor) loss.backward() optimizer.step() if epoch % 100 0: print(fepoch {epoch}, loss {loss.item():.6f})看到没有框架帮你把梯度计算、参数更新都封装起来了你只需要做三件事调用loss.backward()算梯度调用optimizer.zero_grad()清空旧梯度调用optimizer.step()更新参数。这三步的顺序初学者经常搞不清记住每次更新前一定要把梯度清零否则梯度会累加很快模型就崩了。4.2 把模型“变深”搭建一个简单的多层感知机当你理解了线性回归的框架化实现升级到深度学习模型其实就只是改模型定义部分。比如我们搭建一个含有一个隐藏层的神经网络输入1维隐藏层64个神经元用ReLU激活输出层1维class SimpleMLP(nn.Module): def __init__(self): super().__init__() self.hidden nn.Linear(1, 64) self.act nn.ReLU() self.out nn.Linear(64, 1) def forward(self, x): x self.hidden(x) x self.act(x) x self.out(x) return x训练代码基本不变。区别在于模型更复杂了需要的数据量更大训练时间更长也可能出现更多训练问题。如果你用线性回归能拿到很好的结果就不要贸然上深度模型。我先用线性回归拟合一个线性数据效果很好再同一个数据上去跑MLP会发现也能拟合但参数多、容易过拟合。工程上一个重要原则是先用最简单模型建立基线再逐步增加复杂度。这也是我把线性回归看作“一切模型基线”的原因。4.3 从单特征到多特征的扩展真实问题往往不止一个特征。多特征时nn.Linear的输入维度要改成特征数。比如假设我们有面积、房龄、楼层三个特征输入维度是3输出是1。模型定义变成nn.Linear(3, 1)。这时候有个需要注意的地方不同特征的量纲可能差很多。面积可能是上百房龄是几年楼层是个位数。如果一起输入模型梯度更新会偏向数值大的特征。之前我提过特征标准化在深度学习里同样重要。PyTorch中没有内置简单的标准化层习惯做法是在数据预处理阶段用sklearn或手动方式处理数据把每个特征都缩放到均值0、方差1。有人会问深度学习不是有BatchNorm吗它放在层与层之间主要解决中间层的分布漂移问题但输入层的原始特征如果量纲差太大依然会影响训练稳定性。所以在任何深度学习项目开始前第一件事就是检查数据分布该归一化就归一化。5. 常见问题与排查技巧实录5.1 损失不降反升怎么办这是所有模型训练里最让人头疼的问题线性回归也不例外。我总结过的排查路径是这样的打印初始损失确认损失计算是否正确。拿手写回归举例如果一开始损失就是天文数字很可能是y_true和y_pred的单位不一致或者数据里混入异常值。看梯度值的大小。如果梯度过大说明学习率太高或者特征没归一化。尝试把学习率降到非常小比如1e-6看损失曲线是否变成平滑下降。如果还是上升那说明代码有bug优先检查梯度公式和参数更新顺序。我还碰到过一个很隐蔽的错误把w - lr * grad_w写成w w - lr * w.grad如果用NumPy手写没问题但如果用PyTorch的autograd就要注意是否把w.grad清零了。不清零的话下一次的梯度是累加的损失会震荡甚至爆炸。这也是为什么框架训练循环里有optimizer.zero_grad()这一步。5.2 过拟合线性回归也有过拟合问题很多人的印象里过拟合是深度学习的专属问题线性回归这种简单模型怎么也会过拟合其实会尤其是特征很多而样本很少的时候。比如你收集了100条样本但特征有200个线性回归可以学出一组刚好把所有训练点都穿过的参数损失接近0但换一批新数据就崩了。解决思路有这么几个增加样本量或者用数据增强虽然对表格数据来说增强不容易。减少特征数量。先做特征选择把相关性低或者共线性强的特征去掉。使用正则化。线性回归加L2正则化就成了岭回归加L1则是Lasso。正则化的本质是在损失函数里加一个参数大小的惩罚项逼迫模型参数不要过大从而降低过拟合。在深度学习中这个思想演变成了weight_decay权重衰减。在PyTorch里设置optim.SGD(..., weight_decay0.01)即可。它跟线性回归中的L2正则化是同一个数学原理。所以你看基础模型里藏着深度学习的所有核心概念。5.3 特征缩放一个操作解决80%的训练问题我单独把特征缩放拿出来讲是因为它太常被忽视。一个没有归一化的多特征数据集即使把学习率调来调去也可能很难收敛。而归一化之后学习率0.01或者0.1通常都能得到不错的结果。归一化的常用方法有标准化Z-score(x - mean) / std适用于数值分布接近正态的特征。归一化Min-Max(x - min) / (max - min)把数据映射到0~1之间适用于有明确上下界的特征。对长尾分布做对数变换比如销售额、房价等数据往往呈长尾直接标准化效果不佳先log(x1)再标准化会更好。我个人在深度学习项目里的习惯是先画特征分布图看到偏态明显的就做对数变换或Box-Cox变换然后统一标准化。这一步做完训练的稳定性会有立竿见影的提升。5.4 代码实战中的小坑清单最后整理一份我踩过的坑虽然琐碎但每一条都花过不少时间排查PyTorch训练时忘了调用model.train()虽然线性回归影响不大但某些层比如Dropout、BatchNorm在不同模式下行为不同。养成习惯训练前调用model.train()推理前调用model.eval()。使用optimizer.zero_grad()的时机不对。必须在调用loss.backward()之前清空上一轮梯度。放后面清就相当于这轮梯度没被更新下轮却清掉模型永远不会收敛。学习率设置太大导致loss变成nan。可以先设lr0.001试跑损失下降就再调大损失爆炸就调小。手写梯度时维度没对齐矩阵乘法很容易报错或者得到形状不对的结果。建议先用np.random.randn造个小数据跑通再换成真实数据。对代码里每个变量的shape做到心中有数是(n,)还是(n,1)会影响np.sum和广播。对于PyTorch尽量统一用(batch_size, features)的形状。6. 线性回归在复杂模型中的回归应用6.1 保存与部署让模型立刻可用训练完模型总不能只停在Jupyter Notebook里。我自己通常会把模型保存下来再写一个预测函数方便以后调用。如果是PyTorch模型保存有两种方式torch.save(model.state_dict(), model.weights)保存权重或者torch.save(model, model.pkl)整个模型。推荐保存state_dict它只保存参数不保存结构加载的时候需要先定义好同样的模型结构。加载代码model LinearRegressionModel() model.load_state_dict(torch.load(model.weights)) model.eval()部署的话如果是Python服务直接把模型加载到内存里写一个HTTP接口就行。如果对延时敏感可以考虑转成ONNX格式用ONNX Runtime推理速度比PyTorch原生快很多。这部分对纯学习线性回归的人可能用不上但如果工作里要把一个预测模型上线这个思路一定用得上。6.2 线性回归的现代兄弟正则化与鲁棒回归单纯线性回归在工业界其实很少直接使用因为数据很难满足“线性、独立、同方差”这些经典假设。实战中常用它的扩展岭回归在高维数据下比普通线性回归稳定系数不会离谱。Lasso回归会自动让一部分系数变为0相当于自动特征选择。ElasticNet结合上面两者适合特征间存在相关性的情况。Huber回归损失函数对异常值不像MSE那样敏感适合存在离群点的数据。如果你有建模需求我的建议是先用普通线性回归跑通流程看数据是否线性可分如果出现过拟合用岭回归如果特征多且稀疏用Lasso。这些方法在sklearn里都有现成实现直接用LinearRegression、Ridge、Lasso就能调。它们的数学原理和线性回归一脉相承学会线性回归后这些扩展很容易理解。深度学习模型做回归时同样会出现类似问题。比如损失函数选择MSE时对异常值敏感这时可以用Huber Loss替代。PyTorch里nn.SmoothL1Loss就相当于Huber Loss把两个不同范数的损失分区域结合。这些都是从线性回归时代传承下来的经验。6.3 从线性回归到深度学习的最终理解如果说线性回归的核心是“用一条线去拟合数据”那深度学习的核心就是“用很多条线段拼成的曲线去拟合数据”。前者的局限在于线性的表达能力后者的强大在于层级化的非线性拟合。但两者的训练过程都是定义模型、定义损失、计算梯度、更新参数。只要你把线性回归的这几个步骤彻底理解透再看深度学习模型其实没有本质区别只是规模和复杂度上升了。我也遇到过一些从深度学习反推回来的开发者他们习惯用nn.Linear但不知道nn.Linear里封装的就是y wx b。当他们回头理解线性回归以前觉得玄乎的“梯度清零”“反向传播”突然就清晰了。这就是基础知识的威力。所以如果你正在学深度学习千万别跳过线性回归。花一个周末时间手写一遍梯度下降再用PyTorch实现一遍对照看每一步在做什么。然后试着加一层隐藏层换一个激活函数看看模型发生了什么变化。等你把这条线走通再去学CNN也好学Transformer也好都不会再有“空中楼阁”的感觉。最后再分享一个我经常用的土办法在新数据集上跑任何模型之前我都会先跑一个线性回归作为基线。如果线性回归都表现很差那大概率是特征工程没做好或者数据分布有坑如果线性回归表现尚可那再去上深度学习模型才有对比价值。很多人一上来直接上大模型结果效果还不如线性回归就开始怀疑人生。其实线性回归这个基线测试能帮你省下无数个迷茫的夜晚。
返回列表