Old*_*ort 7 python scipy-optimize
我希望获得与 Excel 中求解器函数的工作方式类似的结果。我一直在阅读 Scipy optimization 并尝试构建一个函数来输出我想要找到的最大值。该方程基于四个不同的变量,请参阅下面的代码:
import pandas as pd
import numpy as np
from scipy import optimize
cols = {
'Dividend2': [9390, 7448, 177],
'Probability': [341, 376, 452],
'EV': [0.53, 0.60, 0.55],
'Dividend': [185, 55, 755],
'EV2': [123, 139, 544],
}
df = pd.DataFrame(cols)
def myFunc(params):
"""myFunc metric."""
(ev, bv, vc, dv) = params
df['Number'] = np.where(df['Dividend2'] <= vc, 1, 0) \
+ np.where(df['EV2'] <= dv, 1, 0)
df['Return'] = np.where(
df['EV'] <= ev, 0, np.where(
df['Probability'] >= bv, 0, df['Number'] * df['Dividend'] - (vc + dv)
)
)
return -1 * (df['Return'].sum())
b1 = [(0.2,4), (300,600), (0,1000), (0,1000)]
start = [0.2, 600, 1000, 1000]
result = optimize.minimize(fun=myFunc, bounds=b1, x0=start)
print(result)
Run Code Online (Sandbox Code Playgroud)
所以我想在更改变量 ev、bv、vc 和 dv 时找到 df 中 Return 列的最大值。我希望它们介于 ev: 0.2-4、bv: 300-600、vc: 0-1000 和 dv: 0-1000 之间。
运行我的代码时,函数似乎停止在 x0 处。
我将使用optuna库为您尝试解决的问题类型提供解决方案。我尝试过使用scipy.optimize.minimize,看起来损失景观在大多数地方可能相当平坦,因此容差强制最小化算法(L-BFGS-B)过早停止。
有了 optuna,事情就变得相当简单了。Optuna 仅需要一个objective函数和一个study. trials该研究向该函数发送各种objective数据,该函数反过来评估您选择的指标。
myFunc2我通过主要删除调用来定义另一个度量函数np.where,因为您可以取消它们(减少步骤数)并使函数稍微快一些。
# install optuna with pip\npip install -Uqq optuna\nRun Code Online (Sandbox Code Playgroud)\n尽管我考虑使用相当平滑的损失景观,但有时有必要可视化景观本身。部分的答案B详细阐述了可视化。但是,如果您想使用更平滑的度量函数怎么办?本节D对此进行了一些阐述。
代码执行顺序应该是:
\nC>> B>> B.1>> B.2>> B.3>> A.1>>A.2Dsearch_space如果您使用for 部分中提到的所有可能的参数值创建一个 hiplot(也称为平行坐标图)B.2,并绘制 的最低 50 个输出myFunc2,则它将如下所示:
绘制所有这些点将search_space如下所示:
这些数字表明,对于四个参数中的任何两个,损失情况大部分都是平坦的(ev, bv, vc, dv)。与其他两个采样器(和)GridSampler相比,这可能是 only (强制搜索过程)做得更好的原因。请单击下面的任意图像以查看放大图像。这也可能是立即失败的原因。TPESamplerRandomSamplerscipy.optimize.minimize(method="L-BFGS-B")
# Create contour plots for parameter-pairs\nstudy_name = "GridSampler"\nstudy = studies.get(study_name)\n\nviews = [("dv", "vc"), ("dv", "bv"), ("dv", "ev"), \n ("bv", "ev"), ("vc", "ev"), ("vc", "bv")]\n\nfor i, (x, y) in enumerate(views):\n print(f"Figure: {i}/{len(views)}")\n study_contour_plot(study=study, params=(x, y))\nRun Code Online (Sandbox Code Playgroud)\nstudy_name = "GridSampler"\nstudy = studies.get(study_name)\n\nfig = optuna.visualization.plot_param_importances(study)\nfig.update_layout(title=f\'Hyperparameter Importances: {study.study_name}\', \n autosize=False,\n width=800, height=500,\n margin=dict(l=65, r=50, b=65, t=90))\nfig.show()\nRun Code Online (Sandbox Code Playgroud)\n部分B.3.查找以下指标的最低指标-88.333:
{\'ev\': 0.2, \'bv\': 500.0, \'vc\': 222.2222, \'dv\': 0.0}import warnings\nfrom functools import partial\nfrom typing import Iterable, Optional, Callable, List\n\nimport pandas as pd\nimport numpy as np\nimport optuna\nfrom tqdm.notebook import tqdm\n\nwarnings.filterwarnings("ignore", category=optuna.exceptions.ExperimentalWarning)\noptuna.logging.set_verbosity(optuna.logging.WARNING)\n\nPARAM_NAMES: List[str] = ["ev", "bv", "vc", "dv",]\nDEFAULT_METRIC_FUNC: Callable = myFunc2\n\n\ndef myFunc2(params):\n """myFunc metric v2 with lesser steps."""\n global df # define as a global variable\n (ev, bv, vc, dv) = params\n df[\'Number\'] = (df[\'Dividend2\'] <= vc) * 1 + (df[\'EV2\'] <= dv) * 1\n df[\'Return\'] = (\n (df[\'EV\'] > ev) \n * (df[\'Probability\'] < bv) \n * (df[\'Number\'] * df[\'Dividend\'] - (vc + dv))\n )\n return -1 * (df[\'Return\'].sum())\n\n\ndef make_param_grid(\n bounds: List[Tuple[float, float]], \n param_names: Optional[List[str]]=None, \n num_points: int=10, \n as_dict: bool=True,\n ) -> Union[pd.DataFrame, Dict[str, List[float]]]:\n """\n Create parameter search space.\n\n Example:\n \n grid = make_param_grid(bounds=b1, num_points=10, as_dict=True)\n \n """\n if param_names is None:\n param_names = PARAM_NAMES # ["ev", "bv", "vc", "dv"]\n bounds = np.array(bounds)\n grid = np.linspace(start=bounds[:,0], \n stop=bounds[:,1], \n num=num_points, \n endpoint=True, \n axis=0)\n grid = pd.DataFrame(grid, columns=param_names)\n if as_dict:\n grid = grid.to_dict()\n for k,v in grid.items():\n grid.update({k: list(v.values())})\n return grid\n\n\ndef objective(trial, \n bounds: Optional[Iterable]=None, \n func: Optional[Callable]=None, \n param_names: Optional[List[str]]=None):\n """Objective function, necessary for optimizing with optuna."""\n if param_names is None:\n param_names = PARAM_NAMES\n if (bounds is None):\n bounds = ((-10, 10) for _ in param_names)\n if not isinstance(bounds, dict):\n bounds = dict((p, (min(b), max(b))) \n for p, b in zip(param_names, bounds))\n if func is None:\n func = DEFAULT_METRIC_FUNC\n\n params = dict(\n (p, trial.suggest_float(p, bounds.get(p)[0], bounds.get(p)[1])) \n for p in param_names \n )\n # x = trial.suggest_float(\'x\', -10, 10)\n return func((params[p] for p in param_names))\n\n\ndef optimize(objective: Callable, \n sampler: Optional[optuna.samplers.BaseSampler]=None, \n func: Optional[Callable]=None, \n n_trials: int=2, \n study_direction: str="minimize",\n study_name: Optional[str]=None,\n formatstr: str=".4f",\n verbose: bool=True):\n """Optimizing function using optuna: creates a study."""\n if func is None:\n func = DEFAULT_METRIC_FUNC\n study = optuna.create_study(\n direction=study_direction, \n sampler=sampler, \n study_name=study_name)\n study.optimize(\n objective, \n n_trials=n_trials, \n show_progress_bar=True, \n n_jobs=1,\n )\n if verbose:\n metric = eval_metric(study.best_params, func=myFunc2)\n msg = format_result(study.best_params, metric, \n header=study.study_name, \n format=formatstr)\n print(msg)\n return study\n\n\ndef format_dict(d: Dict[str, float], format: str=".4f") -> Dict[str, float]:\n """\n Returns formatted output for a dictionary with \n string keys and float values.\n """\n return dict((k, float(f\'{v:{format}}\')) for k,v in d.items())\n\n\ndef format_result(d: Dict[str, float], \n metric_value: float, \n header: str=\'\', \n format: str=".4f"): \n """Returns formatted result."""\n msg = f"""Study Name: {header}\\n{\'=\'*30}\n \n \xe2\x9c\x85 study.best_params: \\n\\t{format_dict(d)}\n \xe2\x9c\x85 metric: {metric_value} \n """\n return msg\n\n\ndef study_contour_plot(study: optuna.Study, \n params: Optional[List[str]]=None, \n width: int=560, \n height: int=500):\n """\n Create contour plots for a study, given a list or \n tuple of two parameter names.\n """\n if params is None:\n params = ["dv", "vc"]\n fig = optuna.visualization.plot_contour(study, params=params)\n fig.update_layout(\n title=f\'Contour Plot: {study.study_name} ({params[0]}, {params[1]})\', \n autosize=False,\n width=width, \n height=height,\n margin=dict(l=65, r=50, b=65, t=90))\n fig.show()\n\n\nbounds = [(0.2, 4), (300, 600), (0, 1000), (0, 1000)]\nparam_names = PARAM_NAMES # ["ev", "bv", "vc", "dv",]\npobjective = partial(objective, bounds=bounds)\n\n# Create an empty dict to contain \n# various subsequent studies.\nstudies = dict()\nRun Code Online (Sandbox Code Playgroud)\nOptuna 附带了几种不同类型的采样器。采样器提供了 optuna 如何从参数空间采样点并评估目标函数的策略。
\n\nTPESamplerfrom optuna.samplers import TPESampler\n\nsampler = TPESampler(seed=42)\n\nstudy_name = "TPESampler"\nstudies[study_name] = optimize(\n pobjective, \n sampler=sampler, \n n_trials=100, \n study_name=study_name,\n)\n\n# Study Name: TPESampler\n# ==============================\n# \n# \xe2\x9c\x85 study.best_params: \n# {\'ev\': 1.6233, \'bv\': 585.2143, \'vc\': 731.9939, \'dv\': 598.6585}\n# \xe2\x9c\x85 metric: -0.0 \nRun Code Online (Sandbox Code Playgroud)\nGridSamplerGridSampler需要参数搜索网格。在这里我们使用以下内容search_space。
from optuna.samplers import GridSampler\n\n# create search-space\nsearch_space = make_param_grid(bounds=bounds, num_points=10, as_dict=True)\n\nsampler = GridSampler(search_space)\n\nstudy_name = "GridSampler"\nstudies[study_name] = optimize(\n pobjective, \n sampler=sampler, \n n_trials=2000, \n study_name=study_name,\n)\n\n# Study Name: GridSampler\n# ==============================\n# \n# \xe2\x9c\x85 study.best_params: \n# {\'ev\': 0.2, \'bv\': 500.0, \'vc\': 222.2222, \'dv\': 0.0}\n# \xe2\x9c\x85 metric: -88.33333333333337 \nRun Code Online (Sandbox Code Playgroud)\nRandomSamplerfrom optuna.samplers import RandomSampler\n\nsampler = RandomSampler(seed=42)\n\nstudy_name = "RandomSampler"\nstudies[study_name] = optimize(\n pobjective, \n sampler=sampler, \n n_trials=300, \n study_name=study_name,\n)\n\n# Study Name: RandomSampler\n# ==============================\n# \n# \xe2\x9c\x85 study.best_params: \n# {\'ev\': 1.6233, \'bv\': 585.2143, \'vc\': 731.9939, \'dv\': 598.6585}\n# \xe2\x9c\x85 metric: -0.0 \nRun Code Online (Sandbox Code Playgroud)\n为了可重复性,我保留了此处使用的虚拟数据的记录。
\nimport pandas as pd\nimport numpy as np\nfrom scipy import optimize\n\ncols = {\n \'Dividend2\': [9390, 7448, 177], \n \'Probability\': [341, 376, 452], \n \'EV\': [0.53, 0.60, 0.55], \n \'Dividend\': [185, 55, 755], \n \'EV2\': [123, 139, 544],\n}\n\ndf = pd.DataFrame(cols)\n\ndef myFunc(params):\n """myFunc metric."""\n (ev, bv, vc, dv) = params\n df[\'Number\'] = np.where(df[\'Dividend2\'] <= vc, 1, 0) \\\n + np.where(df[\'EV2\'] <= dv, 1, 0)\n df[\'Return\'] = np.where(\n df[\'EV\'] <= ev, 0, np.where(\n df[\'Probability\'] >= bv, 0, df[\'Number\'] * df[\'Dividend\'] - (vc + dv)\n )\n )\n return -1 * (df[\'Return\'].sum())\n\nb1 = [(0.2,4), (300,600), (0,1000), (0,1000)]\nstart = [0.2, 600, 1000, 1000]\nresult = optimize.minimize(fun=myFunc, bounds=b1, x0=start)\nprint(result)\nRun Code Online (Sandbox Code Playgroud)\n因此,乍一看,代码执行正常并且没有抛出任何错误。它说它成功地找到了最小化解决方案。
\n fun: -0.0\n hess_inv: <4x4 LbfgsInvHessProduct with dtype=float64>\n jac: array([0., 0., 3., 3.])\n message: b\'CONVERGENCE: NORM_OF_PROJECTED_GRADIENT_<=_PGTOL\' # \n nfev: 35\n nit: 2\n status: 0\n success: True\n x: array([2.e-01, 6.e+02, 0.e+00, 0.e+00]) # \nRun Code Online (Sandbox Code Playgroud)\n仔细观察发现,解决方案(参见)与起点没有什么不同[0.2, 600, 1000, 1000]。那么,似乎什么都没发生,算法只是过早完成了?!!
现在看看message上面的内容(参见 )。如果我们对此进行谷歌搜索,您可能会找到如下内容:
概括
\n\n\n\n\n
b\'CONVERGENCE: NORM_OF_PROJECTED_GRADIENT_<=_PGTOL\'如果损失景观没有平滑变化的地形,梯度下降算法很快就会发现从一次迭代到下一次迭代,没有发生太大的变化,因此将终止进一步的搜索。此外,如果损失情况相当平坦,这可能会遇到类似的命运并提前终止。
\n
的二元求值value = 1 if x>5 else 0本质上是一个阶跃函数,它为大于或等于1的所有值进行赋值。但这会带来一个问题——平滑度的不连续性,这可能会在穿越损失景观时带来问题。x50
如果我们使用一个sigmoid函数来引入一些平滑度怎么办?
# Define sigmoid function\ndef sigmoid(x):\n """Sigmoid function."""\n return 1 / (1 + np.exp(-x))\nRun Code Online (Sandbox Code Playgroud)\n对于上面的例子,我们可以修改如下。
\n\n\n您还可以另外引入另一个因素(gamma:\xce\xb3),如下所示并尝试对其进行优化以使景观更加平滑。因此,通过控制该gamma因子,您可以使函数更加平滑并改变它变化的速度x = 5
上图是使用以下代码片段创建的。
\nimport matplotlib.pyplot as plt\n\n%matplotlib inline \n%config InlineBackend.figure_format = \'svg\' # \'svg\', \'retina\' \nplt.style.use(\'seaborn-white\')\n\ndef make_figure(figtitle: str="Sigmoid Function"):\n """Make the demo figure for using sigmoid."""\n\n x = np.arange(-20, 20.01, 0.01)\n y1 = sigmoid(x)\n y2 = sigmoid(x - 5)\n y3 = sigmoid((x - 5)/3)\n y4 = sigmoid((x - 5)/0.3)\n fig, ax = plt.subplots(figsize=(10,5))\n plt.sca(ax)\n plt.plot(x, y1, ls="-", label="$\\sigma(x)$")\n plt.plot(x, y2, ls="--", label="$\\sigma(x - 5)$")\n plt.plot(x, y3, ls="-.", label="$\\sigma((x - 5) / 3)$")\n plt.plot(x, y4, ls=":", label="$\\sigma((x - 5) / 0.3)$")\n plt.axvline(x=0, ls="-", lw=1.3, color="cyan", alpha=0.9)\n plt.axvline(x=5, ls="-", lw=1.3, color="magenta", alpha=0.9)\n plt.legend()\n plt.title(figtitle)\n plt.show()\n\nmake_figure()\nRun Code Online (Sandbox Code Playgroud)\n以下是如何应用函数平滑的示例。
\nfrom functools import partial\n\ndef sig(x, gamma: float=1.):\n return sigmoid(x/gamma)\n\ndef myFunc3(params, gamma: float=0.5):\n """myFunc metric v3 with smoother metric."""\n (ev, bv, vc, dv) = params\n _sig = partial(sig, gamma=gamma)\n df[\'Number\'] = _sig(x = -(df[\'Dividend2\'] - vc)) * 1 \\\n + _sig(x = -(df[\'EV2\'] - dv)) * 1\n df[\'Return\'] = (\n _sig(x = df[\'EV\'] - ev) \n * _sig(x = -(df[\'Probability\'] - bv))\n * _sig(x = df[\'Number\'] * df[\'Dividend\'] - (vc + dv))\n )\n return -1 * (df[\'Return\'].sum())\nRun Code Online (Sandbox Code Playgroud)\n