机器学习周报二十八

文章目录

摘要

本周学习了传统优化算法在高纬度失效的场景,并且实现了基于分解的MOEA/D-DRA算法来适应高纬度优化。

Abstract

This week, we studied the scenarios where traditional optimization algorithms fail in high-dimensional spaces and implemented the decomposition-based MOEA/D-DRA algorithm to adapt to high-dimensional optimization.

1.1 传统优化算法

NSGA-II在高纬度时, 以上是NGSA-II在目标维度是10,种群大小为300,迭代250次之后得到的最终结果,发现NGSA-II的帕累托更杂乱,解没有收敛到清晰的帕累托前沿,而是均匀地、随机地散布在整个目标空间的中间区域。几乎所有线条都穿过了每个目标从0到1的几乎所有值。这意味着很多解在各个目标上都表现平庸,且互相之间不构成明显的支配关系。

1.2高纬度多目标优化算法

MOEA/D-DRA 的核心思想是将 10 目标优化问题分解为多个单目标子问题,通过动态分配搜索资源提升整体优化效果,具体机制如下:

问题分解机制

MOEA/D-DRA 是在 MOEA/D 框架基础上发展而来的改进版本。MOEA/D 通过将多目标问题分解为多个标量子问题,并利用一组均匀分布的权重向量来引导搜索方向。每个子问题对应一个权重向量,并通过聚合函数(如 Tchebycheff 函数)将多目标问题转化为单目标问题:

其中w是权重向量,z^*为理想点。

与标准 MOEA/D 不同,MOEA/D-DRA 引入了动态资源分配机制,即根据子问题的"效用"动态选择一部分子问题进行演化,而不是对所有子问题一视同仁。这种机制的核心在于:

效用函数定义:每个子问题的效用π^i反映了其在最近若干代中的改进程度。具体定义为:

其中Δ^i 是子问题i的适应度改进率,计算如下:

该值越大,说明该子问题在最近的优化中获得了更大的改进,因此更值得投入计算资源。

资源分配策略:在每一代中,算法根据效用值对所有子问题进行排序,优先选择效用高的子问题进行繁殖和更新。这种策略可以有效集中计算资源于"有潜力"的搜索区域,从而提升整体收敛速度和解集质量。

动态资源分配(DRA)

效用评估:根据子问题的历史改进情况计算效用值,反映子问题的优化潜力;

资源倾斜:将搜索资源优先分配给效用值高的子问题,提高搜索效率;

休眠激活:定期激活效用值过低的 "休眠" 子问题,避免算法早熟收敛。

邻域协作机制

为每个子问题构建邻域结构(基于权重向量的欧氏距离);

子问题的演化仅在其邻域内选择父代,保持解的局部一致性;

通过邻域更新实现子问题间的信息共享,促进帕累托前沿的整体推进。

python 复制代码
import random

import numpy as np
from matplotlib import pyplot as plt
import pandas as pd
np.random.seed(42)
random.seed(42)


def dtlz1(x, m):
    x = np.asarray(x)
    n = len(x)
    k = n - m + 1
    assert k >= 1, "n must be >= m"
    g = 100 * (k + np.sum((x[-k:] - 0.5) ** 2 - np.cos(20 * np.pi * (x[-k:] - 0.5))))
    f = []
    for i in range(m):
        fi = 0.5 * (1 + g)
        for j in range(m - i - 1):
            fi *= x[j]
        if i > 0:
            fi *= (1 - x[m - i - 1])
        f.append(fi)
    return np.array(f)


def fast_non_dominated_sort(objectives):
    objectives = np.array(objectives)
    n = objectives.shape[0]
    domination_count = np.zeros(n, dtype=int)
    # 被支配的集合
    dominated_set = [[] for _ in range(n)]
    # 前沿列表
    fronts = []

    for i in range(n):
        for j in range(n):
            if i == j:
                continue
            # 检查i是否支配j
            if np.all(objectives[i] <= objectives[j]) and np.any(objectives[i] < objectives[j]):
                dominated_set[i].append(j)
            # 检查j是否支配i
            elif np.all(objectives[j] <= objectives[i]) and np.any(objectives[j] < objectives[i]):
                domination_count[i] += 1
    # 找到第一前沿(非支配解)
    current_front = [i for i in range(n) if domination_count[i] == 0]
    fronts.append(current_front)

    # 寻找后续前沿
    while current_front:
        next_front = []
        for i in current_front:
            for j in dominated_set[i]:
                domination_count[j] -= 1
                if domination_count[j] == 0:
                    next_front.append(j)
        if next_front:
            fronts.append(next_front)
        current_front = next_front

    return fronts


