遗传算法与深度学习实战(18)——使用网格搜索自动超参数优化
- 0. 前言
- 1. 网格搜索
- 2. 使用网格搜索自动超参数优化
- 小结
- 系列链接
0. 前言
我们已经学习了如何使用随机搜索获得较好的超参数优化 (Hyperparameter Optimization
, HPO
) 结果,但它耗时过长,为了寻找快速且准确的自动 HPO
,需要使用更高级的技术。一种简单有效的技术是网格搜索,特别适用于参数空间较小且相对离散的情况。在本节中,我们将介绍网格搜索的基本原理,并实现网格搜索自动超参数优化。
1. 网格搜索
网格搜索 (Grid Search
) 的工作原理是将搜索区域按照网格模式划分,并系统地遍历网格中的每个单元。网格搜索在二维空间中易于进行可视化,但该技术对于任何维数的问题都是有效的。
下图展示了随机搜索和网格搜索在超参数空间中的比较,图中展示了一种可能的网格遍历模式,在每个单元格中评估学习率和中间层变量。网格搜索是一种有效的方法,可以以有条不紊且高效的方式评估一系列可能的组合。
2. 使用网格搜索自动超参数优化
在本节中,我们将修改随机搜索自动超参数优化,使用更复杂的网格搜索技术。虽然这种技术更强大和高效,但它仍受限于网格的大小,使用较大的网格单元通常会将结果限制在局部最小值或最大值,而较小的网格单元虽然可以找到全局最小值或最大值,但搜索空间也会增加。
接下来,基于随机搜索代码,实现网格搜索。代码的主要区别在于超参数对象需要跟踪一个参数网格。
(1) 首先,导入所需库,并定义相关超参数:
import numpy as np
import torch
import torch.nn as nn
from torch.autograd import Variable
import torch.nn.functional as F
import torch.optim as optim
from torch.utils.data import TensorDataset, DataLoader
from matplotlib import pyplot as plt
from matplotlib import cm
from IPython.display import clear_output
import time
import math
import types
from itertools import cycle
from sklearn.model_selection import ParameterGrid
def function(x):
return (2*x + 3*x**2 + 4*x**3 + 5*x**4 + 6*x**5 + 10)
data_min = -5
data_max = 5
data_step = .5
Xi = np.reshape(np.arange(data_min, data_max, data_step), (-1, 1))
yi = function(Xi)
inputs = Xi.shape[1]
yi = yi.reshape(-1, 1)
plt.plot(Xi, yi, 'o', color='black')
plt.plot(Xi,yi, color="red")
class Net(nn.Module):
def __init__(self, inputs, middle):
super().__init__()
self.fc1 = nn.Linear(inputs,middle)
self.fc2 = nn.Linear(middle,middle)
self.out = nn.Linear(middle,1)
def forward(self, x):
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.out(x)
return x
(2) 实现 HyperparametersGrid
类和 __init__()
函数,将输入参数的名称提取到 self.hparms
中,然后测试第一个输入是否指向一个生成器,如果为真,就使用 self.create_grid
生成一个参数网格;否则,该实例只是一个子超参数容器:
class HyperparametersGrid(object):
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
self.hparms = [d for d in self.__dict__]
self.grid = {}
self.gidx = 0
if isinstance(self.__dict__[self.hparms[0]], types.GeneratorType):
self.grid = self.create_grid()
self.grid_size = len(self.grid)
def __str__(self):
out = ""
for d in self.hparms:
ds = self.__dict__[d]
out += f"{d} = {ds} "
return out
def values(self):
vals = []
for d in self.hparms:
vals.append(self.__dict__[d])
return vals
接下来,在 self.create_grid()
函数中构建参数网格。函数首先创建一个空的网格字典 grid
,然后循环遍历超参数列表,调用超参数生成器,使用 next
返回一个值和总值数。然后再次通过生成器循环,提取每个唯一值并将其附加到行列表 row
中。之后,将行附加到网格中,最后通过将网格注入 ParameterGrid
类来完成。ParameterGrid
是 scikit-learn
中的一个辅助类,它以输入字典和值列表作为输入,并构造一个网格,其中每个单元格表示各种超参数组合。虽然我们在本示例中,使用两个超参数在二维网格上运行,但 ParameterGrid
可以处理任意维数的问题:
def create_grid(self):
grid = {}
for d in self.hparms:
v,len = next(self.__dict__[d])
row = []
for i in range(len):
v,_ = next(self.__dict__[d])
row.append(v)
grid[d] = row
grid = ParameterGrid(grid)
return grid
拥有包含所有超参数组合的内部参数网格后,更新 next
函数。reset
函数用于将索引复位为参数网格。每次调用 next
都会增加索引,并从参数网格 (self.grid
) 中提取下一个值,使用 **
操作将网格值作为输入解包到 HyperparametersGrid
的新实例中:
def reset(self):
self.gidx = 0
def next(self):
self.gidx += 1
if self.gidx > self.grid_size-1:
self.gidx = 0
return HyperparametersGrid(**self.grid[self.gidx])
(3) 使用网格超参数类还需要用于控制超参数创建的生成器。为了简单起见,我们定义了两个函数:一个用于浮点数,另一个用于整数。在每个函数内部,我们从最小值 min
到最大值 max
以步长间隔 step
创建一个名为 grid
的值数组,遍历值列表,得到一个新的值和总列表长度。有了总列表长度,就可以通过迭代生成器来创建参数网格:
def grid(min, max, step):
grid = cycle(np.arange(min, max, step))
len = (max-min) / step
for i in grid:
yield i, int(len)
def grid_int(min, max, step):
grid = cycle(range(min, max, step))
len = (max-min) / step
for i in grid:
yield i, int(len)
(4) 接下来,使用网格超参数类 HyperparametersGrid
和生成器函数来创建父 hp
对象。使用网格生成器函数。在初始化类之后,将创建一个内部参数网格,可以查询关于网格的信息,如获取组合或值的总数。然后,还可以调用父 hp
对象上的 next
来生成一对子对象。可以通过将每个超参数的值数相乘来计算网格组合的数量。在示例中,middle_layer
有 9
个值,learning_rate
有 10
个值,epochs
有 1
个值,batch_size
有 1
个值,总共有 90
个值,即 10×9×1×1=90
。当处理多个变量和较小的步长时,网格大小可能会迅速增大:
hp = HyperparametersGrid(
middle_layer = grid_int(8, 64, 6),
learning_rate = grid(3.5e-02,3.5e-01, 3e-02),
batch_size = grid_int(16, 20, 4),
epochs = grid_int(200,225,25)
)
print(hp.grid_size)
print(hp.grid.param_grid)
print(hp.next())
print(hp.next())
(5) 使用 GPU
进行训练。runs
由 hp.grid_size
定义,并创建一个名为 grid_size
的新变量,它由 runs
的数量定义,第二个变量用于定义在适应度评估图上绘制的网格单元格的大小:
cuda = True if torch.cuda.is_available() else False
print("Using CUDA" if cuda else "Not using CUDA")
Tensor = torch.cuda.FloatTensor if cuda else torch.Tensor
loss_fn = nn.MSELoss()
if cuda:
loss_fn.cuda()
def train_function(hp):
hp = hp.next()
X = np.reshape(
np.arange(
data_min,
data_max,
data_step)
, (-1, 1))
y = function(X)
inputs = X.shape[1]
tensor_x = torch.Tensor(X) # transform to torch tensor
tensor_y = torch.Tensor(y)
dataset = TensorDataset(tensor_x,tensor_y) # create your datset
dataloader = DataLoader(dataset, batch_size= hp.batch_size, shuffle=True) # create your dataloader
model = Net(inputs, hp.middle_layer)
optimizer = optim.Adam(model.parameters(), lr=hp.learning_rate)
if cuda:
model.cuda()
history=[]
start = time.time()
for i in range(hp.epochs):
for X, y in iter(dataloader):
# wrap the data in variables
x_batch = Variable(torch.Tensor(X).type(Tensor))
y_batch = Variable(torch.Tensor(y).type(Tensor))
# forward pass
y_pred = model(x_batch)
# compute and print loss
loss = loss_fn(y_pred, y_batch)
ll = loss.data
history.append(ll.item())
# reset gradients
optimizer.zero_grad()
# backwards pass
loss.backward()
# step the optimizer - update the weights
optimizer.step()
end = time.time() - start
return end, history, model, hp
best = float("inf")
span, history, model, hp_out = train_function(hp)
print(hp_out)
plt.plot(history)
print(min(history).item())
(6) 最后,输出评估图,根据计算的变量设置 grid_size
。使用六边图将适应度值自动映射为颜色,然后,根据组合的数量设置 grid_size
。在示例中,我们假设参数的网格是正方形的,但这可能并不总是准确的:
for i in range(runs):
span, history, model, hp_out = train_function(hp)
y_ = model(torch.Tensor(Xi).type(Tensor))
fitness = loss_fn(y_, torch.Tensor(yi).type(Tensor)).data.item()
run_history.append([fitness,*hp_out.values()])
if fitness < best:
best = fitness
best_hp = hp_out
clear_output()
fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(18,6))
fig.suptitle(f"Best Fitness {best} \n{best_hp}")
fig.text(0,0,f"Run {i+1}/{runs} Current Fitness {fitness} \n{hp_out}")
ax1.plot(history)
ax1.set_xlabel("iteration")
ax1.set_ylabel("loss")
ax2.plot(Xi, yi, 'o', color='black')
ax2.plot(Xi,y_.detach().cpu().numpy(), 'r')
ax2.set_xlabel("X")
ax2.set_ylabel("Y")
rh = np.array(run_history)
hexbins = ax3.hexbin(rh[:, 1], rh[:, 2], C=rh[:, 0],
bins=25, gridsize=grid_size, cmap=cm.get_cmap('gray'))
ax3.set_xlabel("middle_layer")
ax3.set_ylabel("learning_rate")
plt.show()
time.sleep(1)
下图显示了输出结果,显然比随机搜索要快得多,但不够准确。最终适应度(约 12,000
)是随机搜索中适应度的三分之一(约 57,000
)。因此,网格搜索的结果不够准确,但更快更高效。我们可以将搜索范围缩小到较小的范围,并减小步长以提高准确性。
网格搜索是一种优秀的技术,在需要系统地查看各种超参数组合时,它非常有用。然而,需要特别的是,在输出图中,最佳适应度(暗色区域)与最差适应度(浅色区域)之间相差只有两个单元。然而,我们可以看到在这个浅色区域周围有很多具有良好适应度的区域,这表明我们很可能错过了全局最小值和/或最大值。解决此问题的方法是缩小网格范围,只覆盖两到三个单元的区域,以更好地确定最佳超参数。
小结
网格搜索的优势在于其能够完全覆盖预定义的参数空间,确保找到最优解,然而随着参数空间的增大,网格搜索的计算成本会显著增加,因为它需要评估每个可能的参数组合。在本节中,我们介绍了网格搜索的基本原理,并学习了如何通过网格搜索自动超参数优化。
系列链接
遗传算法与深度学习实战(1)——进化深度学习
遗传算法与深度学习实战(2)——生命模拟及其应用
遗传算法与深度学习实战(3)——生命模拟与进化论
遗传算法与深度学习实战(4)——遗传算法(Genetic Algorithm)详解与实现
遗传算法与深度学习实战(5)——遗传算法中常用遗传算子
遗传算法与深度学习实战(6)——遗传算法框架DEAP
遗传算法与深度学习实战(7)——DEAP框架初体验
遗传算法与深度学习实战(8)——使用遗传算法解决N皇后问题
遗传算法与深度学习实战(9)——使用遗传算法解决旅行商问题
遗传算法与深度学习实战(10)——使用遗传算法重建图像
遗传算法与深度学习实战(11)——遗传编程详解与实现
遗传算法与深度学习实战(12)——粒子群优化详解与实现
遗传算法与深度学习实战(13)——协同进化详解与实现
遗传算法与深度学习实战(14)——进化策略详解与实现
遗传算法与深度学习实战(15)——差分进化详解与实现
遗传算法与深度学习实战(16)——神经网络超参数优化
遗传算法与深度学习实战(17)——使用随机搜索自动超参数优化