1. 从零开始:为什么用线性回归预测房价是AI入门的绝佳选择
如果你对人工智能感兴趣,想动手做点东西,但又觉得那些复杂的模型和算法离自己太远,那我强烈建议你从“用线性回归预测房价”这个项目开始。这就像学编程先写“Hello World”一样,是机器学习领域的经典入门项目。我刚开始接触AI的时候,也是从这个项目上手,它帮我绕开了很多抽象的理论,直接看到了代码如何驱动模型、数据如何变成预测结果,那种亲手搭建一个“智能”系统的成就感,是看多少教程都换不来的。
为什么是房价预测呢?首先,房价是我们生活中非常熟悉的概念,它受面积、地段、房龄、学区等多种因素影响,这种“多因素决定一个结果”的逻辑,天然就是线性回归模型的用武之地。线性回归的核心思想,就是找到一条直线(或者一个超平面),来最好地拟合这些因素和房价之间的关系。你可以把它想象成,我们手头有一堆波士顿地区房屋的“体检报告”(比如犯罪率、房间数、税率等13项指标),而线性回归模型就是一个聪明的“估价师”,它通过学习历史报告和最终房价,总结出一套自己的估价公式。下次你只要输入一套新房子的13项指标,它就能根据公式给你估个价。
这个项目麻雀虽小,五脏俱全。你会完整地走一遍机器学习项目的标准流程:获取数据 -> 理解数据 -> 清洗预处理 -> 构建模型 -> 训练模型 -> 评估优化。这个过程里,你会遇到并亲手解决很多实际问题,比如数据怎么读进来、数字大小差异太大怎么办、模型怎么才算“学得好”、学得不好又该怎么调。这些经验,是你未来做更复杂的图像识别、自然语言处理项目的基础。说白了,搞定这个,你就拿到了打开机器学习大门的钥匙。
2. 数据准备:像大厨备菜一样处理你的数据集
拿到数据就直接扔给模型训练,就像不做任何处理就把食材下锅,结果很可能是一团糟。数据处理是模型成功的一半,这一步做扎实了,后面的训练会顺利很多。
2.1 获取与观察数据
我们用的经典数据集是波士顿房价数据集。虽然现在一些主流的机器学习库(如Scikit-learn)出于伦理考虑移除了它,但我们完全可以用结构类似的替代数据集,或者自己构造数据来学习原理。这里,为了完全复现过程,我们可以假设从一个文本文件(比如 housing.data)中读取数据。这个文件里,每一行代表一套房子,每一列代表一个特征,最后一列是房价(即我们要预测的目标)。
import numpy as np
import pandas as pd
# 定义特征名称,方便我们理解每一列是什么
feature_names = ['CRIM', 'ZN', 'INDUS', 'CHAS', 'NOX', 'RM', 'AGE', 'DIS', 'RAD', 'TAX', 'PTRATIO', 'B', 'LSTAT', 'MEDV']
# MEDV 就是我们要预测的房价中位数
# 从文件读取数据
data_path = 'housing.data'
data = np.fromfile(data_path, sep=' ') # 数据以空格分隔
feature_num = len(feature_names)
# 将读取的一维数组重塑成二维数组(样本数 x 特征数)
data = data.reshape([data.shape[0] // feature_num, feature_num])
# 用Pandas的DataFrame看一下数据长什么样,更直观
df = pd.DataFrame(data, columns=feature_names)
print(df.head()) # 查看前5行
print(df.describe()) # 查看数据的基本统计信息,如均值、标准差、最值
运行这几行代码,你就能看到数据的全貌了。df.describe() 的输出尤其重要,它会告诉你每个特征的平均值、波动范围(最小最大值)。这时你可能会发现,有的特征(比如‘TAX’房产税率)数值在几百,而有的特征(比如‘CHAS’查尔斯河虚拟变量)只有0或1。这种数量级的巨大差异,会严重影响模型的训练,我们下一步就要解决它。
2.2 数据集的划分:训练、验证与测试
我们不能用所有的数据来训练模型,否则就无法客观评价模型面对新房子时的预测能力了。通常,我们会把数据分成三份:
- 训练集:用来给模型“上课学习”,调整模型内部的参数(权重和偏置)。这部分数据量最大,通常占70%-80%。
- 验证集:用来在训练过程中“随堂测验”,检查模型学得怎么样,帮助我们调整一些“学习策略”(即超参数,如学习率)。它不参与参数更新,就像模拟考。
- 测试集:用来最终“期末考试”,在模型完全训练好后,评估其真实的、泛化的性能。这部分数据在训练过程中绝对不能被“偷看”。
一个简单且常用的划分比例是 8:1:1。下面是一个手动实现的划分示例:
# 设定随机种子,确保每次划分结果一致,便于复现
np.random.seed(2023)
# 打乱数据顺序,防止原始数据有顺序性影响
np.random.shuffle(data)
# 计算划分比例
total_num = len(data)
train_ratio, eval_ratio = 0.8, 0.1
train_num = int(total_num * train_ratio)
eval_num = int(total_num * eval_ratio)
# 划分
train_data = data[:train_num]
eval_data = data[train_num: train_num + eval_num]
test_data = data[train_num + eval_num:]
print(f"训练集样本数:{len(train_data)}, 验证集样本数:{len(eval_data)}, 测试集样本数:{len(test_data)}")
2.3 数据归一化:让所有特征站在同一起跑线
这是数据处理中最关键的一步。想象一下,你要根据“距离市中心的公里数”和“房间数量”来预测房价。距离的单位是“公里”,数值可能是10、20;房间数的单位是“个”,数值可能是3、4。模型会认为数值大的特征(距离)更重要,这显然不合理。归一化就是把所有特征都缩放到一个相近的数值区间,消除量纲影响。
方法一:Min-Max归一化(最值归一化)
这种方法把数据线性地映射到[0, 1]区间。公式是:新值 = (原值 - 最小值) / (最大值 - 最小值)。它的好处是计算简单,结果严格在0到1之间。但缺点是如果数据集中有特别大或特别小的异常值,会对结果产生很大影响。
# 注意:计算最小值和最大值时,只用训练集的数据!
# 这是为了防止信息泄露,即我们不能从测试集获取任何信息来影响训练过程。
maximums = train_data.max(axis=0) # 每一列的最大值
minimums = train_data.min(axis=0) # 每一列的最小值
# 对训练集、验证集、测试集都使用训练集计算出的最大最小值进行归一化
for i in range(feature_num):
train_data[:, i] = (train_data[:, i] - minimums[i]) / (maximums[i] - minimums[i])
eval_data[:, i] = (eval_data[:, i] - minimums[i]) / (maximums[i] - minimums[i])
test_data[:, i] = (test_data[:, i] - minimums[i]) / (maximums[i] - minimums[i])
方法二:Z-Score归一化(标准化)
这种方法使处理后的数据均值为0,标准差为1,符合标准正态分布。公式是:新值 = (原值 - 均值) / 标准差。它的优点是对异常值不那么敏感,因为标准差考虑了所有数据的分布。在房价预测中,如果某个特征的数据分布比较“正态”,用这个方法效果通常更好。
# 同样,均值和标准差只从训练集计算
means = train_data.mean(axis=0)
stds = train_data.std(axis=0)
# 避免除零错误,标准差为0的列(常数特征)不做处理
for i in range(feature_num):
if stds[i] > 1e-7: # 一个很小的阈值
train_data[:, i] = (train_data[:, i] - means[i]) / stds[i]
eval_data[:, i] = (eval_data[:, i] - means[i]) / stds[i]
test_data[:, i] = (test_data[:, i] - means[i]) / stds[i]
在实际项目中,我通常会两种方法都试试,看哪个能让模型收敛更快、效果更好。对于波士顿房价这类数据,Z-Score往往是更稳妥的选择。
3. 模型构建:亲手搭建一个线性回归“神经网络”
很多人一听“神经网络”就觉得高大上,其实线性回归可以看作是一个没有隐藏层的最简单的神经网络。我们来一步步把它实现出来。
3.1 理解模型的核心:假设与计算
线性回归的假设非常直观:我们认为房价 y 和所有特征 x1, x2, ..., x13 之间是线性关系。用数学公式表示就是:
y_pred = w1*x1 + w2*x2 + ... + w13*x13 + b
其中,w1 到 w13 是每个特征的权重,代表了该特征对房价的重要性(比如 RM房间数的权重可能很大且为正);b 是偏置项,可以理解为基础房价。
我们的目标,就是从数据中学习出最优的一组 w 和 b,使得模型预测的 y_pred 尽可能接近真实的房价 y_true。
3.2 用Python类实现模型
我们用面向对象的方式,创建一个 LinearRegression 类,这样代码更清晰,也符合工业界的习惯。
class LinearRegression:
def __init__(self, num_features):
"""
初始化模型参数。
num_features: 特征的数量,这里是13。
"""
np.random.seed(2023) # 固定随机种子,保证结果可复现
# 权重w初始化为一个很小的随机数,形状为 (13, 1)
self.w = np.random.normal(0, 0.01, size=(num_features, 1))
# 偏置b初始化为0
self.b = 0.0
def forward(self, x):
"""
前向传播(预测)。
x: 输入数据,形状为 (batch_size, 13)
返回: 预测值,形状为 (batch_size, 1)
"""
# 核心计算:y_pred = x * w + b
# np.dot 是矩阵乘法,这里 x (batch,13) 点乘 w (13,1) 得到 (batch,1)
y_pred = np.dot(x, self.w) + self.b
return y_pred
这个 forward 函数就是我们的“估价公式”。给它一批房子的特征数据 x,它就能批量输出预测房价。
3.3 定义损失函数:模型“考”得怎么样?
模型预测得准不准,需要一个量化的标准来衡量,这就是损失函数。对于回归问题,最常用的就是均方误差。它的思想很简单:计算所有预测值和真实值之差的平方,再求平均。差值越大,平方就越大,损失也就越大。
def loss(self, y_pred, y_true):
"""
计算均方误差损失。
y_pred: 模型预测值,形状 (batch_size, 1)
y_true: 真实标签值,形状 (batch_size, 1)
返回: 一个标量损失值
"""
error = y_pred - y_true
# 计算均方误差:平均(误差的平方)
cost = np.mean(error ** 2)
return cost
我们的训练目标,就是通过调整 w 和 b,让这个 loss 的值越小越好。
3.4 实现优化器:让模型“学会”的关键——梯度下降
模型现在会“猜”了,也知道自己“猜”得有多不准(损失值)。那怎么让它变“聪明”呢?这就需要梯度下降算法。你可以把它想象成蒙着眼下山,你要找到山谷的最低点(损失最小)。你每走一步前,都会用脚感受一下四周哪个方向最陡峭(计算梯度),然后就往那个方向迈一步(更新参数)。
梯度就是损失函数对每个参数的偏导数,它指明了参数调整的方向和幅度。
def gradient(self, x, y_true):
"""
计算损失函数关于参数w和b的梯度。
x: 输入数据,形状 (batch_size, 13)
y_true: 真实标签,形状 (batch_size, 1)
返回: gradient_w, gradient_b
"""
y_pred = self.forward(x)
batch_size = x.shape[0]
# 梯度公式推导结果:
# 对w的梯度 = (2/batch_size) * (x.T 点乘 (y_pred - y_true))
# 对b的梯度 = (2/batch_size) * 求和(y_pred - y_true)
error = y_pred - y_true
gradient_w = (2.0 / batch_size) * np.dot(x.T, error)
gradient_b = (2.0 / batch_size) * np.sum(error)
return gradient_w, gradient_b
def update(self, gradient_w, gradient_b, learning_rate):
"""
根据梯度和学习率更新参数。
learning_rate: 学习率,控制每次更新的步长。
"""
self.w = self.w - learning_rate * gradient_w
self.b = self.b - learning_rate * gradient_b
这里出现了第一个超参数——学习率。它决定了我们每次更新参数时“迈的步子”有多大。步子太大,可能会跨过最低点,甚至导致损失越来越大(发散);步子太小,下山速度太慢,训练时间会很长。找到一个合适的学习率非常重要。
4. 模型训练与评估:见证AI的“学习”过程
万事俱备,现在让我们把数据喂给模型,开始真正的训练循环。
4.1 组织训练数据:Mini-Batch的重要性
我们通常不会一次用光所有训练数据来计算梯度(那叫批量梯度下降,计算量大),也不会一次只用一条数据(随机梯度下降,波动太大)。折中的方法是使用Mini-Batch,即每次从训练集中随机抽取一小批(比如16、32、64条)数据进行计算。这样做既兼顾了效率,又保持了梯度更新的稳定性。
def train(self, train_data, eval_data, num_epochs, batch_size, learning_rate):
"""
训练模型。
num_epochs: 遍历整个训练集的轮数。
batch_size: 每个小批次的样本数。
learning_rate: 学习率。
"""
n_train = len(train_data)
train_losses = [] # 记录每批次的训练损失
eval_losses = [] # 记录每轮次的验证损失
for epoch in range(num_epochs):
# 每轮开始前,打乱训练数据顺序
np.random.shuffle(train_data)
# 将训练数据分成多个小批次
mini_batches = [train_data[k: k+batch_size] for k in range(0, n_train, batch_size)]
for batch in mini_batches:
# 分离特征和标签
x_batch = batch[:, :-1] # 前13列是特征
y_batch = batch[:, -1:] # 最后一列是房价标签
# 前向计算,得到预测值
y_pred = self.forward(x_batch)
# 计算当前批次的损失
batch_loss = self.loss(y_pred, y_batch)
train_losses.append(batch_loss)
# 计算梯度
grad_w, grad_b = self.gradient(x_batch, y_batch)
# 更新参数
self.update(grad_w, grad_b, learning_rate)
# 每轮训练结束后,在验证集上评估一次
eval_loss = self.evaluate(eval_data, batch_size)
eval_losses.append(eval_loss)
# 打印进度
print(f"Epoch {epoch+1}/{num_epochs}, Train Loss: {batch_loss:.6f}, Eval Loss: {eval_loss:.6f}")
return train_losses, eval_losses
def evaluate(self, dataset, batch_size):
"""在给定数据集(验证集或测试集)上评估模型损失"""
losses = []
n = len(dataset)
mini_batches = [dataset[k: k+batch_size] for k in range(0, n, batch_size)]
for batch in mini_batches:
x = batch[:, :-1]
y_true = batch[:, -1:]
y_pred = self.forward(x)
batch_loss = self.loss(y_pred, y_true)
losses.append(batch_loss)
return np.mean(losses) # 返回平均损失
4.2 启动训练与观察
现在,让我们初始化模型并开始训练。
# 初始化模型,13个特征
model = LinearRegression(num_features=13)
# 设置超参数
num_epochs = 100 # 训练轮数
batch_size = 32 # 批次大小
learning_rate = 0.01 # 学习率
print("开始训练...")
train_loss_history, eval_loss_history = model.train(
train_data, eval_data, num_epochs, batch_size, learning_rate
)
print("训练结束!")
训练过程中,你会看到控制台不断输出每一轮(Epoch)的损失值。一个健康的训练过程,训练损失和验证损失都应该随着轮数增加而稳步下降,并逐渐趋于平缓。你可以把这两个损失的历史记录画成图,这是观察模型学习状态最直观的方式。
import matplotlib.pyplot as plt
plt.figure(figsize=(10, 5))
# 训练损失是每个批次都记录的,点太多,我们画平滑的曲线或取平均
# 这里简单起见,我们每10个批次取一个平均点来画训练损失
if len(train_loss_history) > 100:
train_loss_smooth = [np.mean(train_loss_history[i:i+10]) for i in range(0, len(train_loss_history), 10)]
plt.plot(range(len(train_loss_smooth)), train_loss_smooth, label='Train Loss (smoothed)')
else:
plt.plot(train_loss_history, label='Train Loss')
plt.plot(eval_loss_history, label='Eval Loss', linewidth=2)
plt.xlabel('Epoch')
plt.ylabel('Loss (MSE)')
plt.title('Training and Evaluation Loss Curve')
plt.legend()
plt.grid(True)
plt.show()
如果图像显示两条曲线都在下降后保持在一个较低的水平,并且没有明显分开(验证损失没有突然大幅上升),说明模型训练得不错,没有严重过拟合。
4.3 最终测试与模型保存
训练和验证都完成后,我们终于可以在从未见过的测试集上检验模型的最终性能了。这是对模型泛化能力的终极考验。
# 在测试集上评估
final_test_loss = model.evaluate(test_data, batch_size=32)
print(f"模型在测试集上的最终损失(MSE)为:{final_test_loss:.6f}")
# 我们可以随机取测试集里的一条数据,看看预测值和真实值的对比
sample_idx = 0
x_sample = test_data[sample_idx, :-1].reshape(1, -1) # 取一个样本的特征
y_true_sample = test_data[sample_idx, -1]
y_pred_sample = model.forward(x_sample)
print(f"\n测试样本 {sample_idx} 预测演示:")
print(f" 真实房价(归一化后): {y_true_sample:.4f}")
print(f" 模型预测房价(归一化后): {y_pred_sample[0,0]:.4f}")
print(f" 预测误差: {abs(y_true_sample - y_pred_sample[0,0]):.4f}")
# 注意:这里的房价是归一化后的值。如果你想看到原始房价,需要反向归一化。
# 假设你之前用的是min-max归一化,且保存了训练集的maximums和minimums
# 反向归一化公式:原始值 = 归一化值 * (max - min) + min
# 这里需要用到最后一列(MEDV)对应的最大值和最小值
# original_price = y_pred_sample * (max_price - min_price) + min_price
最后,别忘了保存你辛苦训练好的模型参数,这样下次可以直接加载使用,无需重新训练。
# 保存模型参数
np.savez('boston_housing_model.npz', w=model.w, b=model.b)
print("模型参数已保存至 'boston_housing_model.npz'")
# 加载模型的示例
# loaded_data = np.load('boston_housing_model.npz')
# model.w = loaded_data['w']
# model.b = loaded_data['b']
5. 调参优化:从“能用”到“好用”的进阶之路
模型第一次跑通,损失值降下来了,这很棒!但你可能发现预测还不是特别准,或者训练过程有点慢。别急,这才是机器学习的精髓所在——调优。下面我分享几个最实用、效果最明显的调参技巧,这也是我踩过不少坑才总结出的经验。
5.1 学习率:寻找“黄金步长”
学习率是影响训练最重要的超参数。我习惯从一个较小的值开始尝试,比如0.01或0.001。
- 现象1:损失值下降非常慢,甚至几乎不变。
这很可能是学习率太小了。模型“步子”迈得太小,在“下山”的路上蠕动,需要很久才能到达谷底。你可以尝试将学习率增大10倍(比如从0.001调到0.01)。
- 现象2:损失值剧烈震荡,时而下降时而飙升,甚至变成NaN(非数字)。
这绝对是学习率太大了!模型“步子”迈得太大,直接从山谷这边跳到了那边,甚至跳出了山谷,导致损失爆炸。你必须立刻减小学习率,比如降到原来的十分之一。
一个更高级的策略是使用学习率衰减。在训练初期,使用较大的学习率快速下降;在训练后期,使用较小的学习率精细调整,避免在最低点附近徘徊。你可以实现一个简单的指数衰减:
initial_lr = 0.01
decay_rate = 0.95
decay_steps = 100 # 每100个批次衰减一次
for epoch in range(num_epochs):
for batch_idx, batch in enumerate(mini_batches):
current_step = epoch * len(mini_batches) + batch_idx
# 计算当前学习率
current_lr = initial_lr * (decay_rate ** (current_step // decay_steps))
# 使用 current_lr 更新参数
model.update(grad_w, grad_b, current_lr)
5.2 批次大小:平衡速度与稳定性
batch_size 决定了每次更新参数时看到的数据量。
batch_size较小(如16,32):梯度更新更频繁,收敛速度可能更快,同时引入的随机噪声有助于模型跳出局部最优解。但每次更新的方向波动较大,不够稳定。batch_size较大(如256,整个训练集):梯度估计更准确,更新方向更稳定。但对内存要求高,且可能会使模型陷入平坦的局部最小值,缺乏“探索”能力。
对于波士顿房价这种几千条数据的中小规模数据集,我通常从32或64开始尝试。你可以固定其他参数,分别用16、32、64、128跑一下,观察训练损失曲线的平滑度和最终验证损失,选择一个表现最好的。
5.3 训练轮数:防止“学过头”
num_epochs 决定了模型看多少遍整个训练集。轮数太少,模型没学够,这叫欠拟合,表现是训练损失和验证损失都还很高。轮数太多,模型把训练数据中的噪声和特定模式都记住了,但在新数据上表现变差,这叫过拟合,表现是训练损失持续降低,但验证损失在某个点后开始上升。
早停法 是应对过拟合的利器。我们在训练时持续监控验证集损失,当发现验证损失连续多个轮次不再下降(甚至开始上升)时,就停止训练,并回滚到验证损失最低的那个轮次的模型参数。
best_eval_loss = float('inf')
patience = 10 # 容忍验证损失不下降的轮数
patience_counter = 0
best_model_params = None
for epoch in range(num_epochs):
# ... 训练一个epoch ...
current_eval_loss = model.evaluate(eval_data, batch_size)
if current_eval_loss < best_eval_loss:
best_eval_loss = current_eval_loss
patience_counter = 0
# 保存当前最优模型参数
best_model_params = (model.w.copy(), model.b.copy())
print(f" 发现更优模型,验证损失更新为: {best_eval_loss:.6f}")
else:
patience_counter += 1
print(f" 验证损失未改善,耐心计数: {patience_counter}/{patience}")
if patience_counter >= patience:
print(f"早停触发!在第 {epoch+1} 轮停止训练。")
# 加载最优参数
model.w, model.b = best_model_params
break
5.4 特征工程:给模型更好的“食材”
有时候,调参到了瓶颈,不如回头看看数据。特征工程就是通过创造、转换或选择特征,让数据更能被模型理解。对于线性回归,一个简单有效的技巧是多项式特征。比如,我们怀疑“房间数”和“房价”可能不是简单的直线关系,而是曲线关系。我们可以创建一个新特征 RM_squared = RM ** 2,把它也加入特征中。
# 假设原始特征矩阵 x 形状为 (n_samples, 13)
# 我们为第5个特征‘RM’(房间数)添加平方项
rm_index = feature_names.index('RM') # 找到RM特征的索引
rm_feature = train_data[:, rm_index]
rm_squared = rm_feature ** 2
# 将新特征拼接到原始数据上
# 注意:需要同时对训练集、验证集、测试集做同样的操作
train_data = np.column_stack((train_data, rm_squared))
# ... 对 eval_data, test_data 做同样操作 ...
# 别忘了更新特征数量 num_features = 14
这个操作相当于让模型去拟合 y = w1*RM + w2*RM^2 + ... + b,它就有了拟合非线性关系的能力。你可以尝试为其他你认为重要的特征添加交互项或高次项,但要注意,增加特征也可能引入噪声或导致过拟合,需要配合验证集谨慎使用。
经过这一系列的构建、训练、评估和调优,你已经完整地实践了一个机器学习项目。这个过程中,你不仅学会了线性回归和梯度下降的原理,更重要的是掌握了处理数据、调试模型、分析结果的完整方法论。下次当你遇到新的预测问题时,这套流程和思考方式依然适用。我刚开始做这个项目时,为了把验证损失降那么一点点,反复调整学习率、尝试不同的归一化方法,看着损失曲线一点点变平缓,那种感觉就像在打磨一件作品。现在,你可以试着用同样的方法,去预测一下股票走势、销售额,或者任何你感兴趣的连续值数据了。记住,动手做,然后不断迭代,是学习AI最快的方式。
9060



被折叠的 条评论
为什么被折叠?



