Scipy最小化问题:selection变量始终未更新,请求协助排查
问题描述
现有按地理区域划分的门店样本,需选取20%的门店,使选中门店的各区域销售占比与整体销售分布一致。但代码中objective函数内的selection变量始终未发生变化,test_fraction保持不变,优化无法正常进行。
核心问题分析
输入参数直接修改导致梯度失效
你在objective函数内直接重新赋值selection = expit(selection).round(),但优化器是基于传入的原始selection数组计算梯度的,修改后的变量无法反馈给优化器的更新逻辑,相当于优化器的迭代完全没有作用在实际影响目标值的变量上。非连续操作导致梯度消失
round()是不连续的阶跃操作,加上expit()在输入接近0或1时输出变化极缓,导致目标函数的梯度几乎为0。SLSQP这类基于梯度的优化器无法通过梯度判断更新方向,自然不会修改selection变量。约束函数同样存在非连续问题
约束函数中使用expit(selection).round(),同样导致约束条件不连续,优化器无法处理这类离散约束。边界设置不合理
你设置的bounds = [(0, 1) for _ in range(num_stores)],而expit(0)=0.5、expit(1)=0.731,round()后要么是0要么是1,初始解的微小变化根本不会改变selection的二进制结果,进一步锁死了优化过程。
修正方案
方案1:连续松弛法(适配scipy梯度优化器)
放弃硬二进制选择,将selection视为每个门店被选中的概率,计算期望的销售占比作为目标,这样目标函数可导,优化器能正常迭代。
import pandas as pd import numpy as np from scipy.optimize import minimize from scipy.special import expit # Sample data np.random.seed(0) data = pd.DataFrame({ 'Store': range(1, 101), 'Geography': np.random.choice(['North', 'South', 'East', 'West'], 100), 'TotalSales': np.random.rand(100) * 1000 }) # Define the number of stores to select num_stores = len(data) target_percentage = 0.2 target_num_stores = int(target_percentage * num_stores) # Calculate overall sales fraction per geography overall_sales = data.groupby('Geography')['TotalSales'].sum() overall_fraction = overall_sales / overall_sales.sum() # 为了对齐所有区域,将overall_fraction转为有序数组 geo_order = overall_fraction.index overall_fraction_arr = overall_fraction.values # 预计算每个门店对应区域的索引 data['geo_idx'] = data['Geography'].map({g:i for i,g in enumerate(geo_order)}) # Objective function:连续松弛版本 def objective(probs): # 将probs限制在[0,1]之间,避免极端值 probs = np.clip(probs, 1e-6, 1-1e-6) # 计算各区域的期望总销售额 expected_sales = np.zeros(len(geo_order)) for i in range(num_stores): expected_sales[data['geo_idx'].iloc[i]] += data['TotalSales'].iloc[i] * probs[i] total_expected = expected_sales.sum() if total_expected < 1e-6: return np.inf test_fraction = expected_sales / total_expected # 计算与整体分布的误差 error = ((overall_fraction_arr - test_fraction) ** 2).sum() # 选中门店数量的惩罚 selected_count = probs.sum() penalty = ((selected_count - target_num_stores) / target_num_stores) ** 2 return error + penalty # 约束:选中门店的期望数量等于目标值 def constraint(probs): return probs.sum() - target_num_stores # 边界:概率在0到1之间 bounds = [(0, 1) for _ in range(num_stores)] # 初始解:均匀概率,或者按整体分布初始化 initial_probs = np.full(num_stores, target_percentage) # 设置约束 constraints = {'type': 'eq', 'fun': constraint} # 优化 result = minimize(objective, x0=initial_probs, bounds=bounds, constraints=constraints, method='SLSQP', options={'disp': True}) # 将概率转为硬选择:取概率最高的target_num_stores个门店 selected_idx = np.argsort(result.x)[-target_num_stores:] selected_stores = data.iloc[selected_idx] # 验证结果 selected_sales = selected_stores.groupby('Geography')['TotalSales'].sum() selected_fraction = selected_sales / selected_sales.sum() print("整体销售分布:") print(overall_fraction) print("\n选中门店销售分布:") print(selected_fraction)
方案2:离散优化法(遗传算法)
如果需要严格的二进制选择,推荐使用专门的离散优化工具(比如DEAP库),这类算法不需要依赖梯度,适合处理0-1选择问题。
关键说明
- 连续松弛法通过概率期望将离散问题转为连续优化,适配scipy的梯度优化器,最后再通过取概率最高的门店得到硬选择结果
- 离散优化法更贴近实际业务需求,但需要额外安装第三方库,实现复杂度稍高
内容的提问来源于stack exchange,提问作者user3490622

