tinyopt 700
Tinyopt, a lightweight, header only optimization library
Loading...
Searching...
No Matches
gradient_check.h
1// Copyright 2026 Julien Michot.
2// SPDX-License-Identifier: Apache-2.0
3
4#pragma once
5
6#include <tinyopt/diff/num_diff.h>
7#include <tinyopt/math.h>
8
9#include <tinyopt/log.h>
10#include <tinyopt/traits.h>
11#include <cstddef>
12#include <type_traits>
13#include "tinyopt/losses/norms.h"
14
15namespace tinyopt::diff {
16
36// `Cost(const X_t& x, Gradient_t& grad, Hessian_t &)`. The function *must*
37// compute
50template <typename X_t, typename AccFunc>
51inline bool CheckGradient(
52 X_t &x, const AccFunc &acc,
53 double eps = std::is_same_v<typename traits::params_trait<X_t>::Scalar, float> ? 1e-2 : 1e-5,
54 const diff::Method method = diff::Method::kCentral, bool verbose = true) {
55 using Scalar = traits::params_trait<X_t>::Scalar;
56 constexpr int Dims = traits::params_trait<X_t>::Dims;
57 const int dims = traits::DynDims(x);
58 using Gradient_t = Vector<Scalar, Dims>;
59 constexpr bool HasH = !std::is_invocable_v<AccFunc, const X_t &, Gradient_t &>;
60 constexpr bool DenseH =
61 std::is_invocable_v<AccFunc, const X_t &, Gradient_t &, Matrix<Scalar, Dims, Dims> &>;
62 using MaybeHessian_t = std::conditional_t<
63 HasH, std::conditional_t<DenseH, Matrix<Scalar, Dims, Dims>, SparseMatrix<Scalar>>,
64 std::nullptr_t>;
65
66 std::nullptr_t nul;
67 // Get the gradient provided by 'acc'
68 Gradient_t grad(dims);
69 grad.setZero();
70
71 auto fg = [&](const X_t &x, Gradient_t &g) {
72 if constexpr (HasH &&
73 !std::is_invocable_v<AccFunc, const X_t &, Gradient_t &, MaybeHessian_t &>)
74 return acc(x, g, nul);
75 else if constexpr (HasH) {
76 MaybeHessian_t H;
77 H.resize(dims, dims);
78 H.setZero();
79 return acc(x, g, H);
80 } else
81 return acc(x, g);
82 };
83 fg(x, grad);
84
85 // Estimate numerical jacobian
86 auto f = [&](const X_t &x) {
87 if constexpr (HasH) {
88 MaybeHessian_t H;
89 H.resize(dims, dims);
90 H.setZero();
91 return acc(x, nul, H);
92 } else
93 return acc(x, nul);
94 };
95
96 const auto grad_num = EstimateNumJac(x, f, method, eps / 10.0f).transpose().eval();
97 const double max_dist = (grad_num - grad).cwiseAbs().maxCoeff();
98 const bool is_within_tol = max_dist < eps;
99 if (!is_within_tol && verbose)
100 TINYOPT_LOG("Wrong gradient {:.3e}>eps. \nGradient:{}, Numerial Gradient:{}", max_dist, grad,
101 grad_num);
102 return is_within_tol;
103}
104
108// This function helps verify the correctness of the
124// (lambda, std::function, or functor) It should have one of the signatures
126// `Residuals(const X_t& x, Gradient_t& grad, Hessian_t &)`. The function
127// *must* compute the gradient and store it in the `grad` argument.
143template <typename X_t, typename AccWithResidualsFunc>
144inline bool CheckResidualsGradient(
145 X_t &x, const AccWithResidualsFunc &acc_res_func,
146 double eps = std::is_same_v<typename traits::params_trait<X_t>::Scalar, float> ? 1e-2 : 1e-5,
147 const diff::Method method = diff::Method::kCentral, bool verbose = true, bool check_H = true,
148 bool downscale = true) {
149 using Scalar = traits::params_trait<X_t>::Scalar;
150 constexpr int Dims = traits::params_trait<X_t>::Dims;
151 const int dims = traits::DynDims(x);
152 using Gradient_t = Vector<Scalar, Dims>;
153 constexpr bool HasH = !std::is_invocable_v<AccWithResidualsFunc, const X_t &, std::nullptr_t &>;
154 constexpr bool DenseH = std::is_invocable_v<AccWithResidualsFunc, const X_t &, Gradient_t &,
155 Matrix<Scalar, Dims, Dims> &>;
156 using MaybeHessian_t = std::conditional_t<
157 HasH, std::conditional_t<DenseH, Matrix<Scalar, Dims, Dims>, SparseMatrix<Scalar>>,
158 std::nullptr_t>;
159 const double s = downscale ? 0.5 : 1.0;
160
161 // Apply L2
162 auto loss = [&](const X_t &x, auto &g, MaybeHessian_t &H) {
163 if constexpr (HasH) {
164 auto res = acc_res_func(x, g, H);
165
166 using ResidualsType = std::decay_t<decltype(res)>;
167 static_assert(!std::is_same_v<ResidualsType, Cost>,
168 "The function must return residuals, not a cost");
169 return s * losses::SquaredL2(res);
170 } else {
171 auto res = acc_res_func(x, g);
172
173 using ResidualsType = std::decay_t<decltype(res)>;
174 static_assert(!std::is_same_v<ResidualsType, Cost>,
175 "The function must return residuals, not a cost");
176
177 return s * losses::SquaredL2(res);
178 }
179 };
180 bool success = CheckGradient(x, loss, eps, method, verbose);
181
182 // Also check the hessian approx Jt*J
183 if constexpr (HasH) {
184 if (!check_H) return success;
185
186 // Get user defined H
187 Gradient_t g;
188 g.resize(dims);
189 g.setZero();
190 MaybeHessian_t H;
191 H.resize(dims, dims);
192 H.setZero();
193 acc_res_func(x, g, H);
194
195 // Estimate numerical J and H = Jt*J
196 auto f = [&](const X_t &x) {
197 std::nullptr_t nul;
198 return acc_res_func(x, nul, H);
199 };
200
201 const auto J_num = EstimateNumJac(x, f, method, eps / 10.0f);
202 const auto H_num = (J_num.transpose() * J_num).eval();
203
204 double max_dist;
205 if constexpr (DenseH)
206 max_dist = (H_num - H).cwiseAbs().maxCoeff();
207 else {
208 MaybeHessian_t H_num_sparse = H_num.sparseView();
209 H_num_sparse.makeCompressed();
210 max_dist = MaxAbsDiff(H_num_sparse, H);
211 }
212
213 const bool is_within_tol = max_dist < eps;
214 if (!is_within_tol && verbose)
215 TINYOPT_LOG("Wrong gradient {:.3e}>eps. \nH:\n{}, Numerial JtJ:\n{}", max_dist, H, H_num);
216 return success && is_within_tol;
217 }
218
219 return success;
220}
221
222} // namespace tinyopt::diff
auto SquaredL2(const T &x)
Return the squared L2 norm of a vector or scalar (a.k.a Sum of Squares).
Definition norms.h:19