How to run stochastic gradient descent

Ronny Bergmann

This tutorial illustrates how to use the stochastic_gradient_descent solver and different DirectionUpdateRules to introduce the average or momentum variant, see Stochastic Gradient Descent.

Computationally, we look at a very simple but large scale problem, the Riemannian Center of Mass or FrΓ©chet mean: for given points $p_i ∈\mathcal M$, $i=1,…,N$ this optimization problem reads

\[\operatorname*{arg\,min}_{p∈\mathcal M} \frac{1}{2N}\sum_{i=1}^{N} \operatorname{d}^2_{\mathcal M}(p,p_i),\]

which of course can be (and is) solved by a gradient descent, see the introductory tutorial or Statistics in Manifolds.jl. If $N$ is very large, evaluating the complete gradient might be quite expensive. A remedy is to evaluate only one of the terms at a time and choose a random order for these.

We first initialize the packages

using Manifolds, Manopt, Random, BenchmarkTools, ManifoldDiffusing ManifoldDiff: grad_distanceRandom.seed!(42);

We next generate a (little) large(r) data set

n = 5000Οƒ = Ο€ / 12M = Sphere(2)p = 1 / sqrt(2) * [1.0, 0.0, 1.0]data = [exp(M, p, Οƒ * rand(M; vector_at = p)) for i in 1:n];

Note that due to the construction of the points as zero mean tangent vectors, the mean should be very close to our initial point p.

In order to use the stochastic gradient, we now need a function that returns the vector of gradients. There are two ways to define it in Manopt.jl: either as a single function that returns a vector, or as a vector of functions.

The first variant is of course easier to define, but the second is more efficient when only evaluating one of the gradients.

For the mean, the gradient is

\[\operatorname{grad}f(p) = \frac{1}{N}\sum_{i=1}^N \operatorname{grad}f_i(p) \quad\text{ where }\quad \operatorname{grad}f_i(p) = -\log_p p_i\]

which we define in Manopt.jl in two different ways: either as one function returning all gradients as a vector (see gradF), or, maybe more fitting for a large scale problem, as a vector of small gradient functions (see gradf)

F(M, p) = 1 / (2 * n) * sum(map(q -> distance(M, p, q)^2, data))gradF(M, p) = [grad_distance(M, q, p) for q in data]gradf = [(M, p) -> grad_distance(M, q, p) for q in data];p0 = 1 / sqrt(3) * [1.0, 1.0, 1.0]
3-element Vector{Float64}:
 0.5773502691896258
 0.5773502691896258
 0.5773502691896258

The calls are only slightly different, but notice that accessing the second gradient element requires evaluating all logs in the first function, while we only call one of the functions in the second array of functions. So while you can use both gradF and gradf in the following call, the second one is (much) faster:

p_opt1 = stochastic_gradient_descent(M, gradF, p0)
3-element Vector{Float64}:
  0.735721541608524
 -0.13555304630490078
  0.6635805790185834
@benchmark stochastic_gradient_descent($M, $gradF, $p0)
BenchmarkTools.Trial: 3 samples with 1 evaluation per sample.
 Range (min … max):  394.351 ms … 6.339 s  β”Š GC (min … max): 11.98% … 15.96%
 Time  (median):        2.230 s            β”Š GC (median):    11.89%
 Time  (mean Β± Οƒ):      2.988 s Β± 3.044 s  β”Š GC (mean Β± Οƒ):  14.77% Β±  2.33%

  β–ˆ                β–ˆ                                       β–ˆ  
  β–ˆβ–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–ˆβ–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–ˆ ▁
  394 ms         Histogram: frequency by time         6.34 s <

 Memory estimate: 527.81 MiB, allocs estimate: 13175376.
p_opt2 = stochastic_gradient_descent(M, gradf, p0)
3-element Vector{Float64}:
  0.6128196000510743
 -0.06482597388852777
  0.7875593507175478
@benchmark stochastic_gradient_descent($M, $gradf, $p0)
BenchmarkTools.Trial: 2012 samples with 1 evaluation per sample.
 Range (min … max):  1.032 ms … 12.873 ms  β”Š GC (min … max): 0.00% … 61.59%
 Time  (median):     2.034 ms              β”Š GC (median):    0.00%
 Time  (mean Β± Οƒ):   2.486 ms Β±  1.664 ms  β”Š GC (mean Β± Οƒ):  9.79% Β± 13.30%

  ▅▅▅▃▃▂▁         β–ˆ                                           
  β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‡β–‡β–…β–…β–…β–„β–„β–…β–ˆβ–„β–‚β–β–‚β–β–‚β–‚β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–‚β–‚β–‚β–‚β–‚β–‚β–‚β–‚β–‚β–‚β–‚β–‚β–‚β–‚β–‚β–‚ β–ƒ
  1.03 ms        Histogram: frequency by time        10.6 ms <

 Memory estimate: 1.39 MiB, allocs estimate: 31664.

This result is reasonably close. We can also modify the search direction by using a DirectionUpdateRule, namely:

On the one hand MomentumGradient, which keeps track of the iterate and parallel transports the last direction to the current iterate. The necessary vector_transport_method= keyword is set to a suitable default on every manifold, see default_vector_transport_method. We get

p_opt3 = stochastic_gradient_descent(    M, gradf, p0; direction = MomentumGradient(; direction = StochasticGradient()))
3-element Vector{Float64}:
 0.6177180904046244
 0.12018904543850745
 0.7771608290073841
