一、直观概念与算法流程(快速上手版)

  • 直觉:把每个解看作“粒子”,粒子在解空间中飞行。每个粒子记住自己遇到过的最好位置(个体最优 pbest),群体也记住到的最好位置(全局最优 gbest)。粒子根据自己的经验和群体经验调整速度和位置,逐步逼近最优解。
  • 每个粒子包含:位置 x(向量)、速度 v(向量)、个体最优位置 pbest 及对应适应度 pbest_val
  • 核心更新公式(标准 PSO)
    • 速度更新: v = w * v + c1 * r1 * (pbest - x) + c2 * r2 * (gbest - x) 其中 w 是惯性权重,c1c2 是学习因子(通常 ~1.5~2.0),r1r2 是 [0,1] 的随机数。
    • 位置更新: x = x + v
  • 典型参数
    • 惯性权重 w:控制探索(大)与开发(小)。常用 0.4–0.9,或随迭代线性递减。
    • 学习因子 c1c2:常见设为 1.5–2.2。若 c1 > c2,粒子更信任自己的经验;反之更信任群体。
    • 速度限制 v_max:防止粒子飞出太远,常设为位置范围的某分数。
  • 边界处理:常见策略包括:截断(clamp)到边界、反弹、将速度设为 0、周期(wrap-around)。
  • 拓扑变体
    • 全局最优(gBest):所有粒子共享一个 gbest(收敛快,易陷入局部)。
    • 局部最优(lBest):每个粒子只与邻居交流,更适合多峰问题。
  • 优点:实现简单、参数少、对目标函数连续可导性无要求、计算开销较低。
  • 缺点:易陷入局部最优(尤其是 gBest),不保证全局收敛;参数敏感。

二、使用建议与调参小技巧

  • 初期用较大的 w(例如 0.9),逐步减小到 0.4,能先大范围探索再细化搜索。
  • 若陷入局部最优:降低 c1 / 提高 c2(或反之)尝试;或使用重启策略、加入扰动或混合其它算法(如模拟退火、差分进化)。
  • 对多峰问题可使用 lBest(环形邻居)或分簇(多群体)策略。
  • 维度高时,增加粒子数(经验:粒子数通常与维度线性或超线性增长)。

三、C++ 示例代码(可直接编译)

  • 功能:用 PSO 最小化 n 维 Sphere 函数f(x) = sum(x_i^2)(全局最优在 0 向量)。
  • 说明:把目标函数 objective() 替换成你自己的连续函数即可。
  • 编译:g++ -O2 -std=c++17 pso.cpp -o pso,运行 ./pso
// pso.cpp
// 简单易用的 PSO 实现(C++17)
// 优化目标:Sphere 函数(可在 objective() 中替换为任意连续目标)
// 编译:g++ -O2 -std=c++17 pso.cpp -o pso

#include <bits/stdc++.h>
using namespace std;
using Vec = vector<double>;
std::mt19937_64 rng((unsigned)chrono::high_resolution_clock::now().time_since_epoch().count());
std::uniform_real_distribution<double> uniform01(0.0, 1.0);

// --------- 可调整的 PSO 超参数 ----------
struct PSOParams {
    int dim = 30;                // 维度
    int swarm_size = 50;         // 粒子数量
    int max_iters = 1000;        // 最大迭代次数
    double w_start = 0.9;        // 惯性权重起始值
    double w_end = 0.4;          // 惯性权重结束值(线性递减)
    double c1 = 1.5;             // 个体学习因子
    double c2 = 1.5;             // 群体学习因子
    double xmin = -5.0;          // 每维搜索下界
    double xmax = 5.0;           // 每维搜索上界
    double vmax_ratio = 0.5;     // v_max = vmax_ratio * (xmax - xmin)
    bool verbose = true;
};
// ---------------------------------------

double rand01() { return uniform01(rng); }

// 用户目标函数:输入向量 x 返回标量值(要**最小化**)
double objective(const Vec &x) {
    // Sphere 函数:全局最优在 x = 0
    double s = 0.0;
    for (double v : x) s += v * v;
    return s;
}

