A diferenciação automática é fácil de usar quando uma expressão explícita para a função de custo está disponível. Normalmente, os programas precisam interagir com programas ou dados externos. Neste capítulo, consideraremos várias abordagens diferentes.
Para tanto, consideraremos o problema de encontrar os parâmetros θ e t para resolver o seguinte problema de otimização na forma de uma transformação afim 2D distorcida:
min ∑ i ∥ yi − f ( ∥ qi ∥ 2 ) qi ∥ 2 tal que qi = R ( θ ) xi + t \begin{split}\min & \quad \sum_i \left \|y_i - f\left (\|q_{i}\|^2\right) q_i \right \| ^2\\ \text{tal que} & \quad q_i = R(\theta) x_i + t\end{split}mintal queeu∑
yeu−f( ∥q _eu∥2 )qeu
2qeu=R ( i ) xeu+t
Aqui, R é uma matriz de rotação 2D parametrizada pelo ângulo θ e t é um vetor 2D. f é uma função de distorção externa.
Vamos primeiro considerar o caso em que temos uma função modelo TemplatedComputeDistortion que calcula uma função f. A implementação do funtor residual correspondente é então direta, como segue:
template <typename T> T TemplatedComputeDistortion(const T r2) {
const double k1 = 0.0082;
const double k2 = 0.000023;
return 1.0 + k1 * r2 + k2 * r2 * r2;
}
struct Affine2DWithDistortion {
Affine2DWithDistortion(const double x_in[2], const double y_in[2]) {
x[0] = x_in[0];
x[1] = x_in[1];
y[0] = y_in[0];
y[1] = y_in[1];
}
template <typename T>
bool operator()(const T* theta,
const T* t,
T* residuals) const {
const T q_0 = cos(theta[0]) * x[0] - sin(theta[0]) * x[1] + t[0];
const T q_1 = sin(theta[0]) * x[0] + cos(theta[0]) * x[1] + t[1];
const T f = TemplatedComputeDistortion(q_0 * q_0 + q_1 * q_1);
residuals[0] = y[0] - f * q_0;
residuals[1] = y[1] - f * q_1;
return true;
}
double x[2];
double y[2];
};
Até aqui tudo bem, mas agora vamos considerar três maneiras de definir f que não podem ser usadas diretamente para diferenciação automática:
- 1. Uma função sem modelo que calcula seu valor.
- 2. Encontre a função de seu valor e derivada.
- 3. Definido em função da tabela de valores a serem interpolados.
Vamos considerá-los por sua vez abaixo.
1、Uma função que retorna seu valor
Suponha que recebemos uma função ComputeDistortionValue declarada da seguinte forma:
double ComputeDistortionValue(double r2);
Calcula o valor de f. A implementação real da função não importa. Existem três etapas para fazer a interface desta função com Affine2DWithDistortion:
- 1.
ComputeDistortionValue
Encapsule-o em uma funçãoComputeDistortionValueFunctor
. - 2. Diferencie
ComputeDistortionValueFunctor
numericamente o NumericDiffCostFunction para criar um CostFunction - 3. Use CostFunctionToFunctor para encapsular CostFunction . Embrulhado para obter uma
operator()
função com operadores modelados. Este método de função de operador pode transformar a matriz jacobiana calculada por NumericDiffCostFunctionJet
em um objeto.
A implementação das três etapas acima é a seguinte:
struct ComputeDistortionValueFunctor {
bool operator()(const double* r2, double* value) const {
*value = ComputeDistortionValue(r2[0]);
return true;
}
};
struct Affine2DWithDistortion {
Affine2DWithDistortion(const double x_in[2], const double y_in[2]) {
x[0] = x_in[0];
x[1] = x_in[1];
y[0] = y_in[0];
y[1] = y_in[1];
compute_distortion.reset(new ceres::CostFunctionToFunctor<1, 1>(
new ceres::NumericDiffCostFunction<ComputeDistortionValueFunctor,
ceres::CENTRAL,
1,
1>(
new ComputeDistortionValueFunctor)));
}
template <typename T>
bool operator()(const T* theta, const T* t, T* residuals) const {
const T q_0 = cos(theta[0]) * x[0] - sin(theta[0]) * x[1] + t[0];
const T q_1 = sin(theta[0]) * x[0] + cos(theta[0]) * x[1] + t[1];
const T r2 = q_0 * q_0 + q_1 * q_1;
T f;
(*compute_distortion)(&r2, &f);
residuals[0] = y[0] - f * q_0;
residuals[1] = y[1] - f * q_1;
return true;
}
double x[2];
double y[2];
std::unique_ptr<ceres::CostFunctionToFunctor<1, 1> > compute_distortion;
};
2、Uma função que retorna seu valor e derivada
Agora suponha que temos uma função ComputeDistortionValue
, podemos obter seu valor e podemos obter sua matriz jacobiana, se quisermos. Sua declaração de função é a seguinte:
void ComputeDistortionValueAndJacobian(double r2,
double* value,
double* jacobian);
Novamente, a implementação real da função não importa. A interface desta função com Affine2DWithDistortion
o encaixe requer duas etapas:
- 1.
ComputeDistortionValueAndJacobian
Encapsule-o em um objeto CostFunction. Chamamos esse objeto CostFunctionComputeDistortionFunction
. - 2. Encapsule o objeto ComputeDistortionFunction obtido com CostFunctionToFunctor. Obtém um Functor com um
operator()
método de operador de modelo que transforma o Jacobiano calculado pelo NumericDiffCostFunction em umJet
objeto de ajuste.
código mostra como abaixo:
class ComputeDistortionFunction : public ceres::SizedCostFunction<1, 1> {
public:
virtual bool Evaluate(double const* const* parameters,
double* residuals,
double** jacobians) const {
if (!jacobians) {
ComputeDistortionValueAndJacobian(parameters[0][0], residuals, nullptr);
} else {
ComputeDistortionValueAndJacobian(parameters[0][0], residuals, jacobians[0]);
}
return true;
}
};
struct Affine2DWithDistortion {
Affine2DWithDistortion(const double x_in[2], const double y_in[2]) {
x[0] = x_in[0];
x[1] = x_in[1];
y[0] = y_in[0];
y[1] = y_in[1];
compute_distortion.reset(
new ceres::CostFunctionToFunctor<1, 1>(new ComputeDistortionFunction));
}
template <typename T>
bool operator()(const T* theta,
const T* t,
T* residuals) const {
const T q_0 = cos(theta[0]) * x[0] - sin(theta[0]) * x[1] + t[0];
const T q_1 = sin(theta[0]) * x[0] + cos(theta[0]) * x[1] + t[1];
const T r2 = q_0 * q_0 + q_1 * q_1;
T f;
(*compute_distortion)(&r2, &f);
residuals[0] = y[0] - f * q_0;
residuals[1] = y[1] - f * q_1;
return true;
}
double x[2];
double y[2];
std::unique_ptr<ceres::CostFunctionToFunctor<1, 1> > compute_distortion;
};
3、Uma função que é definida como uma tabela de valores
O terceiro e último caso que iremos considerar é quando a função f é definida como uma tabela de valores no intervalo [0,100], com um valor para cada inteiro.
vector<double> distortion_values;
Há muitas maneiras de inserir uma tabela de valores. Talvez o método mais simples e comum seja a interpolação linear. Mas usar a interpolação linear não é uma boa ideia, porque a função de interpolação não é diferenciável nos pontos de amostragem.
Outro método de interpolação diferenciável simples, mas excelente, é o Cubic Hermite Spline (interpolação Hermite chinesa). O Ceres fornece todo o processo de interpolação cúbica e bi-cúbica e pode facilmente aplicar algoritmos de diferenciação automática.
Para usar a interpolação cúbica, você precisa primeiro construir um objeto Grid1D para encapsular a tabela de valores e, em seguida, usá-lo para construir um objeto CubicInterpolator.
O código resultante se parece com isso:
struct Affine2DWithDistortion {
Affine2DWithDistortion(const double x_in[2],
const double y_in[2],
const std::vector<double>& distortion_values) {
x[0] = x_in[0];
x[1] = x_in[1];
y[0] = y_in[0];
y[1] = y_in[1];
grid.reset(new ceres::Grid1D<double, 1>(
&distortion_values[0], 0, distortion_values.size()));
compute_distortion.reset(
new ceres::CubicInterpolator<ceres::Grid1D<double, 1> >(*grid));
}
template <typename T>
bool operator()(const T* theta,
const T* t,
T* residuals) const {
const T q_0 = cos(theta[0]) * x[0] - sin(theta[0]) * x[1] + t[0];
const T q_1 = sin(theta[0]) * x[0] + cos(theta[0]) * x[1] + t[1];
const T r2 = q_0 * q_0 + q_1 * q_1;
T f;
compute_distortion->Evaluate(r2, &f);
residuals[0] = y[0] - f * q_0;
residuals[1] = y[1] - f * q_1;
return true;
}
double x[2];
double y[2];
std::unique_ptr<ceres::Grid1D<double, 1> > grid;
std::unique_ptr<ceres::CubicInterpolator<ceres::Grid1D<double, 1> > > compute_distortion;
};
No exemplo acima, usamos Grid1D e CubicInterpolator para interpolar uma tabela de valores unidimensionais. Grid2D é usado em conjunto com CubicInterpolator para permitir que os usuários insiram tabelas bidimensionais de valores. Observe que nem Grid1D nem Grid2D estão limitados a funções com valor escalar, eles também podem usar funções com valor vetorial.