人工智能一种现代的方法 第四章 非经典搜索 上(局部搜索)

人工智能一种现代的方法 第四章 非经典搜索 上

前言

在第三章中,环境是可观察的、确定的、已知的,问题的解是一个行动序列。本章将讨论放宽约束,本文所讲的部分是局部探索算法,考虑解的状态而不是到达该状态的路径,比如八皇后问题,不关心是怎么到目的状态的,只关心最终布局对不对,许多重要应用都有这样的性质,如作业空间调度,自动程序设计等。同时局部搜索也对最优化问题十分有用。

4.1 局部搜索

局部搜索的两大优点

  • 通常能在系统化搜索不适用的规模较大的状态空间中找到合理的解
  • 空间复杂度较低
4.1.1 爬山法

请添加图片描述

  • 求全局最大值
  • 不需要维护搜索树,当前结点只需记录当前状态和目标函数值
def hill_climbing(initial_state, generate_neighbors, evaluate):
    current = initial_state
    
    while True:
        neighbors = generate_neighbors(current)
        best_neighbor = max(neighbors, key=lambda neighbor: evaluate(neighbor))
        
        if evaluate(best_neighbor) >= evaluate(current):
            return current  # 如果最好的邻居状态比当前状态更好,则返回当前状态作为结果
        
        current = best_neighbor  # 更新当前状态为最好的邻居状态

八皇后问题
将八皇后转化为良定义问题

  • 状态:棋盘上的一个格局,表示棋盘上放置了8个皇后,且每列仅1个皇后
  • 动作:某个皇后移动到该列的其它7个位置之一
  • 启发式函数:互相攻击的皇后对数求全局最小值,0表示没有互相攻击的皇后,找到解

可以看出来与经典不同的是,局部搜索一般要有评估值来衡量搜索状态的优劣。评估值用于比较不同搜索状态之间的好坏,并指导搜索过程朝着更优的方向前进。


def generate_neighbors(state)://动作转移
    neighbors = []
    
    for i in range(8):  # 遍历每个皇后所在的行
        for j in range(8):  # 遍历每个列
            if j != state[i]:  # 如果当前位置与原始状态中皇后所在的列不同
                neighbor = state.copy()  # 创建邻居状态,为原始状态的副本
                neighbor[i] = j  # 将邻居状态中第i行的皇后移动到当前位置j
                neighbors.append(neighbor)  # 将邻居状态添加到邻居状态列表
    
    return neighbors

def evaluate(state):
    attacks = 0
    
    for i in range(8)://从第一个皇后开始遍历
        for j in range(i+1, 8)://后面的皇后与前面的皇后相对比
            if state[i] == state[j] or abs(state[i] - state[j]) == abs(i - j): // 判断行 和 对角线是否攻击
                attacks += 1
    
    return attacks

爬山的缺点

  • 局部最大值
  • 山脊:单个局部极大值
  • 高原/平原
    连续局部极大值,发生侧向移动
    侧向移动:最佳邻近结点与当前结点的目标函数值相同
    山肩,不是全局最大值
4.1.2 爬山法变形

随机爬山法

  • 思想:随机选择邻近状态(概率与陡峭程度有关)
  • 首选爬山法:随机生成邻近状态,直到生成一个优于当前状态的邻近状态
def hill_climbing():
    current_state = 0  # 初始状态
    current_value = evaluate_state(current_state)  # 计算初始状态的优劣程度

    while True:
        neighbour = generate_neighbour(current_state)  # 生成邻近状态
        neighbour_value = evaluate_state(neighbour)  # 计算邻近状态的优劣程度

        if neighbour_value > current_value:
            # 找到更好的邻近状态,更新当前状态
            current_state = neighbour
            current_value = neighbour_value
        else:
            # 没有找到更好的邻近状态,结束搜索
            break

    return current_state

首选爬山法旅行商问题

  • 状态x:当前路径
    初始状态x0:随机选择的任意路径
    邻近状态xn:选择当前路径中任意相邻两个城市交换访问次序得到的新路径
    邻域P:当前路径的邻近状态集
  • 评估函数h(x):当前路径的距离,若两城市无路径,设其距离为+∞
  • 动作:当前路径中任意相邻两个城市交换访问次序
import random
import math
# 一家之言,无论是经典还是非经典有两个重要的点,一是状态如何定义,二是状态如何转移
def generate_neighbour(current_path):
    # 生成邻近状态(交换两个相邻城市的访问次序)
    neighbours = []
    for i in range(len(current_path)-1):
        for j in range(i+1, len(current_path)):
            neighbour = current_path.copy()
            neighbour[i], neighbour[j] = neighbour[j], neighbour[i]  # 交换两个城市的访问次序
            neighbours.append(neighbour)
    return neighbours

def evaluate_path(path, distances):
    # 计算路径的总距离
    total_distance = 0
    for i in range(len(path)-1):
        current_city = path[i]
        next_city = path[i+1]
        distance = distances[current_city][next_city]
        total_distance += distance
    return total_distance

def hill_climbing(distances, num_cities):
    current_path = random.sample(range(num_cities), num_cities)  # 随机生成初始路径
    current_distance = evaluate_path(current_path, distances)  # 计算初始路径的总距离

    while True:
        neighbours = generate_neighbour(current_path)  # 生成所有邻近状态
        best_neighbour = min(neighbours, key=lambda x: evaluate_path(x, distances))  # 找到最好的邻近状态
        best_distance = evaluate_path(best_neighbour, distances)  # 最好邻近状态的总距离

        if best_distance <= current_distance:
            # 找到更好的邻近状态,更新当前路径和总距离
            current_path = best_neighbour
            current_distance = best_distance
        else:
            # 没有找到更好的邻近状态,结束搜索
            break

    return current_path

