Ceres の紹介と事例 (10) デリバティブについて(自動デリバティブ)

次に、自動微分アルゴリズムについて説明します。これは、ユーザーが数値微分と同様の作業を行うだけで、正確な導関数を迅速に計算できるアルゴリズムです。以下のコード スニペットは、 Rat43の CostFunction を実装します

struct Rat43CostFunctor {
    
    
  Rat43CostFunctor(const double x, const double y) : x_(x), y_(y) {
    
    }

  template <typename T>
  bool operator()(const T* parameters, T* residuals) const {
    
    
    const T b1 = parameters[0];
    const T b2 = parameters[1];
    const T b3 = parameters[2];
    const T b4 = parameters[3];
    residuals[0] = b1 * pow(1.0 + exp(b2 -  b3 * x_), -1.0 / b4) - y_;
    return true;
  }

  private:
    const double x_;
    const double y_;
};


CostFunction* cost_function =
      new AutoDiffCostFunction<Rat43CostFunctor, 1, 4>(
        new Rat43CostFunctor(x, y));

自動微分ファンクターを定義する場合の唯一の違いは、数値微分と比較して、operator() の設定であることに注意してください。
PS: 数値微分のためのコード

			struct Rat43CostFunctor {
    
    
			  Rat43CostFunctor(const double x, const double y) : x_(x), y_(y) {
    
    }
			
			  bool operator()(const double* parameters, double* residuals) const {
    
    
			    const double b1 = parameters[0];
			    const double b2 = parameters[1];
			    const double b3 = parameters[2];
			    const double b4 = parameters[3];
			    residuals[0] = b1 * pow(1.0 + exp(b2 -  b3 * x_), -1.0 / b4) - y_;
			    return true;
			  }
			
			  const double x_;
			  const double y_;
			}
			
			CostFunction* cost_function =
			  new NumericDiffCostFunction<Rat43CostFunctor, FORWARD, 1, 4>(
			    new Rat43CostFunctor(x, y));

数値微分の場合は、

bool operator()(const double* parameters, double* residuals) const;

自動微分を行うため、次の形式のテンプレート関数です。

template <typename T> bool operator()(const T* parameters, T* residuals) const;

この変更による影響は何ですか? 以下の表は、Rat43 の残差とヤコビアンをさまざまな方法で計算するのに必要な時間を比較しています。

コスト関数

時間 (ns)

Rat43分析

255

Rat43Analytic最適化済み

92

Rat43NumericDiffForward

262

Rat43数値差中央

517

Rat43NumericDiffRidders

3760

Rat43自動差分

129

自動微分 (Rat43AutomaticDiff) を使用すると、正確な微分を取得できます。これは、数値微分コードを記述するのとほぼ同じ作業量ですが、手動で最適化した分析微分よりも 40% 遅いだけです。

では、それはどのように機能するのでしょうか?そのために、デュアル ナンバーとジェットについて学びます。

デュアルナンバーから偶数とジェットへ

Ceres ソルバーでの自動微分の使用には直接関係しない、Jets の実装に関するこのサブセクションと次のセクションをお読みください。ただし、Jets がどのように機能するかを理解することは、デバッグや自動微分のパフォーマンスについて推論するときに非常に役立ちます。

双対偶数は、複素数と同様に実数の拡張です。一方、複素数では虚数単位i 2 = − 1 i^2=-1が導入されます。2=1で実数を拡張し、偶数に無限小単位ϵ ϵϵ、したがってϵ 2 = 0 ϵ^2=0ϵ2=0
偶数a + v ϵ a+vϵある+v ϵには、実数成分 a と微小成分 v の 2 つの成分があります。
PS: 偶数を参照 https://zhuanlan.zhihu.com/p/380140763

驚くべきことに、この単純な変更により、複雑なシンボリック式を操作せずに正確な導関数を計算する便利な方法が得られます。たとえば、次の関数
f ( x ) = x 2 、 f(x) = x^2 について考えてみましょう。f ( x )=バツ2
方程式、
f ( 10 + ϵ ) = ( 10 + ϵ ) 2 = 100 + 20 ϵ + ϵ 2 = 100 + 20 ϵ \begin{split} f(10 + \epsilon) &= (10 + \epsilon) ^2\\ &= 100 + 20 \epsilon + \epsilon^2\\ &= 100 + 20 \epsilon \end{split}f ( 10+) _=( 10+) _2=100+20ϵ _+ϵ2=100+20ϵ _

