news 2026/1/28 2:30:10

常用的贝叶斯代理模型

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
常用的贝叶斯代理模型

主要包含两个部分

一个代理模型(surrogate model),用于对目标函数进行建模。代理模型通常有确定的公式或者能计算梯度,又或者有已知的凹凸性、线性等特性,总之就是更容易用于优化。更泛化地讲,其实它就是一个学习模型,输入是所有观测到的函数值点,训练后可以在给定任意x的情况下给出对f(x)的估计。

一个优化策略(optimization strategy),决定下一个采样点的位置,即下一步应在哪个输入x

处观测函数值f(x)。通常它是通过采集函数(acquisition function) 来实现的:

采集函数通常是一个由代理模型推出的函数,它的输入是可行集(feasible set)A上的任意值,输出值衡量了每个输入x有多值得被观测。通常会从以下两方面考虑:

有多大的可能性在x处取得最优值

评估x是否能减少贝叶斯统计模型的不确定性

采集函数通常也是容易求最优值的函数(例如:有公式/能算梯度等),下一个采样点就是可行集上的最大值点,即使采集函数的取最大值的点。

本文主要学习代理模型,模型代码参考SCOOT(WWW2025 oral)中hebo/model库的代码实现

常用的贝叶斯代理模型

GP:标准高斯过程

class GP(BaseModel):

# support_grad = True 表示该模型支持梯度计算,这对于贝叶斯优化等需要梯度信息的场景非常重要

support_grad = True

def __init__(self, num_cont, num_enum, num_out, **conf):

"""

初始化高斯过程回归模型

参数:

num_cont (int): 连续变量的数量

num_enum (int): 离散/枚举变量的数量

num_out (int): 输出变量的数量(通常为1,表示单目标优化)

**conf: 配置参数字典,可包含以下键值对:

- lr (float): 学习率,默认为3e-2

- num_epochs (int): 训练轮数,默认为100

- verbose (bool): 是否打印训练过程信息,默认为False

- print_every (int): 打印训练信息的频率,默认为10

- pred_likeli (bool): 预测时是否考虑似然噪声,默认为True

- noise_lb (float): 噪声下界,默认为1e-5

- optimizer (str): 优化器类型,可选'lbfgs'、'psgld'或'adam',默认为'psgld'

- noise_guess (float): 噪声初始猜测值,默认为0.01

- ard_kernel (bool): 是否使用ARD核(自动相关性判定),默认为True

"""

# 调用父类BaseModel的初始化方法

super().__init__(num_cont, num_enum, num_out, **conf)

# 从配置中提取参数,如果未提供则使用默认值

self.lr = conf.get('lr', 3e-2) # 学习率

self.num_epochs = conf.get('num_epochs', 100) # 训练轮数

self.verbose = conf.get('verbose', False) # 是否打印训练过程信息

self.print_every = conf.get('print_every', 10) # 打印训练信息的频率

self.pred_likeli = conf.get('pred_likeli', True) # 预测时是否考虑似然噪声

self.noise_lb = conf.get('noise_lb', 1e-5) # 噪声下界

self.optimizer = conf.get('optimizer', 'psgld') # 优化器类型

self.noise_guess = conf.get('noise_guess', 0.01) # 噪声初始猜测值

self.ard_kernel = conf.get('ard_kernel', True) # 是否使用ARD核

# 初始化数据标准化器

# xscaler: 用于连续变量的最小-最大标准化器,将数据缩放到[-1, 1]区间

self.xscaler = TorchMinMaxScaler((-1, 1))

# yscaler: 用于目标变量的标准标准化器,使其均值为0,标准差为1

self.yscaler = TorchStandardScaler()

def fit_scaler(self, Xc : Tensor, Xe : Tensor, y : Tensor):

"""

拟合数据标准化器

参数:

Xc: 连续特征张量

Xe: 离散特征张量

y: 目标变量张量

"""

# 如果存在连续变量且数量大于0,则拟合连续变量标准化器

if Xc is not None and Xc.shape[1] > 0:

self.xscaler.fit(Xc)

# 拟合目标变量标准化器

self.yscaler.fit(y)

def xtrans(self, Xc : Tensor, Xe : Tensor, y : Tensor = None):

"""

对输入数据进行转换(标准化和类型转换)

参数:

Xc: 连续特征张量

Xe: 离散特征张量

y: 可选的目标变量张量

返回:

转换后的特征和目标变量(如果提供了y)

"""

# 处理连续变量:如果存在则标准化,否则创建空张量

if Xc is not None and Xc.shape[1] > 0:

Xc_t = self.xscaler.transform(Xc) # 标准化连续变量

else:

Xc_t = torch.zeros(Xe.shape[0], 0) # 创建空的连续变量张量

# 处理离散变量:如果存在则转换为long类型,否则创建空张量

if Xe is None:

Xe_t = torch.zeros(Xc.shape[0], 0).long() # 创建空的离散变量张量

else:

Xe_t = Xe.long() # 转换为long类型,通常用于嵌入层

# 如果提供了目标变量,则也进行标准化

if y is not None:

y_t = self.yscaler.transform(y) # 标准化目标变量

return Xc_t, Xe_t, y_t

else:

return Xc_t, Xe_t

def fit(self, Xc : Tensor, Xe : Tensor, y : Tensor):

"""

训练高斯过程模型

参数:

Xc: 连续特征张量

Xe: 离散特征张量

y: 目标变量张量

"""

# 1. 数据预处理:过滤NaN值

