LoginSignup
7
3

More than 3 years have passed since last update.

ROS講座126 最適化計算をする

Last updated at Posted at 2021-04-07

環境

この記事は以下の環境で動いています。

項目
CPU Core i5-8250U
Ubuntu 18.04
ROS Melodic

インストールについてはROS講座02 インストールを参照してください。
またこの記事のプログラムはgithubにアップロードされています。ROS講座11 gitリポジトリを参照してください。

概要

ロボット系のアルゴリズムの中には最適化計算を行うものが多くあります。python等では簡単にこのような計算ができるcvxpyなどの便利なライブラリがありますが、c++で使えるものとして最適化計算ツールのipoptと自動微分ツールのcppadを紹介します。

install

sudo apt install -y ros-melodic-ifopt
sudo apt install -y cppad

最適化計算とは

最適化計算とは以下で定式化される数列Xに対して、それの値からの制約(L<D(X)<U)の中で最小のf(X)を求める計算です。

min f(X)\\
(X=(x_0,x_1,x_2...)^T)\\
L<D(X)<U

最小を求めるfのことをコスト関数、Dを制約関数といいます。

今回は例として以下のような計算を行います。

min f(X)=x_0^2+x_1^2\\
1<x_0<2\\
2<x_0+x_1<4\\

今回は2変数の単純な例なので以下のように図示してみます。答えは(x0,x1)=(1,1)とわかります。

領域.png

最適化計算のサンプル1

ヤコビアン

ipoptというライブラリを使用します。このライブラリではコスト関数、制約関数の値とそのヤコビアンを入力する必要があります。
ヤコビアンは関数のそれぞれの変数に対する偏微分のベクトルを指します
今回のコスト関数$f=x_0^2+x_1^2$に対してヤコビアンは

j =(df/dx_0, df/dx_1) = (2x_0, 2x_1)

となります。
また制約関数$D=(x_0+x_1)$に対して

j = (dD/dx_0, dD/dx_1) = (1,  1)

となります。

コード

nav_lecture/src/optimize_sample1.cpp
#include <ifopt/problem.h>
#include <ifopt/ipopt_solver.h>
#include <ifopt/constraint_set.h>
#include <ifopt/variable_set.h>
#include <ifopt/cost_term.h>
#include <cmath>
#include <iostream>

// Define Variables
class TestVariables : public ifopt::VariableSet
{
public:
  TestVariables() : VariableSet(2, "var_set1")
  {
    x0_ = 0.0;
    x1_ = 0.0;
  }

  void SetVariables(const VectorXd& x) override
  {
    x0_ = x(0);
    x1_ = x(1);
  };

  VectorXd GetValues() const override
  {
    return Eigen::Vector2d(x0_, x1_);
  };

  VecBound GetBounds() const override
  {
    VecBound bounds(GetRows());
    bounds.at(0) = ifopt::Bounds(1.0, 2.0);
    bounds.at(1) = ifopt::NoBound;
    return bounds;
  }

private:
  double x0_, x1_;
};

// Define Constraint
class TestConstraint : public ifopt::ConstraintSet
{
public:
  TestConstraint() : ConstraintSet(1, "constraint1")
  {
  }
  VectorXd GetValues() const override
  {
    VectorXd g(GetRows());
    Eigen::Vector2d x = GetVariables()->GetComponent("var_set1")->GetValues();
    g(0) = x(0) + x(1); // Calculate Constraint
    return g;
  };
  VecBound GetBounds() const override
  {
    VecBound b(GetRows());
    b.at(0) = ifopt::Bounds(2.0, 4.0); // bound of g(0)
    return b;
  }
  void FillJacobianBlock(std::string var_set, Jacobian& jac_block) const override
  {
    if (var_set == "var_set1")
    {
      Eigen::Vector2d x = GetVariables()->GetComponent("var_set1")->GetValues();
      jac_block.coeffRef(0, 0) = 1.0; // d(g(0))/d(x(0))
      jac_block.coeffRef(0, 1) = 1.0; // d(g(0))/d(x(1))
    }
  }
};

// Define Cost
class TestCost : public ifopt::CostTerm
{
public:
  TestCost() : CostTerm("cost_term1")
  {
  }

  double GetCost() const override
  {
    Eigen::Vector2d x = GetVariables()->GetComponent("var_set1")->GetValues();
    return std::pow(x(0), 2) + std::pow(x(1), 2); // calcurate cost
  };

  void FillJacobianBlock(std::string var_set, Jacobian& jac) const override
  {
    if (var_set == "var_set1")
    {
      Eigen::Vector2d x = GetVariables()->GetComponent("var_set1")->GetValues();
      jac.coeffRef(0, 0) = 2.0 * x(0); // d(c)/d(x(0))
      jac.coeffRef(0, 1) = 2.0 * x(1); // d(c)/d(x(1))
    }
  }
};

