2022年 11月 16日

Python零基础实践随机爬山算法

      【翻译自 :  Stochastic Hill Climbing in Python from Scratch】

      【说明:Jason Brownlee PhD大神的文章个人很喜欢,所以闲暇时间里会做一点翻译和学习实践的工作,这里是相应工作的实践记录,希望能帮到有需要的人!】

        随机爬山是一种优化算法。它利用随机性作为搜索过程的一部分。 这使得该算法适用于非线性目标函数,而其他局部搜索算法不能很好地运行。它也是一种局部搜索算法,这意味着它修改了单个解决方案并搜索搜索空间的相对局部区域,直到找到局部最优值为止。 这意味着它适用于单峰优化问题或在应用全局优化算法后使用。

      在本教程中,您将发现用于函数优化的爬山优化算法完成本教程后,您将知道:

  1. 爬山是用于功能优化的随机局部搜索算法。
  2. 如何在Python中从头开始实现爬山算法。
  3. 如何应用爬山算法并检查算法结果。

 

教程概述

     本教程分为三个部分: 他们是:

  1. 爬山算法
  2. 爬山算法的实现
  3. 应用爬山算法的示例

爬山算法

     随机爬山算法是一种随机局部搜索优化算法。它以起始点作为输入和步长,步长是搜索空间内的距离。该算法将初始点作为当前最佳候选解决方案,并在提供的点的步长距离内生成一个新点。 计算生成的点,如果它等于或好于当前点,则将其视为当前点。新点的生成使用随机性,通常称为随机爬山。 这意味着该算法可以跳过响应表面的颠簸,嘈杂,不连续或欺骗性区域,作为搜索的一部分。重要的是接受具有相等评估的不同点,因为它允许算法继续探索搜索空间,例如在响应表面的平坦区域上。 限制这些所谓的“横向”移动以避免无限循环也可能是有帮助的。该过程一直持续到满足停止条件,例如最大数量的功能评估或给定数量的功能评估内没有改善为止。该算法之所以得名,是因为它会(随机地)爬到响应面的山坡上,达到局部最优值。 这并不意味着它只能用于最大化目标函数。 这只是一个名字。 实际上,通常,我们最小化功能而不是最大化它们。作为局部搜索算法,它可能会陷入局部最优状态。 然而,多次重启可以允许算法定位全局最优。步长必须足够大,以允许在搜索空间中找到更好的附近点,但步幅不能太大,以使搜索跳出包含局部最优值的区域。

爬山算法的实现

     在撰写本文时,SciPy库未提供随机爬山的实现。但是,我们可以自己实现它。首先,我们必须定义目标函数和每个输入变量到目标函数的界限。 目标函数只是一个Python函数,我们将其命名为Objective()。 边界将是一个2D数组,每个输入变量都具有一个维度,该变量定义了变量的最小值和最大值。例如,一维目标函数和界限将定义如下:

  1. # objective function
  2. def objective(x):
  3. return 0
  4. # define range for input
  5. bounds = asarray([[-5.0, 5.0]])

     接下来,我们可以生成初始解作为问题范围内的随机点,然后使用目标函数对其进行评估。

  1. # generate an initial point
  2. solution = bounds[:, 0] + rand(len(bounds)) * (bounds[:, 1] - bounds[:, 0])
  3. # evaluate the initial point
  4. solution_eval = objective(solution)

      现在我们可以遍历定义为“ n_iterations”的算法的预定义迭代次数,例如100或1,000。

  1. # run the hill climb
  2. for i in range(n_iterations):

      算法迭代的第一步是采取步骤。这需要预定义的“ step_size”参数,该参数相对于搜索空间的边界。 我们将采用高斯分布的随机步骤,其中均值是我们的当前点,标准偏差由“ step_size”定义。 这意味着大约99%的步骤将在当前点的(3 * step_size)之内。

  1. # take a step
  2. candidate = solution + randn(len(bounds)) * step_size

     我们不必采取这种方式。 您可能希望使用0到步长之间的均匀分布。 例如:

  1. # take a step
  2. candidate = solution + rand(len(bounds)) * step_size

       接下来,我们需要评估具有目标函数的新候选解决方案。

  1. # evaluate candidate point
  2. candidte_eval = objective(candidate)

       然后,我们需要检查此新点的评估结果是否等于或优于当前最佳点,如果是,则用此新点替换当前最佳点。

  1. # check if we should keep the new point
  2. if candidte_eval <= solution_eval:
  3. # store the new point
  4. solution, solution_eval = candidate, candidte_eval
  5. # report progress
  6. print('>%d f(%s) = %.5f' % (i, solution, solution_eval))

      就是这样。我们可以将此爬山算法实现为可重用函数,该函数将目标函数的名称,每个输入变量的范围,总迭代次数和步骤作为参数,并返回找到的最佳解决方案及其评估。

  1. # hill climbing local search algorithm
  2. def hillclimbing(objective, bounds, n_iterations, step_size):
  3. # generate an initial point
  4. solution = bounds[:, 0] + rand(len(bounds)) * (bounds[:, 1] - bounds[:, 0])
  5. # evaluate the initial point
  6. solution_eval = objective(solution)
  7. # run the hill climb
  8. for i in range(n_iterations):
  9. # take a step
  10. candidate = solution + randn(len(bounds)) * step_size
  11. # evaluate candidate point
  12. candidte_eval = objective(candidate)
  13. # check if we should keep the new point
  14. if candidte_eval <= solution_eval:
  15. # store the new point
  16. solution, solution_eval = candidate, candidte_eval
  17. # report progress
  18. print('>%d f(%s) = %.5f' % (i, solution, solution_eval))
  19. return [solution, solution_eval]

       现在,我们知道了如何在Python中实现爬山算法,让我们看看如何使用它来优化目标函数。

