这是机器学习的优化速成班!

85 阅读11分钟

机器学习的优化速成班。

在7天内用Python找到函数的最优值。

所有机器学习模型都涉及优化。作为一个从业者,我们为最合适的超参数或特征子集进行优化。决策树算法对分裂进行优化。神经网络对权重进行优化。最有可能的是,我们使用计算算法进行优化。

有很多方法可以进行数值优化。SciPy有很多函数可以方便地用于此。我们也可以尝试自己实现优化算法。

让我们开始吧。

Optimization for Machine Learning (7-Day Mini-Course)

这个速成班是为谁准备的?

本课程是为那些可能知道一些应用机器学习的开发者准备的。也许你已经建立了一些模型,做了一些端到端的项目,或者从流行的工具中修改了现有的示例代码来解决自己的问题。

本课程中的课程确实假定了你的一些情况,如:

  • 你知道基本的Python编程方式。
  • 你可能知道一些基本的NumPy的数组操作。
  • 你听说过梯度下降、模拟退火、BFGS或其他一些优化算法,并想加深理解。

你不需要是

  • 一个数学奇才!
  • 一个机器学习专家!

本速成班将使你从一个对机器学习略知一二的开发者变成一个能够有效和有能力应用函数优化算法的开发者。

注意:本速成班假定你有一个工作的Python 3 SciPy环境,至少安装了NumPy。如果你在环境方面需要帮助,你可以按照这里的步骤教程来做。

速成课程概述

本速成班分为七课。

你可以每天完成一课(推荐),也可以在一天内完成所有课程(硬核)。这真的取决于你有多少时间和你的热情程度。

下面是七个课程的列表,它们将使你开始学习Python中的优化并取得成效。

  • 第1课:为什么要优化?
  • 02课:网格搜索
  • 第03课:SciPy中的优化算法
  • 04课:BFGS算法
  • 05课:爬坡算法
  • 6课:模拟退火
  • 7课:梯度下降

每节课可能需要60秒或30分钟。慢慢来,以你自己的节奏完成这些课程。提出问题,甚至在下面的评论中发表结果。

这些课程可能希望你去寻找如何做事情。我会给你提示,但每节课的部分内容是迫使你学习如何去寻找关于Python中的算法和最佳工具的帮助。

第01课:为什么要优化?

在这一课中,你将发现为什么以及何时我们要进行优化。

机器学习与其他类型的软件项目不同,因为它对我们应该如何编写程序的问题不那么琐碎。编程中的一个玩具例子是写一个for循环来打印从1到100的数字。你清楚地知道你需要一个变量来计数,而且循环应该有100次的迭代来计数。机器学习中的一个玩具例子是使用神经网络进行回归,但你不知道你需要多少次迭代来训练这个模型。你可能设置得太少或太多,你没有一个规则来告诉什么是正确的数字。因此,许多人认为机器学习模型是一个黑盒子。其后果是,虽然模型有许多变量可以调整(例如超参数),但在我们测试之前,我们并不知道什么才是正确的值。

在本课中,你将发现为什么机器学习从业者应该学习优化,以提高他们的技能和能力。优化在数学中也被称为函数优化,旨在找到某些函数的最大值或最小值。对于不同性质的函数,可以采用不同的方法。

机器学习是关于开发预测模型。一个模型是否比另一个好,我们有一些评价指标来衡量一个模型在特定数据集上的表现。在这个意义上,如果我们把创建模型的参数作为输入,把模型的内部算法和相关的数据集作为常数,把从模型中评估出来的指标作为输出,那么我们就构建了一个函数。

以决策树为例。我们知道它是一棵二叉树,因为每一个中间节点都在问一个是-否的问题。这是不变的,我们不能改变它。但这棵树应该有多深,是我们可以控制的超参数。我们允许决策树使用数据中的哪些特征和多少特征是另一个参数。这些超参数的不同值将改变决策树模型,这反过来又给出了一个不同的指标,例如分类问题中k-fold交叉验证的平均精度。然后我们定义了一个函数,把超参数作为输入,把准确率作为输出。

从决策树库的角度来看,一旦你提供了超参数和训练数据,它也可以把它们当作常量,把特征的选择和每个节点的分割阈值当作输入。衡量标准在这里仍然是输出,因为决策树库的共同目标是做出最佳预测。因此,该库也定义了一个函数,但与上面提到的函数不同。

这里的函数并不意味着你需要在编程语言中明确地定义一个函数。一个概念性的就足够了。我们接下来要做的是对输入进行操作,并检查输出,直到我们发现实现了最佳输出。在机器学习的情况下,最佳可以是指

  • 最高的准确性,或精确性,或召回率
  • 最大的ROCAU
  • 分类中最大的F1分数或回归中最大的R2分数
  • 最小的错误,或对数损失