随机重启爬山法算法

随机生成初始状态

def random_restart(distances, num_cities, num_restarts):
    best_path = None
    best_distance = math.inf

    for _ in range(num_restarts):
        current_path, current_distance = hill_climbing(distances, num_cities)
        if current_distance < best_distance:
           
            best_path = current_path
            best_distance = current_distance

    return best_path

4.1.3模拟退火搜索
  • 爬山法不完备,因为会碰到局部极大值
  • 随机走是完备的(因为完全等概率选择后续节点,不受局部极大值影响),但是效率极低。

模拟退火算法:把爬山法和随机行走以某种方式结合,同时得到效率和完备性。

def acceptance_probability(current_energy, new_energy, temperature):
    # 计算接受新解的概率
    if new_energy < current_energy:
        return 1.0
    return math.exp((current_energy - new_energy) / temperature)

def simulated_annealing(initial_solution, evaluate_energy, generate_neighbour, initial_temperature, cooling_rate):
    current_solution = initial_solution
    current_energy = evaluate_energy(current_solution)
    best_solution = current_solution
    best_energy = current_energy
    temperature = initial_temperature

    while temperature > 0.1:
        neighbour = generate_neighbour(current_solution)  # 生成邻近解
        new_energy = evaluate_energy(neighbour)  # 计算邻近解的能量

        if acceptance_probability(current_energy, new_energy, temperature) > random.random():
            # 根据概率接受新解
            current_solution = neighbour
            current_energy = new_energy

        if new_energy < best_energy:
            # 更新最优解
            best_solution = neighbour
            best_energy = new_energy

        temperature *= cooling_rate  # 降低温度

    return best_solution
4.1.4 局部束搜索

局部束搜索

  • 只记录k个状态
  • 从k个随机状态开始,每步生成k个状态的所有后继状态,如果存在目标状态则结束,否则只选取k个最佳的后继状态,重复上述过程
  • 与k个局部搜索并行执行不同

局部束搜索的变体——随机束搜索
随机选取k个后继状态,状态的选择概率与状态的目标函数值一致

4.1.5 遗传算法

请添加图片描述

  • 随机束搜索的变体——遗传算法
  • 通过两个状态的结合生成后继状态
  • 种群:目前的所有状态,从k个随机状态开始
  • 个体:每个状态,通常用长度有限的字符串表示


def select_parents(population, num_parents):
    # 选择父代
    parents = []
    for _ in range(num_parents):
        parent = random.choice(population)
        parents.append(parent)
    return parents
    
def crossover(parents, population_size):
    # 交叉操作
    offspring = []
    while len(offspring) < population_size:
        parent1, parent2 = random.sample(parents, 2)
        crossover_point = random.randint(1, len(parent1)-1)
        child1 = parent1[:crossover_point] + parent2[crossover_point:]
        child2 = parent2[:crossover_point] + parent1[crossover_point:]
        offspring.extend([child1, child2])
    return offspring[:population_size]

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

def genetic_algorithm(population_size, chromosome_length, num_generations, mutation_rate):
    population = initialize_population(population_size, chromosome_length)

    for _ in range(num_generations):
        # 计算适应度
        fitness_values = [evaluate_fitness(chromosome) for chromosome in population]

        # 选择父代
        parents = select_parents(population, population_size // 2)

        # 交叉操作
        offspring = crossover(parents, population_size)

        # 变异操作
        mutated_offspring = [mutate(chromosome, mutation_rate) for chromosome in offspring]

        # 更新种群
        population = mutated_offspring

    # 返回最优解
    best_chromosome = max(population, key=evaluate_fitness)
    return best_chromosome

4.2 连续空间的局部搜索

在讨论过的算法里面,除了首选爬山法和模拟退火,没有一个能够处理连续的状态和动作空间,因为连续空间的分支因子是无限的。 本部分将介绍在连续状态空间寻找最优解的技术

4.2.1 梯度下降

梯度下降的核心思想是通过迭代地更新参数,沿着目标函数的梯度方向逐步接近最优解。梯度表示了函数在当前参数值处的变化率,因此沿着梯度的反方向更新参数可以使目标函数的值减小。通过不断迭代,梯度下降算法可以找到目标函数的局部最小值或全局最小值。

梯度下降以线性回归为例

# 初始化参数
theta = [0, 0]  # 参数向量
learning_rate = 0.01  # 学习率
num_iterations = 1000  # 迭代次数

# 梯度下降
for _ in range(num_iterations):
    # 计算梯度
    gradient = [0, 0]  # 梯度向量
    for i in range(len(X)):
        x = X[i]
        y = y[i]
        gradient[0] += (theta[0] + theta[1]*x - y)  # 对theta0求偏导数
        gradient[1] += (theta[0] + theta[1]*x - y) * x  # 对theta1求偏导数
    gradient[0] /= len(X)
    gradient[1] /= len(X)

    # 更新参数
    theta[0] -= learning_rate * gradient[0]
    theta[1] -= learning_rate * gradient[1]

# 最优参数
print("最优参数:", theta)
4.2.2 约束优化

约束优化问题:min f(x),s.t. g(x) ≥0, x∈D.其中,x是一个n维向量,D是问题的定义域

小结

局部搜索是求的是解节点而不是路径,所以不必维护搜索树。同时局部搜索搜索的是局部,往往需要启发式函数。好的启发式搜索能大大提高搜索性能
好的搜索策略应该引起,运动—避免原地踏步,系统—避免兜圈,运用启发函数—缓解组合爆炸。

本章下一部分,将不再强求环境的确定性和可观察性以及未知性,是不是很大胆?所以还是敬请期待吧!不要走开,马上回来。

猜你喜欢

转载自blog.csdn.net/weixin_61197809/article/details/134261011