struct Particle {
    Vec x;      // 当前位置
    Vec v;      // 速度
    Vec pbest;  // 个体最优位置
    double pbest_val = numeric_limits<double>::infinity();
    Particle(int dim = 0) {
        x.assign(dim, 0.0);
        v.assign(dim, 0.0);
        pbest.assign(dim, 0.0);
    }
};

struct PSOResult {
    Vec best_pos;
    double best_val;
    int iterations;
    vector<double> history; // 每次迭代的全局最优值(可选)
};

// 约束工具:夹住位置与速度
inline void clamp_vec(Vec &v, double lo, double hi) {
    for (double &x : v) {
        if (x < lo) x = lo;
        else if (x > hi) x = hi;
    }
}

PSOResult run_pso(const PSOParams &par) {
    int dim = par.dim;
    int n = par.swarm_size;
    double v_max = par.vmax_ratio * (par.xmax - par.xmin);

    // 初始化粒子群
    vector<Particle> swarm;
    swarm.reserve(n);
    for (int i = 0; i < n; ++i) {
        Particle p(dim);
        for (int d = 0; d < dim; ++d) {
            double r = rand01();
            p.x[d] = par.xmin + r * (par.xmax - par.xmin);
            // 速度初始为小幅随机值
            p.v[d] = (rand01() * 2.0 - 1.0) * v_max;
        }
        p.pbest = p.x;
        p.pbest_val = objective(p.x);
        swarm.push_back(move(p));
    }

    // 初始化全局最优
    Vec gbest_pos(dim);
    double gbest_val = numeric_limits<double>::infinity();
    for (const auto &p : swarm) {
        if (p.pbest_val < gbest_val) {
            gbest_val = p.pbest_val;
            gbest_pos = p.pbest;
        }
    }

    PSOResult result;
    result.best_pos = gbest_pos;
    result.best_val = gbest_val;
    result.iterations = 0;
    result.history.reserve(par.max_iters);

    // 主循环
    for (int iter = 0; iter < par.max_iters; ++iter) {
        // 线性递减惯性权重(也可以选用其他策略)
        double w = par.w_start + (par.w_end - par.w_start) * (double(iter) / max(1, par.max_iters - 1));

        for (auto &p : swarm) {
            // 更新速度与位置
            for (int d = 0; d < dim; ++d) {
                double r1 = rand01();
                double r2 = rand01();
                double cognitive = par.c1 * r1 * (p.pbest[d] - p.x[d]);
                double social    = par.c2 * r2 * (gbest_pos[d] - p.x[d]);
                p.v[d] = w * p.v[d] + cognitive + social;
                // 限制速度
                if (p.v[d] > v_max) p.v[d] = v_max;
                else if (p.v[d] < -v_max) p.v[d] = -v_max;
                // 更新位置
                p.x[d] += p.v[d];
            }
            // 边界处理:简单截断到 [xmin, xmax]
            clamp_vec(p.x, par.xmin, par.xmax);

            // 评估目标并更新个体最优
            double val = objective(p.x);
            if (val < p.pbest_val) {
                p.pbest_val = val;
                p.pbest = p.x;
            }
            // 更新全局最优
            if (val < gbest_val) {
                gbest_val = val;
                gbest_pos = p.x;
            }
        } // end for swarm

        result.history.push_back(gbest_val);
        result.best_val = gbest_val;
        result.best_pos = gbest_pos;
        result.iterations = iter + 1;

        // 可选终止条件:如果足够小就提前结束
        if (gbest_val <= 1e-12) break;

        if (par.verbose && (iter % max(1, par.max_iters/10) == 0 || iter + 1 == par.max_iters)) {
            cout << "Iter " << (iter+1) << "/" << par.max_iters
                 << "  best = " << scientific << setprecision(5) << gbest_val << endl;
        }
    } // end main loop

    return result;
}

int main() {
    PSOParams p;
    p.dim = 30;            // 维度,可改
    p.swarm_size = 60;     // 粒子数量
    p.max_iters = 1000;
    p.w_start = 0.9; p.w_end = 0.4;
    p.c1 = 1.7; p.c2 = 1.7;
    p.xmin = -5.12; p.xmax = 5.12; // Sphere 常见范围

    cout << "Running PSO (dim=" << p.dim << ", swarm=" << p.swarm_size << ", max_iters=" << p.max_iters << ")\n";
    PSOResult res = run_pso(p);

    cout << "Finished. Iterations: " << res.iterations << "\n";
    cout << "Best value: " << setprecision(12) << res.best_val << "\n";
    cout << "Best pos (first 10 dims): ";
    for (int i = 0; i < min(p.dim, 10); ++i) {
        cout << res.best_pos[i] << (i+1==min(p.dim,10) ? "\n" : ", ");
    }
    return 0;
}

