Scipy Optimize Minimize成功终止但未执行任何迭代问题
问题背景
我正在编写优化器,用于寻找最优常量参数,最小化数组y与X上通用函数之间的均方误差(MSE)。该通用函数以前序遍历形式给出,例如针对X的函数x1 + c*x2可表示为[+, x1, *, c, x2],上述示例的优化目标为最小化:
sum_for_all_x (y - (x1 + c*x2))^2
以下是实现代码,提前说明两点前提:
X和y均为PyTorch张量constants为待优化的参数值列表
def loss(self, constants, X, y): stack = [] # 存储部分计算结果的栈 const = 0 # 当前使用的常量索引 for idx in self.traversal[::-1]: # 反转前序遍历序列 if idx > Language.max_variables: # 当前节点是运算符 function = Language.idx_to_token[idx] # 获取对应运算函数 first_operand = stack.pop() # 取出第一个操作数 if function.arity == 1: # 单目运算符(例如sin) stack.append(function.function(first_operand)) # 计算结果入栈 else: # 双目运算符 second_operand = stack.pop() # 取出第二个操作数 stack.append(function.function(first_operand, second_operand)) elif idx == 0: # 当前节点是常量(索引0标记常量位置) stack.append(constants[const]*torch.ones(X.shape[0])) # 常量值入栈 const += 1 # 常量索引自增 else: stack.append(X[:, idx - 1]) # 取X对应列入栈 prediction = stack[0] return (y - prediction).pow(2).mean().cpu().numpy() def optimize_constants(self, X, y): ''' 优化表达式树中的常量参数 ''' if 0 not in self.traversal: # 没有需要优化的常量直接返回 return self.traversal x0 = [0 for i in range(len(self.constants))] # 初始值猜测 ini = time.time() res = minimize(self.loss, x0, args=(X, y), method='BFGS', options={'disp': True}) print(res) print('Time:', time.time() - ini)
遇到的问题
优化器输出提示已成功终止,但全程未执行任何迭代,返回的res结果示例如下:
Optimization terminated successfully. Current function value: 2.920725 Iterations: 0 Function evaluations: 2 Gradient evaluations: 1 fun: 2.9207253456115723 hess_inv: array([[1]]) jac: array([0.]) message: 'Optimization terminated successfully.' nfev: 2 nit: 0 njev: 1 status: 0 success: True x: array([0.])
已尝试的排查方案均未解决问题:
- 更换
minimize使用的优化方法(如Nelder-Mead、SLSQP等),所有方法均出现相同现象 - 修改损失结果的返回方式(例如改用
(y - prediction).pow(2).mean().item()返回标量损失)
问题原因与修复方案
根本原因
核心问题有两个:
- 梯度断裂:
scipy.optimize.minimize传入的constants参数是NumPy数组类型,你直接将NumPy值和PyTorch张量做运算时,PyTorch会把这个NumPy值当成固定常数,不会建立对应的反向传播计算图。scipy默认用有限差分计算梯度时,会发现参数扰动无法正确传递到损失值,计算得到的梯度始终为0,自然判定初始点就是极值点,直接终止迭代。 - 参数维度不匹配:你初始化
x0时取的长度是len(self.constants),但实际优化时的常量总数是遍历表达式过程中遇到idx==0的次数,两个值如果不一致,会导致参数和实际优化位置不匹配。
修复方法
- 损失函数内先将scipy传入的NumPy格式常量转成可追踪梯度的PyTorch张量,再参与前向计算;
- 用PyTorch的自动微分显式计算损失对常量的梯度,返回给scipy,同时在调用
minimize时设置jac=True,告诉优化器损失函数会同时返回损失值和对应梯度,不要用自带的有限差分计算梯度; - 修正初始值
x0的长度,直接统计遍历序列中idx==0的个数作为初始值长度,保证参数维度匹配。
修复后的核心代码参考
def loss(self, constants, X, y): # 将numpy输入转为可求导的torch张量,匹配X的数据类型和设备 consts_t = torch.tensor(constants, dtype=X.dtype, device=X.device, requires_grad=True) stack = [] const_idx = 0 for idx in self.traversal[::-1]: if idx > Language.max_variables: op = Language.idx_to_token[idx] first_op = stack.pop() if op.arity == 1: cal_res = op.function(first_op) else: second_op = stack.pop() cal_res = op.function(first_op, second_op) stack.append(cal_res) elif idx == 0: stack.append(consts_t[const_idx] * torch.ones(X.shape[0], dtype=X.dtype, device=X.device)) const_idx += 1 else: stack.append(X[:, idx-1]) pred = stack[0] loss_val = (y - pred).pow(2).mean() # 显式计算梯度,转成numpy格式返回给scipy grad_val = torch.autograd.grad(loss_val, consts_t)[0].cpu().numpy() return loss_val.item(), grad_val def optimize_constants(self, X, y): if 0 not in self.traversal: return self.traversal # 按表达式中实际常量个数初始化初始值 const_count = sum(1 for idx in self.traversal if idx == 0) x0 = [0.0 for _ in range(const_count)] ini = time.time() # 传入jac=True,使用我们自己计算的梯度 res = minimize(self.loss, x0, args=(X, y), method='BFGS', jac=True, options={'disp': True}) print(res) print('Time:', time.time() - ini)
内容的提问来源于stack exchange,提问作者Alex Ferrando
相关产品推荐
相关产品推荐