应用爬山算法的示例

      在本节中,我们将把爬山优化算法应用于目标函数。首先,让我们定义目标函数。我们将使用一个简单的一维x ^ 2目标函数,其边界为[-5,5]。下面的示例定义了函数,然后为输入值的网格创建了函数响应面的折线图,并用红线标记了f(0.0)= 0.0处的最佳值。

  1. # convex unimodal optimization function
  2. from numpy import arange
  3. from matplotlib import pyplot
  4. # objective function
  5. def objective(x):
  6. return x[0]**2.0
  7. # define range for input
  8. r_min, r_max = -5.0, 5.0
  9. # sample input range uniformly at 0.1 increments
  10. inputs = arange(r_min, r_max, 0.1)
  11. # compute targets
  12. results = [objective([x]) for x in inputs]
  13. # create a line plot of input vs result
  14. pyplot.plot(inputs, results)
  15. # define optimal input value
  16. x_optima = 0.0
  17. # draw a vertical line at the optimal input
  18. pyplot.axvline(x=x_optima, ls='--', color='red')
  19. # show the plot
  20. pyplot.show()

       运行示例将创建目标函数的折线图,并清晰地标记函数的最优值。

        接下来,我们可以将爬山算法应用于目标函数。首先,我们将播种伪随机数生成器。 通常这不是必需的,但是在这种情况下,我想确保每次运行算法时都得到相同的结果(相同的随机数序列),以便以后可以绘制结果。

  1. # seed the pseudorandom number generator
  2. seed(5)

     接下来,我们可以定义搜索的配置。在这种情况下,我们将搜索算法的1,000次迭代,并使用0.1的步长。 假设我们使用的是高斯函数来生成步长,这意味着大约99%的所有步长将位于给定点(0.1 * 3)的距离内,例如 三个标准差。

  1. n_iterations = 1000
  2. # define the maximum step size
  3. step_size = 0.1

      接下来,我们可以执行搜索并报告结果。

  1. # perform the hill climbing search
  2. best, score = hillclimbing(objective, bounds, n_iterations, step_size)
  3. print('Done!')
  4. print('f(%s) = %f' % (best, score))

      结合在一起,下面列出了完整的示例。

  1. # hill climbing search of a one-dimensional objective function
  2. from numpy import asarray
  3. from numpy.random import randn
  4. from numpy.random import rand
  5. from numpy.random import seed
  6. # objective function
  7. def objective(x):
  8. return x[0]**2.0
  9. # hill climbing local search algorithm
  10. def hillclimbing(objective, bounds, n_iterations, step_size):
  11. # generate an initial point
  12. solution = bounds[:, 0] + rand(len(bounds)) * (bounds[:, 1] - bounds[:, 0])
  13. # evaluate the initial point
  14. solution_eval = objective(solution)
  15. # run the hill climb
  16. for i in range(n_iterations):
  17. # take a step
  18. candidate = solution + randn(len(bounds)) * step_size
  19. # evaluate candidate point
  20. candidte_eval = objective(candidate)
  21. # check if we should keep the new point
  22. if candidte_eval <= solution_eval:
  23. # store the new point
  24. solution, solution_eval = candidate, candidte_eval
  25. # report progress
  26. print('>%d f(%s) = %.5f' % (i, solution, solution_eval))
  27. return [solution, solution_eval]
  28. # seed the pseudorandom number generator
  29. seed(5)
  30. # define range for input
  31. bounds = asarray([[-5.0, 5.0]])
  32. # define the total iterations
  33. n_iterations = 1000
  34. # define the maximum step size
  35. step_size = 0.1
  36. # perform the hill climbing search
  37. best, score = hillclimbing(objective, bounds, n_iterations, step_size)
  38. print('Done!')
  39. print('f(%s) = %f' % (best, score))

      运行该示例将报告搜索进度,包括每次检测到改进时的迭代次数,该函数的输入以及来自目标函数的响应。搜索结束时,找到最佳解决方案,并报告其评估结果。在这种情况下,我们可以看到在算法的1,000次迭代中有36处改进,并且该解决方案非常接近于0.0的最佳输入,其计算结果为f(0.0)= 0.0。

  1. >1 f([-2.74290923]) = 7.52355
  2. >3 f([-2.65873147]) = 7.06885
  3. >4 f([-2.52197291]) = 6.36035
  4. >5 f([-2.46450214]) = 6.07377
  5. >7 f([-2.44740961]) = 5.98981
  6. >9 f([-2.28364676]) = 5.21504
  7. >12 f([-2.19245939]) = 4.80688
  8. >14 f([-2.01001538]) = 4.04016
  9. >15 f([-1.86425287]) = 3.47544
  10. >22 f([-1.79913002]) = 3.23687
  11. >24 f([-1.57525573]) = 2.48143
  12. >25 f([-1.55047719]) = 2.40398
  13. >26 f([-1.51783757]) = 2.30383
  14. >27 f([-1.49118756]) = 2.22364
  15. >28 f([-1.45344116]) = 2.11249
  16. >30 f([-1.33055275]) = 1.77037
  17. >32 f([-1.17805016]) = 1.38780
  18. >33 f([-1.15189314]) = 1.32686
  19. >36 f([-1.03852644]) = 1.07854
  20. >37 f([-0.99135322]) = 0.98278
  21. >38 f([-0.79448984]) = 0.63121
  22. >39 f([-0.69837955]) = 0.48773
  23. >42 f([-0.69317313]) = 0.48049
  24. >46 f([-0.61801423]) = 0.38194
  25. >48 f([-0.48799625]) = 0.23814
  26. >50 f([-0.22149135]) = 0.04906
  27. >54 f([-0.20017144]) = 0.04007
  28. >57 f([-0.15994446]) = 0.02558
  29. >60 f([-0.15492485]) = 0.02400
  30. >61 f([-0.03572481]) = 0.00128
  31. >64 f([-0.03051261]) = 0.00093
  32. >66 f([-0.0074283]) = 0.00006
  33. >78 f([-0.00202357]) = 0.00000
  34. >119 f([0.00128373]) = 0.00000
  35. >120 f([-0.00040911]) = 0.00000
  36. >314 f([-0.00017051]) = 0.00000
  37. Done!
  38. f([-0.00017051]) = 0.000000

       以线图的形式查看搜索的进度可能很有趣,该线图显示了每次改进后最佳解决方案的评估变化。每当有改进时,我们就可以更新hillclimbing()来跟踪目标函数的评估,并返回此分数列表。

  1. # hill climbing local search algorithm
  2. def hillclimbing(objective, bounds, n_iterations, step_size):
  3. # generate an initial point
  4. solution = bounds[:, 0] + rand(len(bounds)) * (bounds[:, 1] - bounds[:, 0])
  5. # evaluate the initial point
  6. solution_eval = objective(solution)
  7. # run the hill climb
  8. scores = list()
  9. scores.append(solution_eval)
  10. for i in range(n_iterations):
  11. # take a step
  12. candidate = solution + randn(len(bounds)) * step_size
  13. # evaluate candidate point
  14. candidte_eval = objective(candidate)
  15. # check if we should keep the new point
  16. if candidte_eval <= solution_eval:
  17. # store the new point
  18. solution, solution_eval = candidate, candidte_eval
  19. # keep track of scores
  20. scores.append(solution_eval)
  21. # report progress
  22. print('>%d f(%s) = %.5f' % (i, solution, solution_eval))
  23. return [solution, solution_eval, scores]

      然后,我们可以创建这些分数的折线图,以查看搜索过程中发现的每个改进的目标函数的相对变化。

  1. # line plot of best scores
  2. pyplot.plot(scores, '.-')
  3. pyplot.xlabel('Improvement Number')
  4. pyplot.ylabel('Evaluation f(x)')
  5. pyplot.show()

       结合在一起,下面列出了执行搜索并绘制搜索过程中改进解决方案的目标函数得分的完整示例。

  1. # hill climbing search of a one-dimensional objective function
  2. from numpy import asarray
  3. from numpy.random import randn
  4. from numpy.random import rand
  5. from numpy.random import seed
  6. from matplotlib import pyplot
  7. # objective function
  8. def objective(x):
  9. return x[0]**2.0
  10. # hill climbing local search algorithm
  11. def hillclimbing(objective, bounds, n_iterations, step_size):
  12. # generate an initial point
  13. solution = bounds[:, 0] + rand(len(bounds)) * (bounds[:, 1] - bounds[:, 0])
  14. # evaluate the initial point
  15. solution_eval = objective(solution)
  16. # run the hill climb
  17. scores = list()
  18. scores.append(solution_eval)
  19. for i in range(n_iterations):
  20. # take a step
  21. candidate = solution + randn(len(bounds)) * step_size
  22. # evaluate candidate point
  23. candidte_eval = objective(candidate)
  24. # check if we should keep the new point
  25. if candidte_eval <= solution_eval:
  26. # store the new point
  27. solution, solution_eval = candidate, candidte_eval
  28. # keep track of scores
  29. scores.append(solution_eval)
  30. # report progress
  31. print('>%d f(%s) = %.5f' % (i, solution, solution_eval))
  32. return [solution, solution_eval, scores]
  33. # seed the pseudorandom number generator
  34. seed(5)
  35. # define range for input
  36. bounds = asarray([[-5.0, 5.0]])
  37. # define the total iterations
  38. n_iterations = 1000
  39. # define the maximum step size
  40. step_size = 0.1
  41. # perform the hill climbing search
  42. best, score, scores = hillclimbing(objective, bounds, n_iterations, step_size)
  43. print('Done!')
  44. print('f(%s) = %f' % (best, score))
  45. # line plot of best scores
  46. pyplot.plot(scores, '.-')
  47. pyplot.xlabel('Improvement Number')
  48. pyplot.ylabel('Evaluation f(x)')
  49. pyplot.show()

     运行示例将执行搜索,并像以前一样报告结果。创建一个线形图,显示在爬山搜索期间每个改进的目标函数评估。 在搜索过程中,我们可以看到目标函数评估发生了约36个变化,随着算法收敛到最优值,初始变化较大,而在搜索结束时变化很小,难以察觉。

     鉴于目标函数是一维的,因此可以像上面那样直接绘制响应面。通过将在搜索过程中找到的最佳候选解决方案绘制为响应面中的点,来回顾搜索的进度可能会很有趣。 我们期望沿着响应面到达最优点的一系列点。这可以通过首先更新hillclimbing()函数以跟踪每个最佳候选解决方案在搜索过程中的位置来实现,然后返回最佳解决方案列表来实现。

  1. # hill climbing local search algorithm
  2. def hillclimbing(objective, bounds, n_iterations, step_size):
  3. # generate an initial point
  4. solution = bounds[:, 0] + rand(len(bounds)) * (bounds[:, 1] - bounds[:, 0])
  5. # evaluate the initial point
  6. solution_eval = objective(solution)
  7. # run the hill climb
  8. solutions = list()
  9. solutions.append(solution)
  10. for i in range(n_iterations):
  11. # take a step
  12. candidate = solution + randn(len(bounds)) * step_size
  13. # evaluate candidate point
  14. candidte_eval = objective(candidate)
  15. # check if we should keep the new point
  16. if candidte_eval <= solution_eval:
  17. # store the new point
  18. solution, solution_eval = candidate, candidte_eval
  19. # keep track of solutions
  20. solutions.append(solution)
  21. # report progress
  22. print('>%d f(%s) = %.5f' % (i, solution, solution_eval))
  23. return [solution, solution_eval, solutions]

      然后,我们可以创建目标函数响应面的图,并像以前那样标记最优值。

  1. # sample input range uniformly at 0.1 increments
  2. inputs = arange(bounds[0,0], bounds[0,1], 0.1)
  3. # create a line plot of input vs result
  4. pyplot.plot(inputs, [objective([x]) for x in inputs], '--')
  5. # draw a vertical line at the optimal input
  6. pyplot.axvline(x=[0.0], ls='--', color='red')

        最后,我们可以将搜索找到的候选解的序列绘制成黑点。

  1. # plot the sample as black circles
  2. pyplot.plot(solutions, [objective(x) for x in solutions], 'o', color='black')

       结合在一起,下面列出了在目标函数的响应面上绘制改进解序列的完整示例。

  1. # hill climbing search of a one-dimensional objective function
  2. from numpy import asarray
  3. from numpy import arange
  4. from numpy.random import randn
  5. from numpy.random import rand
  6. from numpy.random import seed
  7. from matplotlib import pyplot
  8. # objective function
  9. def objective(x):
  10. return x[0]**2.0
  11. # hill climbing local search algorithm
  12. def hillclimbing(objective, bounds, n_iterations, step_size):
  13. # generate an initial point
  14. solution = bounds[:, 0] + rand(len(bounds)) * (bounds[:, 1] - bounds[:, 0])
  15. # evaluate the initial point
  16. solution_eval = objective(solution)
  17. # run the hill climb
  18. solutions = list()
  19. solutions.append(solution)
  20. for i in range(n_iterations):
  21. # take a step
  22. candidate = solution + randn(len(bounds)) * step_size
  23. # evaluate candidate point
  24. candidte_eval = objective(candidate)
  25. # check if we should keep the new point
  26. if candidte_eval <= solution_eval:
  27. # store the new point
  28. solution, solution_eval = candidate, candidte_eval
  29. # keep track of solutions
  30. solutions.append(solution)
  31. # report progress
  32. print('>%d f(%s) = %.5f' % (i, solution, solution_eval))
  33. return [solution, solution_eval, solutions]
  34. # seed the pseudorandom number generator
  35. seed(5)
  36. # define range for input
  37. bounds = asarray([[-5.0, 5.0]])
  38. # define the total iterations
  39. n_iterations = 1000
  40. # define the maximum step size
  41. step_size = 0.1
  42. # perform the hill climbing search
  43. best, score, solutions = hillclimbing(objective, bounds, n_iterations, step_size)
  44. print('Done!')
  45. print('f(%s) = %f' % (best, score))
  46. # sample input range uniformly at 0.1 increments
  47. inputs = arange(bounds[0,0], bounds[0,1], 0.1)
  48. # create a line plot of input vs result
  49. pyplot.plot(inputs, [objective([x]) for x in inputs], '--')
  50. # draw a vertical line at the optimal input
  51. pyplot.axvline(x=[0.0], ls='--', color='red')
  52. # plot the sample as black circles
  53. pyplot.plot(solutions, [objective(x) for x in solutions], 'o', color='black')
  54. pyplot.show()

      运行示例将执行爬山搜索,并像以前一样报告结果。像以前一样创建一个响应面图,显示函数的熟悉的碗形,并用垂直的红线标记函数的最佳状态。在搜索过程中找到的最佳解决方案的顺序显示为黑点,沿着碗形延伸到最佳状态。