🌜|优化算法| 遗传算法优化神经网络超参数精简版🌛
文章目录
🌜 前言 🌛
最近学习心电信号的时候突然对于数据预处理的研究很感兴趣,想着使用一些优化算法去优化一下预处理中的超参数,经历一段时间的学习之后虽然效果甚微,但好在有了一点点心得,特此写一篇博客记录一下最近对于优化算法的研究,以及使用各类优化算法优化自己问题的具体实现,本博客以遗传算法为例优化神经网络optimizer
中的lr
和momentum
详尽描述遗传算法的应用,并且尽可能在代码实现的角度展开叙述。
🌜 优化原理介绍 🌛
🌜 遗传算法基本原理 🌛
遗传算法(Genetic Algorithm, GA)是一种基于自然选择和遗传机制的优化和搜索算法,属于进化计算的一种。遗传算法通过模拟生物进化过程中的选择、交叉和变异等机制,在复杂的搜索空间中寻找最优解或近似最优解。其基本思想是将优化问题的可能解表示为个体(通常用二进制编码),并通过模拟遗传进化的过程,不断改进解的质量。
首先遗传算法在思想上非常简单,主要借鉴了达尔文的进化论和孟德尔的遗传学说,进而应用在了机器学习的领域,它本质是一种并行、高效、全局搜索的方法,算法操作完全贯彻了适者生存的思想,通过不断地繁衍迭代使得得到的新个体比原个体更能适应环境。在这个过程中有两个比较关键的步骤分别为:交叉复制和变异。
在交叉复制的过程中,交叉复制方式多种多样,复制过程大概为以下方式:
- 轮盘赌法:讲个体适应度大小映射为概率进行复制,其中适应度更高的个体有更大的概率进行复制,且复制的份数越多。
- 精英产生精英:对适应度高的前N/4的个体进行复制,然后用这些个体把N/4的个体替换掉
替换过程也不是非要使用当前个体的复制体替换下一个个体,也可以使用随机替换
交叉:
-
按顺序交叉:两两个体之间按照先前概率进行一定顺序的交叉。
-
精英之间交叉:对于复制后的适应度高的个体进行按顺序交叉。
-
随机交叉:不按照顺序以一定先验概率的形式交叉父代。
另外交叉的时候可以不用拘泥于两两交叉,也可以三个之间进行信息交互。
变异:
- 全局变异:每个个体都发生变异。
- 局部变异:根据适应度来看,适应度低的后N/4或者N/2进行变异。(有点穷人靠变异的感觉了。。)
- 概率变异:根据设置的概率对于每个个体都发生可能存在的变异
其次想说的一点在神经网络的超参数优化中,始终觉得设置一定步长的变异比设置概率要好用一点,所以后续超参数的优化中会抛弃概率转而对每个超参数设置一定步长的变异策略
🌜 优化超参原理介绍 🌛
遗传算法优化超参数的原理是通过遗传算法寻找最优解的能力获得超参数的最优解,并且通过不断地调整这两个超参数,寻找能使模型在准确率上的最高准确率的组合。
🌜 代码实现 🌛
本节的代码实现大致分别从遗传算法代码实现和超参数优化模板两个方面基于numpy
和pytorch
框架分别阐述优化神经网络的代码。并且在超参数优化部分此处选择以神经网络优化器的lr(学习率)
和momentum(动量)
为超参数进行优化。
🌜 遗传算法实现 🌛
本案例中遗传算法的实现大概分为以下步骤:
首先是初始化种群部分,在这一部分中,种群指的是超参数的不同组合,简单举个例子,假如说我需要优化的超参数为lr(学习率)
和momentum(动量)
,如果此时我设置population_size(种群)
为10,那么我将会得到[lr,momentum]取值的十个不同数组,此时取值到的十个数组为我们的初始种群。初始化种群部分实现代码如下:
#超参数选取范围
a_min,a_max = 1e-5,1
b_min,b_max = 1e-2,2
# 初始化种群
population = np.random.rand(population_size, num) # 每个个体都是 (a,b) 的组合且都在[0,1)范围内均匀分布
population[:,0] = population[:,0] * (a_max - a_min) + a_min
population[:,1] = population[:,1] * (b_max - b_min) + b_min
在初始化种群的代码中,首先定义了两个超参数的范围,其中超参数a
代表学习率,范围为[1e-5,1],超参数b
代表动量,范围为[1e-2,2]。在初始化种群的时候首先均匀分布的取值population_size
个[lr,momentum],并且控制两个超参数的取值范围都在给定的选取范围中。此时我们可以使用print(population)
查看变量。
可以看出此时我们第一代的种群共有‘10人’,每人身上都分别分布我们已经预定义好范围的两个‘基因’(超参数)。
其次就是计算每个个体适应度,在这里我们必须需要一个计算适应度的适应度函数,本次例子中由于要优化学习率和动量两个超参数,在这里就选择测试集准确率作为适应度函数。首先来看一下最终的适应度函数evaluate_accuracy
def evaluate_accuracy(lr,momentum):
optimizer = torch.optim.SGD(model.parameters(),lr=lr,momentum=momentum)
best_acc = 0
for epoch in range(20):
train(model,optimizer)
acc = test(model)
if best_acc < acc:
best_acc = acc
return best_acc
首先定义的是超参数优化后的优化器optimizer
,下文设置每次训练循环20个epoch,取最高的一组测试集准确率,(train函数和test函数的实现将在下文优化模板处给出)。
在重新获得新个体中,大概需要两个步骤获得‘下一代新的基因’,我们先来看一下代码:
# 选择父代
parents_indices = np.random.choice(range(population_size), size=2, replace=False, p=fitness_scores / np.sum(fitness_scores))
parent1 = population[parents_indices[0]]
parent2 = population[parents_indices[1]]
# 交叉操作(取两个父代的平均值)
offspring = (parent1 + parent2) / 2
首先通过随机选择父代,选择出第一代种群中的两个基因,交叉操作即是取两个父代的平均值(当然也可以取两个父代中位数等,因不同任务而异),这步最主要的目的是让选择的父代种群的基因做信息交互。
下面是变异操作,在生物学上讲我们可以理解为变异在一定程度上可以提高遗传的多样性以及促进适应和进化,而在遗传算法中‘变异操作’更是一种关键的机制,用于引入新的基因组合以增加解空间的多样性和探索能力,本次案例中的变异操作使用最原始的随机增加一个极小值来达到变异的效果,并且摒弃了‘变异率’的概念,使用更适合神经网络的‘步长’来实现变异,即每个父代随机变异不同步长。下面是实现代码:
# 变异操作(每个个体的每个基因加上一个小的随机扰动)
mutation = np.array([np.random.randn() * step_a, np.random.randn() * step_b])
offspring += mutation
# 确保变异后的个体在合法范围内并四舍五入到一位小数
offspring[0] = np.round(np.clip(offspring[0], a_min, a_max), 4)
offspring[1] = np.round(np.clip(offspring[1], b_min, b_max), 2)
根据每个超参数的选择,来设定两个步长,并且根据不同的步长对超参数子代进行变异操作。
接下来就是对新产生的子代重新评估其适应度,直接调用适应度函数对其进行评估即可。
offspring_fitness = evaluate_fitness(offspring[0], offspring[1])
遗传算法的最后一步就是对每一代更新最优解,代码如下:
# 更新最优解
if offspring_fitness > best_fitness:
best_solution = offspring
best_fitness = offspring_fitness
print(f'第{i+1}代最优基因为[{best_solution}],此时评估的最优准确率为{best_fitness}')
遗传算法完整代码:
import numpy as np
def genetic_algorithm(population_size, num,generations,evaluate_fitness,step_a,step_b):
'''
遗传算法调参
:param population_size: 种群大小 即每一代中个体的数量
:param num: 超参数个数
:param generations: 遗传算法迭代次数,即进行优化的总迭代次数
:param evaluate_fitness: 评价指标(输入超参数、测试集、模型返回评价指标)
:param step_a: 超参数a的变异步长
:param step_b: 超参数b的变异步长
:return: 返回一组使评价指标最高的超参数
'''
best_solution = None #长度为2的数组,表示超参数(a,b)
best_fitness = -np.inf #标量,表示模型在测试集的准确率(初始值为负无穷)
#超参数选取范围
a_min,a_max = 1e-5,1
b_min,b_max = 1e-2,2
# 初始化种群
population = np.random.rand(population_size, num) # 每个个体都是 (a,b) 的组合且都在[0,1)范围内均匀分布
population[:,0] = population[:,0] * (a_max - a_min) + a_min
population[:,1] = population[:,1] * (b_max - b_min) + b_min
for i in range(generations):
# 计算每个个体的适应度
fitness_scores = [evaluate_fitness(a, b) for a, b in population]
# 选择父代
parents_indices = np.random.choice(range(population_size), size=2, replace=False, p=fitness_scores / np.sum(fitness_scores))
parent1 = population[parents_indices[0]]
parent2 = population[parents_indices[1]]
# 交叉操作(取两个父代的平均值)
offspring = (parent1 + parent2) / 2
# 变异操作(每个个体的每个基因加上一个小的随机扰动)
mutation = np.array([np.random.randn() * step_a, np.random.randn() * step_b])
offspring += mutation
# 确保变异后的个体在合法范围内并四舍五入到一位小数
offspring[0] = np.round(np.clip(offspring[0], a_min, a_max), 1)
offspring[1] = np.round(np.clip(offspring[1], b_min, b_max), 1)
# 评估新个体的适应度
offspring_fitness = evaluate_fitness(offspring[0], offspring[1])
# 更新最优解
if offspring_fitness > best_fitness:
best_solution = offspring
best_fitness = offspring_fitness
print(f'第{i+1}代最优基因为[{best_solution}],此时评估的最优准确率为{best_fitness}')
🌜 优化超参实现 🌛
由于我们是对模型的学习率和步长进行优化,另一方面也是对训练模型的优化器进行了重构,所以在优化超参中需要重新对训练函数,测试函数等进行重构。
首先是加载数据集阶段,就不详细解释了,详细解释可见:Pytroch 自写训练模板适合入门版 包含十五种经典的自己复现的一维模型 1D CNN
import torch
from torch.utils.data import DataLoader, Dataset
from torch.utils.data import random_split
from optimizer.inheritance import genetic_algorithm
from utils import *
from Model.ResNet50 import ResNet50
# 定义随机种子
seed = 1234 # 设置你想要的随机种子
torch.manual_seed(seed)
np.random.seed(seed)
# GPU
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
file_name = "R88415_dataset1250.npy"
data = np.load(f'dataset/{file_name}',allow_pickle=True)
# 划分数据集
train_len = int(len(data) * 0.7)
test_len = int(len(data)) - train_len
train_dataset, test_dataset = random_split(dataset=data, lengths=[train_len, test_len])
# 训练设备选择GPU还是CPU
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
#数据库加载
class Dataset(Dataset):
def __init__(self, data):
self.len = len(data)
self.x_data = torch.from_numpy(np.array(list(map(lambda x: x[0], data)), dtype=np.float32))
self.y_data = torch.from_numpy(np.array(list(map(lambda x: x[-1], data)))).squeeze().long()
def __getitem__(self, index):
return self.x_data[index], self.y_data[index]
def __len__(self):
return self.len
# 数据库dataloader
Train_dataset = Dataset(train_dataset)
Test_dataset = Dataset(test_dataset)
dataloader = DataLoader(Train_dataset, shuffle=True, batch_size=400)
testloader = DataLoader(Test_dataset, shuffle=True, batch_size=200)
# 模型初始化
model = ResNet50(in_channels=2,classes=125)
model.to(device)
# 损失函数选择
criterion = torch.nn.CrossEntropyLoss()
分别导入模型和数据集后,我们需要对训练模型函数重新进行更改。
def train(model,optimizer):
model.train()
for i,data in enumerate(dataloader):
datavalue,datalabel = data
datavalue,datalabel = datavalue.to(device),datalabel.to(device)
datalabel_pred = model(datavalue)
loss = criterion(datalabel_pred,datalabel)
optimizer.zero_grad()
loss.backward()
optimizer.step()
这里训练模型的参数定义为model
和optimizer
,方便后续对优化器进行更改。
测试函数也做了一定程度的更改:
#测试函数
def test(model):
model.eval()
test_correct = 0
test_total = 0
with torch.no_grad():
for i,testdata in enumerate(testloader):
testdatavalue,testdatalabel = testdata
testdatavalue,testdatalabel = testdatavalue.to(device),testdatalabel.to(device)
testdatalabel_pred = model(testdatavalue)
testprobability,testpredicted = torch.max(testdatalabel_pred.data,dim = 1)
test_total += testdatalabel_pred.size(0)
test_correct += (testpredicted == testdatalabel).sum().item()
test_acc = round(100 * test_correct / test_total,4)
return test_acc
由于最后需要使用测试集准确率做评估的依据,所以需要返回一个测试集准确率。
最后是效应评估函数:
#评估效应函数
def evaluate_accuracy(lr,momentum):
optimizer = torch.optim.SGD(model.parameters(),lr=lr,momentum=momentum)
best_acc = 0
for epoch in range(20):
train(model,optimizer)
acc = test(model)
if best_acc < acc:
best_acc = acc
return best_acc
使用每一个epoch的测试集准确率作为评估依据,迭代的次数可以根据自己需要进行更改。
完整代码:
import torch
from torch.utils.data import DataLoader, Dataset
from torch.utils.data import random_split
from optimizer.inheritance import genetic_algorithm
from utils import *
from Model.ResNet50 import ResNet50
# 定义随机种子
seed = 1234 # 设置你想要的随机种子
torch.manual_seed(seed)
np.random.seed(seed)
# GPU
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
file_name = "R88415_dataset1250.npy"
data = np.load(f'dataset/{file_name}',allow_pickle=True)
# 划分数据集
train_len = int(len(data) * 0.7)
test_len = int(len(data)) - train_len
train_dataset, test_dataset = random_split(dataset=data, lengths=[train_len, test_len])
# 训练设备选择GPU还是CPU
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
#数据库加载
class Dataset(Dataset):
def __init__(self, data):
self.len = len(data)
self.x_data = torch.from_numpy(np.array(list(map(lambda x: x[0], data)), dtype=np.float32))
self.y_data = torch.from_numpy(np.array(list(map(lambda x: x[-1], data)))).squeeze().long()
def __getitem__(self, index):
return self.x_data[index], self.y_data[index]
def __len__(self):
return self.len
# 数据库dataloader
Train_dataset = Dataset(train_dataset)
Test_dataset = Dataset(test_dataset)
dataloader = DataLoader(Train_dataset, shuffle=True, batch_size=400)
testloader = DataLoader(Test_dataset, shuffle=True, batch_size=200)
# 模型初始化
model = ResNet50(in_channels=2,classes=125)
model.to(device)
# 损失函数选择
criterion = torch.nn.CrossEntropyLoss()
#训练函数
def train(model,optimizer):
model.train()
for i,data in enumerate(dataloader):
datavalue,datalabel = data
datavalue,datalabel = datavalue.to(device),datalabel.to(device)
datalabel_pred = model(datavalue)
loss = criterion(datalabel_pred,datalabel)
optimizer.zero_grad()
loss.backward()
optimizer.step()
#测试函数
def test(model):
model.eval()
test_correct = 0
test_total = 0
with torch.no_grad():
for i,testdata in enumerate(testloader):
testdatavalue,testdatalabel = testdata
testdatavalue,testdatalabel = testdatavalue.to(device),testdatalabel.to(device)
testdatalabel_pred = model(testdatavalue)
testprobability,testpredicted = torch.max(testdatalabel_pred.data,dim = 1)
test_total += testdatalabel_pred.size(0)
test_correct += (testpredicted == testdatalabel).sum().item()
test_acc = round(100 * test_correct / test_total,4)
return test_acc
#评估效应函数
def evaluate_accuracy(lr,momentum):
optimizer = torch.optim.SGD(model.parameters(),lr=lr,momentum=momentum)
best_acc = 0
for epoch in range(20):
train(model,optimizer)
acc = test(model)
if best_acc < acc:
best_acc = acc
return best_acc
genetic_algorithm(10,2,10,evaluate_accuracy,1e-5,0.01)
🌜 结论 🌛
只是学了一段时间优化算法的浅薄见解,有不好的地方可以及时提出把。