四、如何把代码改成你需要的样子

  • 要最小化你的目标函数:把 objective() 函数替换为你的函数(保持输入 Vec &x)。
  • 若希望最大化,把返回值取负号(或改判断方向)。
  • 要优化带约束的问题(如等式/不等式),常用方法:
    • 惩罚函数(penalty):目标 + 大的罚项;
    • 修复操作:把违规粒子映射回可行域;
    • 使用特殊的启发式方式来生成可行解。
  • 若要并行评估(目标函数贵重),可以把粒子目标评估替换为并行 std::async / OpenMP /线程池调用。

五、常见问题(FAQ)

  • Q:PSO 会不会保证找到全局最优?
    A:不能保证(启发式算法)。但对于很多连续优化问题,PSO 在实践中效果不错;必要时与其它方法结合或多次重启改善可信度。
  • Q:粒子数选多少合适?
    A:经验值:20–100。维度更高时需要更多粒子。常见经验:swarm_size ≈ 10 * dim(只是经验)。
  • Q:迭代次数如何设置?
    A:看问题复杂度和目标函数评价时间。可以设置简单的早停条件(目标值阈值或若干次无改善后停止)。

1️⃣ 什么叫“维度”?

  • 数学里:维度(Dimension)就是“有多少个变量”。
    • 一维:(x)
    • 二维:((x_1,x_2))
    • 三维:((x_1,x_2,x_3))
  • 优化里:维度就是你要同时优化的参数的个数

举例:
Griewank 函数:
[
G(x_1,x_2)=1+\frac{x_1^2+x_2^2}{4000}-\cos x_1\cos\frac{x_2}{\sqrt2}
]

这里 n=2 表示有两个变量 (x_1)、(x_2)。
所以搜索空间就是一个平面(x1,x2),这就是“二维”。

如果 n=3,就是 ((x_1,x_2,x_3)) 三个变量,搜索空间是一个三维空间。


2️⃣ 在粒子群优化(PSO)里“维度”的作用

每一只粒子就是一个“解”,它在搜索空间里有一个位置。

  • 位置 position 就是一个向量,长度 = 维度数
  • 速度 velocity 也是同样长度的向量

如果 n=2:

struct Particle {
    std::vector<double> position; // [x1, x2]
    std::vector<double> velocity; // [v1, v2]
    std::vector<double> bestPosition; // 个人最好位置
    double bestValue; // 个人最好值
};

如果 n=3,就会是 [x1,x2,x3]


3️⃣ 结合代码看一只“粒子”的运动

比如 n=2 的 Griewank 函数,粒子初始时:

particle.position = {rand(-600,600), rand(-600,600)};
particle.velocity = {rand(-1,1), rand(-1,1)};

然后迭代更新:

for (int d=0; d<dim; d++) {
    // 速度更新
    particle.velocity[d] = w * particle.velocity[d]
        + c1 * r1 * (particle.bestPosition[d] - particle.position[d])
        + c2 * r2 * (globalBestPosition[d] - particle.position[d]);
    // 位置更新
    particle.position[d] += particle.velocity[d];
}
  • d0dim-1
  • 每一维度的速度和位置都单独更新,这样粒子才能在多维空间中移动。

4️⃣ 为什么要这么做?

因为多维空间的每个坐标轴都是一个独立的变量。
粒子在多维空间里移动,本质就是在每个维度单独调整数值,一步步靠近最优解。

如果你只更新一个维度,另一个维度不动,粒子就不能在空间里“自由搜索”。


总结口诀

  • 维度 = 变量个数
  • 粒子的位置 = 一个解(多维向量)
  • 粒子群优化就是在多维空间里“找最优解”
  • 每个维度分别更新位置和速度 → 粒子才能搜索整个空间

对于这个问题,使用粒子群优化算法,自需要替换目标函数即可:

//*目标函数