Xc, Xe, y = filter_nan(Xc, Xe, y, 'all')

# 2. 拟合标准化器并转换数据

self.fit_scaler(Xc, Xe, y)

Xc, Xe, y = self.xtrans(Xc, Xe, y)

# 3. 验证数据维度

assert(Xc.shape[1] == self.num_cont)

assert(Xe.shape[1] == self.num_enum)

assert(y.shape[1] == self.num_out)

# 4. 保存训练数据

self.Xc = Xc

self.Xe = Xe

self.y = y

# 5. 设置似然函数(噪声模型)

n_constr = GreaterThan(self.noise_lb) # 噪声下界约束

n_prior = LogNormalPrior(np.log(self.noise_guess), 0.5) # 对数正态先验

# 创建高斯似然函数,包含噪声约束和先验

self.lik = GaussianLikelihood(noise_constraint = n_constr, noise_prior = n_prior)

# 6. 创建GPyTorch模型

self.gp = GPyTorchModel(self.Xc, self.Xe, self.y, self.lik, **self.conf)

# 7. 初始化噪声参数

self.gp.likelihood.noise = max(1e-2, self.noise_lb)

# 8. 设置模型为训练模式

self.gp.train()

self.lik.train()

# 9. 选择优化器

if self.optimizer.lower() == 'lbfgs':

# L-BFGS优化器,适用于小批量数据的准牛顿方法

opt = torch.optim.LBFGS(self.gp.parameters(), lr = self.lr, max_iter = 5, line_search_fn = 'strong_wolfe')

elif self.optimizer == 'psgld':

# 预处理随机梯度Langevin动力学,用于贝叶斯采样

