Chordia / src /models /pad_predictor.py
Corolin's picture
first commit
0a6452f
"""
PAD预测器模型
PAD Predictor Model for emotion and physiological state change prediction
该模型实现了一个多层感知机(MLP)来预测用户情绪和生理状态的变化。
输入:7维 (User PAD 3维 + Vitality 1维 + Current PAD 3维)
输出:5维 (ΔPAD 3维 + ΔPressure 1维 + Confidence 1维)
"""
import torch
import torch.nn as nn
import torch.nn.functional as F
from typing import Dict, Any, Optional, Tuple
import logging
class PADPredictor(nn.Module):
"""
PAD情绪和生理状态变化预测器
网络架构:
- 输入层:7维特征
- 隐藏层1:128神经元 + ReLU + Dropout(0.3)
- 隐藏层2:64神经元 + ReLU + Dropout(0.3)
- 隐藏层3:32神经元 + ReLU
- 输出层:5神经元 + Linear激活
"""
def __init__(self,
input_dim: int = 10,
output_dim: int = 4,
hidden_dims: list = [128, 64, 32],
dropout_rate: float = 0.3,
weight_init: str = "xavier_uniform",
bias_init: str = "zeros"):
"""
初始化PAD预测器
Args:
input_dim: 输入维度,默认10维(7原始特征+3差异特征)
output_dim: 输出维度,默认4维(ΔPAD 3维 + ΔPressure 1维)
hidden_dims: 隐藏层维度列表
dropout_rate: Dropout概率
weight_init: 权重初始化方法
bias_init: 偏置初始化方法
"""
super(PADPredictor, self).__init__()
self.input_dim = input_dim
self.output_dim = output_dim
self.hidden_dims = hidden_dims
self.dropout_rate = dropout_rate
self.weight_init = weight_init
self.bias_init = bias_init
# 构建网络层
self._build_network()
# 初始化权重
self._initialize_weights()
# 设置日志
self.logger = logging.getLogger(__name__)
def _build_network(self):
"""构建网络架构"""
layers = []
# 输入层到第一个隐藏层
layers.append(nn.Linear(self.input_dim, self.hidden_dims[0]))
layers.append(nn.ReLU())
layers.append(nn.Dropout(self.dropout_rate))
# 中间隐藏层
for i in range(len(self.hidden_dims) - 1):
layers.append(nn.Linear(self.hidden_dims[i], self.hidden_dims[i + 1]))
layers.append(nn.ReLU())
layers.append(nn.Dropout(self.dropout_rate))
# 最后一个隐藏层(不加Dropout)
layers.append(nn.Linear(self.hidden_dims[-1], self.output_dim))
# 输出层使用线性激活(回归任务)
# 构建序列网络
self.network = nn.Sequential(*layers)
def _initialize_weights(self):
"""初始化网络权重"""
for m in self.modules():
if isinstance(m, nn.Linear):
# 权重初始化
if self.weight_init == "xavier_uniform":
nn.init.xavier_uniform_(m.weight)
elif self.weight_init == "xavier_normal":
nn.init.xavier_normal_(m.weight)
elif self.weight_init == "kaiming_uniform":
nn.init.kaiming_uniform_(m.weight, nonlinearity='relu')
elif self.weight_init == "kaiming_normal":
nn.init.kaiming_normal_(m.weight, nonlinearity='relu')
else:
# 默认使用Xavier均匀初始化
nn.init.xavier_uniform_(m.weight)
# 偏置初始化
if self.bias_init == "zeros":
nn.init.zeros_(m.bias)
elif self.bias_init == "ones":
nn.init.ones_(m.bias)
else:
# 默认使用零初始化
nn.init.zeros_(m.bias)
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""
前向传播
Args:
x: 输入张量,形状为 (batch_size, input_dim)
Returns:
输出张量,形状为 (batch_size, output_dim)
"""
# 输入验证
if x.dim() != 2:
raise ValueError(f"输入张量应该是2维的 (batch_size, input_dim),但得到的是 {x.dim()}维")
if x.size(1) != self.input_dim:
raise ValueError(f"输入维度应该是 {self.input_dim},但得到的是 {x.size(1)}")
# 前向传播
output = self.network(x)
return output
def predict_components(self, x: torch.Tensor) -> Dict[str, torch.Tensor]:
"""
预测并分解输出组件
Args:
x: 输入张量
Returns:
包含各组件的字典:
- 'delta_pad': ΔPAD (3维)
- 'delta_pressure': ΔPressure (1维)
- 'confidence': Confidence (1维)
"""
output = self.forward(x)
# 分解输出
delta_pad = output[:, :3] # 前3维:ΔPAD
delta_pressure = output[:, 3:4] # 第4维:ΔPressure
confidence = output[:, 4:5] # 第5维:Confidence
return {
'delta_pad': delta_pad,
'delta_pressure': delta_pressure,
'confidence': confidence
}
def get_model_info(self) -> Dict[str, Any]:
"""
获取模型信息
Returns:
包含模型信息的字典
"""
total_params = sum(p.numel() for p in self.parameters())
trainable_params = sum(p.numel() for p in self.parameters() if p.requires_grad)
return {
'model_type': 'PADPredictor',
'input_dim': self.input_dim,
'output_dim': self.output_dim,
'hidden_dims': self.hidden_dims,
'dropout_rate': self.dropout_rate,
'weight_init': self.weight_init,
'bias_init': self.bias_init,
'total_parameters': total_params,
'trainable_parameters': trainable_params,
'architecture': str(self.network)
}
def save_model(self, filepath: str, include_optimizer: bool = False, optimizer: Optional[torch.optim.Optimizer] = None):
"""
保存模型
Args:
filepath: 保存路径
include_optimizer: 是否包含优化器状态
optimizer: 优化器对象
"""
save_dict = {
'model_state_dict': self.state_dict(),
'model_config': {
'input_dim': self.input_dim,
'output_dim': self.output_dim,
'hidden_dims': self.hidden_dims,
'dropout_rate': self.dropout_rate,
'weight_init': self.weight_init,
'bias_init': self.bias_init
},
'model_info': self.get_model_info()
}
if include_optimizer and optimizer is not None:
save_dict['optimizer_state_dict'] = optimizer.state_dict()
torch.save(save_dict, filepath)
self.logger.info(f"模型已保存到: {filepath}")
@classmethod
def load_model(cls, filepath: str, device: str = 'cpu'):
"""
加载模型
Args:
filepath: 模型文件路径
device: 设备类型
Returns:
PADPredictor实例
"""
# 针对 PyTorch 2.6+ 的安全性更新,显式设置 weights_only=False 以加载包含 numpy 标量的 checkpoint
try:
checkpoint = torch.load(filepath, map_location=device, weights_only=False)
except TypeError:
# 兼容旧版本 PyTorch
checkpoint = torch.load(filepath, map_location=device)
# 尝试获取配置,兼容不同的键名
model_config = checkpoint.get('model_config') or checkpoint.get('config')
if model_config is None:
raise KeyError("Checkpoint 中未找到 'model_config' 或 'config' 键")
model = cls(**model_config)
# 加载权重
model.load_state_dict(checkpoint['model_state_dict'])
logging.info(f"模型已从 {filepath} 加载")
return model
def freeze_layers(self, layer_names: list = None):
"""
冻结指定层
Args:
layer_names: 要冻结的层名称列表,如果为None则冻结所有层
"""
if layer_names is None:
# 冻结所有参数
for param in self.parameters():
param.requires_grad = False
self.logger.info("所有层已被冻结")
else:
# 冻结指定层
for name, param in self.named_parameters():
for layer_name in layer_names:
if layer_name in name:
param.requires_grad = False
break
self.logger.info(f"指定层 {layer_names} 已被冻结")
def unfreeze_layers(self, layer_names: list = None):
"""
解冻指定层
Args:
layer_names: 要解冻的层名称列表,如果为None则解冻所有层
"""
if layer_names is None:
# 解冻所有参数
for param in self.parameters():
param.requires_grad = True
self.logger.info("所有层已被解冻")
else:
# 解冻指定层
for name, param in self.named_parameters():
for layer_name in layer_names:
if layer_name in name:
param.requires_grad = True
break
self.logger.info(f"指定层 {layer_names} 已被解冻")
def create_pad_predictor(config: Optional[Dict[str, Any]] = None) -> PADPredictor:
"""
创建PAD预测器的工厂函数
Args:
config: 配置字典,包含模型参数
Returns:
PADPredictor实例
"""
if config is None:
# 使用默认配置
return PADPredictor()
# 从配置中提取参数
model_config = config.get('architecture', {})
return PADPredictor(
input_dim=config.get('dimensions', {}).get('input_dim', 10), # 默认10维(7原始+3差异)
output_dim=config.get('dimensions', {}).get('output_dim', 4), # 默认4维(移除confidence)
hidden_dims=[layer['size'] for layer in model_config.get('hidden_layers', [])],
dropout_rate=model_config.get('dropout_config', {}).get('rate', 0.3),
weight_init=config.get('initialization', {}).get('weight_init', 'xavier_uniform'),
bias_init=config.get('initialization', {}).get('bias_init', 'zeros')
)
if __name__ == "__main__":
# 测试代码
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# 创建模型
model = PADPredictor().to(device)
# 打印模型信息
print("模型信息:")
info = model.get_model_info()
for key, value in info.items():
print(f" {key}: {value}")
# 测试前向传播
batch_size = 4
x = torch.randn(batch_size, 7).to(device)
with torch.no_grad():
output = model(x)
components = model.predict_components(x)
print(f"\n输入形状: {x.shape}")
print(f"输出形状: {output.shape}")
print(f"ΔPAD形状: {components['delta_pad'].shape}")
print(f"ΔPressure形状: {components['delta_pressure'].shape}")
print(f"Confidence形状: {components['confidence'].shape}")
print("\n模型测试完成!")