深入浅出:多项式回归的全解析
在机器学习与数据科学领域,多项式回归是一种强大的建模工具,用于处理输入变量和输出变量之间的非线性关系。它通过引入高次项特征来拟合复杂的数据模式,成为解决许多非线性问题的重要选择。本文将从多项式回归的基本原理出发,结合理论和实践案例,全面介绍其应用场景、实现方式及优势与挑战。
1. 多项式回归简介
1.1 什么是多项式回归?
多项式回归是线性回归的扩展形式,通过将输入变量的高次幂作为特征,构造出非线性模型。其目标是通过调整模型参数来最小化预测值与实际值之间的误差,从而实现对非线性关系的拟合。
多项式回归的通用形式为:
y = β 0 + β 1 x + β 2 x 2 + ⋯ + β d x d + ϵ y = \beta_0 + \beta_1x + \beta_2x^2 + \cdots + \beta_dx^d + \epsilon y=β0+β1x+β2x2+⋯+βdxd+ϵ
其中:
- ( y ):目标变量(响应变量);
- ( x ):输入特征;
- ( d ):多项式的阶数;
- ( \beta_0, \beta_1, \cdots, \beta_d ):回归系数;
- ( \epsilon ):随机误差。
1.2 应用场景
多项式回归广泛应用于非线性关系建模场景,典型应用包括:
- 经济预测:预测商品价格与市场供需的非线性关系。
- 工程建模:拟合机械设备的性能曲线。
- 医学研究:分析药物剂量和疗效之间的关系。
- 自然现象模拟:研究温度变化与海洋潮汐的关联。
1.3 线性回归与多项式回归的区别
维度 | 线性回归 | 多项式回归 |
---|---|---|
模型形式 | 输出是输入特征的线性组合 | 输出是输入特征的多项式组合 |
适用场景 | 输入和输出变量呈线性关系 | 输入和输出变量呈非线性关系 |
模型复杂度 | 参数少,模型简单 | 参数多,模型复杂 |
风险 | 容易欠拟合 | 容易过拟合 |
2. 多项式回归模型的构建
2.1 多项式回归的基本步骤
-
数据准备:
收集并清洗数据,确保其适合进行建模。 -
特征转换:
将原始输入特征提升为高次幂,生成多项式特征。例如,对于输入变量 ( x ),构造特征 ( [1, x, x^2, x^3, \dots, x^d] )。 -
模型训练:
利用最小二乘法拟合模型参数,最小化实际值和预测值之间的误差。 -
模型评估:
通过均方误差(MSE)、决定系数(( R^2 ))等指标评估模型性能。
2.2 特征转换与矩阵表示
多项式回归通过扩展特征空间将非线性问题转化为线性问题。例如,对于输入特征 ( x = [x_1, x_2, \dots, x_n] ):
- 原始特征矩阵:( X = [x_1, x_2, \dots, x_n] )。
- 转换后的特征矩阵:( X_{poly} = [1, x, x^2, \dots, x^d] )。
矩阵形式如下:
X
p
o
l
y
=
[
1
x
1
x
1
2
⋯
x
1
d
1
x
2
x
2
2
⋯
x
2
d
⋮
⋮
⋮
⋱
⋮
1
x
n
x
n
2
⋯
x
n
d
]
X_{poly} = \begin{bmatrix} 1 & x_1 & x_1^2 & \cdots & x_1^d \\ 1 & x_2 & x_2^2 & \cdots & x_2^d \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ 1 & x_n & x_n^2 & \cdots & x_n^d \end{bmatrix}
Xpoly=
11⋮1x1x2⋮xnx12x22⋮xn2⋯⋯⋱⋯x1dx2d⋮xnd
2.3 多项式阶数的选择
模型阶数 ( d ) 是多项式回归中的重要超参数:
- 低阶多项式:模型简单,可能欠拟合。
- 高阶多项式:模型复杂,可能过拟合。
通过交叉验证选择最优阶数是常见的策略。
3. 多项式回归的优缺点
3.1 优势
-
适应非线性数据:
能够捕捉输入变量和输出变量之间的复杂关系。 -
灵活性高:
随着阶数增加,可以拟合更加复杂的模式。 -
易于实现:
在现有的线性回归框架中稍作改动即可实现。
3.2 劣势
-
易过拟合:
高阶多项式可能导致模型对噪声过于敏感。 -
计算复杂度高:
阶数过高会显著增加模型训练时间。 -
可解释性差:
随着阶数增加,模型的可解释性显著降低。
4. 实例解析
4.1 数据拟合实例
假设我们希望预测房屋面积与价格的关系,数据如下:
面积 (m²) | 价格 (万元) |
---|---|
50 | 80 |
60 | 95 |
70 | 110 |
80 | 125 |
90 | 140 |
代码实现
import numpy as np
import matplotlib.pyplot as plt
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
# 数据准备
X = np.array([50, 60, 70, 80, 90]).reshape(-1, 1)
y = np.array([80, 95, 110, 125, 140])
# 特征转换
poly = PolynomialFeatures(degree=2)
X_poly = poly.fit_transform(X)
# 模型训练
model = LinearRegression()
model.fit(X_poly, y)
# 预测
X_test = np.linspace(40, 100, 100).reshape(-1, 1)
y_pred = model.predict(poly.transform(X_test))
# 可视化
plt.scatter(X, y, color='blue', label='Original Data')
plt.plot(X_test, y_pred, color='red', label='Polynomial Fit')
plt.xlabel('Area (m²)')
plt.ylabel('Price (万元)')
plt.legend()
plt.title('Polynomial Regression Fit')
plt.show()
4.2 模型评估与优化
模型评估
常用的评估指标包括:
-
均方误差(MSE):
M S E = 1 n ∑ i = 1 n ( y i − y ^ i ) 2 MSE = \frac{1}{n}\sum_{i=1}^n (y_i - \hat{y}_i)^2 MSE=n1i=1∑n(yi−y^i)2 -
决定系数(( R^2 )):
R 2 = 1 − ∑ i = 1 n ( y i − y ^ i ) 2 ∑ i = 1 n ( y i − y ˉ ) 2 R^2 = 1 - \frac{\sum_{i=1}^n (y_i - \hat{y}_i)^2}{\sum_{i=1}^n (y_i - \bar{y})^2} R2=1−∑i=1n(yi−yˉ)2∑i=1n(yi−y^i)2
正则化的引入
为了防止过拟合,可以使用正则化方法(如 L2 正则化)来约束模型复杂度。
5.生成带有随机噪声的二次曲线数据集
我们将假设数据符合一个特定的二次函数模型,并加入一些随机噪声来模拟现实世界中的数据波动。从而论证在多项式回归中的过拟合、欠拟合、正好情况!
假设我们的模型是:
y = 2 x 2 + 3 x + 4 + ϵ y=2x^2+3x+4+\epsilon y=2x2+3x+4+ϵ
其中 ϵ \epsilon ϵ 是一个随机噪声项,可以看作是正态分布的随机变量,具有零均值和一定的标准差。
数据生成
- X X X 是从-10到10均匀分布的100个点。
- n o i s e noise noise 是均值为0,标准差为10的正态分布随机噪声。
- Y Y Y 是基于二次函数 y = 2 x 2 + 3 x + 4 y=2x^2+3x+4 y=2x2+3x+4 计算的结果加上噪声。
生成数据集的Python代码
import numpy as np
import matplotlib.pyplot as plt
# 设置随机种子以确保结果可复现
np.random.seed(0)
# 定义样本数量
n_samples = 100
# 生成x值,范围从-10到10
X = np.linspace(-10, 10, n_samples)
# 添加随机噪声,这里我们设定噪声的标准差为10
noise = np.random.normal(loc=0.0, scale=10, size=n_samples)
# 根据二次函数模型生成y值
Y = 2 * X**2 + 3 * X + 4 + noise
# 将数据存储在列表中
data = list(zip(X, Y))
# 输出前几个数据点
print("前几个数据点:")
for i in range(5):
print(f"x: {X[i]}, y: {Y[i]}")
# 绘制数据点
plt.figure(figsize=(10, 6))
plt.scatter(X, Y, label='Data Points', color='blue')
plt.title('Generated Quadratic Data with Noise')
plt.xlabel('X values')
plt.ylabel('Y values')
plt.legend()
plt.grid(True)
plt.show()
# 打印整个数据列表
print("\n完整数据列表:")
print(data)
这段代码首先定义了一个包含100个均匀分布在-10到10之间的 x x x值数组。然后,我们生成了一个正态分布的噪声数组,并将其添加到了按照给定二次函数计算得到的 y y y值上,从而得到带有噪声的 y y y值。
结果
图一:产生的数据
根据图一我们可以看到,我们根据二次函数随机产生了十组数据,同时加入噪声,使得数据分布更真实,但是整体趋势还是符合二次函数图像。接下来我们定义模型进行训练,对这组数据进行拟合。
多项式回归——欠拟合
定义
欠拟合是指模型在训练数据上的表现较差,即模型未能很好地捕捉数据中的基本模式。这种情况通常发生在模型过于简单或参数设置不当的情况下。欠拟合会导致模型在训练数据上的误差较高,从而在新的未知数据上表现不佳。解决欠拟合的方法包括增加模型复杂度、引入更多特征或调整模型参数。
数学原理
线性回归模型试图找到一条直线来拟合数据点。这条直线的方程可以表示为:
y = a x + b y=ax+b y=ax+b
其中:
- y y y 是预测值
- x x x 是输入特征
- a a a 是斜率
- b b b 是 y y y轴截距。
我们的目标是找到最优的 a a a 和 b b b 值,使得预测值与实际值之间的误差最小。
损失函数
我们使用均方误差(Mean Squared Error, MSE)作为损失函数:
M S E = 1 n ∑ i = 1 n ( y i − y ^ i ) 2 MSE=\frac{1}{n} \sum_{i=1}^n (y_i-\hat{y}_i)^2 MSE=n1i=1∑n(yi−y^i)2
其中:
- n n n 是数据点的数量
- y i y_i yi 是实际值
- y ^ i \hat{y}_i y^i 是预测值
梯度下降
为了最小化损失函数,我们使用梯度下降算法。梯度下降的更新规则如下:
a ← a − α ∂ M S E ∂ a a \leftarrow a-\alpha \frac{\partial MSE}{\partial a} a←a−α∂a∂MSE
b ← b − α ∂ M S E ∂ b b \leftarrow b-\alpha \frac{\partial MSE}{\partial b} b←b−α∂b∂MSE
其中:
- α \alpha α 是学习率
- ∂ M S E ∂ a \frac{\partial MSE}{\partial a} ∂a∂MSE 和 ∂ M S E ∂ b \frac{\partial MSE}{\partial b} ∂b∂MSE 分别是损失函数对 a a a 和 b b b 的偏导数
实现思路
- 数据准备:
- 将给定的数据点分为训练集和测试集
- 训练集用于模型训练,测试集用于评估模型性能
- 模型定义:
- 创建一个
LinearModel
类,包含参数 a a a 和 b b b - 实现
predict
方法用于预测 - 实现
loss
方法计算均方误差 - 实现
gradient_descent
方法更新模型参数
- 创建一个
- 模型训练:
- 使用梯度下降算法迭代更新模型参数
- 记录每次迭代的训练损失和测试损失
- 可视化:
- 绘制训练损失和测试损失随迭代次数的变化曲线
- 绘制数据点和模型预测线
- 分析:
- 观察损失曲线,评估模型是否收敛
- 比较训练损失和测试损失,判断是否存在过拟合或欠拟合
- 观察预测线是否合理拟合数据点
这个实现允许我们直观地观察线性回归模型的训练过程和最终效果,有助于理解模型的性能和潜在的问题。
代码实现
import numpy as np
import matplotlib.pyplot as plt
data_list = [(-10.0, 98.64052345967664), (-7.777777777777778, 49.939843688610495),
(-5.555555555555555, 30.540466260810476), (-3.333333333333333, 27.853376436459023),
(-1.1111111111111107, 18.68792558051202), (1.1111111111111107, -5.315988675307321),
(3.333333333333334, 28.278661953033676), (5.555555555555557, 41.46173655899835),
(7.777777777777779, 76.01719419811381), (10.0, 125.10598501938372)]
class LinearModel:
def __init__(self):
self.a = 0.0
self.b = 0.0
self.learning_rate = 0.001
def predict(self, x):
"""给定输入x,返回模型的预测值"""
return self.a * x + self.b
def loss(self, y_true, y_pred):
"""计算预测值与真实值之间的均方误差"""
return np.mean((y_true - y_pred) ** 2)
def gradient_descent(self, x, y_true, y_pred):
"""使用梯度下降法更新模型参数"""
da = -2 * np.mean((y_true - y_pred) * x)
db = -2 * np.mean(y_true - y_pred)
self.a -= self.learning_rate * da
self.b -= self.learning_rate * db
def process_data(data_list):
"""从数据列表中提取x和y值"""
X = [point[0] for point in data_list]
Y = [point[1] for point in data_list]
return np.array(X), np.array(Y)
def train_model(model, X_train, Y_train, X_test, Y_test, epochs):
"""训练模型的函数"""
train_loss_history = []
test_loss_history = []
for epoch in range(epochs):
y_pred_train = model.predict(X_train)
current_train_loss = model.loss(Y_train, y_pred_train)
model.gradient_descent(X_train, Y_train, y_pred_train)
y_pred_test = model.predict(X_test)
current_test_loss = model.loss(Y_test, y_pred_test)
train_loss_history.append(current_train_loss)
test_loss_history.append(current_test_loss)
if epoch % 100 == 0:
print(f"Epoch {epoch}, Train Loss: {current_train_loss}, Test Loss: {current_test_loss}")
return train_loss_history, test_loss_history
def plot_loss(train_loss_history, test_loss_history):
"""绘制训练集和测试集的损失曲线"""
plt.figure(figsize=(10, 5))
plt.plot(train_loss_history, label='Train Loss')
plt.plot(test_loss_history, label='Test Loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.title('Training and Test Loss Over Time')
plt.legend()
plt.show()
def plot_data_and_prediction(X_train, Y_train, X_test, Y_test, model):
"""绘制数据点和模型预测线"""
plt.figure(figsize=(10, 5))
plt.scatter(X_train, Y_train, color='blue', label='Train Data')
plt.scatter(X_test, Y_test, color='green', label='Test Data')
X_all = np.concatenate((X_train, X_test))
X_plot = np.linspace(X_all.min(), X_all.max(), 100)
plt.plot(X_plot, model.predict(X_plot), color='red', label='Prediction Line')
plt.xlabel('X')
plt.ylabel('Y')
plt.title('Data and Model Prediction')
plt.legend()
plt.show()
def process_and_train(data_list, epochs=1000):
"""处理数据并训练模型"""
X, Y = process_data(data_list)
# 分割训练集和测试集
split = int(0.8 * len(X))
X_train, X_test = X[:split], X[split:]
Y_train, Y_test = Y[:split], Y[split:]
model = LinearModel()
train_loss_history, test_loss_history = train_model(model, X_train, Y_train, X_test, Y_test, epochs)
# 绘制损失曲线
plot_loss(train_loss_history, test_loss_history)
# 绘制数据点和模型预测线
plot_data_and_prediction(X_train, Y_train, X_test, Y_test, model)
return model, train_loss_history, test_loss_history
# 训练模型
model, train_loss_history, test_loss_history = process_and_train(data_list)
结果
图二:欠拟合损失函数图
图三:欠拟合模型曲线及数据图
根据图二的图像可以看出,随着训练轮数的增加,模型在训练集和测试集上的损失值一直下降。根据图三结果可以看出,使用一次多项式回归去训练,并不能够完美拟合我们生成的数据,即发生了欠拟合现象。
多项式回归——正好
定义
刚好拟合是指模型既不过于简单也不过于复杂,能够在训练数据上表现良好,并且在新的未知数据上也有较好的泛化能力。这种状态表明模型既没有欠拟合也没有过拟合,能够较好地捕捉数据的基本模式。实现刚好拟合通常需要仔细选择模型复杂度、调整超参数并使用合适的正则化技术。
数学原理
二次多项式回归模型试图用一条抛物线来拟合数据点。这条抛物线的方程可以表示为:
y = a x 2 + b x + c y=ax^2+bx+c y=ax2+bx+c
其中:
- y y y 是预测值
- x x x 是输入特征
- a , b , c a, b, c a,b,c 是模型参数
我们的目标是找到最优的 a , b , c a, b, c a,b,c 值,使得预测值与实际值之间的误差最小。
损失函数
我们使用均方误差(Mean Squared Error, MSE)作为损失函数:
M S E = 1 n ∑ i = 1 n ( y i − y ^ i ) 2 MSE=\frac{1}{n} \sum_{i=1}^n (y_i-\hat{y}_i)^2 MSE=n1i=1∑n(yi−y^i)2
梯度下降
为了最小化损失函数,我们使用梯度下降算法。梯度下降的更新规则如下:
a ← a − α ∂ M S E ∂ a a \leftarrow a-\alpha \frac{\partial MSE}{\partial a} a←a−α∂a∂MSE
b ← b − α ∂ M S E ∂ b b \leftarrow b-\alpha \frac{\partial MSE}{\partial b} b←b−α∂b∂MSE
c ← c − α ∂ M S E ∂ c c \leftarrow c-\alpha \frac{\partial MSE}{\partial c} c←c−α∂c∂MSE
实现思路
- 数据准备:
- 对输入数据进行最小-最大归一化处理
- 将归一化后的数据分为训练集和测试集
- 模型定义:
- 创建一个
QuadraticModel
类,包含参数 a , b , c a, b, c a,b,c - 实现
predict
方法用于预测 - 实现
loss
方法计算均方误差 - 实现
gradient_descent
方法更新模型参数
- 创建一个
- 模型训练:
- 使用梯度下降算法迭代更新模型参数
- 记录每次迭代的训练损失和测试损失
- 可视化:
- 绘制训练损失和测试损失随迭代次数的变化曲线
- 绘制原始数据点和模型预测曲线
- 分析:
- 观察损失曲线,评估模型是否收敛
- 比较训练损失和测试损失,判断是否存在过拟合或欠拟合
- 观察预测曲线是否合理拟合数据点
代码实现
import numpy as np
import matplotlib.pyplot as plt
data_list = [(-10.0, 98.64052345967664), (-7.777777777777778, 49.939843688610495),
(-5.555555555555555, 30.540466260810476), (-3.333333333333333, 27.853376436459023),
(-1.1111111111111107, 18.68792558051202), (1.1111111111111107, -5.315988675307321),
(3.333333333333334, 28.278661953033676), (5.555555555555557, 41.46173655899835),
(7.777777777777779, 76.01719419811381), (10.0, 125.10598501938372)]
class QuadraticModel:
def __init__(self):
self.a = 0.0
self.b = 0.0
self.c = 0.0
self.learning_rate = 0.5
def predict(self, x):
"""给定输入x,返回模型的预测值"""
return self.a * x ** 2 + self.b * x + self.c
def loss(self, y_true, y_pred):
"""计算预测值与真实值之间的均方误差"""
return np.mean((y_true - y_pred) ** 2)
def gradient_descent(self, x, y_true, y_pred):
"""使用梯度下降法更新模型参数"""
n = len(x)
da = -2 * np.sum(x ** 2 * (y_true - y_pred)) / n
db = -2 * np.sum(x * (y_true - y_pred)) / n
dc = -2 * np.sum(y_true - y_pred) / n
self.a -= self.learning_rate * da
self.b -= self.learning_rate * db
self.c -= self.learning_rate * dc
def normalize_data(data):
"""对数据进行最小-最大归一化"""
X, Y = zip(*data)
X = np.array(X)
Y = np.array(Y)
X_min, X_max = X.min(), X.max()
Y_min, Y_max = Y.min(), Y.max()
X_normalized = (X - X_min) / (X_max - X_min)
Y_normalized = (Y - Y_min) / (Y_max - Y_min)
return X_normalized, Y_normalized, (X_min, X_max), (Y_min, Y_max)
def denormalize_data(normalized_value, min_max):
"""将归一化的数据还原回原始范围"""
min_val, max_val = min_max
return normalized_value * (max_val - min_val) + min_val
def train_model(model, X_train, Y_train, X_test, Y_test, epochs):
"""训练模型的函数"""
train_loss_history = []
test_loss_history = []
for epoch in range(epochs):
y_pred_train = model.predict(X_train)
current_train_loss = model.loss(Y_train, y_pred_train)
model.gradient_descent(X_train, Y_train, y_pred_train)
y_pred_test = model.predict(X_test)
current_test_loss = model.loss(Y_test, y_pred_test)
train_loss_history.append(current_train_loss)
test_loss_history.append(current_test_loss)
if epoch % 100 == 0:
print(f"Epoch {epoch}, Train Loss: {current_train_loss}, Test Loss: {current_test_loss}")
return train_loss_history, test_loss_history
def plot_loss(train_loss_history, test_loss_history):
"""绘制训练集和测试集的损失曲线"""
plt.figure(figsize=(10, 5))
plt.plot(train_loss_history, label='Train Loss')
plt.plot(test_loss_history, label='Test Loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.title('Training and Test Loss Over Time')
plt.legend()
plt.show()
def plot_data_and_prediction(X_train, Y_train, X_test, Y_test, model, X_original_range, Y_original_range):
"""绘制数据点和模型预测曲线"""
plt.figure(figsize=(10, 5))
# 绘制训练数据点
X_train_original = denormalize_data(X_train, X_original_range)
Y_train_original = denormalize_data(Y_train, Y_original_range)
plt.scatter(X_train_original, Y_train_original, color='blue', label='Train Data')
# 绘制预测曲线
x_plot = np.linspace(X_original_range[0], X_original_range[1], 1000)
x_plot_normalized = (x_plot - X_original_range[0]) / (X_original_range[1] - X_original_range[0])
y_plot_normalized = model.predict(x_plot_normalized)
y_plot = denormalize_data(y_plot_normalized, Y_original_range)
plt.plot(x_plot, y_plot, color='red', label='Prediction Curve')
plt.xlabel('X')
plt.ylabel('Y')
plt.title('Data and Model Prediction')
plt.legend()
plt.show()
def process_and_train(data_list, epochs=1000):
"""处理数据并训练模型"""
X, Y, X_original_range, Y_original_range = normalize_data(data_list)
# 分割训练集和测试集
split = int(0.8 * len(X))
X_train, X_test = X[:split], X[split:]
Y_train, Y_test = Y[:split], Y[split:]
model = QuadraticModel()
train_loss_history, test_loss_history = train_model(model, X_train, Y_train, X_test, Y_test, epochs)
# 绘制损失曲线
plot_loss(train_loss_history, test_loss_history)
# 绘制数据点和模型预测曲线
plot_data_and_prediction(X_train, Y_train, X_test, Y_test, model, X_original_range, Y_original_range)
return model, train_loss_history, test_loss_history
# 训练模型
model, train_loss_history, test_loss_history = process_and_train(data_list)
这个实现允许我们直观地观察二次多项式回归模型的训练过程和最终效果,有助于理解模型的性能和潜在的问题。通过运行这段代码,我们可以得到损失曲线和数据拟合图,从而分析模型的表现。与线性回归相比,二次多项式回归模型能够捕捉数据中的非线性关系,可能会提供更好的拟合效果。
结果
图四:训练正好损失函数图
图五:正好模型曲线及数据图
根据图四、图五的结果图,我们可以清晰的看到,二次项回归模型的训练结果可以很好拟合数据集,曲线可以穿过大部分的数据点,这也符合我们的预期。
多项式回归——过拟合
定义
过拟合是指模型在训练数据上表现非常好,但在新的未知数据上表现较差。这是因为模型过度学习了训练数据中的细节和噪声,从而失去了泛化能力。过拟合通常发生在模型过于复杂或训练数据量不足的情况下。解决过拟合的方法包括简化模型、增加正则化项、使用交叉验证或增加训练数据量。
数学原理
多项式回归模型试图用一个高阶多项式来拟合数据点。这个多项式的一般形式为:
y = a n x n + a n − 1 x n − 1 + ⋯ + a 1 x + a 0 y=a_n x^n+a_{n-1} x^{n-1}+\cdots+a_1 x+a_0 y=anxn+an−1xn−1+⋯+a1x+a0
其中:
- y y y 是预测值
- x x x 是输入特征
- a n , a n − 1 , ⋯ , a 1 , a 0 a_n,a_{n-1},\cdots,a_1,a_0 an,an−1,⋯,a1,a0 是模型参数
- n n n 是多项式的阶数
在这个实现中,我们使用了3阶多项式 ( n = 3 n=3 n=3):
y = a x 3 + b x 2 + c x + d y=ax^3+bx^2+cx+d y=ax3+bx2+cx+d
损失函数
我们使用均方误差(Mean Squared Error, MSE)作为损失函数:
M S E = 1 m ∑ i = 1 m ( y i − y ^ i ) 2 MSE=\frac{1}{m} \sum_{i=1}^m (y_i-\hat{y}_i)^2 MSE=m1i=1∑m(yi−y^i)2
梯度下降
为了最小化损失函数,我们使用梯度下降算法。梯度下降的更新规则如下:
a j ← a j − α ∂ M S E ∂ a j a_j \leftarrow a_j-\alpha \frac{\partial MSE}{\partial a_j} aj←aj−α∂aj∂MSE
其中:
- α \alpha α 是学习率
- ∂ M S E ∂ a j \frac{\partial MSE}{\partial a_j} ∂aj∂MSE 是损失函数对参数 a j a_j aj 的偏导数
实现思路
- 数据准备:
- 对输入数据进行最小-最大归一化处理
- 将归一化后的数据分为训练集和测试集
- 模型定义:
- 创建一个
PolynomialModel
类,包含参数 a , b , c , d a, b, c, d a,b,c,d - 实现
predict
方法用于预测 - 实现
loss
方法计算均方误差 - 实现
gradient_descent
方法更新模型参数
- 创建一个
- 模型训练:
- 使用梯度下降算法迭代更新模型参数
- 记录每次迭代的训练损失和测试损失
- 可视化:
- 绘制训练损失和测试损失随迭代次数的变化曲线
- 绘制原始数据点和模型预测曲线
- 分析:
- 观察损失曲线,评估模型是否收敛
- 比较训练损失和测试损失,判断是否存在过拟合或欠拟合
- 观察预测曲线是否合理拟合数据点
代码实现
import numpy as np
import matplotlib.pyplot as plt
data_list = [(-10.0, 98.64052345967664), (-7.777777777777778, 49.939843688610495),
(-5.555555555555555, 30.540466260810476), (-3.333333333333333, 27.853376436459023),
(-1.1111111111111107, 18.68792558051202), (1.1111111111111107, -5.315988675307321),
(3.333333333333334, 28.278661953033676), (5.555555555555557, 41.46173655899835),
(7.777777777777779, 76.01719419811381), (10.0, 125.10598501938372)]
class PolynomialModel:
def __init__(self):
self.a = 0.0
self.b = 0.0
self.c = 0.0
self.d = 0.0
self.learning_rate = 0.01
def predict(self, x):
"""给定输入x,返回模型的预测值"""
return self.a * x**3 + self.b * x**2 + self.c * x + self.d
def loss(self, y_true, y_pred):
"""计算预测值与真实值之间的均方误差"""
return np.mean((y_true - y_pred) ** 2)
def gradient_descent(self, x, y_true, y_pred):
"""使用梯度下降法更新模型参数"""
n = len(x)
da = -2 * np.sum(x**3 * (y_true - y_pred)) / n
db = -2 * np.sum(x**2 * (y_true - y_pred)) / n
dc = -2 * np.sum(x * (y_true - y_pred)) / n
dd = -2 * np.sum(y_true - y_pred) / n
self.a -= self.learning_rate * da
self.b -= self.learning_rate * db
self.c -= self.learning_rate * dc
self.d -= self.learning_rate * dd
def normalize_data(data):
"""对数据进行最小-最大归一化"""
X, Y = zip(*data)
X = np.array(X)
Y = np.array(Y)
X_min, X_max = X.min(), X.max()
Y_min, Y_max = Y.min(), Y.max()
X_normalized = (X - X_min) / (X_max - X_min)
Y_normalized = (Y - Y_min) / (Y_max - Y_min)
return X_normalized, Y_normalized, (X_min, X_max), (Y_min, Y_max)
def denormalize_data(normalized_value, min_max):
"""将归一化的数据还原回原始范围"""
min_val, max_val = min_max
return normalized_value * (max_val - min_val) + min_val
def train_model(model, X_train, Y_train, X_test, Y_test, epochs):
"""训练模型的函数"""
train_loss_history = []
test_loss_history = []
for epoch in range(epochs):
y_pred_train = model.predict(X_train)
current_train_loss = model.loss(Y_train, y_pred_train)
model.gradient_descent(X_train, Y_train, y_pred_train)
y_pred_test = model.predict(X_test)
current_test_loss = model.loss(Y_test, y_pred_test)
train_loss_history.append(current_train_loss)
test_loss_history.append(current_test_loss)
if epoch % 100 == 0:
print(f"Epoch {epoch}, Train Loss: {current_train_loss}, Test Loss: {current_test_loss}")
return train_loss_history, test_loss_history
def plot_loss(train_loss_history, test_loss_history):
"""绘制训练集和测试集的损失曲线"""
plt.figure(figsize=(10, 5))
plt.plot(train_loss_history, label='Train Loss')
plt.plot(test_loss_history, label='Test Loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.title('Training and Test Loss Over Time')
plt.legend()
plt.show()
def plot_data_and_prediction(X_train, Y_train, X_test, Y_test, model, X_original_range, Y_original_range):
"""绘制数据点和模型预测曲线"""
plt.figure(figsize=(10, 5))
# 绘制训练数据点
X_train_original = denormalize_data(X_train, X_original_range)
Y_train_original = denormalize_data(Y_train, Y_original_range)
plt.scatter(X_train_original, Y_train_original, color='blue', label='Train Data')
# 绘制测试数据点
X_test_original = denormalize_data(X_test, X_original_range)
Y_test_original = denormalize_data(Y_test, Y_original_range)
plt.scatter(X_test_original, Y_test_original, color='green', label='Test Data')
# 绘制预测曲线
x_plot = np.linspace(X_original_range[0], X_original_range[1], 1000)
x_plot_normalized = (x_plot - X_original_range[0]) / (X_original_range[1] - X_original_range[0])
y_plot_normalized = model.predict(x_plot_normalized)
y_plot = denormalize_data(y_plot_normalized, Y_original_range)
plt.plot(x_plot, y_plot, color='red', label='Prediction Curve')
plt.xlabel('X')
plt.ylabel('Y')
plt.title('Data and Model Prediction')
plt.legend()
plt.show()
def process_and_train(data_list, epochs=10000):
"""处理数据并训练模型"""
X, Y, X_original_range, Y_original_range = normalize_data(data_list)
# 分割训练集和测试集
split = int(0.8 * len(X))
X_train, X_test = X[:split], X[split:]
Y_train, Y_test = Y[:split], Y[split:]
model = PolynomialModel()
train_loss_history, test_loss_history = train_model(model, X_train, Y_train, X_test, Y_test, epochs)
# 绘制损失曲线
plot_loss(train_loss_history, test_loss_history)
# 绘制数据点和模型预测曲线
plot_data_and_prediction(X_train, Y_train, X_test, Y_test, model, X_original_range, Y_original_range)
return model, train_loss_history, test_loss_history
# 训练模型
model, train_loss_history, test_loss_history = process_and_train(data_list)
这个实现允许我们直观地观察3阶多项式回归模型的训练过程和最终效果,有助于理解模型的性能和潜在的问题。通过运行这段代码,我们可以得到损失曲线和数据拟合图,从而分析模型的表现。
与线性回归和二次多项式回归相比,3阶多项式回归模型能够捕捉数据中更复杂的非线性关系,可能会提供更好的拟合效果。然而,需要注意的是,高阶多项式也可能导致过拟合,特别是在数据量较少的情况下。因此,在实际应用中,需要仔细权衡模型的复杂度和泛化能力。
结果
图六:过拟合模型损失函数图
图七:过拟合模型曲线及数据图
根据图六的结果可以看出,模型在训练过程中,在测试集上的损失值先逐渐下降后逐渐上升,这是非常突出的过拟合的现象。接着我们看图七,训练好的曲线并不能够反应数据真正的分布特征,拟合结果差,这就是典型的过拟合现象。
总结与实践建议
-
多项式回归适用于非线性数据建模,通过适当选择阶数,可以在模型复杂度和泛化性能之间找到平衡。
-
正则化方法是防止过拟合的有效工具,在高维场景中尤为重要。
-
实践中的注意事项:
- 确保数据标准化,以减少特征尺度的影响。
- 使用交叉验证选择最优模型超参数。
- 在非线性建模时,结合多项式回归与其他算法(如决策树)可能获得更优的结果。
标签:loss,plt,多项式,模型,深入浅出,train,test,解析,history From: https://blog.csdn.net/m0_74882984/article/details/144175428