ϵ の係数を見ると、Df(10)=20 であることがわかります。実際、これは多項式ではない関数にも一般化できます。任意の微分可能な関数 f(x) を考えると、
x の周りの f のテイラー展開を考慮することでf ( x + ϵ ) f(x + \epsilon) を計算できます。f ( x+ϵ )、無限小関数
f ( x + ϵ ) = f ( x ) + D f ( x ) ϵ + D 2 f ( x ) ϵ 2 2 + D 3 f ( x ) ϵ 3 6 + ⋯ f ( x + ϵ ) = f ( x ) + D f ( x ) ϵ \begin{split} f(x + \epsilon) &= f(x) + Df(x) \epsilon + D^2f(x) \frac {\epsilon^2}{2} + D^3f(x) \frac{\epsilon^3}{6} + \cdots\\ f(x + \epsilon) &= f(x) + Df( x) \epsilon\end{分割}f ( x+) _f ( x+ϵ)。=f ( x )+D f ( x ) ϵ+D2 f(x)2ϵ2+D3 f(x)6ϵ3+=f ( x )+D f ( x ) ϵ

ϵ 2 = 0 \epsilon^2 = 0 に注意してください。ϵ2=0

Jet は n 次元の双対数です。このうち n 個の無限小単位ϵ i , i = 1 , . . . , n \epsilon_i,\ i=1,...,n を使用します。ϵ私は =1 ... n は実数を増やすための性質であり、性質∀ i , j : ϵ i ϵ j = 0 \forall i, j\ :\epsilon_i\epsilon_j = 0∀i _j :ϵ私はϵj=0この場合、Jet は実数部 a と n 次元の微小部 v で構成されます。つまり、
x = a + ∑ jvj ϵ jx = a + \sum_j v_{j} \epsilon_jバツ=ある+jvjϵj

和の表記は面倒なので、
x = a + v . x = a + \mathbf{v} と書きましょう。バツ=ある+v

上の式ϵ i \epsilon_iϵ私はが暗示されています。次に、上記と同じテイラー級数展開を使用すると、 f ( a + v ) = f ( a ) + D f ( a ) v . f(a + \mathbf{v}) = f(a ) + がわかります
Df(a) \mathbf{v}。f ( _+v )=f ( a )+D f ( a ) v
同様に、多変量関数f の場合、 R n → R mf:\mathbb{R}^{n}\rightarrow \mathbb{R}^mf:RnRmxi = ai + vi , ∀ i = 1 ,, n x_i = a_i + \mathbf{v}_i,\ \forall i = 1,...,nバツ私は=ある私は+v私は ∀i _=1 ... n

すべてのvi = ei \mathbf{v}_i = e_iについてv私は=e私はは i 番目の標準基底ベクトルです。次に、上記の式を
f ( x 1 , . . . , xn ) = f ( a 1 , . . . , an ) + ∑ i D if ( a 1 , . . . , an ) ϵ if( x_1, ..., x_n) = f(a_1, ..., a_n) + \sum_i D_i f(a_1, ..., a_n) \epsilon_if ( x1... バツ)=f ( _1... ある)+D私はf ( _1... ある) ϵ私は

の係数を調べることでヤコビアン座標ϵ i \epsilon_iを抽出できます。ϵ私は

ジェットの実装

実際に役立つようにするには、実数だけでなく双対数についても任意の関数 f を計算できる必要がありますが、通常はテイラー展開を介して関数を計算しません。

ここで、C++ テンプレートと演算子のオーバーロードが登場します。以下のコード スニペットには、Jet の単純な実装と、それらを操作するためのいくつかの演算子/関数が含まれています。

template<int N> struct Jet {
    
    
  double a;
  Eigen::Matrix<double, 1, N> v;
};

template<int N> Jet<N> operator+(const Jet<N>& f, const Jet<N>& g) {
    
    
  return Jet<N>(f.a + g.a, f.v + g.v);
}

