65.9K
CodeProject 正在变化。 阅读更多。
Home

适用于 .NET 和 Java 的无导数非线性优化

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (6投票s)

2012年12月14日

MIT

10分钟阅读

viewsIcon

36989

downloadIcon

956

宣布适用于 .NET 和 Java 平台的无导数非线性优化器的独立实现

介绍 

非线性目标函数和潜在的非线性约束的优化在许多科学、工程和商业领域是一个常见的问题。 

但目标函数和约束函数通常不易于微分,这限制了应用更高效的基于导数的优化算法的可能性(其中 IPOPT 是一个著名的开源示例)。

有几种或多或少有效率地解决(约束)非线性优化问题的方法,包括模拟退火、遗传算法和直接搜索方法,例如 Nelder-Mead 方法(这里 提供了对数学优化方法的良好概述,并包含多个链接)。 

开发进展 

.NET 的 COBYLA2 

一个相当流行的代码,用于解决涉及(潜在非线性)约束的非线性优化问题是 *COBYLA2* 方法。最初由 Michael Powell 在 Fortran 77 中制定和实现,该方法已被移植到 Fortran 90C(通过 f2c 转换),并且还被集成到 Python 科学计算库 scipy 中。*COBYLA2* 使用简单,但收敛速度相当慢;定位满足约束的最优值所需的目标函数和约束函数评估次数通常非常大。然而,当目标函数和约束函数的梯度复杂或派生耗时时,*COBYLA2* 通常是作为首选求解器的一个不错的选择。

由于我主要是一名 .NET 开发人员,直到最近才能够方便地集成 *COBYLA2*。当然,可以通过 P/Invoke 构建一个本地 DLL 并调用 *COBYLA2*。这在完整的 .NET Framework 环境中相对有效,但在 Silverlight 应用程序中仅勉强适用,在 Windows Phone 应用程序中则完全不适用。另一方面,Fortran 代码结构相对良好,文档也完善,所以我最近决定将 *COBYLA2* 移植到 C#。

我基于 Fortran 90 代码进行移植,因为该实现已经利用了更多类似 C/C++/C# 的结构。移植成功了,我已将最终结果作为开源项目 cscobyla 发布在 Github 上。该项目包含演示 C# 应用程序中 *COBYLA2* 用法的单元测试。C# 代码完全可移植到所有 Windows 技术,因此应该可以轻松地将其集成到 Windows Store(又称 Metro)、Silverlight 和 Windows Phone 应用程序中,就像集成到常规 .NET 应用程序中一样。当构建为 C# 类库时,从任何其他 .NET 语言引用 *COBYLA2* 优化也应该很方便。

Java 的 COBYLA2 

在成功移植到 C# 的鼓励下,我接着开始了 Java 移植的尝试!我找不到纯 Java 的 *COBYLA2* 实现,所以我认为这是一个很好的 Java 开发 练习。Java 不提供委托,多维数组只能表示为锯齿数组,Java 也不支持 *goto* 语句,因此我被迫做了一些设计上的更改。尽管如此,Java 移植工作也成功了,我也已将此结果作为开源项目发布在 Github 上,即 jcobyla 项目。

.NET 的 BOBYQA  

这还不是全部!如前所述,*COBYLA2* 收敛速度慢。Michael Powell 在解决特定情况下的问题方面付出了许多努力,通过使用局部二次近似而不是 *COBYLA2* 中使用的线性近似。这些改进已在用于非线性目标函数无约束优化的 NEWUOA 代码中提供,最近又在用于非线性目标函数有界约束优化的 BOBYQA(二次近似的有界优化)代码中提供。与 *COBYLA2* 相比,这些代码表现出显著改善的收敛性能,尽管仅限于无约束或有界约束问题。 

特别地,我认为 *BOBYQA* 是一个非常有趣的发展,因为许多问题,例如在我自己专长的放射治疗领域遇到的问题,都可以被表述为有界约束问题。*BOBYQA* 除了通过 P/Invoke 之外,一直没有适用于 .NET 平台,所以再次我决定将 Fortran 代码移植到 C#。这次我不得不依赖 Fortran 77 实现,因为我还没有找到任何 *BOBYQA* 的 Fortran 90 移植版本。这花费了一些额外的努力,但即使是 *BOBYQA* 现在也以开源 C# 的形式可用。我将此项目命名为 csbobyqa 并将其发布在 Github 上。该代码也使用标准的 C# 结构编写,应易于集成到任何 .NET、Windows Store (Metro)、Silverlight 或 Windows Phone 应用程序中。

Java 的 BOBYQA  

在 *BOBYQA* 的情况下,实际上已经有一个开源 Java 实现作为 Apache Commons Math 项目的一部分。这个 Java 类的 API 可以在 这里找到。