int main()
{
  // Define problem
  ifopt::Problem nlp;
  nlp.AddVariableSet(std::make_shared<TestVariables>());
  nlp.AddConstraintSet(std::make_shared<TestConstraint>());
  nlp.AddCostSet(std::make_shared<TestCost>());

  // Initialize solver
  ifopt::IpoptSolver ipopt;
  ipopt.SetOption("linear_solver", "mumps");
  ipopt.SetOption("jacobian_approximation", "exact");
  ipopt.SetOption("print_level", 0); // supress log

  ipopt.Solve(nlp);
  std::cout << nlp.GetOptVariables()->GetValues().transpose() << std::endl;
}
  • 最適化計算をするためには変数クラス、制約クラス、コストクラスの3つのモデルに対するクラスが必要です。
  • 変数クラスはifopt::VariableSetを継承するクラスでコンストラクターでは各変数の初期値を決めます。またGetBounds()メソッドで各変数の上下限を定めることが出来ます。もちろん上下限なしにもできます。
  • 制約クラスはifopt::ConstraintSetを継承するクラスです。制約はx<yのような形ではなくx-y<0の様に必ず値として評価できる形で記述します。GetValues()メソッドではその制約を評価する値を、GetBounds()ではその値の制約をifopt::Bounds(2.0, 4.0)の様に記述します。=1などの場合はifopt::Bounds(1.0, 1.0)と記述します。FillJacobianBlock()では計算用にヤコビアンを入れます。制約はもちろん複数入れることが出来ます。
  • コストクラスはifopt::CostTermを継承するクラスです。GetValues()FillJacobianBlock()メソッドを実装します。

実行

実行すると以下のような表示が出ます。解析解(1,1)とほぼあった値が出ます。

******************************************************************************
This program contains Ipopt, a library for large-scale nonlinear optimization.
 Ipopt is released as open source code under the Eclipse Public License (EPL).
         For more information visit http://projects.coin-or.org/Ipopt
******************************************************************************

 1.00286 0.997138

自動微分のサンプル

上記のようにすれば最適化計算をすれば計算ができますが、正しくヤコビアンを入れないと間違った答が出ます。ヤコビアンの取得を自動化する
ために自動微分ライブラリであるCppAdを使ってみます。

コード

nav_lecture/src/ad_sample.cpp
#include <cppad/cppad.hpp>
#include <iostream>

int main()
{
  std::vector<CppAD::AD<double>> ax(2);
  CppAD::Independent(ax);
  std::vector<CppAD::AD<double>> ay(1);
  ay[0] = 2 * CppAD::pow(ax[0], 3) + CppAD::pow(ax[1], 2);
  CppAD::ADFun<double> f(ax, ay);

  std::vector<double> x(2);
  x[0] = 2;
  x[1] = 3;
  std::vector<double> fw0 = f.Forward(0, x);
  std::cout << fw0[0] << "=f(" << x[0] << "," << x[1] << ")" << std::endl;

  std::vector<double> jac = f.Jacobian(x);
  std::cout << jac[0] << "=dy/dx0(" << x[0] << "," << x[1] << ")" << std::endl;
  std::cout << jac[1] << "=dy/dx1(" << x[0] << "," << x[1] << ")" << std::endl;
}
  • 今回対象とする関数は$2x_0^3+x_1^2$です。一応手でヤコビアンを計算すると$(\frac{dy}{dx_0}, \frac{dy}{dx_1}) = (6x_0^2, 2x_1)$となります。
  • f.Forward(0, x)では$x=(x_0,x_1)=(2,3)$に対してこの関数の元の値(=25)が取得できます。
  • f.Jacobian(x)では$x=(x_0,x_1)=(2,3)$にたいしてこの関数のヤコビアンの値($(\frac{dy}{dx_0}, \frac{dy}{dx_1}) = (24, 4)$)が取得できます。

実行結果

25=f(2,3)
24=dy/dx0(2,3)
6=dy/dx1(2,3)

最適化計算のサンプル2

ではCppAdを使って上記のサンプルを書き直します。

nav_lecture/src/optimize_sample2.cpp
#include <ifopt/problem.h>
#include <ifopt/ipopt_solver.h>
#include <ifopt/constraint_set.h>
#include <ifopt/variable_set.h>
#include <ifopt/cost_term.h>
#include <cppad/cppad.hpp>
#include <cmath>
#include <iostream>

// Define Variables
class TestVariables : public ifopt::VariableSet
{
public:
  TestVariables() : VariableSet(2, "var_set1")
  {
    x0_ = 0.0;
    x1_ = 0.0;
  }