opt = pSGLD(self.gp.parameters(), lr = self.lr, factor = 1. / y.shape[0], pretrain_step = self.num_epochs // 10)

else:

# Adam优化器,默认选择

opt = torch.optim.Adam(self.gp.parameters(), lr = self.lr)

# 10. 创建边际对数似然目标函数

mll = gpytorch.mlls.ExactMarginalLogLikelihood(self.lik, self.gp)

# 11. 训练循环

for epoch in range(self.num_epochs):

def closure():

"""

优化器的闭包函数,计算损失并梯度

"""

# 前向传播:获取GP的后验分布

dist = self.gp(self.Xc, self.Xe)

# 计算负边际对数似然损失

loss = -1 * mll(dist, self.y.squeeze())

# 清零梯度

opt.zero_grad()

# 反向传播

loss.backward()

return loss

# 优化器步进

opt.step(closure)

# 打印训练信息

if self.verbose and ((epoch + 1) % self.print_every == 0 or epoch == 0):

print('After %d epochs, loss = %g' % (epoch + 1, closure().item()), flush = True)

# 12. 设置模型为评估模式

self.gp.eval()

self.lik.eval()

def predict(self, Xc, Xe):

"""

使用训练好的模型进行预测

参数:

Xc: 连续特征张量

Xe: 离散特征张量

返回:

mu: 预测均值

var: 预测方差

"""

# 1. 转换输入数据

Xc, Xe = self.xtrans(Xc, Xe)

# 2. 使用快速预测设置进行预测

with gpytorch.settings.fast_pred_var(), gpytorch.settings.debug(False):

# 获取GP预测

pred = self.gp(Xc, Xe)

# 如果考虑似然噪声,通过似然函数进行预测

if self.pred_likeli:

pred = self.lik(pred)

# 提取均值和方差

mu_ = pred.mean.reshape(-1, self.num_out)

var_ = pred.variance.reshape(-1, self.num_out)

# 3. 将预测结果转换回原始尺度

mu = self.yscaler.inverse_transform(mu_) # 逆标准化均值

var = var_ * self.yscaler.std**2 # 调整方差到原始尺度

# 4. 确保方差不为零(避免数值问题)

return mu, var.clamp(min = torch.finfo(var.dtype).eps)

def sample_y(self, Xc, Xe, n_samples = 1) -> FloatTensor:

"""

从后验分布中采样目标变量

参数:

Xc: 连续特征张量

Xe: 离散特征张量

n_samples: 采样数量

返回:

采样结果,形状为(n_samples, 样本数, 输出维度)

"""

# 1. 转换输入数据

Xc, Xe = self.xtrans(Xc, Xe)

# 2. 进行采样

with gpytorch.settings.debug(False):

if self.pred_likeli:

# 考虑噪声的采样

pred = self.lik(self.gp(Xc, Xe))

else:

# 不考虑噪声的采样(函数值采样)

pred = self.gp(Xc, Xe)

# 从后验分布中采样

samp = pred.rsample(torch.Size((n_samples,))).view(n_samples, Xc.shape[0], self.num_out)

# 将采样结果转换回原始尺度

return self.yscaler.inverse_transform(samp)

def sample_f(self):

"""

采样函数值(不支持,使用sample_y代替)

"""

raise NotImplementedError('Thompson sampling is not supported for GP, use `sample_y` instead')

@property

def noise(self):

"""

获取估计的噪声水平(转换回原始尺度)

"""

return (self.gp.likelihood.noise * self.yscaler.std**2).view(self.num_out).detach()

class GPyTorchModel(gpytorch.models.ExactGP):

"""

GPyTorch模型实现类,继承自ExactGP(精确高斯过程)

"""

def __init__(self,

x : torch.Tensor, # 训练数据的连续特征

xe : torch.Tensor, # 训练数据的离散特征

y : torch.Tensor, # 训练目标值

lik : GaussianLikelihood, # 似然函数

**conf): # 配置参数

# 调用父类构造函数,传入训练数据和似然函数

super().__init__((x, xe), y.squeeze(), lik)

# 特征提取器:处理连续和离散特征的组合

self.fe = deepcopy(conf.get('fe', DummyFeatureExtractor(x.shape[1], xe.shape[1], conf.get('num_uniqs'), conf.get('emb_sizes'))))

# 均值函数:默认使用常数均值

self.mean = deepcopy(conf.get('mean', ConstantMean()))

# 核函数选择:根据配置选择默认核或随机分解核

if conf.get("rd", False):

# 使用随机分解核(Random Decomposition)

self.cov = deepcopy(conf.get('kern', default_kern_rd(x, xe, y, self.fe.total_dim, conf.get('ard_kernel', True), conf.get('fe'), E=conf.get("E", 0.2))))

else:

# 使用默认核函数

self.cov = deepcopy(conf.get('kern', default_kern(x, xe, y, self.fe.total_dim, conf.get('ard_kernel', True), conf.get('fe'))))

def forward(self, x, xe):

"""

前向传播,定义高斯过程的行为

参数:

x: 连续特征

xe: 离散特征

返回:

MultivariateNormal: 多元正态分布,表示高斯过程的预测

"""

# 1. 特征提取:将连续和离散特征组合成统一表示

x_all = self.fe(x, xe)

# 2. 计算均值函数

m = self.mean(x_all)

# 3. 计算协方差矩阵(核函数)

K = self.cov(x_all)

# 4. 返回多元正态分布

return MultivariateNormal(m, K)

GPyGP:输入扭曲的高斯过程模型

class GPyGP(BaseModel):

"""

Input warped GP model implemented using GPy instead of GPyTorch

使用GPy库实现的输入扭曲高斯过程模型(而非GPyTorch)

Why doing so: 为什么这样做:

- Input warped GP 支持输入扭曲的高斯过程

"""

def __init__(self, num_cont, num_enum, num_out, **conf):

super().__init__(num_cont, num_enum, num_out, **conf) # 调用父类初始化

total_dim = num_cont # 总维度初始化为连续变量数量

if num_enum > 0: # 如果存在离散变量

self.one_hot = OneHotTransform(self.conf['num_uniqs']) # 创建one-hot编码器

total_dim += self.one_hot.num_out # 增加离散变量编码后的维度

self.xscaler = TorchMinMaxScaler((-1, 1)) # 连续变量标准化器,范围[-1,1]

self.yscaler = TorchStandardScaler() # 目标变量标准化器

self.verbose = self.conf.get('verbose', False) # 是否显示训练信息

self.num_epochs = self.conf.get('num_epochs', 200) # 训练轮数

self.warp = self.conf.get('warp', True) # 是否使用输入扭曲

self.space = self.conf.get('space') # 设计空间定义

self.num_restarts = self.conf.get('num_restarts', 10) # 优化重启次数

self.rd = self.conf.get('rd', False) # 是否使用随机分解

self.E = self.conf.get('E', 0.2) # 随机分解的边概率

if self.space is None and self.warp: # 如果没有设计空间但启用了扭曲

warnings.warn('Space not provided, set warp to False') # 发出警告

self.warp = False # 禁用扭曲

if self.warp: # 如果启用扭曲

for i in range(total_dim): # 为每个维度禁用日志记录器

logging.getLogger(f'a{i}').disabled = True # 禁用参数a的日志

logging.getLogger(f'b{i}').disabled = True # 禁用参数b的日志

def fit_scaler(self, Xc : FloatTensor, y : FloatTensor):

if Xc is not None and Xc.shape[1] > 0: # 如果存在连续变量

if self.space is not None: # 如果有设计空间信息

cont_lb = self.space.opt_lb[:self.space.num_numeric].view(1, -1).float() # 获取连续变量下界

cont_ub = self.space.opt_ub[:self.space.num_numeric].view(1, -1).float() # 获取连续变量上界

self.xscaler.fit(torch.cat([Xc, cont_lb, cont_ub], dim = 0)) # 结合数据和边界来拟合标准化器

else:

self.xscaler.fit(Xc) # 仅使用数据拟合标准化器

self.yscaler.fit(y) # 拟合目标变量标准化器

def trans(self, Xc : Tensor, Xe : Tensor, y : Tensor = None):

if Xc is not None and Xc.shape[1] > 0: # 处理连续变量

Xc_t = self.xscaler.transform(Xc) # 标准化连续变量

else:

Xc_t = torch.zeros(Xe.shape[0], 0) # 创建空的连续变量张量

if Xe is None or Xe.shape[1] == 0: # 处理离散变量

Xe_t = torch.zeros(Xc.shape[0], 0) # 创建空的离散变量张量

else:

Xe_t = self.one_hot(Xe.long()) # one-hot编码离散变量

Xall = torch.cat([Xc_t, Xe_t], dim = 1) # 合并连续和离散特征

if y is not None: # 如果提供了目标变量

y_t = self.yscaler.transform(y) # 标准化目标变量

return Xall.numpy(), y_t.numpy() # 转换为numpy数组返回

return Xall.numpy() # 只返回特征数据

def fit(self, Xc : FloatTensor, Xe : LongTensor, y : LongTensor):

Xc, Xe, y = filter_nan(Xc, Xe, y, 'all') # 过滤NaN值

self.fit_scaler(Xc, y) # 拟合标准化器

X, y = self.trans(Xc, Xe, y) # 转换数据格式

if self.rd: # 如果使用随机分解

cliques = get_random_graph(X.shape[1], self.E) # 生成随机图团结构

# process first clique 处理第一个团

pair = cliques[0] # 获取第一个团

k1 = GPy.kern.Linear(len(pair), active_dims=pair, ARD = False) # 线性核

k2 = GPy.kern.Matern32(len(pair), active_dims=pair, ARD = True) # Matern32核,启用ARD

k2.lengthscale = np.std(X, axis = 0)[pair] # 设置长度尺度为特征标准差

k2.variance = 0.5 # 设置核方差

k2.variance.set_prior(GPy.priors.Gamma(0.5, 1)) # 设置方差先验为Gamma分布

kern = k1 + k2 # 线性核 + Matern核

# process remaining cliques 处理剩余团

for pair in cliques[1:]:

k1 = GPy.kern.Linear(len(pair), active_dims=pair, ARD = False) # 线性核

k2 = GPy.kern.Matern32(len(pair), active_dims=pair, ARD = True) # Matern32核

geo_mean = 1 # 初始化几何均值

for d in pair: # 计算团内特征的几何标准差

geo_mean *= np.std(X, axis = 0)[d]

k2.lengthscale = geo_mean**(1/len(pair)) # 设置长度尺度为几何均值

k2.variance = 0.5 # 设置核方差

k2.variance.set_prior(GPy.priors.Gamma(0.5, 1)) # 设置方差先验

kern += k1 + k2 # 累加到总核函数

else: # 如果不使用随机分解

k1 = GPy.kern.Linear(X.shape[1], ARD = False) # 全局线性核

k2 = GPy.kern.Matern32(X.shape[1], ARD = True) # 全局Matern32核,启用ARD

k2.lengthscale = np.std(X, axis = 0).clip(min = 0.02) # 设置长度尺度,最小0.02

k2.variance = 0.5 # 设置核方差

k2.variance.set_prior(GPy.priors.Gamma(0.5, 1), warning = False) # 设置方差先验

kern = k1 + k2 # 线性核 + Matern核

if not self.warp: # 如果不使用输入扭曲

self.gp = GPy.models.GPRegression(X, y, kern) # 创建标准高斯过程回归

else: # 如果使用输入扭曲

xmin = np.zeros(X.shape[1]) # 初始化最小边界

xmax = np.ones(X.shape[1]) # 初始化最大边界

xmin[:Xc.shape[1]] = -1 # 设置连续变量范围为[-1,1]

warp_f = GPy.util.input_warping_functions.KumarWarping(X, Xmin = xmin, Xmax = xmax) # 创建Kumar扭曲函数

self.gp = GPy.models.InputWarpedGP(X, y, kern, warping_function = warp_f) # 创建输入扭曲高斯过程

self.gp.likelihood.variance.set_prior(GPy.priors.LogGaussian(-4.63, 0.5), warning = False) # 设置噪声先验

# 多重启优化:最大迭代次数、是否显示信息、重启次数、鲁棒模式

self.gp.optimize_restarts(max_iters = self.num_epochs, verbose = self.verbose, num_restarts = self.num_restarts, robust = True)

return self # 返回自身用于链式调用

def predict(self, Xc : FloatTensor, Xe : LongTensor) -> (FloatTensor, FloatTensor):

Xall = self.trans(Xc, Xe) # 转换输入数据

py, ps2 = self.gp.predict(Xall) # 使用GPy模型预测(均值和方差)

mu = self.yscaler.inverse_transform(FloatTensor(py).view(-1, 1)) # 逆标准化均值

var = self.yscaler.std**2 * FloatTensor(ps2).view(-1, 1) # 调整方差到原始尺度

return mu, var.clamp(torch.finfo(var.dtype).eps) # 返回均值和确保非负的方差

def sample_f(self):

raise NotImplementedError('Thompson sampling is not supported for GP, use `sample_y` instead') # 不支持函数采样

@property

def noise(self):

var_normalized = self.gp.likelihood.variance[0] # 获取标准化后的噪声方差

return (var_normalized * self.yscaler.std**2).view(self.num_out) # 转换到原始尺度并调整形状

RF:随机森林回归

class RF(BaseModel):

"""

随机森林回归模型实现

"""

def __init__(self, num_cont, num_enum, num_out, **conf):

super().__init__(num_cont, num_enum, num_out, **conf) # 调用父类初始化

self.n_estimators = self.conf.get('n_estimators', 100) # 树的数量,默认100

self.rf = RandomForestRegressor(n_estimators = self.n_estimators) # 创建随机森林回归器

self.est_noise = torch.zeros(self.num_out) # 初始化估计噪声为零张量

if self.num_enum > 0: # 如果存在离散变量

self.one_hot = OneHotTransform(self.conf['num_uniqs']) # 创建one-hot编码器

def xtrans(self, Xc : FloatTensor, Xe: LongTensor) -> np.ndarray:

"""

转换输入数据为numpy数组格式

"""

if self.num_enum == 0: # 如果没有离散变量

return Xc.detach().numpy() # 直接返回连续变量的numpy数组

else: # 如果有离散变量

Xe_one_hot = self.one_hot(Xe) # 对离散变量进行one-hot编码

if Xc is None: # 如果没有连续变量

Xc = torch.zeros(Xe.shape[0], 0) # 创建空的连续变量张量

return torch.cat([Xc, Xe_one_hot], dim = 1).numpy() # 合并连续和离散特征并转为numpy

def fit(self, Xc : torch.Tensor, Xe : torch.Tensor, y : torch.Tensor):

"""

训练随机森林模型

"""

Xc, Xe, y = filter_nan(Xc, Xe, y, 'all') # 过滤包含NaN的数据点

Xtr = self.xtrans(Xc, Xe) # 转换输入特征为numpy格式

ytr = y.numpy().reshape(-1) # 转换目标变量为一维numpy数组

self.rf.fit(Xtr, ytr) # 训练随机森林模型

# 计算训练集上的MSE作为噪声估计

mse = np.mean((self.rf.predict(Xtr).reshape(-1) - ytr)**2).reshape(self.num_out)

self.est_noise = torch.FloatTensor(mse) # 保存估计的噪声水平

@property

def noise(self):

"""

返回估计的噪声水平

"""

return self.est_noise

def predict(self, Xc : torch.Tensor, Xe : torch.Tensor):

"""

使用训练好的模型进行预测

返回: (预测均值, 预测方差 + 估计噪声)

"""

X = self.xtrans(Xc, Xe) # 转换输入数据

mean = self.rf.predict(X).reshape(-1, 1) # 预测均值(所有树的平均)

preds = [] # 存储每棵树的独立预测

for estimator in self.rf.estimators_: # 遍历所有决策树

preds.append(estimator.predict(X).reshape([-1,1])) # 收集每棵树的预测

var = np.var(np.concatenate(preds, axis=1), axis=1) # 计算树间预测方差

# 返回均值和总方差(模型方差 + 估计噪声)

return torch.FloatTensor(mean.reshape([-1,1])), torch.FloatTensor(var.reshape([-1,1])) + self.noise

SVGP:稀疏变分高斯过程(Stochastic Variational Gaussian Process)

class SVGP(BaseModel):

"""

稀疏变分高斯过程 (Stochastic Variational Gaussian Process)

适用于大规模数据的近似高斯过程模型

"""

support_grad = True # 支持梯度计算

support_multi_output = True # 支持多输出任务

def __init__(self, num_cont, num_enum, num_out, **conf):

super().__init__(num_cont, num_enum, num_out, **conf) # 调用父类初始化

# 配置参数

self.use_ngd = conf.get('use_ngd', False) # 是否使用自然梯度下降

self.lr = conf.get('lr', 1e-2) # 基础学习率

self.lr_vp = conf.get('lr_vp', 1e-1) # 变分参数学习率

self.lr_fe = conf.get('lr_fe', 1e-3) # 特征提取器学习率

self.num_inducing = conf.get('num_inducing', 128) # 诱导点数量

self.ard_kernel = conf.get('ard_kernel', True) # 是否使用ARD核

self.pred_likeli = conf.get('pred_likeli', True) # 预测时是否考虑噪声

self.beta = conf.get('beta', 1.0) # ELBO的beta参数

# 训练参数

self.batch_size = conf.get('batch_size', 64) # 批大小

self.num_epochs = conf.get('num_epochs', 300) # 训练轮数

self.verbose = conf.get('verbose', False) # 是否显示训练信息

self.print_every = conf.get('print_every', 10) # 打印频率

self.noise_lb = conf.get('noise_lb', 1e-5) # 噪声下界

# 数据标准化器

self.xscaler = TorchMinMaxScaler((-1, 1)) # 输入标准化器

self.yscaler = TorchStandardScaler() # 输出标准化器

def fit_scaler(self, Xc : FloatTensor, Xe : LongTensor, y : FloatTensor):

"""拟合数据标准化器"""

if Xc is not None and Xc.shape[1] > 0: # 如果有连续变量

self.xscaler.fit(Xc) # 拟合输入标准化器

self.yscaler.fit(y) # 拟合输出标准化器

def xtrans(self, Xc : FloatTensor, Xe : LongTensor, y : FloatTensor = None):

"""转换输入数据格式"""

if Xc is not None and Xc.shape[1] > 0: # 处理连续变量

Xc_t = self.xscaler.transform(Xc) # 标准化连续变量

else:

Xc_t = torch.zeros(Xe.shape[0], 0) # 创建空的连续变量张量

if Xe is None: # 处理离散变量

Xe_t = torch.zeros(Xc.shape[0], 0).long()

else:

Xe_t = Xe.long() # 确保离散变量为long类型

if y is not None: # 如果提供了目标变量

y_t = self.yscaler.transform(y) # 标准化目标变量

return Xc_t, Xe_t, y_t

else:

return Xc_t, Xe_t

def fit(self, Xc : FloatTensor, Xe : LongTensor, y : FloatTensor):

"""训练SVGP模型"""

Xc, Xe, y = filter_nan(Xc, Xe, y, 'any') # 过滤包含NaN的数据点

self.fit_scaler(Xc, Xe, y) # 拟合标准化器

Xc, Xe, y = self.xtrans(Xc, Xe, y) # 转换数据格式

# 验证数据维度

assert(Xc.shape[1] == self.num_cont)

assert(Xe.shape[1] == self.num_enum)

assert(y.shape[1] == self.num_out)

# 设置噪声约束和创建模型

n_constr = GreaterThan(self.noise_lb) # 噪声下界约束

self.gp = SVGPModel(Xc, Xe, y, **self.conf) # 创建SVGP模型

# 为每个输出创建独立的高斯似然函数

self.lik = nn.ModuleList([GaussianLikelihood(noise_constraint = n_constr) for _ in range(self.num_out)])

# 设置模型为训练模式

self.gp.train()

self.lik.train()

# 创建数据加载器

ds = TensorDataset(Xc, Xe, y) # 创建Tensor数据集

dl = DataLoader(ds, batch_size = self.batch_size, shuffle = True, drop_last = y.shape[0] > self.batch_size) # 数据加载器

# 配置优化器

if self.use_ngd: # 如果使用自然梯度下降

opt = torch.optim.Adam([

{'params' : self.gp.fe.parameters(), 'lr' : self.lr_fe}, # 特征提取器参数

{'params' : self.gp.gp.hyperparameters()}, # GP超参数

{'params' : self.lik.parameters()}, # 似然函数参数

], lr = self.lr)

opt_ng = gpytorch.optim.NGD(self.gp.variational_parameters(), lr = self.lr_vp, num_data = y.shape[0]) # 自然梯度优化器

else: # 使用标准Adam优化器

opt = torch.optim.Adam([

{'params' : self.gp.fe.parameters(), 'lr' : self.lr_fe}, # 特征提取器参数

{'params' : self.gp.gp.hyperparameters()}, # GP超参数

{'params' : self.gp.gp.variational_parameters(), 'lr' : self.lr_vp}, # 变分参数

{'params' : self.lik.parameters()}, # 似然函数参数

], lr = self.lr)

# 为每个输出创建变分ELBO目标函数

mll = [gpytorch.mlls.VariationalELBO(self.lik[i], self.gp.gp[i], num_data = y.shape[0], beta = self.beta) for i in range(self.num_out)]

# 训练循环

for epoch in range(self.num_epochs):

epoch_loss = 0. # 累计损失

epoch_cnt = 1e-6 # 批次计数(避免除零)

for bxc, bxe, by in dl: # 遍历数据批次

dist_list = self.gp(bxc, bxe, by) # 前向传播,获取分布列表

loss = 0 # 初始化损失

valid = torch.isfinite(by) # 检查有效值

# 计算每个输出的损失

for i, dist in enumerate(dist_list):

loss += -1 * mll[i](dist, by[valid[:, i], i]) * valid[:, i].sum() # 加权ELBO损失

loss /= by.shape[0] # 平均损失

# 反向传播和优化

if self.use_ngd:

opt.zero_grad()

opt_ng.zero_grad()

loss.backward()

opt.step()

opt_ng.step()

else:

opt.zero_grad()

loss.backward()

opt.step()

# 累计损失和计数

epoch_loss += loss.item()

epoch_cnt += 1

epoch_loss /= epoch_cnt # 计算平均epoch损失

if self.verbose and ((epoch + 1) % self.print_every == 0 or epoch == 0):

print('After %d epochs, loss = %g' % (epoch + 1, epoch_loss), flush = True)

# 设置模型为评估模式

self.gp.eval()

self.lik.eval()

def predict(self, Xc, Xe):

"""模型预测"""

Xc, Xe = self.xtrans(Xc, Xe) # 转换输入数据

with gpytorch.settings.fast_pred_var(), gpytorch.settings.debug(False): # 快速预测设置

pred = self.gp(Xc, Xe) # 获取预测分布

if self.pred_likeli: # 如果考虑似然噪声

for i in range(self.num_out):

pred[i] = self.lik[i](pred[i]) # 通过似然函数转换

# 合并所有输出的均值和方差

mu_ = torch.cat([pred[i].mean.reshape(-1, 1) for i in range(self.num_out)], dim = 1)

var_ = torch.cat([pred[i].variance.reshape(-1, 1) for i in range(self.num_out)], dim = 1)

# 逆标准化到原始尺度

mu = self.yscaler.inverse_transform(mu_)

var = var_ * self.yscaler.std**2

return mu, var.clamp(min = torch.finfo(var.dtype).eps) # 确保方差非负

def sample_y(self, Xc, Xe, n_samples = 1) -> FloatTensor:

"""

从后验分布采样目标变量

返回: (n_samples, 样本数, 输出维度)

"""

Xc, Xe = self.xtrans(Xc, Xe) # 转换输入数据

with gpytorch.settings.debug(False):

pred = self.gp(Xc, Xe) # 获取预测分布

if self.pred_likeli: # 如果考虑似然噪声

for i in range(self.num_out):

pred[i] = self.lik[i](pred[i]) # 通过似然函数转换

# 从每个输出分布采样并合并

samp = [pred[i].rsample(torch.Size((n_samples, ))).reshape(n_samples, -1, 1) for i in range(self.num_out)]

samp = torch.cat(samp, dim = -1)

return self.yscaler.inverse_transform(samp) # 逆标准化采样结果

def sample_f(self):

"""不支持函数采样"""

raise NotImplementedError('Thompson sampling is not supported for GP, use `sample_y` instead')

@property

def noise(self):

"""获取估计的噪声水平"""

noise = torch.FloatTensor([lik.noise for lik in self.lik]).view(self.num_out).detach() # 各输出噪声

return noise * self.yscaler.std**2 # 转换到原始尺度

SVIDKL:稀疏变分深度核学习(Stochastic Variational Deep Kernel Learning)

class DKLFe(nn.Module):

"""

深度核学习特征提取器 (Deep Kernel Learning Feature Extractor)

使用神经网络自动学习特征表示,替代手动设计的核函数

"""

def __init__(self, num_cont, num_enum, num_out, **conf):

super().__init__()

# 神经网络配置参数

self.num_hiddens = conf.get('num_hiddens', 64) # 隐藏层维度,默认64

self.num_layers = conf.get('num_layers', 2) # 隐藏层数量,默认2层

self.act = conf.get('act', nn.LeakyReLU()) # 激活函数,默认LeakyReLU

self.sn_norm = conf.get('sn_norm') # 谱归一化,默认不使用

# 特征预处理:处理连续和离散变量的嵌入转换

self.emb_trans = DummyFeatureExtractor(num_cont, num_enum, conf.get('num_uniqs'), conf.get('emb_sizes'))

# 构建深度神经网络特征提取器

self.fe = construct_hidden(self.emb_trans.total_dim, self.num_layers, self.num_hiddens, self.act, self.sn_norm)

self.total_dim = self.num_hiddens # 输出特征维度

def forward(self, x, xe):

"""

前向传播:将原始输入转换为深度特征表示

"""

x_all = self.emb_trans(x, xe) # 首先进行嵌入转换,处理混合类型输入

return self.fe(x_all) # 通过深度网络提取高级特征

class SVIDKL(SVGP):

"""

稀疏变分深度核学习 (Stochastic Variational Deep Kernel Learning)

在SVGP基础上集成深度特征学习的扩展

"""

def __init__(self, num_cont, num_enum, num_out, **conf):

# 创建配置的深拷贝,避免修改原始配置

new_conf = deepcopy(conf)

# 关键变化1:禁用ARD核

new_conf.setdefault('ard_kernel', False)

# 解释:当有神经网络特征提取器时,不需要使用ARD核

# 因为神经网络已经自动学习了特征的重要性权重

# 关键变化2:使用深度特征提取器

new_conf.setdefault('fe', DKLFe(num_cont, num_enum, num_out, **new_conf))

# 解释:用深度神经网络替代简单的特征转换,自动学习数据表示

# 关键变化3:使用简化的核函数

new_conf.setdefault('kern', ScaleKernel(MaternKernel(nu = 2.5)))

# 解释:由于特征提取器已经处理了复杂性,可以使用更简单的核函数

# 调用父类SVGP的初始化,传入修改后的配置

super().__init__(num_cont, num_enum, num_out, **new_conf)

CatBoost:梯度提升树模型

class CatBoost(BaseModel):

"""

CatBoost梯度提升树模型实现

专门优化类别特征处理的梯度提升算法

"""

def __init__(self, num_cont, num_enum, num_out, **conf):

super().__init__(num_cont, num_enum, num_out, **conf) # 调用父类初始化

# CatBoost配置参数

self.num_epochs = self.conf.get('num_epochs', 100) # 最大树的数量(迭代次数)

self.lr = self.conf.get('lr', 0.2) # 学习率

self.depth = self.conf.get('depth', 10) # 树深度,推荐范围[1, 10]

self.loss_function = self.conf.get('loss_function', 'RMSEWithUncertainty') # 损失函数,支持不确定性估计

self.posterior_sampling = self.conf.get('posterior_sampling', True) # 是否使用后验采样

self.verbose = self.conf.get('verbose', False) # 是否显示训练过程

self.random_seed = self.conf.get('random_seed', 42) # 随机种子

self.num_ensembles = self.conf.get('num_ensembles', 10) # 虚拟集成数量

# 确保迭代次数足够用于集成

if self.num_epochs < 2 * self.num_ensembles:

self.num_epochs = self.num_ensembles * 2 # 至少是集成数量的2倍

# 创建CatBoost回归器实例

self.model = CatBoostRegressor(

iterations=self.num_epochs, # 迭代次数

learning_rate=self.lr, # 学习率

depth=self.depth, # 树深度

loss_function=self.loss_function, # 损失函数(支持不确定性)

posterior_sampling=self.posterior_sampling, # 后验采样

verbose=self.verbose, # 是否显示训练信息

random_seed=self.random_seed, # 随机种子

allow_writing_files=False) # 禁止写入文件

def xtrans(self, Xc: FloatTensor, Xe: LongTensor) -> FeaturesData:

"""

转换输入数据为CatBoost所需的格式

"""

# 处理连续变量:转换为numpy float32格式

num_feature_data = Xc.numpy().astype(np.float32) if self.num_cont != 0 else None

# 处理离散变量:转换为字符串格式(CatBoost要求)

cat_feature_data = Xe.numpy().astype(str).astype(object) if self.num_enum != 0 else None

# 返回CatBoost特征数据对象

return FeaturesData(

num_feature_data=num_feature_data, # 数值特征

cat_feature_data=cat_feature_data) # 类别特征

def fit(self, Xc: FloatTensor, Xe: LongTensor, y: FloatTensor):

"""

训练CatBoost模型

"""

Xc, Xe, y = filter_nan(Xc, Xe, y, 'all') # 过滤NaN值

# 创建训练数据池

train_data = Pool(

data=self.xtrans(Xc=Xc, Xe=Xe), # 转换特征数据

label=y.numpy().reshape(-1)) # 转换标签为一维数组

self.model.fit(train_data) # 训练模型

def predict(self, Xc: FloatTensor, Xe: LongTensor) -> (FloatTensor, FloatTensor):

"""

使用CatBoost进行预测,返回均值和方差

"""

test_data = Pool(data=self.xtrans(Xc=Xc, Xe=Xe)) # 创建测试数据池

# 使用虚拟集成进行预测,获取不确定性估计

preds = self.model.virtual_ensembles_predict(

data=test_data, # 测试数据

prediction_type='TotalUncertainty', # 预测类型:总不确定性

virtual_ensembles_count=self.num_ensembles) # 虚拟集成数量

# 解析预测结果:

# preds[:, 0] = 预测均值

# preds[:, 1] = 知识不确定性(模型不确定性)

# preds[:, 2] = 偶然不确定性(数据噪声)

mean = preds[:, 0] # 预测均值

var = preds[:, 1] + preds[:, 2] # 总方差 = 模型方差 + 数据噪声

# 返回PyTorch张量格式的结果

return torch.FloatTensor(mean.reshape([-1,1])), \

torch.FloatTensor(var.reshape([-1,1]))

DeepEnsemble:深度集成模型

class DeepEnsemble(BaseModel):

"""

深度集成模型 - 通过多个独立训练的神经网络集成来估计不确定性

基于Lakshminarayanan et al. (2017) "Simple and Scalable Predictive Uncertainty Estimation using Deep Ensembles"

"""

# 模型能力标识

support_ts = True # 支持Thompson采样

support_grad = True # 支持梯度计算

support_multi_output = True # 支持多输出任务

support_warm_start = True # 支持热启动(从已有模型继续训练)

def __init__(self, num_cont, num_enum, num_out, **conf):

"""

初始化深度集成模型

参数:

num_cont: 连续变量数量

num_enum: 离散变量数量

num_out: 输出维度

**conf: 配置参数

"""

super().__init__(num_cont, num_enum, num_out, **conf) # 调用父类初始化

# 集成策略配置

self.bootstrap = self.conf.setdefault('bootstrap', False) # 是否使用自助采样创建数据多样性

self.rand_prior = self.conf.setdefault('rand_prior', False) # 是否使用随机先验网络

self.output_noise = self.conf.setdefault('output_noise', True) # 是否输出噪声估计

self.num_ensembles = self.conf.setdefault('num_ensembles', 5) # 集成模型数量

self.num_process = self.conf.setdefault('num_processes', 1) # 并行训练进程数

self.num_epochs = self.conf.setdefault('num_epochs', 500) # 每个模型的训练轮数

self.print_every = self.conf.setdefault('print_every', 50) # 训练信息打印频率

# 网络架构配置

self.num_layers = self.conf.setdefault('num_layers', 1) # 隐藏层数量

self.num_hiddens = self.conf.setdefault('num_hiddens', 128) # 隐藏层维度

self.l1 = self.conf.setdefault('l1', 1e-3) # L1正则化系数

self.batch_size = self.conf.setdefault('batch_size', 32) # 批大小

self.lr = self.conf.setdefault('lr', 5e-3) # 学习率

self.adv_eps = self.conf.setdefault('adv_eps', 0.) # 对抗训练扰动大小(未使用)

self.verbose = self.conf.setdefault('verbose', False) # 是否显示训练详情

self.basenet_cls = self.conf.setdefault('basenet_cls', BaseNet) # 基础网络类

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/1/26 13:04:03

PCSX2模拟器《真实犯罪:纽约》高清渲染优化全攻略

PCSX2模拟器《真实犯罪&#xff1a;纽约》高清渲染优化全攻略 【免费下载链接】pcsx2 PCSX2 - The Playstation 2 Emulator 项目地址: https://gitcode.com/GitHub_Trending/pc/pcsx2 在使用PCSX2模拟器体验《真实犯罪&#xff1a;纽约》时&#xff0c;许多玩家都面临着…

作者头像 李华
网站建设 2026/1/27 17:50:16

Tera Term:全能终端仿真器的深度探索与实践指南

想要在Windows环境下获得媲美Linux终端的强大功能吗&#xff1f;Tera Term作为一款开源的终端仿真器&#xff0c;正是您梦寐以求的解决方案&#xff01;它不仅支持SSH连接和串行通信&#xff0c;还提供了丰富的自定义选项&#xff0c;让您的终端使用体验达到全新高度。 【免费下…

作者头像 李华
网站建设 2026/1/24 6:11:43

RPCS3汉化补丁终极安装指南:轻松打造完美中文游戏体验

RPCS3汉化补丁终极安装指南&#xff1a;轻松打造完美中文游戏体验 【免费下载链接】rpcs3 PS3 emulator/debugger 项目地址: https://gitcode.com/GitHub_Trending/rp/rpcs3 想要在PC上畅玩中文版的PS3经典游戏吗&#xff1f;RPCS3模拟器通过其强大的补丁系统&#xff0…

作者头像 李华
网站建设 2026/1/19 19:26:18

SNMP 请求响应报文传输分片定位

1.分片报文 通过tcpdump 抓包&#xff0c;查看响应报文得内容如下&#xff1a;image-20251017170120282有一段很关键得报文内容如下&#xff1a;"6876","2025-10-16 15:56:25.677396","172.16.25.13","172.16.11.102","IPv4&quo…

作者头像 李华
网站建设 2026/1/27 7:09:39

记一次 .NET 某医联体管理系统 崩溃分析

一&#xff1a;背景1. 讲故事这段时间都在跑外卖&#xff0c;感觉好久都没写文章了&#xff0c;今天继续给大家带来一篇崩溃类的生产事故&#xff0c;这是微信上有位老朋友找到我的&#xff0c;让我帮忙看下为啥崩溃了&#xff0c;dump也在手&#xff0c;接下来就可以一顿分析。…

作者头像 李华