找到变量的值以最大化Python中函数的回报

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 处。

Cyp*_*erX 3

解决方案

\n

我将使用optuna库为您尝试解决的问题类型提供解决方案。我尝试过使用scipy.optimize.minimize,看起来损失景观在大多数地方可能相当平坦,因此容差强制最小化算法(L-BFGS-B)过早停止。

\n\n

有了 optuna,事情就变得相当简单了。Optuna 仅需要一个objective函数和一个study. trials该研究向该函数发送各种objective数据,该函数反过来评估您选择的指标。

\n

myFunc2我通过主要删除调用来定义另一个度量函数np.where,因为您可以取消它们(减少步骤数)并使函数稍微快一些。

\n
# install optuna with pip\npip install -Uqq optuna\n
Run Code Online (Sandbox Code Playgroud)\n

尽管我考虑使用相当平滑的损失景观,但有时有必要可视化景观本身。部分的答案B详细阐述了可视化。但是,如果您想使用更平滑的度量函数怎么办?本节D对此进行了一些阐述。

\n

代码执行顺序应该是:

\n
    \n
  • 版块:C>> B>> B.1>> B.2>> B.3>> A.1>>A.2D
  • \n
\n

A. 建立直觉

\n

search_space如果您使用for 部分中提到的所有可能的参数值创建一个 hiplot(也称为平行坐标图)B.2,并绘制 的最低 50 个输出myFunc2,则它将如下所示:

\n

hiplot 搜索空间子集

\n

绘制所有这些点将search_space如下所示:

\n

hiplot 搜索空间已满

\n

A.1. 各种参数对的损失景观视图

\n

这些数字表明,对于四个参数中的任何两个,损失情况大部分都是平坦的(ev, bv, vc, dv)。与其他两个采样器(和)GridSampler相比,这可能是 only (强制搜索过程)做得更好的原因。请单击下面的任意图像以查看放大图像。这也可能是立即失败的原因。TPESamplerRandomSamplerscipy.optimize.minimize(method="L-BFGS-B")

\n
\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
01-dv-vc

01. dv-vc
02-dv-bv

02. dv-bv
03-dv-ev

03. dv-ev
04-bv-ev

04. bv-ev
05-CV-EV

05. cv-ev
06-vc-bv

06. vc-bv
\n
\n
# 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))\n
Run Code Online (Sandbox Code Playgroud)\n

A2。参数重要性

\n

07-参数重要性

\n
study_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()\n
Run Code Online (Sandbox Code Playgroud)\n

B、代码

\n

部分B.3.查找以下指标的最低指标-88.333

\n
    \n
  • {\'ev\': 0.2, \'bv\': 500.0, \'vc\': 222.2222, \'dv\': 0.0}
  • \n
\n
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()\n
Run Code Online (Sandbox Code Playgroud)\n

Optuna 附带了几种不同类型的采样器。采样器提供了 optuna 如何从参数空间采样点并评估目标函数的策略。

\n\n

B.1 使用TPESampler

\n
from 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 \n
Run Code Online (Sandbox Code Playgroud)\n

B.2. 使用GridSampler

\n

GridSampler需要参数搜索网格。在这里我们使用以下内容search_space

\n

搜索空间

\n
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 \n
Run Code Online (Sandbox Code Playgroud)\n

B.3. 使用RandomSampler

\n
from 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 \n
Run Code Online (Sandbox Code Playgroud)\n

C. 虚拟数据

\n

为了可重复性,我保留了此处使用的虚拟数据的记录。

\n
import 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)\n
Run Code Online (Sandbox Code Playgroud)\n

C.1. 观察

\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]) # \n
Run Code Online (Sandbox Code Playgroud)\n

仔细观察发现,解决方案(参见)与起点没有什么不同[0.2, 600, 1000, 1000]。那么,似乎什么都没发生,算法只是过早完成了?!!

\n

现在看看message上面的内容(参见 )。如果我们对此进行谷歌搜索,您可能会找到如下内容:

\n
    \n
  • 概括

    \n
    \n

    b\'CONVERGENCE: NORM_OF_PROJECTED_GRADIENT_<=_PGTOL\'

    \n

    如果损失景观没有平滑变化的地形,梯度下降算法很快就会发现从一次迭代到下一次迭代,没有发生太大的变化,因此将终止进一步的搜索。此外,如果损失情况相当平坦,这可能会遇到类似的命运并提前终止。

    \n
    \n\n
  • \n
\n

D. 使损失形势更加平稳

\n

的二元求值value = 1 if x>5 else 0本质上是一个阶跃函数,它为大于或等于1的所有值进行赋值。但这会带来一个问题——平滑度的不连续性,这可能会在穿越损失景观时带来问题。x50

\n

如果我们使用一个sigmoid函数来引入一些平滑度怎么办?

\n\n\n

\n\n

# Define sigmoid function\ndef sigmoid(x):\n    """Sigmoid function."""\n    return 1 / (1 + np.exp(-x))\n
Run Code Online (Sandbox Code Playgroud)\n

对于上面的例子,我们可以修改如下。

\n\n\n\n

\n

您还可以另外引入另一个因素(gamma:\xce\xb3),如下所示并尝试对其进行优化以使景观更加平滑。因此,通过控制该gamma因子,您可以使函数更加平滑并改变它变化的速度x = 5

\n\n\n\n

\n

sigmoid 演示

\n

上图是使用以下代码片段创建的。

\n
import 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()\n
Run Code Online (Sandbox Code Playgroud)\n

D.1. 度量平滑示例

\n

以下是如何应用函数平滑的示例。

\n
from 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())\n
Run Code Online (Sandbox Code Playgroud)\n