线性回归
线性回归是最基础的机器学习算法,用来学习特征 与目标值 之间的线性关系:
其中 是权重(斜率), 是偏置(截距)。目标是学出一组 ,让模型的预测值尽量接近真实值。
本 notebook 用三种方式实现线性回归,方便对比不同工具的使用方法:
- PyTorch 实现:利用自动微分和优化器,快速搭建线性模型
- NumPy 从零实现:手动推导并实现成本函数、梯度与梯度下降,理解算法原理
- scikit-learn 实现:多项式线性回归,一行代码完成训练
学习目标:看懂每一行代码,理解「建模 → 训练 → 评估 → 可视化」的完整流程。
一、使用 PyTorch 实现线性回归
1. 导入库与基础设置
先导入 PyTorch(张量与神经网络)、NumPy(数值计算)和
Matplotlib(绘图),并设置中文字体避免乱码。torch.manual_seed(42) 与
np.random.seed(42) 固定随机种子,保证每次运行结果一致。
# 用pytorch实现线性回归
import torch
import torch.nn as nn
import numpy as np
import matplotlib.pyplot as plt
#pyplot隐式实例化,Matplotlib 在背后自动创建并管理了两个核心对象:画布和子图。
# plt 本身并不是一个 “实例对象”,而是一个 模块(module),它内部维护了一套 “全局状态管理机制”
# 设置中文字体,解决中文显示问题
plt.rcParams["font.family"] = ["SimHei"]
# 解决负号显示问题
plt.rcParams['axes.unicode_minus'] = False # 部分中文字体对 Unicode 负号字符的支持不完善,导致负号无法正确渲染。默认值为 True,表示使用 Unicode 编码的负号(−,U+2212)。
# 设置为 False 后,会改用 ASCII 编码的减号(-,U+002D),这种符号在几乎所有字体中都能正常显示,避免了负号乱码问题。
# 设置随机种子,保证结果可复现
torch.manual_seed(42) #设置后,每次运行程序时,PyTorch 生成的随机数序列会完全相同。
np.random.seed(42) #为 NumPy 库设置随机种子
2. 生成模拟数据
用一个已知的线性关系生成训练数据:
true_w = 2.5、true_b = 1.8是”真实参数”,用来验证学习结果- 是 之间的 100 个随机点, 上叠加了高斯噪声,模拟真实世界中”数据不完美”的情况
- 注意 PyTorch 的广播机制:标量 会自动扩展成与 相同的形状参与逐元素运算
# 1. 生成模拟数据
# 真实的权重和偏置
true_w = 2.5
true_b = 1.8
# 生成输入数据x,将 NumPy 数组(numpy.ndarray)转为 PyTorch 张量(torch.Tensor)
# torch.tensor 是 PyTorch 中用于创建 “张量(Tensor)” 的基础函数。Tensor是 PyTorch 处理数据的基本单位(类似 NumPy 中的 ndarray)。
# 它不仅能存储数值,还支持 自动微分(反向传播的核心)、GPU 加速计算等深度学习必需的功能
x = torch.tensor(np.random.rand(100, 1) * 10, dtype=torch.float32)
# 生成带有噪声的目标值y。所有参与运算的元素最终都会统一为 torch.float32 类型的张量。
# 广播机制!形状不匹配但 “兼容” 的张量,能进行 element-wise(逐元素)运算
# yTorch 的广播是「逻辑上的扩展」,不会实际复制数据,只是在计算时 “虚拟地” 将标量的数值应用到每个位置
y = true_w * x + true_b + torch.tensor(np.random.randn(100, 1) * 2, dtype=torch.float32)
3. 定义线性回归模型
nn.Module 是所有 PyTorch 模型的基类。我们的模型内部只包含一个
nn.Linear(in_features=1, out_features=1):输入一个特征,输出一个预测值,即
。forward 方法定义前向传播,模型会在训练时自动学习层内的
和 。
# 2. 定义线性回归模型
# nn.Module 是 PyTorch 中 所有神经网络模型的 “基类”(可以理解为 “模板”),它封装了模型训练所需的核心功能,比如:
#自动管理模型中的可训练参数(如线性回归的 w 和 b);支持前向传播(forward 方法)的调用逻辑;
# 提供参数初始化、模型保存 / 加载、GPU 迁移等工具函数;配合自动微分(autograd)实现反向传播时的梯度计算。
# nn指神经网络
class LinearRegressionModel(nn.Module):
def __init__(self):
super(LinearRegressionModel, self).__init__()
# 定义一个线性层,输入维度1,输出维度1
self.linear = nn.Linear(in_features=1, out_features=1)
def forward(self, x):
# 前向传播
return self.linear(x)
# 实例化模型
model = LinearRegressionModel()
4. 定义损失函数与优化器
- 损失函数:均方误差(MSE),衡量预测与真实的差距
- 优化器:随机梯度下降(SGD),学习率
lr=0.01,决定每次参数更新的步长
# 3. 定义损失函数和优化器
# 使用均方误差作为损失函数
criterion = nn.MSELoss()
# 使用随机梯度下降(SGD)作为优化器,学习率0.01
# 每次更新参数前,不从全部样本中计算梯度,而是随机挑选部分样本。后面可以指定选取全部。
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
5. 训练模型
训练循环是深度学习的核心套路,共四步:
- 前向传播:
y_pred = model(x)得到预测值 - 计算损失:比较预测与真实
- 反向传播:
loss.backward()自动计算梯度 - 参数更新:
optimizer.step()沿负梯度方向更新 、
记得每轮先 optimizer.zero_grad() 清零梯度,避免多次累加。每训练 10
轮打印一次损失和参数,观察收敛过程。
# 4. 训练模型
epochs = 30 # 训练轮次
losses = [] # 记录损失变化
for epoch in range(epochs):
# 前向传播:计算模型预测值
y_pred = model(x) # 全量
# 计算损失
loss = criterion(y_pred, y)
losses.append(loss.item())
# 反向传播和优化
optimizer.zero_grad() # 清零梯度
# 在 PyTorch 中,当调用 loss.backward() 时,计算出的梯度会自动累加到参数的 .grad 属性中(而不是直接覆盖)
# 自动累加可以节约显存
loss.backward() # 反向传播计算梯度
optimizer.step() # 更新参数
# 每100轮打印一次损失和参数值
if (epoch + 1) % 10 == 0:
# 获取当前的权重和偏置
w, b = model.parameters()
# f-string 是 Python 3.6+ 引入的字符串格式化方式,以 f 或 F 开头,字符串内部通过 {变量/表达式} 嵌入动态内容
print(f'Epoch [{epoch+1}/{epochs}], Loss: {loss.item():.4f}, w: {w.item():.4f}, b: {b.item():.4f}')
6. 查看学习到的参数
训练结束后,把学习到的 、 与真实参数对比。学习率合适时,两者应当比较接近(噪声会让结果有轻微偏差)。
# 5. 输出最终学习到的参数和真实参数
# str.format() 是 Python 中用于字符串格式化的经典方法(适用于所有 Python 3 版本,兼容性比 f-string 更广)。其核心逻辑是:
#先定义一个「带占位符的字符串模板」(占位符用 {} 表示);
# 调用 format() 方法,传入需要填充到占位符的变量;变量会按顺序(或按占位符名称)替换占位符,生成最终的格式化字符串。
print("\n真实参数: w = {:.4f}, b = {:.4f}".format(true_w, true_b))
w_final, b_final = model.parameters()
print("学习到的参数: w = {:.4f}, b = {:.4f}".format(w_final.item(), b_final.item()))
7. 可视化拟合结果
- 上图:散点展示原始数据,红色直线展示拟合效果
- 下图:绘制损失随训练轮次下降的曲线,验证模型确实在收敛
detach() 的作用是把张量从计算图中剥离,再用 .numpy() 转成 Matplotlib
可用的 NumPy 数组。
# 6. 可视化结果
# 生成预测值
# .detach():将张量从计算图中 “剥离”(PyTorch 的张量默认会记录计算历史用于反向传播),此时后续转换为 NumPy 数组时避免因关联计算图而报错。
# .numpy():将 PyTorch 张量转换为 NumPy 数组(numpy.ndarray),因为 Matplotlib 绘图(plt)仅支持 NumPy 数组或 Python 基本类型的数据绘图。
y_pred = model(x).detach().numpy()
plt.scatter(x.numpy(), y.numpy(), label='原始数据') #Matplotlib 中绘制散点图的函数,用于展示原始数据的分布。
plt.plot(x.numpy(), y_pred, 'r-', label='拟合直线') #plt.plot:Matplotlib 中绘制线图的函数,用于展示模型的预测结果(拟合直线)。
plt.xlabel('x')
plt.ylabel('y')
plt.title('线性回归拟合结果')
plt.legend() #显示图例(legend)
plt.show()
plt.subplot(2,1 , 2) # 是 Matplotlib 中用于创建子图(Subplot) 的核心函数,作用是在同一个画布(Figure)中划分出多个独立的绘图区域,
#并指定当前要绘制的 “子图位置”。1,2,2指 划分为1行2列,当前操作子图编号2
plt.plot(range(epochs), losses)
plt.xlabel('训练轮次')
plt.ylabel('损失值')
plt.title('损失随训练轮次的变化')
plt.tight_layout()
plt.show()
二、使用 NumPy 从零实现线性回归
1. 导入库与基础设置
与第一部分不同,这里不使用任何深度学习框架,只用 NumPy 手工实现线性回归的每一步,真正理解算法内部发生了什么。
# 用纯numpy实现线性回归
import copy
import math
import numpy as np
import matplotlib.pyplot as plt
# 设置中文字体,解决中文显示问题
plt.rcParams["font.family"] = ["SimHei"]
# 解决负号显示问题
plt.rcParams['axes.unicode_minus'] = False
2. 生成示例数据函数
generate_sample_data 生成 个样本、
个特征的多维线性回归数据:
返回特征矩阵 、目标值 以及用于生成数据的”真实参数” 。
# 生成示例数据(模拟真实世界的多特征数据集)
def generate_sample_data(m=100, n=3, random_state=42):
"""生成用于测试的样本数据
Args:
m: 样本数量
n: 特征数量
random_state: 随机种子,保证结果可复现
Returns:
X: 特征矩阵 (m, n)
y: 目标值向量 (m,)
true_w: 真实权重,用于生成数据
true_b: 真实偏置,用于生成数据
"""
np.random.seed(random_state)
# 生成真实参数
true_w = np.random.randn(n) * 10 # 随机权重
true_b = np.random.randn() * 5 # 随机偏置
# 生成特征数据(正态分布)
X = np.random.randn(m, n)
# 生成目标值,加入一些噪声
y = np.dot(X, true_w) + true_b + np.random.randn(m) * 2
return X, y, true_w, true_b
3. 成本函数(均方误差)
成本函数衡量当前参数 下模型的表现:
分母取 是为了让后续求导更简洁。目标是让 尽量小。
def compute_cost(X, y, w, b):
"""
计算线性回归的成本(均方误差损失)
Args:
X (ndarray (m,n)): 输入数据,m个样本,每个样本有n个特征
y (ndarray (m,)) : 目标值向量
w (ndarray (n,)) : 模型权重参数
b (scalar) : 模型偏置参数
Returns:
cost (scalar): 计算得到的成本值
"""
m = X.shape[0] # 获取样本数量
cost = 0.0
for i in range(m):
# 计算第i个样本的预测值
f_wb_i = np.dot(X[i], w) + b
# 累加平方误差
cost = cost + (f_wb_i - y[i])**2
# 计算平均成本(除以2m是为了后续求导方便)
cost = cost / (2 * m)
return cost
4. 梯度函数
梯度是成本函数对每个参数的偏导数,指向成本增长最快的方向,负梯度就是下降方向:
\frac{\partial J}{\partial b} = \frac{1}{m}\sum_{i=1}^{m}\left(f_{w,b}(x^{(i)}) - y^{(i)}\right)$$ 代码用两层循环逐个样本累加误差,最后除以 $m$ 取平均。 ``` python def compute_gradient(X, y, w, b): """ 计算线性回归的梯度 Args: X (ndarray (m,n)): 输入数据,m个样本,每个样本有n个特征 y (ndarray (m,)) : 目标值向量 w (ndarray (n,)) : 模型权重参数 b (scalar) : 模型偏置参数 Returns: dj_dw (ndarray (n,)): 成本函数对每个权重w的偏导数(梯度) dj_db (scalar): 成本函数对偏置b的偏导数(梯度) """ m, n = X.shape # m是样本数,n是特征数 dj_dw = np.zeros((n,)) # 初始化权重梯度为0 dj_db = 0.0 # 初始化偏置梯度为0 for i in range(m): # 计算第i个样本的预测误差 err = (np.dot(X[i], w) + b) - y[i] # 计算每个权重的梯度 for j in range(n): # 成本函数对w_j的偏导数为:(err)·X[i,j] dj_dw[j] = dj_dw[j] + err * X[i, j] # 计算偏置的梯度 dj_db = dj_db + err # 求平均梯度 dj_dw = dj_dw / m dj_db = dj_db / m return dj_db, dj_dw ``` ### 5. 梯度下降主循环 参数更新规则: $$w \leftarrow w - \alpha \frac{\partial J}{\partial w},\quad b \leftarrow b - \alpha \frac{\partial J}{\partial b}$$ 其中 $\alpha$ 是学习率。`gradient_descent` 每轮计算梯度并更新参数,同时记录成本历史,方便观察收敛过程。 ``` python def gradient_descent(X, y, w_in, b_in, cost_function, gradient_function, alpha, num_iters): """ 执行批量梯度下降算法来学习模型参数,通过num_iters次迭代更新参数,学习率为alpha Args: X (ndarray (m,n)) : 输入数据,m个样本,每个样本有n个特征 y (ndarray (m,)) : 目标值向量 w_in (ndarray (n,)) : 初始权重参数 b_in (scalar) : 初始偏置参数 cost_function : 计算成本的函数 gradient_function : 计算梯度的函数 alpha (float) : 学习率 num_iters (int) : 迭代次数 Returns: w (ndarray (n,)) : 更新后的权重参数 b (scalar) : 更新后的偏置参数 J_history: 每次迭代的成本值,用于后续可视化 """ # 存储每次迭代的成本值,用于后续绘图分析 J_history = [] # 深拷贝初始参数,避免修改外部变量 w = copy.deepcopy(w_in) b = b_in for i in range(num_iters): # 计算当前参数下的梯度 dj_db, dj_dw = gradient_function(X, y, w, b) # 使用梯度下降更新参数 w = w - alpha * dj_dw b = b - alpha * dj_db # 保存当前迭代的成本值(防止过多迭代导致内存耗尽) if i < 100000: J_history.append(cost_function(X, y, w, b)) # 定期打印成本值,监控训练进度 # 总共打印10次,或者如果迭代次数少于10,则每次迭代都打印 if i % math.ceil(num_iters / 10) == 0: print(f"迭代次数 {i:4d}: 成本 {J_history[-1]:8.2f}") return w, b, J_history # 返回最终参数和成本历史 ``` ### 6. 可视化辅助函数 `plot_regression_results` 画出两个图表: - 预测值 vs 真实值的散点图(越靠近 $y=x$ 直线说明预测越准) - 每个特征与目标值的关系,以及拟合曲线与真实曲线的对比 ``` python def plot_regression_results(X, y, w, b, true_w, true_b): """ 绘制回归结果的可视化图表 Args: X: 特征矩阵 y: 目标值向量 w: 学习到的权重 b: 学习到的偏置 true_w: 真实权重 true_b: 真实偏置 """ # 1. 绘制预测值与真实值的对比 y_pred = np.dot(X, w) + b plt.figure(figsize=(15, 5)) plt.subplot(1, 2, 1) plt.scatter(y, y_pred, alpha=0.6) plt.plot([y.min(), y.max()], [y.min(), y.max()], 'r--') # 理想的预测线(y=x) plt.xlabel('真实值') plt.ylabel('预测值') plt.title('预测值 vs 真实值') plt.grid(True, alpha=0.3) # 2. 为每个特征绘制其与目标值的关系及拟合曲线 n_features = X.shape[1] fig, axes = plt.subplots(1, n_features, figsize=(5 * n_features, 4)) if n_features == 1: axes = [axes] # 处理单特征的情况 # 计算每个特征的平均值,用于固定其他特征 X_mean = np.mean(X, axis=0) for i, ax in enumerate(axes): # 提取当前特征 x_feature = X[:, i] # 创建特征范围,用于绘制平滑曲线 x_range = np.linspace(x_feature.min(), x_feature.max(), 100) # 固定其他特征为平均值,只变化当前特征 X_fixed = np.tile(X_mean, (100, 1)) X_fixed[:, i] = x_range # 计算预测值(学习到的模型) y_pred_range = np.dot(X_fixed, w) + b # 计算真实值(生成数据的模型) y_true_range = np.dot(X_fixed, true_w) + true_b # 绘制散点图和拟合曲线 ax.scatter(x_feature, y, alpha=0.5, label='样本数据') ax.plot(x_range, y_pred_range, 'r-', linewidth=2, label='拟合曲线') ax.plot(x_range, y_true_range, 'g--', linewidth=2, label='真实曲线') ax.set_xlabel(f'特征 {i+1}') ax.set_ylabel('目标值') ax.set_title(f'特征 {i+1} 与目标值的关系') ax.legend() ax.grid(True, alpha=0.3) plt.tight_layout() plt.show() ``` ### 7. 主程序:执行多维度线性回归 主程序按流程运行:生成数据 → 初始化参数 → 计算初始成本与梯度 → 设置超参数 → 运行梯度下降 → 打印结果 → 绘制成本曲线和拟合效果。运行后对比学习到的参数与真实参数,检验算法是否正确。 ``` python # 主程序:执行多维度线性回归 if __name__ == "__main__": # 1. 生成样本数据 # 生成100个样本,每个样本有3个特征 X_train, y_train, true_w, true_b = generate_sample_data(m=100, n=3) print(f"生成的数据 - 真实权重: {true_w}, 真实偏置: {true_b:.2f}") # 2. 初始化模型参数 initial_w = np.zeros_like(true_w) # 初始权重设为0 initial_b = 0.0 # 初始偏置设为0 # 3. 计算初始成本和梯度(测试用) initial_cost = compute_cost(X_train, y_train, initial_w, initial_b) print(f"\n初始成本: {initial_cost:.2f}") tmp_dj_db, tmp_dj_dw = compute_gradient(X_train, y_train, initial_w, initial_b) print(f"初始参数下的dj_db: {tmp_dj_db:.4f}") print(f"初始参数下的dj_dw: {tmp_dj_dw}") # 4. 设置梯度下降的超参数 iterations = 1000 # 迭代次数 alpha = 5.0e-3 # 学习率(根据实际数据调整) # 5. 运行梯度下降算法 print("\n开始梯度下降...") w_final, b_final, J_hist = gradient_descent( X_train, y_train, initial_w, initial_b, compute_cost, compute_gradient, alpha, iterations ) # 6. 输出训练结果 print(f"\n梯度下降找到的参数 - b: {b_final:0.2f}, w: {w_final}") print(f"真实参数 - b: {true_b:0.2f}, w: {true_w}") # 7. 打印部分预测结果与真实值的对比 print("\n部分预测结果与真实值对比:") m, _ = X_train.shape # 打印前5个样本的预测结果 for i in range(min(5, m)): prediction = np.dot(X_train[i], w_final) + b_final print(f"预测值: {prediction:0.2f}, 真实值: {y_train[i]:0.2f}") # 8. 绘制成本随迭代次数变化的曲线 fig, (ax1, ax2) = plt.subplots(1, 2, constrained_layout=True, figsize=(12, 4)) # 绘制完整的成本曲线 ax1.plot(J_hist) ax1.set_title("成本 vs 迭代次数") ax1.set_ylabel('成本') ax1.set_xlabel('迭代次数') # 绘制后期的成本曲线(从第100次迭代开始),更清晰地看到收敛情况 if len(J_hist) > 100: ax2.plot(100 + np.arange(len(J_hist[100:])), J_hist[100:]) ax2.set_title("成本 vs 迭代次数(后期)") ax2.set_ylabel('成本') ax2.set_xlabel('迭代次数') else: ax2.plot(J_hist) ax2.set_title("成本 vs 迭代次数") ax2.set_ylabel('成本') ax2.set_xlabel('迭代次数') plt.show() # 9. 绘制最终的回归函数曲线和拟合效果 plot_regression_results(X_train, y_train, w_final, b_final, true_w, true_b) ``` ## 三、使用 scikit-learn 实现多项式线性回归 ### 1. 导入库与基础设置 scikit-learn 提供了开箱即用的 `LinearRegression`,同时引入多项式特征、数据划分与评估工具。 ``` python # 用sklearn实现多项式线性回归 import numpy as np import matplotlib.pyplot as plt from sklearn.linear_model import LinearRegression from sklearn.preprocessing import PolynomialFeatures from sklearn.model_selection import train_test_split from sklearn.metrics import mean_squared_error # 设置打印精度 np.set_printoptions(precision=2) # 定义颜色 dlblue = '#0096ff'; dlorange = '#FF9300'; dldarkred='#C00000'; dlmagenta='#FF40FF'; dlpurple='#7030A0'; ``` ### 2. 生成示例数据 生成 $y = 2 + x + 0.5x^2 + \text{噪声}$ 的数据。目标值与 $x$ 是**二次**关系,但线性模型通过多项式特征依然能拟合。 ``` python # 1. 生成示例数据 np.random.seed(42) X = np.linspace(-3, 3, 100).reshape(-1, 1) # 特征 y = 2 + X + 0.5 * X**2 + np.random.normal(0, 0.5, size=X.shape) # 二次函数关系 + 噪声 y = y.ravel() # 将y转换为一维数组,避免后续问题 ``` ### 3. 划分训练集与测试集 用 `train_test_split` 把数据分成训练集(80%)和测试集(20%)。训练集用来学参数,测试集用来检验泛化能力。 ``` python # 2. 划分训练集和测试集,测试集占20% X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) ``` ### 4. 创建多项式特征 原始特征只有 $x$,通过 `PolynomialFeatures` 扩展为 $[x,\ x^2]$,这样线性模型就能拟合二次曲线: $$\hat{y} = w_1 x + w_2 x^2 + b$$ ``` python # 3. 创建多项式特征 (这里选择2次多项式) degree = 2 # 多项式阶数 poly = PolynomialFeatures(degree=degree, include_bias=False) X_train_poly = poly.fit_transform(X_train) # 转换训练集特征 X_test_poly = poly.transform(X_test) # 转换测试集特征 print(f"原始特征形状: {X_train.shape}") print(f"多项式特征形状: {X_train_poly.shape}") print("前5个样本的多项式特征:") print(X_train_poly[:5]) ``` ### 5. 训练模型 `LinearRegression().fit(...)` 内部用最小二乘法一步求出最优参数,无需手动迭代。 ``` python # 4. 训练多项式线性回归模型 model = LinearRegression() model.fit(X_train_poly, y_train) #自动计算出最优的模型参数 ``` ### 6. 查看模型参数 打印学习到的偏置 $b$ 和权重 $w$,与真实生成关系 $y = 2 + x + 0.5x^2$ 对照:$b \approx 2,\ w_1 \approx 1,\ w_2 \approx 0.5$。 ``` python # 5. 获取模型参数,确保是标量 b = model.intercept_ # 偏置 w = model.coef_ # 系数 # 如果b是数组,取第一个元素 if isinstance(b, np.ndarray): b = b[0] print(f"\n模型参数:") print(f"偏置 b = {b:.2f}") print(f"权重 w = {w}") ``` ### 7. 模型预测与评估 在训练集和测试集上分别预测,并用 MSE 评估误差。测试集 MSE 反映模型的**泛化能力**。 ``` python # 6. 模型预测与评估 y_pred_train = model.predict(X_train_poly) y_pred_test = model.predict(X_test_poly) train_mse = mean_squared_error(y_train, y_pred_train) test_mse = mean_squared_error(y_test, y_pred_test) print(f"\n训练集MSE: {train_mse:.4f}") print(f"测试集MSE: {test_mse:.4f}") ``` ### 8. 可视化结果 绘制原始数据、训练集预测和二次多项式拟合曲线,直观看到拟合效果。 ``` python # 7. 可视化结果 plt.figure(figsize=(10, 6)) plt.scatter(X, y, color=dlblue, alpha=0.5, label='原始数据') plt.scatter(X_train, y_pred_train, color=dlorange, label='训练集预测') plt.plot(X, model.predict(poly.transform(X)), color=dldarkred, linewidth=2, label=f'{degree}次多项式拟合') plt.xlabel('X') plt.ylabel('y') plt.title('多项式线性回归示例') plt.legend() plt.grid(True) plt.show() ```