3深度学习

机器学习基础

从数据中学习

容量过拟合正则化超参数

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第3章:机器学习基础(Machine Learning Basics)

一、导读

1.1 本章学习目标

第3章从线性代数和概率论的数学基础过渡到机器学习的核心概念。本章搭建起从传统机器学习到深度学习的桥梁,讲解那些在深度学习中依然适用的基本原则。无论模型多么复杂,容量控制、过拟合、欠拟合、正则化、优化等核心问题始终存在。

本章的核心学习目标包括:

  • 理解机器学习的基本任务:监督学习、无监督学习、强化学习的区别与应用场景
  • 掌握容量与泛化的核心矛盾:模型复杂度与泛化能力之间的权衡
  • 理解过拟合与欠拟合:诊断方法和解决策略
  • 掌握正则化技术:L1/L2正则化、Dropout、数据增强的原理
  • 理解优化算法的基础:梯度下降及其变体
  • 掌握模型评估方法:交叉验证、偏差-方差分解

1.2 为什么本章如此重要

深度学习是机器学习的一个子领域,机器学习的核心原理在深度学习中完全适用。不理解这些基础概念,就无法正确训练和评估深度学习模型。

关键问题包括:

  • 为什么模型在训练集上表现好但在测试集上表现差?(过拟合)
  • 为什么增加模型参数不一定能提高性能?(容量与泛化的权衡)
  • 为什么需要正则化?各种正则化方法的本质是什么?
  • 为什么深度学习需要大量的数据?数据量与模型复杂度的关系是什么?

这些问题的答案构成了正确实践深度学习的基础。

1.3 核心问题引导

什么是模型的容量?如何度量?容量过大或过小分别导致什么问题?

为什么训练误差不能反映泛化能力?如何估计泛化误差?

正则化的本质是什么?为什么L1产生稀疏解而L2不产生?

偏差-方差分解如何帮助我们理解模型的错误来源?

为什么深度学习在小数据集上容易过拟合?如何应对?

交叉验证的原理是什么?为什么需要交叉验证?


二、核心概念详解

2.1 机器学习的基本任务

2.1.1 监督学习

监督学习从标注数据中学习输入到输出的映射:

python
# 监督学习的基本范式
# 训练数据: {(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}")  # 接近1

2.1.2 无监督学习

无监督学习从无标注数据中发现结构:

python
# 聚类示例
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 强化学习

强化学习从与环境的交互中学习策略:

python
# 简单的多臂老虎机(强化学习的最简形式)
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 模型容量

模型容量指模型能够表达的函数复杂度。容量过低无法拟合数据(欠拟合),容量过高会记住噪声(过拟合)。

python
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 泛化误差

python
# 训练误差与泛化误差的关系
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 诊断

python
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 正则化(权重衰减)

python
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_penalty

2.4.2 L1 正则化

python
# 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_penalty

2.4.3 Dropout

python
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 梯度下降

python
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 学习率的影响

python
# 不同学习率的效果
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 实验验证

python
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 案例一:学习曲线分析

python
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 案例二:正则化效果对比

python
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正则化,比较它们在图像分类上的效果。

早停实现:实现基于验证集损失的早停策略。

偏差-方差实验:用蒙特卡洛方法估计不同模型的偏差和方差。

学习率调度:实现学习率衰减策略,观察对收敛的影响。

3/10机器学习
0%