double objective(const Vec &x){

    double sum = 0.0;

    double prod = 1.0;

    for (size_t i = 0; i < x.size(); ++i) {

        double xi = x[i];

        sum += xi * xi;

        prod *= cos(xi / sqrt(double(i+1)));

    }

    return 1.0 + sum / 4000.0 - prod;

}

粒子群优化(PSO)代码的逐步详解

1) PSO 的总体流程(映射到代码)

把算法按步骤拆开,和代码位置一一对应。

  1. 初始化粒子位置与速度(代码里 for (i in swarm) 那段)
    代码片段(简化): p.x[d] = xmin + rand01() * (xmax - xmin); p.v[d] = (rand01()*2.0 - 1.0) * v_max; p.pbest = p.x; p.pbest_val = objective(p.x); 目的/直觉:在解空间内随机散布粒子以覆盖不同区域;速度也随机以产生初始“动量”。把当前点设为个体最优(pbest),以后根据探索结果更新。
  2. 评估初始 pbest 与 gbest
    找出当前群体中适应度(目标值)最好的粒子,作为全局最优 gbest。
  3. 主循环:每代做两件主要事
    • 更新速度(核心公式)
    • 更新位置x = x + v),然后评估并更新 pbestgbest
    代码片段(速度/位置更新的关键行): p.v[d] = w * p.v[d] + c1 * r1 * (p.pbest[d] - p.x[d]) + c2 * r2 * (gbest_pos[d] - p.x[d]); p.x[d] += p.v[d]; 这里 wc1c2r1r2 是关键参数/随机量(下文详细解释)。
  4. 边界与速度限制 if (p.v[d] > v_max) p.v[d] = v_max; clamp_vec(p.x, xmin, xmax); 防止粒子一步飞出过远(数值稳定性、避免无限振荡或越界)。
  5. 终止条件:到达最大迭代或满足精度阈值(gbest_val <= tol)或无改善若干代。

2) 核心公式逐项拆解(为什么要有这 3 项?)

速度更新公式(一次写完整):
[
v \leftarrow w v + c_1 r_1 (pbest - x) + c_2 r_2 (gbest - x)
]

  • w * v惯性项(momentum)
    作用:保留之前速度的“惯性”,使粒子不会只做贪心跳转,而是有平滑的运动轨迹。
    直觉:有助于继续向有利方向探索,避免被短暂噪声迷惑。
  • c1 * r1 * (pbest - x)认知(个体)项
    作用:粒子被自己历史上最好的位置吸引(“我知道哪儿对我最好”)。
    直觉:保持个体探索能力,如果一个粒子曾在某位置找到好解,会尝试回去或靠近它进一步局部搜寻。
  • c2 * r2 * (gbest - x)社会(群体)项
    作用:粒子被群体最好位置吸引(“大家的经验很重要”)。
    直觉:把群体中表现好的信息共享,带动整个群体向优良区域聚集,加速收敛。
  • r1, r2 — 两个随机数(均匀 [0,1])
    作用:在每一维、每次更新加入随机缩放,防止完全确定性更新,使得粒子表现出随机扰动,增强探索多样性。
    直觉:即便 pbest 和 gbest 的差向量方向相同,随机缩放能让不同粒子采取不同步幅,避免同步陷入同一局部。

3) 为什么要把速度和位置分开(而不是直接用 (x \leftarrow x + \alpha(pbest-x) + \beta(gbest-x)))?

两个原因:

  1. 动量和惯性效应:速度累积能够产生连贯的搜索轨迹,能“越过”浅局部、避免在狭小谷底来回抖动。直接移动没有记忆,可能太贪心、步长忽大忽小。
  2. 控制步长:通过 v_max 可以限制每一步的最大移动距离,利于数值稳定和更可控的搜索节奏。

4) 参数(w, c1, c2, v_max)对行为的影响(直观建议)

  • w(惯性权重)
    • w(例如 0.8–1.0):更强调惯性→更广泛探索(跳得远)。
    • w(例如 0.2–0.4):更强调当前吸引项→局部搜索(收敛)。
    • 常见做法:线性递减(代码用的 w_startw_end)——先探索、后收敛。
  • c1(个体学习因子) vs c2(群体学习因子)
    • c1 大:粒子更相信自己的经验→多样化(可避免过早被群体拉偏)。
    • c2 大:粒子更容易向 gbest 聚集→收敛快但易陷入局部。
    • 常见:c1 ≈ c2 ≈ 1.5~2.0,也可做不对称设置根据问题调整。
  • v_max(速度上限)
    • 太大:粒子可能越界并错过细粒度最优,数值不稳定。
    • 太小:搜索变慢、可能被局部困住。
    • 经验:v_max = ratio * (xmax - xmin),ratio 在 0.10.5 之间常见。