或这一行的其他东西。我们可以通过随机方法,如抽样或随机扰动来操纵输入。我们还可以假设函数具有某些属性,并尝试一连串的输入来利用这些属性。当然,我们也可以检查所有可能的输入,当我们用尽各种可能性时,我们就会知道最佳答案。

这些是我们为什么要做优化的基本知识,它是关于什么的,以及我们如何能做到这一点。你可能没有注意到,但训练一个机器学习模型就是在做优化。你也可以明确地进行优化来选择特征或微调超参数。正如你所看到的,优化在机器学习中很有用。

你的任务

在本课中,你必须找到一个机器学习模型,并列出三个优化可能被使用或可能有助于训练和使用该模型的例子。这些可能与上面的一些原因有关,也可能是你自己的个人动机。

在下一课中,你将发现如何对一个任意函数进行网格搜索。

第02课:网格搜索

在本课中,你将发现网格搜索的优化。

让我们从这个函数开始。

f*(x*,y) =x2+y2

这是一个具有二维输入*(x*,y)和一维输出的函数。我们可以做什么来找到这个函数的最小值?换句话说,对于什么xy,我们可以有最小的f**(xy)?

在不看f**(xy)是什么的情况下,我们可以先假设xy在某个有界区域内,比如说,从-5到+5。然后我们可以在这个范围内检查xy的每个组合。如果我们记住f**(x,y)的值,并记录下我们曾经看到的最小值,那么我们就可以在穷尽这个区域后找到它的最小值。在Python代码中,它是这样的。

from numpy import arange, inf

# objective function
def objective(x, y):
    return x**2.0 + y**2.0

# define range for input
r_min, r_max = -5.0, 5.0
# generate a grid sample from the domain sample = list()
step = 0.1
for x in arange(r_min, r_max+step, step):
    for y in arange(r_min, r_max+step, step):
        sample.append([x,y])
# evaluate the sample
best_eval = inf
best_x, best_y = None, None
for x,y in sample:
    eval = objective(x,y)
    if eval < best_eval:
        best_x = x
        best_y = y
        best_eval = eval
# summarize best solution
print('Best: f(%.5f,%.5f) = %.5f' % (best_x, best_y, best_eval))

这段代码从范围的下限-5扫描到上限+5,每一步的增量为0.1。这个范围对xy都是一样的,这将创造大量的*(x*,y)对的样本。这些样本是由xy在一定范围内的组合产生的。如果我们把它们的坐标画在一张图画纸上,它们就形成了一个网格,因此我们称之为网格搜索。

有了这个网格样本,我们对每个样本*(x*,y)的目标函数f**(xy)进行评估。我们对数值进行跟踪,并记住我们曾经看到的最小值。一旦我们用尽了网格上的样本,我们就会想起我们发现的最小值作为优化的结果。

你的任务

在本课中,你应该查找如何使用numpy.meshgrid()函数并重写示例代码。然后你可以尝试将目标函数替换为f**(x,y,z) =(x-y+ 1)2+z2,这是一个具有三维输入的函数。

在下一课,你将学习如何使用scipy来优化一个函数。

第03课:SciPy中的优化算法

在这一课中,你将发现如何利用SciPy来优化你的函数。

在文献中,有很多优化算法。每一种都有它的优点和缺点,而且每一种都适合于不同的情况。重复使用我们在上一课中介绍的同一个函数。

f*(x*,y) =x2+y2

我们可以利用SciPy中的一些预定义算法来寻找其最小值。最简单的可能是Nelder-Mead算法。这个算法是基于一系列的规则来决定如何探索函数的表面。在不深入了解的情况下,我们可以简单地调用SciPy并应用Nelder-Mead算法来寻找一个函数的最小值。

from scipy.optimize import minimize
from numpy.random import rand

# objective function
def objective(x):
	return x[0]**2.0 + x[1]**2.0

# define range for input
r_min, r_max = -5.0, 5.0
# define the starting point as a random sample from the domain
pt = r_min + rand(2) * (r_max - r_min)
# perform the search
result = minimize(objective, pt, method='nelder-mead')
# summarize the result
print('Status : %s' % result['message'])
print('Total Evaluations: %d' % result['nfev'])
# evaluate solution
solution = result['x']
evaluation = objective(solution)
print('Solution: f(%s) = %.5f' % (solution, evaluation))

在上面的代码中,我们需要用一个单一的向量参数来写我们的函数。因此,该函数实际上变成了

