Nelder-Mead算法简介

Nelder-Mead算法(也称单纯形法)是一种常用的无导数优化方法,特别适合于:

  • 函数导数难以计算或不存在的情况
  • 寻找非线性函数的局部最小值
  • 维度适中的问题

这种算法被广泛应用于机器学习、计算机视觉、信号处理等领域,特别是当目标函数计算成本高昂时尤为有用。

SciPy版本的特点:

  1. 边界约束支持:SciPy版本添加了对变量边界的支持,通过bounds参数实现。它使用np.clip确保所有单纯形顶点都在指定边界内,这是标准Nelder-Mead算法的扩展。
  2. 自适应参数策略:通过adaptive参数启用,基于Gao和Han
    2012年的论文。这种策略根据问题维度自动调整反射、扩展和收缩参数,使算法在高维问题上表现更好。
  3. 双重收敛条件:同时使用函数值容差(fatol)和参数值容差(xatol)作为终止条件,这比只用单一条件更稳健。
  4. 完善的终止处理:支持通过最大迭代次数(maxiter)或最大函数评估次数(maxfev)限制算法运行时间,并提供详细的终止状态。

更详细的分析见本文结尾。

代码

纯头文件库,包含opti.h,opti.inl。还有一个测试程序main.cpp
opti.h:


#ifndef MY_OPTIMIZE_H
#define MY_OPTIMIZE_H
#include <Eigen/Core>
#include <algorithm>
#include <cmath>
#include <functional>
#include <iostream>
#include <limits>
#include <vector>
// NelderMead算法选项
struct NelderMeadOptions {
double xatol = 1e-4; // x收敛容差
double fatol = 1e-4; // 函数值收敛容差
int maxiter = -1; // 最大迭代次数,-1表示使用默认值
int maxfev = -1; // 最大函数评估次数,-1表示使用默认值
bool disp = false; // 是否显示收敛消息
bool return_all = false; // 是否返回所有迭代的结果
bool adaptive = false; // 是否使用自适应参数
// 边界,每个变量的(min, max)对,std::nullopt表示无边界
std::vector<std::pair<double, double>> bounds;
};
// NelderMead算法结果
struct NelderMeadResult {
Eigen::VectorXd x; // 最优解
double fun; // 最优解对应的函数值
int nit; // 迭代次数
int nfev; // 函数评估次数
int status; // 状态码:0=成功,1=达到最大函数评估次数,2=达到最大迭代次数
bool success; // 是否成功
std::string message; // 状态信息
std::vector<Eigen::VectorXd> allvecs; // 所有迭代的结果(如果return_all=true)
};
/**
* Nelder-Mead单纯形算法
*
* @param func 目标函数
* @param x0 初始猜测点
* @param initial_simplex 初始单纯形,如果提供则覆盖x0
* @param options 算法参数选项
* @param callback 回调函数,每次迭代后调用
* @return 优化结果
*/
template <typename Scalar = double>
NelderMeadResult
minimize_neldermead(std::function<Scalar(const Eigen::Matrix<Scalar, Eigen::Dynamic, 1>&)> func,
const Eigen::Matrix<Scalar, Eigen::Dynamic, 1>& x0,
const Eigen::Matrix<Scalar, Eigen::Dynamic, Eigen::Dynamic>* initial_simplex = nullptr,
const NelderMeadOptions& options = NelderMeadOptions(),
std::function<bool(const NelderMeadResult&)> callback = nullptr);
/**
* 简化版Nelder-Mead优化函数,类似于scipy的fmin
*
* @param func 目标函数
* @param x0 初始猜测点
* @param initial_simplex 初始单纯形,如果提供则覆盖x0
* @param xtol x收敛容差
* @param ftol 函数值收敛容差
* @param maxiter 最大迭代次数
* @param maxfun 最大函数评估次数
* @param full_output 是否返回完整输出
* @param disp 是否显示收敛消息
* @param retall 是否返回所有迭代的结果
* @param callback 回调函数
* @return 优化结果点或完整结果(取决于full_output)
*/
template <typename Scalar = double>
Eigen::Matrix<Scalar, Eigen::Dynamic, 1>
fmin(std::function<Scalar(const Eigen::Matrix<Scalar, Eigen::Dynamic, 1>&)> func,
const Eigen::Matrix<Scalar, Eigen::Dynamic, 1>& x0,
const Eigen::Matrix<Scalar, Eigen::Dynamic, Eigen::Dynamic>* initial_simplex = nullptr,
double xtol = 1e-4,
double ftol = 1e-4,
int maxiter = -1,
int maxfun = -1,
bool full_output = false,
bool disp = true,
bool retall = false,
std::function<bool(const NelderMeadResult&)> callback = nullptr);
#include "opti.inl"
#endif //MY_OPTIMIZE_H

