|
| 1 | +<!doctype html> |
| 2 | +<html class="no-js" lang="en"> |
| 3 | +<head> |
| 4 | + <meta charset="utf-8"> |
| 5 | + <meta name="viewport" content="width=device-width, initial-scale=1"> |
| 6 | + <link rel="stylesheet" href="https://interactivecomputergraphics.github.io/physics-simulation/examples/style.css"> |
| 7 | + <script type="text/x-mathjax-config"> |
| 8 | + MathJax.Hub.Config({ |
| 9 | + extensions: ["tex2jax.js"], |
| 10 | + jax: ["input/TeX", "output/HTML-CSS"], |
| 11 | + tex2jax: { |
| 12 | + inlineMath: [ ['$','$'], ["\\(","\\)"] ], |
| 13 | + displayMath: [ ['$$','$$'], ["\\[","\\]"] ], |
| 14 | + processEscapes: true |
| 15 | + }, |
| 16 | + "HTML-CSS": { fonts: ["TeX"] } |
| 17 | + }); |
| 18 | + </script> |
| 19 | + <script type="text/javascript" async src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.4/MathJax.js"></script> |
| 20 | + <script src="https://cdn.plot.ly/plotly-2.5.1.min.js"></script> |
| 21 | + <title>Newton's Method with Backtracking Line Search</title> |
| 22 | +</head> |
| 23 | +<body> |
| 24 | + |
| 25 | +<header class="page-header"> |
| 26 | + <h1>Newton's Method with Backtracking Line Search</h1> |
| 27 | +</header> |
| 28 | + |
| 29 | +<main> |
| 30 | + <!-- Simulation panel: canvas + controls --> |
| 31 | + <div class="card sim-panel"> |
| 32 | + <div class="sim-canvas-wrap"> |
| 33 | + <div id="plotOutput" style="width: 100%; height: auto;border:0px solid #000000;border-radius: 0px;background-color:#EEEEEE"></div> |
| 34 | + </div> |
| 35 | + </div> |
| 36 | + <div class="controls-panel" style="width: 100%;align:center;margin-left:auto;margin-right:auto"> |
| 37 | + <h3>Controls</h3> |
| 38 | + <div class="controls-grid" style="width: 400px;align:left"> |
| 39 | + <label>Newton steps</label> |
| 40 | + <p> |
| 41 | + <button onclick="plot.changeSteps(-1)" style="width:30px;height:30px;font-size:16px;cursor:pointer">−</button> |
| 42 | + <input type="text" id="textInput" value="1" readonly style="width:40px;text-align:center"> |
| 43 | + <button onclick="plot.changeSteps(+1)" style="width:30px;height:30px;font-size:16px;cursor:pointer">+</button> |
| 44 | + </p> |
| 45 | + |
| 46 | + <label for="armijo_c">Armijo parameter $c$</label> |
| 47 | + <input type="text" id="textInputC" value="0.50" readonly> |
| 48 | + |
| 49 | + <label></label> |
| 50 | + <input onchange="document.getElementById('textInputC').value=parseFloat(this.value).toFixed(4);plot.reset()" id="armijo_c" value="0.50" type="range" min="0.0001" max="0.99" step="0.0001"> |
| 51 | + |
| 52 | + <label for="tau">Backtracking factor $\tau$</label> |
| 53 | + <input type="text" id="textInputTau" value="0.50" readonly> |
| 54 | + |
| 55 | + <label></label> |
| 56 | + <input onchange="document.getElementById('textInputTau').value=parseFloat(this.value).toFixed(2);plot.reset()" id="tau" value="0.50" type="range" min="0.10" max="0.95" step="0.05"> |
| 57 | + |
| 58 | + <label for="fct">Function</label> |
| 59 | + <select onchange="plot.reset()" id="fct" size="1"> |
| 60 | + <option selected="selected">Hyperbolic function</option> |
| 61 | + <option>Sinusoidal function</option> |
| 62 | + <option>Gaussian bump function</option> |
| 63 | + <option>Log-rational function</option> |
| 64 | + </select> |
| 65 | + </div> |
| 66 | + </div> |
| 67 | + |
| 68 | + <!-- Theory section --> |
| 69 | + <div class="card theory"> |
| 70 | + <h2>Newton's method with backtracking line search</h2> |
| 71 | + |
| 72 | + <p>Newton's method for minimization uses the <em>Newton direction</em> $d_n = -f'(x_n)/f''(x_n)$ and updates:</p> |
| 73 | + $$x_{n+1} = x_n + \alpha_n \, d_n = x_n - \alpha_n \frac{f'(x_n)}{f''(x_n)}$$ |
| 74 | + <p>With $\alpha_n = 1$ this recovers the standard Newton step. However, far from the minimum the full Newton step can overshoot. Backtracking line search selects $\alpha_n$ automatically to guarantee sufficient decrease.</p> |
| 75 | + |
| 76 | + <h2>The Armijo condition</h2> |
| 77 | + |
| 78 | + <p>For a general descent direction $d_n$, the <strong>Armijo condition</strong> requires:</p> |
| 79 | + $$f(x_n + \alpha\, d_n) \;\leq\; f(x_n) + c\,\alpha\, f'(x_n)\,d_n$$ |
| 80 | + <p>Substituting the Newton direction $d_n = -f'(x_n)/f''(x_n)$ gives:</p> |
| 81 | + $$f\!\left(x_n - \alpha\frac{f'(x_n)}{f''(x_n)}\right) \;\leq\; f(x_n) - c\,\alpha\,\frac{[f'(x_n)]^2}{f''(x_n)}$$ |
| 82 | + <p>Here $c \in (0,1)$ is a small constant (typically $c = 10^{-4}$ in practice; larger values are used here to make the effect visible).</p> |
| 83 | + |
| 84 | + <h2>The backtracking algorithm</h2> |
| 85 | + |
| 86 | + <p>Starting from $\alpha = 1$ (the full Newton step), the step is repeatedly shrunk by a factor $\tau \in (0,1)$ until the Armijo condition is satisfied:</p> |
| 87 | + <ol> |
| 88 | + <li>Set $\alpha \leftarrow 1$.</li> |
| 89 | + <li>If $f(x_n - \alpha f'(x_n)/f''(x_n)) \leq f(x_n) - c\,\alpha\,[f'(x_n)]^2/f''(x_n)$, accept $\alpha$.</li> |
| 90 | + <li>Otherwise set $\alpha \leftarrow \tau\alpha$ and go to step 2.</li> |
| 91 | + </ol> |
| 92 | + |
| 93 | + <p>The plot shows: the function $f(x)$ (blue); the tangent line at $x_n$ (gray dashed); the orange dashed line marking the <em>Armijo RHS threshold</em> $f(x_n) - c\,\alpha\,[f'(x_n)]^2/f''(x_n)$ for the accepted step size. The Armijo condition is satisfied when the function value lies <em>at or below</em> the orange line. Rejected trial steps (from the last iteration) are shown as faded red crosses × on the curve; the accepted step is the solid red dot. The vertical black dashed lines mark successive iterates $x_0, x_1, \ldots$</p> |
| 94 | + |
| 95 | + <p>Use the +/− buttons and sliders to observe how a larger $c$ makes the condition stricter (requiring more backtracking), and how a smaller $\tau$ shrinks the step more aggressively.</p> |
| 96 | + </div> |
| 97 | + |
| 98 | +</main> |
| 99 | + |
| 100 | +<script id="simulation_code" type="text/javascript"> |
| 101 | + class Plot |
| 102 | + { |
| 103 | + constructor() |
| 104 | + { |
| 105 | + this.reset(); |
| 106 | + } |
| 107 | + |
| 108 | + reset() |
| 109 | + { |
| 110 | + this.num_steps = parseInt(document.getElementById('textInput').value); |
| 111 | + this.c = parseFloat(document.getElementById('armijo_c').value); |
| 112 | + this.tau = parseFloat(document.getElementById('tau').value); |
| 113 | + this.fct = document.getElementById('fct').value; |
| 114 | + this.plotFunctions(); |
| 115 | + } |
| 116 | + |
| 117 | + changeSteps(delta) |
| 118 | + { |
| 119 | + const el = document.getElementById('textInput'); |
| 120 | + let val = parseInt(el.value) + delta; |
| 121 | + val = Math.max(1, Math.min(30, val)); |
| 122 | + el.value = val; |
| 123 | + this.reset(); |
| 124 | + } |
| 125 | + |
| 126 | + // ------------------------------------------------------------------ // |
| 127 | + // Function definitions |
| 128 | + // All four are chosen so that the full Newton step (α=1) fails the |
| 129 | + // Armijo condition from the given starting point. |
| 130 | + // ------------------------------------------------------------------ // |
| 131 | + |
| 132 | + // f(x) = log(cosh(2x)) — minimum at x=0 |
| 133 | + // Hessian = 4·sech²(2x) is tiny far from 0 → Newton step is huge. |
| 134 | + hyperbolic_function(x) { return Math.log(Math.cosh(2*x)); } |
| 135 | + grad_hyperbolic_function(x) { return 2*Math.tanh(2*x); } |
| 136 | + hess_hyperbolic_function(x) { const c = Math.cosh(2*x); return 4/(c*c); } |
| 137 | + |
| 138 | + // f(x) = 0.5x² + 2sin(x) — minimum near x ≈ -1.03 |
| 139 | + // From x=-2.5 the Newton step overshoots to the wrong side. |
| 140 | + sinusoidal_function(x) { return 0.5*x*x + 2*Math.sin(x); } |
| 141 | + grad_sinusoidal_function(x) { return x + 2*Math.cos(x); } |
| 142 | + hess_sinusoidal_function(x) { return 1 - 2*Math.sin(x); } |
| 143 | + |
| 144 | + // f(x) = 0.5x² + 5·exp(−0.5x²) — minima at x ≈ ±1.794 |
| 145 | + // Gaussian bump makes f non-convex near x=0; the full Newton step |
| 146 | + // from x=3 lands in the non-convex region, failing Armijo. |
| 147 | + gaussian_bump_function(x) { return 0.5*x*x + 5*Math.exp(-0.5*x*x); } |
| 148 | + grad_gaussian_bump_function(x) { return x*(1 - 5*Math.exp(-0.5*x*x)); } |
| 149 | + hess_gaussian_bump_function(x) { return 1 + 5*Math.exp(-0.5*x*x)*(x*x - 1); } |
| 150 | + |
| 151 | + // f(x) = x² − 4·log(x²+1) + 1 — minima at x ≈ ±√3, local max at x=0 |
| 152 | + // Logarithmic term gives a W-shape; Newton step from x=3 overshoots. |
| 153 | + log_rational_function(x) { return x*x - 4*Math.log(x*x + 1) + 1; } |
| 154 | + grad_log_rational_function(x) { return 2*x*(x*x - 3)/(x*x + 1); } |
| 155 | + hess_log_rational_function(x) { const d = x*x + 1; return 2*(x*x-3)/d + 16*x*x/(d*d); } |
| 156 | + |
| 157 | + // ------------------------------------------------------------------ // |
| 158 | + // Backtracking line search (Newton direction) — returns accepted α |
| 159 | + // ------------------------------------------------------------------ // |
| 160 | + backtrack(fct, grad_fct, hess_fct, xn, c, tau) |
| 161 | + { |
| 162 | + const fn = fct(xn); |
| 163 | + const gn = grad_fct(xn); |
| 164 | + const hn = hess_fct(xn); |
| 165 | + if (hn <= 0) return { alpha: 0, attempts: 0, rejected: [] }; // not a descent direction |
| 166 | + const dn = -gn / hn; // Newton direction |
| 167 | + let alpha = 1.0; |
| 168 | + const maxIter = 60; |
| 169 | + const rejected = []; |
| 170 | + for (let k = 0; k < maxIter; k++) |
| 171 | + { |
| 172 | + const xNew = xn + alpha * dn; |
| 173 | + // Armijo: f(xn + α·dn) ≤ f(xn) + c·α·f'(xn)·dn |
| 174 | + if (fct(xNew) <= fn + c * alpha * gn * dn) |
| 175 | + return { alpha: alpha, attempts: k + 1, rejected: rejected, converged: true }; |
| 176 | + rejected.push(xNew); |
| 177 | + alpha *= tau; |
| 178 | + } |
| 179 | + return { alpha: alpha, attempts: maxIter, rejected: rejected, converged: false }; |
| 180 | + } |
| 181 | + |
| 182 | + // ------------------------------------------------------------------ // |
| 183 | + // Build all Plotly traces |
| 184 | + // ------------------------------------------------------------------ // |
| 185 | + computeData(fct, grad_fct, hess_fct, x0, data) |
| 186 | + { |
| 187 | + const c = this.c; |
| 188 | + const tau = this.tau; |
| 189 | + |
| 190 | + // ---- main function curve ---- |
| 191 | + const numPts = 5000; |
| 192 | + const xMin = -3, xMax = 3; |
| 193 | + const xArr = [], yArr = []; |
| 194 | + for (let i = 0; i <= numPts; i++) |
| 195 | + { |
| 196 | + const xi = xMin + i * (xMax - xMin) / numPts; |
| 197 | + xArr.push(xi); |
| 198 | + yArr.push(fct(xi)); |
| 199 | + } |
| 200 | + data.push({ |
| 201 | + x: xArr, y: yArr, |
| 202 | + name: "f(x)", |
| 203 | + showlegend: true, |
| 204 | + line: { color: 'rgb(31, 119, 180)', width: 2 } |
| 205 | + }); |
| 206 | + |
| 207 | + // ---- collect iterates ---- |
| 208 | + let iterates = [x0]; |
| 209 | + let alphas = []; |
| 210 | + let attempts = []; |
| 211 | + let rejectedAll = []; |
| 212 | + let xCur = x0; |
| 213 | + let converged = true; |
| 214 | + for (let i = 0; i < this.num_steps; i++) |
| 215 | + { |
| 216 | + const res = this.backtrack(fct, grad_fct, hess_fct, xCur, c, tau); |
| 217 | + alphas.push(res.alpha); |
| 218 | + attempts.push(res.attempts); |
| 219 | + rejectedAll.push(res.rejected); |
| 220 | + // Newton update: x_{n+1} = x_n - α * f'(x_n)/f''(x_n) |
| 221 | + xCur = xCur - res.alpha * grad_fct(xCur) / hess_fct(xCur); |
| 222 | + iterates.push(xCur); |
| 223 | + if (!res.converged) { converged = false; break; } |
| 224 | + } |
| 225 | + |
| 226 | + // ---- per-step decorations ---- |
| 227 | + for (let i = 0; i < this.num_steps; i++) |
| 228 | + { |
| 229 | + const xn = iterates[i]; |
| 230 | + const xn1 = iterates[i + 1]; |
| 231 | + const fn = fct(xn); |
| 232 | + const gn = grad_fct(xn); |
| 233 | + const alpha = alphas[i]; |
| 234 | + const hn = hess_fct(xn); |
| 235 | + const dn = (hn > 0) ? -gn / hn : 0; // Newton direction |
| 236 | + const rejPts = rejectedAll[i]; |
| 237 | + const isLast = (i === this.num_steps - 1); |
| 238 | + |
| 239 | + // Range for the tangent line (centred on xn, wide enough to cover all trials) |
| 240 | + const halfW = Math.max(Math.abs(alpha * dn) + 0.5, 0.8); |
| 241 | + const lo = xn - halfW; |
| 242 | + const hi = xn + halfW; |
| 243 | + const nPts = 200; |
| 244 | + |
| 245 | + // Tangent line: f(xn) + f'(xn)*(x - xn) |
| 246 | + const txArr = [], tyArr = []; |
| 247 | + for (let j = 0; j <= nPts; j++) |
| 248 | + { |
| 249 | + const xt = lo + j * (hi - lo) / nPts; |
| 250 | + txArr.push(xt); |
| 251 | + tyArr.push(fn + gn * (xt - xn)); |
| 252 | + } |
| 253 | + data.push({ |
| 254 | + x: txArr, y: tyArr, |
| 255 | + line: { color: 'rgba(100,100,100,0.6)', width: 1.5, dash: 'dash' }, |
| 256 | + name: "tangent at xₙ", |
| 257 | + showlegend: i === 0 |
| 258 | + }); |
| 259 | + |
| 260 | + // Armijo RHS: horizontal dashed line — only for the current (last) iteration |
| 261 | + if (isLast) |
| 262 | + { |
| 263 | + const threshY_val = fn + c * alpha * gn * dn; |
| 264 | + data.push({ |
| 265 | + x: [lo, hi], y: [threshY_val, threshY_val], |
| 266 | + mode: 'lines', |
| 267 | + line: { color: 'rgba(210,120,0,0.9)', width: 2, dash: 'dash' }, |
| 268 | + name: "Armijo RHS: f(xₙ) − c·α·[f'(xₙ)]²/f''(xₙ)", |
| 269 | + showlegend: true |
| 270 | + }); |
| 271 | + } |
| 272 | + |
| 273 | + // X markers: rejected trial points — only for the last iteration |
| 274 | + if (isLast && rejPts.length > 0) |
| 275 | + { |
| 276 | + data.push({ |
| 277 | + x: rejPts, |
| 278 | + y: rejPts.map(xr => fct(xr)), |
| 279 | + mode: 'markers', |
| 280 | + marker: { color: 'rgba(180, 60, 60, 0.55)', size: 10, symbol: 'x', line: { width: 2, color: 'rgba(180,60,60,0.55)' } }, |
| 281 | + name: "rejected step", |
| 282 | + showlegend: true |
| 283 | + }); |
| 284 | + } |
| 285 | + |
| 286 | + // Red dot: accepted next point (xn1, f(xn1)) |
| 287 | + const nBacktracks = attempts[i] - 1; |
| 288 | + const stepLabel = nBacktracks === 0 |
| 289 | + ? "accepted (full Newton step)" |
| 290 | + : "backtracked " + nBacktracks + "×"; |
| 291 | + data.push({ |
| 292 | + x: [xn1], y: [fct(xn1)], |
| 293 | + mode: 'markers', |
| 294 | + marker: { color: 'red', size: 10 }, |
| 295 | + name: stepLabel, |
| 296 | + showlegend: true |
| 297 | + }); |
| 298 | + |
| 299 | + // Vertical dashed line at current iterate xn |
| 300 | + data.push({ |
| 301 | + x: [xn, xn], y: [0, fn], |
| 302 | + mode: 'lines+markers+text', |
| 303 | + line: { color: 'rgb(0,0,0)', width: 2, dash: 'dash' }, |
| 304 | + text: ["x_" + i, ""], |
| 305 | + textposition: "bottom center", |
| 306 | + name: "xₙ", |
| 307 | + showlegend: i === 0 |
| 308 | + }); |
| 309 | + } |
| 310 | + |
| 311 | + // Vertical dashed line for the final iterate |
| 312 | + const xFinal = iterates[this.num_steps]; |
| 313 | + const fFinal = fct(xFinal); |
| 314 | + data.push({ |
| 315 | + x: [xFinal, xFinal], y: [0, fFinal], |
| 316 | + mode: 'lines+markers+text', |
| 317 | + line: { color: 'rgb(0,0,0)', width: 2, dash: 'dash' }, |
| 318 | + text: ["x_" + this.num_steps, ""], |
| 319 | + textposition: "bottom center", |
| 320 | + name: "xₙ", |
| 321 | + showlegend: false |
| 322 | + }); |
| 323 | + |
| 324 | + if (!converged) |
| 325 | + { |
| 326 | + data.push({ |
| 327 | + x: [xFinal], y: [fFinal], |
| 328 | + mode: 'markers+text', |
| 329 | + marker: { color: 'red', size: 10 }, |
| 330 | + text: ["failed to converge"], |
| 331 | + textposition: "top center", |
| 332 | + name: "failed to converge", |
| 333 | + showlegend: true |
| 334 | + }); |
| 335 | + } |
| 336 | + } |
| 337 | + |
| 338 | + plotFunctions() |
| 339 | + { |
| 340 | + const data = []; |
| 341 | + |
| 342 | + if (this.fct === "Hyperbolic function") |
| 343 | + this.computeData(this.hyperbolic_function.bind(this), |
| 344 | + this.grad_hyperbolic_function.bind(this), |
| 345 | + this.hess_hyperbolic_function.bind(this), 1.5, data); |
| 346 | + |
| 347 | + if (this.fct === "Sinusoidal function") |
| 348 | + this.computeData(this.sinusoidal_function.bind(this), |
| 349 | + this.grad_sinusoidal_function.bind(this), |
| 350 | + this.hess_sinusoidal_function.bind(this), -2.5, data); |
| 351 | + |
| 352 | + if (this.fct === "Gaussian bump function") |
| 353 | + this.computeData(this.gaussian_bump_function.bind(this), |
| 354 | + this.grad_gaussian_bump_function.bind(this), |
| 355 | + this.hess_gaussian_bump_function.bind(this), 3.0, data); |
| 356 | + |
| 357 | + if (this.fct === "Log-rational function") |
| 358 | + this.computeData(this.log_rational_function.bind(this), |
| 359 | + this.grad_log_rational_function.bind(this), |
| 360 | + this.hess_log_rational_function.bind(this), 3.0, data); |
| 361 | + |
| 362 | + const layout = { |
| 363 | + title: "Newton's method with backtracking line search (Armijo condition)", |
| 364 | + autosize: true, |
| 365 | + xaxis: { range: [-4, 4] }, |
| 366 | + yaxis: { range: [-4, 8] } |
| 367 | + }; |
| 368 | + |
| 369 | + Plotly.newPlot('plotOutput', data, layout); |
| 370 | + } |
| 371 | + } |
| 372 | + |
| 373 | + const plot = new Plot(); |
| 374 | + plot.reset(); |
| 375 | +</script> |
| 376 | + |
| 377 | +</body> |
| 378 | +</html> |
0 commit comments