为了确认我的 C# 实现足够高效,我还确定了 Java 实现中运行时间最长的单元测试(具有不同插值点数量的有界约束 Rosen 函数),并在 C# 单元测试库中实现了相应的单元测试。在我的笔记本电脑上,C# 单元测试每次连续运行时大约需要 15 秒,而 Java 实现的相应单元测试则需要 15 到 40 秒。令人鼓舞的是,在这种情况下,C# 实现的性能持续等于或优于 Java 实现。 

使用代码

这些实现相对忠实于原始的 Fortran 77 和 90 实现。然而,应该注意的是,这些实现中的变量和约束数组的索引是从零开始的,也就是说,对于一个有 3 个变量的问题,在目标函数(以及在适用的情况下,约束函数)的评估中应该使用 `x[0]`、`x[1]` 和 `x[2]`。 

从 C# 调用 COBYLA2  

要成功地从 C# 调用 *COBYLA2* 算法,请将 *cscobyla* 项目中的 *Cobyla.cs* 文件集成到您的应用程序中。然后,实现一个计算目标函数和(潜在)约束函数的方法,签名如下: 

void calcfc(int n, int m, double[] x, out double f, double[] con)

其中 `n` 是变量的数量,`m` 是约束的数量,`x` 是变量数组,`f` 是计算出的目标函数值,`con` 是计算出的约束函数值数组。 

为了在约束条件下最小化目标函数,请调用两个重载的 `Cobyla.FindMinimum` 方法之一:

CobylaExitStatus FindMinimum(CalcfcDelegate calcfc, int n, int m, double[] x,
                 double rhobeg, double rhoend, int iprint, int maxfun,
                 out double f, out double[] con, out int iters, TextWriter logger);
CobylaExitStatus FindMinimum(CalcfcDelegate calcfc, int n, int m, double[] x,
                 double rhobeg, double rhoend, int iprint, int maxfun, TextWriter logger);

其中,输入时的 `x` 是初始变量数组,`rhobeg` 和 `rhoend` 是单纯形的初始值和最终值,`iprint` (0..3) 指定控制台输出级别,`maxfun` 是允许的最大函数评估次数,`f` 是最优变量的目标函数值,`con` 是最优变量的约束函数值,`iters` 是优化中实际执行的函数评估次数。如果定义了 `logger`,则它是一个文本写入器,用于记录 *COBYLA2* 的输出。

返回值 `x` 是获得的优化变量值。该方法返回最终优化状态,该状态是正常终止、达到最大迭代次数或舍入误差发散之一。

后一个重载方法还实现了默认值,如下所示:`rhobeg = 0.5`,`rhoend = 1.0e-6`,`iprint = 2`,`maxfun = 3500`,`logger = null`。因此,可以这样选择性地调用该方法,在最小化中使用默认参数值:

var status = Cobyla.FindMinimum(calcfc, n, m, x); 

下面是一个简单的例子,用于最小化两个变量的乘积,前提是变量被限制在单位圆的边界内。首先,实现目标函数和约束计算方法:

public static void calcfc1(int n, int m, double[] x, out double f, double[] con)
{
    f = x[0] * x[1];
    con[0] = 1.0 - x[0] * x[0] - x[1] * x[1];
} 

隐含地,要求所有约束函数值都为非负,即 `con[j] ≥ 0` 对于所有约束 `j`。 

要执行实际优化,请定义初始变量值并调用 `Cobyla.FindMinimum`: 

var x = new[] { 1.0, 1.0 };
var status = Cobyla.FindMinimum(calcfc1, 2, 1, x);  

从 Java 调用 COBYLA2  

*jcobyla* 项目中的 *Cobyla.java*、*Calcfc.java* 和 *CobylaExitStatus.java* 文件可以包含在任何 Java 项目的 *com.cureos.numerics* 包中。 

在 Java 中,目标函数和(潜在)约束函数计算由 `Calcfc` 接口中的 `Compute` 方法表示。显式或匿名地实现该接口。`Compute` 方法的签名如下: 

double Compute(int n, int m, double[] x, double[] con)

其中 `n` 是变量的数量,`m` 是约束的数量,`x` 是变量数组,`con` 是计算出的约束函数值数组。该方法应返回目标函数的值。为了在约束条件下最小化目标函数,请调用静态 `Cobyla.FindMinimum` 方法:

CobylaExitStatus FindMinimum(Calcfc calcfc, int n, int m, double[] x,
                 double rhobeg, double rhoend, int iprint, int maxfun); 

其中,输入时的 `x` 是初始变量数组,`rhobeg` 和 `rhoend` 是单纯形的初始值和最终值,`iprint` (0..3) 指定控制台输出级别,`maxfun` 是允许的最大函数评估次数。 

输出时,`x` 包含获得的优化变量值。该方法返回最终优化状态,该状态是正常终止、达到最大迭代次数或舍入误差发散之一。 