MG = MomentumGradient(; direction = StochasticGradient());@benchmark stochastic_gradient_descent($M, $gradf, $p0; direction = $MG)
BenchmarkTools.Trial: 835 samples with 1 evaluation per sample.
 Range (min … max):  5.072 ms … 13.540 ms  β”Š GC (min … max):  0.00% … 58.79%
 Time  (median):     5.216 ms              β”Š GC (median):     0.00%
 Time  (mean Β± Οƒ):   5.989 ms Β±  2.226 ms  β”Š GC (mean Β± Οƒ):  12.47% Β± 17.60%

  β–ˆβ–‡β–ƒβ–ƒ                                                        
  β–ˆβ–ˆβ–ˆβ–ˆβ–‡β–„β–β–„β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–„β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–„β–‡β–ˆβ–‡β–ˆβ–ˆβ–‡β–‡β–‡ β–‡
  5.07 ms      Histogram: log(frequency) by time       13 ms <

 Memory estimate: 8.26 MiB, allocs estimate: 211670.

And on the other hand the AverageGradient computes an average of the last n gradients. This is done by

p_opt4 = stochastic_gradient_descent(    M, gradf, p0; direction = AverageGradient(; n = 10, direction = StochasticGradient()),)
3-element Vector{Float64}:
 -0.12579991348829062
  0.8980164391742205
  0.42159323611651117
AG = AverageGradient(; n = 10, direction = StochasticGradient());@benchmark stochastic_gradient_descent($M, $gradf, $p0; direction = $AG)
BenchmarkTools.Trial: 409 samples with 1 evaluation per sample.
 Range (min … max):   9.966 ms … 19.720 ms  β”Š GC (min … max):  0.00% … 42.61%
 Time  (median):     10.506 ms              β”Š GC (median):     0.00%
 Time  (mean Β± Οƒ):   12.230 ms Β±  3.297 ms  β”Š GC (mean Β± Οƒ):  14.63% Β± 17.99%

   β–…β–ˆβ–…β–                                                        
  β–‡β–ˆβ–ˆβ–ˆβ–ˆβ–…β–ƒβ–ƒβ–‚β–‚β–‚β–β–β–β–β–β–β–β–β–β–β–‚β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–‚β–„β–ƒβ–ƒβ–ƒβ–„β–…β–ƒβ–ƒβ–ƒβ–ƒβ–‚β–‚ β–ƒ
  9.97 ms         Histogram: frequency by time        19.3 ms <

 Memory estimate: 19.70 MiB, allocs estimate: 511691.

Note that the default StoppingCriterion here is StopAfterIteration(10000) combined with StopWhenGradientNormLess(1e-9).

For both update rules we have to internally specify that we are still in the stochastic setting, since both rules can also be used with the IdentityUpdateRule within gradient_descent.

For this not-that-large-scale example we can of course also use a gradient descent with ArmijoLinesearch,

fullGradF(M, p) = 1 / n * sum(grad_distance(M, q, p) for q in data)p_opt5 = gradient_descent(M, F, fullGradF, p0; stepsize = ArmijoLinesearch())
3-element Vector{Float64}:
  0.7050420976839262
 -0.006374163322665686
  0.7091368066426853

but in general it is expected to be a bit slow.

AL = ArmijoLinesearch();@benchmark gradient_descent($M, $F, $fullGradF, $p0; stepsize = $AL)
BenchmarkTools.Trial: 46 samples with 1 evaluation per sample.
 Range (min … max):  102.437 ms … 114.911 ms  β”Š GC (min … max):  6.87% … 13.54%
 Time  (median):     110.810 ms               β”Š GC (median):    13.56%
 Time  (mean Β± Οƒ):   108.708 ms Β±   3.863 ms  β”Š GC (mean Β± Οƒ):  11.68% Β±  3.01%

                                          β–ˆβ–β–β–β–ƒ                  
  β–†β–β–‡β–†β–„β–‡β–„β–„β–†β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–β–„β–β–„β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–β–β–„β–β–„β–β–„β–β–β–β–β–β–β–β–β–„ ▁
  102 ms           Histogram: frequency by time          115 ms <

 Memory estimate: 138.38 MiB, allocs estimate: 3397521.
Technical Details

This tutorial is cached. It was last run on the following package versions.

Status `~/work/Manopt.jl/Manopt.jl/tutorials/Project.toml`
  [47edcb42] ADTypes v1.24.0
  [6e4b80f9] BenchmarkTools v1.8.0
  [5ae59095] Colors v0.13.1
  [a0c0ee7d] DifferentiationInterface v0.7.21
  [31c24e10] Distributions v0.25.131
  [26cc04aa] FiniteDifferences v0.12.34
  [f6369f11] ForwardDiff v1.4.5
  [8ac3fa9e] LRUCache v1.6.2
  [af67fdf4] ManifoldDiff v0.4.5
  [1cead3c2] Manifolds v0.11.29
  [3362f125] ManifoldsBase v2.5.1
  [0fc0a36d] Manopt v0.6.7 `.`
  [91a5bcdd] Plots v1.41.7
  [731186ca] RecursiveArrayTools v4.5.1
  [37e2e46d] LinearAlgebra v1.12.0
  [9a3f8284] Random v1.11.0

This tutorial was last rendered September 9, 2026, 5:30:1.