def crowed_distance_assignment(objectives):
    pop_size, num_objectives = objectives.shape
    distances = np.zeros(pop_size)

    for m in range(num_objectives):
        indices = np.argsort(objectives[:, m])
        distances[indices[0]] = np.inf  # 边界个体
        distances[indices[-1]] = np.inf  # 边界个体
        norm = objectives[indices[-1], m] - objectives[indices[0], m]

        if norm == 0: continue  # 如果所有目标值相同,则跳过

        for i in range(1, pop_size - 1):
            distances[indices[i]] += (objectives[indices[i + 1], m] - objectives[indices[i - 1], m]) / norm

    return distances


def sbx_crossover(parent1, parent2, eta, bounds, crossover_rate=1.0):
    if random.random() > crossover_rate:
        return np.array(parent1), np.array(parent2)

    parent1 = np.array(parent1)
    parent2 = np.array(parent2)
    vars = len(parent1)
    child1 = np.copy(parent1)
    child2 = np.copy(parent2)

    for i in range(vars):
        x1 = parent1[i]
        x2 = parent2[i]
        low, high = bounds[i]

        if abs(x1 - x2) < 1e-14:
            child1[i] = x1
            child2[i] = x2
            continue
        if x1 > x2:
            x1, x2 = x2, x1
            swapped = True
        else:
            swapped = False
        u = random.random()
        if u <= 0.5:
            beta = (2.0 * u) ** (1.0 / (eta + 1.0))
        else:
            beta = (1.0 / (2.0 * (1.0 - u))) ** (1.0 / (eta + 1.0))
        c1 = 0.5 * ((1 + beta) * x1 + (1 - beta) * x2)
        c2 = 0.5 * ((1 - beta) * x1 + (1 + beta) * x2)

        c1 = np.clip(c1, low, high)
        c2 = np.clip(c2, low, high)

        if swapped:
            child1[i] = c2
            child2[i] = c1
        else:
            child1[i] = c1
            child2[i] = c2

    return child1, child2


def polynomial_mutation(entry, bounds, mutation_rate=1.0, eta=20):
    individual = np.array(entry, dtype=float)
    mutated = np.copy(individual)
    n = len(individual)

    for i in range(n):
        if random.random() >= mutation_rate:
            continue

        low, high = bounds[i]
        y = mutated[i]

        u = random.random()
        if u <= 0.5:
            delta = (2 * u) ** (1.0 / (eta + 1)) - 1.0
        else:
            delta = 1.0 - (2 * (1 - u)) ** (1.0 / (eta + 1))

        y_new = y + delta * (high - low)
        y_new = np.clip(y_new, low, high)
        mutated[i] = y_new

    return mutated


def binary_tournament_selection(population, rank, crowding_distances):
    pop_size = len(population)
    idx1, idx2 = np.random.choice(pop_size, 2, replace=False)

    # 优先选择rank小的
    if rank[idx1] < rank[idx2]:
        return population[idx1]
    elif rank[idx1] > rank[idx2]:
        return population[idx2]
    else:
        # rank相同,选择拥挤距离大的
        if crowding_distances[idx1] > crowding_distances[idx2]:
            return population[idx1]
        else:
            return population[idx2]


def generate_weight(N, m):
    return np.random.dirichlet(np.ones(m), size=N)


def tchebycheff(y, weight, z):
    return np.max(weight * np.abs(y - z))


def differential_evolution(parent1, parent2, parent3, F=0.5):
    parent1 = np.array(parent1)
    parent2 = np.array(parent2)
    parent3 = np.array(parent3)
    # 基本差分变异
    mutant = parent1 + F * (parent2 - parent3)
    return mutant


