线性回归
从解析解到梯度下降的原理与手写实现
回归(regression)是一类能为一个或多个自变量与因变量之间关系建模的方法。在自然科学和社会科学领域,回归经常用来表示输入与输出之间的关系。
线性回归(linear regression)的历史可以追溯到 19 世纪初(最小二乘法由勒让德于 1805 年首次发表),它在回归的各种标准工具中最简单也最流行。线性回归基于几个简单的假设:首先,假设自变量 x 与因变量 y 之间的关系是线性的,即 y 可以表示为 x 中元素的加权和,这里通常允许包含观测值的一些噪声;其次,我们假设任何噪声都比较正常,例如噪声服从正态分布。
模型参数(model parameters)的求解需要两样东西:
- (1)一种模型质量的度量方式,通常为损失函数(loss function);
- (2)一种能够更新模型以提高预测质量的方法,通常包括求解解析解和随机梯度下降(stochastic gradient descent,SGD)两种方式。
解析解
线性回归刚好是一个很简单的优化问题。与其他大部分模型不同,线性回归的解可以用一个公式简洁地表达出来,这类解叫作解析解(analytical solution)。
首先,我们将偏置 b 合并到权重 w 中,方法是在包含所有参数的矩阵中附加一列。此时预测问题是最小化 ‖y − Xw‖²,这在损失平面上只有一个临界点,且对应于整个区域的损失极小值点。将损失关于 w 的导数设为 0,便可得到解析解:
w* = (XᵀX)⁻¹ Xᵀy这就是所谓的正规方程(normal equation)。
像线性回归这样的简单问题存在解析解,但并不是所有问题都存在解析解。解析解可以进行很好的数学分析,但它对问题的限制非常严格,因此在深度学习中难以广泛应用。
随机梯度下降
即使我们无法得到解析解,仍然可以有效地训练模型。在许多任务上,那些难以优化的模型反而效果更好,因此,弄清楚如何训练这些难以优化的模型就显得非常重要。
梯度下降(gradient descent)几乎可以用来优化所有的深度学习模型。它通过不断地在损失函数递减的方向上更新参数来降低误差。
梯度下降最简单的用法是计算损失函数(数据集中所有样本的损失均值)关于模型参数的导数(这里也称为梯度)。但直接这样做在实际中可能会非常慢:因为每一次更新参数之前,都需要遍历整个数据集。因此,我们通常会在每次需要计算更新时随机抽取一小批样本,这种变体叫做小批量随机梯度下降(minibatch stochastic gradient descent)。
二元线性回归模型训练实例
下面给出一个完全手写的二元线性回归模型训练实例,涵盖从数据生成到模型训练的全过程:
- 完全自主实现;
- 不使用任何机器学习框架;
- 手动实现梯度计算与参数更新。
训练过程:
- 手工计算均方误差(MSE)损失;
- 手动推导梯度计算公式:
- ∂Loss/∂w₁ = (2/n)Σ(y_pred − y_true)·x₁
- ∂Loss/∂w₂ = (2/n)Σ(y_pred − y_true)·x₂
- ∂Loss/∂b = (2/n)Σ(y_pred − y_true)
- 参数更新公式:θ ← θ − α·∇θ。
说明:下文代码中实现的是批量梯度下降(batch gradient descent),即每次更新遍历全部样本;其损失采用平方误差(未除以 2),梯度公式中的常数因子
2会被吸收进学习率,因此代码里的累积梯度写作error * x,更新时再除以n。
关于线性回归梯度下降的详细推导,可参考这篇文章:线性回归的求解:矩阵方程和梯度下降、数学推导及 NumPy 实现。
import random
# 数据生成函数(手动创建训练集)
def generate_data(num_samples=100):
# 真实参数:w1=2, w2=-3, b=5
w_true = [2, -3]
b_true = 5
data = []
for _ in range(num_samples):
# 生成两个特征(范围 0-10)
x1 = random.uniform(0, 10)
x2 = random.uniform(0, 10)
# 计算目标值并添加噪声
noise = random.gauss(0, 1) # 高斯噪声
y = w_true[0] * x1 + w_true[1] * x2 + b_true + noise
data.append(([x1, x2], y))
return data
# 模型定义
class LinearRegression:
def __init__(self):
# 手动初始化参数
self.w = [random.uniform(-1, 1) for _ in range(2)] # 权重
self.b = random.uniform(-1, 1) # 偏置项
def predict(self, x):
# 前向计算:y = w1*x1 + w2*x2 + b
return self.w[0] * x[0] + self.w[1] * x[1] + self.b
def train(self, data, learning_rate=0.01, epochs=100):
n = len(data)
for epoch in range(epochs):
total_loss = 0
grad_w = [0.0, 0.0] # 梯度累积
grad_b = 0.0
# 遍历所有样本(批量梯度下降)
for x, y_true in data:
# 计算预测值
y_pred = self.predict(x)
# 计算损失(平方误差)
loss = (y_pred - y_true) ** 2
total_loss += loss
# 计算梯度(手工求导,常数因子 2 被吸收进学习率)
error = y_pred - y_true
grad_w[0] += error * x[0]
grad_w[1] += error * x[1]
grad_b += error
# 参数更新(批量梯度下降)
self.w[0] -= learning_rate * (grad_w[0] / n)
self.w[1] -= learning_rate * (grad_w[1] / n)
self.b -= learning_rate * (grad_b / n)
# 打印训练进度
if (epoch + 1) % 10 == 0:
avg_loss = total_loss / n
print(f"Epoch {epoch + 1}/{epochs} | Loss: {avg_loss:.4f}")
# 训练流程
if __name__ == "__main__":
# 生成训练数据(10000 个样本)
train_data = generate_data(10000)
# 初始化模型
model = LinearRegression()
print("初始参数:")
print(f"w1={model.w[0]:.3f}, w2={model.w[1]:.3f}, b={model.b:.3f}")
# 开始训练
model.train(data=train_data, learning_rate=0.001, epochs=200000)
# 显示最终参数
print("\n训练后参数:")
print(f"w1={model.w[0]:.3f} (真实值:2.000)")
print(f"w2={model.w[1]:.3f} (真实值:-3.000)")
print(f"b={model.b:.3f} (真实值:5.000)")
# 测试预测
test_sample = [3.0, 4.0]
pred = model.predict(test_sample)
true_value = 2 * 3.0 + (-3) * 4.0 + 5
print(f"\n测试样本预测:{pred:.3f} (真实值:{true_value:.3f})")