f*(x*[0],x[1]) =(x[0])2+(x[1])2

Nelder-Mead算法需要一个起点。我们在-5到+5的范围内选择一个随机点(rand(2)是numpy生成0到1之间的随机坐标对的方法)。函数minimize()返回一个OptimizeResult对象,其中包含通过键访问的结果信息。message "键提供了关于搜索成功或失败的人类可读信息,而 "nfev "键告诉我们在优化过程中进行的函数评估的数量。最重要的是 "x "键,它规定了达到最小值的输入值。

Nelder-Mead算法对凸函数效果很好,这些函数的形状是平滑的,像一个盆地。对于更复杂的函数,该算法可能停留在局部最优,但无法找到真正的全局最优。

你的任务

在这一课中,你应该用下面的代码替换上面的例子中的目标函数。

from numpy import e, pi, cos, sqrt, exp
def objective(v):
    x, y = v
    return ( -20.0 * exp(-0.2 * sqrt(0.5 * (x**2 + y**2)))
             - exp(0.5 * (cos(2 * pi C *x)+cos(2*pi*y))) + e + 20 )

这定义了Ackley函数。全局最小值是在v=[0,0]。然而,Nelder-Mead很可能找不到它,因为这个函数有许多局部最小值。试着重复你的代码几次,观察输出。每次运行程序时,你应该得到不同的输出。

在下一课中,你将学习如何使用同一个SciPy函数来应用不同的优化算法。

第04课:BFGS算法

在这一课中,你将发现如何利用SciPy来应用BFGS算法来优化你的函数。

正如我们在上一课中所看到的,我们可以利用scipy.optimize()函数,用Nelder-Meadd算法来优化一个函数。这是一种简单的 "模式搜索 "算法,不需要知道一个函数的导数。

一阶导数意味着对目标函数进行一次微分。同样,二阶导数是对一阶导数再进行一次微分。如果我们有了目标函数的二阶导数,我们就可以应用牛顿方法来寻找其最佳状态。

还有一类优化算法,可以从一阶导数中近似得出二阶导数,并使用近似值来优化目标函数。它们被称为准牛顿方法。BFGS是这一类中最著名的一种。

重新审视我们在前几节课中使用的同一个目标函数。

f*(x*,y) =x2+y2

我们可以知道,一阶导数是。

∇f = [2x, 2y]。

这是一个两个分量的向量,因为函数f**(x,y)接收一个两个分量的向量值(x,y)并返回一个标量值。

如果我们为一阶导数创建一个新函数,我们可以调用SciPy并应用BFGS算法。

from scipy.optimize import minimize
from numpy.random import rand

# objective function
def objective(x):
	return x[0]**2.0 + x[1]**2.0

# derivative of the objective function
def derivative(x):
	return [x[0] * 2, x[1] * 2]

# define range for input
r_min, r_max = -5.0, 5.0
# define the starting point as a random sample from the domain
pt = r_min + rand(2) * (r_max - r_min)
# perform the bfgs algorithm search
result = minimize(objective, pt, method='BFGS', jac=derivative)
# summarize the result
print('Status : %s' % result['message'])
print('Total Evaluations: %d' % result['nfev'])
# evaluate solution
solution = result['x']
evaluation = objective(solution)
print('Solution: f(%s) = %.5f' % (solution, evaluation))

目标函数的一阶导数以 "jac "参数提供给minim()函数。该参数是以雅各布矩阵命名的,我们就是这样称呼一个取一个向量并返回一个向量的函数的一阶导数的。BFGS算法将利用一阶导数来计算Hessian矩阵的逆(即向量函数的二阶导数),并利用它来寻找最优值。

除了BFGS,还有L-BFGS-B。它是前者的一个版本,使用较少的内存("L"),域被限定为一个区域("B")。要使用这个变体,我们只需替换方法的名称。

...
result = minimize(objective, pt, method='L-BFGS-B', jac=derivative)

你的任务

在本课中,你应该创建一个参数多得多的函数(即函数的向量参数远远超过两个分量),并观察BFGS和L-BFGS-B的性能。你是否注意到速度上的差异?这两种方法的结果有什么不同?如果你的函数不是凸的,但有很多局部最优,会怎么样?

第05课:爬坡算法

在这一课中,你将发现如何实现爬坡算法并使用它来优化你的函数。

爬坡算法的理念是,从目标函数上的一个点开始。然后我们把这个点向一个随机的方向移动一下。如果这个移动让我们找到一个更好的解决方案,我们就保留新的位置。否则我们就保持原来的位置。经过足够多的迭代,我们应该足够接近这个目标函数的最优值。这个过程之所以被命名为 "进步",是因为它就像我们在一座山上爬行,只要有机会,我们就会不断地朝任何方向上升(或下降)。

