第3章:机器学习基础(Machine Learning Basics)
一、导读
1.1 本章学习目标
第3章从线性代数和概率论的数学基础过渡到机器学习的核心概念。本章搭建起从传统机器学习到深度学习的桥梁,讲解那些在深度学习中依然适用的基本原则。无论模型多么复杂,容量控制、过拟合、欠拟合、正则化、优化等核心问题始终存在。
本章的核心学习目标包括:
- 理解机器学习的基本任务:监督学习、无监督学习、强化学习的区别与应用场景
- 掌握容量与泛化的核心矛盾:模型复杂度与泛化能力之间的权衡
- 理解过拟合与欠拟合:诊断方法和解决策略
- 掌握正则化技术:L1/L2正则化、Dropout、数据增强的原理
- 理解优化算法的基础:梯度下降及其变体
- 掌握模型评估方法:交叉验证、偏差-方差分解
1.2 为什么本章如此重要
深度学习是机器学习的一个子领域,机器学习的核心原理在深度学习中完全适用。不理解这些基础概念,就无法正确训练和评估深度学习模型。
关键问题包括:
- 为什么模型在训练集上表现好但在测试集上表现差?(过拟合)
- 为什么增加模型参数不一定能提高性能?(容量与泛化的权衡)
- 为什么需要正则化?各种正则化方法的本质是什么?
- 为什么深度学习需要大量的数据?数据量与模型复杂度的关系是什么?
这些问题的答案构成了正确实践深度学习的基础。
1.3 核心问题引导
什么是模型的容量?如何度量?容量过大或过小分别导致什么问题?
为什么训练误差不能反映泛化能力?如何估计泛化误差?
正则化的本质是什么?为什么L1产生稀疏解而L2不产生?
偏差-方差分解如何帮助我们理解模型的错误来源?
为什么深度学习在小数据集上容易过拟合?如何应对?
交叉验证的原理是什么?为什么需要交叉验证?
二、核心概念详解
2.1 机器学习的基本任务
2.1.1 监督学习
监督学习从标注数据中学习输入到输出的映射:
# 监督学习的基本范式
# 训练数据: {(x_1, y_1), (x_2, y_2), ..., (x_n, y_n)}
# 目标: 学习函数 f: X -> Y 使得 f(x) ≈ y
import numpy as np
# 回归任务示例
np.random.seed(42)
X_train = np.random.randn(100, 1)
y_train = 2 * X_train + 1 + 0.3 * np.random.randn(100, 1)
# 线性回归
from sklearn.linear_model import LinearRegression
model = LinearRegression()
model.fit(X_train, y_train)
print(f"权重: {model.coef_[0][0]:.3f}") # 接近2
print(f"偏置: {model.intercept_[0]:.3f}") # 接近12.1.2 无监督学习
无监督学习从无标注数据中发现结构:
# 聚类示例
from sklearn.cluster import KMeans
X = np.random.randn(300, 2)
X[:100] += [2, 2]
X[100:200] += [-2, -2]
kmeans = KMeans(n_clusters=3, random_state=42)
labels = kmeans.fit_predict(X)
# 可视化
import matplotlib.pyplot as plt
plt.scatter(X[:, 0], X[:, 1], c=labels, cmap='viridis', alpha=0.7)
plt.scatter(kmeans.cluster_centers_[:, 0], kmeans.cluster_centers_[:, 1],
s=200, c='red', marker='x')
plt.title('K-Means Clustering')
plt.savefig('kmeans_clustering.png', dpi=150)
plt.show()2.1.3 强化学习
强化学习从与环境的交互中学习策略:
# 简单的多臂老虎机(强化学习的最简形式)
class MultiArmedBandit:
def __init__(self, n_arms=5):
self.true_values = np.random.randn(n_arms)
self.n_arms = n_arms
def pull(self, arm):
return self.true_values[arm] + np.random.randn()
# ε-贪婪策略
bandit = MultiArmedBandit(5)
epsilon = 0.1
n_steps = 1000
action_counts = np.zeros(5)
action_values = np.zeros(5)
for step in range(n_steps):
if np.random.random() < epsilon:
action = np.random.randint(5)
else:
action = np.argmax(action_values)
reward = bandit.pull(action)
action_counts[action] += 1
action_values[action] += (reward - action_values[action]) / action_counts[action]
print(f"真实值: {bandit.true_values}")
print(f"估计值: {action_values}")
print(f"最优臂: {np.argmax(bandit.true_values)}, 选择次数: {action_counts[np.argmax(bandit.true_values)]}")2.2 容量与泛化
2.2.1 模型容量
模型容量指模型能够表达的函数复杂度。容量过低无法拟合数据(欠拟合),容量过高会记住噪声(过拟合)。
import numpy as np
import matplotlib.pyplot as plt
# 生成数据
np.random.seed(42)
X = np.linspace(0, 1, 30)
y = np.sin(2 * np.pi * X) + 0.3 * np.random.randn(30)
# 不同复杂度的多项式拟合
degrees = [1, 3, 9, 15]
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
for idx, degree in enumerate(degrees):
ax = axes[idx // 2, idx % 2]
# 多项式拟合
coeffs = np.polyfit(X, y, degree)
poly = np.poly1d(coeffs)
X_test = np.linspace(0, 1, 200)
y_pred = poly(X_test)
ax.scatter(X, y, s=20, alpha=0.7, label='训练数据')
ax.plot(X_test, y_pred, 'r-', label=f'degree={degree}')
ax.plot(X_test, np.sin(2 * np.pi * X_test), 'g--', label='真实函数')
ax.set_ylim(-2, 2)
ax.set_title(f'多项式拟合 (degree={degree})')
ax.legend()
plt.tight_layout()
plt.savefig('model_capacity.png', dpi=150)
plt.show()2.2.2 训练误差 vs 泛化误差
# 训练误差与泛化误差的关系
from sklearn.model_selection import learning_curve
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import PolynomialFeatures
from sklearn.pipeline import make_pipeline
np.random.seed(42)
X = np.random.randn(200, 1)
y = np.sin(X).ravel() + 0.3 * np.random.randn(200)
X_train, X_test = X[:100], X[100:]
y_train, y_test = y[:100], y[100:]
degrees = [1, 3, 10, 20]
train_errors = []
test_errors = []
for degree in degrees:
model = make_pipeline(PolynomialFeatures(degree), LinearRegression())
model.fit(X_train, y_train)
train_pred = model.predict(X_train)
test_pred = model.predict(X_test)
train_mse = np.mean((train_pred - y_train) ** 2)
test_mse = np.mean((test_pred - y_test) ** 2)
train_errors.append(train_mse)
test_errors.append(test_mse)
print(f"degree={degree:2d}: 训练MSE={train_mse:.4f}, 测试MSE={test_mse:.4f}")
plt.figure(figsize=(8, 5))
plt.plot(degrees, train_errors, 'bo-', label='训练误差')
plt.plot(degrees, test_errors, 'ro-', label='测试误差')
plt.xlabel('模型复杂度(多项式阶数)')
plt.ylabel('均方误差')
plt.title('训练误差 vs 测试误差')
plt.legend()
plt.savefig('train_vs_test_error.png', dpi=150)
plt.show()2.3 过拟合与欠拟合
2.3.1 诊断
def diagnose_model(train_error, test_error, threshold=0.1):
"""诊断模型状态"""
if train_error > 0.3 and test_error > 0.3:
return "欠拟合(高偏差):模型太简单"
elif train_error < 0.05 and test_error > train_error + threshold:
return "过拟合(高方差):模型太复杂"
elif abs(train_error - test_error) < threshold:
return "模型良好"
else:
return "需要进一步分析"
print(diagnose_model(0.4, 0.45)) # 欠拟合
print(diagnose_model(0.01, 0.3)) # 过拟合
print(diagnose_model(0.05, 0.08)) # 良好2.3.2 解决策略
欠拟合的解决策略:
- 增加模型复杂度(更多层、更多参数)
- 减少正则化
- 训练更长时间
- 使用更强大的优化器
过拟合的解决策略:
- 增加训练数据
- 正则化(L1、L2、Dropout)
- 数据增强
- 早停(Early Stopping)
- 降低模型复杂度
2.4 正则化
2.4.1 L2 正则化(权重衰减)
import torch
import torch.nn as nn
# PyTorch中的L2正则化
model = nn.Linear(10, 1)
optimizer = torch.optim.SGD(model.parameters(), lr=0.01, weight_decay=1e-4)
# weight_decay 就是 L2 正则化系数
# 手动实现L2正则化
def l2_regularized_loss(model, X, y, lambda_l2):
predictions = model(X)
mse = nn.functional.mse_loss(predictions, y)
l2_penalty = lambda_l2 * sum(p.pow(2).sum() for p in model.parameters())
return mse + l2_penalty2.4.2 L1 正则化
# L1正则化(手动实现)
def l1_regularized_loss(model, X, y, lambda_l1):
predictions = model(X)
mse = nn.functional.mse_loss(predictions, y)
l1_penalty = lambda_l1 * sum(p.abs().sum() for p in model.parameters())
return mse + l1_penalty2.4.3 Dropout
import torch.nn as nn
class DropoutModel(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(784, 256)
self.dropout1 = nn.Dropout(0.5) # 50%的神经元随机失活
self.fc2 = nn.Linear(256, 128)
self.dropout2 = nn.Dropout(0.3)
self.fc3 = nn.Linear(128, 10)
def forward(self, x):
x = torch.relu(self.fc1(x))
x = self.dropout1(x)
x = torch.relu(self.fc2(x))
x = self.dropout2(x)
x = self.fc3(x)
return x
model = DropoutModel()
# 训练时dropout自动生效,评估时自动关闭
model.train()
x = torch.randn(32, 784)
out1 = model(x)
out2 = model(x)
print(f"训练时两次输出不同: {not torch.allclose(out1, out2)}")
model.eval()
out3 = model(x)
out4 = model(x)
print(f"评估时两次输出相同: {torch.allclose(out3, out4)}")2.5 优化基础
2.5.1 梯度下降
import numpy as np
def gradient_descent(gradient, initial_x, learning_rate, n_iterations):
"""梯度下降优化"""
x = initial_x
history = [x]
for _ in range(n_iterations):
grad = gradient(x)
x = x - learning_rate * grad
history.append(x)
return np.array(history)
# 示例:最小化 f(x) = x^2
def gradient(x):
return 2 * x
history = gradient_descent(gradient, initial_x=5.0, learning_rate=0.1, n_iterations=20)
plt.figure(figsize=(10, 4))
plt.subplot(1, 2, 1)
x_vals = np.linspace(-6, 6, 100)
plt.plot(x_vals, x_vals**2, 'b-', label='f(x) = x²')
plt.plot(history, history**2, 'ro-', markersize=5, label='梯度下降')
plt.legend()
plt.title('梯度下降轨迹')
plt.subplot(1, 2, 2)
plt.plot(history, 'bo-')
plt.xlabel('迭代次数')
plt.ylabel('x 值')
plt.title('参数收敛过程')
plt.tight_layout()
plt.savefig('gradient_descent.png', dpi=150)
plt.show()2.5.2 学习率的影响
# 不同学习率的效果
learning_rates = [0.01, 0.1, 0.5, 0.99]
plt.figure(figsize=(12, 4))
for idx, lr in enumerate(learning_rates):
history = gradient_descent(gradient, 5.0, lr, 20)
plt.subplot(1, 3, 1)
plt.plot(history, label=f'lr={lr}')
plt.xlabel('迭代次数')
plt.ylabel('x')
plt.title('参数变化')
plt.legend()
plt.subplot(1, 3, 2)
plt.plot(history**2, label=f'lr={lr}')
plt.xlabel('迭代次数')
plt.ylabel('f(x)')
plt.title('损失变化')
plt.legend()
plt.subplot(1, 3, 3)
for lr in learning_rates:
history = gradient_descent(gradient, 5.0, lr, 50)
plt.plot(history**2, label=f'lr={lr}')
plt.xlabel('迭代次数')
plt.ylabel('f(x)')
plt.title('不同学习率的收敛对比')
plt.legend()
plt.tight_layout()
plt.savefig('learning_rate_effect.png', dpi=150)
plt.show()2.6 偏差-方差分解
2.6.1 理论
泛化误差可以分解为三个部分:
E[(y - f̂(x))²] = Bias[f̂(x)]² + Var[f̂(x)] + σ²- 偏差(Bias):模型预测的期望与真实值的差距,反映模型的欠拟合程度
- 方差(Variance):模型预测的波动程度,反映模型对训练数据的敏感度
- 不可约误差(σ²):数据本身的噪声
2.6.2 实验验证
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
# 生成大量数据集来估计偏差和方差
n_datasets = 100
n_samples = 50
all_train_mse = []
all_test_mse = []
for degree in [1, 5, 15]:
predictions_test = []
for i in range(n_datasets):
X = np.random.randn(n_samples, 1)
y = np.sin(X).ravel() + 0.3 * np.random.randn(n_samples)
model = make_pipeline(PolynomialFeatures(degree), LinearRegression())
model.fit(X, y)
X_test = np.linspace(-3, 3, 100).reshape(-1, 1)
y_true = np.sin(X_test).ravel()
y_pred = model.predict(X_test)
predictions_test.append(y_pred)
predictions_test = np.array(predictions_test)
# 偏差²:(E[f̂(x)] - f(x))²
mean_prediction = np.mean(predictions_test, axis=0)
bias_sq = np.mean((mean_prediction - np.sin(X_test).ravel()) ** 2)
# 方差:Var[f̂(x)]
variance = np.mean(np.var(predictions_test, axis=0))
print(f"degree={degree:2d}: Bias²={bias_sq:.4f}, Variance={variance:.4f}, Total={bias_sq+variance:.4f}")三、实战案例
3.1 案例一:学习曲线分析
from sklearn.model_selection import learning_curve
def plot_learning_curve(model, X, y, title="Learning Curve"):
train_sizes, train_scores, test_scores = learning_curve(
model, X, y, cv=5, scoring='neg_mean_squared_error',
train_sizes=np.linspace(0.1, 1.0, 10)
)
train_mean = -np.mean(train_scores, axis=1)
train_std = np.std(train_scores, axis=1)
test_mean = -np.mean(test_scores, axis=1)
test_std = np.std(test_scores, axis=1)
plt.figure(figsize=(8, 5))
plt.plot(train_sizes, train_mean, 'bo-', label='训练误差')
plt.fill_between(train_sizes, train_mean - train_std, train_mean + train_std, alpha=0.2)
plt.plot(train_sizes, test_mean, 'ro-', label='验证误差')
plt.fill_between(train_sizes, test_mean - test_std, test_mean + test_std, alpha=0.2)
plt.xlabel('训练样本数')
plt.ylabel('均方误差')
plt.title(title)
plt.legend()
plt.savefig('learning_curve.png', dpi=150)
plt.show()
# 生成数据
X = np.random.randn(500, 1)
y = np.sin(X).ravel() + 0.3 * np.random.randn(500)
# 简单模型(欠拟合)
simple_model = make_pipeline(PolynomialFeatures(1), LinearRegression())
plot_learning_curve(simple_model, X, y, "欠拟合模型的学习曲线")
# 复杂模型(过拟合)
complex_model = make_pipeline(PolynomialFeatures(15), LinearRegression())
plot_learning_curve(complex_model, X, y, "过拟合模型的学习曲线")3.2 案例二:正则化效果对比
from sklearn.linear_model import Ridge, Lasso
# 生成高维数据
np.random.seed(42)
n_samples, n_features = 100, 50
X = np.random.randn(n_samples, n_features)
true_weights = np.zeros(n_features)
true_weights[:5] = [1, -2, 3, -1, 2] # 只有5个特征有效
y = X @ true_weights + 0.5 * np.random.randn(n_samples)
# 不同正则化方法
models = {
'无正则化': LinearRegression(),
'L2 (Ridge)': Ridge(alpha=1.0),
'L1 (Lasso)': Lasso(alpha=0.1),
}
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
for idx, (name, model) in enumerate(models.items()):
model.fit(X, y)
weights = model.coef_
axes[idx].bar(range(n_features), weights, alpha=0.7)
axes[idx].axhline(y=0, color='k', linestyle='-', alpha=0.3)
axes[idx].set_title(name)
axes[idx].set_xlabel('特征索引')
axes[idx].set_ylabel('权重值')
# 统计非零权重
n_nonzero = np.sum(np.abs(weights) > 0.01)
print(f"{name}: 非零权重数 = {n_nonzero}/{n_features}")
plt.tight_layout()
plt.savefig('regularization_comparison.png', dpi=150)
plt.show()四、本章小结
本章建立了机器学习的核心概念框架:
任务分类:监督学习、无监督学习、强化学习各有不同的数据形式和目标。
容量与泛化:模型容量决定了拟合能力,但容量过大导致过拟合,过小导致欠拟合。泛化能力才是最终目标。
正则化:通过限制模型复杂度来提高泛化能力。L1产生稀疏解,L2防止权重过大,Dropout实现隐式集成。
偏差-方差分解:将泛化误差分解为偏差、方差和噪声三部分,指导我们诊断和解决模型问题。
优化基础:梯度下降是深度学习优化的基石,学习率的选择至关重要。
这些概念贯穿整个深度学习领域,是正确实践的理论基础。
五、思考与练习
思考题
为什么深度学习模型通常需要大量数据?数据量与模型容量的关系是什么?
Dropout为什么能防止过拟合?它与模型集成有什么关系?
偏差-方差分解在深度学习中还适用吗?深度学习中是否存在"双重下降"现象?
L1正则化为什么产生稀疏解?从优化景观的角度解释。
为什么深度学习中的测试误差有时会随着训练时间先降后升再降?
编程练习
学习曲线:在MNIST数据集上,用不同复杂度的模型绘制学习曲线。
正则化对比:实现L1、L2和Dropout正则化,比较它们在图像分类上的效果。
早停实现:实现基于验证集损失的早停策略。
偏差-方差实验:用蒙特卡洛方法估计不同模型的偏差和方差。
学习率调度:实现学习率衰减策略,观察对收敛的影响。