这是一个摘自 Roger Fletcher 的著作《*Practical Methods of Optimization, Volume 2*》的简单示例(方程式 9.1.15)。首先,实现 `Calcfc` 接口。可以完全进行隐式接口实现:

Calcfc calcfc = new Calcfc() {
    @Override
    public double Compute(int n, int m, double[] x, double[] con) {
        con[0] = x[1] - x[0] * x[0];
        con[1] = 1.0 - x[0] * x[0] - x[1] * x[1];
        return -x[0] - x[1];
    }
}; 

隐含地,要求所有约束函数值都为非负,即 `con[j] ≥ 0` 对于所有约束 `j`。 接下来,定义初始变量值并使用合适的控制参数调用 `Cobyla.FindMinimum`:

double[] x = {1.0, 1.0 };
CobylaExitStatus result = Cobyla.FindMinimum(calcfc, 2, 2, x, 0.5, 1.0e-6, 1, 3500); 

从 C# 调用 BOBYQA

要成功地从 C# 调用 *BOBYQA* 算法,请将 *csbobyqa* 项目中的 *Bobyqa.cs* 文件集成到您的应用程序中。然后,实现一个计算目标函数的签名方法:

double calfun(int n, double[] x)

其中 `n` 是变量的数量,`x` 是变量数组。该方法应返回计算出的目标函数值。 

为了在边界约束条件下最小化目标函数,请调用静态 `Bobyqa.FindMinimum` 方法:

BobyqaExitStatus FindMinimum(Func<int, double[], double> calfun, int n, double[] x, 
                                 double[] xl, double[] xu, int npt, double rhobeg, 
                                 double rhoend, int iprint, int maxfun, TextWriter logger)

其中,输入时的 `x` 是初始变量数组,`xl` 和 `xu` 分别是变量的下界和上界,`npt` 是插值条件的数量(推荐值为 `2 * n + 1`),`rhobeg` 和 `rhoend` 是信任域半径的初始值和最终值,`iprint` (0..3) 指定控制台输出级别,`maxfun` 是允许的最大函数评估次数,`logger` 是一个文本写入器,BOBYQA 的日志将输出到该写入器。 

如果 `xl` 和/或 `xu` 被设置为 `null`,则所有优化变量都被认为分别在向下和向上无界。如果 `npt` 被设置为非正值,则优化中应用的 `npt` 值等于 `2 * n + 1`。如果 `rhobeg` 被设置为非正值,则优化中应用的 `rhobeg` 值将基于变量的起始值和边界范围。如果 `rhoend` 被设置为非正值,则优化中应用的 `rhoend` 值将是应用 `rhobeg` 的一百万分之一。 

该 `FindMinimum` 方法还实现了默认值,如下所示:`xl = null`,`xu = null`,`npt = -1`,`rhobeg = -1`,`rhoend = -1`,`iprint = 1`,`maxfun = 10000`,`logger = null`。因此,可以这样调用该方法来求解无界优化问题,在最小化中使用上述默认参数值:

var status = Bobyqa.FindMinimum(calfun, n, x);

输出时,`x` 提供获得的优化变量值。该方法返回一个枚举的优化状态值,如果优化成功执行,则应等于 `Normal`。 

这是一个简单的例子 HS05,摘自 Hock-Schittkowski 集合。请注意,目标函数不必是外部定义的,也可以足够地包含在方法调用中的 lambda 表达式中: 

var xx = new[] { 0.0, 0.0 };
var status = Bobyqa.FindMinimum(
    (n, x) =>                 /* Objective function */
        Math.Sin(x[0] + x[1]) + Math.Pow(x[0] - x[1], 2.0) - 1.5 * x[0] + 2.5 * x[1] + 1, 
    2,                        /* Number of variables */
    xx,                       /* Variable array */
    new[] { -1.5, -3.0 },     /* Lower bounds */
    new[] { 4.0, 3.0 });      /* Upper bounds */  

代码位置 

本文包含相应软件包的最新修订版本。在 C# 存档中,已排除用于第三方库管理的 *NuGet* 可执行文件。包含 *NuGet* 的完整 Visual Studio 解决方案可在 GitHub 上获取。 

重申一下,以下是我开源优化项目的链接:

.NET COBYLA2: https://github.com/cureos/cscobyla
.NET BOBYQA: https://github.com/cureos/csbobyqa
Java COBYLA2: https://github.com/cureos/jcobyla

Java 版的 *BOBYQA* 已集成到 Apache Commons Math 中:http://commons.apache.org/math/ 

对 Java 版 *COBYLA2* 的开发也启发了 Reinhard Oldenburg 制作了一个 Javascript 版的 *COBYLA2*。因此,对于那些希望直接在网站上集成非线性优化的人来说,这里 有一个很好的选择。 

祝您无导数优化顺利! 

历史

2012年12月14日:初版,改编自 我博客上的原文。 
2012年12月19日:向文章添加了源代码的最新修订版本。 

© . All rights reserved.