在Python中,我们可以将上述最小化的爬山算法写成一个函数。

from numpy.random import randn

def in_bounds(point, bounds):
	# enumerate all dimensions of the point
	for d in range(len(bounds)):
		# check if out of bounds for this dimension
		if point[d] < bounds[d, 0] or point[d] > bounds[d, 1]:
			return False
	return True

def hillclimbing(objective, bounds, n_iterations, step_size):
	# generate an initial point
	solution = None
	while solution is None or not in_bounds(solution, bounds):
		solution = bounds[:, 0] + rand(len(bounds)) * (bounds[:, 1] - bounds[:, 0])
	# evaluate the initial point
	solution_eval = objective(solution)
	# run the hill climb
	for i in range(n_iterations):
		# take a step
		candidate = None
		while candidate is None or not in_bounds(candidate, bounds):
			candidate = solution + randn(len(bounds)) * step_size
		# evaluate candidate point
		candidte_eval = objective(candidate)
		# check if we should keep the new point
		if candidte_eval <= solution_eval:
			# store the new point
			solution, solution_eval = candidate, candidte_eval
			# report progress
			print('>%d f(%s) = %.5f' % (i, solution, solution_eval))
	return [solution, solution_eval]

这个函数允许传递任何目标函数,只要它接受一个向量并返回一个标量值。bounds "参数应该是一个n×2维的numpy数组,其中n是目标函数所期望的向量的大小。它告诉我们应该寻找最小值的范围的下限和上限。例如,我们可以为目标函数设置如下边界,该函数期望二维向量(如上一课中的向量),向量的分量在-5到+5之间。

bounds = np.asarray([[-5.0, 5.0], [-5.0, 5.0]])

这个 "爬坡 "函数将在边界内随机挑选一个初始点,然后在迭代中测试目标函数。每当它能发现目标函数产生一个较小的值时,解决方案就会被记住,下一个要测试的点就会从它的附近产生。

你的任务

在这一课中,你应该提供你自己的目标函数(例如复制上一课的目标函数),设置 "n_iterations "和 "step_size",并应用 "Hillclimbing "函数来寻找最小值。观察该算法如何找到一个解决方案。试着用不同的 "step_size "值,比较达到接近最终解决方案所需的迭代次数。

第06课:模拟退火

在这一课中,你将发现模拟退火是如何工作的以及如何使用它。

对于非凸函数,你在前几课学到的算法可能很容易被困于局部最优,而无法找到全局最优。原因是算法的贪婪性。每当发现一个更好的解决方案,它就不会放过。因此,如果有更好的解决方案存在,但不在附近,该算法将无法找到它。

模拟退火法试图通过在探索利用之间取得平衡来改善这种行为。在开始时,当算法对要优化的函数了解不多,它更愿意探索其他的解决方案,而不是停留在找到的最佳解决方案上。在后期,随着探索的方案越来越多,找到更好的方案的机会也越来越少,算法会更倾向于留在它找到的最佳方案的附近。

下面是模拟退火法作为Python函数的实现。

from numpy.random import randn, rand

def simulated_annealing(objective, bounds, n_iterations, step_size, temp):
	# generate an initial point
	best = bounds[:, 0] + rand(len(bounds)) * (bounds[:, 1] - bounds[:, 0])
	# evaluate the initial point
	best_eval = objective(best)
	# current working solution
	curr, curr_eval = best, best_eval
	# run the algorithm
	for i in range(n_iterations):
		# take a step
		candidate = curr + randn(len(bounds)) * step_size
		# evaluate candidate point
		candidate_eval = objective(candidate)
		# check for new best solution
		if candidate_eval < best_eval:
			# store new best point
			best, best_eval = candidate, candidate_eval
			# report progress
			print('>%d f(%s) = %.5f' % (i, best, best_eval))
		# difference between candidate and current point evaluation
		diff = candidate_eval - curr_eval
		# calculate temperature for current epoch
		t = temp / float(i + 1)
		# calculate metropolis acceptance criterion
		metropolis = exp(-diff / t)
		# check if we should keep the new point
		if diff < 0 or rand() < metropolis:
			# store the new current point
			curr, curr_eval = candidate, candidate_eval
	return [best, best_eval]

