本篇針對非線性規(優)劃(化)問題,介紹兩個好用的模型構建不求解工具:CppAD與pyomo 。這兩個工具都可以只構建目標函數與約束函數的形式,而不需要求其雅可比、海森矩陣。兩個工具都可以接受非線性模型形式,並且有與各種優化求解器的接口。
系統:ubuntu 18.04 gcc-7.4.0 g++-7.4.0
1. 背景
系統的真實模型都是非線性,在實際模型構建或使用過程中,我們會對模型進行線性化。例如,我們根據已有的模型求解最優控制問題:讓機器人根據當前的狀態輸出最優的動作,從而使成本函數取最小值。
常見的優化庫都需要我們求目標函數與約速函數的導數、雅可比矩陣以及海森矩陣。如果模型簡單,姑且可以手動求解,模型一旦複雜些(例如大型的非線性模型,約束函數爲微分方程or差分方程形式),非常容易出錯。雖然也有一些庫能夠用於自動微分,例如python的:sympy、autograd、tangent等,c++的:Automatic differentiation(AD)。這些庫僅僅用來微分求導的話是非常合適的,如果要跟最優化求解器(也即優化庫)結合,需要相當多的結口函數。同樣的情況,當問題變得複雜,頭就大了,非常容易出錯,且不好調試。
針對上面的問題,本篇介紹c++與python中將自動微分與優化求解器融合一體的庫:CppAD (for c++), pyomo (for python)。我們只需要確定模型的目標函數、約束函數的形式,這些工具就可以幫我們算出在約束條件下,使目標函數取最值的解。重點:這兩個工具都不需要我們對模型進行線性化,不需要手動求雅可比、海森矩陣等。
2. CppAD [for C++]
2.1 CppAD簡介
CppAD是Bradley M. Bell針對運籌學COIN-OR的計算基礎部分進行的一個開源項目。CppAD特點如下:
- 支持正向、反向的優化求解模式;
- 任何階數的導數;
- 使用自動微分(AD)的功能;
- 稀疏模式;
- 數值庫模版(可以與其它AD庫一起用);
- 很多使用例程;
- 支持windows 與 Linux系統
CppAD源碼:https://github.com/coin-or/CppAD
說明文檔:https://coin-or.github.io/CppAD/doc/cppad.htm#Features.Base%20Type
應用例程集錦:https://coin-or.github.io/CppAD/doc/listallexamples.htm
CppAD: A Package for Differentiation of C++ Algorithms
2.2 CppAD安裝
Ubuntu : sudo apt-get install cppad
CppAD需要調用系統中已經安裝好的優化求解器,如非線性優化器ipopt、Gurobi、GLPK。由於本篇介紹使用CppAD的例子中用到ipopt,此處先介紹一下ipopt的安裝.
Ubuntu下:
sudo apt-get install gfortran
sudo apt-get install unzip
wget https://www.coin-or.org/download/source/Ipopt/Ipopt-3.12.7.zip && unzip Ipopt-3.12.7.zip && rm Ipopt-3.12.7.zip
sudo bash install_ipopt.sh Ipopt-3.12.7
2.3 一個例子
這個例子利用CppAD與Ipopt求解以下非線性規劃問題(這個問題類似於各編程語言的print(“hello world”)):
完整c++代碼如下:
#include <iostream>
#include <cppad/ipopt/solve.hpp>
using namespace std;
namespace {
using CppAD::AD;
class FG_eval {
public:
typedef CPPAD_TESTVECTOR(AD<double>) ADvector;
void operator()(ADvector& fg, const ADvector& x)
{
assert(fg.size() == 3);
assert(x.size() == 4);
// variables
AD<double> x1 = x[0];
AD<double> x2 = x[1];
AD<double> x3 = x[2];
AD<double> x4 = x[3];
// f(x) objective function
fg[0] = x1 * x4 * (x1 + x2 + x3) + x3;
// constraints
fg[1] = x1 * x2 * x3 * x4;
fg[2] = x1 * x1 + x2 * x2 + x3 * x3 + x4 * x4;
return;
}
};
}
bool get_started(void)
{
bool ok = true;
size_t i;
typedef CPPAD_TESTVECTOR(double) Dvector;
size_t nx = 4; // number of varibles
size_t ng = 2; // number of constraints
Dvector x0(nx); // initial condition of varibles
x0[0] = 1.0;
x0[1] = 5.0;
x0[2] = 5.0;
x0[3] = 1.0;
// lower and upper bounds for varibles
Dvector xl(nx), xu(nx);
for(i = 0; i < nx; i++)
{
xl[i] = 1.0;
xu[i] = 5.0;
}
Dvector gl(ng), gu(ng);
gl[0] = 25.0; gu[0] = 1.0e19;
gl[1] = 40.0; gu[1] = 40.0;
// object that computes objective and constraints
FG_eval fg_eval;
// options
string options;
// turn off any printing
options += "Integer print_level 0\n";
options += "String sb yes\n";
// maximum iterations
options += "Integer max_iter 10\n";
//approximate accuracy in first order necessary conditions;
// see Mathematical Programming, Volume 106, Number 1,
// Pages 25-57, Equation (6)
options += "Numeric tol 1e-6\n";
//derivative tesing
options += "String derivative_test second-order\n";
// maximum amount of random pertubation; e.g.,
// when evaluation finite diff
options += "Numeric point_perturbation_radius 0.\n";
CppAD::ipopt::solve_result<Dvector> solution; // solution
CppAD::ipopt::solve<Dvector, FG_eval>(options, x0, xl, xu, gl, gu, fg_eval, solution); // solve the problem
cout<<"solution: "<<solution.x<<endl;
//
//check some of the solution values
//
ok &= solution.status == CppAD::ipopt::solve_result<Dvector>::success;
//
double check_x[] = {1.000000, 4.743000, 3.82115, 1.379408};
double check_zl[] = {1.087871, 0., 0., 0. };
double check_zu[] = {0., 0., 0., 0. };
double rel_tol = 1e-6; // relative tolerance
double abs_tol = 1e-6; // absolute tolerance
for(i = 0; i < nx; i++)
{
ok &= CppAD::NearEqual(
check_x[i], solution.x[i], rel_tol, abs_tol);
ok &= CppAD::NearEqual(
check_zl[i], solution.zl[i], rel_tol, abs_tol);
ok &= CppAD::NearEqual(
check_zu[i], solution.zu[i], rel_tol, abs_tol);
}
return ok;
}
int main()
{
cout << "CppAD : Hello World Demo!" << endl;
get_started();
return 0;
}
輸出如下:
我們只需要設置初始參數、初始狀態、狀態約束值、等式與不等式約束值、優化目標方程。其它的就交給CppAD吧!
3. pyomo [for Python]
3.1 pyomo簡介
pyomo與CppAD的功能一致,最終我們僅僅需要把待求解問題構建好(可以是非線性,不需要手動求微分),然後扔給它就行了。但是,pyomo比CppAD好的一點是:pyomo具有深刻的模型概念,它把整個問題就當作是一個模型。CppAD雖然功能與pyomo相似,但是卻沒有這樣一個比較讓容易把握整體的概念。
3.2 pyomo安裝
強列建議使用anaconda環境! 強列建議使用anaconda環境! 強列建議使用anaconda環境!
本人環境:Ubuntu 18.04 + Anaconda2.7 (python 2.7)
本人開始是嘗試pip install來安裝配置pyomo,出現很多問題,例如:能裝上包,但是運行出錯(可能是版本問題)。因此,本人乾脆安裝pyomo安裝教程使用Anaconda環境。然後一切非常順利。Anaconda的一個比較全的安裝教程。
在Ubuntu + Anaconda環境下安裝pyomo與ipopt:
conda update conda
conda update anaconda
conda install -c conda-forge pyomo
conda install -c conda-forge pyomo.extras
conda install -c conda-forge coincbc
conda install -c conda-forge ipopt
當然,也可以安裝基它可能會用到的優化求解庫:
conda install -c conda-forge glpk
3.3 一個例子
此處仍採用式(1)作爲求解對象來給出pyomo的一個應用例子。
import numpy as np
from pyomo.environ import *
from pyomo.dae import *
model = ConcreteModel()
model.x = Var(RangeSet(1, 4), bounds=(1, 25))
model.cons1 = Constraint(rule=lambda model: 40==model.x[1]**2+model.x[2]**2+model.x[3]**2+model.x[4]**2)
model.cons2 = Constraint(rule=lambda model: 25<=model.x[1]*model.x[2]*model.x[3]*model.x[4])
model.obj = Objective(expr = model.x[1]*model.x[4]*(model.x[1] + model.x[2] + model.x[3]) + model.x[3], sense=minimize)
SolverFactory('ipopt').solve(model)
solutions = [model.x[i]() for i in range(1, 5)]
print("solutins is :"+str(solutions))
對比CppAD與pyomo求解的結果(如下),兩者是一致的(但是,pyomo的代碼更爲簡潔):
CppAD與pyomo時間成本比較:
pyomo:time cost is:0.0659720897675
CppAD: time cost is:0.004138
總結
爲了簡化求解最優問題的過程,本篇介紹了兩種非線性規劃工具:CppAD for C++;pyomo for python。兩者都能省去求雅可比、海森矩陣的過程。我們只需要定義好求解的模型即可。兩個工具在同一問題上求解結果一致,但是CppAD比pyomo時間成本更小。但是,pyomo在實現代碼量上明顯小於CppAD,這是pyomo的優點。如果系統對實時性要求高,建議採用C++編程+CppAD,如果只是平時做簡單算法測試,建議採用更簡潔的python+pyomo組合。
以上
by windSeS 2019-12-20