启发式算法

引入

        以一个著名的问题为例——旅行商问题(TSP)。

假设有一个商人要拜访N个城市,每个城市只能拜访一次,最后回到原来出发的城市,求最短路径。

        这是一个NP-hard问题,即目前来看,要求出最优解只能枚举,复杂度为O((n-1)!)。n只要稍微大一点,就会无法在正常时间内求出来。

        现在我们退一步,要求在一定时间内求出来,但不要求最优的解,只要一个相对比较优秀的解就行,这就引出了启发式算法。

启发式算法

         基于直观或经验构造的算法,在可接受的计算时间和空间条件下,给出解决优化问题的一个可行解。

        不保证找到最优解,只是在有限资源下找到还不错的解。

下面是几个经典的启发式算法。

模拟退火

        退火

        统计力学表明材料中粒子的不同结构对应于粒子的不同能量水平。

        在高温条件下,粒子的能量较高,可以自由运动和重新排列。

        在低温条件下,粒子能量较低。如果从高温开始,非常缓慢地降温(这个过程被称为退火),粒子就可以在每个温度下达到热平衡。

        当系统完全被冷却时,最终形成处于低能状态的晶体。

        从退火到模拟退火算法

        从退火过程我们体会到,在过程刚开始的时候需要大范围的跳动,随着温度降低,跳动范围变小。

        模拟退火算法思想大致为,为了不被局部最优解困住,需要以一定概率跳出当前位置,暂时接受一个不太好的解。在搜索最优解的过程中逐渐降温,初期跳出去的概率比较大,进行广泛搜索;后期跳出去的概率比较小,尽量收敛到较优解。

        步骤

  •         随机生成一个初始解X,设定一个初始温度T。
  •         在上一次解的基础上进行调整,生成新解X',并对比旧解和新解
  •                 如果新解更好了,那就接受。
  •                 如果新解更差了,那就以e^{-\frac{f(x')-f(x)}{T}}的概率接受
  •         降温并重复上述步骤,直到迭代一定的次数

        解空间

  •         解空间就是所有valid解的集合,每次生成的解必须valid
  •         旅行商问题的解空间就是1~n的全排列 

        初始解

  •         初始解可以随意选取,但比较好的初始解可以帮助算法尽快收敛
  •         可先随机生成少量几个序列,选一个比较小的;或者采用贪心算法选择初始解

        目标函数

  •         根据我们要优化的目标确定,TSP中的目标函数就是路径长度。 

        降温方法 

  •         初始温度、降温方法的选择没有固定标准,只能试。

        如何根据旧的解生成新的解

  •         这是重点,最体现创造性的地方。比如,任意选择两个标号,调换位置;任意选择两个标号,颠倒它们的中间的序列。任意选择三个标号,将前两个标号中间的序列放到第三个之后。 

        Python代码

# 本功能实现最小值的求解
from matplotlib import pyplot as plt
import numpy as np
import random
import math

plt.ion()
# 将 matplotlib 设置为交互模式,以便实时更新图形。

# 初始值设定
hi = 3  # 最大值范围。
lo = -3  # 最小值范围。
alf = 0.95  # 温度下降的速率。
T = 100  # 初始温度。


# 目标函数
def f(x):
    return 11 * np.sin(x) + 7 * np.cos(5 * x)


# 注意这里要是np.sin


# 可视化函数(开始清楚一次然后重复的画)
def visual(x):
    # 用于绘制目标函数和当前点的位置。
    # 绘制了函数的图像,并在图中标记当前点的位置。
    plt.cla()
    # 清除当前图形。
    plt.axis([lo - 1, hi + 1, -20, 20])
    # 设置坐标轴的范围。
    m = np.arange(lo, hi, 0.0001)
    # 绘制函数曲线。
    plt.plot(m, f(m))
    # 在当前点处绘制一个红色的圆点。
    plt.plot(x, f(x), marker='o', color='red', markersize='4')
    # 设置图形标题,显示当前温度。
    plt.title('temperature={}'.format(T))
    plt.pause(0.5)
    # 暂停一段时间,使图形能够更新。


# 在指定的范围内随机生成初始值。
def init():
    return random.uniform(lo, hi)


# 新解的随机产生
# 根据当前点 x 和温度 T 生成新的点 x1。
# 如果生成的点在指定范围内,返回该点;否则,在范围边界处生成点。
def new(x):
    x1 = x + T * random.uniform(-1, 1)
    if (x1 <= hi) & (x1 >= lo):
        return x1
    elif x1 < lo:
        rand = random.uniform(-1, 1)
        return rand * lo + (1 - rand) * x
    else:
        rand = random.uniform(-1, 1)
        return rand * hi + (1 - rand) * x


# p函数
# 计算接受新解的概率。
def p(x, x1):
    return math.exp(-abs(f(x) - f(x1)) / T)

# 初始化温度和当前点。
# 在温度大于阈值时,进行迭代搜索。
# 在每次迭代中,通过新解函数生成新的点,并根据 Metropolis 准则决定是否接受新点。
# 最后输出找到的最小值。
def main():
    global x
    global T
    x = init()
    while T > 0.01:
        visual(x)
        for i in range(500):
            x1 = new(x)
            if f(x1) <= f(x):
                x = x1
            else:
                if random.random() <= p(x, x1):
                    x = x1
                else:
                    continue
        T = T * alf
    print('最小值为:{}'.format(f(x)))


main()

        遗传算法

        遗传算法是一种基于自然选择原理和自然遗传机制的搜索(寻优)算法,它是模拟自然界中的生命进化机制,在人工系统中实现特定目标的优化。

        突变和基因重组是进化的原因,遗传算法是通过群体搜索技术,根据适者生存的原则逐代进化,最终得到准最优解。

        操作包括:初始群体的产生、求每一个体的适应度、根据适者生存的原则选择优良个体、被选出的优良个体两两配对,通过随机交叉其染色体的基因并随机变异某些染色体的基因生成下一代群体,按此方法使群体逐代进化,直到满足进化终止条件。

        步骤

        遗传算法的步骤如下:

  1.         产生M个初始解,构成初始种群
  2.         每对父母以一定概率生成一个新解(交配产生后代)
  3.         每个个体以一定概率发生突变(即将自己的解变变换产生新解)
  4.         父代和子代合在一起,留下M个最好的个体进入下一轮,其余淘汰(进行自然选择)
  5.         重复以上迭代,最后输出最好的个体

        参数选择 

                没有标准,试。

        如何交配产生子代

                交配方法是最能体现创新性的地方,应该尽量继承父代,但也要进行足够的调整。例如:

  •         选择父亲的一个标号,在母亲那里找到它后面的全部数字,并依序取出;
  •         把父亲标号t后面的部分接到母亲后面
  •         把母亲取出来的数字接到父亲后面

        Python代码

import numpy as np
import matplotlib.pyplot as plt
from matplotlib import cm
from mpl_toolkits.mplot3d import Axes3D

DNA_SIZE = 24
POP_SIZE = 200
CROSSOVER_RATE = 0.8
MUTATION_RATE = 0.005
N_GENERATIONS = 50
X_BOUND = [-3, 3]
Y_BOUND = [-3, 3]


# DNA_SIZE 表示每个个体的二进制表示的长度。
# POP_SIZE 是种群的大小。
# CROSSOVER_RATE 和 MUTATION_RATE 分别确定交叉和突变的概率。
# N_GENERATIONS 是迭代的代数,
# X_BOUND 和 Y_BOUND 表示变量的边界。

def F(x, y):
    return 3 * (1 - x) ** 2 * np.exp(-(x ** 2) - (y + 1) ** 2) - 10 * (x / 5 - x ** 3 - y ** 5) * np.exp(
        -x ** 2 - y ** 2) - 1 / 3 ** np.exp(-(x + 1) ** 2 - y ** 2)


# 定义了遗传算法试图优化的目标函数 F(x, y)。
# 该函数接受两个变量 x 和 y,并根据复杂的数学表达式返回一个标量值。

def plot_3d(ax):
    X = np.linspace(*X_BOUND, 100)
    Y = np.linspace(*Y_BOUND, 100)
    X, Y = np.meshgrid(X, Y)
    Z = F(X, Y)
    ax.plot_surface(X, Y, Z, rstride=1, cstride=1)
    ax.set_zlim(-10, 10)
    ax.set_xlabel('x')
    ax.set_ylabel('y')
    ax.set_zlabel('z')
    plt.show()
    plt.pause(3)

# 定义了一个用于三维绘图的函数,


# 定义了一个函数来计算种群中每个个体的适应度。适应度基于目标函数,并进行了一些调整,以确保正适应度值。
def get_fitness(pop):
    x, y = translateDNA(pop)
    pred = F(x, y)
    return (pred - np.min(pred)) + 1e-3
    # 减去最小的适应度是为了防止适应度出现负数,
    # 通过这一步fitness的范围为[0, np.max(pred)-np.min(pred)],
    # 最后在加上一个很小的数防止出现为0的适应度


def translateDNA(pop):  # pop表示种群矩阵,一行表示一个二进制编码表示的DNA,矩阵的行数为种群数目
    x_pop = pop[:, 1::2]  # 奇数列表示X
    y_pop = pop[:, ::2]  # 偶数列表示y

    # pop:(POP_SIZE,DNA_SIZE)*(DNA_SIZE,1) --> (POP_SIZE,1)
    x = x_pop.dot(2 ** np.arange(DNA_SIZE)[::-1]) / float(2 ** DNA_SIZE - 1) * (X_BOUND[1] - X_BOUND[0]) + X_BOUND[0]
    y = y_pop.dot(2 ** np.arange(DNA_SIZE)[::-1]) / float(2 ** DNA_SIZE - 1) * (Y_BOUND[1] - Y_BOUND[0]) + Y_BOUND[0]
    return x, y


# 这个函数用于将种群中个体的二进制编码表示转换为实际的变量值 x 和 y。
# x_pop 和 y_pop 分别提取了二进制编码中奇数和偶数位置的位,这是因为在二进制编码中,奇数位置表示变量 x,偶数位置表示变量 y。
# 接着,通过矩阵运算将二进制编码转换为十进制值。具体地,对每个个体的二进制编码进行加权求和,得到十进制值。
# 最后,将十进制值映射到变量的范围内,以得到实际的变量值 x 和 y。这里使用了线性映射,通过除以 float(2 ** DNA_SIZE - 1) 实现。

def crossover_and_mutation(pop, CROSSOVER_RATE=0.8):
    new_pop = []
    for father in pop:  # 遍历种群中的每一个个体,将该个体作为父亲
        child = father  # 孩子先得到父亲的全部基因(这里把一串二进制串的那些0,1称为基因)
        if np.random.rand() < CROSSOVER_RATE:  # 产生子代时不是必然发生交叉,而是以一定的概率发生交叉
            mother = pop[np.random.randint(POP_SIZE)]  # 再种群中选择另一个个体,并将该个体作为母亲
            cross_points = np.random.randint(low=0, high=DNA_SIZE * 2)  # 随机产生交叉的点
            child[cross_points:] = mother[cross_points:]  # 孩子得到位于交叉点后的母亲的基因
        mutation(child)  # 每个后代有一定的机率发生变异
        new_pop.append(child)

    return new_pop


def mutation(child, MUTATION_RATE=0.003):
    if np.random.rand() < MUTATION_RATE:  # 以MUTATION_RATE的概率进行变异
        mutate_point = np.random.randint(0, DNA_SIZE)  # 随机产生一个实数,代表要变异基因的位置
        child[mutate_point] = child[mutate_point] ^ 1  # 将变异点的二进制为反转


def select(pop, fitness):  # nature selection wrt pop's fitness
    idx = np.random.choice(np.arange(POP_SIZE), size=POP_SIZE, replace=True,
                           p=fitness / (fitness.sum()))
    return pop[idx]


def print_info(pop):
    fitness = get_fitness(pop)
    max_fitness_index = np.argmax(fitness)
    print("max_fitness:", fitness[max_fitness_index])
    x, y = translateDNA(pop)
    print("最优的基因型:", pop[max_fitness_index])
    print("(x, y):", (x[max_fitness_index], y[max_fitness_index]))


if __name__ == "__main__":
    fig = plt.figure()
    ax = Axes3D(fig)
    plt.ion()  # 将画图模式改为交互模式,程序遇到plt.show不会暂停,而是继续执行
    plot_3d(ax)

    pop = np.random.randint(2, size=(POP_SIZE, DNA_SIZE * 2))
    # 生成一个大小为 (POP_SIZE, DNA_SIZE * 2) 的随机整数矩阵 pop,表示遗传算法的种群,
    # 其中每个个体的基因由长度为 DNA_SIZE * 2 的二进制序列表示。
    for _ in range(N_GENERATIONS):  # 迭代N代
        x, y = translateDNA(pop)
        # 将二进制基因序列转换为实数向量,用于后续的目标函数计算。
        if 'sca' in locals():
            sca.remove()
        else:
            sca = None
        #  如果已经存在散点图对象 sca,则移除它。
        #  这可能是为了在每一代更新散点图时清除上一代的图形。
        sca = ax.scatter(x, y, F(x, y), c='black', marker='o')
        # 根据实数向量 (x, y) 和目标函数值 F(x, y) 在3D坐标轴上绘制散点图。
        plt.show()
        plt.pause(0.1)
        pop = np.array(crossover_and_mutation(pop, CROSSOVER_RATE))
        # 应用交叉和突变操作生成新的种群。
        fitness = get_fitness(pop)
        # 计算新种群中每个个体的适应度值。
        pop = select(pop, fitness)
        # 根据适应度值进行选择,生成新的种群。

    print_info(pop)
    print(plt.ioff())
    plot_3d(ax)

        粒子群算法

        粒子群算法(Particle Swarm Optimization,PSO)是一种基于群体智能的优化算法,灵感来自鸟群和鱼群等群体行为的观察。

        PSO模拟了鸟群或鱼群等生物体群体行为的现象。

        在算法中,候选解被称为粒子,这些粒子在搜索空间中移动,每个粒子的位置代表一个可能的解,而粒子的运动方向和速度则由其个体经验和群体经验引导。

        PSO通过评估每个粒子的适应度(或目标函数值)来指导搜索,并通过不断更新粒子的速度和位置来寻找最优解。

        PSO的基本思想可以简述为:

  •         初始化粒子群: 在搜索空间中随机生成一群粒子,并给定它们的初始位置和速度。
  •         评估适应度: 计算每个粒子的适应度,即目标函数值。
  •         更新速度和位置: 根据个体经验和群体经验,更新每个粒子的速度和位置。这个更新过程考虑了粒子个体经验中的最佳位置和整个群体中的最佳位置。
  •         重复迭代: 通过不断的迭代,粒子群在搜索空间中逐渐聚集向潜在的最优解。
  •         终止条件: 当满足预定的终止条件时,算法停止执行,返回找到的最优解或近似最优解

        Python 代码 

import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D


def fit_fun(x):  # 适应函数
    return sum(100.0 * (x[0][1:] - x[0][:-1] ** 2.0) ** 2.0 + (1 - x[0][:-1]) ** 2.0)


# 返回适应度函数的计算结果。该函数采用了罗森布洛克函数(Rosenbrock function)作为适应度函数,
# 用于优化问题。适应度函数的计算公式如上所示。

class Particle:
    # 定义类 Particle 的初始化方法,
    # 接受三个参数:x_max(粒子位置的最大值)、max_vel(粒子速度的最大值)和 dim(粒子的维度)。
    def __init__(self, x_max, max_vel, dim):
        self.__pos = np.random.uniform(-x_max, x_max, (1, dim))
        # 初始化粒子的位置,使用 NumPy 的随机数生成函数 np.random.uniform
        # 生成一个指定范围内的随机数矩阵作为粒子的初始位置。
        self.__vel = np.random.uniform(-max_vel, max_vel, (1, dim))
        # 初始化粒子的速度,使用 NumPy 的随机数生成函数 np.random.uniform
        # 生成一个指定范围内的随机数矩阵作为粒子的初始速度。
        self.__bestPos = np.zeros((1, dim))
        # 初始化粒子的最佳位置为全零矩阵。
        self.__fitnessValue = fit_fun(self.__pos)
        # 计算粒子的初始适应度函数值,调用前面定义的 fit_fun 函数。

    def set_pos(self, value):
        self.__pos = value

    # 用于设置粒子的位置。

    def get_pos(self):
        return self.__pos

    # 用于获取粒子的位置。

    def set_best_pos(self, value):
        self.__bestPos = value

    # 用于设置粒子的最佳位置。

    def get_best_pos(self):
        return self.__bestPos

    # 用于获取粒子的最佳位置。

    def set_vel(self, value):
        self.__vel = value

    # 用于设置粒子的速度。

    def get_vel(self):
        return self.__vel

    # 用于获取粒子的速度。

    def set_fitness_value(self, value):
        self.__fitnessValue = value

    # 用于设置粒子的适应度函数值。

    def get_fitness_value(self):
        return self.__fitnessValue
    # 用于获取粒子的适应度函数值。


class PSO:
    def __init__(self, dim, size, iter_num, x_max, max_vel, tol, best_fitness_value=float('Inf'), C1=2, C2=2, W=1):
        self.C1 = C1  # 个体认知因子
        self.C2 = C2  # 社会学习因子
        self.W = W  # 惯性权重
        self.dim = dim  # 粒子的维度,表示问题的参数个数。
        self.size = size  # 粒子群的大小,即粒子的数量。
        self.iter_num = iter_num  # 迭代次数,算法将运行的总次数。
        self.x_max = x_max  # 粒子位置的最大值。
        self.max_vel = max_vel  # 粒子最大速度
        self.tol = tol  # 算法的截至条件,一般是目标函数值收敛到一定程度。
        self.best_fitness_value = best_fitness_value  # 种群的最佳适应值,初始值为正无穷。
        self.best_position = np.zeros((1, dim))  # 种群最优位置
        self.fitness_val_list = []  # 存储每次迭代的最优适应值。

        # 对种群进行初始化
        self.Particle_list = [Particle(self.x_max, self.max_vel, self.dim) for i in range(self.size)]
        # 初始化粒子群,创建了size个粒子,每个粒子的位置、速度等属性由Particle类来管理,
        # 这里通过列表推导式创建了一个粒子列表。

    def set_bestFitnessValue(self, value):
        self.best_fitness_value = value

    # 设置最佳适应值的方法,允许外部代码修改最佳适应值。

    def get_bestFitnessValue(self):
        return self.best_fitness_value

    # 获取最佳适应值的方法,允许外部代码读取最佳适应值。

    def set_bestPosition(self, value):
        self.best_position = value

    # 设置最佳位置的方法,允许外部代码修改最佳位置。

    def get_bestPosition(self):
        return self.best_position

    # 获取最佳位置的方法,允许外部代码读取最佳位置。

    # 更新速度
    def update_vel(self, part):
        vel_value = self.W * part.get_vel() + self.C1 * np.random.rand() * (part.get_best_pos() - part.get_pos()) \
                    + self.C2 * np.random.rand() * (self.get_bestPosition() - part.get_pos())
        # 计算新的速度值。其中,self.W是惯性权重,self.C1和self.C2分别是个体认知因子和社会学习因子。
        # part.get_vel()获取当前粒子的速度,
        # np.random.rand()生成一个随机数,
        # part.get_best_pos()获取粒子的个体最优位置,
        # self.get_bestPosition()获取整个种群的全局最优位置。
        vel_value[vel_value > self.max_vel] = self.max_vel
        # 如果新计算的速度值超过了最大速度self.max_vel,则将其设置为最大速度。
        vel_value[vel_value < -self.max_vel] = -self.max_vel
        # 如果新计算的速度值低于最小速度(负最大速度),则将其设置为负最大速度。
        part.set_vel(vel_value)
        # 将计算得到的新速度值设置给粒子对象part。

    # 更新位置
    def update_pos(self, part):
        pos_value = part.get_pos() + part.get_vel()
        #  计算新的位置值,即当前位置加上当前速度。
        part.set_pos(pos_value)
        # 将计算得到的新位置值设置给粒子对象part。
        value = fit_fun(part.get_pos())
        # 计算当前位置的适应值,通过调用fit_fun函数来实现
        if value < part.get_fitness_value():
            # 检查新的适应值是否优于粒子的个体最优适应值。
            part.set_fitness_value(value)
            part.set_best_pos(pos_value)
        if value < self.get_bestFitnessValue():
            # 检查新的适应值是否优于整个种群的全局最优适应值。
            self.set_bestFitnessValue(value)
            self.set_bestPosition(pos_value)

    def update_ndim(self):
        # 用于执行整个PSO算法的迭代更新过程。
        for i in range(self.iter_num):
            # 遍历粒子群中的每一个粒子。
            for part in self.Particle_list:
                self.update_vel(part)  # 更新速度
                self.update_pos(part)  # 更新位置
            self.fitness_val_list.append(self.get_bestFitnessValue())
            # 每次迭代完把当前的最优适应度存到列表
            print('第{}次最佳适应值为{}'.format(i, self.get_bestFitnessValue()))
            # 打印每次迭代后的最优适应值。
            if self.get_bestFitnessValue() < self.tol:
                break
            #  检查是否达到了截至条件,如果是,则跳出循环。
        return self.fitness_val_list, self.get_bestPosition()


if __name__ == '__main__':
    # test 香蕉函数
    pso = PSO(4, 5, 10000, 30, 60, 1e-4, C1=2, C2=2, W=1)
    # 创建了PSO类的一个实例,具有特定的参数。参数包括dim(问题的维度)、
    # particle_num(粒子数量)、iter_num(迭代次数)、max_vel(最大速度)、
    # tol(容差)以及PSO算法的系数C1、C2和W。
    fit_var_list, best_pos = pso.update_ndim()
    # 调用PSO实例(pso)的update_ndim方法,并捕获返回的值。
    # fit_var_list将包含迭代过程中的适应度值,best_pos将保存找到的最佳位置。
    print("最优位置:" + str(best_pos))
    print("最优解:" + str(fit_var_list[-1]))
    plt.plot(range(len(fit_var_list)), fit_var_list, alpha=0.5)
    plt.show()

 生成的迭代过程图:(横轴表示迭代次数,纵轴表示相应的适应度值。)

启发式算法_第1张图片

 

你可能感兴趣的:(美赛,启发式算法,算法,python,数学建模)