-
Notifications
You must be signed in to change notification settings - Fork 109
Expand file tree
/
Copy pathtrustregions.m
More file actions
925 lines (840 loc) · 39.6 KB
/
Copy pathtrustregions.m
File metadata and controls
925 lines (840 loc) · 39.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
function [x, cost, info, options] = trustregions(problem, x, options)
% Riemannian trust-regions solver for optimization on manifolds.
%
% function [x, cost, info, options] = trustregions(problem)
% function [x, cost, info, options] = trustregions(problem, x0)
% function [x, cost, info, options] = trustregions(problem, x0, options)
% function [x, cost, info, options] = trustregions(problem, [], options)
%
% This is the Riemannian Trust-Region solver for Manopt, named RTR.
% This solver tries to minimize the cost function described in the problem
% structure. It requires the availability of the cost function and of its
% gradient. It issues calls for the Hessian.
%
% If no Hessian nor approximation for it is provided, an approximation of
% Hessian-vector products is computed with finite differences of gradients.
%
% If no gradient is provided, an approximation of the gradient is computed,
% but this can be slow for manifolds of high dimension.
%
% At each iteration a subproblem is solved using a trust-region subproblem
% (TRS) solver. The default is @trs_tCG_cached. This one (and some others)
% use the preconditioner if one is supplied.
%
% For a description of the algorithm and theorems offering convergence
% guarantees, see the references below. Documentation for this solver is
% available online, but may be outdated.
% http://www.manopt.org/solver_documentation_trustregions.html
%
%
% The initial iterate is x0 if it is provided. Otherwise, a random point on
% the manifold is picked. To specify options whilst not specifying an
% initial iterate, give x0 as [] (the empty matrix).
%
% The two outputs 'x' and 'cost' are the last reached point on the manifold
% and its cost. Notice that x is not necessarily the best reached point,
% because this solver is not forced to be a descent method. In particular,
% very close to convergence, it is sometimes preferable to accept very
% slight increases in the cost value (on the order of the machine epsilon)
% in the process of reaching fine convergence. Other than that, the cost
% function value does decrease monotonically with iterations.
%
% The output 'info' is a struct-array which contains information about the
% iterations:
% iter (integer)
% The (outer) iteration number, or number of steps considered
% (whether accepted or rejected). The initial guess is 0.
% cost (double)
% The corresponding cost value.
% gradnorm (double)
% The (Riemannian) norm of the gradient.
% time (double)
% The total elapsed time in seconds to reach the corresponding cost.
% rho (double)
% The performance ratio for the iterate.
% rhonum, rhoden (double)
% Regularized numerator and denominator of the performance ratio:
% rho = rhonum/rhoden. See options.rho_regularization.
% accepted (boolean)
% Whether the proposed iterate was accepted or not.
% stepsize (double)
% The (Riemannian) norm of the vector returned by the inner solver
% and which is retracted to obtain the proposed next iterate. If
% accepted = true for the corresponding iterate, this is the size of
% the step from the previous to the new iterate. If accepted is
% false, the step was not executed and this is the size of the
% rejected step.
% Delta (double)
% The trust-region radius at the outer iteration.
% limitedbyTR (boolean)
% true if the subproblemsolver was limited by the trust-region
% radius (a boundary solution was returned).
% And possibly additional information logged by the subproblemsolver or
% by options.statsfun.
% For example, type [info.gradnorm] to obtain a vector of the successive
% gradient norms reached at each (outer) iteration.
%
% The options structure is used to overwrite the default values. All
% options have a default value and are hence optional. To force an option
% value, pass an options structure with a field options.optionname, where
% optionname is one of the following and the default value is indicated
% between parentheses:
%
% tolgradnorm (1e-6)
% The algorithm terminates if the norm of the gradient drops below
% this. For well-scaled problems, a rule of thumb is that you can
% expect to reduce the gradient norm by 8 orders of magnitude
% (sqrt(eps)) compared to the gradient norm at a "typical" point (a
% rough initial iterate for example). Further decrease is sometimes
% possible, but inexact floating point arithmetic limits the final
% accuracy. If tolgradnorm is set too low, the algorithm may end up
% iterating forever (or until another stopping criterion triggers).
% maxiter (1000)
% The algorithm terminates after at most maxiter (outer) iterations.
% maxtime (Inf)
% The algorithm terminates if maxtime seconds elapsed.
% miniter (0)
% Minimum number of outer iterations: this overrides all other
% stopping criteria. Can be helpful to escape saddle points.
% Delta_bar (problem.M.typicaldist() or sqrt(problem.M.dim()))
% Maximum trust-region radius. If you specify this parameter but not
% Delta0, then Delta0 is set to 1/8 times this parameter.
% Delta0 (Delta_bar/8)
% Initial trust-region radius. If you observe a long plateau at the
% beginning of the convergence plot (gradient norm vs iteration), it
% may pay off to try to tune this parameter to shorten the plateau.
% You should not set this parameter without setting Delta_bar too (at
% a larger value).
% subproblemsolver (@trs_tCG_cached)
% Function handle to a subproblem solver. The subproblem solver also
% sees this options structure, so that parameters can be passed to it
% through here as well. Built-in solvers include:
% trs_tCG_cached
% trs_tCG
% trs_lanczos
% trs_gep
% Note that trs_gep solves the subproblem exactly which may be slow.
% It is included mainly for prototyping or for solving the subproblem
% exactly in low dimensional subspaces.
% rho_prime (0.1)
% Accept/reject threshold : if rho is at least rho_prime, the outer
% iteration is accepted. Otherwise, it is rejected. In case it is
% rejected, the trust-region radius will have been decreased.
% To ensure this, rho_prime >= 0 must be strictly smaller than 1/4.
% If rho_prime is negative, the algorithm is not guaranteed to
% produce monotonically decreasing cost values. It is strongly
% recommended to set rho_prime > 0, to aid convergence.
% rho_regularization (1e3)
% Close to convergence, evaluating the performance ratio rho is
% numerically challenging. Meanwhile, close to convergence, the
% quadratic model should be a good fit and the steps should be
% accepted. Regularization lets rho go to 1 as the model decrease and
% the actual decrease go to zero. Set this option to zero to disable
% regularization (not recommended). See in-code for the specifics.
% When this is not zero, it may happen that the iterates produced are
% not monotonically improving the cost when very close to
% convergence. This is because the corrected cost improvement could
% change sign if it is negative but small: see 'allowcostincrease'.
% allowcostincrease (true)
% When rho is computed with regularization, it may happen that a step
% should be accepted as determined by rho, yet for which the true
% cost function value goes up (typically, only by a little).
% This may be beneficial, so it is allowed by default.
% Setting this option to "false" makes it so that if this is about to
% happen, the step is rejected and the solver terminates.
% stopiftrstrouble (false)
% Terminate TR if the subproblem solver detects numerical issues that
% are most likely due to finite numerical precision. This is false be
% default because a user may want to push the method continue, as
% perhaps it can still find even more accurate solutions (though this
% can be unreliable and slow). This should not be flipped to "true"
% if using a finite-difference approximation of the Hessian, as then
% the typical numerical issues detector (in tCG-style subproblem
% solvers) would be fooled by the nonlinearity.
% statsfun (none)
% Function handle to a function that is called after each iteration
% to provide the opportunity to log additional statistics.
% They are returned in the info struct. See the generic Manopt
% documentation about solvers for further information. statsfun is
% called with the point x that was reached last, after the
% accept/reject decision. See comment below.
% stopfun (none)
% Function handle to a function that is called at each iteration to
% provide the opportunity to specify additional stopping criteria.
% See the generic Manopt documentation about solvers for further
% information.
% verbosity (2)
% Integer number used to tune the amount of output the algorithm logs
% during execution (mostly as text in the command window).
% The higher, the more output. 0 means silent. 3 and above includes a
% display of the options structure at the beginning of the execution.
% debug (false)
% Set to true to allow the algorithm to perform additional
% computations for debugging purposes. If a debugging test fails, you
% will be informed of it, usually via the command window. Be aware
% that these additional computations appear in the algorithm timings
% too, and may interfere with operations such as counting the number
% of cost evaluations, etc. The debug calls get storedb too.
% storedepth (2)
% Maximum number of different points x of the manifold for which a
% store structure may be kept in memory in the storedb for caching.
% If memory usage is an issue, you may try to lower this number.
% Profiling or manopt counters may then help to investigate if a
% performance hit was incurred as a result.
% hook (none)
% A function handle which allows the user to change the current point
% x at the beginning of each iteration, before the stopping criterion
% is evaluated. See applyHook for help on how to use this option.
%
% Notice that statsfun is called with the point x that was reached last,
% after the accept/reject decision. Hence: if the step was accepted, we get
% that new x, with a store which only saw the call for the cost and for the
% gradient. If the step was rejected, we get the same x as previously, with
% the store structure containing everything that was computed at that point
% (possibly including previous rejects at that same point). Hence, statsfun
% should not be used in conjunction with the store to count operations for
% example. Instead, you should use manopt counters: see statscounters.
%
%
% Please cite the Manopt paper as well as the research paper:
% @Article{genrtr,
% Title = {Trust-region methods on {Riemannian} manifolds},
% Author = {Absil, P.-A. and Baker, C. G. and Gallivan, K. A.},
% Journal = {Foundations of Computational Mathematics},
% Year = {2007},
% Number = {3},
% Pages = {303--330},
% Volume = {7},
% Doi = {10.1007/s10208-005-0179-9}
% }
%
% See also: steepestdescent conjugategradient manopt/examples
% An explicit, general listing of this algorithm, with preconditioning,
% can be found in the following paper:
% @Article{boumal2015lowrank,
% Title = {Low-rank matrix completion via preconditioned optimization on the {G}rassmann manifold},
% Author = {Boumal, N. and Absil, P.-A.},
% Journal = {Linear Algebra and its Applications},
% Year = {2015},
% Pages = {200--239},
% Volume = {475},
% Doi = {10.1016/j.laa.2015.02.027},
% }
% When the Hessian is not specified, it is approximated with
% finite-differences of the gradient. The resulting method is called
% RTR-FD. Some convergence theory for it is available in this paper:
% @incollection{boumal2015rtrfd
% author={Boumal, N.},
% title={Riemannian trust regions with finite-difference Hessian approximations are globally convergent},
% year={2015},
% booktitle={Geometric Science of Information}
% }
% This file is part of Manopt: www.manopt.org.
% This code is an adaptation to Manopt of the original GenRTR code:
% RTR - Riemannian Trust-Region
% (c) 2004-2007, P.-A. Absil, C. G. Baker, K. A. Gallivan
% Florida State University
% School of Computational Science
% (http://www.math.fsu.edu/~cbaker/GenRTR/?page=download)
% See accompanying license file.
% The adaptation was executed by Nicolas Boumal.
%
%
% Change log:
%
% NB April 3, 2013:
% tCG now returns the Hessian along the returned direction eta, so
% that we do not compute that Hessian redundantly: some savings at
% each iteration. Similarly, if the useRand flag is on, we spare an
% extra Hessian computation at each outer iteration too, owing to
% some modifications in the Cauchy point section of the code specific
% to useRand = true.
%
% NB Aug. 22, 2013:
% This function is now Octave compatible. The transition called for
% two changes which would otherwise not be advisable. (1) tic/toc is
% now used as is, as opposed to the safer way:
% t = tic(); elapsed = toc(t);
% And (2), the (formerly inner) function savestats was moved outside
% the main function to not be nested anymore. This is arguably less
% elegant, but Octave does not (and likely will not) support nested
% functions.
%
% NB Dec. 2, 2013:
% The in-code documentation was largely revised and expanded.
%
% NB Dec. 2, 2013:
% The former heuristic which triggered when rhonum was very small and
% forced rho = 1 has been replaced by a smoother heuristic which
% consists in regularizing rhonum and rhoden before computing their
% ratio. It is tunable via options.rho_regularization. Furthermore,
% the solver now detects if tCG did not obtain a model decrease
% (which is theoretically impossible but may happen because of
% numerical errors and/or because of a nonlinear/nonsymmetric Hessian
% operator, which is the case for finite difference approximations).
% When such an anomaly is detected, the step is rejected and the
% trust region radius is decreased.
% Feb. 18, 2015 note: this is less useful now, as tCG now guarantees
% model decrease even for the finite difference approximation of the
% Hessian. It is still useful in case of numerical errors, but this
% is less stringent.
%
% NB Dec. 3, 2013:
% The stepsize is now registered at each iteration, at a small
% additional cost. The defaults for Delta_bar and Delta0 are better
% defined. Setting Delta_bar in the options will automatically set
% Delta0 accordingly. In Manopt 1.0.4, the defaults for these options
% were not treated appropriately because of an incorrect use of the
% isfield() built-in function.
%
% NB Feb. 18, 2015:
% Added some comments. Also, Octave now supports safe tic/toc usage,
% so we reverted the changes to use that again (see Aug. 22, 2013 log
% entry).
%
% NB April 3, 2015:
% Works with the new StoreDB class system.
%
% NB April 8, 2015:
% No Hessian warning if approximate Hessian explicitly available.
%
% NB Nov. 1, 2016:
% Now uses approximate gradient via finite differences if need be.
%
% NB Aug. 2, 2018:
% Using storedb.remove() to keep the cache lean, which allowed to
% reduce storedepth to 2 from 20 (by default).
%
% NB July 19, 2020:
% Added support for options.hook.
%
% VL Aug. 17, 2022:
% Refactored code to use various subproblem solvers with a new input
% output pattern. Modified how information about iterations is
% printed to accomodate new subproblem solvers. Moved all useRand and
% cauchy logic to trs_tCG. Options pertaining to tCG are still
% available but have moved to that file. Made trs_tCG_cached default.
%
% NB Aug. 16, 2024:
% Now allowing trs subproblem solvers to return a regularization term
% to add to rhonum and rhoden before computing rho (their ratio).
% This is stored in trsoutput.rho_reg;
%
% NB Sep. 4, 2024:
% Added options.allowcostincrease, true by default (so that the
% default behavior does not change).
% To implement this new option, the logic for step accept/reject and
% the logic for radius increase/decrease were permuted, and the pair
% of variables stop_next_time and stop_next_time_reason were added.
%
% NB Sep. 6, 2024:
% Added options.stopiftrstrouble, false by default (so that the
% default behavior does not change). This is useful to detect that we
% are so close to convergence that inexact arithmetic is starting to
% dominate the behavior of the method and so we should stop.
%
% NB Sep. 11, 2024:
% Removed legacy debug code that used to print the actual rho and the
% used rho, because those are now displayed under "rho" and
% "rho_noreg" at each iteration when verbosity is high enough.
%
% NB Dec. 13, 2024:
% Fixed a bug with AD caching which was introduced Aug. 17, 2024.
% Behavior unchanged.
%
% NB Jan. 12, 2026:
% Construction of trsinput was changed from struct('x', x, ...) to
% trsinput = struct(); trsinput.x = x; ... This prevents Matlab from
% silently creating the struct as a struct-array instead whenever the
% inputs involve (for example) cells. This caused some issues with
% powermanifold.
% Verify that the problem description is sufficient for the solver.
if ~canGetCost(problem)
warning('manopt:getCost', ...
'No cost provided. The algorithm will likely abort.');
end
if ~canGetGradient(problem) && ~canGetApproxGradient(problem)
% Note: we do not give a warning if an approximate gradient is
% explicitly given in the problem description, as in that case the user
% seems to be aware of the issue.
warning('manopt:getGradient:approx', ...
['No gradient provided. Using FD approximation (slow).\n' ...
'It may be necessary to increase options.tolgradnorm.\n' ...
'To disable this warning: ' ...
'warning(''off'', ''manopt:getGradient:approx'')']);
problem.approxgrad = approxgradientFD(problem);
end
if ~canGetHessian(problem) && ~canGetApproxHessian(problem)
% Note: we do not give a warning if an approximate Hessian is
% explicitly given in the problem description, as in that case the user
% seems to be aware of the issue.
warning('manopt:getHessian:approx', ...
['No Hessian provided. Using FD approximation.\n' ...
'To disable this warning: ' ...
'warning(''off'', ''manopt:getHessian:approx'')']);
problem.approxhess = approxhessianFD(problem);
end
% Set local defaults here
localdefaults.verbosity = 2;
localdefaults.maxtime = inf;
localdefaults.miniter = 0;
localdefaults.maxiter = 1000;
localdefaults.rho_prime = 0.1;
localdefaults.rho_regularization = 1e3;
localdefaults.subproblemsolver = @trs_tCG_cached;
localdefaults.tolgradnorm = 1e-6;
localdefaults.allowcostincrease = true;
localdefaults.stopiftrstrouble = false;
% Merge global and local defaults, then merge w/ user options, if any.
localdefaults = mergeOptions(getGlobalDefaults(), localdefaults);
if ~exist('options', 'var') || isempty(options)
options = struct();
end
options = mergeOptions(localdefaults, options);
M = problem.M;
% If no initial point x is given by the user, generate one at random.
if ~exist('x', 'var') || isempty(x)
x = M.rand();
end
% Set default Delta_bar and Delta0 separately to deal with additional
% logic: if Delta_bar is provided but not Delta0, let Delta0 automatically
% be some fraction of the provided Delta_bar.
if ~isfield(options, 'Delta_bar')
if isfield(M, 'typicaldist')
options.Delta_bar = M.typicaldist();
else
options.Delta_bar = sqrt(M.dim());
end
end
if ~isfield(options, 'Delta0')
options.Delta0 = options.Delta_bar / 8;
end
% Check some option values
assert(options.rho_prime < 1/4, ...
'options.rho_prime must be strictly smaller than 1/4.');
assert(options.Delta_bar > 0, ...
'options.Delta_bar must be positive.');
assert(options.Delta0 > 0 && options.Delta0 <= options.Delta_bar, ...
'options.Delta0 must be positive and smaller than Delta_bar.');
% It is sometimes useful to check what the actual option values are.
if options.verbosity >= 3
disp(options);
end
% Flip to "true" anytime to tell the solver to terminate in the next loop.
stop_next_time = false;
stop_next_time_reason = '';
% Create a store database and get a key for the current x
storedb = StoreDB(options.storedepth);
key = storedb.getNewKey();
ticstart = tic();
%% Initializations
% k counts the outer (TR) iterations. The semantic is that k counts the
% number of iterations fully executed so far.
k = 0;
% accept tracks if the proposed step is accepted (true) or declined (false)
accept = true;
% Initialize solution and companion measures: f(x), fgrad(x)
[fx, fgradx] = getCostGrad(problem, x, storedb, key);
norm_grad = M.norm(x, fgradx);
% Initialize trust-region radius
Delta = options.Delta0;
% Depending on the subproblem solver, different kinds of statistics are
% logged and displayed. This initial call to the solver tells us ahead of
% time what to write in the column headers for displayed information, and
% how to initialize the info struct-array.
trsinfo = options.subproblemsolver([], [], options);
% printheader is a string that contains the header for the subproblem
% solver's printed output.
printheader = trsinfo.printheader;
% initstats is a struct of initial values for the stats that the subproblem
% solver wishes to store.
initstats = trsinfo.initstats;
stats = savestats(problem, x, storedb, key, options, k, fx, norm_grad, ...
Delta, ticstart, initstats);
info(1) = stats;
info(min(10000, options.maxiter+1)).iter = [];
% Display headers, depending on verbosity level, then also the initial row.
if options.verbosity == 2
fprintf(['%3s %3s iter ', ...
'%15scost val %2sgrad. norm %s\n'], ...
' ', ' ', ' ', ' ', printheader);
fprintf(['%3s %3s %5d ', ...
'%+.16e %12e\n'], ...
' ', ' ', k, fx, norm_grad);
elseif options.verbosity > 2
fprintf(['%3s %3s iter ', ...
'%15scost val %2sgrad. norm %10srho %4srho_noreg ' ...
'%7sDelta %s\n'], ...
' ', ' ', ' ', ' ', ' ', ' ', ...
' ', printheader);
fprintf(['%3s %3s %5d ', ...
'%+.16e %12e\n'], ...
' ',' ', k, fx, norm_grad);
end
% To keep track of consecutive radius changes, so that we can warn the
% user if it appears necessary.
consecutive_TRplus = 0;
consecutive_TRminus = 0;
% **********************
% ** Start of TR loop **
% **********************
while true
% Start clock for this outer iteration
ticstart = tic();
% Apply the hook function if there is one: this allows external code to
% move x to another point. If the point is changed (indicated by a true
% value for the boolean 'hooked'), we update our knowledge about x.
[x, key, info, hooked] = applyHook(problem, x, storedb, key, ...
options, info, k+1);
if hooked
[fx, fgradx] = getCostGrad(problem, x, storedb, key);
norm_grad = M.norm(x, fgradx);
end
% Run standard stopping criterion checks
[stop, reason] = stoppingcriterion(problem, x, options, info, k+1);
% Ensure trustregions runs at least options.miniter iterations
if k < options.miniter
stop = 0;
end
if stop
if options.verbosity >= 1
fprintf([reason, '\n']);
end
break;
end
if stop_next_time
if options.verbosity >= 1
fprintf([stop_next_time_reason, '\n']);
end
break;
end
if options.debug > 0
fprintf([repmat('*', 1, 98) '\n']);
end
% *************************
% ** Begin TR Subproblem **
% *************************
% Solve TR subproblem with solver specified by options.subproblemsolver
trsinput = struct();
trsinput.x = x;
trsinput.fgradx = fgradx;
trsinput.Delta = Delta;
trsinput.accept = accept;
trsoutput = options.subproblemsolver(problem, trsinput, options, ...
storedb, key);
eta = trsoutput.eta;
Heta = trsoutput.Heta;
limitedbyTR = trsoutput.limitedbyTR;
trsnumericaltrouble = isfield(trsoutput, 'numericaltrouble') && ...
trsoutput.numericaltrouble;
trsprintstr = trsoutput.printstr;
trsstats = trsoutput.stats;
if trsnumericaltrouble && options.stopiftrstrouble
stop_next_time = true;
stop_next_time_reason = ['TRS encountered numerical issues. ' ...
'(This is expected once the gradient ' ...
'norm nears machine precision.)\n' ...
'Force TR to continue by setting ' ...
'options.stopiftrstrouble = false;'];
end
% This is computed for logging purposes and may be useful for some
% user-defined stopping criteria.
norm_eta = M.norm(x, eta);
if options.debug > 0
testangle = M.inner(x, eta, fgradx) / (norm_eta*norm_grad);
end
% Compute the tentative next iterate (the proposal)
x_prop = M.retr(x, eta);
key_prop = storedb.getNewKey();
% Compute the function value of the proposal
fx_prop = getCost(problem, x_prop, storedb, key_prop);
% Will we accept the proposal or not?
% Check the performance of the quadratic model against the actual cost.
rhonum = fx - fx_prop;
vecrho = M.lincomb(x, 1, fgradx, .5, Heta);
rhoden = -M.inner(x, eta, vecrho);
rho_noreg = rhonum / rhoden;
% Heuristic -- added Dec. 2, 2013 (NB) to replace the former heuristic.
% This heuristic is documented in the book by Conn Gould and Toint on
% trust-region methods, section 17.4.2.
% rhonum measures the difference between two numbers. Close to
% convergence, these two numbers are very close to each other, so
% that computing their difference is numerically challenging: there may
% be a significant loss in accuracy. Since the acceptance or rejection
% of the step is conditioned on the ratio between rhonum and rhoden,
% large errors in rhonum result in a very large error in rho, hence in
% erratic acceptance / rejection. Meanwhile, close to convergence,
% steps are usually trustworthy and we should transition to a Newton-
% like method, with rho=1 consistently. The heuristic thus shifts both
% rhonum and rhoden by a small amount such that far from convergence,
% the shift is irrelevant and close to convergence, the ratio rho goes
% to 1, effectively promoting acceptance of the step.
% The rationale is that close to convergence, both rhonum and rhoden
% are quadratic in the distance between x and x_prop. Thus, when this
% distance is on the order of sqrt(eps), the value of rhonum and rhoden
% is on the order of eps, which is indistinguishable from the numerical
% error, resulting in badly estimated rho's.
% For abs(fx) < 1, this heuristic is invariant under offsets of f but
% not under scaling of f. For abs(fx) > 1, the opposite holds. This
% should not alarm us, as this heuristic only triggers at the very last
% iterations if very fine convergence is demanded.
rho_reg_numeric = max(1, abs(fx)) * eps * options.rho_regularization;
% Added Aug. 16, 2024 (NB, RD)
% Some subproblem solvers (e.g., trs_tCG_randomized) may want to add
% specific regularization (not necessarilly positive) to rhonum and
% rhoden.
rho_reg_trs = 0;
if isfield(trsoutput, 'rho_reg')
rho_reg_trs = trsoutput.rho_reg;
end
% The rho we actually use is the one that was possibly regularized.
rhonum = rhonum + rho_reg_numeric + rho_reg_trs;
rhoden = rhoden + rho_reg_numeric + rho_reg_trs;
rho = rhonum / rhoden;
if options.debug > 0
fprintf('DBG: rhonum : %e\n', rhonum);
fprintf('DBG: rhoden : %e\n', rhoden);
end
% If a linear, symmetric operator is used for the Hessian (approx.) and
% if we had infinite numerical precision, then the model would always
% decrease when using a good subproblem solver. However:
% In practice, nonlinear approximations of the Hessian such as the
% built-in finite difference approximation on the one hand, and finite
% numerical accuracy on the other hand can cause the model to increase.
% In such scenarios, we decide to force a rejection of the step and a
% reduction of the trust-region radius. We test the sign of the
% regularized rhoden since the regularization is supposed to capture
% the accuracy to which rhoden is computed:
% If rhoden was negative before regularization but not after, that
% should not be (and is not) detected as a failure.
%
% Note (Feb. 17, 2015, NB): the most recent version of trs_tCG already
% includes a mechanism to ensure model decrease if the Cauchy step
% attained a decrease (which is theoretically the case under very lax
% assumptions). This being said, it is always possible that numerical
% errors will prevent this, so that it is good to keep a safeguard.
%
% The current strategy is that, if this should happen, then we reject
% the step and reduce the trust region radius. This also ensures that
% the actual cost values are monotonically decreasing (unless rho is
% regularized).
%
model_decreased = (rhoden >= 0);
if ~model_decreased
trsprintstr = [trsprintstr ', model did not decrease']; %#ok<AGROW>
end
% Added June 30, 2015 following observation by BM.
% With this modification, it is guaranteed that a step rejection is
% always accompanied by a TR reduction. This prevents stagnation in
% this "corner case" (NaN's really aren't supposed to occur, but it's
% nice if we can handle them nonetheless).
if isnan(rho)
fprintf(['rho is NaN! Forcing a radius decrease. ' ...
'This should not happen.\n']);
if isnan(fx_prop)
fprintf(['The cost function returned NaN (perhaps the ' ...
'retraction returned a bad point?)\n']);
else
fprintf('The cost function did not return a NaN value.\n');
end
end
% Compute this now (before fx is replaced by fx_prop).
fun_decrease = fx - fx_prop;
% Choose to accept or reject the proposed step based on the model
% performance. Note the strict inequality on rho.
accept = false;
accstr = 'REJ';
should_accept = (model_decreased && rho > options.rho_prime);
forbidden_cost_increase = fun_decrease < 0 && ...
~options.allowcostincrease;
if should_accept && ~forbidden_cost_increase
% Dec. 13, 2024
% This was commented out and the related computations were moved to
% the end of the if-block, because it broke something related to
% caching for AD. I do not know why.
%
% The commit that broke things (and which is reverted here) is:
% https://github.com/NicolasBoumal/manopt/commit/ ...
% 0d9b9c62b3df0d4aeac3374a7b7663b107a83384
% fgradx_prop = getGradient(problem, x_prop, storedb, key_prop);
% norm_grad_prop = M.norm(x_prop, fgradx_prop);
% To consider: we might want to check that the cost function value
% actually decreases (see comments below), and if not, we might
% want to require that the gradient norm decreases, that is,
% checking fun_decrease > 0 || norm_grad_prop < norm_grad.
% Maybe make this an optional stopping criterion? Make it soft?
accept = true;
accstr = 'acc';
% We accept the step: no need to keep the old cache.
storedb.removefirstifdifferent(key, key_prop);
x = x_prop;
key = key_prop;
fx = fx_prop;
fgradx = getGradient(problem, x, storedb, key); % fgradx_prop;
norm_grad = M.norm(x, fgradx); % norm_grad_prop;
end
if ~accept
% We reject the step: no need to keep cache related to the
% tentative step.
storedb.removefirstifdifferent(key_prop, key);
end
% A side effect of regularizing rho is that it can happen that the cost
% function goes up by a small amount for some accepted steps. This may
% be good because, numerically, computing the difference between
% fx_prop and fx is more difficult than computing the improvement in
% the model, because fx_prop and fx are on the same order of magnitude
% yet are separated by a very small gap near convergence, whereas the
% model improvement is computed as a sum of two small terms. Such steps
% may help reduce the gradient norm for example.
% However, for nonisolated minimizers, allowing cost increases may lead
% to drift along the solution set, making it difficult for the solver
% to settle down at a good solution.
% Code below informs the user of this event.
% In further updates, we could also introduce this as a stopping
% criterion.
% Note: we could consider returning x_prop rather than x if, for
% example, the gradient norm there is smaller.
if fun_decrease < 0 && options.verbosity >= 2
if accept
fprintf(['Between line above and below, cost function ' ...
'increased by %.2g (step size: %.2g)\n'], ...
-fun_decrease, norm_eta);
elseif should_accept
fprintf(['Between line above and below, cost function ' ...
'would have increased by %.2g (step size: %.2g), ' ...
'but the step was rejected instead.\n'], ...
-fun_decrease, norm_eta);
end
end
if should_accept && forbidden_cost_increase
stop_next_time = true;
stop_next_time_reason = sprintf(['Cost would have increased; ' ...
'options.allowcostincrease = false, ' ...
'options.rho_regularization = %.3g.'], ...
options.rho_regularization);
end
% Choose the new TR radius based on the model performance
trstr = ' ';
% If the actual decrease is smaller than 1/4 of the predicted decrease,
% then reduce the TR radius.
% To cover all edge cases, if the step is not accepted, definitely
% reduce the radius to avoid an infinite loop.
if ~accept || rho < 1/4 || isnan(rho)
trstr = 'TR-';
Delta = Delta/4;
consecutive_TRplus = 0;
consecutive_TRminus = consecutive_TRminus + 1;
if consecutive_TRminus >= 5 && options.verbosity >= 2
consecutive_TRminus = -inf;
fprintf([' +++ Detected many consecutive TR- (radius ' ...
'decreases).\n' ...
' +++ Consider dividing options.Delta_bar by 10.\n' ...
' +++ Current values: options.Delta_bar = %g and ' ...
'options.Delta0 = %g.\n'], options.Delta_bar, ...
options.Delta0);
end
% If the actual decrease is at least 3/4 of the predicted decrease and
% the subproblem solver hit the TR boundary, increase TR radius.
% We also keep track of the number of consecutive trust-region radius
% increases. If there are many, this may indicate the need to adapt the
% initial and maximum radii.
elseif rho > 3/4 && limitedbyTR
trstr = 'TR+';
Delta = min(2*Delta, options.Delta_bar);
consecutive_TRminus = 0;
consecutive_TRplus = consecutive_TRplus + 1;
if consecutive_TRplus >= 5 && options.verbosity >= 2
consecutive_TRplus = -inf;
fprintf([' +++ Detected many consecutive TR+ (radius ' ...
'increases).\n' ...
' +++ Consider multiplying options.Delta_bar by 10.\n' ...
' +++ Current values: options.Delta_bar = %g and ' ...
'options.Delta0 = %g.\n'], options.Delta_bar, ...
options.Delta0);
end
else
% Otherwise, keep the TR radius constant.
consecutive_TRplus = 0;
consecutive_TRminus = 0;
end
% k is the number of iterations we have accomplished.
k = k + 1;
% Make sure we don't use too much memory for the store database.
storedb.purge();
% Log statistics for freshly executed iteration.
% Everything after this in the loop is not accounted for in the timing.
stats = savestats(problem, x, storedb, key, options, k, fx, ...
norm_grad, Delta, ticstart, trsstats, ...
info, rho, rhonum, rhoden, accept, norm_eta, ...
limitedbyTR);
info(k+1) = stats;
% Display
if options.verbosity == 2
fprintf('%3s %3s %5d %+.16e %12e %s\n', ...
accstr, trstr, k, fx, norm_grad, trsprintstr);
elseif options.verbosity > 2
fprintf(['%3s %3s %5d %+.16e %.6e %+.6e ' ...
'%+.6e %.6e %s\n'], ...
accstr, trstr, k, fx, norm_grad, rho, ...
rho_noreg, Delta, trsprintstr);
if options.debug > 0
fprintf(' Delta : %f |eta| : %e\n', ...
Delta, norm_eta);
end
end
if options.debug > 0
fprintf('DBG: cos ang(eta, gradf): %d\n', testangle);
if rho == 0
fprintf('DBG: rho = 0: likely to hinder convergence.\n');
end
end
end % of TR loop (counter: k)
% Restrict info struct-array to useful part
info = info(1:k+1);
if options.debug > 0
fprintf([repmat('*', 1, 98) '\n']);
end
if options.verbosity > 0
fprintf('Total time is %f [s] (excludes statsfun)\n', info(end).time);
end
% Return the best cost reached
cost = fx;
end
% Routine in charge of collecting the current iteration stats
function stats = savestats(problem, x, storedb, key, options, k, fx, ...
norm_grad, Delta, ticstart, trsstats, info, rho, ...
rhonum, rhoden, accept, norm_eta, limitedbyTR)
stats.iter = k;
stats.cost = fx;
stats.gradnorm = norm_grad;
stats.Delta = Delta;
if k == 0
stats.time = toc(ticstart);
stats.rho = inf;
stats.rhonum = NaN;
stats.rhoden = NaN;
stats.accepted = true;
stats.stepsize = NaN;
stats.limitedbyTR = false;
fields = fieldnames(trsstats);
for i = 1 : length(fields)
stats.(fields{i}) = trsstats.(fields{i});
end
else
stats.time = info(k).time + toc(ticstart);
stats.rho = rho;
stats.rhonum = rhonum;
stats.rhoden = rhoden;
stats.accepted = accept;
stats.stepsize = norm_eta;
stats.limitedbyTR = limitedbyTR;
fields = fieldnames(trsstats);
for i = 1 : length(fields)
stats.(fields{i}) = trsstats.(fields{i});
end
end
% See comment about statsfun above: the x and store passed to statsfun
% are that of the most recently accepted point after the iteration
% fully executed.
stats = applyStatsfun(problem, x, storedb, key, options, stats);
end