上一篇主要把机器学习入门、监督学习、线性回归模型和代价函数顺了一遍。那时候我已经知道,训练线性回归大概就是找到合适的 $w$ 和 $b$,让代价函数 $J(w,b)$ 尽可能小。

这一篇就接着往下走:模型不是靠我手动试 $w$ 和 $b$,而是用梯度下降一点点自己调整参数。然后再从单特征扩展到多维特征,看看向量化、特征缩放和多项式回归为什么会出现。

我现在对这部分的理解是:梯度下降是训练的核心方法,多维特征让模型能处理更真实的问题,特征缩放和向量化则是在让训练更顺、更高效。

梯度下降:模型怎么自己变好

梯度下降(Gradient Descent,梯度下降)可以先理解成:沿着代价函数下降的方向,一步一步调整参数。

在线性回归里,参数更新公式是:

这里:

  • $w,b$:模型参数。
  • $J(w,b)$:代价函数,用来衡量预测错多少。
  • $\alpha$:学习率,控制每一步走多大。
  • $\frac{\partial J}{\partial w}$、$\frac{\partial J}{\partial b}$:告诉参数应该往哪个方向调整。
  • := 表示更新赋值。

我现在把它想成下坡:当前位置是当前参数,地形高度是代价函数。梯度告诉我坡往哪里下,学习率决定我每一步迈多大。

这里还有一个细节:$w$ 和 $b$ 应该同步更新。也就是说,这一轮更新 $w$ 和 $b$ 时,都应该基于同一组旧参数来计算,而不是先更新了 $w$,再拿新的 $w$ 去更新 $b$。

梯度下降用于线性回归

线性回归模型是:

代价函数是:

这里 $m$ 是训练样本数量,$x^{(i)}$ 是第 $i$ 个样本的输入,$y^{(i)}$ 是对应的真实值。

线性回归里,梯度可以写成:

我现在先不强行记推导,更关注它在代码里做了什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def compute_gradient(x, y, w, b):
m = x.shape[0]
dj_dw = 0
dj_db = 0

for i in range(m):
f_wb = w * x[i] + b
error = f_wb - y[i]
dj_dw += error * x[i]
dj_db += error

dj_dw = dj_dw / m
dj_db = dj_db / m

return dj_dw, dj_db

这段代码是在计算当前 $w,b$ 下,代价函数分别对 $w$ 和 $b$ 的斜率。

  • error 是预测值和真实值之间的差。
  • dj_dw 会把误差和输入特征一起考虑。
  • dj_db 只累加误差。
  • 最后除以 m,得到平均意义上的梯度。

有了梯度以后,就可以写梯度下降:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def gradient_descent(x, y, w_init, b_init, alpha, num_iters):
w = w_init
b = b_init
J_history = []

for i in range(num_iters):
dj_dw, dj_db = compute_gradient(x, y, w, b)

w = w - alpha * dj_dw
b = b - alpha * dj_db

J_history.append(compute_cost(x, y, w, b))

return w, b, J_history

这段代码的主线就是:

  1. 计算当前参数的梯度。
  2. 根据学习率更新参数。
  3. 记录当前代价函数。
  4. 重复很多次。

J_history 对我来说很有用,因为它可以让我看训练是不是在往好的方向走。如果代价函数一直下降,说明梯度下降大概率在工作;如果忽高忽低,甚至越来越大,就要检查学习率或代码。

批梯度下降

课程里这里用的是批梯度下降(Batch Gradient Descent,批梯度下降)。

它的意思是:每次更新参数时,都使用全部训练样本来计算梯度。

我现在先这样理解:

  • Batch:一次看完整个训练集。
  • 优点:更新方向比较稳定。
  • 缺点:如果数据特别大,每次都看完整训练集会比较慢。

现在入门阶段用它比较合适,因为逻辑最清楚:每一轮都是基于全部数据算一次平均方向。

从单特征到多维特征

上一篇和前面代码里,一个样本只有一个输入特征:

但真实任务通常不只靠一个特征。比如预测房价时,可能会看:

  • 面积
  • 卧室数量
  • 楼层
  • 房龄

这时模型就变成多维特征线性回归:

也可以写成向量形式:

这里:

  • $\mathbf{x}$:一个样本的所有特征。
  • $\mathbf{w}$:每个特征对应的参数。
  • $n$:特征数量。
  • $m$:训练样本数量。

代码里可以这样表示数据:

1
2
3
4
5
6
7
8
9
import numpy as np

X_train = np.array([
[2104, 5, 1, 45],
[1416, 3, 2, 40],
[852, 2, 1, 35]
])

y_train = np.array([460, 232, 178])

这里:

  • X_train 是二维数组。
  • 每一行是一套房子。
  • 每一列是一个特征。
  • X_train.shape(m, n)
  • y_train 是每套房子对应的真实价格。

多维特征不是换了一个完全陌生的模型,而是把一个输入扩展成多个输入。

向量化:让代码更接近公式

如果不用向量化,可以用循环算一个样本的预测:

1
2
3
4
5
def predict_loop(x, w, b):
p = 0
for j in range(x.shape[0]):
p += x[j] * w[j]
return p + b

这段代码很直观:每个特征乘上对应权重,再全部加起来。

但用 NumPy 可以写得更短:

1
2
def predict(x, w, b):
return np.dot(x, w) + b

np.dot(x, w) 对应数学里的点积:

我现在把向量化(Vectorization,向量化)理解成:用数组运算替代手写循环。

