课程承诺:1 个核心概念(多特征线性回归)+1 个核心思想(过拟合与泛化)+1 段实战代码。学完你能解决真实世界的多变量预测问题,一眼识别并解决机器学习最常见的 "死穴"。
本节课目标:从单特征升级到多特征预测,彻底搞懂 "过拟合" 这个所有 AI 工程师每天都在战斗的问题,亲手用正则化把过拟合的模型拉回正轨。
🧩 先回答上一课的思考题
问题:如果损失函数不是一个完美的 "碗" 形,而是有多个山谷,梯度下降会发生什么?
答案 :它会掉进第一个遇到的局部山谷(局部最小值),而不一定能走到最深的那个全局山谷(全局最小值)。
不过不用太担心:
- 在简单的线性回归中,损失函数永远是完美的碗形,只有一个全局最小值
- 在复杂的深度学习中,科学家发现:绝大多数局部最小值的效果和全局最小值几乎一样好
🧠 第一个核心概念:多特征线性回归
前两课我们只用了一个特征(面积 / 学习时间)来预测结果,但真实世界的所有问题,都是多特征共同作用的结果。
比如房价,不仅和面积有关,还和:
- 房间数量
- 所在楼层
- 房龄
- 学区
- 地铁距离
- ... 等等几十上百个因素有关
多特征线性回归的公式
单特征:y = w*x + b多特征:y = w1*x1 + w2*x2 + w3*x3 + ... + wn*xn + b
x1, x2, ..., xn:不同的特征(面积、房间数、楼层...)w1, w2, ..., wn:每个特征对应的权重(表示这个特征对结果的影响大小)b:截距
权重的意义:
- 权重为正:这个特征越大,结果越大(比如面积越大,房价越高)
- 权重为负:这个特征越大,结果越小(比如房龄越大,房价越低)
- 权重绝对值越大:这个特征对结果的影响越大
代码实战:多特征房价预测
我们用 4 个特征预测房价:面积、卧室数、卫生间数、房龄
python
运行
python
import numpy as np
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
# 1. 准备多特征数据(真实数据的简化版)
# 特征:[面积(平米), 卧室数, 卫生间数, 房龄(年)]
X = np.array([
[50, 1, 1, 10], [60, 2, 1, 8], [70, 2, 1, 5], [80, 3, 2, 3], [90, 3, 2, 2],
[100, 3, 2, 1], [110, 4, 2, 15], [120, 4, 3, 10], [130, 4, 3, 5], [140, 5, 3, 2]
])
# 标签:房价(万元)
y = np.array([100, 125, 150, 180, 210, 240, 220, 270, 310, 350])
# 2. 划分训练集和测试集(极其重要!)
# 80%的数据用来训练,20%的数据用来测试模型的真实能力
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# 3. 创建并训练模型(和单特征代码完全一样!)
model = LinearRegression()
model.fit(X_train, y_train)
# 4. 查看每个特征的权重
print("各特征权重:")
print(f"面积:{model.coef_[0]:.2f} 万元/平米")
print(f"卧室数:{model.coef_[1]:.2f} 万元/个")
print(f"卫生间数:{model.coef_[2]:.2f} 万元/个")
print(f"房龄:{model.coef_[3]:.2f} 万元/年")
print(f"截距:{model.intercept_:.2f} 万元")
# 5. 评估模型
y_train_pred = model.predict(X_train)
y_test_pred = model.predict(X_test)
print(f"\n训练集误差(MSE):{mean_squared_error(y_train, y_train_pred):.2f}")
print(f"测试集误差(MSE):{mean_squared_error(y_test, y_test_pred):.2f}")
# 6. 预测一套新房子的价格
new_house = np.array([[105, 3, 2, 7]])
predicted_price = model.predict(new_house)
print(f"\n预测105平米、3卧2卫、7年房龄的房子价格:{predicted_price[0]:.2f}万元")
运行结果解读
你会看到类似这样的输出:
plaintext
python
各特征权重:
面积:2.15 万元/平米
卧室数:5.32 万元/个
卫生间数:12.78 万元/个
房龄:-3.21 万元/年
截距:-15.67 万元
训练集误差(MSE):12.34
测试集误差(MSE):18.56
- 卫生间数的权重最大,说明在这个数据集中,卫生间数量对房价的影响最大
- 房龄的权重为负,符合我们的常识:房子越老越便宜
- 测试集误差比训练集误差大一点,这是正常的
💡 第一个核心思想:过拟合与泛化能力
这是机器学习最重要、最核心的思想,没有之一。所有 AI 工程师 90% 的工作,都是在和这个问题战斗。
最通俗的类比:学生考试
我们训练模型的过程,就像学生备考:
- 训练集 = 课本上的例题
- 测试集 = 期末考试题(学生从来没见过的题)
- 模型训练 = 学生做例题学习
- 模型预测 = 学生做考试题
现在有三个学生:
-
学渣(欠拟合):上课不听,例题也不会做,考试当然考不好
- 表现:训练集误差大,测试集误差也大
- 原因:模型太简单,连数据的基本规律都没学到
-
学霸(拟合良好):理解了知识点,例题会做,考试也能考高分
- 表现:训练集误差小,测试集误差也小
- 这是我们想要的理想模型
-
书呆子(过拟合):死记硬背了所有例题,例题能考满分,但考试遇到新题就不会
- 表现:训练集误差极小,甚至为 0,但测试集误差极大
- 原因:模型太复杂,学到了很多数据中的随机噪声,而不是真正的规律
什么是泛化能力?
模型在从未见过的新数据 上的表现能力,就叫泛化能力。
- 我们训练模型的最终目标,不是让它在训练集上表现好,而是让它在新数据上表现好
- 过拟合的模型,泛化能力极差,完全没有实用价值
为什么会发生过拟合?
- 数据太少
- 特征太多
- 模型太复杂
💻 代码实战:亲眼看到过拟合
我们用一个简单的多项式回归例子,让你亲眼看到过拟合是怎么发生的。
完整代码
python
运行
python
import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import PolynomialFeatures
from sklearn.metrics import mean_squared_error
# 1. 生成带噪声的模拟数据
np.random.seed(42)
X = np.linspace(0, 10, 10).reshape(-1, 1)
y = 2*X + 3 + np.random.normal(0, 1, 10).reshape(-1, 1) # 真实规律是y=2x+3,加了一点噪声
# 2. 定义一个函数,用来训练不同次数的多项式模型
def train_polynomial_model(degree):
# 把原始特征转换成多项式特征
poly = PolynomialFeatures(degree=degree)
X_poly = poly.fit_transform(X)
# 训练模型
model = LinearRegression()
model.fit(X_poly, y)
# 预测
X_plot = np.linspace(0, 10, 100).reshape(-1, 1)
X_plot_poly = poly.transform(X_plot)
y_plot = model.predict(X_plot_poly)
# 计算误差
y_pred = model.predict(X_poly)
mse = mean_squared_error(y, y_pred)
return X_plot, y_plot, mse
# 3. 训练不同次数的模型,对比效果
degrees = [1, 3, 9]
plt.figure(figsize=(15, 5))
for i, degree in enumerate(degrees):
X_plot, y_plot, mse = train_polynomial_model(degree)
plt.subplot(1, 3, i+1)
plt.scatter(X, y, color='red', label='真实数据')
plt.plot(X_plot, y_plot, color='blue', label=f'拟合曲线(次数={degree})')
plt.title(f'多项式次数={degree},训练集误差={mse:.2f}')
plt.xlabel('X')
plt.ylabel('y')
plt.legend()
plt.ylim(0, 25)
plt.tight_layout()
plt.show()
运行结果分析
你会看到三张图:
- 次数 = 1(线性回归) :拟合曲线是一条直线,训练集误差≈0.8
- 这是欠拟合,因为真实数据有一点弯曲,直线无法捕捉
- 次数 = 3 :拟合曲线很平滑,很好地捕捉了数据的真实规律,训练集误差≈0.5
- 这是拟合良好,泛化能力最好
- 次数 = 9 :拟合曲线弯弯曲曲,完美地经过了每一个数据点,训练集误差≈0
- 这就是严重过拟合!它记住了所有的噪声点,但如果给它一个新的 x 值,预测结果会非常离谱
🛠️ 解决过拟合的神器:正则化
解决过拟合有很多方法,最常用、最有效的就是正则化。
正则化的核心思想
让模型变得更简单。
我们在损失函数中加入一个 "惩罚项",惩罚那些绝对值太大的参数。参数越小,模型越简单,越不容易过拟合。
最常用的正则化是L2 正则化 ,对应的模型叫岭回归(Ridge)。
代码实战:用岭回归解决过拟合
python
运行
python
from sklearn.linear_model import Ridge
# 1. 还是用刚才的9次多项式特征
poly = PolynomialFeatures(degree=9)
X_poly = poly.fit_transform(X)
# 2. 对比普通线性回归和岭回归
model_linear = LinearRegression()
model_linear.fit(X_poly, y)
# alpha是正则化强度,alpha越大,惩罚越重,模型越简单
model_ridge = Ridge(alpha=1.0)
model_ridge.fit(X_poly, y)
# 3. 预测并画图
X_plot = np.linspace(0, 10, 100).reshape(-1, 1)
X_plot_poly = poly.transform(X_plot)
y_linear = model_linear.predict(X_plot_poly)
y_ridge = model_ridge.predict(X_plot_poly)
plt.figure(figsize=(10, 6))
plt.scatter(X, y, color='red', label='真实数据')
plt.plot(X_plot, y_linear, color='blue', linestyle='--', label='普通线性回归(过拟合)')
plt.plot(X_plot, y_ridge, color='green', linewidth=2, label='岭回归(正则化后)')
plt.title('正则化解决过拟合')
plt.xlabel('X')
plt.ylabel('y')
plt.legend()
plt.ylim(0, 25)
plt.show()
# 4. 对比参数大小
print(f"普通线性回归参数最大值:{np.max(np.abs(model_linear.coef_)):.2f}")
print(f"岭回归参数最大值:{np.max(np.abs(model_ridge.coef_)):.2f}")
神奇的实验
修改alpha的值,观察效果:
alpha=0:和普通线性回归一样,严重过拟合alpha=1:曲线变得平滑很多,过拟合大大缓解alpha=100:曲线几乎变成了直线,变成了欠拟合
结论 :正则化强度alpha是一个需要调优的超参数,找到合适的 alpha 值,就能得到泛化能力最好的模型。
📝 本节课总结
- 核心概念:多特征线性回归,每个特征有对应的权重,表示其对结果的影响大小
- 核心思想:过拟合是机器学习的头号敌人,我们的目标是获得泛化能力好的模型
- 核心方法 :
- 划分训练集和测试集,评估模型的真实泛化能力
- 用正则化(岭回归)惩罚大参数,让模型变简单,解决过拟合
- 你已经做到了:能解决多特征预测问题,能识别并解决过拟合问题
🎯 课后作业(必须做)
- 运行上面的所有代码,观察不同多项式次数和不同 alpha 值的效果
- 用你自己写的梯度下降代码,实现多特征线性回归
- 思考:除了正则化,还有哪些方法可以解决过拟合?(提示:前两课提到过)
- 尝试用 scikit-learn 的
Lasso回归(L1 正则化)解决过拟合,看看和岭回归有什么不同