1. 从零开始构建线性模型
记得我第一次接触机器学习时,导师就说过:"线性回归是机器学习的'Hello World',但90%的新手都没真正搞懂它。"今天我们就用Python和NumPy从零开始实现一个线性回归模型,不借助任何现成的机器学习框架。这种方式能让你真正理解梯度下降、损失函数这些核心概念的运作机制。
为什么选择线性模型作为入门?首先,它是理解更复杂模型的基础;其次,在特征工程得当的情况下,线性模型往往能产生出乎意料的好效果;最重要的是,通过手动实现,你会对矩阵运算、求导链式法则等数学知识有更直观的认识。本文假设你已经具备基础的Python编程能力和高中数学知识,我们会从理论到实践完整走一遍建模流程。
2. 理论基础与模型设计
2.1 线性回归数学表达
线性模型的基本形式为:
code复制ŷ = w·x + b
其中:
- ŷ 是我们的预测值
- w 是权重(weight)向量
- x 是输入特征向量
- b 是偏置(bias)项
在多元情况下,这可以扩展为矩阵运算:
code复制Ŷ = XW + b
这里X是m×n的特征矩阵(m个样本,n个特征),W是n×1的权重矩阵。
2.2 损失函数选择
我们使用均方误差(MSE)作为损失函数:
code复制L = 1/m * Σ(ŷ_i - y_i)²
MSE的优点在于:
- 处处可微,便于梯度计算
- 对离群点敏感(这既是优点也是缺点)
- 几何意义明确(预测值与真实值的欧式距离)
注意:当数据存在大量离群点时,可以考虑使用Huber损失等鲁棒性更强的损失函数
2.3 梯度下降原理
参数更新公式:
code复制w := w - α * ∂L/∂w
b := b - α * ∂L/∂b
其中α是学习率,控制每次更新的步长。
计算梯度:
code复制∂L/∂w = 2/m * Xᵀ(Ŷ - Y)
∂L/∂b = 2/m * Σ(Ŷ - Y)
3. Python实现细节
3.1 数据准备
我们首先生成一些合成数据用于测试:
python复制import numpy as np
# 设置随机种子保证可复现
np.random.seed(42)
# 生成100个样本,1个特征
m = 100
X = 2 * np.random.rand(m, 1)
y = 4 + 3 * X + np.random.randn(m, 1) # 添加高斯噪声
# 划分训练集测试集
split = int(m*0.8)
X_train, X_test = X[:split], X[split:]
y_train, y_test = y[:split], y[split:]
3.2 模型实现
完整线性回归类实现:
python复制class LinearRegression:
def __init__(self, learning_rate=0.01, n_iters=1000):
self.lr = learning_rate
self.n_iters = n_iters
self.weights = None
self.bias = None
def fit(self, X, y):
# 初始化参数
n_samples, n_features = X.shape
self.weights = np.zeros(n_features)
self.bias = 0
# 梯度下降
for _ in range(self.n_iters):
y_pred = np.dot(X, self.weights) + self.bias
# 计算梯度
dw = (2/n_samples) * np.dot(X.T, (y_pred - y))
db = (2/n_samples) * np.sum(y_pred - y)
# 更新参数
self.weights -= self.lr * dw
self.bias -= self.lr * db
def predict(self, X):
return np.dot(X, self.weights) + self.bias
3.3 训练与评估
python复制# 实例化并训练模型
regressor = LinearRegression(learning_rate=0.1, n_iters=1000)
regressor.fit(X_train, y_train)
# 预测测试集
y_pred = regressor.predict(X_test)
# 计算MSE
mse = np.mean((y_pred - y_test)**2)
print(f"测试集MSE: {mse:.4f}")
print(f"学得参数: w={regressor.weights[0]:.2f}, b={regressor.bias:.2f}")
典型输出结果:
code复制测试集MSE: 0.8923
学得参数: w=2.94, b=4.12
4. 关键问题与优化
4.1 学习率选择
学习率对训练效果影响巨大:
- 过大:震荡甚至发散
- 过小:收敛速度慢
建议策略:
- 先用0.001、0.01、0.1等典型值尝试
- 观察损失曲线:
- 理想情况:平滑单调下降
- 震荡:调小学习率
- 下降过慢:适当增大
4.2 特征缩放的重要性
当特征量纲差异大时,应当进行标准化:
python复制from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
原因:
- 使梯度下降更快收敛
- 避免某些特征主导更新过程
- 帮助更合理地设置统一学习率
4.3 迭代终止条件
除了固定迭代次数,还可以:
- 设置损失阈值:当损失变化<ε时停止
- 早停法:验证集性能不再提升时停止
改进后的训练循环:
python复制for epoch in range(self.n_iters):
y_pred = self._approximation(X)
loss = self._compute_loss(y, y_pred)
# 早停检查
if abs(loss - prev_loss) < 1e-6:
break
prev_loss = loss
# 更新参数...
5. 扩展与改进
5.1 添加正则化项
为了防止过拟合,可以在损失函数中加入L2正则化:
python复制class RidgeRegression(LinearRegression):
def __init__(self, alpha=1.0, **kwargs):
super().__init__(**kwargs)
self.alpha = alpha # 正则化系数
def _compute_loss(self, y, y_pred):
mse = super()._compute_loss(y, y_pred)
l2_penalty = self.alpha * np.sum(self.weights**2)
return mse + l2_penalty
def _compute_gradients(self, X, y, y_pred):
dw = super()._compute_gradients(X, y, y_pred)
dw += 2 * self.alpha * self.weights
return dw
5.2 批量与随机梯度下降
我们实现的是批量梯度下降(BGD),还可以尝试:
- 随机梯度下降(SGD):每次用一个样本更新
- 小批量梯度下降(Mini-batch GD):折中方案
SGD实现关键部分:
python复制for epoch in range(self.n_iters):
for i in range(n_samples):
# 随机选择一个样本
idx = np.random.randint(n_samples)
x_i = X[idx:idx+1]
y_i = y[idx:idx+1]
# 计算单个样本的梯度并更新
y_pred = self._approximation(x_i)
dw, db = self._compute_gradients(x_i, y_i, y_pred)
self.weights -= self.lr * dw
self.bias -= self.lr * db
5.3 多项式特征扩展
线性模型可以通过特征工程处理非线性关系:
python复制from sklearn.preprocessing import PolynomialFeatures
poly = PolynomialFeatures(degree=2, include_bias=False)
X_poly = poly.fit_transform(X)
这样模型就能拟合形如 y = w1x + w2x² + b 的二次关系。
6. 完整代码示例
以下是整合了所有优化后的完整实现:
python复制import numpy as np
from sklearn.preprocessing import StandardScaler
class LinearRegression:
def __init__(self, learning_rate=0.01, n_iters=1000, batch_size=None, alpha=0):
self.lr = learning_rate
self.n_iters = n_iters
self.batch_size = batch_size # None=全批量, 1=SGD, >1=Mini-batch
self.alpha = alpha # L2正则化系数
self.weights = None
self.bias = None
self.scaler = StandardScaler()
def fit(self, X, y, verbose=False):
# 特征标准化
X = self.scaler.fit_transform(X)
# 初始化参数
n_samples, n_features = X.shape
self.weights = np.zeros(n_features)
self.bias = 0
# 训练循环
prev_loss = float('inf')
for epoch in range(self.n_iters):
if self.batch_size:
# 小批量梯度下降
indices = np.random.permutation(n_samples)
X_shuffled = X[indices]
y_shuffled = y[indices]
for i in range(0, n_samples, self.batch_size):
X_batch = X_shuffled[i:i+self.batch_size]
y_batch = y_shuffled[i:i+self.batch_size]
y_pred = self._approximation(X_batch)
dw, db = self._compute_gradients(X_batch, y_batch, y_pred)
self._update_parameters(dw, db)
else:
# 全批量梯度下降
y_pred = self._approximation(X)
dw, db = self._compute_gradients(X, y, y_pred)
self._update_parameters(dw, db)
# 计算当前损失
y_pred = self._approximation(X)
loss = self._compute_loss(y, y_pred)
if verbose and epoch % 100 == 0:
print(f"Epoch {epoch}, Loss: {loss:.4f}")
# 早停检查
if abs(loss - prev_loss) < 1e-6:
if verbose:
print(f"Early stopping at epoch {epoch}")
break
prev_loss = loss
def _approximation(self, X):
return np.dot(X, self.weights) + self.bias
def _compute_loss(self, y, y_pred):
mse = np.mean((y_pred - y)**2)
l2_penalty = self.alpha * np.sum(self.weights**2)
return mse + l2_penalty
def _compute_gradients(self, X, y, y_pred):
error = y_pred - y
dw = (2/X.shape[0]) * np.dot(X.T, error) + 2 * self.alpha * self.weights
db = (2/X.shape[0]) * np.sum(error)
return dw, db
def _update_parameters(self, dw, db):
self.weights -= self.lr * dw
self.bias -= self.lr * db
def predict(self, X):
X = self.scaler.transform(X)
return self._approximation(X)
使用示例:
python复制# 生成数据
X = 2 * np.random.rand(100, 1)
y = 4 + 3 * X + np.random.randn(100, 1)
# 创建模型实例
model = LinearRegression(
learning_rate=0.1,
n_iters=1000,
batch_size=16, # 小批量梯度下降
alpha=0.1 # L2正则化
)
# 训练模型
model.fit(X, y, verbose=True)
# 预测新数据
X_new = np.array([[1.5], [2.0]])
y_pred = model.predict(X_new)
print(f"预测结果: {y_pred.flatten()}")
7. 实际应用建议
-
学习率调度:随着训练进行动态调整学习率,如:
python复制self.lr = initial_lr / (1 + decay_rate * epoch) -
动量加速:加入动量项加速收敛:
python复制self.velocity = 0.9 * self.velocity + self.lr * dw self.weights -= self.velocity -
特征重要性分析:通过权重大小判断特征重要性:
python复制feature_importance = np.abs(model.weights) -
模型持久化:保存和加载模型参数:
python复制def save(self, path): np.savez(path, weights=self.weights, bias=self.bias) @classmethod def load(cls, path): data = np.load(path) model = cls() model.weights = data['weights'] model.bias = data['bias'] return model -
可视化训练过程:绘制损失曲线和决策边界:
python复制plt.plot(loss_history) plt.xlabel('Epoch') plt.ylabel('Loss') plt.title('Training Loss Curve')
手动实现线性模型的最大收获是真正理解了每个参数更新的数学原理。在实际项目中,虽然我们会使用scikit-learn等成熟库,但这段经历让我能更自信地调试模型参数。建议每个机器学习从业者都至少亲手实现一次这些基础算法——它们就像乐高积木的基础块,看似简单,却是构建复杂模型的基石。