Introdução e exemplos do Ceres (10) Sobre Derivativos (Derivados Automáticos)

Agora vamos discutir o algoritmo de diferenciação automática. É um algoritmo que pode calcular rapidamente derivadas exatas, enquanto o usuário só precisa fazer o trabalho semelhante à diferenciação numérica. O trecho de código abaixo implementa CostFunction para Rat43 .

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));

Observe que a única diferença ao definir um Functor de diferenciação automática é a configuração de operator() em comparação com a diferenciação numérica.
PS: código para diferenciação numérica

			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));

No caso da diferenciação numérica, é

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

Para diferenciação automática, é uma função modelo da forma

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

Qual é o impacto desta mudança? A tabela abaixo compara o tempo necessário para calcular os resíduos e o Jacobiano de Rat43 com diferentes métodos.

CostFunction

Tempo (ns)

Rat43Analytic

255

Rat43AnalyticOptimized

92

Rat43NumericDiffForward

262

Rat43NumericDiffCentral

517

Rat43NumericDiffRidders

3760

Rat43AutomaticDiff

129

Podemos obter a diferenciação exata usando a diferenciação automática (Rat43AutomaticDiff), que dá tanto trabalho quanto escrever um código de diferenciação numérica, mas apenas 40% mais lento do que a diferenciação analítica otimizada manualmente.

Então, como isso funciona? Para isso, aprenderemos sobre números duais e jatos.

Números duplos para pares e jatos

Leia esta subseção e a próxima seção sobre a implementação de Jets, não diretamente relacionada ao uso de diferenciação automática no solucionador Ceres. No entanto, entender como o Jets funciona é muito útil ao depurar e raciocinar sobre o desempenho da diferenciação automática.

Os números pares duplos são uma extensão dos números reais, semelhantes aos números complexos: enquanto os números complexos introduzem uma unidade imaginária i 2 = − 1 i^2=-1eu2=1 para expandir números reais e introduzir uma unidade infinitesimal deϵ εϵ , assimϵ2 = 0 ϵ^2=0ϵ2=0 .
Número para + v ε a+vϵa+v ε tem duas componentes, a componente real a e a componente infinitesimal v.
PS: Consulte os números pares https://zhuanlan.zhihu.com/p/380140763

Surpreendentemente, essa simples mudança leva a uma maneira conveniente de calcular derivadas exatas sem manipular expressões simbólicas complexas. Por exemplo, considere a seguinte função
f ( x ) = x 2 , f(x) = x^2 ,f ( x )=x2 ,
equação,
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 ϵ

Observando o coeficiente de ε, descobrimos que Df(10)=20. Na verdade, isso pode ser generalizado para funções que não são polinômios. Considere uma função diferenciável arbitrária f(x) então podemos calcular f ( x + ϵ ) f(x + \epsilon)
considerando a expansão de Taylor de f em torno de xf ( x+ϵ ) ,Seja a função escalar
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{split}f ( x+) _f ( x+ϵ ).=f ( x )+D f ( x ) ϵ+D2 f(x)2ϵ2+D3 f(x)6ϵ3+=f ( x )+D f ( x ) ϵ

Nota, ϵ 2 = 0 \epsilon^2 = 0ϵ2=0

Jet é um número dual n-dimensional. Entre eles, usamos n unidades infinitesimais ϵ i , i = 1 , . . . , n \epsilon_i,\ i=1,...,nϵeu, eu=1 ,... ,n para aumentar números reais, e existem propriedades∀ i , j : ϵ i ϵ j = 0 \forall i, j\ :\epsilon_i\epsilon_j = 0eu ,j :ϵeuϵj=0 . Então Jet consiste na parte real a e na parte infinitesimal n-dimensional v, ou seja,
x = a + ∑ jvj ϵ jx = a + \sum_j v_{j} \epsilon_jx=a+jvjϵj

A notação de soma é tediosa, então vamos escrever
x = a + v . x = a + \mathbf{v}.x=a+v .

A fórmula acima, ϵ i \epsilon_iϵeuestá implícito. Então, usando a mesma expansão em série de Taylor acima, podemos ver:
f ( a + v ) = f ( a ) + D f ( a ) v . f(a + \mathbf{v}) = f(a ) + Df(a) \mathbf{v}.f ( um+v )=f ( a )+D f ( a ) v .
Da mesma forma, para uma função multivariadaf : R n → R mf:\mathbb{R}^{n}\rightarrow \mathbb{R}^mf:RnRm,paraxi = ai + vi , ∀ i = 1 , . . . , n x_i = a_i + \mathbf{v}_i,\ \forall i = 1,...,nxeu=aeu+veu, eu=1 ,... ,n

Para cada vi = ei \mathbf{v}_i = e_iveu=eeué o i-ésimo vetor de base padrão. Em seguida, simplifique a expressão acima para
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,... ,xn)=f ( um1,... ,an)+euDeuf ( um1,... ,an) ϵeu

Podemos extrair as coordenadas Jacobianas ϵ i \epsilon_i examinando os coeficientes deϵeu

Implementação de Jatos

Para ser útil na prática, precisamos ser capazes de calcular funções arbitrárias f, não apenas em números reais, mas também em números duais, mas geralmente não calculamos funções via expansão de Taylor,

É aqui que os modelos C++ e a sobrecarga de operadores entram em jogo. O trecho de código abaixo tem uma implementação simples do Jet e alguns operadores/funções para manipulá-los.

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);
}

Com essas funções sobrecarregadas, agora podemos chamar Rat43CostFunctor com uma matriz de Jets em vez de doubles. Combinando isso com Jets devidamente inicializados, podemos calcular o jacobiano da seguinte forma:

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_;
};

Na verdade, é assim que AutoDiffCostFunction funciona.

armadilha

A diferenciação automática libera o usuário do fardo de computar e raciocinar sobre expressões simbólicas jacobianas, mas essa liberdade tem um preço. Por exemplo, considere o seguinte funtor simples:

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;
  }
};

Olhando para o cálculo residual do código, nenhum problema é previsto. No entanto, se olharmos para a expressão analítica para a matriz jacobiana
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}yD1você=1x02+x12 =x02+x12 x0, D2y=x02+x12 x1

Nós o encontramos em x 0 = 0 , x 1 = 0 x_0 = 0, x_1 = 0x0=0 ,x1=0 é um infinitivo.

Não existe uma solução única para este problema. Em alguns casos, é preciso apontar explicitamente possíveis pontos de incerteza e usar expressões alternativas usando a regra de L'Hopital (ver, por exemplo, algumas rotinas de conversão em rotação.h), em outros casos pode ser necessária a regularização da expressão para eliminar esses pontos.

Acho que você gosta

Origin blog.csdn.net/wanggao_1990/article/details/129724388
Recomendado
Clasificación