  void SetVariables(const VectorXd& x) override
  {
    x0_ = x(0);
    x1_ = x(1);
  };

  VectorXd GetValues() const override
  {
    return Eigen::Vector2d(x0_, x1_);
  };

  VecBound GetBounds() const override
  {
    VecBound bounds(GetRows());
    bounds.at(0) = ifopt::Bounds(1.0, 2.0);
    bounds.at(1) = ifopt::NoBound;
    return bounds;
  }

private:
  double x0_, x1_;
};

// Define Constraint
class TestConstraint : public ifopt::ConstraintSet
{
public:
  TestConstraint() : ConstraintSet(1, "constraint1")
  {
  }
  VectorXd GetValues() const override
  {
    Eigen::Vector2d x = GetVariables()->GetComponent("var_set1")->GetValues();
    std::vector<double> tx(2);
    tx[0] = x(0);
    tx[1] = x(1);
    std::vector<double> fw = getAdFun().Forward(0, tx);
    VectorXd g(GetRows());
    g(0) = fw[0];
    return g;
  };
  VecBound GetBounds() const override
  {
    VecBound b(GetRows());
    b.at(0) = ifopt::Bounds(2.0, 4.0); // bound of g(0)
    return b;
  }
  void FillJacobianBlock(std::string var_set, Jacobian& jac_block) const override
  {
    if (var_set == "var_set1")
    {
      Eigen::Vector2d x = GetVariables()->GetComponent("var_set1")->GetValues();
      std::vector<double> tx(2);
      tx[0] = x(0);
      tx[1] = x(1);
      std::vector<double> ad_jac = getAdFun().Jacobian(tx);
      jac_block.coeffRef(0, 0) = ad_jac[0]; // d(c)/d(x(0))
      jac_block.coeffRef(0, 1) = ad_jac[1]; // d(c)/d(x(1))
    }
  }
private:
  CppAD::ADFun<double> getAdFun(void) const {
    std::vector<CppAD::AD<double>> ax(2);
    CppAD::Independent(ax);
    std::vector<CppAD::AD<double>> ay(1);
    ay[0] = ax[0] + ax[1];
    CppAD::ADFun<double> f(ax, ay);
    return f;
  }
};

// Define Cost
class TestCost : public ifopt::CostTerm
{
public:
  TestCost() : CostTerm("cost_term1")
  {
  }

  double GetCost() const override
  {
    Eigen::Vector2d x = GetVariables()->GetComponent("var_set1")->GetValues();
    std::vector<double> tx(2);
    tx[0] = x(0);
    tx[1] = x(1);
    std::vector<double> fw0;
    fw0 = getAdFun().Forward(0, tx);
    return fw0[0];
  };

  void FillJacobianBlock(std::string var_set, Jacobian& jac) const override
  {
    if (var_set == "var_set1")
    {
      Eigen::Vector2d x = GetVariables()->GetComponent("var_set1")->GetValues();
      std::vector<double> tx(2);
      tx[0] = x(0);
      tx[1] = x(1);
      std::vector<double> ad_jac = getAdFun().Jacobian(tx);
      jac.coeffRef(0, 0) = ad_jac[0]; // d(c)/d(x(0))
      jac.coeffRef(0, 1) = ad_jac[1]; // d(c)/d(x(1))
    }
  }

private:
  CppAD::ADFun<double> getAdFun(void) const {
    std::vector<CppAD::AD<double>> ax(2);
    CppAD::Independent(ax);
    std::vector<CppAD::AD<double>> ay(1);
    ay[0] = CppAD::pow(ax[0], 2) + CppAD::pow(ax[1], 2);
    CppAD::ADFun<double> f(ax, ay);
    return f;
  }
};

int main()
{
  // Define problem
  ifopt::Problem nlp;
  nlp.AddVariableSet(std::make_shared<TestVariables>());
  nlp.AddConstraintSet(std::make_shared<TestConstraint>());
  nlp.AddCostSet(std::make_shared<TestCost>());

  // Initialize solver
  ifopt::IpoptSolver ipopt;
  ipopt.SetOption("linear_solver", "mumps");
  ipopt.SetOption("jacobian_approximation", "exact");
  ipopt.SetOption("print_level", 0); // supress log
  ipopt.SetOption("sb", "yes"); // supress startup message

  ipopt.Solve(nlp);
  std::cout << nlp.GetOptVariables()->GetValues().transpose() << std::endl;
}
  • ipopt.SetOption("sb", "yes")とすると最適化計算時のスタートアップメッセージが出なくなります

実行

 1.00286 0.997138

参考

ifopt
ifopt API

目次ページへのリンク

ROS講座の目次へのリンク

7
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
3