template<int N> Jet<N> operator-(const Jet<N>& f, const Jet<N>& g) {
    
    
  return Jet<N>(f.a - g.a, f.v - g.v);
}

template<int N> Jet<N> operator*(const Jet<N>& f, const Jet<N>& g) {
    
    
  return Jet<N>(f.a * g.a, f.a * g.v + f.v * g.a);
}

template<int N> Jet<N> operator/(const Jet<N>& f, const Jet<N>& g) {
    
    
  return Jet<N>(f.a / g.a, f.v / g.a - f.a * g.v / (g.a * g.a));
}

template <int N> Jet<N> exp(const Jet<N>& f) {
    
    
  return Jet<T, N>(exp(f.a), exp(f.a) * f.v);
}

// This is a simple implementation for illustration purposes, the
// actual implementation of pow requires careful handling of a number
// of corner cases.
template <int N>  Jet<N> pow(const Jet<N>& f, const Jet<N>& g) {
    
    
  return Jet<N>(pow(f.a, g.a),
                g.a * pow(f.a, g.a - 1.0) * f.v +
                pow(f.a, g.a) * log(f.a); * g.v);
}

これらのオーバーロードされた関数を使用すると、double の代わりに Jet の配列を使用して Rat43CostFunctor を呼び出すことができるようになります。これを適切に初期化された Jet と組み合わせると、次のようにヤコビアンを計算できます。

class Rat43Automatic : public ceres::SizedCostFunction<1,4> {
    
    
 public:
  Rat43Automatic(const Rat43CostFunctor* functor) : functor_(functor) {
    
    }
  virtual ~Rat43Automatic() {
    
    }
  virtual bool Evaluate(double const* const* parameters,
                        double* residuals,
                        double** jacobians) const {
    
    
    // Just evaluate the residuals if Jacobians are not required.
    if (!jacobians) return (*functor_)(parameters[0], residuals);

    // Initialize the Jets
    ceres::Jet<4> jets[4];
    for (int i = 0; i < 4; ++i) {
    
    
      jets[i].a = parameters[0][i];
      jets[i].v.setZero();
      jets[i].v[i] = 1.0;
    }

    ceres::Jet<4> result;
    (*functor_)(jets, &result);

    // Copy the values out of the Jet.
    residuals[0] = result.a;
    for (int i = 0; i < 4; ++i) {
    
    
      jacobians[0][i] = result.v[i];
    }
    return true;
  }

 private:
  std::unique_ptr<const Rat43CostFunctor> functor_;
};

実際、これが AutoDiffCostFunction の仕組みです。

トラップ

自動微分により、ユーザーはヤコビアンの記号式についての計算と推論の負担から解放されますが、この自由には代償が伴います。たとえば、次の単純なファンクターを考えてみましょう。

struct Functor {
    
    
  template <typename T> bool operator()(const T* x, T* residual) const {
    
    
    residual[0] = 1.0 - sqrt(x[0] * x[0] + x[1] * x[1]);
    return true;
  }
};

コードの残差計算を見ると、問題は予想されません。ただし、ヤコビ行列 y = 1 − x 0 2 + x 1 2 D 1 y = − x 0 x 0 2 + x 1 2 の解析式を見ると、D 2 y = − x 1 x 0 2 +
x 1 2 \begin{split} y &= 1 - \sqrt{x_0^2 + x_1^2}\\ D_1y &= -\frac{x_0}{\sqrt{x_0^2 + x_1^2}},\ D_2y = -\frac{x_1}{\sqrt{x_0^2 + x_1^2}}\end{split}yD1はい=1バツ02+バツ12 =バツ02+バツ12 バツ0 D2y=バツ02+バツ12 バツ1

それは x 0 = 0 、 x 1 = 0 x_0 = 0、x_1 = 0で見つかります。バツ0=0 バツ1=0は不定詞です。

この問題に対する単一の解決策はありません。場合によっては、不確実性の可能性がある点を明示的に指摘し、L'Hopital のルールを使用して別の式を使用する必要があります (たとえば、rotation.h の変換ルーチンを参照)。その他の場合には、これらを排除するために式の正規化が必要になる場合があります。ポイント。

おすすめ

転載: blog.csdn.net/wanggao_1990/article/details/129724388