吴恩达机器学习课程(PyTorch适配)学习笔记:2.2 前向传播与推理
前向传播(Forward Propagation)是神经网络进行预测的核心机制,而推理(Inference)则是利用训练好的模型进行实际预测的过程。理解这两个概念及其在PyTorch中的实现方式,对于掌握深度学习至关重要。
2.2.1 单层前向传播(流程 + 公式)
单层神经网络是深度学习中最基础的结构,理解其前向传播过程是掌握更复杂网络的基础。
单层神经网络结构
一个典型的单层神经网络包含:
- 输入层:接收输入特征
- 一个全连接层:包含权重和偏置参数
- 激活函数:引入非线性变换
- 输出层:产生预测结果
图:单层神经网络结构示意图
前向传播流程
单层神经网络的前向传播过程可以分为以下步骤:
- 接收输入:获取输入特征向量 xxx
- 加权求和:计算输入特征与权重的线性组合,并加上偏置
- 激活函数:将线性组合的结果通过激活函数进行非线性变换
- 输出结果:产生最终的预测值
数学公式表达
对于单个样本:
-
加权求和(线性变换):
z=w1x1+w2x2+...+wnxn+b=wTx+bz = w_1x_1 + w_2x_2 + ... + w_nx_n + b = \mathbf{w}^T\mathbf{x} + bz=w1x1+w2x2+...+wnxn+b=wTx+b
其中:- x=[x1,x2,...,xn]T\mathbf{x} = [x_1, x_2, ..., x_n]^Tx=[x1,x2,...,xn]T 是输入特征向量
- w=[w1,w2,...,wn]T\mathbf{w} = [w_1, w_2, ..., w_n]^Tw=[w1,w2,...,wn]T 是权重向量
- bbb 是偏置项
- zzz 是线性组合结果
-
激活函数(非线性变换):
y^=f(z)\hat{y} = f(z)y^=f(z)
其中:- f(⋅)f(\cdot)f(⋅) 是激活函数
- y^\hat{y}y^ 是网络的输出(预测值)
对于批量样本(mmm 个样本):
使用矩阵表示可以同时处理多个样本,提高计算效率:
Z=XWT+b\mathbf{Z} = \mathbf{X}\mathbf{W}^T + \mathbf{b}Z=XWT+b
Y^=f(Z)\hat{\mathbf{Y}} = f(\mathbf{Z})Y^=f(Z)
其中:
- X∈Rm×n\mathbf{X} \in \mathbb{R}^{m \times n}X∈Rm×n 是输入矩阵,每行代表一个样本
- W∈Rk×n\mathbf{W} \in \mathbb{R}^{k \times n}W∈Rk×n 是权重矩阵,kkk 是输出维度
- b∈Rk\mathbf{b} \in \mathbb{R}^{k}b∈Rk 是偏置向量
- Z∈Rm×k\mathbf{Z} \in \mathbb{R}^{m \times k}Z∈Rm×k 是线性组合结果矩阵
- Y^∈Rm×n\hat{\mathbf{Y}} \in \mathbb{R}^{m \times n}Y^∈Rm×n 是输出矩阵
PyTorch实现单层前向传播
import torch
import torch.nn as nn
import torch.nn.functional as F# 1. 手动实现单层前向传播
def manual_forward_pass(x, weights, bias, activation='sigmoid'):"""手动实现单层神经网络的前向传播参数:x: 输入张量,形状为(batch_size, input_features)weights: 权重张量,形状为(output_features, input_features)bias: 偏置张量,形状为(output_features,)activation: 激活函数类型"""# 步骤1: 加权求和 z = x * w^T + bz = torch.matmul(x, weights.t()) + bias# 步骤2: 应用激活函数if activation == 'sigmoid':output = torch.sigmoid(z)elif activation == 'relu':output = F.relu(z)elif activation == 'tanh':output = torch.tanh(z)elif activation == 'linear':output = zelse:raise ValueError(f"不支持的激活函数: {activation}")return output, z# 2. 使用PyTorch的nn.Linear实现
class SingleLayerNN(nn.Module):def __init__(self, input_size, output_size, activation='sigmoid'):super(SingleLayerNN, self).__init__()self.linear = nn.Linear(input_size, output_size)self.activation_type = activationdef forward(self, x):# 线性变换z = self.linear(x)# 激活函数if self.activation_type == 'sigmoid':output = torch.sigmoid(z)elif self.activation_type == 'relu':output = F.relu(z)elif self.activation_type == 'tanh':output = torch.tanh(z)elif self.activation_type == 'linear':output = zelse:raise ValueError(f"不支持的激活函数: {self.activation_type}")return output, z# 测试单层前向传播
if __name__ == "__main__":# 随机输入数据 (10个样本,每个样本5个特征)x = torch.randn(10, 5)# 1. 测试手动实现weights = torch.randn(3, 5) # 3个输出特征,5个输入特征bias = torch.randn(3)output_manual, z_manual = manual_forward_pass(x, weights, bias, activation='relu')print("手动实现输出形状:", output_manual.shape) # 应该是(10, 3)# 2. 测试PyTorch实现model = SingleLayerNN(input_size=5, output_size=3, activation='relu')output_pytorch, z_pytorch = model(x)print("PyTorch实现输出形状:", output_pytorch.shape) # 应该是(10, 3)
注意事项与易错点
-
维度匹配
- 确保权重矩阵的维度与输入特征维度匹配
- 权重矩阵形状应为 (output_size, input_size)
- 输入数据形状应为 (batch_size, input_size)
-
广播机制
- PyTorch会自动处理偏置的广播(从(output_size,) 广播到 (batch_size, output_size))
- 理解广播规则有助于避免维度错误
-
激活函数选择
- 根据任务类型选择合适的激活函数
- 回归问题通常使用线性激活(无激活函数)
- 二分类问题输出层常用sigmoid
- 多分类问题输出层常用softmax
-
数值范围
- 注意输入数据的数值范围,极端值可能导致激活函数饱和
- 例如,sigmoid在输入值很大或很小时会饱和,导致梯度消失
2.2.2 前向传播通用实现(框架思路)
对于深层神经网络,我们需要一种通用的前向传播实现框架,能够灵活处理任意层数和结构的网络。
深层神经网络的前向传播流程
深层神经网络的前向传播是单层网络的自然扩展,数据从输入层开始,依次通过每个隐藏层,最终到达输出层:
- 输入层接收原始数据 a[0]=xa^{[0]} = xa[0]=x
- 对于每一层 lll(从1到L):
- 计算加权和:z[l]=W[l]a[l−1]+b[l]z^{[l]} = W^{[l]}a^{[l-1]} + b^{[l]}z[l]=W[l]a[l−1]+b[l]
- 应用激活函数:a[l]=f[l](z[l])a^{[l]} = f^{[l]}(z^{[l]})a[l]=f[l](z[l])
- 输出层产生最终预测:y^=a[L]\hat{y} = a^{[L]}y^=a[L]
其中,上标 [l][l][l] 表示第 lll 层的参数或输出。
图:深层神经网络的前向传播过程
通用实现框架思路
实现深层神经网络的前向传播可以采用以下思路:
- 模块化设计:将网络层、激活函数等封装为独立模块
- 层的有序组织:使用列表或字典有序地存储网络各层
- 循环迭代计算:通过循环依次计算每一层的输出
- 灵活配置:允许为不同层配置不同的激活函数和参数
PyTorch实现通用前向传播框架
PyTorch的nn.Module
和nn.Sequential
提供了构建深层网络的便捷方式:
import torch
import torch.nn as nn
import torch.nn.functional as F# 1. 使用nn.Sequential实现简单的前向传播
def create_sequential_model(input_size, hidden_sizes, output_size, activations=None):"""创建一个序列模型,实现通用前向传播参数:input_size: 输入特征维度hidden_sizes: 列表,每个元素表示隐藏层的大小output_size: 输出维度activations: 列表,每个元素表示对应层的激活函数"""# 默认激活函数为ReLUif activations is None:activations = ['relu'] * len(hidden_sizes)# 输出层默认使用线性激活if output_size > 1:activations.append('softmax')else:activations.append('sigmoid')layers = []sizes = [input_size] + hidden_sizes + [output_size]for i in range(len(sizes) - 1):# 添加线性层layers.append(nn.Linear(sizes[i], sizes[i+1]))# 添加激活函数if activations[i] == 'relu':layers.append(nn.ReLU())elif activations[i] == 'sigmoid':layers.append(nn.Sigmoid())elif activations[i] == 'tanh':layers.append(nn.Tanh())elif activations[i] == 'softmax':layers.append(nn.Softmax(dim=1))elif activations[i] == 'linear':pass # 不添加激活函数else:raise ValueError(f"不支持的激活函数: {activations[i]}")return nn.Sequential(*layers)# 2. 自定义Module实现更灵活的前向传播
class FlexibleNN(nn.Module):def __init__(self, input_size, hidden_sizes, output_size, activations=None):super(FlexibleNN, self).__init__()self.sizes = [input_size] + hidden_sizes + [output_size]# 默认激活函数if activations is None:activations = ['relu'] * len(hidden_sizes)activations.append('sigmoid' if output_size == 1 else 'softmax')self.activations = activations# 创建线性层self.layers = nn.ModuleList()for i in range(len(self.sizes) - 1):self.layers.append(nn.Linear(self.sizes[i], self.sizes[i+1]))def forward(self, x, return_intermediates=False):"""前向传播参数:x: 输入张量return_intermediates: 是否返回中间层输出"""intermediates = []current = xfor i, layer in enumerate(self.layers):# 线性变换z = layer(current)# 应用激活函数if self.activations[i] == 'relu':current = F.relu(z)elif self.activations[i] == 'sigmoid':current = torch.sigmoid(z)elif self.activations[i] == 'tanh':current = torch.tanh(z)elif self.activations[i] == 'softmax':current = F.softmax(z, dim=1)elif self.activations[i] == 'linear':current = z# 保存中间结果if return_intermediates:intermediates.append((z, current))if return_intermediates:return current, intermediatesreturn current# 测试通用前向传播框架
if __name__ == "__main__":# 网络配置input_size = 10hidden_sizes = [20, 15] # 两个隐藏层,分别有20和15个神经元output_size = 3 # 输出层3个神经元(多分类)# 1. 测试Sequential模型seq_model = create_sequential_model(input_size, hidden_sizes, output_size)x = torch.randn(5, input_size) # 5个样本output_seq = seq_model(x)print("Sequential模型输出形状:", output_seq.shape) # 应该是(5, 3)print("输出概率和:", output_seq.sum(dim=1)) # softmax输出和应为1# 2. 测试自定义灵活模型flex_model = FlexibleNN(input_size, hidden_sizes, output_size)output_flex, intermediates = flex_model(x, return_intermediates=True)print("灵活模型输出形状:", output_flex.shape) # 应该是(5, 3)# 打印中间层信息print("\n中间层信息:")for i, (z, a) in enumerate(intermediates):print(f"第{i+1}层 - z形状: {z.shape}, 激活值形状: {a.shape}")
通用框架的优势与扩展性
- 灵活性:可以轻松调整网络层数和每层大小
- 可维护性:模块化设计使代码更易于理解和修改
- 可扩展性:可以方便地添加新的层类型或激活函数
- 一致性:确保网络各层的前向传播遵循相同的接口规范
注意事项与最佳实践
-
网络深度选择
- 过深的网络可能导致梯度消失/爆炸和训练困难
- 过浅的网络可能无法捕捉复杂模式
- 建议从较简单的网络开始,逐步增加复杂度
-
激活函数组合
- 隐藏层通常使用ReLU或其变体
- 输出层根据任务选择合适的激活函数
- 避免在深层网络中使用sigmoid和tanh作为隐藏层激活函数
-
参数初始化
- 合理的参数初始化对深层网络至关重要
- PyTorch的默认初始化通常效果不错
- 对于特定激活函数(如ReLU),可以使用专门的初始化方法
-
中间结果跟踪
- 在调试时跟踪中间层输出有助于理解网络行为
- 注意中间层输出的分布,避免激活值过于集中或分散
2.2.3 推理过程(预测逻辑 + PyTorch 适配)
推理是使用训练好的模型对新数据进行预测的过程,它是将模型应用于实际问题的关键步骤。
推理过程概述
推理过程可以分为以下几个关键步骤:
- 准备输入数据:获取并预处理新的输入数据
- 加载模型与参数:加载训练好的模型结构和权重参数
- 执行前向传播:将输入数据通过模型进行前向计算
- 解析输出结果:将模型输出转换为有意义的预测结果
- 返回最终预测:以合适的形式返回推理结果
预测逻辑详解
根据不同的任务类型,预测逻辑有所不同:
-
分类任务:
- 输出层通常使用softmax(多分类)或sigmoid(二分类)激活函数
- 预测结果为概率分布
- 通常选择概率最高的类别作为最终预测:y^=argmax(y^prob)\hat{y} = \arg\max(\hat{y}_{prob})y^=argmax(y^prob)
-
回归任务:
- 输出层通常不使用激活函数(线性输出)
- 预测结果为连续值
- 直接使用输出值作为预测结果,或根据需要进行后处理
-
序列任务:
- 输出可能是序列或序列概率分布
- 可能需要使用束搜索(beam search)等方法生成最终序列
PyTorch适配的推理实现
PyTorch提供了便捷的API来实现推理过程:
import torch
import torch.nn as nn
import torchvision.transforms as transforms
from PIL import Image
import numpy as np# 1. 定义模型(与训练时相同)
class Classifier(nn.Module):def __init__(self, input_size, num_classes):super(Classifier, self).__init__()self.fc1 = nn.Linear(input_size, 128)self.fc2 = nn.Linear(128, 64)self.fc3 = nn.Linear(64, num_classes)def forward(self, x):x = torch.relu(self.fc1(x))x = torch.relu(self.fc2(x))x = torch.softmax(self.fc3(x), dim=1)return x# 2. 通用推理函数
def predict(model, input_data, device='cpu', return_probs=False):"""通用预测函数参数:model: 训练好的模型input_data: 输入数据张量device: 运行设备 ('cpu' 或 'cuda')return_probs: 是否返回概率分布"""# 将模型和数据移动到指定设备model = model.to(device)input_data = input_data.to(device)# 设置为评估模式model.eval()# 禁用梯度计算with torch.no_grad():# 前向传播outputs = model(input_data)# 对于分类任务,获取预测类别if outputs.shape[1] > 1: # 多分类_, predicted = torch.max(outputs, 1)else: # 二分类predicted = (outputs > 0.5).float().squeeze()# 转换为numpy数组(如果需要)predicted = predicted.cpu().numpy() if device == 'cuda' else predicted.numpy()if return_probs:probs = outputs.cpu().numpy() if device == 'cuda' else outputs.numpy()return predicted, probsreturn predicted# 3. 针对不同数据类型的推理实现# 3.1 表格数据推理
def tabular_inference(model, input_data, scaler=None):"""表格数据推理,支持特征缩放"""# 数据预处理if scaler is not None:input_data = scaler.transform(input_data)# 转换为张量input_tensor = torch.FloatTensor(input_data)# 确保输入有批次维度if input_tensor.ndim == 1:input_tensor = input_tensor.unsqueeze(0)# 预测return predict(model, input_tensor)# 3.2 图像数据推理
def image_inference(model, image_path, image_size=(32, 32)):"""图像数据推理"""# 图像预处理transform = transforms.Compose([transforms.Resize(image_size),transforms.ToTensor(),transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5])])# 加载并预处理图像image = Image.open(image_path).convert('RGB')image_tensor = transform(image).unsqueeze(0) # 添加批次维度# 预测return predict(model, image_tensor)# 测试推理过程
if __name__ == "__main__":# 创建模型并加载预训练权重input_size = 30 # 示例输入特征数num_classes = 5 # 示例类别数model = Classifier(input_size, num_classes)# 模拟加载预训练权重(实际应用中使用真实权重)# model.load_state_dict(torch.load('model_weights.pth'))# 1. 测试表格数据推理tabular_data = np.random.randn(10, input_size) # 10个样本predictions = tabular_inference(model, tabular_data)print("表格数据预测结果:", predictions)print("预测结果形状:", predictions.shape)# 2. 测试图像数据推理(需要实际图像文件)# 注意:这里使用占位路径,实际使用时替换为真实图像路径# image_path = "test_image.jpg"# try:# image_pred = image_inference(model, image_path)# print("图像预测结果:", image_pred)# except Exception as e:# print("图像推理测试:", str(e))# 3. 测试返回概率sample_data = torch.randn(1, input_size)pred, probs = predict(model, sample_data, return_probs=True)print("\n样本预测类别:", pred)print("类别概率分布:", probs)print("概率和:", probs.sum())
不同任务类型的推理适配
-
二分类任务:
def binary_classification_inference(model, input_data, threshold=0.5):"""二分类任务推理,支持自定义阈值"""input_tensor = torch.FloatTensor(input_data)if input_tensor.ndim == 1:input_tensor = input_tensor.unsqueeze(0)# 获取概率_, probs = predict(model, input_tensor, return_probs=True)probs = probs.squeeze()# 根据阈值判断类别predictions = (probs >= threshold).astype(int)return predictions, probs
-
多分类任务:
def multiclass_inference(model, input_data, top_k=1):"""多分类任务推理,支持返回Top-K预测结果"""input_tensor = torch.FloatTensor(input_data)if input_tensor.ndim == 1:input_tensor = input_tensor.unsqueeze(0)# 获取概率_, probs = predict(model, input_tensor, return_probs=True)# 获取Top-K预测top_probs, top_indices = torch.topk(torch.tensor(probs), k=top_k, dim=1)return top_indices.numpy(), top_probs.numpy()
-
回归任务:
def regression_inference(model, input_data, scale_factor=1.0):"""回归任务推理,支持结果缩放"""input_tensor = torch.FloatTensor(input_data)if input_tensor.ndim == 1:input_tensor = input_tensor.unsqueeze(0)# 设置模型为评估模式并预测model.eval()with torch.no_grad():predictions = model(input_tensor).numpy()# 结果缩放(如果需要)predictions = predictions * scale_factorreturn predictions.squeeze()
注意事项与优化建议
-
输入数据一致性
- 推理时的输入预处理必须与训练时完全一致
- 确保特征缩放、标准化等操作使用训练数据的统计量
-
设备兼容性
- 确保模型和输入数据在同一设备上(CPU/GPU)
- 对于GPU推理,注意批量大小不要超过显存限制
-
数值稳定性
- 注意输入数据范围,避免极端值导致的数值问题
- 对于概率计算,考虑使用log_softmax避免数值下溢
-
推理效率
- 对于大规模部署,考虑批量处理以提高效率
- 对于实时应用,可使用模型量化、剪枝等技术加速推理
2.2.4 推理代码框架(eval 模式 + 无梯度计算)
在PyTorch中进行推理时,正确设置模型状态和计算环境至关重要。本部分将介绍完整的推理代码框架,包括eval
模式的使用和无梯度计算的实现。
推理代码框架的核心组件
一个完整的推理代码框架应包含以下核心组件:
- 模型定义:与训练时一致的网络结构
- 模型加载:加载训练好的权重参数
- 设备配置:选择合适的计算设备(CPU/GPU)
- 数据预处理:对输入数据进行必要的转换
- 推理模式设置:将模型设置为
eval
模式 - 无梯度计算上下文:使用
torch.no_grad()
禁用梯度计算 - 前向传播:执行推理计算
- 结果后处理:解析和格式化模型输出
完整推理代码框架实现
import torch
import torch.nn as nn
import numpy as np
from typing import Tuple, Union, List# 1. 模型定义(必须与训练时一致)
class BaseModel(nn.Module):"""基础模型类,可根据具体任务继承扩展"""def __init__(self, input_dim: int, output_dim: int):super(BaseModel, self).__init__()self.input_dim = input_dimself.output_dim = output_dimdef forward(self, x: torch.Tensor) -> torch.Tensor:"""前向传播方法,需要在子类中实现"""raise NotImplementedError("子类必须实现forward方法")# 示例模型:用于分类任务
class ClassificationModel(BaseModel):def __init__(self, input_dim: int, num_classes: int, hidden_dims: List[int] = None):super(ClassificationModel, self).__init__(input_dim, num_classes)if hidden_dims is None:hidden_dims = [128, 64]# 构建网络层layers = []prev_dim = input_dimfor dim in hidden_dims:layers.extend([nn.Linear(prev_dim, dim),nn.ReLU(),nn.BatchNorm1d(dim)])prev_dim = dim# 输出层layers.append(nn.Linear(prev_dim, num_classes))self.model = nn.Sequential(*layers)def forward(self, x: torch.Tensor) -> torch.Tensor:logits = self.model(x)return torch.softmax(logits, dim=1)# 2. 推理框架类
class InferenceEngine:def __init__(self, model: nn.Module, weights_path: str, device: str = None):"""初始化推理引擎参数:model: 模型实例weights_path: 模型权重文件路径device: 计算设备,None则自动选择"""# 自动选择设备self.device = self._get_device(device)# 加载模型self.model = model.to(self.device)self._load_weights(weights_path)# 设置为评估模式self.model.eval()# 记录输入输出信息self.input_shape = Noneself.output_shape = Nonedef _get_device(self, device: Union[str, None]) -> str:"""自动选择计算设备"""if device is not None:return devicereturn 'cuda' if torch.cuda.is_available() else 'cpu'def _load_weights(self, weights_path: str) -> None:"""加载模型权重"""try:# 加载权重,忽略可能的设备不匹配问题state_dict = torch.load(weights_path, map_location=self.device)self.model.load_state_dict(state_dict)print(f"成功加载权重: {weights_path}")except Exception as e:raise RuntimeError(f"加载权重失败: {str(e)}")def preprocess(self, input_data: Union[np.ndarray, List[float]]) -> torch.Tensor:"""数据预处理参数:input_data: 原始输入数据返回:处理后的张量"""# 转换为numpy数组if isinstance(input_data, list):input_data = np.array(input_data)# 确保至少有2维(批次维度 + 特征维度)if input_data.ndim == 1:input_data = input_data.reshape(1, -1)# 记录输入形状self.input_shape = input_data.shape# 转换为张量并移动到指定设备input_tensor = torch.FloatTensor(input_data).to(self.device)return input_tensordef postprocess(self, output_tensor: torch.Tensor, return_probs: bool = False) -> Union[np.ndarray, Tuple[np.ndarray, np.ndarray]]:"""结果后处理参数:output_tensor: 模型输出张量return_probs: 是否返回概率分布返回:处理后的预测结果"""# 移动到CPU并转换为numpy数组output_np = output_tensor.cpu().numpy()self.output_shape = output_np.shape# 对于分类任务,获取预测类别if self.model.output_dim > 1: # 多分类predictions = np.argmax(output_np, axis=1)else: # 二分类predictions = (output_np > 0.5).astype(int).squeeze()if return_probs:return predictions, output_npreturn predictionsdef predict(self, input_data: Union[np.ndarray, List[float]], return_probs: bool = False) -> Union[np.ndarray, Tuple[np.ndarray, np.ndarray]]:"""执行推理参数:input_data: 输入数据return_probs: 是否返回概率分布返回:预测结果"""# 数据预处理input_tensor = self.preprocess(input_data)# 无梯度计算的前向传播with torch.no_grad():output_tensor = self.model(input_tensor)# 结果后处理return self.postprocess(output_tensor, return_probs)def batch_predict(self, input_data: Union[np.ndarray, List[float]], batch_size: int = 32, return_probs: bool = False) -> Union[np.ndarray, Tuple[np.ndarray, np.ndarray]]:"""批量推理,适用于大量数据参数:input_data: 输入数据batch_size: 批次大小return_probs: 是否返回概率分布返回:预测结果"""# 数据预处理input_tensor = self.preprocess(input_data)num_samples = input_tensor.shape[0]# 初始化结果存储all_predictions = []all_probs = []# 分批处理for i in range(0, num_samples, batch_size):batch = input_tensor[i:i+batch_size]with torch.no_grad():output_tensor = self.model(batch)# 后处理批次结果if return_probs:preds, probs = self.postprocess(output_tensor, return_probs=True)all_predictions.append(preds)all_probs.append(probs)else:preds = self.postprocess(output_tensor)all_predictions.append(preds)# 合并结果if return_probs:return np.concatenate(all_predictions), np.concatenate(all_probs)return np.concatenate(all_predictions)# 3. 使用示例
if __name__ == "__main__":# 配置INPUT_DIM = 30NUM_CLASSES = 5WEIGHTS_PATH = "model_weights.pth" # 替换为实际权重路径# 创建模型实例model = ClassificationModel(input_dim=INPUT_DIM, num_classes=NUM_CLASSES)# 创建推理引擎try:# 注意:这里使用了模拟权重,实际应用中请使用真实训练好的权重# engine = InferenceEngine(model, WEIGHTS_PATH)# 为了演示,我们创建一个不加载权重的引擎class DummyInferenceEngine(InferenceEngine):def _load_weights(self, weights_path):print("演示模式:未加载实际权重")engine = DummyInferenceEngine(model, WEIGHTS_PATH)print(f"使用设备: {engine.device}")# 生成测试数据test_data = np.random.randn(100, INPUT_DIM) # 100个测试样本# 单批推理predictions, probs = engine.predict(test_data[:5], return_probs=True)print("\n前5个样本的预测结果:", predictions)print("预测概率形状:", probs.shape)# 批量推理batch_preds = engine.batch_predict(test_data, batch_size=16)print("\n批量预测结果形状:", batch_preds.shape)except Exception as e:print(f"推理过程出错: {str(e)}")
eval模式的重要性
在PyTorch中,model.eval()
方法用于将模型设置为评估模式,这对推理至关重要,因为:
-
Batch Normalization行为改变:
- 训练时:使用批次的均值和方差
- 评估时:使用训练过程中累积的移动均值和方差
-
Dropout层行为改变:
- 训练时:随机丢弃部分神经元
- 评估时:不丢弃任何神经元,使用所有神经元
-
其他训练特定层:
- 如InstanceNorm、LayerNorm等归一化层的行为调整
- 某些自定义层可能有训练/评估模式的区别
# 演示eval模式的影响
def demonstrate_eval_mode():# 创建一个包含BatchNorm和Dropout的模型class TestModel(nn.Module):def __init__(self):super(TestModel, self).__init__()self.fc = nn.Linear(10, 10)self.bn = nn.BatchNorm1d(10)self.dropout = nn.Dropout(0.5)def forward(self, x):x = self.fc(x)x = self.bn(x)x = self.dropout(x)return xmodel = TestModel()x = torch.randn(5, 10) # 5个样本,10个特征# 训练模式model.train()output_train1 = model(x)output_train2 = model(x) # Dropout会导致不同结果print("训练模式两次输出是否相同:", torch.allclose(output_train1, output_train2)) # 应该是False# 评估模式model.eval()output_eval1 = model(x)output_eval2 = model(x) # 无Dropout,结果应相同print("评估模式两次输出是否相同:", torch.allclose(output_eval1, output_eval2)) # 应该是Truedemonstrate_eval_mode()
无梯度计算的优势
使用torch.no_grad()
上下文管理器禁用梯度计算有以下优势:
- 节省内存:不需要存储计算图和梯度信息,减少内存占用
- 提高速度:跳过梯度计算步骤,加快推理速度
- 避免意外修改:防止在推理过程中意外修改模型参数
# 演示无梯度计算的性能优势
def demonstrate_no_grad_performance():model = ClassificationModel(input_dim=100, num_classes=10, hidden_dims=[200, 100])x = torch.randn(1000, 100) # 1000个样本# 有梯度计算model.train()start_time = torch.utils.benchmark.default_timer()with torch.enable_grad(): # 显式启用梯度output = model(x)time_with_grad = torch.utils.benchmark.default_timer() - start_time# 无梯度计算model.eval()start_time = torch.utils.benchmark.default_timer()with torch.no_grad(): # 禁用梯度output = model(x)time_without_grad = torch.utils.benchmark.default_timer() - start_timeprint(f"有梯度计算时间: {time_with_grad:.6f}秒")print(f"无梯度计算时间: {time_without_grad:.6f}秒")print(f"速度提升: {time_with_grad / time_without_grad:.2f}倍")demonstrate_no_grad_performance()
推理框架的扩展与定制
根据具体需求,可以扩展推理框架:
- 添加模型验证:验证输入输出形状是否符合预期
- 实现缓存机制:缓存频繁使用的输入的推理结果
- 添加性能计时:记录预处理、推理和后处理各阶段的时间
- 支持模型 ensemble:集成多个模型的预测结果
- 添加结果解释:集成SHAP、LIME等模型解释工具
注意事项与最佳实践
-
模型状态保存
- 保存模型时建议只保存
state_dict
而非整个模型 - 确保保存和加载的模型结构完全一致
- 保存模型时建议只保存
-
设备管理
- 推理时可以使用与训练不同的设备
- 对于GPU推理,注意释放不再使用的张量以节省显存
-
异常处理
- 对输入数据进行有效性检查
- 处理可能的文件加载错误和设备错误
-
可重复性
- 对于需要可重复结果的场景,设置随机种子
- 注意某些操作在GPU上可能具有非确定性
-
日志记录
- 记录推理过程中的关键信息(输入形状、输出形状、耗时等)
- 对错误和异常情况进行详细日志记录
小结
前向传播是神经网络将输入映射到输出的核心过程,而推理则是利用训练好的模型进行实际预测的关键环节。本章详细介绍了单层和深层网络的前向传播流程与实现方式,解释了推理过程的完整逻辑,并提供了基于PyTorch的推理代码框架。
重点需要掌握:
- 前向传播的数学原理和实现方式
- PyTorch中
eval
模式的重要性及其对网络层的影响 torch.no_grad()
在推理中的应用及其性能优势- 完整推理框架的构建,包括数据预处理、模型加载、前向传播和结果后处理