opti.inl:


#ifndef MY_OPTIMIZE_INL
#define MY_OPTIMIZE_INL
#include <algorithm>
#include <cmath>
#include <iostream>
template <typename Scalar>
NelderMeadResult minimize_neldermead(std::function<Scalar(const Eigen::Matrix<Scalar, Eigen::Dynamic, 1>&)> func,
const Eigen::Matrix<Scalar, Eigen::Dynamic, 1>& x0,
const Eigen::Matrix<Scalar, Eigen::Dynamic, Eigen::Dynamic>* initial_simplex,
const NelderMeadOptions& options,
std::function<bool(const NelderMeadResult&)> callback) {
using Vector = Eigen::Matrix<Scalar, Eigen::Dynamic, 1>;
using Matrix = Eigen::Matrix<Scalar, Eigen::Dynamic, Eigen::Dynamic>;
// 提取选项参数
double xatol = options.xatol;
double fatol = options.fatol;
int maxiter = options.maxiter;
int maxfev = options.maxfev;
bool disp = options.disp;
bool return_all = options.return_all;
bool adaptive = options.adaptive;
const auto& bounds = options.bounds;
// 设置算法参数
double rho, chi, psi, sigma;
if (adaptive) {
double dim = static_cast<double>(x0.size());
rho = 1.0;
chi = 1.0 + 2.0 / dim;
psi = 0.75 - 1.0 / (2.0 * dim);
sigma = 1.0 - 1.0 / dim;
} else {
rho = 1.0;
chi = 2.0;
psi = 0.5;
sigma = 0.5;
}
double nonzdelt = 0.05;
double zdelt = 0.00025;
// 检查边界
bool has_bounds = !bounds.empty();
Vector lower_bound, upper_bound;
if (has_bounds) {
if (bounds.size() != static_cast<size_t>(x0.size())) {
throw std::invalid_argument("Bounds size does not match x0 size");
}
lower_bound(x0.size());
upper_bound(x0.size());
for (int i = 0; i < x0.size(); ++i) {
lower_bound(i) = bounds[i].first;
upper_bound(i) = bounds[i].second;
}
if ((lower_bound.array() > upper_bound.array()).any()) {
throw std::invalid_argument("Nelder Mead - one of the lower bounds is greater than an upper bound.");
}
if (((x0.array() < lower_bound.array()) || (x0.array() > upper_bound.array())).any() && disp) {
std::cerr << "Warning: Initial guess is not within the specified bounds" << std::endl;
}
}
// 将点裁剪到边界内
auto clip = [&](const Vector& x) -> Vector {
if (!has_bounds)
return x;
return x.cwiseMax(lower_bound).cwiseMin(upper_bound);
};
// 按函数值排序单纯形顶点
auto sort_simplex = [](Vector& fsim, Matrix& sim) {
const int N = sim.cols();
std::vector<std::pair<Scalar, int>> pairs(N + 1);
for (int i = 0; i < N + 1; ++i) {
pairs[i] = {fsim(i), i};
}
std::sort(pairs.begin(), pairs.end());
// 重排fsim和sim
Vector fsim_sorted(N + 1);
Matrix sim_sorted(N + 1, N);
for (int i = 0; i < N + 1; ++i) {
fsim_sorted(i) = pairs[i].first;
sim_sorted.row(i) = sim.row(pairs[i].second);
}
fsim = fsim_sorted;
sim = sim_sorted;
};
// 创建初始单纯形
Vector x0_copy = x0;
if (has_bounds) {
x0_copy = clip(x0_copy);
}
int N = x0_copy.size();
Matrix sim;
if (initial_simplex == nullptr) {
sim = Matrix(N + 1, N);
sim.row(0) = x0_copy;
for (int k = 0; k < N; ++k) {
Vector y = x0_copy;
if (y(k) != 0) {
y(k) = (1.0 + nonzdelt) * y(k);
} else {
y(k) = zdelt;
}
sim.row(k + 1) = y;
}
} else {
if (initial_simplex->rows() != N + 1 || initial_simplex->cols() != N) {
throw std::invalid_argument("`initial_simplex` should be an array of shape (N+1,N)");
}
sim = *initial_simplex;
}
// 如果边界存在,确保所有单纯形顶点都在边界内
if (has_bounds) {
for (int i = 0; i < sim.rows(); ++i) {
Vector vertex = sim.row(i);
sim.row(i) = clip(vertex);
}
}
// 如果既没有设置maxiter也没有设置maxfev,设置它们为默认值
if (maxiter < 0 && maxfev < 0) {
maxiter = N * 200;
maxfev = N * 200;
} else if (maxiter < 0) {
if (maxfev == std::numeric_limits<int>::max()) {
maxiter = N * 200;
} else {
maxiter = std::numeric_limits<int>::max();
}
} else if (maxfev < 0) {
if (maxiter == std::numeric_limits<int>::max()) {
maxfev = N * 200;
} else {
maxfev = std::numeric_limits<int>::max();
}
}
// 计算初始单纯形中每个点的函数值
Vector fsim = Vector::Constant(N + 1, std::numeric_limits<Scalar>::max());
int fcalls = 0;
for (int k = 0; k < N + 1; ++k) {
fsim(k) = func(sim.row(k).transpose());
fcalls++;
if (fcalls >= maxfev) {
break;
}
}
// 按函数值排序单纯形顶点
sort_simplex(fsim, sim);
// 保存所有迭代结果
std::vector<Vector> allvecs;
if (return_all) {
allvecs.push_back(sim.row(0).transpose());
}
// 开始迭代
int iterations = 1;
while (fcalls < maxfev && iterations < maxiter) {
// 检查收敛
double max_dist = 0.0;
for (int i = 1; i < N + 1; ++i) {
max_dist = std::max(max_dist, (sim.row(i) - sim.row(0)).norm());
}
double max_diff = 0.0;
for (int i = 1; i < N + 1; ++i) {
max_diff = std::max(max_diff, std::abs(fsim(i) - fsim(0)));
}
if (max_dist <= xatol && max_diff <= fatol) {
break;
}
// 计算质心(除了最差点)
// 1. sim.topRows(N) - 选择前N行,等价于Python的sim[:-1]
// (因为单纯形有N+1个顶点,选择前N行就是除了最后一行外的所有行)
// 2. .colwise().sum() - 对每一列分别求和,结果是一个行向量
// (等价于Python的np.add.reduce(..., 0))
// 3. .transpose() - 将行向量转置为列向量
// (因为在Eigen中Vector通常表示列向量)
// 4. / static_cast<Scalar>(N) - 除以N得到平均值(质心)
Vector xbar = sim.topRows(N).colwise().sum().transpose() / static_cast<Scalar>(N);
// 执行反射
Vector xr = (1 + rho) * xbar - rho * sim.row(N).transpose();
if (has_bounds) {
xr = clip(xr);
}
Scalar fxr = func(xr);
fcalls++;
bool doshrink = false;
if (fxr < fsim(0)) {
// 反射点比最好点还好,尝试扩展
Vector xe = (1 + rho * chi) * xbar - rho * chi * sim.row(N).transpose();
if (has_bounds) {
xe = clip(xe);
}
Scalar fxe = func(xe);
fcalls++;
if (fxe < fxr) {
// 扩展点更好,接受扩展
sim.row(N) = xe.transpose();
fsim(N) = fxe;
} else {
// 反射点更好,接受反射

更多推荐