设为首页添加收藏

您好! 欢迎来到广东某某建材科技有限公司

微博
扫码关注官方微博
微信
扫码关注官方微信
电话:400-123-4567

您的位置: 主页 > 杏鑫资讯 > 公司新闻
公司新闻

机器学习、神经网络 的 超参数优化算法

发布日期:2024-04-07 来源: 网络 阅读量(

我研一期间学了一年的遗传算法、群智能等优化算法终于有用武之地了!(指拿来写这篇文章)

简单来说,这篇文章将介绍神经网络的一些超参数优化算法,例如传统的网格搜索法、随机搜索、贝叶斯优化、超参数自适应、遗传算法、基于梯度的优化方法、自动化超参数优化库。

码字不易,如果like的话给个点赞收藏关注吧!

什么是参数超参数呢?

参数(Parameters): 参数是模型内部可以通过训练来学习的值,它们直接影响模型的预测能力。在神经网络中,参数通常是权重(weights)和偏置(biases),它们控制了模型对输入数据的转换。训练过程的目标是通过调整这些参数,使得模型在训练数据上能够最小化预测与实际标签之间的误差,从而实现更好的泛化能力。在神经网络中,参数是通过反向传播算法来更新的,该算法根据损失函数的梯度,调整参数以最小化损失。

eg:

form torchvision.models import resnet50
resnet=resnet50()
for idx,p in resnet.parameters():
  print(idx,"-> 参数为:", p)
>>> 输出模型各个层的参数,即权重和偏置这类模型里的信息,参数的优化方法就是我们设置的optimizer,例如optim.SGD等

超参数(Hyperparameters): 超参数是在模型训练之前设置的,它们不会直接在训练过程中被学习。超参数影响了模型的结构、拓扑以及训练过程的细节,从而影响了模型的学习过程和性能。不同的超参数值可能导致不同的训练结果。

一些常见的超参数包括:

  • 学习率(learning rate): 控制参数在每次迭代中更新的步长。
  • 批大小(batch size): 每次迭代中用于更新参数的样本数量。
  • 迭代次数(epochs): 完整遍历训练数据的次数。
  • 神经网络的层数和神经元数量: 影响了模型的复杂度和表达能力。
  • 正则化参数: 控制模型的过拟合程度。
  • 激活函数的选择: 影响神经元的激活方式。
  • 优化算法的选择: 如梯度下降、Adam等。
  • Dropout 比例: 用于控制在训练过程中随机失活神经元的比例。

其实换言之,可以近似理解为只要不是参数,那就是超参数。

优化算法是在机器学习、深度学习和数值优化等领域中常用的一类算法,用于找到最优解或近似最优解的问题。这些问题通常涉及最小化或最大化一个目标函数,目标函数可以是损失函数、成本函数、评价指标等。在机器学习和深度学习中,优化算法的目标通常是通过调整模型的参数来最小化训练数据上的损失函数,从而使模型能够更好地拟合数据并提高泛化性能。

其实,SGD、Adam这些都是常见的优化算法,更复杂的有群智能等优化算法,核心都是优化某一个目标函数,找到函数最小(大)的输入,即找到 argmin_{x}f(x)

在参数的优化策略中,我们根据实际问题定义了损失函数(即预测和真实结果的误差)作为优化目标,所以神经网络在一次次训练中能够降低误差,就是因为优化算法在寻找 argmin_{权重}损失函数(权重) ,即训练后找到了一个能够使得损失函数值(预测和真实结果误差值)最小时的权重,即模型。

同样,超参数也是可以优化的,因为不同的超参数配置也会影响训练模型的最终表现,我们可以找到使模型表现最好情况下的超参数。

神经网络模型内部的参数(权重、偏置)的优化算法我觉得大家应该都是相当熟悉的,因此本文主要介绍超参数的优化算法。

提到超参数优化,我们首先需要明白超参数优化的函数大多数情况是非凸或不可微的,且不同的超参数之间是离散的,因此并不能简单的用梯度下降的方式去实现。

网格搜索法是暴力穷举法,下面将举例,分别使用学习率为0.001,0.01或0.1,神经网络隐藏层维度为16,32或64进行暴力穷举。

import torch
import torch.nn as nn
import torch.optim as optim

# 定义简单的模型
class SimpleModel(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        super(SimpleModel, self).__init__()
        self.fc1 = nn.Linear(input_size, hidden_size)
        self.relu = nn.ReLU()
        self.fc2 = nn.Linear(hidden_size, output_size)
        
    def forward(self, x):
        x = self.fc1(x)
        x = self.relu(x)
        x = self.fc2(x)
        return x

# 数据和标签(示例数据)
data = torch.randn(100, 10)
labels = torch.randint(0, 2, (100,))

# 定义网格搜索算法超参数候选值列表
learning_rates = [0.001, 0.01, 0.1]
hidden_sizes = [16, 32, 64]

best_accuracy = 0.0
best_params = {}

# 遍历所有可能的超参数组合
for lr in learning_rates:
    for hidden_size in hidden_sizes:
        # 初始化模型
        model = SimpleModel(input_size=10, hidden_size=hidden_size, output_size=2)
        criterion = nn.CrossEntropyLoss()
        optimizer = optim.SGD(model.parameters(), lr=lr)
        
        # 训练模型
        for epoch in range(10):
            optimizer.zero_grad()
            outputs = model(data)
            loss = criterion(outputs, labels)
            loss.backward()
            optimizer.step()
        
        # 在验证集上评估模型性能
        # 这里假设你有一个验证集 val_data 和对应的标签 val_labels
        val_outputs = model(val_data)
        _, predicted = torch.max(val_outputs, 1)
        accuracy = (predicted == val_labels).float().mean()
        
        # 记录最佳参数组合
        if accuracy > best_accuracy:
            best_accuracy = accuracy
            best_params = {'learning_rate': lr, 'hidden_size': hidden_size}

print("Best Accuracy:", best_accuracy)
print("Best Parameters:", best_params)

虽然网格搜索法可以在一定程度上保证找到相对较好的超参数组合,但在超参数空间较大的情况下,它可能会变得非常耗时。因为它需要尝试所有可能的组合,所以其计算开销随着超参数数量的增加而指数级增加。这也是为什么在实际应用中,网格搜索法可能不适用于超参数空间非常庞大的情况。


随机搜索法其实是网格搜索法的略微变种,本质就是由遍历搜索变成随机搜索。

import torch
import torch.nn as nn
import torch.optim as optim
import random

# 定义简单的模型
class SimpleModel(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        super(SimpleModel, self).__init__()
        self.fc1 = nn.Linear(input_size, hidden_size)
        self.relu = nn.ReLU()
        self.fc2 = nn.Linear(hidden_size, output_size)
        
    def forward(self, x):
        x = self.fc1(x)
        x = self.relu(x)
        x = self.fc2(x)
        return x

# 数据和标签(示例数据)
data = torch.randn(100, 10)
labels = torch.randint(0, 2, (100,))

# 定义超参数的搜索范围
learning_rates = [0.001, 0.01, 0.1]
hidden_sizes = [16, 32, 64]

best_accuracy = 0.0
best_params = {}

# 进行随机搜索
num_trials = 20  # 随机搜索次数
for _ in range(num_trials):
    # 随机选择超参数
    learning_rate = random.choice(learning_rates)
    hidden_size = random.choice(hidden_sizes)
    
    # 初始化模型
    model = SimpleModel(input_size=10, hidden_size=hidden_size, output_size=2)
    criterion = nn.CrossEntropyLoss()
    optimizer = optim.SGD(model.parameters(), lr=learning_rate)
    
    # 训练模型
    for epoch in range(10):
        optimizer.zero_grad()
        outputs = model(data)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()
    
    # 在验证集上评估模型性能
    # 这里假设你有一个验证集 val_data 和对应的标签 val_labels
    val_outputs = model(val_data)
    _, predicted = torch.max(val_outputs, 1)
    accuracy = (predicted == val_labels).float().mean()
    
    # 更新最佳参数组合
    if accuracy > best_accuracy:
        best_accuracy = accuracy
        best_params = {'learning_rate': learning_rate, 'hidden_size': hidden_size}

print("Best Accuracy:", best_accuracy)
print("Best Parameters:", best_params)

Hyperband可以认为是随机搜索法的改进版本,它生成小版本的数据集,并为每个超参数组合分配相同的预算。在Hyperband的每次迭代中,都会消除性能不佳的超参数配置,以节省时间和资源。

import torch
import torch.nn as nn
import torch.optim as optim
import random

# 定义简单的模型
class SimpleModel(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        super(SimpleModel, self).__init__()
        self.fc1=nn.Linear(input_size, hidden_size)
        self.relu=nn.ReLU()
        self.fc2=nn.Linear(hidden_size, output_size)
        
    def forward(self, x):
        x=self.fc1(x)
        x=self.relu(x)
        x=self.fc2(x)
        return x

# 数据和标签(示例数据)
data=torch.randn(100, 10)
labels=torch.randint(0, 2, (100,))

# 定义超参数搜索空间
learning_rates=[0.001, 0.01, 0.1]
hidden_sizes=[16, 32, 64]

# Hyperband算法
max_resources=1000  # 最大训练资源(迭代次数)
num_brackets=5  # 资源分配的不同级别

for s in range(num_brackets):
    # 计算不同配置的资源分配
    resources=max_resources * (s + 1) // num_brackets
    
    # 随机采样不同的超参数配置
    configurations=[]
    for _ in range(20):  # 20个随机配置
        learning_rate=random.choice(learning_rates)
        hidden_size=random.choice(hidden_sizes)
        configurations.append((learning_rate, hidden_size))
    
    # 在每个配置上运行训练
    for lr, hs in configurations:
        model=SimpleModel(input_size=10, hidden_size=hs, output_size=2)
        criterion=nn.CrossEntropyLoss()
        optimizer=optim.SGD(model.parameters(), lr=lr)
        
        for epoch in range(resources):
            optimizer.zero_grad()
            outputs=model(data)
            loss=criterion(outputs, labels)
            loss.backward()
            optimizer.step()
        
        # 在验证集上评估模型性能
        val_outputs=model(val_data)
        _, predicted=torch.max(val_outputs, 1)
        accuracy=(predicted==val_labels).float().mean()
        
        print(f"Configuration: lr={lr}, hs={hs}, Accuracy:{accuracy:.4f}")


实际的Hyperband算法还包括更复杂的资源分配策略和对不同配置的选择。在实际会使用现有的库(如Hyperopt或Ray Tune)来更方便地实现Hyperband。


它通过构建一个目标函数的概率模型来在潜在的高性能区域进行采样,从而逐步地寻找出最优解或接近最优解的点。核心思想基于贝叶斯定理,将已知的信息(先验)与新观测的信息(数据)相结合,更新目标函数的后验分布。整个过程可以分为以下几个关键步骤:选择合适的代理模型(例如高斯过程(GP)、随机森林(RF)和树结构Parzen估计器(TPE)模型)->选择采样策略->更新代理模型->根据代理模型进行采样重复迭代。

贝叶斯优化算法

下面是一个简单的贝叶斯优化案例。这里使用随机搜索来模拟贝叶斯优化:

首先进行一些初始随机采样,然后迭代地选择下一个采样点,将其添加到采样集中。然后根据已有的采样点,选择具有最大目标函数值的点作为当前最优点,然后根据这个最优点来更新超参数搜索范围。这个简化示例只了展示其中的基本思想,贝叶斯优化涉及到很多复杂的数学计算和高斯过程建模,实际使用还是得调用库)

import random
import math

# 目标函数(示例)
def target_function(x):
    return -(x - 2) ** 2 

# 定义超参数搜索范围
x_range = (0, 5)

# 初始化初始采样点
num_initial_samples = 5
samples = [(random.uniform(*x_range), target_function(0)) for _ in range(num_initial_samples)]

# 贝叶斯优化迭代
num_iterations = 20
for _ in range(num_iterations):
    # 选择下一个采样点(随机搜索)
    next_sample = (random.uniform(*x_range), target_function(0))
    samples.append(next_sample)
    
    # 从已有采样点中找出最优的点
    best_sample = max(samples, key=lambda x: x[1])
    
    # 更新超参数搜索范围(模拟贝叶斯优化的不确定性估计)
    center = best_sample[0]
    width = 0.1  # 控制搜索范围的扩散程度
    x_range = (max(x_range[0], center - width), min(x_range[1], center + width))

# 输出最优结果
best_sample = max(samples, key=lambda x: x[1])
print("Best parameter:", best_sample[0])
print("Best value:", best_sample[1])

贝叶斯优化模型根据之前测试的超参数值的结果确定下一个超参数值,避免了许多不必要的评估。因此,与网格搜索和随机搜索相比,贝叶斯优化可以在更少的迭代中检测到最优的超参数组合。然而,由于贝叶斯优化模型顺序优化的特点,很难并行化。

实际调库使用贝叶斯的案例:

import torch
import torch.nn as nn
import torch.optim as optim
import optuna

# 定义简单的模型
class SimpleModel(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        super(SimpleModel, self).__init__()
        self.fc1 = nn.Linear(input_size, hidden_size)
        self.relu = nn.ReLU()
        self.fc2 = nn.Linear(hidden_size, output_size)
        
    def forward(self, x):
        x = self.fc1(x)
        x = self.relu(x)
        x = self.fc2(x)
        return x

# 数据和标签(示例数据)
data = torch.randn(100, 10)
labels = torch.randint(0, 2, (100,))

def objective(trial):
    # 定义超参数搜索空间
    learning_rate = trial.suggest_loguniform('learning_rate', 1e-5, 1e-1)
    hidden_size = trial.suggest_int('hidden_size', 16, 64)
    
    # 初始化模型
    model = SimpleModel(input_size=10, hidden_size=hidden_size, output_size=2)
    criterion = nn.CrossEntropyLoss()
    optimizer = optim.SGD(model.parameters(), lr=learning_rate)
    
    # 训练模型
    for epoch in range(10):
        optimizer.zero_grad()
        outputs = model(data)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()
    
    # 在验证集上评估模型性能
    # 这里假设你有一个验证集 val_data 和对应的标签 val_labels
    val_outputs = model(val_data)
    _, predicted = torch.max(val_outputs, 1)
    accuracy = (predicted == val_labels).float().mean()
    
    return -accuracy  # 负号因为 Optuna 寻找最小值

# 创建 Optuna 的 study 对象
study = optuna.create_study(direction='minimize')
study.optimize(objective, n_trials=100)  # 运行100次试验

print("Best trial:")
trial = study.best_trial
print("Value: ", -trial.value)  # 因为我们求的是负准确率,所以要取负号
print("Params: ")
for key, value in trial.params.items():
    print(f"{key}:{value}")

终于到了擅长的了。遗传算法是一种启发式优化算法,灵感来源于生物学中的进化机制。它模拟了自然界中生物进化的过程,通过模拟遗传、选择、交叉和变异等操作来优化问题的解。遗传算法通常适用于那些解空间复杂、难以直接求解的问题。以下是遗传算法的基本操作:

编码与初始化种群: 将问题的解表示为一组基因,称为个体(Individual),每个个体对应一个可能的解,即 argmin_{x}f(x) 中的 x 。通常,个体的编码采用二进制编码。初始化一个由多个个体组成的种群(Population),每个个体的基因编码随机生成。

适应度函数: 为了评估个体的质量,引入适应度函数(Fitness Function)这里的适应度函数即目标函数, argmin_{x}f(x) 中的 f(x) ,用于计算个体的适应度值。适应度值反映了个体解在问题中的表现好坏,目标是找到适应度值高的个体。

选择: 选择操作基于个体的适应度值,以概率选择高适应度个体作为父代,用于繁殖下一代。常用的选择方法包括轮盘赌选择、锦标赛选择等,其中适应度高的个体有更大的概率被选中。

交叉: 交叉操作模拟了基因的交换,用于产生下一代个体。选择的父代个体通过交叉操作生成新的子代个体,从而将两个父代个体的基因信息进行组合。

变异: 变异操作模拟了基因的突变,以保持种群的多样性。在变异操作中,个体的某些基因可能会随机改变,以引入新的解。

选择、交叉和变异是对 argmin_{x}f(x) 中的 x 进行变化(更新)的过程。

新一代种群: 通过选择、交叉和变异操作,生成新一代种群。新一代种群中的个体根据适应度函数的计算结果,可能更接近于最优的解。

收敛和终止条件: 遗传算法重复进行上述步骤,生成多代种群。如果在连续几代中没有显著的改进,算法可能会收敛。算法可以根据预定的终止条件(如达到一定代数、达到一定适应度等)来停止。

输出最优解: 遗传算法最终输出的最优解是种群中适应度值最高的个体对应的基因编码。

遗传算法的逻辑:

import random

# 目标函数(示例,求解 f(x)=-x^2 的最大值)
def objective_function(x):
    return -x**2

# 随机生成初始种群
def initialize_population(population_size, gene_length):
    return [[random.randint(0, 1) for _ in range(gene_length)] for _ in range(population_size)]

# 计算适应度(fitness)
def calculate_fitness(gene):
    x = binary_to_decimal(gene)
    return objective_function(x)

# 二进制转十进制
def binary_to_decimal(binary):
    decimal = 0
    for bit in binary:
        decimal = decimal * 2 + bit
    return decimal

# 选择操作(轮盘赌选择)
def selection(population, fitness_values):
    total_fitness = sum(fitness_values)
    probabilities = [fitness / total_fitness for fitness in fitness_values]
    selected_index = random.choices(range(len(population)), probabilities)[0]
    return population[selected_index]

# 交叉操作
def crossover(parent1, parent2):
    crossover_point = random.randint(1, len(parent1) - 1)
    child1 = parent1[:crossover_point] + parent2[crossover_point:]
    child2 = parent2[:crossover_point] + parent1[crossover_point:]
    return child1, child2

# 变异操作
def mutation(child, mutation_rate):
    for i in range(len(child)):
        if random.random() < mutation_rate:
            child[i] = 1 - child[i]  # 变异

# 遗传算法主程序
def genetic_algorithm(population_size, gene_length, generations, mutation_rate):
    population = initialize_population(population_size, gene_length)
    
    for generation in range(generations):
        fitness_values = [calculate_fitness(gene) for gene in population]
        new_population = []
        
        for _ in range(population_size // 2):
            parent1 = selection(population, fitness_values)
            parent2 = selection(population, fitness_values)
            child1, child2 = crossover(parent1, parent2)
            mutation(child1, mutation_rate)
            mutation(child2, mutation_rate)
            new_population.extend([child1, child2])
        
        population = new_population
    
    best_gene = max(population, key=calculate_fitness)
    best_x = binary_to_decimal(best_gene)
    best_value = objective_function(best_x)
    return best_x, best_value

# 参数设置
population_size = 50
gene_length = 6
generations = 50
mutation_rate = 0.1

# 运行遗传算法
best_x, best_value = genetic_algorithm(population_size, gene_length, generations, mutation_rate)

print("Best solution:")
print("x=", best_x)
print("f(x)=", best_value)

实际在模型超参数优化中的使用案例:

import torch
import torch.nn as nn
import torch.optim as optim
import random

# 数据和标签(示例数据)
data = torch.randn(100, 10)
labels = torch.randint(0, 2, (100,))

# 定义简单的模型
class SimpleModel(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        super(SimpleModel, self).__init__()
        self.fc1 = nn.Linear(input_size, hidden_size)
        self.relu = nn.ReLU()
        self.fc2 = nn.Linear(hidden_size, output_size)
        
    def forward(self, x):
        x = self.fc1(x)
        x = self.relu(x)
        x = self.fc2(x)
        return x

# 定义适应度函数
def calculate_fitness(learning_rate, hidden_size):
    model = SimpleModel(input_size=10, hidden_size=hidden_size, output_size=2)
    criterion = nn.CrossEntropyLoss()
    optimizer = optim.SGD(model.parameters(), lr=learning_rate)
    
    for epoch in range(10):
        optimizer.zero_grad()
        outputs = model(data)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()
    
    val_outputs = model(val_data)
    _, predicted = torch.max(val_outputs, 1)
    accuracy = (predicted == val_labels).float().mean()
    return accuracy.item()

# 遗传算法
population_size = 20
gene_length = 2  # 二进制编码,每个基因有2位,分别表示学习率和隐藏层大小
generations = 10
mutation_rate = 0.1

# 随机生成初始种群
def initialize_population(population_size, gene_length):
    return [[random.randint(0, 1) for _ in range(gene_length)] for _ in range(population_size)]

# 二进制转十进制
def binary_to_decimal(binary):
    decimal = 0
    for bit in binary:
        decimal = decimal * 2 + bit
    return decimal

# 遗传算法主程序
population = initialize_population(population_size, gene_length)

for generation in range(generations):
    fitness_values = [calculate_fitness(binary_to_decimal(gene[:1]), binary_to_decimal(gene[1:])) for gene in population]
    new_population = []
    
    for _ in range(population_size // 2):
        parent1 = population[fitness_values.index(max(fitness_values))]
        parent2 = population[fitness_values.index(max(fitness_values))]
        child1, child2 = crossover(parent1, parent2)
        mutation(child1, mutation_rate)
        mutation(child2, mutation_rate)
        new_population.extend([child1, child2])
    
    population = new_population

# 输出最优结果
best_gene = population[fitness_values.index(max(fitness_values))]
best_learning_rate = binary_to_decimal(best_gene[:1])
best_hidden_size = binary_to_decimal(best_gene[1:])
best_accuracy = max(fitness_values)

print("Best Learning Rate:", best_learning_rate)
print("Best Hidden Size:", best_hidden_size)
print("Best Accuracy:", best_accuracy)

说起来,yolov5中也有遗传算法搜索超参数的方案,感兴趣可以去看一下yolov5超参数优化部分。


太熟了,能默写代码的程度,我毕业论文的idea之一。以粒子群为首的群智能算法在我研究了一年后认为是完全水论文的算法,没有多少实际应用场景,因此后来研二我转方向机器学习算法了,没想到现在用上了。这里PSO只做抛砖引玉,我阅读过的群智能算法得有上百篇了,什么灰狼算法、成长鸡群算法、混合青蛙跳算法balabala....甚至某位博士期间研究元启发算法的导师也侧面透露过这个研究方向很水,所以我这里就只举一个PSO例子。

粒子群中的粒子就是 argmin_{x}f(x) 中的 x ,适应度值就是其中的 f(x) 。通过随机初始化了一堆的 x ,利用此时刻的位置去

速度更新公式:

\\begin{equation}v_{ij}(t+1)=w \\cdot v_{ij}(t) + c_1 \\cdot r_1 \\cdot (pbest_{ij}- x_{ij}(t)) + c_2 \\cdot r_2 \\cdot (gbest_{j}- x_{ij}(t)) \\end{equation}

位置更新公式:

\\begin{equation}x_{ij}(t+1)=x_{ij}(t) + v_{ij}(t+1) \\end{equation}

其中:

 \\begin{align*}v_{ij}(t) & : \	ext{粒子}i \	ext{ 在维度}j \	ext{ 上的速度}\\\\ x_{ij}(t) & : \	ext{粒子}i \	ext{ 在维度}j \	ext{ 上的位置}\\\\ pbest_{ij}& : \	ext{粒子}i \	ext{ 的局部最优位置}\\\\ gbest_{j}& : \	ext{全局最优位置}\\\\ w & : \	ext{惯性权重}\\\\ c_1, c_2 & : \	ext{加速系数}\\\\ r_1, r_2 & : \	ext{随机数}\\end{align*}

import random

# 目标函数(示例,求解 f(x)=x^2 的最小值)
def objective_function(x):
    return x**2

# 粒子群优化算法
def particle_swarm_optimization(num_particles, num_dimensions, max_iterations, c1, c2):
    # 初始化粒子位置和速度
    particles = [{'position': [random.uniform(-5, 5) for _ in range(num_dimensions)],
                  'velocity': [random.uniform(-1, 1) for _ in range(num_dimensions)],
                  'best_position': None,
                  'best_value': float('inf')} for _ in range(num_particles)]
    
    global_best_position = None
    global_best_value = float('inf')
    
    for iteration in range(max_iterations):
        for particle in particles:
            # 计算当前粒子的适应度值
            current_value = objective_function(particle['position'])
            
            # 更新局部最优值
            if current_value < particle['best_value']:
                particle['best_value'] = current_value
                particle['best_position'] = particle['position']
            
            # 更新全局最优值
            if current_value < global_best_value:
                global_best_value = current_value
                global_best_position = particle['position']
            
            # 更新粒子速度和位置
            for d in range(num_dimensions):
                r1 = random.random()
                r2 = random.random()
                particle['velocity'][d] = particle['velocity'][d] + c1 * r1 * (particle['best_position'][d] - particle['position'][d]) + c2 * r2 * (global_best_position[d] - particle['position'][d])
                particle['position'][d] = particle['position'][d] + particle['velocity'][d]
    
    return global_best_position, global_best_value

# 参数设置
num_particles = 50
num_dimensions = 1
max_iterations = 100
c1 = 2.0
c2 = 2.0

# 运行PSO算法
best_position, best_value = particle_swarm_optimization(num_particles, num_dimensions, max_iterations, c1, c2)

print("Best solution:")
print("x=", best_position)
print("f(x)=", best_value)

网格搜索 vs 随机搜索 vs Hyperband

网格搜索、随机搜索、Hyperband可以看成同一个方案的不断改进。这三种方法都支持并行执行。Hyperband平衡了模型性能和资源使用,因此比RS更高效,特别是在时间和资源有限的情况下。然而,它们独立地对待每个超参数,不考虑超参数的相关性。因此对于具有条件超参数的ML算法,如SVM、DBSCAN和逻辑回归效率还是很低的。

贝叶斯

根据代理模型的不同,贝叶斯优化(BO)模型分为BO-GP、SMAC和BO-TPE三种不同的模型。BO算法根据之前评估的结果确定下一个超参数值,减少不必要的评估,提高效率。BO-GP主要支持连续和离散超参数(四舍五入),但不支持条件超参数。而SMAC和BO-TPE都能够处理分类、离散、连续和条件超参数。SMAC在分类参数和条件参数较多或交叉验证时表现较好,而BO-GP仅在少数连续参数下表现较好。BO-TPE保留了指定的条件关系,因此BO-TPE相对于BO-GP的一个优点是它天生支持指定的条件超参数。

遗传算法 vs 粒子群

包括遗传算法和粒子群在内的元启发式算法比许多其他HPO算法更复杂,但通常在复杂的优化问题上表现良好。它们支持所有类型的超参数,并且对于大型配置空间特别有效,因为它们甚至可以在很少的迭代中获得接近最优的解决方案。

然而在实际应用中,遗传算法和粒子群算法各有优缺点。粒子群算法能够支持大规模并行化,特别适用于连续和条件HPO问题,另一方面,遗传算法是顺序执行的,很难并行化。因此,粒子群算法的执行速度通常比遗传算法快,特别是在大配置空间和大数据集的情况下。然而,适当的种群初始化对粒子群优化至关重要;否则,它可能收敛缓慢或只识别局部最优而不是全局最优。然而,适当的种群初始化对遗传算法的影响不如对粒子群算法的影响那么显著。遗传算法的另一个限制是它引入了额外的超参数,如交叉率和突变率。

PS:群智能算法基本研究的都是如何加快收敛、避免局部最优,因此这两个问题也是以粒子群为首的群智能常见困扰。

网格搜索算法: O(n^k)

随机搜索法:O(n)

Hyperband:O(n\\cdot logn)

贝叶斯优化:BO-GP: O(n^3);BO-TPE: O(n\\cdot logn)

遗传算法:O(n^2)

粒子群算法:O(n\\cdot logn)

如某些基于最近邻、聚类的和降维的算法,只需要调整一个离散超参数。对于KNN,主要的超参数是k,即考虑最近邻数量。k-means、分层聚类和EM最重要的超参数是聚类的数量。同样,对于降维算法,包括PCA和LDA,它们的基本超参数是“n components”,即要提取的特征的数量。在这种情况下,贝叶斯优化是最好的选择

一些线性模型,包括岭回归和lasso回归算法,通常只有一个重要的连续超参数需要调整。在ridge和lasso算法中,连续超参数是正则化强度alpha。在上述三种NB算法中,临界超参数也被命名为“alpha”,但它代表的是加性平滑参数。在这些ML算法中,BO-GP是最好的选择,因为它擅长优化少量连续超参数。

少量有条件的超参数

许多ML算法都有条件超参数,如SVM、LR和DBSCAN。LR有三个相关的超参数,“惩罚”、“C”和求解器类型。类似地,DBSCAN有“eps”和“最小样本”,它们必须一起调优。SVM更加复杂,因为在设置了不同的内核类型之后,还有一组单独的条件超参数需要进行调优,因此,一些不能有效优化条件超参数的HPO方法,如GS、RS、BO-GP、Hyperband等,不适用于具有条件超参数的ML模型。对于这些ML方法,如果我们在超参数之间有预定义的关系,BO-TPE是最好的选择。SMAC也是一个不错的选择,因为它在调优条件超参数方面也表现良好。也可以使用遗传算法和粒子群算法。

基于树的算法,包括DT、RF、ET和XGBoost,以及深度学习算法,如DNN、CNN、RNN,是最复杂的机器学习算法类型,因为它们有许多不同类型的超参数。

对于这些ML模型,PSO是最佳选择,因为它支持并行执行以提高效率,特别是对于通常需要大量训练时间的DL模型。也可以使用其他一些技术,如GA、BO-TPE和SMAC,但它们可能比PSO花费更多的时间,因为很难并行化这些技术。

话又说回来,在大模型的今天,显存也是一个很大的压力,这里PSO厉害是厉害在PSO本身是大规模并行计算,这也意味着你可以快,但你显卡得管够~因此实际上GA的应用也很多,例如yolov5里就自带了GA优化超参数的案例。另外PSO其实是群智能算法里最简单基础的,各期刊里有无数种群智能算法,但这些群智能算法似乎也遵循着“No Free Lunch”理论,因此选择一个合适的比最新的会更好。

平台注册入口