人工智能中的最优化问题
机器学习(如训练神经网络)的全部过程,就是寻找一组参数,使得模型在特定任务上的错误率
最小。这,就是最优化问题。
最优化的基本数学模型如下公式所示:
损失函数(Loss Function):衡量“好坏”的标准
目标函数的选择完全取决于你希望模型完成什么任务。不同的任务需要不同的“量尺”来衡量错误
适用于回归问题 (Regression)
回归问题是预测一个连续数值(如房价、股票价格、气温)
均方误差 (Mean Squared Error, MSE)
编辑
import torch import torch.nn as nn # --- 均方误差损失 (MSELoss) --- print("--- 均方误差损失 (MSELoss) ---") # 模拟回归任务的预测值和真实值 # 预测值 (模型的输出) predictions_mse = torch.tensor([10.0, 12.0, 15.0], requires_grad=True) # 真实值 (数据的标签) targets_mse = torch.tensor([11.0, 13.0, 14.0]) # 实例化 MSELoss mse_loss_fn = nn.MSELoss() # 计算损失 loss_mse = mse_loss_fn(predictions_mse, targets_mse) print(f"预测值: {predictions_mse}") print(f"真实值: {targets_mse}") print(f"MSE 损失: {loss_mse.item():.4f}") # .item() 获取张量中的标量值 # 演示反向传播 (梯度计算) loss_mse.backward() print(f"MSE 损失对预测值的梯度: {predictions_mse.grad}") print("-" * 30)
适用于分类问题 (Classification)
分类问题是预测一个离散的类别(如猫/狗、垃圾邮件/非垃圾邮件)
交叉熵损失 (Cross-Entropy Loss)
这是几乎所有现代分类任务(如图像识别、自然语言处理)的首选损失函数
编辑
import torch import torch.nn as nn # --- 二分类交叉熵损失 (BCELoss 和 BCEWithLogitsLoss) --- print("--- 二分类交叉熵损失 (BCELoss & BCEWithLogitsLoss) ---") # 模拟二分类任务的预测值和真实值 # 真实标签 (0 或 1) targets_bce = torch.tensor([1.0, 0.0, 1.0]) # ----------------- 使用 nn.BCELoss ----------------- # 预测概率 (0到1之间)。注意:如果模型输出的是 logits (Logit是Sigmoid之前的原始输出值), # 则需要先手动通过 sigmoid 激活函数。 predictions_bce_prob = torch.tensor([0.9, 0.2, 0.8], requires_grad=True) bce_loss_fn = nn.BCELoss() loss_bce_prob = bce_loss_fn(predictions_bce_prob, targets_bce) print("\n--- 使用 nn.BCELoss (需要手动 Sigmoid) ---") print(f"预测概率: {predictions_bce_prob}") print(f"真实标签: {targets_bce}") print(f"BCELoss 损失: {loss_bce_prob.item():.4f}") loss_bce_prob.backward() print(f"BCELoss 对预测概率的梯度: {predictions_bce_prob.grad}") # ----------------- 使用 nn.BCEWithLogitsLoss (推荐) ----------------- # 模型的原始输出 (logits),无需手动 sigmoid predictions_bce_logits = torch.tensor([2.2, -1.4, 1.8], requires_grad=True) # 对应 sigmoid(2.2)=0.9, sigmoid(-1.4)=0.2, sigmoid(1.8)=0.8 bce_logits_loss_fn = nn.BCEWithLogitsLoss() loss_bce_logits = bce_logits_loss_fn(predictions_bce_logits, targets_bce) print("\n--- 使用 nn.BCEWithLogitsLoss (内部包含 Sigmoid, 更稳定) ---") print(f"预测 Logits: {predictions_bce_logits}") print(f"真实标签: {targets_bce}") print(f"BCEWithLogitsLoss 损失: {loss_bce_logits.item():.4f}") loss_bce_logits.backward() print(f"BCEWithLogitsLoss 对 Logits 的梯度: {predictions_bce_logits.grad}") print("-" * 30)
多分类交叉熵损失
编辑
import torch import torch.nn as nn # --- 多分类交叉熵损失 (CrossEntropyLoss) --- print("--- 多分类交叉熵损失 (CrossEntropyLoss) ---") # 模拟三分类任务的预测值和真实值 (例如:猫, 狗, 鸟) num_classes = 3 # 模型的原始输出 (logits)。假设 batch_size=2 # 第一个样本: 倾向于第0类 (猫) # 第二个样本: 倾向于第1类 (狗) predictions_ce_logits = torch.tensor([ [2.0, 0.5, -1.0], # 第一个样本的 logits (对应 Cat, Dog, Bird) [0.1, 1.5, 0.3] # 第二个样本的 logits ], requires_grad=True) # 真实类别索引 (不需要 One-Hot 编码) # 第一个样本真实是第0类 (猫) # 第二个样本真实是第1类 (狗) targets_ce = torch.tensor([0, 1]) # 实例化 CrossEntropyLoss cross_entropy_loss_fn = nn.CrossEntropyLoss() # 计算损失 loss_ce = cross_entropy_loss_fn(predictions_ce_logits, targets_ce) print(f"预测 Logits:\n{predictions_ce_logits}") print(f"真实类别索引: {targets_ce}") print(f"CrossEntropyLoss 损失: {loss_ce.item():.4f}") # 演示反向传播 loss_ce.backward() print(f"CrossEntropyLoss 对预测 Logits 的梯度:\n{predictions_ce_logits.grad}") print("-" * 30)
优化算法的核心——梯度下降
目的:求损失函数的最小值和对应的参数
原理:梯度方向是函数值变化最快的方向
编辑
import numpy as np import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D from matplotlib.animation import FuncAnimation # --- 1. 定义目标函数和梯度 --- # 假设目标函数是简单的二次函数 (简化版,类似上图中的盆地) def loss_function(w, b): # J(w, b) = (w - w_optimal)^2 + (b - b_optimal)^2 + offset return (w - 0.05)**2 + (b + 0.5)**2 + 0.2 # 定义梯度(偏导数) def gradient(w, b): dw = 2 * (w - 0.05) # 损失函数对 w 的偏导 db = 2 * (b + 0.5) # 损失函数对 b 的偏导 return dw, db # --- 2. 运行梯度下降并记录历史 --- def run_gd(start_w, start_b, alpha, n_iterations): w, b = start_w, start_b history = [(w, b, loss_function(w, b))] for _ in range(n_iterations): dw, db = gradient(w, b) # 参数更新核心公式 w = w - alpha * dw b = b - alpha * db current_loss = loss_function(w, b) history.append((w, b, current_loss)) return np.array(history) # 设置参数 alpha = 0.05 # 学习率 n_iterations = 50 # 迭代次数 (动画帧数) start_w, start_b = 0.5, 0.5 # 随机起点 path_history = run_gd(start_w, start_b, alpha, n_iterations) # --- 3. 3D 可视化设置 --- fig = plt.figure(figsize=(10, 8)) ax = fig.add_subplot(111, projection='3d') # 生成曲面数据 w_range = np.linspace(path_history[:, 0].min() - 0.1, path_history[:, 0].max() + 0.1, 100) b_range = np.linspace(path_history[:, 1].min() - 0.1, path_history[:, 1].max() + 0.1, 100) W, B = np.meshgrid(w_range, b_range) Z = loss_function(W, B) # 绘制曲面 ax.plot_surface(W, B, Z, cmap=plt.cm.coolwarm, alpha=0.8, antialiased=True) # 设置轴标签 ax.set_xlabel('w', fontsize=16) ax.set_ylabel('b', fontsize=16) ax.set_zlabel('costs', fontsize=16) ax.set_title('梯度下降 3D 动态可视化', fontsize=18) # --- 4. 动画函数 --- # 初始化路径线和当前点 line, = ax.plot([], [], [], 'k--', alpha=0.6, label='下降路径') # 虚线路径 point, = ax.plot([], [], [], 'o', color='navy', markersize=8, label='当前参数') # 移动的点 def animate(i): # 更新虚线路径 (从起点到第 i 步) line.set_data(path_history[:i, 0], path_history[:i, 1]) line.set_3d_properties(path_history[:i, 2]) # 更新当前点的位置 point.set_data(path_history[i:i+1, 0], path_history[i:i+1, 1]) point.set_3d_properties(path_history[i:i+1, 2]) # 更新迭代次数标签 (对应您图中的 'epochs: 140000') ax.legend([line, point], [f'迭代次数: {i}', '当前参数'], loc='upper left') return line, point, # 5. 创建动画并显示/保存 ani = FuncAnimation(fig, animate, frames=len(path_history), interval=100, blit=False) # 在 Jupyter/Colab 中,可以使用 HTML(ani.to_jshtml()) 显示 # 在本地,可以使用 ani.save('gradient_descent.gif', writer='pillow') 保存为GIF plt.show()
分类
更新参数 局部最优解
批量梯度下降(BGD) 基于所有样本 可能停留
随机梯度下降(SGD) 基于一个样本 可跳出
小批量梯度下降(MGD) 基于一个小批量样本 可跳出
import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader, TensorDataset import matplotlib.pyplot as plt # --- 1. 模拟数据 (与之前相同) --- true_weight = 2.0 true_bias = 3.0 X_train = torch.randn(100, 1) * 10 # 100个样本 y_train = true_weight * X_train + true_bias + torch.randn(100, 1) * 2 # 将数据打包成 TensorDataset train_dataset = TensorDataset(X_train, y_train) # --- 2. 定义模型 --- class LinearRegression(nn.Module): def __init__(self): super(LinearRegression, self).__init__() self.linear = nn.Linear(1, 1) def forward(self, x): return self.linear(x) # --- 3. 定义损失函数 --- criterion = nn.MSELoss() # --- 4. 训练函数 (封装训练过程,方便复用) --- def train_model(model_instance, optimizer_instance, data_loader_instance, num_epochs, description): loss_history = [] print(f"\n--- 开始训练: {description} ---") for epoch in range(num_epochs): current_epoch_loss = 0.0 num_batches = 0 for batch_X, batch_y in data_loader_instance: optimizer_instance.zero_grad() outputs = model_instance(batch_X) loss = criterion(outputs, batch_y) loss.backward() optimizer_instance.step() current_epoch_loss += loss.item() num_batches += 1 # 记录每个 epoch 的平均损失 avg_epoch_loss = current_epoch_loss / num_batches loss_history.append(avg_epoch_loss) if (epoch + 1) % (num_epochs // 5) == 0: # 每 1/5 打印一次 print(f'Epoch [{epoch+1}/{num_epochs}], Avg Loss: {avg_epoch_loss:.4f}') print(f"--- 训练完成: {description} ---") print(f"最终模型参数: W={model_instance.linear.weight.item():.4f}, B={model_instance.linear.bias.item():.4f}") return loss_history # --- 5. 实例化模型和优化器 (每次实验都重新实例化,避免参数污染) --- # --- 实验 1: 批量梯度下降 (Batch Gradient Descent, BGD) --- # batch_size = 总样本数,确保每个 epoch 只进行一次参数更新 model_bgd = LinearRegression() optimizer_bgd = optim.SGD(model_bgd.parameters(), lr=0.001) # BGD通常需要较小的学习率,否则容易震荡 dataloader_bgd = DataLoader(dataset=train_dataset, batch_size=len(X_train), shuffle=False) # BGD一般不shuffle loss_history_bgd = train_model(model_bgd, optimizer_bgd, dataloader_bgd, num_epochs=200, description="批量梯度下降 (BGD)") # --- 实验 2: 随机梯度下降 (Stochastic Gradient Descent, SGD) --- # batch_size = 1,每次只用一个样本 model_sgd = LinearRegression() optimizer_sgd = optim.SGD(model_sgd.parameters(), lr=0.01) # SGD通常需要较大的学习率,因为梯度噪声大 dataloader_sgd = DataLoader(dataset=train_dataset, batch_size=1, shuffle=True) # SGD通常shuffle loss_history_sgd = train_model(model_sgd, optimizer_sgd, dataloader_sgd, num_epochs=200, description="随机梯度下降 (SGD)") # --- 实验 3: 小批量梯度下降 (Mini-batch Gradient Descent, MGD) --- # batch_size = 一个合理的小批量大小 (例如 16, 32, 64) model_mgd = LinearRegression() optimizer_mgd = optim.SGD(model_mgd.parameters(), lr=0.01) # MGD学习率也比较灵活 dataloader_mgd = DataLoader(dataset=train_dataset, batch_size=16, shuffle=True) # MGD通常shuffle loss_history_mgd = train_model(model_mgd, optimizer_mgd, dataloader_mgd, num_epochs=200, description="小批量梯度下降 (MGD)")
优化技巧
小批量梯度下降 (Mini-batch GD):每个批的反向传播后就直接对参数进行更新。
带动量的随机梯度下降 (Momentum):通过计算前几步的滑动平均来计算动量,用于当前步的更新。
Nesterov 加速的梯度下降 (Nesterov Accelerated GD):不算当前位置的梯度,而是计算这个位置大概的梯度。
自适应梯度Adagrad:对每个参数的学习率惊醒不同动态的改变,首先得到的是每个参数的梯度,在更新时候,学习率除于一个正则项。
自适应动量Adam:不仅对梯度进行自适应的调整,也对动量进行自适应的调整。
import torch import torch.nn as nn import torch.optim as optim # 假设模型和损失函数已经定义 # model = MyModel() # criterion = nn.CrossEntropyLoss() # 假设 model 已经实例化 # 示例:定义模型参数 model_params = model.parameters() learning_rate = 0.001 # 1. 动量法 (Momentum) 的实现 # PyTorch 的 optim.SGD 可以通过设置 momentum 参数实现动量法 # 通常 momentum=0.9 optimizer_momentum = optim.SGD(model_params, lr=learning_rate, momentum=0.9) # 2. Nesterov 加速的梯度下降 (NAG) 的实现 # 在 optim.SGD 中设置 nesterov=True # 注意:只有当 momentum > 0 时,nesterov 才会生效 optimizer_nesterov = optim.SGD(model_params, lr=learning_rate, momentum=0.9, nesterov=True) # 3. 自适应梯度 (Adagrad) 的实现 # 学习率 lr 仍然需要传入 optimizer_adagrad = optim.Adagrad(model_params, lr=0.01) # 4. 自适应动量 (Adam) 的实现 # Adam 不需要动量因子,它使用 beta1 和 beta2 参数,默认值通常很好 # lr=0.001 是 Adam 的常见起始学习率 optimizer_adam = optim.Adam(model_params, lr=0.001)