Levenberg_marquardt.cpp
Go to the documentation of this file.
1 #include <iostream>
2 #include <iomanip>
4 #include "Package_mango.hpp"
6 
7 #ifdef MANGO_EIGEN_AVAILABLE
8 #include <Eigen/Dense>
9 #endif
10 
11 #ifndef MANGO_EIGEN_AVAILABLE
12 // Eigen is NOT available.
13 
15  throw std::runtime_error("ERROR: The algorithm mango_levenberg_marquardt was selected. This algorithm requires Eigen, but MANGO was built without Eigen.");
16 }
17 
19 
20 #else
21 // The rest of this file is used when Eigen IS available.
22 
23 //! Constructor
25  // Call constructors for members
26  : state_vector(solver_in->state_vector, solver_in->N_parameters),
27  targets(solver_in->targets, solver_in->N_terms),
28  sigmas(solver_in->sigmas, solver_in->N_terms)
29 {
30  solver = solver_in;
31 
32  // If check_least_squares_solution == true, the least-squares problem will be solved
33  // a second way, to make sure the 2 methods agree. This check adds some computational cost.
34  check_least_squares_solution = false;
35 
36  // Initial value for the Levenberg-Marquardt parameter:
37  central_lambda = 0.01;
38  lambda_reduction_on_success = 10.0;
39 
40  max_line_search_iterations = 4;
41  max_outer_iterations = 100000;
42  save_lambda_history = true;
43 
44  // Define shorthand variable names:
45  N_parameters = solver->N_parameters;
46  N_terms = solver->N_terms;
47  verbose = solver->verbose;
48  N_line_search = solver->N_line_search;
49  proc0_world = solver->mpi_partition->get_proc0_world();
50  comm_group_leaders = solver->mpi_partition->get_comm_group_leaders();
51 
52  if (solver->verbose > 0) std::cout << "Hello from levenberg_marquardt. N_line_search=" << N_line_search << std::endl;
53 
54  if (N_line_search < 1) throw std::runtime_error("N_line_search must be >= 1.");
55 
56  // Set sizes for Eigen vectors and matrices:
57  state_vector_tentative.resize(N_parameters);
58  residuals.resize(N_terms);
59  shifted_residuals.resize(N_terms);
60  Jacobian.resize(N_terms,N_parameters);
61  residuals_extended.resize(N_terms + N_parameters);
62  Jacobian_extended.resize(N_terms + N_parameters, N_parameters);
63  delta_x.resize(N_parameters);
64  lambda_scan_residuals.resize(N_terms, N_line_search);
65  lambda_scan_state_vectors.resize(N_parameters, N_line_search);
66  lambdas.resize(N_line_search);
67  lambda_scan_objective_functions.resize(N_line_search);
68  if (check_least_squares_solution) {
69  delta_x_direct.resize(N_parameters);
70  alpha.resize(N_parameters, N_parameters);
71  alpha_prime.resize(N_parameters, N_parameters);
72  beta.resize(N_parameters);
73  }
74 
75  residuals_extended.bottomRows(N_parameters) = Eigen::VectorXd::Zero(N_parameters);
76  Jacobian_extended.bottomRows(N_parameters) = Eigen::MatrixXd::Zero(N_parameters,N_parameters);
77 
78  failed = false;
79  lambda_increase_factor = compute_lambda_increase_factor(N_line_search);
80  normalized_lambda_grid = new double[N_line_search];
81  compute_lambda_grid(N_line_search, lambda_increase_factor, normalized_lambda_grid);
82  if (verbose>0 && proc0_world) {
83  std::cout << "lambda_increase_factor: " << lambda_increase_factor << std::endl;
84  std::cout << "normalized_lambda_grid:";
85  for (j_lambda_grid=0; j_lambda_grid<N_line_search; j_lambda_grid++) std::cout << " " << normalized_lambda_grid[j_lambda_grid];
86  std::cout << std::endl;
87  }
88 
89 }
90 
91 //! The main driver for the Levenberg-Marquardt solver
92 /**
93  *
94  */
96  // Open output file:
97  if (save_lambda_history && proc0_world) {
98  std::string filename = solver->output_filename + "_levenberg_marquardt";
99  lambda_file.open(filename.c_str());
100  if (!lambda_file.is_open()) {
101  std::cerr << "Levenberg-Marquardt output file: " << filename << std::endl;
102  throw std::runtime_error("Error! Unable to open Levenberg-Marquardt output file.");
103  }
104  lambda_file << "outer_iteration,j_line_search,lambdas(1:N_line_search),objective_functions(1:N_line_search),min_objective_function_index,line_search_succeeded" << std::endl;
105  }
106 
107  keep_going_outer = true;
108  outer_iteration = 0;
109  // if (solver->mpi_partition->get_proc0_world()) {
110  while (keep_going_outer && (outer_iteration < max_outer_iterations)) {
111  outer_iteration++;
112  // In finite_difference_Jacobian, proc0 will bcast, so other procs need a corresponding bcast here:
113  if (! proc0_world) MPI_Bcast(&data,1,MPI_INT,0,comm_group_leaders);
114  // Evaluate the Jacobian:
115  solver->finite_difference_Jacobian(state_vector.data(), residuals.data(), Jacobian.data());
116 
117  // Apply the transformation involving sigmas and targets.
118  // Do this only on proc0, since only proc0 has the Jacobian, and possibly only proc0 will have targets & sigmas.
119  if (proc0_world) {
120  shifted_residuals = (residuals - targets).cwiseQuotient(sigmas);
121  for (j=0; j<N_parameters; j++) {
122  // If it weren't for wanting to compute alpha, we could store results directly in Jacobian_extended.
123  Jacobian.col(j) = Jacobian.col(j).cwiseQuotient(sigmas);
124  }
125  }
126  // Broadcast the Jacobian and shifted_residuals to all group leaders:
127  MPI_Bcast(Jacobian.data(), N_terms*N_parameters, MPI_DOUBLE, 0, comm_group_leaders);
128  MPI_Bcast(shifted_residuals.data(), N_terms, MPI_DOUBLE, 0, comm_group_leaders);
129  // At this point, all group leaders have the correct Jacobian and shifted_residuals.
130 
131  objective_function = shifted_residuals.dot(shifted_residuals);
132 
133  residuals_extended.topRows(N_terms) = shifted_residuals;
134  if (verbose>0 && proc0_world) {
135  std::cout << "Here comes state_vector from Eigen" << std::endl;
136  std::cout << std::setprecision(16) << std::scientific << state_vector << std::endl;
137  std::cout << "Here comes shifted_residuals from Eigen" << std::endl;
138  std::cout << shifted_residuals << std::endl;
139  std::cout << "Here comes residuals_extended from Eigen" << std::endl;
140  std::cout << residuals_extended << std::endl;
141  std::cout << "Here comes Jacobian from Eigen" << std::endl;
142  std::cout << Jacobian << std::endl;
143  }
144 
145  if (check_least_squares_solution) {
146  alpha = Jacobian.transpose() * Jacobian;
147  alpha_prime = alpha;
148  beta = -Jacobian.transpose() * shifted_residuals;
149  }
150 
151  line_search();
152  if (!line_search_succeeded) {
153  keep_going_outer = false;
154  if (verbose>0) std::cout << "Line search failed, so exiting outer loop on proc" << solver->mpi_partition->get_rank_world() << std::endl;
155  }
156  } // while (keep_going_outer)
157 
158  // Finalize output file:
159  if (save_lambda_history && proc0_world) lambda_file.close();
160 
161  delete[] normalized_lambda_grid;
162 
163 }
164 
165 //! Given a Jacobian, search over values of lambda to find a step that yields a decreased objective function
166 /**
167  *
168  */
169 void mango::Levenberg_marquardt::line_search() {
170  line_search_succeeded = false;
171  // Loop over values of central_lambda:
172  for (j_line_search = 0; j_line_search < max_line_search_iterations; j_line_search++) {
173  evaluate_on_lambda_grid(); // This is the expensive parallelized evaluation of the residuals.
174  process_lambda_grid_results(); // This is fast post-processing on proc0_world to determine which evaluation was best, & updating lambda.
175  }
176  MPI_Bcast(&line_search_succeeded, 1, MPI_C_BOOL, 0, comm_group_leaders);
177  MPI_Bcast(&keep_going_outer, 1, MPI_C_BOOL, 0, comm_group_leaders);
178  MPI_Bcast(state_vector.data(), N_parameters, MPI_DOUBLE, 0, comm_group_leaders); // Need to broadcast the state vector here; finite_difference_Jacobian only broadcasts a copy of the state vector, not the original one.
179 }
180 
181 
182 //! Evaluate the residuals for a set of trial steps corresponding to a set of values for lambda
183 /**
184  *
185  */
186 void mango::Levenberg_marquardt::evaluate_on_lambda_grid() {
187  lambda_scan_residuals = Eigen::MatrixXd::Zero(N_terms, N_line_search); // Initialize residuals to 0.
188  lambda_scan_state_vectors = Eigen::MatrixXd::Zero(N_parameters, N_line_search); // Initialize to 0.
189  // Perform concurrent function evaluations for several values of lambda:
190  for (j_lambda_grid = 0; j_lambda_grid < N_line_search; j_lambda_grid++) {
191  lambda = central_lambda * normalized_lambda_grid[j_lambda_grid];
192  lambdas(j_lambda_grid) = lambda; // Do this on all procs so proc0_world has the complete list of lambdas.
193  // Check if this MPI proc owns this point in the lambda grid:
194  if ((j_lambda_grid % solver->mpi_partition->get_N_worker_groups()) == solver->mpi_partition->get_rank_group_leaders()) {
195  if (verbose>0) std::cout << "Proc " << solver->mpi_partition->get_rank_world() << " is handling j_lambda_grid=" << j_lambda_grid
196  << ", lambda=" << lambda << std::endl;
197  Jacobian_extended.topRows(N_terms) = Jacobian;
198  for (j=0; j<N_parameters; j++) {
199  Jacobian_extended(N_terms+j,j) = sqrt(lambda * Jacobian.col(j).dot(Jacobian.col(j)));
200  }
201  if (verbose>0 && proc0_world) std::cout << "Here comes Jacobian_extended from Eigen" << std::endl << Jacobian_extended << std::endl;
202 
203  // Solve the linear least-squares system to compute the step in parameter space
204  delta_x = -Jacobian_extended.bdcSvd(Eigen::ComputeThinU | Eigen::ComputeThinV).solve(residuals_extended);
205  if (verbose>0 && proc0_world) std::cout << "Here comes delta_x from Eigen" << std::endl << delta_x << std::endl;
206 
207  if (check_least_squares_solution) {
208  // Direct approach
209  for (j=0; j<N_parameters; j++) {
210  alpha_prime(j,j) = alpha(j,j) * (1 + lambda);
211  }
212  delta_x_direct = alpha_prime.colPivHouseholderQr().solve(beta); // Solve alpha * delta_x = beta for delta_x.
213  if (verbose>0 && proc0_world) std::cout << "Here comes delta_x_direct from Eigen" << std::endl << delta_x_direct << std::endl;
214  difference = (delta_x - delta_x_direct).norm();
215  if (verbose>0 && proc0_world) std::cout << "(delta_x - delta_x_direct).norm() = " << difference << std::endl;
216  if (difference > 1e-10) {
217  std::cerr << "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" << std::endl;
218  std::cerr << "WARNING!!! delta_x vs delta_x_norm disagree!!" << std::endl;
219  std::cerr << "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" << std::endl;
220  }
221  }
222 
223  state_vector_tentative = state_vector + delta_x;
224  lambda_scan_state_vectors.col(j_lambda_grid) = state_vector_tentative;
225 
226  // Evaluate the residuals at the new point
227  solver->residual_function(&N_parameters, state_vector_tentative.data(), &N_terms, residuals.data(), &failed_int, solver->problem, solver->user_data);
228  lambda_scan_residuals.col(j_lambda_grid) = residuals;
229  } // if this MPI proc owns this point in the lambda grid
230  } // End of loop over lambda grid.
231 
232  // Send the computed state vectors and residuals back to proc0_world.
233  // The approach requiring least communication would be to have each proc only send the residuals it evaluated.
234  // However, for simpicity, I'll do a MPI_SUM reduction. This could be changed to the faster approach if needed.
235  if (proc0_world) {
236  MPI_Reduce(MPI_IN_PLACE, lambda_scan_state_vectors.data(), N_line_search * N_parameters, MPI_DOUBLE, MPI_SUM, 0, comm_group_leaders);
237  MPI_Reduce(MPI_IN_PLACE, lambda_scan_residuals.data(), N_line_search * N_terms, MPI_DOUBLE, MPI_SUM, 0, comm_group_leaders);
238  } else {
239  MPI_Reduce(lambda_scan_state_vectors.data(), lambda_scan_state_vectors.data(), N_line_search * N_parameters, MPI_DOUBLE, MPI_SUM, 0, comm_group_leaders);
240  MPI_Reduce(lambda_scan_residuals.data(), lambda_scan_residuals.data(), N_line_search * N_terms, MPI_DOUBLE, MPI_SUM, 0, comm_group_leaders);
241  }
242 }
243 
244 //! Given a set of residual evaluations on a grid of values of lambda, determine which was the best, and determine the next value of lambda to use.
245 /**
246  *
247  */
248 void mango::Levenberg_marquardt::process_lambda_grid_results() {
249  int original_j_line_search = j_line_search;
250 
251  if (proc0_world) {
252  // proc0 is responsible for finding the best function evaluation and deciding how to proceed.
253  failed = false;
254  for (j_lambda_grid = 0; j_lambda_grid < N_line_search; j_lambda_grid++) {
255  // Record the function evaluations from the lambda scan. This line also increments the counter for function evaluations.
256  solver->record_function_evaluation_pointer(lambda_scan_state_vectors.col(j_lambda_grid).data(), lambda_scan_residuals.col(j_lambda_grid).data(), failed);
257  // Apply the transformation involving sigmas and targets:
258  shifted_residuals = (lambda_scan_residuals.col(j_lambda_grid) - targets).cwiseQuotient(sigmas);
259  tentative_objective_function = shifted_residuals.dot(shifted_residuals);
260  if (verbose>0) std::cout<< "For j_lambda_grid=" << j_lambda_grid << ", objective function=" << tentative_objective_function << std::endl;
261  lambda_scan_objective_functions(j_lambda_grid) = tentative_objective_function;
262  // Find the index in the lambda grid with smallest value of the objective function:
263  if (j_lambda_grid==0) {
264  min_objective_function = tentative_objective_function;
265  min_objective_function_index = 0;
266  } else if (tentative_objective_function < min_objective_function) {
267  min_objective_function = tentative_objective_function;
268  min_objective_function_index = j_lambda_grid;
269  }
270  }
271  if (verbose>0 && proc0_world) std::cout << "Best j_lambda_grid=" << min_objective_function_index <<
272  ", lambda=" << central_lambda * normalized_lambda_grid[min_objective_function_index] << std::endl;
273  if (min_objective_function < objective_function) {
274  // Success: we reduced the objective function.
275  state_vector = lambda_scan_state_vectors.col(min_objective_function_index);
276  objective_function = min_objective_function;
277  // Take the optimal lambda from the previous step, and try reducing it a bit so the next step will be more like a Newton step.
278  central_lambda = central_lambda * normalized_lambda_grid[min_objective_function_index] / lambda_reduction_on_success;
279  if (verbose>0) std::cout << "Line search succeeded. New central lambda = " << central_lambda << std::endl;
280  line_search_succeeded = true;
281  j_line_search = max_line_search_iterations; // Exit "for" loop
282  } else {
283  // Objective function did not decrease. Try a step that is more like gradient descent.
284  central_lambda = central_lambda * lambda_increase_factor;
285  if (verbose>0) std::cout << "Increasing central lambda to " << central_lambda << std::endl;
286  }
287  //std::cout << "solver->function_evaluations: " << solver->function_evaluations << ", solver->max_function_evaluations: " << solver->max_function_evaluations << std::endl;
288  if (solver->function_evaluations >= solver->max_function_evaluations) {
289  // Quit due to hitting max_function_evaluations
290  j_line_search = max_line_search_iterations; // Exit inner "for" loop
291  keep_going_outer = false;
292  if (verbose>0) std::cout << "Maximum number of function evaluations reached." << std::endl;
293  }
294 
295  // Record results in the _levenberg_marquardt output file:
296  if (save_lambda_history) {
297  lambda_file << std::setw(6) << outer_iteration << "," << std::setw(3) << original_j_line_search << ",";
298  for (int j=0; j<N_line_search; j++) lambda_file << std::setw(24) << std::setprecision(16) << std::scientific << lambdas(j) << ",";
299  for (int j=0; j<N_line_search; j++) lambda_file << std::setw(24) << std::setprecision(16) << std::scientific << lambda_scan_objective_functions(j) << ",";
300  lambda_file << std::setw(3) << min_objective_function_index << ", " << std::setw(1) << line_search_succeeded << std::endl << std::flush;
301  }
302 
303  } // if proc0_world
304  // Broadcast results from proc0 to all group leaders.
305  MPI_Bcast(&central_lambda, 1, MPI_DOUBLE, 0, comm_group_leaders);
306  MPI_Bcast(&j_line_search, 1, MPI_INT, 0, comm_group_leaders);
307 }
308 
309 //! Compute the factor by which the Levenberg-Marquardt lambda parameter is reduced when a step causes the objective function to increase.
310 /**
311  * @param[in] N_line_search The number of points considered simultaneously in a set of trial steps.
312  * @return The factor by which the Levenberg-Marquardt lambda parameter will be reduced when a step causes the objective function to increase.
313 */
314 double mango::Levenberg_marquardt::compute_lambda_increase_factor(const int N_line_search) {
315  // Step size when N_line_search -> infinity:
316  double max_lambda_step = 1.0e6;
317 
318  // Step size when N_line_search = 1:
319  double min_lambda_step = 10.0;
320 
321  // When factor = 0, lambda_step increases rapidly with N_line_search.
322  // When factor >> 1, lambda_step increases slowly with N_line_search.
323  double factor = 2.0;
324 
325  return exp(log(max_lambda_step) - (log(max_lambda_step) - log(min_lambda_step)) * (1+factor)/(N_line_search+factor));
326 }
327 
328 //! Compute a grid of values for the Levenberg-Marquardt parameter lambda that will be used for each set of concurrent function evaluations.
329 /**
330  * @param[in] N_line_search The number of points considered simultaneously in a set of trial steps.
331  * @param[in] lambda_step The factor by which the Levenberg-Marquardt lambda parameter will be reduced when a step causes the objective function to increase.
332  * @param[out] lambdas The computed grid of values. It will be logarithmically centered about 1.0.
333  */
334 void mango::Levenberg_marquardt::compute_lambda_grid(const int N_line_search, const double lambda_step, double* lambdas) {
335  double log_lambda_step = log(lambda_step);
336  for (int j = 0; j < N_line_search; j++) {
337  lambdas[j] = exp(((j+0.5)/N_line_search - 0.5) * log_lambda_step);
338  }
339 }
340 
341 #endif // MANGO_EIGEN_AVAILABLE
mango::Levenberg_marquardt::Levenberg_marquardt
Levenberg_marquardt(Least_squares_solver *)
Definition: Levenberg_marquardt.cpp:14
Least_squares_solver.hpp
mango::Least_squares_solver
Definition: Least_squares_solver.hpp:28
mango::Levenberg_marquardt::solve
void solve()
Definition: Levenberg_marquardt.cpp:18
Package_mango.hpp
mango::Solver::N_parameters
int N_parameters
Definition: Solver.hpp:43
Levenberg_marquardt.hpp