5) 为什么要随机初始化?为什么用均匀分布?

随机初始化保证覆盖不同区域,避免全部粒子集中在一个区域开始搜索。均匀分布是最简单且通常有效的选择。如果已知先验信息(某区间更可能有好解),可以用非均匀分布(正态分布、聚类初始化)来加速。


6) 关于 pbest、gbest 的更新(为什么这样做)

  • pbest:每个粒子保存自己见过的最好的位置。这样粒子有“记忆”并能把本地调优的成果保留下来。
  • gbest:群体共享的最好位置。它是群体合作的信息汇总,能快速把整个群体导向优良区域。

把 pbest 与 gbest 同时保留,是平衡个体探索群体协作的关键。


7) 常见陷阱与解决策略(尤其针对 Griewank 这样的多峰函数)

Griewank 有很多局部极小值(很多“坑”),所以 PSO 容易被卡住。对策:

  • 增大探索期:增大 w_start,或延长线性递减的时间窗口(让群体更长时间探索)。
  • 增大粒子数:二维问题可以用 50–100 个粒子覆盖更多区域。
  • 增加随机扰动 / 重启:当若干代没有改进时,随机重置部分粒子位置。
  • 局部拓扑(lBest):改为每个粒子只被邻居最优影响,能在复杂多峰上保留多样化解;对多峰函数通常更稳。
  • 混合方法:先用 PSO 做全局粗搜索,再对找到的若干候选用局部搜索(如梯度法或 Nelder–Mead)精炼。

8) 边界处理有哪些选择?代码里用了 clamp,还有其它吗?

  • 截断(clamp)(代码里用):简单可靠,把越界位置设回边界。优点易实现;缺点可能让粒子卡在边界。
  • 反弹(reflect):把超出部分反向映射,相当于在边界弹回。
  • 周期(wrap):把超出部分从另一侧进入(类似圆周)。
  • 重新随机化:超出则随机放回可行域内(增加多样性)。
    选择取决于问题语义与边界含义。

9) 如何把你的代码改成更稳健 / 更适合 Griewank?

  • 使用更大的 swarm_size(例如 80–150)来覆盖多峰。
  • vmax_ratio 设大一点(例如 0.3–0.5)以便粒子能跳出局部坑,再在后期减小 w 以精细搜索。
  • 实现早停后“局部重启”:若 gbest 在 200 代内没有显著改进,就随机重启 20% 的粒子。
  • 尝试 lBest:把 gbest 改为每个粒子邻居的最好(例如环形邻居 4 个),保留多峰多簇解。

10) 代码和调参的快速对照指南(recipes)

  • 想要更快收敛(但更易陷入局部):减小 w、增大 c2、减小 v_max
  • 想要更强全局探索:增大 w_start、增大 v_max、增大 swarm_size、适当增大 c1
  • 想得到稳定而靠谱的解:多次独立运行 PSO,统计最优值的均值和方差(即做多次随机试验)。

11) 与你代码相关的几个实现细节提示

  • objective()(例如 griewank())应尽量高效:目标函数是瓶颈时,把评估并行化(std::async 或 OpenMP),每代同时评估多个粒子。
  • history.csv 很好:画出 gbest 随代数变化,通常看到快速下降阶段 + 平台期。平台期说明算法已收敛或被卡住。
  • 若要最大化目标,直接返回 -objective(x) 或把比较方向反过来。

12) 小结(一句话版)

  • PSO 用“个体记忆 + 群体信息 + 随机性 + 惯性”来在解空间里平衡探索与开发。
  • 代码里的每一项(初始化、速度三个项、随机数、速度限制、边界处理、惯性权重的衰减)都是为了解决:覆盖、稳定、避免局部、以及收敛速度之间的折衷。

届ける言葉を今は育ててる
最后更新于 2025-09-29