它的好处是:

  • 代码更短。
  • 计算更快。
  • 更适合多特征。
  • 和数学公式更一致。

后面到了神经网络,向量化会更常见,所以这里先建立感觉很重要。

多维特征的代价函数和梯度

多维特征下,代价函数还是衡量预测值和真实值之间的差:

向量化代码可以写成:

1
2
3
4
5
6
def compute_cost(X, y, w, b):
m = X.shape[0]
f_wb = X @ w + b
errors = f_wb - y
cost = (1 / (2 * m)) * np.sum(errors ** 2)
return cost

这里:

  • X @ w + b 一次算出所有样本的预测值。
  • errors 是所有样本的预测误差。
  • np.sum(errors ** 2) 把误差平方加起来。

梯度也可以向量化:

1
2
3
4
5
6
7
8
9
def compute_gradient(X, y, w, b):
m = X.shape[0]
f_wb = X @ w + b
error = f_wb - y

dj_dw = (1 / m) * (X.T @ error)
dj_db = (1 / m) * np.sum(error)

return dj_db, dj_dw

这里最关键的是 X.T @ error。它一次性算出了所有特征对应的梯度。

我现在先把它理解成:每个特征都要看自己和预测误差之间的关系,X.T @ error 就是在同时做这件事。

特征缩放

特征缩放(Feature Scaling,特征缩放)是多维特征里很重要的一步。

原因很直观:不同特征的数值范围可能差很多。

比如:

  • 面积可能是几百到几千。
  • 卧室数量可能只有 1 到 5。
  • 房龄可能是几十。

如果这些特征直接放进模型,梯度下降可能会变慢,因为不同参数更新时面对的尺度差别太大。

常见特征缩放方法有几种。

最大值归一化:

均值归一化:

Z-score 标准化:

这里我重点先记 Z-score。它的意思是:每个特征先减去自己的平均值,再除以自己的标准差。

代码是:

1
2
3
4
5
def zscore_normalize_features(X):
mu = np.mean(X, axis=0)
sigma = np.std(X, axis=0)
X_norm = (X - mu) / sigma
return X_norm, mu, sigma

这里:

  • mu 是每一列特征的平均值。
  • sigma 是每一列特征的标准差。
  • axis=0 表示按列计算。
  • X_norm 是标准化后的特征矩阵。

标准化后,不同特征会处在更接近的范围里,梯度下降通常会更稳定。

学习率怎么选

学习率 $\alpha$ 决定每次更新参数走多大一步。

我现在还是用“下坡步长”来理解它:

  • $\alpha$ 太小:下降很慢。
  • $\alpha$ 合适:代价函数稳定下降。
  • $\alpha$ 太大:可能越过最低点,甚至发散。

调试梯度下降时,可以看 $J$ 是否随着迭代次数下降。

如果 J_history 下降得很慢,学习率可能太小;如果 J_history 越来越大,学习率可能太大。

特征缩放也会影响学习率选择。特征范围比较接近时,学习率通常更容易选。

特征工程

特征工程(Feature Engineering,特征工程)可以理解成:人为构造更有用的输入特征。

比如房价不一定和面积完全线性相关。原来只有一个面积特征 $x$,可以加入面积的平方:

这不是让模型自己从零发现所有东西,而是我根据对问题的理解,给模型提供更合适的输入。

现在我对特征工程的感觉是:它提醒我,机器学习不只是调公式,数据怎么表示也很重要。

多项式回归

多项式回归(Polynomial Regression,多项式回归)就是一个特征工程的例子。

模型可以写成:

代码里可以构造多项式特征:

1
2
3
4
x = np.arange(0, 20, 1)
X = np.c_[x, x**2, x**3]

X_norm, mu, sigma = zscore_normalize_features(X)

这段代码做了两件事:

  • np.c_[x, x**2, x**3] 把一个特征扩展成多个多项式特征。
  • zscore_normalize_features(X) 对扩展后的特征做标准化。

虽然模型里出现了 $x^2$、$x^3$,但它对参数 $w_1,w_2,w_3,b$ 仍然是线性的,所以还可以使用线性回归那套训练方法。

这里我先不展开过拟合问题。多项式次数太高会带来什么风险,放到下一篇和逻辑回归、正则化一起整理。

我现在容易混淆的点

第一,梯度下降不是一个新的模型,而是一种优化方法。它负责帮模型找到更合适的参数。

第二,批梯度下降每次更新会看完整个训练集。它方向比较稳定,但数据很大时会慢。

第三,多维特征不是把线性回归推翻,而是把输入从一个数字扩展成多个数字。

第四,向量化不是为了写得高级,而是为了让代码更接近公式,也更适合大规模计算。

第五,特征缩放不会改变标签含义,它主要是让输入特征的范围更适合训练。

第六,多项式回归虽然能拟合更复杂的趋势,但复杂度变高后也会带来新的问题。

这一篇的总结

这一篇从梯度下降开始,把线性回归往实际训练推进了一步。

我现在理解到:

  • 梯度下降是让模型参数自动变好的核心方法。
  • 学习率决定每一步走多大。
  • 批梯度下降每次用全部训练样本计算梯度。
  • 多维特征让线性回归可以处理更真实的问题。
  • 向量化让公式和代码更统一。
  • 特征缩放能让梯度下降更稳定、更快。
  • 特征工程和多项式回归让线性回归可以拟合更复杂的趋势。

下一篇准备进入分类问题,也就是逻辑回归。同时也要继续整理过拟合和正则化,因为模型变复杂以后,训练集效果好不一定代表模型真的好。