def compute_utility(old_objs, new_objs, weights, z, delta=1e-8):
    old_val = tchebycheff(old_objs, weights, z)
    new_val = tchebycheff(new_objs, weights, z)

    if abs(old_val) < delta:
        return 0.0

    improvement = (old_val - new_val) / old_val
    return max(0.0, improvement)  # 只考虑改进


# 动态资源分配选择
def dra_selection(pi, k):
    n = len(pi)
    k = min(k, n)
    norm = pi + 1e-10
    pi_min, pi_max = norm.min(), norm.max()

    if pi_max - pi_min > 1e-10:
        pi_norm = (norm - pi_min) / (pi_max - pi_min)
    else:
        pi_norm = np.ones(n)

    p = pi_norm / pi_norm.sum()
    if np.any(np.isnan(p)) or np.any(p < 0):
        p = np.ones(n) / n

    selected = np.random.choice(n, size=k, p=p, replace=False)
    return selected


def evaluate_population(population, m):
    objectives = []
    for ind in population:
        objs = dtlz1(ind, m)
        objectives.append(objs)
    return np.array(objectives)


def MOEA(m=10, D=30, n=200, max_gen=500, mutation_rate=0.0, delta=0.8, nr=2,
         crossover_rate=0.9, sbx_eta=20, poly_eta=20, ref_point=None):
    lambdas = generate_weight(n, m)  # shape (N, m)
    T = min(30, n)
    neighbors = []
    bounds = [(0.0, 1.0)] * D

    # 计算邻居
    for i in range(n):
        dists = np.linalg.norm(lambdas - lambdas[i], axis=1)
        idx = np.argsort(dists)[:T]
        neighbors.append(idx)

    # 初始化种群
    population = np.random.rand(n, D)
    objectives = evaluate_population(population, m)

    z = np.min(objectives, axis=0)  # shape (m,)
    pi = np.ones(n)

    gen = 0
    f_history = objectives.copy()  # shape (N, m)

    hv_history = []

    while gen < max_gen:
        # 使用dra_selection进行动态资源分配选择
        selected = set()

        # 选择每个目标上的最优解
        for obj in range(m):
            idx = np.argmin(objectives[:, obj])
            selected.add(idx)

        # 使用dra_selection选择剩余个体
        remaining = max(1, n // 5 - len(selected))  # 确保至少选择1个
        if remaining > 0:
            selected_indices = dra_selection(pi, remaining)
            selected.update(selected_indices)
        I = list(selected)
        # 对选中的个体进行进化操作
        for i in I:
            # 选择邻域或整个种群
            if np.random.rand() < delta:
                P = neighbors[i].copy()
            else:
                P = np.arange(n).copy()
            if len(P) < 2:
                continue
            # 选择父代
            a, b = np.random.choice(P, size=2, replace=False)
            x1, x2 = population[a], population[b]
            # SBX交叉
            child1, child2 = sbx_crossover(x1, x2, sbx_eta, bounds, crossover_rate)
            # 多项式变异
            child1 = polynomial_mutation(child1, bounds, mutation_rate, poly_eta)
            child2 = polynomial_mutation(child2, bounds, mutation_rate, poly_eta)
            # 评估子代
            y1 = dtlz1(child1, m)
            y2 = dtlz1(child2, m)
            # 更新理想点
            z = np.minimum(z, y1)
            z = np.minimum(z, y2)

            # 更新邻域解
            for child, y in [(child1, y1), (child2, y2)]:
                utilities = []
                for j in P:
                    old_objs = objectives[j]
                    utility = compute_utility(old_objs, y, lambdas[j], z)
                    utilities.append((j, utility))

                # 选择效用最大的nr个进行更新
                utilities.sort(key=lambda x: x[1], reverse=True)

                for j, utility in utilities[:nr]:
                    if utility > 0:  # 只有改进才更新
                        population[j] = child
                        objectives[j] = y
        gen += 1
        if gen % 50 == 0:
            for i in range(n):
                old_objs = f_history[i]
                new_objs = objectives[i]
                utility = compute_utility(old_objs, new_objs, lambdas[i], z)
                if utility < 0.001:
                    pi[i] *= 0.95  # 效用低,减少资源分配
                else:
                    pi[i] = min(pi[i] * 1.05, 2.0)  # 效用高,增加资源分配
            f_history = objectives.copy()
        if gen % 10 == 0:
            f = fast_non_dominated_sort(objectives)
            hv = hypervolume(objectives[f[0]], ref_point, n_samples=200000)
            hv_history.append((gen, hv))
            avg_g = np.mean([tchebycheff(objectives[i], lambdas[i], z) for i in range(n)])
            print(f"Generation {gen}, Avg Tchebycheff: {avg_g:.4f}, HV: {hv:.4f}")

    return population, objectives, z, hv_history


def NSGA(m=10, D=30, n=200, max_gen=500, crossover_rate=0.9, mutation_rate=0.0, sbx_eta=20, poly_eta=20,
         ref_point=None):
    bounds = [(0.0, 1.0)] * D

    population = np.random.rand(n, D)
    objectives = evaluate_population(population, m)  # shape (pop_size, m)

    hv_history = []

    f = fast_non_dominated_sort(objectives)
    rank = np.zeros(n, dtype=int)
    for i, front in enumerate(f):
        for idx in front:
            rank[idx] = i
    crowding_distances = crowed_distance_assignment(objectives)

    gen = 0
    while gen < max_gen:
        offspring = []
        while len(offspring) < n:
            # 选择两个父代
            parent1 = binary_tournament_selection(population, rank, crowding_distances)
            parent2 = binary_tournament_selection(population, rank, crowding_distances)

            # SBX 交叉
            child1, child2 = sbx_crossover(parent1, parent2, sbx_eta, bounds, crossover_rate)

            # 多项式变异
            child1 = polynomial_mutation(child1, bounds, mutation_rate, poly_eta)
            child2 = polynomial_mutation(child2, bounds, mutation_rate, poly_eta)

            offspring.append(child1)
            offspring.append(child2)
        offspring = np.array(offspring[:n])

        combined_pop = np.vstack([population, offspring])  # (2N, n)
        combined_obj = evaluate_population(combined_pop, m)  # (2N, m)

        fronts = fast_non_dominated_sort(combined_obj)

        new_population = []
        new_objectives = []

        for front in fronts:
            if len(new_population) + len(front) <= n:
                # 全部加入
                new_population.extend(combined_pop[front])
                new_objectives.extend(combined_obj[front])
            else:
                # 按拥挤距离截断
                remaining = n - len(new_population)
                front_objs = combined_obj[front]
                distances = crowed_distance_assignment(front_objs)
                # 按拥挤距离降序排序
                sorted_indices = np.argsort(-distances)  # 负号 → 降序
                selected = [front[i] for i in sorted_indices[:remaining]]
                new_population.extend(combined_pop[selected])
                new_objectives.extend(combined_obj[selected])
                break

        population = np.array(new_population)
        objectives = np.array(new_objectives)

        # 更新 rank 和 crowding_distances
        fronts_current = fast_non_dominated_sort(objectives)
        rank = np.zeros(n, dtype=int)
        for i, front in enumerate(fronts_current):
            for idx in front:
                rank[idx] = i
        crowding_distances = crowed_distance_assignment(objectives)

        gen += 1
        if gen % 10 == 0:
            f = fast_non_dominated_sort(objectives)
            hv = hypervolume(objectives[f[0]], ref_point, n_samples=200000)
            hv_history.append((gen, hv))
            print(f"Gen {gen},HV: {hv:.4f}")

    ideal = np.min(objectives, axis=0)

    return population, objectives, ideal, hv_history


def draw(objs, m, filename):
    # 高维目标空间,平行坐标图
    plt.figure(figsize=(20, 10))
    norm = (objs - objs.min(axis=0)) / (objs.max(axis=0) - objs.min(axis=0) + 1e-10)

    for i in range(len(norm)):
        plt.plot(range(m), norm[i], alpha=0.3, linewidth=1.5)

    plt.xticks(range(m), [f'f{i + 1}' for i in range(m)])
    plt.xlabel('Objectives', fontsize=12)
    plt.ylabel('Normalized Value', fontsize=12)
    plt.title(f'Parallel Coordinates Plot for {m}-Objective Pareto Front', fontsize=14)
    plt.grid(True)
    plt.savefig('result/' + filename)
    plt.show()


def hypervolume(front, ref_point, n_samples=200000):
    front = np.array(front)
    m = front.shape[1]

    min_bound = np.full(m, 0.0)
    max_bound = ref_point

    samples = np.random.uniform(min_bound, max_bound, size=(n_samples, m))
    box = np.prod(max_bound - min_bound)

    count = 0
    for s in samples:
        # 检查是否被 front 中任意一点支配
        dominated = np.any(np.all(front <= s, axis=1) & np.any(front < s, axis=1))
        if dominated:
            count += 1

    return (count / n_samples) * box


if __name__ == "__main__":
    m = 10
    D = 12
    n = 300
    max_gen = 250

    ref_point = np.ones(m) * 1.3

    pop, objectives, ideal, hv_m = MOEA(m, D, n, max_gen=max_gen, mutation_rate=1.0 / D, ref_point=ref_point)
    print("Final ideal point:", ideal)
    print("Population size:", len(pop))
    draw(objectives, m=10, filename="MOEA D-DRA.jpg")
    df = pd.DataFrame(objectives, columns=[f"f{i + 1}" for i in range(m)])
    df.to_csv("MOEA_D_DRA_result.csv", index=False)

    pop, objectives, ideal, hv_n = NSGA(m, D, n, max_gen=max_gen, mutation_rate=1.0 / D, ref_point=ref_point)
    print("Final ideal point:", ideal)
    print("Population size:", len(pop))
    draw(objectives, m=10, filename="NGSA-II.jpg")

    df = pd.DataFrame(objectives, columns=[f"f{i + 1}" for i in range(m)])
    df.to_csv("NSGA-II_result.csv", index=False)

    x1, y1 = zip(*hv_m)
    x2, y2 = zip(*hv_n)
    plt.figure(figsize=(10, 6))
    plt.plot(x1, y1, 'o-', label='MOEA/D-DRA', color='blue')
    plt.plot(x2, y2, 's-', label='NSGA-II', color='red')
    plt.xlabel('Generation')
    plt.ylabel('Estimated Hypervolume')
    plt.title(f'Hypervolume Comparison (m={m}, Monte Carlo samples={200000})')
    plt.legend()
    plt.grid(True)
    plt.tight_layout()
    plt.savefig('result/Hypervolume.jpg')
    plt.show()

最终的结果

MOEA/D-DRA的超体积逐渐上升,而NSGA-II的超体积却不怎么变化,说明MOEA/D-DRA在高维时对于dtlz1函数具有更好的效果,且MOEA/D-DRA在f1-f5的帕累托前沿的覆盖面更大。

总结

本周主要完成这个高维多目标优化算法。

相关推荐
weixin_437497778 小时前
读书笔记:Context Engineering 2.0 (上)
人工智能·nlp
喝拿铁写前端8 小时前
前端开发者使用 AI 的能力层级——从表面使用到工程化能力的真正分水岭
前端·人工智能·程序员
goodfat8 小时前
Win11如何关闭自动更新 Win11暂停系统更新的设置方法【教程】
人工智能·禁止windows更新·win11优化工具
北京领雁科技8 小时前
领雁科技反洗钱案例白皮书暨人工智能在反洗钱系统中的深度应用
人工智能·科技·安全
落叶,听雪8 小时前
河南建站系统哪个好
大数据·人工智能·python
清月电子9 小时前
杰理AC109N系列AC1082 AC1074 AC1090 芯片停产替代及资料说明
人工智能·单片机·嵌入式硬件·物联网
Dev7z9 小时前
非线性MPC在自动驾驶路径跟踪与避障控制中的应用及Matlab实现
人工智能·matlab·自动驾驶
七月shi人9 小时前
AI浪潮下,前端路在何方
前端·人工智能·ai编程
橙汁味的风9 小时前
1隐马尔科夫模型HMM与条件随机场CRF
人工智能·深度学习·机器学习