与上一课中的爬山算法类似,该函数从一个随机的初始点开始。与上一课类似,该算法以 "n_iterations "数规定的循环方式运行。在每一次迭代中,都会选取当前点的一个随机邻域点,并对其进行目标函数评估。找到的最佳解决方案被保存在变量 "best "和 "best_eval "中。与爬山算法不同的是,每次迭代中的当前点 "curr "不一定是最佳解。该点是否被移到一个邻域或留下取决于一个概率,该概率与我们所做的迭代次数和邻域能有多少改进有关。由于这种随机性,我们有机会走出局部最小值,获得更好的解决方案。最后,不管我们最终的结果如何,我们总是返回模拟退火算法的迭代中所发现的最佳解决方案。

事实上,机器学习中遇到的大多数超参数调整或特征选择问题都不是凸的。因此,对于这些优化问题,模拟退火法应该比爬山法更适合。

你的任务

在本课中,你应该用上面的模拟退火代码重复你在上一课中做的练习。试着用目标函数f**(x,y) =x2+y2,这是一个凸形函数。你是否看到模拟退火或爬坡法需要更少的迭代?用第3课中介绍的Ackley函数代替目标函数。你是否看到模拟退火或爬坡法找到的最小值更小?

第07课:梯度下降

在本课中,你将发现如何实现梯度下降算法。

梯度下降算法用于训练神经网络的算法。虽然有许多变体,但所有这些变体都是基于梯度,或函数的一阶导数。其思想在于一个函数的梯度的物理意义。如果函数取一个向量并返回一个标量值,那么函数在任何一点的梯度将告诉你函数增加最快的方向。因此,如果我们的目标是找到函数的最小值,我们应该探索的方向是与梯度完全相反的。

在数学方程中,如果我们要寻找f**(x)的最小值,其中x是一个向量,而f**(x)的梯度用∇f*(x*)表示(也是一个向量),那么我们知道

xnew=x-α × ∇f*(x*)

现在让我们试着在Python中实现这一点。重用我们在第四天学到的样本目标函数及其导数,这就是梯度下降算法及其用于寻找目标函数的最小值。

from numpy import asarray
from numpy import arange
from numpy.random import rand

# objective function
def objective(x):
	return x[0]**2.0 + x[1]**2.0

# derivative of the objective function
def derivative(x):
	return asarray([x[0]*2, x[1]*2])

# gradient descent algorithm
def gradient_descent(objective, derivative, bounds, n_iter, step_size):
	# generate an initial point
	solution = bounds[:, 0] + rand(len(bounds)) * (bounds[:, 1] - bounds[:, 0])
	# run the gradient descent
	for i in range(n_iter):
		# calculate gradient
		gradient = derivative(solution)
		# take a step
		solution = solution - step_size * gradient
		# evaluate candidate point
		solution_eval = objective(solution)
		# report progress
		print('>%d f(%s) = %.5f' % (i, solution, solution_eval))
	return [solution, solution_eval]

# define range for input
bounds = asarray([[-5.0, 5.0], [-5.0, 5.0]])
# define the total iterations
n_iter = 40
# define the step size
step_size = 0.1
# perform the gradient descent search
solution, solution_eval = gradient_descent(objective, derivative, bounds, n_iter, step_size)
print("Solution: f(%s) = %.5f" % (solution, solution_eval))

这个算法不仅取决于目标函数,而且还取决于其导数。因此,它可能不适合所有类型的问题。这种算法对步长也很敏感,相对于目标函数而言,步长过大可能导致梯度下降算法无法收敛。如果发生这种情况,我们会看到进度没有向低值移动。

有几种变化可以使梯度下降算法更加稳健,例如。

  • 在这个过程中加入一个动量,这个动量不仅遵循梯度,而且部分遵循以前迭代中梯度的平均值。
  • 对向量x的每个分量的步长不同。
  • 使步长与进度相适应

你的任务

在本课中,你应该用不同的 "步长 "和 "n_iter "来运行上面的例子程序,并观察算法进展的差异。在什么 "步长 "下你会看到上述程序不收敛?然后尝试在gradient_descent()函数中添加一个新的参数β作为动量权重,现在的更新规则变成了

xnew=x-α × ∇f*(x*) - β× g

其中g是∇f*(x*)的平均值,例如在之前的5次迭代中。你认为这个优化有什么改进吗?它是一个适合使用动量的例子吗?

这就是最后一课。

结束!

花点时间,回头看看你已经走了多远。

  • 优化在应用机器学习中的重要性。
  • 如何进行网格搜索,通过穷尽所有可能的解决方案进行优化。
  • 如何使用SciPy来优化你自己的函数。
  • 如何实现优化的爬坡算法。
  • 如何使用模拟退火算法进行优化。
  • 什么是梯度下降,如何使用它,以及这种算法的一些变化。