<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://jordialonsoesteve.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://jordialonsoesteve.github.io/" rel="alternate" type="text/html" /><updated>2026-03-31T09:29:58+00:00</updated><id>https://jordialonsoesteve.github.io/feed.xml</id><title type="html">Jordi’s ML blog</title><subtitle> Interesting bits in applied mathematics &amp; machine learning</subtitle><author><name>Jordi Alonso</name></author><entry><title type="html">LASSO: Subgradient method, projected gradient descent, proximal gradient descent and coordinate descent.</title><link href="https://jordialonsoesteve.github.io/2026/03/13/LASSO-algos.html" rel="alternate" type="text/html" title="LASSO: Subgradient method, projected gradient descent, proximal gradient descent and coordinate descent." /><published>2026-03-13T00:00:00+00:00</published><updated>2026-03-13T00:00:00+00:00</updated><id>https://jordialonsoesteve.github.io/2026/03/13/LASSO-algos</id><content type="html" xml:base="https://jordialonsoesteve.github.io/2026/03/13/LASSO-algos.html"><![CDATA[<link href="/css/syntax.css" rel="stylesheet" />

<p>We will cover, superficially, the main optimization approaches for a l1 penalized regression. Jupyter notebook with the implementation can be found <a href="https://github.com/JordiAlonsoEsteve/JordiAlonsoEsteve.github.io/blob/main/_posts/Optimization/Code/lasso_algos.ipynb">here</a>.</p>

<h2 id="the-least-absolute-shrinkage-and-selection-operator-lasso-problem">The Least Absolute Shrinkage and Selection Operator (LASSO) problem</h2>

<p>Let $\mathbf{X} \in \mathbb{R}^{N \times p}$, $\mathbf{y} \in \mathbb{R}^{N}$, being $p$ the number of “features” and $N$ the number of “observations”. The LASSO problem is:</p>

\[\begin{aligned}
\min_{\beta \in \mathbb{R}^p} \quad &amp; \frac{1}{2N} \|\mathbf{y} - \mathbf{X}\beta\|_2^2 \\
\text{subject to} \quad &amp; \|\beta\|_1 \leq t
\end{aligned}\]

<p>Which, as long as the constrain is active, we have a one-to-one correspondence with a Lagrangian formulation in terms of \(\lambda\):</p>

\[\min_{\beta \in \mathbb{R}^p} \left\{ \frac{1}{2n} \|\mathbf{y} - \mathbf{X}\beta\|_2^2 + \lambda \|\beta\|_1 \right\}\]

<p>This is probably one of the easiest interesting problems, stemming from the fact that the absolute value \(\vert x \vert\) is not a differentiable function at \(x = 0\). This is rather obvious since</p>

\[\lim_{h \to 0^+} \frac{|0 + h| - |0|}{h} \neq  \lim_{h \to 0^-} \frac{|0 + h| - |0|}{h}.\]

<h2 id="subgradient-method">Subgradient method</h2>
<p>A workaround (which easily holds for convex functions) is the subgradient:</p>

<p>A vector $g \in \mathbb{R}^n$ is a subgradient of $f:\mathbb{R}^n \rightarrow \mathbb{R}$ at $x \in \text{dom}\, f$ if 
\(f(z) \geq f(x) + g^{\top}(z - x)\)
$\forall z \in \text{dom} \,f.$ The set of all subgradients at $x$ is refered to as subdifferential ($\partial f(x)$).</p>

<p>For $\vert x\vert$ is actually very simple:</p>

\[\partial \vert x\vert = 
\begin{cases} 
\{1\} &amp; \text{if } x &gt; 0 \\ 
\{-1\} &amp; \text{if } x &lt; 0 \\ 
[-1, 1] &amp; \text{if } x = 0 
\end{cases}\]

<p>Now, we have that:</p>

\[0 \in \partial f(x) \implies f(y) \geq f(x) \, \forall y \in \text{dom} \, f.\]

<p>And hence that is a condition for optimality.</p>

<p>This allows to proceed with a subgradient method approach, akin to gradient descent! However, the convergence rate of this algorithm is $\mathcal{O}(\frac{1}{\sqrt{k} })$ for $k$ being the number of iterations. This is remarcably worse than gradient descent, which is an interesting result in itself.</p>

<p>The algorithm is very simple:</p>

<p>looking at the lasso problem, taking the derivative:</p>

\[\frac{\partial \big (\frac{1}{2n} \|\mathbf{y} - \mathbf{X}\beta\|_2^2 + \lambda \|\beta\|_1 \big )}{\partial \beta} = -\mathbf{X}^{\top}\mathbf{y} + \mathbf{X}^{\top}\mathbf{X}\beta +  \lambda \partial \|\beta\|_1.\]

<p>And we would be kind of done. However, the step size is a tricky issue in this case. In fact, this is probably one of the most interesting bits, although we will not dive in the theory here (will be covered in some other notes).</p>

<p>For a convex and L-smooth function (where the gradient is Lipschitz continuous with constant L), gradient gescent is guaranteed to converge to the global optimum using a fixed step size $\eta$, provided that $0&lt;\eta\leq\frac{1}{L}$ at a rate $\mathcal{O}(\frac{1}{k})$. However, this is <strong>not</strong> the case for the subgradient method. The guarantee of convergence to the optimum in the non-differentiable case requires diminishing step sizes, an easy option that works is, for iteration $k$:</p>

\[\eta_k = \eta_0 / \sqrt{k}.\]

<p>In the LASSO case, this has a clear intuition. For a fixed step we will never reach actual 0s because the subgradient of the absolute value​ remains constant in magnitude regardless of how close a coefficient is to the origin (which is very different from an l2 norm, for example). Instead of settling, the iterates will simply “bounce” across the axis in a perpetual oscillation of size proportional to $\eta \lambda$.</p>

<p>For example, 50 samples 100 dimensions, and some small noise, starting at $\eta = 0.1$. Only the first 10 dimensions are non zero but the subgradient approach gets no actual zeros!</p>

<h3 id="projected-gradient-descent">Projected gradient descent</h3>

<p>We are back at the constrained optimization problem formulation. The idea is to keep $\beta$ inside of the set described by the constraint $|\beta|_1 \leq t$. This is, while applying the gradient step might get the parameters out of the set, we will push them back intto it. In particular, we will find the element in the set that is closest to our current value in terms of euclidean distance.</p>

<p>The projection operator $P_C$ is defined as:</p>

\[P_C(x) := \arg\min_{z \in C} \frac{1}{2} \|z - x\|_2^2,\]

<p>where \(C = \{\beta :\|\beta\|_1 \leq t \}\).</p>

<p>The actual solution to $P_C(x)$ is not trivial (see [2])</p>
<p align="center">
  <img src="/assets/images/LASSO/l1_projection.png" alt="l1 projection" />
</p>

<p>Somewhat suprisingly, the convergence rate of projected gradient descent is the same as gradient descent: $\mathcal{O}(\frac{1}{k})$</p>

<p>In this case we get ~70 zeroes.</p>

<h3 id="proximal-gradient-descent">Proximal gradient descent</h3>

<p>The second most common approach (used in ISTA, FISTA…) is using a proximal operator:</p>

\[\text{Prox}_h(z) :=  \arg\min_{\theta \in \mathbb{R}^p} \frac{1}{2} \|z - \theta\|_2^2 + h(\theta)\]

<p>If $h(\theta)$ becomes the indicator function for a set $C$ we recover projected gradient descent, so this is indeed a generalization. The interesting bit with this is that the solution for that operator when \(h(\theta) = \|\theta\|_1\) is a piece-wise soft-treshold operation. This is extremely fast.</p>

<p>Arriving at the soft-tresholding function is kind of fun;</p>

\[\begin{align*}

\frac{1}{2} \|z - \theta\|_2^2 + h(\theta) &amp;= \\
\frac{1}{2} \sum_i^N \left [ (z_i - \theta_i)^2 + 2\lambda |\theta_i|\right ] &amp; = \\
\frac{1}{2} \sum_i^N \left [ z_i^2 - 2 z_i \theta_i + \theta_i^2 + 2\lambda |\theta_i|\right ] \propto \\
\frac{1}{2} \sum_i^N \left [ - 2 z_i \theta_i + \theta_i^2 + 2\lambda |\theta_i|\right ]. \\
\end{align*}\]

<p>Finding the minimum:</p>

\[\sum_i^N \left [ - z_i + \theta_i + \lambda \partial |\theta_i|\right ]  = 0, \\\]

<p>using the subgradient definition, three options appear:</p>

<ul>
  <li><strong>Case 1:</strong> If $\theta_i &gt; 0 \implies -z_i + \theta_i + \lambda = 0 \implies \theta_i = z_i - \lambda$ (Valid only if $z_i &gt; \lambda$)</li>
  <li><strong>Case 2:</strong> If $\theta_i &lt; 0 \implies -z_i + \theta_i - \lambda = 0 \implies \theta_i = z_i + \lambda$ (Valid only if $z_i &lt; -\lambda$)</li>
  <li><strong>Case 3:</strong> If $\theta_i = 0 \implies z_i = 0$</li>
</ul>

<p>But, clearly, $z_i &lt; 0 \implies \theta_i &lt; 0$ and $z_i &gt; 0 \implies \theta_i &gt; 0$. Otherwise we won’t be minimizing! Hence, we have an implication that defines the value of $\theta_i$ from the known value of $z_i$. In particular, this boils down to:</p>

\[\text{Prox}_{\lambda \|\cdot\|_1}(z_i) = S_\lambda(z_i) = \text{sign}(z_i) \cdot (|z_i| - \lambda)_+\]

<p>This yields ISTA, with convergence rate $\mathcal{O}(\frac{1}{k})$.</p>

<p>Again, we get ~70 zeroes.</p>

<h3 id="coordinate-descent">Coordinate descent</h3>

<p>This is the most common, implemented in glmnet, used in sklearn and so on. The convex, separable objective function for LASSO can be optimized one parameter at a time:</p>

\[\beta_k^{t+1}
=
\arg\min_{\beta_k}
f(\beta_1^{t}, \beta_2^{t}, \dots, \beta_{k-1}^{t}, \beta_k, \dots, \beta_p^{t}).\]

<p>This has a closed form solution using an argument very close to the ISTA one using the Soft-tresholding. Consider the LASSO problem</p>

\[\min_{\beta}
\frac{1}{N}\sum_{i=1}^{N}
\left(
y_i - \sum_{k=1}^{p} x_{ik}\beta_k
\right)^2
+
\sum_{k=1}^{p} \lambda |\beta_k|,\]

<p>define:</p>

\[r_i^{(j)} :=
y_i - \sum_{k \ne j} x_{ik}\beta_k.\]

<p>Then the problem over each coordinate becomes:</p>

\[\min_{\beta_j}
\frac{1}{N}\sum_{i=1}^{N}
(r_i^{(j)} - x_{ij}\beta_j)^2
+
\lambda |\beta_j|\]

<p>Now, expanding and taking the derivative;</p>

\[\frac{1}{N}\sum_{i=1}^{N}
\left[
r_i^{(j)2}
-2x_{ij}r_i^{(j)}\beta_j
+
x_{ij}^2\beta_j^2
\right]
+
\lambda |\beta_j|,\]

\[-\frac{1}{N}\sum_{i=1}^{N} x_{ij} r_i^{(j)}
+
\frac{1}{N}\sum_{i=1}^{N} x_{ij}^2 \beta_j
+
\lambda \, \partial |\beta_j|
=0\]

<p>Let</p>

\[a = \frac{1}{N}\sum_{i=1}^{N} x_{ij}^2, \quad
b = \frac{1}{N}\sum_{i=1}^{N} x_{ij} r_i^{(j)}\]

<p>Then</p>

\[a \beta_j + \lambda \partial |\beta_j| = b.\]

<p>Case 1: $\beta_j &gt; 0$</p>

\[\beta_j a + \lambda = b \Rightarrow \beta_j = \frac{b - \lambda}{a}\]

<p>If $b &gt; 0 \Rightarrow \beta_j &gt; 0$, otherwise we have a contradiction, since $a$ and $\lambda$ are positive scalars.</p>

<p>Case 2: $\beta_j &lt; 0$</p>

\[\beta_j a - \lambda = b
\Rightarrow \beta_j = \frac{b + \lambda}{a}\]

<p>Case 3: $\beta_j = 0$</p>

<p>which leads to the <strong>soft-thresholding update</strong>:</p>

\[\hat{\beta}_j =
\frac{
S_\lambda
\left(
\frac{1}{N}\sum_{i=1}^{N} r_i^{(j)} x_{ij}
\right)
}{
\frac{1}{N}\sum_{i=1}^{N} x_{ij}^2
}\]

<p>This approach is nice because it finds the optimal solution coordinate wise (conditioned on the other parameters). While the implementation we use is naive and consists on recursively applying the above equation on a nested loop of iterations and parameters, SOTA implementations exploit the sparsity in cool ways to make this faster.</p>

<h3 id="putting-all-together">Putting all together</h3>

<p>We can check the performance of the algorithms evaluating the fit to test data and the distance from the actual parameters, by the optimization step.</p>

<p>Note that projected and proximal gradient descent were implemented with backtracking using Armijo’s rule.</p>

<p>Coordinate descent is, in this case, much faster, reaching the tolerance level of $1 \times 10^{-16}$ before the 500 iterations limit. Although the comparison is not fair because the iterations are not really comparable. Regardless, it took less time than the others.</p>

<p align="center">
  <img src="/assets/images/LASSO/performance.png" alt="l1 projection" />
</p>

<p>Note that projected gradient has the perfect “penalty” parameter $t = |\beta_i|_1$, $\lambda$ has been extracted by trial and error.</p>

<p>IIn any case, the limitations of my NumPy implementations likely dominate many of the observed performance differences. The only robust conclusion is that the subgradient method is clearly suboptimal for the vanilla LASSO problem and offers little practical justification here. In particular, one should be careful not to ignore the non-differentiability of the l1 norm For example, performing backpropagation + “gradient descent” through an absolute value function (PyTorch will allow without complaint) perhaps deserves some thought!</p>

<h2 id="references">References</h2>
<ol>
  <li>
    <p><strong>Hastie, T., Tibshirani, R., &amp; Wainwright, M. (2015).</strong></p>

    <p><em><a href="https://hastie.su.domains/StatLearnSparsity/">Statistical learning with Sparsity</a></em></p>
  </li>
  <li>
    <p><strong>Duchi, John and Shalev-Shwartz, Shai and Singer, Yoram and Chandra, Tushar (2008)</strong></p>

    <p><em><a href="https://ai.stanford.edu/~jduchi/projects/jd_ss_ys_l1.pdf">Efficient Projections onto the ℓ1-Ball for Learning in High Dimensions</a></em></p>
  </li>
  <li>
    <p><strong>Ryan Tibshirani</strong></p>

    <p><em><a href="https://www.stat.cmu.edu/~ryantibs/convexopt-F18/lectures/sg-method.pdf">Subgradient Method lecture notes</a></em></p>

    <p><em><a href="https://www.stat.cmu.edu/~ryantibs/convexopt-F13/scribes/lec6.pdf">Gradient descent lecture notes</a></em></p>
  </li>
</ol>]]></content><author><name>Jordi Alonso</name></author><category term="Optimization" /><category term="ML" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Inequality constrained optimization (SVM “the technical problem”).</title><link href="https://jordialonsoesteve.github.io/2026/01/06/Understanding-SVMs_3.html" rel="alternate" type="text/html" title="Inequality constrained optimization (SVM “the technical problem”)." /><published>2026-01-06T00:00:00+00:00</published><updated>2026-01-06T00:00:00+00:00</updated><id>https://jordialonsoesteve.github.io/2026/01/06/Understanding-SVMs_3</id><content type="html" xml:base="https://jordialonsoesteve.github.io/2026/01/06/Understanding-SVMs_3.html"><![CDATA[<link href="/css/syntax.css" rel="stylesheet" />

<script src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML" type="text/javascript"></script>

<p>All the code for this post can be found <a href="https://github.com/neggor/neggor.github.io/tree/main/_posts/UnderstandingSVMs/SVM_cvxopt">here</a>!</p>

<h1 id="from-optimal-separating-hyperplane-to-support-vector-machines">From Optimal Separating Hyperplane to Support Vector Machines</h1>
<p>Previously we already explained the basics of <a href="/2024/01/16/Understanding-SVMs_1.html">Optimal Separating Hyperplanes</a>. There we considered the problem of optimally separating hyperplanes also with expansions of the input space, which allowed perfect separability of the classes. That extension was already in Support Vector Machines (SVMs) territory, but we did not consider the soft margin. Furthermore, there we used an off-the-shelf library for the QP problem. Here we slightly generalize the framework to that of the SVMs. The focus here, however, is on the optimization part of the problem, on the “technical” side. We will dive with quite a lot of detail into inequality constraint optimization theory for convex problems, with the SVM problem bieng just an illustration (a rather simple one) of the applicability of those algorithms.</p>

<p>In essence, SVM is a optimal separating hyperplane with two additions; first, the mapping of the input space to a high-dimensional feature space (e.g.,polynomial expansion of degree \(d\)). Second, soft margin classification, which allows non-perfect separability of the classes.</p>

<p>After solving for the primal and plugging in the solution, we have the following dual problem for the optimal separating hyperplane:</p>

\[\begin{align*}
\text{maximize}_{\alpha} \quad &amp; 
\sum_{i=1}^n \alpha_i - \frac{1}{2} \sum_{i=1}^n \sum_{j=1}^n \alpha_i \alpha_j y_i y_j \mathbf{x}_i^\top \mathbf{x}_j \\
\text{subject to} \quad &amp; 
\sum_{i=1}^n \alpha_i y_i = 0, \\
&amp; \alpha_i \geq 0, \quad i = 1, \dots, n.
\end{align*}\]

<p>Generalizing this to the soft margin hyperplane is very simple, introducing a complexity parameter \(C\):</p>

\[\begin{align*}
\text{maximize}_{\alpha} \quad &amp; 
\sum_{i=1}^n \alpha_i - \frac{1}{2} \sum_{i=1}^n \sum_{j=1}^n \alpha_i \alpha_j y_i y_j \mathbf{x}_i^\top \mathbf{x}_j \\
\text{subject to} \quad &amp; 
\sum_{i=1}^n \alpha_i y_i = 0, \\
&amp; 0 \le \alpha_i \le C, \quad i = 1, \dots, n.
\end{align*}\]

<p>Intuitively, \(C\) is an upper bound on the Lagrange multiplier associated with an observation. It limits the importance of the constraint of proper classification of the points. The higher \(C\) the more importance it has to missclasify, with a very big \(C\) we recover the hard-margin classifier because the “natural” Lagrange multipliers will be below \(C\).</p>

<p>The combination of basis expansion with maximum margin separation is extremely interesting from the statistical learning perspective. The conceptual motivation for SVMs is a very rich topic. Rather than selecting a hypothesis by directly controlling model complexity through the number of parameters or network architecture, SVMs introduce an inductive bias based on margin maximization. The inductive step is taken by restricting attention to classifiers that achieve zero empirical risk on the training data (in the separable case), and among these, choosing the one that minimizes an upper bound on the probability of misclassification on unseen data. This shift, from fitting capacity to maximum margin, marks a fundamental departure from more traditional forms of inductive bias.</p>

<p>We will dive into the “conceptual” problem of SVMs in a future post.</p>

<p>For now, let’s dive into inequality constrained convex optimization. Note that an explanation of Newton’s method for equality constrained optimization is available <a href="/2025/12/25/SteepestDescent.html">here</a>. We build from there.</p>

<h1 id="inequality-constrained-optimization">Inequality constrained optimization</h1>
<p>Often when looking into how SVM works, at some point a QP solver is invoked. We will code that explicitly. Here we illustrate two interior point methods and an ad-hoc algorithm for SVMs. In the first two points we loosely follow chapter 11 from [1].</p>

<h2 id="log-barrier-method">log-barrier method</h2>
<p>It is not immediately clear how one go about solving an inequality constrained minimization problem. Equality constrained minimization seems easier, we just have to make updates orthogonal to the constraint matrix. Perhaps the first thing that one can try is to add a penalty that goes to \(\infty\) assuming we are minimizing (wlog).</p>

<p>The Log Barrier method is a rigorous approach in that direction.</p>

<p>Define the barrier functional:</p>

\[\phi(x) = - \sum_{i=1}^m \log{(-f_i(x))}.\]

<p>This function clearly goes to \(\infty\) when \(x \rightarrow 0\), and it is only defined for \(f_i(x) &lt; 0\). Hence, this is a perfect penalty to add to our objective function, transforming the inequality constrained optimization problem into an equality constrained optimization one. However, to not change the objective we want to optimize, we need to make the penalty very small when the parameters are feasible. To do that a constant “small” \(t &gt; 0\) is added.</p>

<p>The gradient and Hessian of \(\phi(x)\) are very important:</p>

\[\begin{align*}
\nabla \phi(x) &amp;= -\sum_{i=1}^m \frac{1}{f_i(x)} \, \nabla f_i(x) \\

\nabla^2 \phi(x) &amp;= \sum_{i=1}^m \frac{1}{f_i(x)^2} \, \nabla f_i(x) \, \nabla f_i(x)^\top
+ \sum_{i=1}^m \frac{1}{f_i(x)} \, \nabla^2 f_i(x)
\end{align*}\]

<p>For the SVM case we have two inequality constraints \(f^i_1(\alpha) = -\alpha_i \leq 0\) (\(\alpha\) must be positive) and \(f^i_2(\alpha) = \alpha_i - C \leq 0\) (\(\alpha\) must be below \(C\)). In this case the barrier functional is (note that we are maximizing, so the first sign is changed to look for \(-\infty\)):</p>

\[\phi(\alpha) = \sum_{i=1}^m \big( \log{(-f^i_1(x))} + \log{( - f^i_1(x))} \big) =  \sum_{i=1}^m \big( \log{\alpha_i} + \log{(C - \alpha_i)} \big).\]

<p>Hence, the new <em>centralizer</em> problem is:</p>

\[\begin{align*}
\text{maximize}_{\alpha} \quad &amp; 
\sum_{i=1}^n \alpha_i 
- \frac{1}{2} \sum_{i=1}^n \sum_{j=1}^n \alpha_i \alpha_j y_i y_j \mathbf{x}_i^\top \mathbf{x}_j
+ t \sum_{i=1}^n \Big( \log \alpha_i + \log (C - \alpha_i) \Big) \\
\text{subject to} \quad &amp; 
\sum_{i=1}^n \alpha_i y_i = 0,
\end{align*}\]

<p>Clearly, for each value of \(t\)  we have a different solution for this problem; we call them \(\alpha^*(t)\). Now, one could choose a very small \(t\)  and just fit Newton’s method for equality constraints. However, this is unlikely to work well, because the Hessian is likely to vary very rapidly when the parameters of the inequality constraints are close to 0 (as per the Hessian definition above). In that case, the Lipschitz constant of the Hessian is huge, and we may need many iterations (to find a region where the gradient is already tiny) to reach the quadratically convergent phase of Newton’s method. Hence, the idea is to decrease \(t\)  “slowly” and initialize the Newton’s method in the previous solution, effectively having an outer loop decreasing \(t\)  and an inner loop solving the respective equality constrained problems starting at the solution of the previous iteration.</p>

<p>In any case, it is not completely obvious that this should yield the actual optimal for the original inequality constrained problem. Nevertheless, this is actually the case. In particular, for the general minimization case (assuming $f_0$ convex and differentiable, affine equality constraints ($h_i$) and convex inequality constraints ($f_i$)):</p>

\[\lim_{t \rightarrow 0} f_0(x^*(t)) = f_0(x^*).\]

<p>When \(t\)  is very small, then the objective value associated to the optimal parameters of the centralizer problem does indeed converge to the objective value of the optimal parameters in the original problem. To see this, note the following parallelism between the stationarity condition of the Lagrangian of the centralizer problem and the stationary condition of the original problem. For the centralizer problem:</p>

\[\nabla f_0(x^*(t)) + t \nabla \phi (x^*(t)) + A^{\top}\nu^*(t) = 0,\]

<p>for the original problem:</p>

\[\nabla f_0(x^*) + \sum_{i=1}^m \lambda^*_i \nabla f_i(x^*) + A^{\top}\nu^* = 0.\]

<p>Since \(\nabla \phi(x) = -\sum_{i=1}^m \frac{1}{f_i(x)} \, \nabla f_i(x)\) then we have that we can define</p>

\[\begin{equation}
\lambda^*(t) = -\frac{t}{f_i(x^*(t))},
\label{eq:subst}
\end{equation}\]

<p>then the dual becomes (assuming \(x^*(t)\) is feasible)</p>

\[f_0(x^*) \geq g(\lambda^*(t), \nu^*(t)) 
= f_0(x^*(t)) + \sum_{i=1}^m \lambda_i^*(t) f_i(x^*(t)) + \nu^{*}(t)^\top (Ax^*(t) - b)
= f_0(x^*(t)) - mt.\]

<p>Hence, \(x^*(t)\) is only \(mt\) suboptimal so indeed the intuition is correct and we will recover the optimal solution. Note that, without strict convexity, \(x^*\) might not be unique!</p>

<p>Finally, note that we do require always work with a feasible $x$. This is again not trivial; in fact, it requires an optimization step in itself (solving \(Ax = b\) for \(x \leq 0\) is as hard as an LP!). This boils down to solving:</p>

\[\begin{aligned}
\text{minimize}_{x, s} \quad &amp; s \\
\text{subject to} \quad &amp; Ax = b, \\
&amp; f_i(x) \le s, \quad i = 1,\dots,n.
\end{aligned}\]

<p>Which is AGAIN an inequality constrained optimization problem. However, it suffices to solve this once with a “big” \(t\).</p>

<h3 id="the-algorithm-pieces-for-the-general-case">The algorithm pieces for the general case</h3>
<p>We derive now the Newton’s equations for the general case.</p>

<h4 id="phase-i-finding-a-feasible-initial-point">Phase I (finding a feasible initial point)</h4>
<p>Construct the Lagrangian for a big \(t\):</p>

\[L(x, s, \nu) = s - t\sum_{i = 1}^m \log(s - f_i(x)) + \nu^{\top}(Ax -b).\]

<p>Gradients and Hessians:</p>

\[\begin{align*}
\nabla_x L(x, s, \nu) &amp;= \sum_{i=1}^m \frac{t}{s - f_i(x)} \nabla f_i(x) + A^\top \nu, \\
\nabla_s L(x, s, \nu) &amp;= 1 - \sum_{i=1}^m \frac{t}{s - f_i(x)}, \\
\nabla_\nu L(x, s, \nu) &amp;= Ax - b,
\end{align*}\]

\[\begin{align*}
\nabla_x^2 L(x, s, \nu) &amp;= \sum_{i=1}^m \left( \frac{t}{s - f_i(x)}\nabla^2 f_i(x) + \frac{t}{(s - f_i(x))^2} \nabla f_i(x) \nabla f_i(x)^\top \right), \\
\nabla_s^2 L(x, s, \nu) &amp;= \sum_{i=1}^m \frac{t}{(s - f_i(x))^2}, \\
\nabla_{xs}^2 L(x, s, \nu) &amp;= - \sum_{i=1}^m \frac{t}{(s - f_i(x))^2} 
\nabla f_i(x),\\
\nabla_{\nu x}^2 L(x, s, \nu) &amp;= A, \\
\nabla_{\nu s}^2 L(x, s, \nu) &amp;= 0, \\
\nabla_{\nu \nu}^2 L(x, s, \nu) &amp;= 0.
\end{align*}\]

<p>Now, putting it all together:</p>

\[\begin{bmatrix}
\nabla_x^2 L &amp; \nabla_{xs}^2 L &amp; A^\top \\
(\nabla_{xs}^2 L)^\top &amp; \nabla_s^2 L &amp; 0 \\
A &amp; 0 &amp; 0
\end{bmatrix}
\begin{bmatrix}
\Delta x \\
\Delta s \\
\Delta \nu
\end{bmatrix}
= -
\begin{bmatrix}
\nabla_x L \\
\nabla_s L \\
\nabla_\nu L
\end{bmatrix}\]

<h4 id="centering-step">Centering step</h4>

\[L(x, \nu) = f_0(x) - t\sum_{i = 1}^m \log(-f_i(x)) + \nu^{\top}(Ax -b).\]

\[\begin{bmatrix}
\nabla^2 f_0(x) + t \nabla^2 \phi(x) &amp; A^\top \\
A &amp; 0
\end{bmatrix}
\begin{bmatrix}
\Delta x \\
\Delta \nu
\end{bmatrix}
= -
\begin{bmatrix}
\nabla f_0(x) + t \nabla \phi(x) + A^\top \nu \\
Ax - b
\end{bmatrix}\]

<p>However, since \(x\) is feasible, we can focus on the simpler system (this simplification is not applied below, with no effect besides making the whole description and the code a bit more cumbersome):</p>

\[\begin{bmatrix}
\nabla^2 f_0(x) + t \nabla^2 \phi(x) &amp; A^\top \\
A &amp; 0
\end{bmatrix}
\begin{bmatrix}
\Delta x \\
\nu
\end{bmatrix}
= -
\begin{bmatrix}
\nabla f_0(x) + t \nabla \phi(x) \\
0
\end{bmatrix}\]

<h3 id="log-barrier-method-svm-algorithm">log-barrier Method SVM algorithm</h3>

<p>Now we’ve got everything we need. Bear in mind that we are optimizing the dual of the SVM, so we have a maximization problem! Furthermore, note that there are only 2 inequality constraints and 1 equality constraint acting in \(\alpha\) as a vector.</p>

<p>First, let’s get the derivatives and Hessians for the Phase I and central path problems.</p>

<p>The Lagrangian for Phase I is:</p>

\[L(\alpha, s, \nu) = s - t \sum_{i=1}^n \left( \log(s + \alpha_i) + \log(s + C - \alpha_i) \right) + \nu (\mathbf{y}^\top \alpha).\]

\[\begin{align*}
\nabla_\alpha L &amp;= -t \left( \frac{1}{s + \alpha} - \frac{1}{s + C - \alpha} \right) + \nu \mathbf{y} \quad \text{Using elementwise division, } \nu \in \mathbb{R} \text{ (scalar)},\\
\nabla_s L &amp;= 1 - t \sum_{i=1}^n \left( \frac{1}{s + \alpha_i} + \frac{1}{s + C - \alpha_i} \right), \\
\nabla_\nu L &amp;= \mathbf{y}^\top \alpha.
\end{align*}\]

<p>Let \(d_1 = \frac{1}{(s+\alpha)^2}\) and \(d_2 = \frac{1}{(C + s -\alpha)^2}\) elementwise!</p>

\[\begin{align*}
\nabla_{\alpha \alpha}^2 L &amp;= t \, \text{diag}(d_1 + d_2), \\
\nabla_{ss}^2 L &amp;= t \sum_{i=1}^n (d_1 + d_2), \\
\nabla_{\alpha s}^2 L &amp;= t (d_1 - d_2).
\end{align*}\]

<p>Finally:</p>

\[\begin{bmatrix}
t \, \text{diag}(d_1 + d_2) &amp; t(d_1 - d_2) &amp; \mathbf{y} \\
t(d_1 - d_2)^\top &amp; t \sum (d_1 + d_2) &amp; 0 \\
\mathbf{y}^\top &amp; 0 &amp; 0
\end{bmatrix}
\begin{bmatrix}
\Delta \alpha \\
\Delta s \\
\Delta \nu
\end{bmatrix}
= -
\begin{bmatrix}
\nabla_\alpha L \\
\nabla_s L \\
\mathbf{y}^\top \alpha
\end{bmatrix}.\]

<p>The Lagrangian for the Central Path problems is:</p>

\[L(\alpha, \nu) = \mathbf{1}^\top \alpha - \frac{1}{2} \alpha^\top Q \alpha + t \sum_{i=1}^n \Big( \log \alpha_i + \log (C - \alpha_i) \Big) + \nu (\mathbf{y}^\top \alpha),\]

<p>where \(Q_{ij} = y_i y_j \mathbf{x}_i^\top \mathbf{x}_j\). Note the change of sign, the penalty goes negative because we are maximizing.</p>

<p>Again, gradients, Hessians, and Newton’s matrix (abusing a bit the elementwise operations…):</p>

\[\begin{align*}
\nabla_{\alpha} L &amp;= \mathbf{1} - Q\alpha + t \left( \frac{1}{\alpha} - \frac{1}{C - \alpha} \right) + \nu \mathbf{y}, \\
\nabla_\nu L &amp;=  \mathbf{y}^\top \alpha,
\end{align*}\]

<p>Let \(d_1 = \frac{1}{\alpha^2}\) and \(d_2 = \frac{1}{(C -\alpha)^2}\) elementwise!</p>

\[\begin{align*}
\nabla_{\alpha \alpha}^2 L &amp;= -Q - t  \, \text{diag}(d_1 + d_2,) \\
\nabla_{\nu \alpha}^2 L &amp;= \mathbf{y}^\top ,\\
\nabla_{\nu \nu}^2 L &amp;= 0. \\
\end{align*}\]

<p>And finally</p>

\[\begin{bmatrix}
-Q - t \, \text{diag}(d_1 + d_2) &amp; \mathbf{y} \\
\mathbf{y}^\top &amp; 0
\end{bmatrix}
\begin{bmatrix}
\Delta \alpha \\
\Delta \nu
\end{bmatrix}
= -
\begin{bmatrix}
\nabla_\alpha L \\
\nabla_\nu L
\end{bmatrix}\]

<p>Now all the math is ready, the algorithm then is:</p>
<div style="text-align: center;">
<img src="/assets/images/SVMs/LogBarrierSVM.png" alt="LogBarrier image Error" width="600" />
</div>

<div style="text-align: center;">
<img src="/assets/images/SVMs/LB.gif" alt="LogBarrier image Error" width="600" />
</div>

<p>In the above demo we used RBF kernel for coolness purposes, but that has actually nothing to do with the optimization problem since it is literally just a different \(Q\). In the code implementation and the above algorithm we are updating \(\nu\). This is not necessary and is actually not part of the canonical log-barrier method which is strictly a primal algorithm. Here we are doing a primal-dual algorithm (because of \(\nu\)), even though this is not necessary since \(x\) is guaranteed to be feasible.</p>

<h2 id="primal-dual-algorithm">Primal-Dual algorithm</h2>
<p>The log-barrier method can be interpreted as solving a modified KKT system in each inner loop. A “continuous deformation” that indeed converges to the final canonical KKT system. In particular, again for the standard minimization problem:</p>

\[\begin{aligned}
Ax &amp;= b, \\
f_i(x) &amp;\le 0, \quad i = 1,\dots,m, \\
\lambda &amp;\ge 0, \\[6pt]

\nabla f_0(x)
+ \sum_{i=1}^m \lambda_i \nabla f_i(x)
+ A^{T}\nu
&amp;= 0, \\[6pt]

-\lambda_i f_i(x) &amp;= t,
\quad i = 1,\dots,m.
\end{aligned}\]

<p>The only difference is in the complementary slackness condition. In the log-barrier method \(\lambda\) is eliminated via substitution (equation \ref{eq:subst}). The idea now is to directly focus on solving the modified KKT directly, explicitly optimizing \(\lambda\). This greatly simplifies things. First, now we are allowed to work with not feasible values for primal and dual variables, hence; we skip Phase I altogether (which should make this already trivially twice as fast…). Convergence will ensure feasibility, but it is not a requirement at the start! (Which is why below the evolution of the objective function is not monotonically increasing.) Second, there is only one loop, \(t\) is updated at the same time as \(\lambda, x, \nu\).</p>

<p>Let’s check the theory again for the standard case and then we fill it in with the SVM problem specifics. The modified KKT equation to solve is:</p>

\[\begin{bmatrix}
\nabla^2 f_0(x) + \sum_{i=1}^m \lambda_i \nabla^2 f_i(x) &amp; Df(x)^T &amp; A^T \\
-\mathbf{diag}(\lambda)Df(x) &amp; -\mathbf{diag}(f(x)) &amp; 0 \\
A &amp; 0 &amp; 0
\end{bmatrix}
\begin{bmatrix}
\Delta x \\
\Delta \lambda \\
\Delta \nu
\end{bmatrix}
= -
\begin{bmatrix}
\nabla f_0(x) + Df(x)^{T}\lambda + A^{T}\nu \\
- \operatorname{diag}(\lambda)\, f(x) - t\mathbf{1} \\
Ax - b
\end{bmatrix}\]

<p>Define \(\hat{\eta}(x, \lambda) = - \sum_{i=1}^m f_i(x)\lambda_i\).</p>

<p>If</p>

\[\bigg\|\begin{bmatrix}
\nabla f_0(x) + Df(x)^{T}\lambda + A^{T}\nu \\
- \operatorname{diag}(\lambda)\, f(x) - t\mathbf{1} \\
Ax - b
\end{bmatrix}\bigg\|_2 = 0,\]

<p>then \(\hat{\eta}(x, \lambda) = mt\). If \(\lambda, x, \nu\) satisfies the modified KKT, then \(mt\) is the duality gap. Note that then \(t = \frac{\hat{\eta}(x, \lambda)}{m}\). And here we have the idea for the algorithm, we are going to decrease the duality gap by decreasing \(t\) (as before, using \(\mu \in (0, 1)\)) and at the same time forcing feasibility by the minimization of the residual. <sup><a href="#note-1">1</a></sup></p>

<p>Now, turning back to SVMs.</p>

<p>Residuals:</p>

\[\begin{align*}
r_{\text{dual}} &amp;= \mathbf{1} - Q \alpha - \lambda^{\top} 
\begin{bmatrix}
-\mathbf{I} \\
\mathbf{I}
\end{bmatrix}
+ \nu y \\

r_{\text{cent}} &amp;= -\text{diag}(\lambda)\begin{bmatrix}
-\alpha \\
\alpha - C 
\end{bmatrix} -t\mathbf{1} \quad \lambda \text{ is positive and the function values are negative (when feasible)}\\

r_{\text{pri}} &amp;= \mathbf{y}^\top \alpha.
\end{align*}\]

<p>So the KKT matrix:</p>

\[\begin{bmatrix}
- Q &amp; \begin{bmatrix}-I \\ I \end{bmatrix}^\top &amp; y \\
-\text{diag}(\lambda) \begin{bmatrix}-I \\ I \end{bmatrix} &amp; \text{diag}\begin{bmatrix}
-\alpha \\
\alpha - C 
\end{bmatrix} &amp; 0 \\
y^\top &amp; 0 &amp; 0
\end{bmatrix}
\begin{bmatrix}
\Delta \alpha \\
\Delta \lambda \\
\Delta \nu
\end{bmatrix}
=
-\begin{bmatrix} \mathbf{1} - Q \alpha - \lambda^{\top} 
\begin{bmatrix}
-\mathbf{I} \\
\mathbf{I}
\end{bmatrix}
+ \nu y \\ -\text{diag}(\lambda)\begin{bmatrix}
-\alpha \\
\alpha - C 
\end{bmatrix} -t\mathbf{1}\\ \mathbf{y}^\top \alpha \end{bmatrix}.\]

<p>Note that, while in the log-barrier method we effectively inserted the inequality constraint in the Lagrangian and then continued (sort of) like in equaility constrained optimization, here we are NOT working on finding stationary points of any Lagrangian! Here everything follows from the residual vector. In other words, the modified KKT conditions here do not arise from the Lagrangian.</p>

<p>The line search must ensure that the \(\lambda\) stays positive and the constraint functions negative. It also ensures that the norm of the residuals decreases sufficiently. From [1] §11.7:</p>
<blockquote>
  <p>One iteration of the primal-dual interior-point algorithm is the same as one step of the infeasible Newton method, applied to solving \(\, r_t(x, \lambda, \nu) = 0\), but modified to ensure \(\, \lambda &gt; 0\) and \(\, f(x) &lt; 0\).</p>
</blockquote>

<p>Finally, the algorithm:</p>

<div style="text-align: center;">
<img src="/assets/images/SVMs/Primal_dual_svm.png" alt="LogBarrier image Error" width="600" />
</div>
<div style="text-align: center;">
<img src="/assets/images/SVMs/PD.gif" alt="LogBarrier image Error" width="600" />
</div>

<p>Coding-wise, this felt much cleaner than the log-barrier method…</p>

<h2 id="sequential-minimal-optimization-smo">Sequential minimal optimization (SMO)</h2>

<h3 id="derivation-of-the-update-equations">Derivation of the update equations</h3>

<p>In 1998 Jonh C. Platt proposed an ad hoc algorithm for SVMs. The idea is basically a coordinate descent approach but with a catch. Again we are going to be dealing with the dual function. Optimizing one multiplier at a time is no possible because the linear equality constraint provides an identity that we have to respect. Imagine we are looking to optimize observation j:</p>

\[- \sum_{i \neq j} y_i\alpha_i = \alpha_j y_j,\]

<p>so we do not have any univariable direction of improvement that respects feasibility. So what now? Turns out that allowing for 2 degrees of freedom at each step actually allows increasing the dual function. The drawback is that then we have $N^2$ possible pairs to optimize, so a naive loop is absolutely terrible. Furthermore, in a real world scenario where SVMs generalize decently it is likely that $\alpha$ will be quite a sparse vector, so there may be updates that do not even increase the dual. In any case, heuristics are needed to solve this.</p>

<p>Let’s first derive the update rules, following [3] [4]. The aim is to exploit a closed form solution for</p>

\[\begin{align*}
\text{maximize}_{\alpha} \quad &amp; 
\sum_{i=1}^n \alpha_i - \frac{1}{2} \sum_{i=1}^n \sum_{j=1}^n \alpha_i \alpha_j y_i y_j \mathbf{x}_i^\top \mathbf{x}_j \\
\text{subject to} \quad &amp; 
\sum_{i=1}^n \alpha_i y_i = 0, \\
&amp; 0 \le \alpha_i \le C, \quad i = 1, \dots, n.
\end{align*}\]

<p>if we let all $\alpha$ fixed but two, let’s call those two \(\alpha_i\) and \(\alpha_j\). Then we have that the pair-dual is:</p>

\[D(\alpha_i, \alpha_j) = \alpha_i + \alpha_j +\sum_{k \neq i,j}^n \alpha_k
- \frac{1}{2} \left[ \alpha_i^2 K_{ii} + \alpha_j^2 K_{jj} + 2 y_i y_j \alpha_i \alpha_j K_{ij} \right] 
- y_i \alpha_i \sum_{k \neq i,j}^n y_k \alpha_k K_{ik} 
- y_j \alpha_j \sum_{k \neq i,j}^n y_k \alpha_k K_{jk} 
- \frac{1}{2} \sum_{k \neq i,j}^n \sum_{m \neq i,j}^n y_k y_m \alpha_k \alpha_m K_{km}.\]

<p>Where \(K_{lm}\) is the inner product of \(\phi(x_l)\) and \(\phi(x_m)\) (\(\phi(x)\) being a function that puts \(x\) in another space, e.g., \(n^{th}\) degree polynomial expansion). This is, a particular entry of the Kernel matrix.</p>

<p>Now, we have that, to respect the equality constraint:</p>

\[y_i \alpha_i + y_j \alpha_j = - \sum_{k \neq i, j} y_k\alpha_k.\]

<p>Multiplying both sides by \(y_i\) (recall that \(y\) is either \(1\) or \(-1\)):</p>

\[\alpha_i + y_iy_j \alpha_j = - y_i\sum_{k \neq i, j} y_k\alpha_k = \zeta,\]

<p>hence,</p>

\[\alpha_i = \zeta - y_iy_j \alpha_j .\]

<p>We can plug this in and we get:</p>

\[\begin{aligned}
D(\alpha_j)
&amp;= \zeta + (1 - y_i y_j)\alpha_j
- \frac{1}{2}\Big[
(\zeta - y_i y_j \alpha_j)^2 K_{ii}
+ \alpha_j^2 K_{jj}
+ 2 y_i y_j (\zeta - y_i y_j \alpha_j)\alpha_j K_{ij}
\Big] \\
&amp;\quad
- y_i (\zeta - y_i y_j \alpha_j)\sum_{k\neq i,j} y_k \alpha_k K_{ik}
- y_j \alpha_j \sum_{k\neq i,j} y_k \alpha_k K_{jk}
+ \text{const}.
\end{aligned}\]

<p>Rearranging the term within brackets we get:</p>

\[\begin{aligned}
(\zeta - y_i y_j \alpha_j)^2 K_{ii}
&amp;+ \alpha_j^2 K_{jj}
+ 2 y_i y_j (\zeta - y_i y_j \alpha_j)\alpha_j K_{ij} \\
&amp;=
\zeta^2 K_{ii}
+
2 \zeta y_i y_j \alpha_j (K_{ij}-K_{ii})
+
\alpha_j^2 (K_{ii}+K_{jj}-2K_{ij}).
\end{aligned}\]

<p>Rearranging the rest:</p>

\[\begin{aligned}
&amp; \zeta + (1 - y_i y_j)\alpha_j - y_i (\zeta - y_i y_j \alpha_j)\sum_{k\neq i,j} y_k \alpha_k K_{ik}
- y_j \alpha_j \sum_{k\neq i,j} y_k \alpha_k K_{jk}
\\
&amp;= \zeta + (1 - y_i y_j)\alpha_j - \zeta y_i \sum_{k\neq i,j} y_k \alpha_k K_{ik} 
+ \alpha_j y_j \sum_{k\neq i,j} y_k \alpha_k K_{ik}
- \alpha_j y_j \sum_{k\neq i,j} y_k \alpha_k K_{jk} \\
&amp;= \zeta + (1 - y_i y_j)\alpha_j - \zeta y_i \sum_{k\neq i,j} y_k \alpha_k K_{ik} 
+ \alpha_j y_j \sum_{k\neq i,j} y_k \alpha_k (K_{ik}-K_{jk}).
\end{aligned}\]

<p>Putting it together:</p>

\[\begin{aligned}

D(\alpha_j)
&amp;=  - \frac{1}{2}\Big[\zeta^2 K_{ii}
+
2 \zeta y_i y_j \alpha_j (K_{ij}-K_{ii})
+
\alpha_j^2 (K_{ii}+K_{jj}-2K_{ij}) \Big] \\
&amp;\quad
- \zeta + (1 - y_i y_j)\alpha_j - \zeta y_i \sum_{k\neq i,j} y_k \alpha_k K_{ik} 
+ \alpha_j y_j \sum_{k\neq i,j} y_k \alpha_k (K_{ik}-K_{jk})
+ \text{const} \\
&amp;=
-\frac{1}{2}(K_{ii}+K_{jj}-2K_{ij})\alpha_j^2 
+\alpha_j\Big[
(1-y_i y_j)
-\zeta y_i y_j (K_{ij}-K_{ii})
+ y_j \sum_{k\neq i,j} y_k \alpha_k (K_{ik}-K_{jk})
\Big] 
+\text{const}.

\end{aligned}\]

<p>Now we have a nice for for our scalar quadratic equation. However, we need to do a bit more work with the linear term. We have that:</p>

\[y_j \sum_{k\neq i,j} y_k \alpha_k (K_{ik}-K_{jk}) = y_j \Bigg(\Big[u_i - b - \alpha_i y_i K_{ii} - \alpha_j y_j K_{ji} \Big] - \Big[u_j - b - \alpha_j y_j K_{jj} - \alpha_i y_i K_{ij} \Big]\Bigg),\]

<p>where</p>

\[u_i = \sum_{k=1}^N \alpha_k y_k K_{ki} + b,\]

<p>this is, the prediction for the value \(x_i\) in the hyperplane (note \(w = \sum_{k=1}^N \alpha_k y_k \phi(x_k)\)). Now, let \(E_k = u_k - y_k\) be the error of the prediction. Substituting \(\zeta\) back:</p>

\[\begin{aligned}

&amp; (1-y_i y_j)
-\zeta y_i y_j (K_{ij}-K_{ii})
+ y_j \Bigg(\Big[u_i - b - \alpha_i y_i K_{ii} - \alpha_j y_j K_{ji} \Big] - \Big[u_j - b - \alpha_j y_j K_{jj} - \alpha_i y_i K_{ij} \Big]\Bigg) \\
&amp;= (1-y_i y_j) + ( \alpha_iy_i y_j + \alpha_j) (K_{ii} - K_{ij}) + \Bigg(  y_j(u_i - u_j) + \alpha_j K_{jj} - \alpha_i y_i y_j K_{ii} + \alpha_i y_j y_i K_{ji} - \alpha_j K_{ji}\Bigg)  \\
&amp; = (1-y_i y_j) + ( \alpha_i y_i y_j)(K_{ii} - K_{ij} - K_{ii} +  K_{ji}) + (\alpha_j)(K_{ii} - K_{ij} + K_{jj} - K_{ji})  + y_j(u_i - u_j) \\
&amp; =  (1-y_i y_j) + \alpha_j(K_{ii} - 2K_{ij} + K_{jj})  + y_j(u_i - u_j).\\
\end{aligned}\]

<p>And finally, since \(E_i - E_j =  u_i - y_i - u_j + y_j\):</p>

\[\begin{aligned}

 &amp;\alpha_j(K_{ii} - 2K_{ij} + K_{jj}) + y_j(E_i - E_j) \\
 &amp;= \alpha_j(K_{ii} - 2K_{ij} + K_{jj}) + y_ju_i - y_j y_i - y_j u_j + 1 \\
 &amp;= \alpha_j(K_{ii} - 2K_{ij} + K_{jj}) + (1-y_i y_j)  + y_j(u_i - u_j).
 \end{aligned}\]

<p>Hence we are ready to get the analytical solution:</p>

\[\begin{aligned}

D(\alpha_j) =
-\frac{1}{2}(K_{ii}+K_{jj}-2K_{ij})\alpha_j^2 
+\alpha_j\Big[
\alpha_j^{old}(K_{ii} - 2K_{ij} + K_{jj}) + y_j(E_i - E_j)
\Big] 
+\text{const}.

\end{aligned}\]

<p>Importantly, note that \(\alpha_j^{old}\) inside the brackets is a given value. Taking the derivative and equating to 0 we arrive at the solution:</p>

\[\begin{equation}
\alpha_j^{new} = \alpha_j^{old} + \frac{y_j(E_i - E_j)}{(K_{ii}+K_{jj}-2K_{ij})}, \quad \alpha_i^{new}= \zeta - y_jy_i \alpha_j^{new} = \alpha_i^{old} + y_i y_j(\alpha_j^{old} - \alpha_j^{new}).
\label{eq:updateSMO}
\end{equation}\]

<p>The second equation follows from the fact that \(y_jy_i\alpha_j^{old} + \alpha_i^{old} = \zeta\) since this is respected throughout.</p>

<p>Great! We’ve got a solution. Nevertheless, we still need to enforce the “box” constraints. There are two cases, either \(y_iy_j\) is \(1\) or \(-1\). If it is \(1\) then</p>

\[\alpha_j + \alpha_i = \zeta.\]

<p>Since \(\alpha\) cannot be higher than \(C\) or below 0. We have that if \(\zeta &lt; C\) the minimum for \(\alpha_j\) is 0 and the maximum \(\zeta\). On the other case, if \(\zeta \geq C\) then the minimum is \(\zeta - C\) (which implies \(\alpha_i = C\) which is the upper limit) and the maximum is \(C\). Hence the lower bound for \(y_iy_j = 1\) is \(\text{max}(0, \zeta - C)\) and the upper bound \(\text{min}(\zeta, C)\).  Reasoning in the same way for \(y_iy_j = -1\) we have that
the lower bound is \(\text{max}(0, -\zeta)\) and the upper bound \(\text{min}(C - \zeta, C)\). Those bounds have to be respected acting like a clip on the update, they ensure that the \(\alpha\) remains feasible (e.g., think of projected gradient descent).</p>

<p>Finally, the updates in:</p>

\[\begin{aligned}

w^{new} &amp;= \sum_{n \neq i, j} y_n \alpha^{old}_n x_n + y_j \alpha_j^{new}x_j + y_i \alpha_i^{new}x_i\\
&amp;= w^{old} + y_j \alpha_j^{new}x_j - y_j \alpha_j^{old}x_j + y_i \alpha_i^{new}x_i - y_i \alpha_i^{old}x_i\\
&amp;= w^{old} + y_j \triangle \alpha_jx_j + y_i \triangle \alpha_ix_i\\
\end{aligned}\]

<p>And for \(b\) we have two cases, if \(\alpha_i\) or \(\alpha_j\) are not at the bounds (note that in that case we have a support vector for at least one of the two), and hence, the error is 0. In that case we look for the \(b\) that forces \(u_k = y_k\). Suppose that \(\alpha_i\) is not saturated, then:</p>

\[b_i = y_i - w^{new}\phi(x_k) = \sum_{n=1}^N y_n \alpha^{new}_n K_{ni},\]

<p>the same applies for \(b_j\). If neither is saturated, either works. If both are saturated (non-support vectors or just wrongly classified), we take the average. The above formulation is intuitive but slow; in practice, it is better to use:</p>

\[\begin{aligned}
b_1 &amp;= b^{old} - E^{old}_i - y_i(\alpha_i^{new} - \alpha_i^{old})K_{ii} - y_j(\alpha_j^{new} - \alpha_j^{old})K_{ij} \\
b_2 &amp;= b^{old} - E^{old}_j - y_i(\alpha_i^{new} - \alpha_i^{old})K_{ij} - y_j(\alpha_j^{new} - \alpha_j^{old})K_{jj}.
\end{aligned}\]

<p>Which arises from (assuming \(E_k^{new} = 0\)):</p>

\[\begin{aligned}
&amp; E_k^{new} - E_k^{old} = \sum_{n=1}^N y_n \alpha^{new}_n K_{ni} +b^{new}- \sum_{n=1}^N y_n \alpha^{old}_n K_{ni} - b^{old} \quad \text{focusing only on the terms that change}\\ 
&amp; = y_i(\alpha_i^{new} - \alpha_i^{old})K_{ij} - y_j(\alpha_j^{new} - \alpha_j^{old})K_{jj} b^{new} - b^{old}. \\
\end{aligned}\]

<h3 id="pair-selection-heuristics">Pair selection heuristics</h3>

<p>As mentioned, a naive implementation has \(N^2\) pairs to optimize. However, SMO uses heuristics to reduce the candidates. First, we have to ensure that at least one of the multipliers is violating KKT conditions. Second, the support vectors (non-saturated Lagrange multipliers) are the most likely to play a big role. The first heuristic, then, is loop over all the dataset and update the multipliers that violate KKT conditions, then loop only over the support vectors (several times if needed) until all support vectors comply with KKT conditions. Then the process is repeated, to make sure “no more” updates are possible. When no updates are made, the algorithm stops.</p>

<p>The second heuristic is to select the second multiplier once the first is selected. In the implementation for this post, we tried to use the argmax of the denominator of the update of \(\alpha_j\) to select just the best step for each \(\alpha_j\). While it worked ok-ish, it did not converge very close to the sklearn implementation of the SVM. Using all indices seemed to work perfect but is very wasteful. Taking just the top 50 seemed like an ok compromise so in this implementation we do 50 updates for each \(\alpha_j\) focusing on the top \(\alpha_i\) that are more likely to yield a change. Another option could be to compute the denominator, but this implies computing the whole kernel matrix. In any case, this likley is very far away from actual top performing implenentations of the algorithm, but that is beyond the post.</p>

<p>One particularly interesting bit of the SMO implementation is that, since the error at the beginning is known (\(y\), since \(w = 0\) and \(b = 0\)) and we update just a couple of multipliers at a time, we do not need to recompute all the errors but change the errors associated with the multipliers that we updated. This is very important.</p>

<h3 id="putting-all-together">Putting all together</h3>

<p>The algorithm is relatively simple without the heuristics (essentially a “coupled” coordinate descent). In the code it looks something like follows:</p>

<div style="text-align: center;">
<img src="/assets/images/SVMs/SMO_algo.png" alt="LogBarrier image Error" width="600" />
</div>
<div style="text-align: center;">
<img src="/assets/images/SVMs/SMO.gif" alt="SMO image Error" width="600" />
</div>

<p>In the optimization process one can tell the difference from the “global” Newton’s method step.</p>

<h1 id="svm-in-mnist-telling-a-5-from-a-3">SVM in MNIST: Telling a 5 from a 3</h1>

<p>An important thing to note is that, when selecting the support vectors to infer \(b\) (for support vectors prediction matches the label exactly), we have to allow for some numerical room. This is, the filter has to be something like \(0 - \epsilon &lt; \alpha &lt; C +\epsilon\). Finally, after all this, we can contemplate how the default SVM implementation obliterates our algorithms! This is obviously expected since sklearn SVM optimization process is based on <a href="https://www.csie.ntu.edu.tw/~cjlin/libsvm/">libsvm</a> which offers a highly optimized SMO algorithm.  For some reason the gamma parameter in sklearn-SVM behaves differently than in my implementation. In any case, here are the results for 1232 training examples in 256 dimensions.</p>

<div style="text-align: center;">
<img src="/assets/images/SVMs/Comparison.png" alt="SMO image Error" width="600" />
</div>

<p>Quite cool that the algorithms are somewhat decent, taking into account this was pure numpy!</p>

<hr />

<h2 id="references">References</h2>

<p>These notes are a personal synthesis and extensions based on lectures from <strong>Mastermath (NL) Continuous Optimization Course 2025</strong>  along with standard references (below). Any errors are my own.</p>

<ol>
  <li>
    <p><strong>Boyd, S., &amp; Vandenberghe, L.</strong><br />
<em><a href="https://stanford.edu/~boyd/cvxbook/">Convex Optimization</a></em></p>
  </li>
  <li>
    <p><strong>Vapnik, V. N.</strong><br />
<em><a href="https://statisticalsupportandresearch.wordpress.com/wp-content/uploads/2017/05/vladimir-vapnik-the-nature-of-statistical-learning-springer-2010.pdf">The Nature of Statistical Learning Theory</a></em></p>
  </li>
  <li>
    <p>HMC resource: 
<a href="https://pages.hmc.edu/ruye/MachineLearning/lectures/ch9/node9.html">SMO</a></p>
  </li>
  <li>
    <p><strong>John C. Platt</strong> 
<em><a href="https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/tr-98-14.pdf">A fast algorithm for Training SVM</a></em></p>
  </li>
</ol>

<h2 id="footnotes">Footnotes</h2>
<details id="note-1">
      <summary><strong>1.</strong></summary>
    That intuition is not complete for me... I am struggling to see this, the key is in the definition of the surrogate dual gap.
 

</details>]]></content><author><name>Jordi Alonso</name></author><category term="ML" /><category term="Optimization" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Steepest descent: A geometrical take from gradient descent to Newton’s method.</title><link href="https://jordialonsoesteve.github.io/2025/12/25/SteepestDescent.html" rel="alternate" type="text/html" title="Steepest descent: A geometrical take from gradient descent to Newton’s method." /><published>2025-12-25T00:00:00+00:00</published><updated>2025-12-25T00:00:00+00:00</updated><id>https://jordialonsoesteve.github.io/2025/12/25/SteepestDescent</id><content type="html" xml:base="https://jordialonsoesteve.github.io/2025/12/25/SteepestDescent.html"><![CDATA[<link href="/css/syntax.css" rel="stylesheet" />

<h2 id="gradient-descent-with-exact-line-search-convergence-analysis">Gradient descent with exact line search: convergence analysis</h2>
<p>A key element in understanding gradient descent is its relationship with the condition number of the Hessian of the function we are minimizing. This will give an interesting framework to understand Newton’s method advantages. Let’s follow §9.3.1 from [1]. 
The idea here is to relate how many iterations we need to reach the optimum using gradient descent to the condition number of the Hessian.</p>

<p>The gradient descent algorithm with exact line search:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="syntax"><code>Algorithm: Gradient descent with exact line search

Given x0 in R^n, tolerance epsilon &gt; 0

for k = 0, 1, 2, ...:
    g_k = grad f(x_k)
    if ||g_k|| &lt;= epsilon:
        return x_k
    alpha_k = argmin_{alpha &gt; 0} f(x_k - alpha*g_k)
    x_{k+1} = x_k - alpha_k * g_k
</code></pre></div></div>

<p>Note that this involves solving:</p>

\[\alpha_k = \arg\min_{\alpha &gt; 0} f(x_k - \alpha g_k),\]

<p>which may or may not be difficult. For simplicity, let us assume that the exact optimal step is available.</p>

<p>Let $f(x): \mathbb{R^n} \rightarrow \mathbb{R}$ be a twice differentiable and strongly convex <strong>quadratic</strong> function (we relax this later).</p>

<p>In what follows we first derive a lower
bound on the norm of the gradient and then an upper bound on the distance to the objective value after one iteration of the algorithm. The idea is to provide an
upper bound on the distance to the optimal value as a function of the previous step.</p>

<p>Consider the <strong>second order Taylor approximation</strong> to $f$ around $x$ with <a href="https://en.wikipedia.org/wiki/Taylor%27s_theorem#Explicit_formulas_for_the_remainder">Lagrange remainder</a>:</p>

\[f(y) = f(x) + \nabla f(x)^{\top}(y-x) + \frac{1}{2} (y-x)^{\top}\nabla ^2 f(z) (y - x),\]

<p>for some  $z =  \theta x + (1-\theta) y \quad $  with $\theta \in [0, 1]$. Since $f$ is an strictly convex function we have that the second derivative must be positive definite <sup><a href="#note-1">1</a></sup>. Now, we want to find a lower and upper bound of $\frac{1}{2} (y-x)^{\top}\nabla ^2 f(z) (y - x)$. By considering the smallest eigenvalue $\nu_{min}$ of $\nabla ^2 f(z)$ <sup><a href="#note-2">2</a></sup> we obtain a lower bound:</p>

\[\begin{equation}
f(y) \geq f(x) + \nabla f(x)^{\top}(y-x) + \frac{1}{2} \nu_{min}\|y - x\|^2_2.
\label{eq:lowb}
\end{equation}\]

<p>From here, we can derive a lower bound on the difference to the optimal function value. Looking for the $\bar{y}$ that minimizies the
r.h.s. of equation \ref{eq:lowb}. Equating the first derivative w.r.t $y$ to $0$:</p>

\[\nabla f(x) + \nu_{min}(y-x) = 0,\]

<p>then,</p>

\[\bar{y} =  x - \frac{\nabla f(x)}{\nu_{min}}.\]

<p>Hence, plugging in, we have</p>

\[\begin{align*}
f(x)
&amp;+ \nabla f(x)^{\top}(y - x)
+ \frac{1}{2}\nu_{\min}\|y - x\|_2^2 \\
&amp;\ge
f(x)
+ \nabla f(x)^{\top}\!\left(-\frac{\nabla f(x)}{\nu_{\min}}\right)
+ \frac{1}{2}\nu_{\min}
\|\frac{\nabla f(x)}{\nu_{\min}}\|_2^2 \\
&amp;=
f(x)
- \frac{1}{\nu_{\min}}\|\nabla f(x)\|_2^2
+ \frac{1}{2\nu_{\min}}\|\nabla f(x)\|_2^2 \\
&amp;=
f(x)
- \frac{1}{2\nu_{\min}}\|\nabla f(x)\|_2^2 .
\end{align*}\]

<p>Finally, since the above holds for any $y$, we have that</p>

\[\begin{equation}
2\nu_{\min}(-p^* + f(x)) \leq \|\nabla f(x)\|_2^2
\label{eq:lowbound_g}
\end{equation}\]

<p>Now for the upper bound:</p>

\[\begin{equation}
f(y) \leq f(x) + \nabla f(x)^{\top}(y-x) + \frac{1}{2} \nu_{max}\|y - x\|^2_2.
\label{eq:uppb}
\end{equation}\]

<p>Let’s plug in the line search step in the above equation \eqref{eq:uppb} :</p>

\[\begin{aligned}
f(x - \alpha\nabla f(x))
&amp;\leq f(x)
+ \nabla f(x)^{\top}\big((x - \alpha\nabla f(x)) - x\big)
+ \tfrac{1}{2}\nu_{\max}\|(x - \alpha\nabla f(x)) - x\|_2^2 \\
&amp;= f(x)
+ \nabla f(x)^{\top}\big(-\alpha\nabla f(x)\big)
+ \tfrac{1}{2}\nu_{\max}\|(\alpha\nabla f(x))\|_2^2 \\
&amp;= f(x) - \alpha\|\nabla f(x)\|^2_2 
+ \tfrac{\alpha^2}{2}\nu_{\max}\|\nabla f(x)\|_2^2.
\end{aligned} \\\]

<p>Now, minimizing over $\alpha$ (is a simple quadratic equation):</p>

\[-\|\nabla f(x)\|^2_2 + \alpha\nu_{\max}\|\nabla f(x)\|_2^2 = 0\]

<p>so the optimal $\alpha$:</p>

\[\alpha = \frac{\|\nabla f(x)\|^2_2}{\nu_{\max}\|(\nabla f(x))\|_2^2} = \frac{1}{\nu_{\max}}.\]

<p>Plugging in:</p>

\[\begin{aligned}

f(x - \alpha\nabla f(x)) &amp; \leq f(x) - \frac{1}{\nu_{\max}}\|\nabla f(x)\|^2_2 
+ \frac{1}{2\nu_{\max}^2}\nu_{\max}\|\nabla f(x)\|_2^2 \\
&amp;= f(x) - \frac{1}{\nu_{\max}}\|\nabla f(x)\|^2_2 
+ \frac{1}{2\nu_{\max}}\|\nabla f(x)\|_2^2 \\
&amp;= f(x) - \frac{1}{2\nu_{\max}}\|\nabla f(x)\|^2_2.
\end{aligned} \\\]

<p>Substracting the optimal value $p^*$ and using equation \ref{eq:lowbound_g}:</p>

\[\begin{aligned}

f(x - \alpha \nabla f(x)) - p^* \leq f(x) - p^* - \frac{1}{2\nu_{\max}}\|\nabla f(x)\|^2_2 \leq f(x) - p^* - \frac{\nu_{\min}}{\nu_{\max}}(-p^* + f(x)) = (1 - \frac{\nu_{\min}}{\nu_{\max}})(f(x) - p^*)
\end{aligned} \\\]

<p>We are almost there. Appartenly if $\frac{\nu_{\min}}{\nu_{\max}} = 0$ is possible that we make no progress towards the optimum, since the value of the function after applying the optimal step may be the same. Let’s look closer:</p>

<p>Let</p>

\[\begin{aligned}
x_{k+1} := x_k - \alpha_k \nabla f(x_k),\\
\rho := 1 - \frac{\nu_{\min}}{\nu_{\max}}.
\end{aligned} \\\]

<p>Then, since $f(x_{k+1}) - p^* \leq \rho \bigl(f(x_k) - p^*\bigr)$, we have that</p>

\[f(x_{k+2}) - p^* \leq \rho \bigl(f(x_{k+1}) - p^*\bigr) \leq \rho^2 \bigl(f(x_k) - p^*\bigr),\]

<p>So, we have found a way of constructing an upper bound for <strong>any</strong> iteration in the algorithm based on the initial difference and the 
the smallest and biggest eigenvalue of the Hessian:</p>

\[f(x_{k}) - p^* \leq \rho^k \bigl(f(x_0) - p^*\bigr).\]

<p>Solving for $k$, we can make an explicit formula for the maximum number of iterations required to reach a particular loss. In particular, let $\epsilon$ be the desired suboptimality, we have that $f(x_{k}) - p^* \leq \epsilon$ if:</p>

\[\begin{aligned}
\rho^n = \frac{\epsilon}{\bigl(f(x_0) - p^*\bigr)}; \\
\log(\rho)n = \log(\frac{\epsilon}{\bigl(f(x_0) - p^*\bigr)}); \\
n = \frac{\log(\frac{\epsilon}{\bigl(f(x_0) - p^*\bigr)})}{\log(\rho)};\\
n = \frac{\log(\frac{\bigl(f(x_0) - p^*\bigr)}{\epsilon})}{\log(\frac{1}{\rho})} \\
\end{aligned}\]

<p>Looking closer at the denominator we see that, if the condition number of the Hessian is big, $\rho$ approaches 1 and hence the denominator approaches 0, increasing the number of iterations required!</p>

<div style="text-align: center;">
<img src="/assets/images/SteepestDescent/gif_gd/gd_contour_2x2.gif" alt="Gradient Descent Error" width="600" />
</div>

<h4 id="a-note-for-arbitrary-convex-functions">A note for arbitrary convex functions</h4>
<p>Above, treating the quadratic case allowed to make intuitive use of the maximum and minimum eigenvalue. However, in general, this is not possible since the
Hessian will be a function of $x$. Nevertheless, the above reasoning holds when changing the maximum and minimum eigenvalue by $M$ and $m$ respectively:</p>

\[\nabla^2 f(x) - m I \in \mathbb{S}^n_+,
\qquad
M I - \nabla^2 f(x) \in \mathbb{S}^n_+
\quad \forall x \in Q.\]

<p>For some set $Q$ being the domain of $f$, e.g. $\mathbb{R}^n$. $\mathbb{S}^n_+$ is the set of PSD matrices.</p>

<h2 id="steepest-descent">Steepest descent</h2>

<p>Gradient descent is a particular instance of a more general framework. Let us now consider <em>steepest descent</em>, we will follow §9.4 from [1].</p>

<p>Again, we start with a Taylor approxmation, in this case, a first order one:</p>

\[f(x + v) \approx f(x) + \nabla f(x)^{\top}(v).\]

<p>The idea is now to minimize this approximation with respect to $v$, but with a constraint on the norm of $v$. The “normalized”
steepest descent direction is defined then as:</p>

\[\triangle x_{nsd} = \text{argmin}\{\nabla f(x)^\top v \: |\: \|v\| \leq 1 \},\]

<p>for some norm. Selecting this norm is the key here.</p>

<p>We can also look at the “unnormalized” steepest descent direction:</p>

\[\triangle x_{sd} = \|\nabla f(x)\|_*\triangle x_{nsd}.\]

<p>We have that:</p>

\[\|\nabla f(x)\|_* = \text{sup}\{\nabla f(x)^\top z \: : \|z\| \leq 1\}\]

<p>i.e. what is the maximum directional derivative of $f$ at $x$ over all directions with unit norm? Following that line of thought we must have that:</p>

\[\nabla f(x)^\top \triangle x_{nsd} = -\|\nabla f(x)\|_*.\]

<p>Why? Because $ \triangle x_{nsd}$ is trying to find the direction with unit norm that minimizes the directional derivative. So, evaluating that directional 
derivative is exactly the definition of the dual norm up to a sign.</p>

<h3 id="euclidean-norm-gradient-descent">Euclidean norm: Gradient Descent</h3>

<p>For the euclidean norm we have that, since we need a vector whose angle is 0 with the gradient but still unit norm,</p>

\[\triangle x_{nsd} = -\frac{\nabla f(x)}{\|\nabla f(x)\|_2},\]

<p>and hence,</p>

\[\triangle x_{sd} = \|\nabla f(x)\|_*\triangle x_{nsd} = -{\|\nabla f(x)\|_2}\frac{\nabla f(x)}{\|\nabla f(x)\|_2} = -\nabla f(x).\]

<h3 id="quadratic-norm-newtons-method">Quadratic norm: <em>Newton’s Method</em></h3>

<p>The description above was intended to motivate Newton’s method advantage over gradient descent. In particular, the problem of gradient descent with
high condition numbers of the Hessian even for simple quadratic equations.</p>

<p>Turns out that we can propose a change of basis where this problem dissapears! In fact, for quadratic equations we will need just one iteration. While this is somewhat obvious when treating Newton’s method as a root finding agorithm for a second degree Taylor approximation to the function, that interpretation does not illustrate the interesting geometry that is going on “behind” it.</p>

<p>So, let’s consider the quadratic norm to see how to get rid of the problematic condition number:</p>

\[\|z\|_P = (z^\top P z)^{0.5} = \|P^{0.5}z\|_2,\]

<p>where $P \in \mathbb{S}_{++}$ (a positive definite matrix). Let us then consider</p>

\[\triangle x_{nsd} = \text{argmin}\{\nabla f(x)^\top v \: |\: \|v\|_P \leq 1 \}.\]

<p>We need to solve this inequality constrained optimization problem. Here we solve it in a simple way, but there are other ways<sup><a href="#note-3">3</a></sup>:</p>

\[\begin{aligned}
\triangle x_{nsd} &amp;= \text{argmin}\{\nabla f(x)^\top v \: |\: \|v\|_P \leq 1 \}, \\
&amp;= \text{argmin}\{\nabla f(x)^\top v \: |\: v^tPv = 1 \}, \quad \text{since is a linear objective function, constraint must be saturated} \\
&amp;= \text{argmin}\{\nabla f(x)^\top v \: |\: (P^{0.5}v)^\top(P^{0.5}v) = 1 \}, \\
&amp;= P^{-0.5}\text{argmin}\{\nabla f(x)^\top P^{-0.5}u \: |\: u^\top u = 1 \}, \quad v = P^{-0.5}u \\
&amp;= P^{-0.5}\text{argmin}\{ (P^{-0.5}\nabla f(x))^\top u \: |\: \| u \|_2= 1 \} \\
&amp;= \frac{P^{-1}\nabla f(x)}{\| P^{-0.5}\nabla f(x) \|_2}.\\
\end{aligned}\]

<p>And then \(\triangle x_{sd} = P^{-1}\nabla f\).</p>

<h4 id="change-of-basis-interpretation">Change of basis interpretation</h4>

<p>Let $\bar{x} = Ax$ and $\bar{f}(\bar{x}) =  f(A^{-1}\bar{x})$ and let $A \in \mathbb{S}_{++}^n$. Essentially, $\bar{f}$ is a function that describes the same level sets in another basis:</p>

<div style="text-align: center;">
<img src="/assets/images/SteepestDescent/Coordinate_transform.png" alt="Coordinate descent image Error" width="600" />
</div>
<p style="text-align: center; font-style: italic;">
    Illustration of a coordinate transformation: the left plot shows the original function, and the right plot shows the function on a different basis. The quadratic norm allows to pursue gradient descent for the same function on a different basis. In particular the linear transformation consist of an stretch of the vertical axis. Importantly, in this basis the condition number of the Hessian has improved.
</p>

<p>Consider the derivative:</p>

\[\nabla \bar{f}(\bar{x}) = A^{-1}\nabla f(A^{-1}\bar{x}) =  A^{-1}\nabla f(x).\]

<p>Now, mapping back to the original space, the search direction is:</p>

\[v = A^{-2}\nabla f(x)\]

<p>So, if we let $A = P^{0.5}$ then note that this has the same form as the normalized Steepest Descent direction for the quadratic norm. This shows that Steepest Descent with a quadratic norm is just gradient descent on a different basis <sup><a href="#note-4">4</a></sup> !</p>

<h4 id="newtons-method-algorithm">Newton’s Method algorithm</h4>
<p>We now have all the pieces (it was more difficult than I expected…)!</p>

<p>Let $A = (\nabla^2 f(x))^{0.5}$. Then, $\nabla^2  \bar{f}(\bar{x}) = I$. The condition number is 1.</p>

<p>And that’s it, Newton’s method is a gradient descent method with <em>change of basis that aliviates the condition number of the Hessian</em>.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="syntax"><code>Algorithm: Newton's method

Given x0 in R^n, tolerance epsilon &gt; 0

for k = 0, 1, 2, ...:
  Calculate Newton's step and decrement:
    v = -h_k(x)^{-1} g_k(x)
    lambda = g_k(x)^T h_k(x)^{-1} g_k(x)
  If lambda &lt; epsilon:
    Quit
  Line search:
    Get alpha_k
  Update:
  x_k = x_{k-1} + alpha_k v

</code></pre></div></div>

<h2 id="convergence-analysis-for-newtons-method">Convergence analysis for Newton’s method</h2>

<p>This is going to be a digestion of §9.5.3 from [1], hopefully bringing us some intuition. Alright, recall that for quadratic functions Newton’s method is trivial, and in fact, those can be solved analytically. So here we will focus on arbitrary strongly convex and differentiable functions. Additionally we are going to assume:</p>

\[\|\nabla^2 f(x) - \nabla^2 f(y) \|_2 \leq L\|x-y\|_2,\]

<p>i.e. the Hessian is Lipschitz continuous. Essentially, $L$ will measure how good is the second order Taylor approximation for $f$.</p>

<p>The idea is to show that <strong>if</strong> we start close enough to the optimum $x^*$ then Newton’s method will converge quadratically. In this case we consider iterates of Newton’s method with unit stepsize (“pure Newton’s step”).</p>

<p>Let $\triangle_{nt} = -(\nabla^2 f(x))^{-1} \nabla f(x)$:</p>

\[\begin{aligned}
\|\nabla f(x_k + \triangle_{nt}^k)\|_2 = \|\nabla f(x_{k+1})\|_2 &amp;= \| \nabla f(x_k + \triangle_{nt}^k) - \nabla f(x_k) + \nabla^2 f(x_k) (\nabla^2 f(x))^{-1} \nabla f(x)\|_2 \\
&amp;= \| \nabla f(x_k + \triangle_{nt}^k) - \nabla f(x_k) - \nabla^2 f(x_k) \triangle_{nt}^k\|_2 \\ 
&amp;= \| \int_0^1 \big( \nabla^2 f(x_k + t\triangle_{nt}^k) - \nabla^2 f(x_k) \big) dt \triangle_{nt}^k\|_2 \quad \text{(Fundamental theorem of calculus)} \\
&amp;\leq \int_0^1 \| \big( \nabla^2 f(x_k + t\triangle_{nt}^k) - \nabla^2 f(x_k) \big) \triangle_{nt}^k\|_2dt \quad \text{(Triangle inequality)} \\ 
&amp;\leq \int_0^1 \| \big( \nabla^2 f(x_k + t\triangle_{nt}^k) - \nabla^2 f(x_k) \big)\|_2\|\triangle_{nt}^k\|_2dt \quad \text{(Cauchy–Schwarz inequality)} \\
&amp;\leq \int_0^1 L(t \|\triangle_{nt}^k \|_2 ) \|\triangle_{nt}^k \|_2 dt \quad \text{(Lipschitz continuity definition)} \\
&amp;= \frac{L}{2}\|-(\nabla^2 f(x))^{-1} \nabla f(x)\|^2_2 \\
&amp;\leq \frac{L}{2}\| m^{-1}\nabla f(x_k)\|^2_2 \quad \text{(Effect of the Hessian is bounded below by smallest eigenvalue possible in the domain)} \\
&amp;= \frac{L}{2m^2}\| \nabla f(x_k)\|^2_2.\\
\end{aligned}\]

<p>So, the norm of the gradient at $k+1$ is bounded above by some value proportional to the gradient at the $k$. We have that <sup><a href="#note-5">5</a></sup> \(f(x) - x^* \leq \frac{1}{2m} \| \nabla f(x)\|_2^2\) and that \(\frac{L}{2m^2} \|\nabla f(x_{k+1})\|_2 \leq \big( \frac{L}{2m^2}\| \nabla f(x_k)\|_2 \big)^2\) so:</p>

\[\begin{aligned}

f(x) - x^* &amp;\leq \frac{1}{2m} \| \nabla f(x_k)\|_2^2 \\
&amp;= \frac{2m^3}{L^2} \big(\frac{L}{2m^2}\| \nabla f(x_k)\|_2 \big)^2 \\
&amp;\leq \frac{2m^3}{L^2} \big(\frac{L}{2m^2}\| \nabla f(x_{k-1})\|_2 \big)^4 \\
&amp;\leq \cdots \\ 
&amp;\leq \frac{2m^3}{L^2} \big(\frac{L}{2m^2}\| \nabla f(x_{0})\|_2 \big)^{2^{k + 1}} \\
\end{aligned}\]

<p>So, if $| \nabla f(x_{0})|_2 &lt; \frac{2m^2}{L}$ then covergence is extremely fast! Much faster than gradient descent. However, if we are far away from the optimum the algorithm could be very slow or diverge. In that case, line search is essential.</p>

<p>Importantly, note that the notion of distance depends on $L$ and $m$. In particular, note that if $L$ is huge, the gradient must be very small in order to enter the quadratic convergence regime! The more stable the Hessian is, the better. Intuitively, lower $L$ implies that the function resembles a quadratic function and hence the second order Taylor approximaton is better. This is particularly relevant for log-barrier methods but we will not dive into that here.</p>

<h2 id="some-comments-on-equality-constrained-optimization">Some comments on equality constrained optimization</h2>
<p>Let’s consider the following minimization problem:</p>

\[\begin{aligned}
\text{minimize}_{x \in \mathbb{R}^n} \quad &amp; f(x) \\
\text{subject to} \quad &amp; Ax = b 
\end{aligned}\]

<p>Assume $f(x)$ is strongly convex and differentiable as before. Furthermore, $A$ encodes linear constraints and we have less constraints than dimensions $n » m$. This implies that the system of equations is underdetermined and there are infinitely many solutions in a subspace of dimension $n-m$, leaving room for optimization. We want to find the solution that minimizes $f(x)$ in that subspace.</p>

<p>The Lagrangian encodes the idea that, at the optimum, the derivative of the function must be proportional to the derivative of the constraint (otherwise we could move in some direction orthogonal to the constraint and improve the function value while staying feasible):</p>

\[L(x, \nu) = f(x) + \nu^{\top}(Ax - b),\]

<p>defines a saddle, we want to minimize the function w.r.t $x$ and maximize it w.r.t $\nu$. The optimum is the critical point of the saddle. There is much more to this, but we do not dive into it here.</p>

<h3 id="gradient-descent">Gradient descent</h3>

<p>Importantly, minimizing the Lagrangian is not defined, it does not have a minimum. Hence, applying gradient descent to the Lagrangian makes no sense. Treating $\nu$ as a hyperparameter and use gradient descent is likely wrong in most of the cases.</p>

<p>Another, but still not proper, way to go about this is to use gradient descent on the euclidean norm of the derivative of the Lagrangian.</p>

\[\begin{equation}
\begin{aligned}
\nabla_x L(x, \nu) &amp;= (\nabla f(x) + A^{\top} \nu), \\ 
\nabla_{\nu} L(x, \nu) &amp;= (Ax - b) \\
\end{aligned}
\label{eq:derivatives_const}
\end{equation}\]

<p>Then,</p>

\[\begin{aligned}

\frac{1}{2} \|F\|^2_2 &amp;= \frac{1}{2} \Big( \|\nabla f(x) + \nu^\top A \|_2^2 + \|Ax - b\|_2^2 \Big) \\
&amp;=  \frac{1}{2} \bigg( (\nabla f(x) + \nu^{\top}A)^{\top} (\nabla f(x) + \nu^{\top}A) + (Ax - b)^{\top}(Ax - b) \bigg) \\
&amp;=  \frac{1}{2} \bigg( \|\nabla f(x)\|^2_2 + 2\nu^{\top} A \nabla f(x) + \nu^{\top}AA^{\top}\nu + x^{\top}A^{\top}Ax - 2x^{\top}A^{\top}b + \|b\|^2_2 \bigg).
\end{aligned}\]

<p>Taking derivatives (again):</p>

\[\begin{aligned}
 \nabla_x  \frac{1}{2}\|F\|^2_2 = \nabla^2 f(x) \big( \nabla f(x) + \nu^{\top}A \big) + A^{\top}(Ax -b) \\
 \nabla_{\nu}  \frac{1}{2}\|F\|^2_2 = A \nabla f(x) + AA^{\top}\nu
\end{aligned}\]

<p>Here we explicitly traget the critical point of the Lagrangian. This is also a bad idea, since it requires calculation of the Hessian but it still is a first order method.</p>

<p>An example of a proper approach is projected gradient descent (PGD). In this case it is quite simple, we only need to project the gradient update $\nabla f(x)$ into the kernel of $A$, starting from a feasible point (assuming constraints are linearly independent):</p>

\[g_{pgd} = (I - A^{\top}\big(AA^{\top}\big)^{-1}A)\nabla f(x) \quad \text{Remove from } \nabla f(x) \text{ what lies in the row space}.\]

<p>Here we achieve that $A g_{pgd} = 0$ and hence $A (x + g_{pgd} ) = Ax = b$ so we have feasibility if the original point was feasible.</p>

<div style="text-align: center;">
<img src="/assets/images/SteepestDescent/constrained_optimization_comparison_gd.png" alt="Coordinate descent image Error" />
</div>
<p style="text-align: center; font-style: italic;">
    Just an example of minimizing the norm of the gradient (root seeking) of the lagrangian and Projected GD. The function under consideration is $\frac{1}{2}x^{\top}Qx + qx$. In this case $x \in R^{1000}$ and $A \in R^{100\times 1000}$. For both algorithms a fixed step size of 0.001 is (just to keep it simple), loop stops if gradient is below 0.0001. 
</p>

<p>Importantly, while projected gradient descent enforces feasibility during the whole training process, minimizing the norm of the gradient of the Lagrangian reaches feasibility asymptotically.</p>

<h3 id="newtons-method-application">Newton’s method application</h3>

<p>While gradient descent cannot be applied directly, Newton’s method is a root seeking algorithm so it might be applied directly to the Lagrangian<sup><a href="#note-6">6</a></sup> . We can start from equations in \ref{eq:derivatives_const}, then calculate the second derivative and we have the <strong>Newton system</strong>:</p>

\[\begin{pmatrix}
\nabla^2 f(x_k) &amp; A^\top \\
A &amp; 0
\end{pmatrix}
\begin{pmatrix}
\Delta x_k \\ \Delta \nu_k
\end{pmatrix}
=
-\begin{pmatrix}
\nabla f(x_k) + A^\top \nu_k \\ Ax_k - b
\end{pmatrix} = 
-\begin{pmatrix}
\nabla f(x_k) + A^\top \nu_k \\ 0
\end{pmatrix}\]

<p>and the update is</p>

\[x_{k+1} = x_k - \Delta x_k, \quad
\nu_{k+1} = \nu_k - \Delta \nu_k.\]

<p>There is an important distinction in the actual algorithm if the start is or not feasible (\(Ax = b\)). If the start is not feasible we are essentially looking at a primal-dual optimization algorithm and we look at minimizing the residual vector. This has implications in the line search, convergence criteria and so on. The second equality in the Newton system would not be true in that case. However, it is often pretty simple to provide a feasible starting point. In that case, we can actually do some simple algebra</p>

\[\begin{aligned}
\nabla^2 f(x_k) \Delta x_k + A^\top \Delta \nu_k &amp;= - \nabla f(x_k) - A^\top \nu_k \\
\nabla^2 f(x_k) \Delta x_k &amp;= - \nabla f(x_k) - A^\top (\nu_k + \Delta \nu_k)
\end{aligned}\]

<p>so let \(\nu = (\nu_k + \Delta \nu_k)\) to reach the equivalent simpler system:</p>

\[\begin{pmatrix}
\nabla^2 f(x_k) &amp; A^\top \\
A &amp; 0
\end{pmatrix}
\begin{pmatrix}
\Delta x_k \\ \nu
\end{pmatrix} = 
-\begin{pmatrix}
\nabla f(x_k) \\ 0
\end{pmatrix}.\]

<p>In the latter we actually do not care much about \(\nu\) until convergence. Since it does not play any role in determining the updates for \(x\).</p>

<p>In the above example, a very naive implementation of Newton’s method takes only 0.068 seconds.</p>

<hr />

<h2 id="references">References</h2>

<p>These notes are a personal synthesis and extensions based on lectures from <strong>Mastermath (NL) Continuous Optimization Course 2025</strong>  along with standard references (below). Any errors are my own.</p>

<ol>
  <li><strong>Boyd, S., &amp; Vandenberghe, L.</strong><br />
<em><a href="https://stanford.edu/~boyd/cvxbook/">Convex Optimization</a></em></li>
</ol>

<!--
2. **Carl D. Meyer**  
    *[Matrix Analysis and Applied Linear Algebra](https://www.stat.uchicago.edu/~lekheng/courses/309/books/Meyer.pdf)*

-->

<h2 id="footnotes">Footnotes</h2>
<details id="note-1">
      <summary><strong>1. Why convexity requires second derivative PSD?</strong></summary>
    The first order condition for convexity: 
    \[
    f(y) \geq f(x) + \nabla f(x)^{\top}(y-x) \forall x, y:
    \]
   <p>
    <img src="/assets/images/SteepestDescent/gif_foc/first_order_convexity.gif" alt="First-order optimality condition" width="500" style="display:block; margin:auto;" />
  </p>

  Now, again making use of the Lagrange remainder is easy to see that the second derivative must be greater
  or equal to 0 in the scalar case or PSD in the vector case:
    \[
    f(y) =  f(x) + \nabla f(x)^{\top}(y-x) + \frac{1}{2} (y-x)^{\top}\nabla ^2 f(z) (y - x) \geq f(x) + \nabla f(x)^{\top}(y-x) 
    \]

</details>

<details id="note-2">
      <summary><strong>2. Why the smallest eigenvalue gives a lower bound?</strong></summary>

     Let the Hessian (<a href="https://en.wikipedia.org/wiki/Symmetry_of_second_derivatives">which is always symmetric</a>) at \(z\) have an eigendecomposition
    \[
    \nabla^2 f(z) = Q \nu Q^{\top},
    \]
    where \(Q\) is an <a href="https://en.wikipedia.org/wiki/Spectral_theorem">orthogonal matrix of eigenvectors</a>, and 
    \(\nu = \mathrm{diag}(\nu_1, \ldots, \nu_n)\) contains the real eigenvalues.

      In this basis, write \(v = Q^{\top}(y-x)\).  
      Then the quadratic form becomes
      \[
      (y-x)^{\top}\nabla^2 f(z)(y-x) 
      = v^{\top}\nu v 
      = \sum_{i=1}^n \nu_i v_i^2.
      \]

      Since each \(v_i^2 \ge 0\), we have
      \[
      \sum_{i=1}^n \nu_i v_i^2 
      \ge \nu_{\min} \sum_{i=1}^n v_i^2 
      = \nu_{\min} \|v\|^2_2 
      = \nu_{\min} \|y-x\|^2_2.
      \]

      Therefore,
      \[
      \frac{1}{2}(y-x)^{\top}\nabla^2 f(z)(y-x)
      \ge \frac{1}{2}\nu_{\min}\,\|y-x\|^2_2.
      \]
</details>

<details id="note-3">
      <summary><strong>3. Derivation using primal/dual arguments </strong></summary>
        This way is longer but for me seems more intuitive:

        We derive the primal solution analytically, this is indeed a minimizer if $\nu &gt; 0$, since then, the Lagrangian is bounded below w.r.t $v$:

        \[
        L(v, \nu) = \nabla f(x) + \nu \bigg(v^\top P v - 1\bigg).
        \]

        Differentiating and equating to 0 (very easy) we find that:
         \[
        v = -P^{-1}\frac{\nabla f(x)}{2\nu}.
        \]

        Good, now plugging this in we got rid of $v$ and we focus on maximizing the dual function:
        \[
        \begin{aligned}
        g(\nu) &amp;= \min_v L(v, \nu) \\
        &amp;= \nabla f(x)^\top \Big(-\frac{1}{2\nu} P^{-1} \nabla f(x)\Big) 
        + \nu \Big( \big(-\frac{1}{2\nu} P^{-1} \nabla f(x)\big)^\top P \big(-\frac{1}{2\nu} P^{-1} \nabla f(x)\big) - 1 \Big) \\
        &amp;= -\frac{1}{4\nu} \nabla f(x)^\top P^{-1} \nabla f(x) - \nu
        \end{aligned}
        \]

        Maximizing over \(\nu &gt; 0\) gives
        \[
        \frac{d g}{d \nu} = \frac{1}{4 \nu^2} \nabla f(x)^\top P^{-1} \nabla f(x) - 1 = 0 
        \quad \rightarrow \quad 
        \nu = \frac{1}{2} \sqrt{\nabla f(x)^\top P^{-1} \nabla f(x)}
        \]

        Plugging \(\nu\) back into \(v(\nu)\) recovers the primal solution:
        \[
        v = - \frac{P^{-1} \nabla f(x)}{\sqrt{\nabla f(x)^\top P^{-1} \nabla f(x)}} = \frac{P^{-1}\nabla f(x)}{\| P^{-0.5}\nabla f(x) \|_2}.
        \]
</details>

<details id="note-4">
      <summary><strong>4. Some other derivations from here... </strong></summary>
  Hessian:

\[
\nabla^2 \bar{f}(\bar{x}) = A^{-\top} \nabla^2 f(A^{-1}\bar{x}) A^{-1} = A^{-\top} \nabla^2 f(x) A^{-1}
\] 

If we look at directional derivatives and quadratic form with the Hessian then we have:


\[
\begin{aligned}
\nabla \bar{f}(\bar{x})^{\top} \color{green}{v}
&amp;=  \nabla \bar{f}(\bar{x})^{\top} \color{green}{v} \\
&amp;=  \nabla f(x)^{\top} \color{green}{A^{-1} v} \\
\end{aligned}
\] 

and 

\[
\begin{aligned}
\color{green}{v^{\top}} \color{white}{\nabla^2 \bar{f}(\bar{x}) } \color{green}{v} \color{white}{=} \color{green}{\bigg(A^{-1}v\bigg)^{\top}} \color{white}{\nabla^2 f(x) } \color{green}{\bigg(A^{-1}v\bigg)}.
\end{aligned}
\] 
</details>

<details id="note-5">
      <summary><strong>5. Proof for $f(x) - x^* \leq \frac{1}{2m} \| \nabla f(x)\|_2^2$ </strong></summary>

      Start with second order Taylor expansion with Lagrange remainder and continue using the lower bound on the minimum eigenvalue of the Hessian:
    \[ 
      \begin{aligned}
      f(y) &amp;= f(x) + \nabla f(x)^{\top}(y-x) + \frac{1}{2}(y-x)\nabla ^2 f(z)(y-x) \\
      &amp;\geq f(x) + \nabla f(x)^{\top}(y-x) + \frac{1}{2}(y-x)\nabla ^2 mI(y-x) \\
      &amp;= f(x) + \nabla f(x)^{\top}(y-x) + \frac{m}{2}\|(y-x)\|_2^2 .\\
      \end{aligned}
    \]

  Now, minimize with respect to $y$. This is clearly a lower bound.
    \[ 
      \begin{aligned}
      f(y) &amp;\geq \text{minimize}_w \nabla f(x)^{\top}(w-x) + \frac{m}{2}\|(w-x)\|_2^2\\
      &amp;= f(x) - \frac{1}{2m} \| \nabla f(x)\|^2_2.
      \end{aligned}
    \]

  Note the solution to the minimization problem:
  \[
    \nabla f(x) +  m(w + x) = 0 \rightarrow w = x - \frac{1}{m} \nabla f(x).
  \]

  Finally, $f(y)\geq f(x) - \frac{1}{2m} \| \nabla f(x)\|^2_2 \rightarrow f(x) - f(y) \leq \frac{1}{2m} \| \nabla f(x)\|^2_2$. Since this holds for any $y$ we can plug in the optimum value of the parameters and hence $f(x) - x^* \leq \frac{1}{2m} \| \nabla f(x)\|_2^2$.
</details>

<details id="note-6">
<summary><strong>6. </strong></summary>
     I am actually struggling to frame this under the condition number correction... So I will just give the traditional justifications.

     
</details>]]></content><author><name>Jordi Alonso</name></author><category term="Optimization" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Pendulum, equations of motion from first principles and solutions using ML.</title><link href="https://jordialonsoesteve.github.io/2024/07/12/Pendulum.html" rel="alternate" type="text/html" title="Pendulum, equations of motion from first principles and solutions using ML." /><published>2024-07-12T00:00:00+00:00</published><updated>2024-07-12T00:00:00+00:00</updated><id>https://jordialonsoesteve.github.io/2024/07/12/Pendulum</id><content type="html" xml:base="https://jordialonsoesteve.github.io/2024/07/12/Pendulum.html"><![CDATA[<link href="/css/syntax.css" rel="stylesheet" />

<script src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML" type="text/javascript"></script>

<h2 id="references">References</h2>

<p>These notes are based on:</p>

<p><strong>[1]</strong> <a href="https://scholar.harvard.edu/files/david-morin/files/cmchap6.pdf">Hardvard notes on Lagrangian method</a></p>

<p><strong>[2]</strong> <a href="https://theoreticalminimum.com/">Classical Mechanics: The Theoretical Minimum</a></p>

<p><strong>[3]</strong> <a href="https://www.nature.com/articles/s41467-018-07210-0">Deep learning for universal linear embeddings of nonlinear dynamics</a></p>

<h1 id="introduction">Introduction</h1>

<p>Lately I have been reading “Classical Mechanics: The theoretical minimum”. Since I mostly deal with purely data-driven approaches, I wanted to dive a bit into the first-principle derivation of predictive models.</p>

<p>Here I look into the simplest pendulum, a mass whose motion is restricted to a unit circle.</p>

<p align="center">
  <img src="/assets/images/Pendulum/pendulum1.gif" alt="Initial pendulum example" />
</p>

<p>First I derive a differential equation in which we can study the motion of the pendulum. Then I solve this differential equation in a data-driven way (i.e. construct a function of \(t\) that returns the angle and velocity of the pendulum given some initial conditions) using a linear model on a coordinate system generated by an autoencoder.</p>

<h2 id="derivation-of-the-potential-energy-from-first-principles">Derivation of the potential energy from first principles</h2>

<p>Potential energy is just the amount of work that is <em>potentially</em> stored. In this case it is clear that when the red ball is at the top 
of the circle (0,1) the potential energy is maximum. This is because we are assuming that the only acting force here is gravity, so the 
higher up, the more potential energy. Then, we can be sure that the total potential energy is proportional to the y coordinate.</p>

\[V(\theta) = mg(1-\cos(\theta))\]

<p>For \(\theta\) being the angle between the black and red line (see above .gif). On \((0, 1)\) the potential energy is at is 
maximum since \(\cos(\pi) = -1.\) \(mg\) is the constant to which the potential energy is proportional. Intuitively, this should be how much is the pull
of the force down (gravity, in earth \(9.8 m/s^2\)) and the mass of the ball (assumed to be 1).</p>

<p>So far I have only used the fact that, the higher up,  the more potential energy.</p>

<h3 id="derivation-of-the-kinetic-energy-from-first-principles">Derivation of the kinetic energy from first principles</h3>

<p>Assuming that the acceleration is constant (which makes sense because the force applied is constant), kinetic energy is defined as \(T(\theta) = \frac{1}{2}mv^2\). This comes 
from the sum of the work applied over distance, distance being \(d = \frac{1}{2}vt\) (assuming speed 0 at time 0, think of the area of a triangle) and work per 
unit of time being the increase of momentum, \(F = ma = m\frac{v}{t}\) because \(a\) is constant, so \(W = Fd = mad = \frac{1}{2}mv^2.\)</p>

<p>Surely, the velocity can be expressed as the change in the angle per unit of time so \(v = \dot{\theta}.\) Then the kinetic energy is
\(T(\theta) = \frac{1}{2}m\dot{\theta}^2.\) Going down is indeed negative velocity (the angle is getting smaller).</p>

<h3 id="derivation-of-the-equation-of-motion">Derivation of the equation of motion</h3>
<p>Now, since the total energy is constant, I could use that to try to get the equation of motion. However, using the 
principle of stationary action is cooler. This is defined as the integral of all possible paths of the difference between kinetic and potential energy (why this works is a matter for further study…).</p>

\[S = \int_{t_1}^{t_2} (T(\dot{\theta}) - V(\theta)) dt = \int_{t_1}^{t_2} L(\theta, \dot{\theta}) dt\]

<p>And we want stationary trajectories, such that the first derivative of S w.r.t the trajectory is 0. This yields<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>:</p>

\[\frac{d}{dt}\frac{\partial L}{\partial \dot{\theta}} - \frac{\partial L}{\partial \theta} = 0\]

<p>So, let’s plug in and see what pops out.</p>

\[\frac{\partial L}{\partial \theta} = \frac{\partial}{\partial \theta}(T(\dot{\theta}) - V(\theta)) = \frac{\partial}{\partial \theta}(- V(\theta))
= -mg\sin(\theta)\]

<p>Because \(T\) does not depend on \(\theta\). Now, the other term:</p>

\[\frac{\partial L}{\partial \dot{\theta}} = \frac{\partial}{\partial \dot{\theta}}(T(\dot{\theta}) - V(\theta)) = \frac{\partial}{\partial \dot{\theta}}(\frac{1}{2}m\dot{\theta}^2) =  m\dot{\theta}\]

<p>Since potential energy does not depend on velocity. Taking the derivative w.r.t time of the previous equation we get:</p>

\[\frac{d}{dt}\frac{\partial L}{\partial \dot{\theta}} = m\ddot{\theta}\]

<p>Plugging this in the Euler-Lagrange equation we get:</p>

\[m\ddot{\theta} = - mg\sin(\theta)\]

<p>Rewriting this equation we get:</p>

\[\ddot{\theta} = - g\sin(\theta)\]

<p>This cannot be solved analytically (some work can be done but is beyond me right now…), but it is possible to “solve” it numerically.</p>

<h3 id="numerical-solution-simulation">Numerical solution, simulation.</h3>

<p>The first thing to do is to reframe this second order differential equation into a system of 2 first order differential equations.
This is, \(\dot{z_1} = \dot{\theta}\) and \(\dot{z_2} = \ddot{\theta}.\) Now, for an initial condition of position (angle) and velocity:</p>

\[\mathbf{f}(z_1, z_2) = (z_2, - g\sin(z_1))\]

<p>Note that the function returns the change in position and the change in velocity. With that, a simple numerical ODE solver such as Euler’s method should do. I use
the default settings in the ‘solve_ivp’ from Scipy. Then for a given initial condition (in this case \(0.85 \pi\) initial position and \(0\) initial velocity.) we can visualize the solution:</p>

<p align="center">
  <img src="/assets/images/Pendulum/pendulum2.gif" alt="Visualize solution" />
</p>

<p>It looks a bit weird, and that is because it does not have any friction, it conserves the initial energy forever. We can visualize the evolution of
the angle in time and we will see that it is indeed a perfect sinusoidal wave. With friction we expect a dumped oscillator.</p>

<p align="center">
  <img src="/assets/images/Pendulum/Angle_vs_time.png" alt="Visualize solution" />
</p>

<h4 id="adding-friction">Adding friction</h4>
<p>Friction can be added to the equation of motion as a term that is proportional to the velocity. This is, the equation of motion becomes:</p>

\[\ddot{\theta} = - g\sin(\theta) - c \dot{\theta}\]

<p>This makes the animations look more realistic, here I use a damping factor of 0.1.</p>

<h3 id="solving-the-pendulum-equations">Solving the pendulum equations.</h3>

<p>As I mentioned above, here I am going to take a data-driven approach. Nevertheless, using a naive neural network, even with a PINN like loss (using the second derivative as extra penalty) function does not work. It trivially interpolates the seen data, but it is not able to generalize to unseen \(t\), which is also sort of expected. There is an interesting paper around this: <a href="https://proceedings.neurips.cc/paper/2020/file/1160453108d3e537255e9f7b931f4e90-Paper.pdf">Neural Networks Fail to Learn Periodic Functions
and How to Fix It</a>, nevertheless their proposed solution also did not work for me. As an example:</p>

<p align="center">
  <img src="/assets/images/Pendulum/theta_vs_time_NN.png" alt="Visualize solution" />
</p>

<p>The title of each plot is the initial condition, blue is the simulated solution and orange is the neural network solution. It is clear that, beyond the training data, the neural network fails.</p>

<p>So, naive approaches will not work, and probably there is a lesson to be learned here about the extrapolating capabilities of neural networks. Anyway, now I dive into how to address this using a Koopman operator-like approach. Essentially this boils down to construct a basis where the system advances linearly. In this case, I use a simple autoencoder to approximate this basis. In what follows I essentially replicate the paper by Lusch et al. (reference 3).</p>

<p>As explained in the paper the pendulum has a continuous spectrum, meaning that has infinite possible frequencies. This implies that a simple linear model (which essentially describes a limited number of frequencies and/or decay/growth rates) will not be able to capture the full possible behaviors of the system. To address this, more flexibility is needed. What they propose is to use an auxiliary network to parameterize the eigenvalues of this linear model in a continuous fashion. In this way, we do not need infinite dimensional operator to approximate infinite frequencies.</p>

<p>Long story short, construct some latent space with an autoencoder such that we can find the next time step using a linear model in this latent space.</p>

<h4 id="complex-numbers-as-rotation-matrices-using-jordan-canonical-form-to-parameterize-the-eigenvalues">Complex numbers as rotation matrices, using Jordan canonical form to parameterize the eigenvalues.</h4>

<p>A bit of background is needed to understand the next approach. In the paper they parameterize the eigenvalues directly, and, since we do not want to have to deal with complex types inside our neural network, a bit of math is needed. Complex eigenvalue pairs can be represented in a real block-diagonal matrix using the Jordan canonical form. This is because it is the same to multiply a two complex numbers as to multiply a vector by a 2x2 matrix:</p>

\[(a + bi)(c + di) = (ac - bd) + (ad + bc)i\]

<p>and</p>

\[\begin{bmatrix} a &amp; -b \\ b &amp; a \end{bmatrix} \begin{bmatrix} c \\ d \end{bmatrix} = \begin{bmatrix} ac - bd \\ ad + bc \end{bmatrix}.\]

<p>To specify a particular angle, we can use the polar coordinate form to see that (using <a href="https://en.wikipedia.org/wiki/List_of_trigonometric_identities#Angle_sum_and_difference_identities">angle sum and difference identities</a> ):</p>

\[(a + bi)(c + di) = r(\cos(\theta) + i\sin(\theta)) \cdot z(\cos(\phi) + i\sin(\phi)) = r z (\cos(\theta + \phi) + i\sin(\theta + \phi))\]

<p>Which for a magnitude of 1, this is equivalent to a rotation matrix (determinant 1) in 2D:</p>

\[\begin{bmatrix} \cos(\theta) &amp; -\sin(\theta) \\ \sin(\theta) &amp; \cos(\theta) \end{bmatrix} \cdot \begin{bmatrix} \cos(\phi) \\ \sin(\phi) \end{bmatrix} = \begin{bmatrix} \cos(\theta + \phi) \\ \sin(\theta + \phi) \end{bmatrix}.\]

<p>This will be the kind of linear (“kind of” because \(\theta\) is parameterized by a non-linear function) that will advance the system in time. Here the expressive power of a neural net is constrained to fit a very particular form. Additionally, a magnitude is needed to specify dumped oscillators, this will also be given by the auxiliary network, essentially parameterizing a complex number in polar form.</p>

<p>Finally, we need to add the time displacement, so the Koopman operator (for a latent space of 2 dimensions) will look like:</p>

\[\mathbf{K}(\theta, r, \Delta t) = r\begin{bmatrix} \cos(\theta \cdot \Delta t) &amp; -\sin(\theta \cdot \Delta t) \\ \sin(\theta \cdot \Delta t) &amp; \cos(\theta \cdot \Delta t) \end{bmatrix},\]

<p>Where \((\theta, r) = f_{\text{aux}}(z(t))\) and \(z(t)\) is the latent space representation of the system at time \(t\). Note that the latent space can be of any dimension, but care must be taken with the fact that rotation matrices work in 2D.</p>

<h3 id="network-architecture-objective-functions-and-training">Network architecture, objective functions and training.</h3>

<p>I simplify the objective function proposed by the paper by just taking an autoencoder reconstruction loss and a linear dynamics loss. The autoencoder reconstruction loss is simply the MSE of the reconstructed input without advancing it in time:</p>

\[\mathcal{L}_{\text{AE}} = \frac{\sum_i ||x_t^i - f_{\text{dec}}(f_{\text{enc}}(x_t^i))||^2}{N}.\]

<p>The linear dynamic loss:</p>

\[\mathcal{L}_{\text{dyn}} = \frac{\sum_i ||x_{\Delta t}^i - f_{\text{dec}}\bigl(\mathbf{K}(f_{\text{aux}}(f_{\text{enc}}(x_0^i)), \Delta t) \cdot f_{\text{enc}}(x_0^i)\bigr)||^2}{N}.\]

<p>Being \(\mathbf{K}\) the matrix that advances \(x_0\) (a vector of angle and velocity of the pendulum at time 0) to \(x_t\) in time. The training data, therefore, consists of tuples \((t, \theta_0, \dot{\theta}_0, \theta_t, \dot{\theta}_t)\). The network takes initial conditions and returns the position and velocity at time \(t\) and looks like:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="syntax"><code><span class="n">Koopman_autoencoder</span><span class="p">(</span>
  <span class="p">(</span><span class="n">encoder</span><span class="p">):</span> <span class="n">encoder</span><span class="p">(</span>
    <span class="p">(</span><span class="n">encoder</span><span class="p">):</span> <span class="n">Sequential</span><span class="p">(</span>
      <span class="p">(</span><span class="mi">0</span><span class="p">):</span> <span class="n">Linear</span><span class="p">(</span><span class="n">in_features</span><span class="o">=</span><span class="mi">2</span><span class="p">,</span> <span class="n">out_features</span><span class="o">=</span><span class="mi">64</span><span class="p">,</span> <span class="n">bias</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
      <span class="p">(</span><span class="mi">1</span><span class="p">):</span> <span class="n">GELU</span><span class="p">(</span><span class="n">approximate</span><span class="o">=</span><span class="s">'none'</span><span class="p">)</span>
      <span class="p">(</span><span class="mi">2</span><span class="p">):</span> <span class="n">Linear</span><span class="p">(</span><span class="n">in_features</span><span class="o">=</span><span class="mi">64</span><span class="p">,</span> <span class="n">out_features</span><span class="o">=</span><span class="mi">64</span><span class="p">,</span> <span class="n">bias</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
      <span class="p">(</span><span class="mi">3</span><span class="p">):</span> <span class="n">GELU</span><span class="p">(</span><span class="n">approximate</span><span class="o">=</span><span class="s">'none'</span><span class="p">)</span>
      <span class="p">(</span><span class="mi">4</span><span class="p">):</span> <span class="n">Linear</span><span class="p">(</span><span class="n">in_features</span><span class="o">=</span><span class="mi">64</span><span class="p">,</span> <span class="n">out_features</span><span class="o">=</span><span class="mi">64</span><span class="p">,</span> <span class="n">bias</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
      <span class="p">(</span><span class="mi">5</span><span class="p">):</span> <span class="n">GELU</span><span class="p">(</span><span class="n">approximate</span><span class="o">=</span><span class="s">'none'</span><span class="p">)</span>
      <span class="p">(</span><span class="mi">6</span><span class="p">):</span> <span class="n">Linear</span><span class="p">(</span><span class="n">in_features</span><span class="o">=</span><span class="mi">64</span><span class="p">,</span> <span class="n">out_features</span><span class="o">=</span><span class="mi">2</span><span class="p">,</span> <span class="n">bias</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
    <span class="p">)</span>
  <span class="p">)</span>
  <span class="p">(</span><span class="n">decoder</span><span class="p">):</span> <span class="n">decoder</span><span class="p">(</span>
    <span class="p">(</span><span class="n">decoder</span><span class="p">):</span> <span class="n">Sequential</span><span class="p">(</span>
      <span class="p">(</span><span class="mi">0</span><span class="p">):</span> <span class="n">Linear</span><span class="p">(</span><span class="n">in_features</span><span class="o">=</span><span class="mi">2</span><span class="p">,</span> <span class="n">out_features</span><span class="o">=</span><span class="mi">64</span><span class="p">,</span> <span class="n">bias</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
      <span class="p">(</span><span class="mi">1</span><span class="p">):</span> <span class="n">GELU</span><span class="p">(</span><span class="n">approximate</span><span class="o">=</span><span class="s">'none'</span><span class="p">)</span>
      <span class="p">(</span><span class="mi">2</span><span class="p">):</span> <span class="n">Linear</span><span class="p">(</span><span class="n">in_features</span><span class="o">=</span><span class="mi">64</span><span class="p">,</span> <span class="n">out_features</span><span class="o">=</span><span class="mi">64</span><span class="p">,</span> <span class="n">bias</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
      <span class="p">(</span><span class="mi">3</span><span class="p">):</span> <span class="n">GELU</span><span class="p">(</span><span class="n">approximate</span><span class="o">=</span><span class="s">'none'</span><span class="p">)</span>
      <span class="p">(</span><span class="mi">4</span><span class="p">):</span> <span class="n">Linear</span><span class="p">(</span><span class="n">in_features</span><span class="o">=</span><span class="mi">64</span><span class="p">,</span> <span class="n">out_features</span><span class="o">=</span><span class="mi">64</span><span class="p">,</span> <span class="n">bias</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
      <span class="p">(</span><span class="mi">5</span><span class="p">):</span> <span class="n">GELU</span><span class="p">(</span><span class="n">approximate</span><span class="o">=</span><span class="s">'none'</span><span class="p">)</span>
      <span class="p">(</span><span class="mi">6</span><span class="p">):</span> <span class="n">Linear</span><span class="p">(</span><span class="n">in_features</span><span class="o">=</span><span class="mi">64</span><span class="p">,</span> <span class="n">out_features</span><span class="o">=</span><span class="mi">2</span><span class="p">,</span> <span class="n">bias</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
    <span class="p">)</span>
  <span class="p">)</span>
  <span class="p">(</span><span class="n">K</span><span class="p">):</span> <span class="n">K</span><span class="p">(</span>
    <span class="p">(</span><span class="n">auxiliary_network</span><span class="p">):</span> <span class="n">Sequential</span><span class="p">(</span>
      <span class="p">(</span><span class="mi">0</span><span class="p">):</span> <span class="n">Linear</span><span class="p">(</span><span class="n">in_features</span><span class="o">=</span><span class="mi">2</span><span class="p">,</span> <span class="n">out_features</span><span class="o">=</span><span class="mi">64</span><span class="p">,</span> <span class="n">bias</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
      <span class="p">(</span><span class="mi">1</span><span class="p">):</span> <span class="n">GELU</span><span class="p">(</span><span class="n">approximate</span><span class="o">=</span><span class="s">'none'</span><span class="p">)</span>
      <span class="p">(</span><span class="mi">2</span><span class="p">):</span> <span class="n">Linear</span><span class="p">(</span><span class="n">in_features</span><span class="o">=</span><span class="mi">64</span><span class="p">,</span> <span class="n">out_features</span><span class="o">=</span><span class="mi">64</span><span class="p">,</span> <span class="n">bias</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
      <span class="p">(</span><span class="mi">3</span><span class="p">):</span> <span class="n">GELU</span><span class="p">(</span><span class="n">approximate</span><span class="o">=</span><span class="s">'none'</span><span class="p">)</span>
      <span class="p">(</span><span class="mi">4</span><span class="p">):</span> <span class="n">Linear</span><span class="p">(</span><span class="n">in_features</span><span class="o">=</span><span class="mi">64</span><span class="p">,</span> <span class="n">out_features</span><span class="o">=</span><span class="mi">2</span><span class="p">,</span> <span class="n">bias</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
    <span class="p">)</span>
  <span class="p">)</span>
<span class="p">)</span>
</code></pre></div></div>

<p>After training I get quite alright extrapolation  (definitely superior to the naive approach) performance, not perfect, however:</p>

<p align="center">
  <img src="/assets/images/Pendulum/theta_vs_time_Koopman.png" alt="Visualize solution" />
</p>

<h3 id="checking-the-latent-space">Checking the latent space</h3>

<p>Probably the most interesting plot here is the next one:</p>

<p align="center">
  <img src="/assets/images/Pendulum/latent_and_phase_space_trajectory.png" alt="Visualize solution" />
</p>

<p>In phase space, the system describes an ellipse. This can be appreciated also above in the vector field plot. To be able to advance the system using a linear model, the coordinate system has to be transformed such that trajectories can be described by a circle (spiral circle in this case because of the friction). Going from an ellipse to a circle seems simple enough, particularly since the ellipse presents also this cycle behavior. It would be more interesting to check if this kind of approach could work with a system whose phase space is very far from cyclic.</p>

<p>In any case, it is quite satisfying to see that the autoencoder learns exactly the mapping that one would expect, very cool paper!</p>

<p>The code for making these figures and so on is available <a href="https://github.com/neggor/Pendulum_Koopman">here</a>.</p>

<p align="center">
  <img src="/assets/images/Pendulum/pendulum5.gif" alt="Visualize solution" />
</p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p>See the first reference for the proof. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Jordi Alonso</name></author><category term="Physics" /><category term="ML" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Optimal Separating Hyperplanes</title><link href="https://jordialonsoesteve.github.io/2024/01/16/Understanding-SVMs_1.html" rel="alternate" type="text/html" title="Optimal Separating Hyperplanes" /><published>2024-01-16T00:00:00+00:00</published><updated>2024-01-16T00:00:00+00:00</updated><id>https://jordialonsoesteve.github.io/2024/01/16/Understanding-SVMs_1</id><content type="html" xml:base="https://jordialonsoesteve.github.io/2024/01/16/Understanding-SVMs_1.html"><![CDATA[<link href="/css/syntax.css" rel="stylesheet" />

<script src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML" type="text/javascript"></script>

<h1 id="references">References:</h1>
<p>The following notes are mostly based on the following sources:</p>

<p><strong>[1]</strong> <a href="https://hastie.su.domains/ElemStatLearn/printings/ESLII_print12_toc.pdf">Elements of Statistical Learning</a></p>

<p><strong>[2]</strong> <a href="https://www.math.uci.edu/~qnie/Publications/NumericalOptimization.pdf">Numerical optimization, Nocedal</a></p>

<p><strong>[3]</strong> <a href="https://www.khanacademy.org/math/multivariable-calculus/applications-of-multivariable-derivatives/lagrange-multipliers-and-constrained-optimization/v/the-lagrangian">Khan Academy (Lagrange multipliers, by 3blue1brown)</a></p>

<p><strong>[4]</strong> <a href="https://www-cs.stanford.edu/people/davidknowles/lagrangian_duality.pdf">Lagrangian Duality for Dummies</a></p>

<h1 id="introduction">Introduction</h1>

<p>Now that everyone is looking into Large Language Models (me included) I wanted to get back to the basics, to the more mathematically precise. For a while, I wanted to implement Support Vector Machines (SVMs) from scratch and <a href="https://www.youtube.com/watch?v=STFcvzoxVw4">this</a> podcast motivated me to do it. Eventually, I want to get into the invariances that Vladimir Vapnik talks about, which, as I see it, quite resemble the ideas in geometric deep learning.</p>

<p>In any case, SVMs are not trivial, when I went through them in class (just one lecture!) I did not get anything, and Elements Of Statistical Learning is not very explicit. So, in this post I start with the optimal separating hyperplanes problem, focusing on the geometry. I let the SVMs for a second post.</p>

<p>So, we are looking for a hyperplane that optimally separates data into two classes. We aim to find the hyperplane that maximizes the margin between the two classes. This works only when the data are linearly separable. However, we can always expand the basis where our data lives to be able to separate it, at the cost of possibly overfitting.</p>

<h1 id="geometrical-details">Geometrical details.</h1>

<p>First, let’s define the separating hyperplane:</p>

\[\mathbf{\beta}^T\mathbf{x} + \beta_0 = 0\]

<p>We are interested in the set: \(H = \{\mathbf{x} \in \mathbb{R}^n : \mathbf{\beta}^T\mathbf{x} + \beta_0 = 0\}\). Note that \(\mathbf{\beta}\) is a vector of n dimensions and \(\beta_0\) a scalar. These points are equidistant from the two hyperplanes \(S1 = \{\mathbf{x} \in \mathbb{R}^n : \mathbf{\beta}^T\mathbf{x} + \beta_0 = 1\}\) and \(S2 = \{\mathbf{x} \in \mathbb{R}^n : \mathbf{\beta}^T\mathbf{x} + \beta_0 = -1\}\). Those hyperplanes will pass through the support vectors (more on this later).</p>

<p>This is very similar to logistic regression. In logistic regression, we construct a function \(\mathbb{R}^n \rightarrow [0, 1]\) and then we classify points based on whether they are above or below some probability threshold (e.g. 0.5). Indeed, by fixing a threshold we define a hyperplane. In SVMs, we construct a function \(\mathbb{R}^n \rightarrow \mathbb{R}\) and then we classify points based on whether they are above 1 or below -1. However, we expect SVMs to generalize better since the margin in this classification is maximized.</p>

<p>To plot this say for \(\mathbf{x} \in \mathbb{R}^2\), \(\mathbf{\beta} = [1, 2]\) and \(\beta_0 = 1\) we just need to realize that \(\mathbf{\beta}^T\mathbf{x} + \beta_0 = 0\) is the same as \(b_0 x_1 + b_1 x_2 + \beta_0 = 0\). So given \(x_1\) we can find \(x_2\) and vice versa:</p>

\[x_2 = \frac{-\beta_0 - b_0 x_1}{b_1}\]

<p>The same idea applies to higher dimensions.</p>

<p align="center">
  <img src="/assets/images/SVMs/svm_margin.png" alt="Hyperplane example" />
</p>

<p>Now, we want to find \(\mathbf{\beta}\) and \(\beta_0\) that maximize the margin between the two hyperplanes so first we need to define this distance. To do so we use the normal (perpendicular vector, often of unit length) to the hyperplanes.</p>

<p>To find the normal to a hyperplane we first define a vector parallel to the hyperplane; any vector that goes from one point to another in the hyperplane. We can accomplish this by setting the starting point of \(\mathbf{x}_1\) to \(\mathbf{x}_2\) such that \(\mathbf{v} = \mathbf{x}_1 - \mathbf{x}_2\). What is important, however, is the direction, which is given by \(\mathbf{v}\). The normal \(\mathbf{w}\) to the hyperplane must be such that \(\mathbf{w}(\mathbf{x}_1 - \mathbf{x}_2)=0 \quad \forall \quad \mathbf{x}_1, \mathbf{x}_2 \in  H\). This \(\mathbf{w}\) is given by \(\frac{\mathbf{\beta}}{\lVert\beta\rVert}\) (dividing by the norm to get unit length) by the definition of the hyperplane \(H\).</p>

<p align="center">
  <img src="/assets/images/SVMs/svm_normal.png" alt="Normal (to fix)" />
</p>

<p>To find the distance between two hyperplanes, S1 and S2, we focus on a given point \(\mathbf{x}_1 \in S1\). Let’s find the point in S2 in the direction of the perpendicular from \(\mathbf{x}_1\). The perpendicular line that passes through \(\mathbf{x}_1\) is given by 
\(\mathbf{x}_1 + m\frac{\mathbf{\beta}}{\lVert\beta\rVert}\). For \(m\) being… the margin!</p>

<p>Again, divide \(\mathbf{\beta}\) by its norm to get a unit vector there. Clearly, for m = 0, it intersects \(\mathbf{x}_1\). Which is the corresponding x in S2?</p>

<p>We know that \(\mathbf{\beta}^T(x_1 + m\frac{\mathbf{\beta}}{\lVert\beta\rVert}) + \beta_0 = -1\) for some \(m\), which is precisely the number we want to find. So with some algebra;</p>

\[\mathbf{\beta}^T x_1 + m\frac{\lVert\beta\rVert^2}{\lVert\beta\rVert} + \beta_0 = -1\]

\[\mathbf{\beta}^T x_1 + m\lVert\beta\rVert + \beta_0 = -1\]

\[m = \frac{-\mathbf{\beta}^T x_1 - \beta_0 - 1}{\lVert\beta\rVert}\]

<p>And from the definition of S1, we know \(\beta^T x_1  = 1 - \beta_0\),</p>

\[m = \frac{-1 + \beta_0 - \beta_0 - 1}{\lVert\beta\rVert} = \frac{-2}{\lVert\beta\rVert} \propto \frac{1}{2}\frac{1}{\lVert\beta\rVert}\]

<p>And that is the definition of the margin, it is inversely proportional to the norm of \(\beta\). Importantly, we only care about its absolute value. It is relevant to notice that this function will be maximized at the same point as \(\frac{1}{\lVert\beta\rVert^2}\), this is important because it will make the optimization problem easier.</p>

<p>To me, that result was not intuitive, let’s see this in action:</p>

<p align="center">
  <img src="/assets/images/SVMs/svm_margin_norm.png" alt="Different margin with different norms, for the same direction" />
</p>

<p>Yep, it seems to work… Intuitively, given a direction, we can think of the increase/decrease in \(\mathbf{\lVert x \rVert}\) that is required to move from one hyperplane to the other (e.g. from S1 to S2). The bigger the sensitivity to \(\mathbf{x}\) the less we need to move to get a change of hyperplane. This is why minimizing the norm of \(\mathbf{\beta}\) is equivalent to maximizing the margin.</p>

<h1 id="optimal-separating-hyperplane-problem-formulation">Optimal Separating Hyperplane problem formulation.</h1>

<p>Now we have the geometrical background to frame the problem:</p>

<p>We can do this by minimizing \(\frac{1}{2}\lVert\beta\rVert^2\). However, we need to make sure that this hyperplane separates the data correctly. We can do this by subjecting this minimization process to the constraint of \(y_i(\mathbf{\beta}^T\mathbf{x}_i + \beta_0) \geq 1 \quad \forall \quad i = 1, \dots, n\). Being \(y_i\) the class of \(\mathbf{x}_i\) which is eihter 1 or -1. 
This constraint is equivalent to saying that the point is on the right side of the hyperplane and with enough margin. Negative values imply wrong classification, values between 0 and 1 imply that the point is between the two hyperplanes, which is not what we want. Finally, the problem is:</p>

\[\min_{\mathbf{\beta}, \beta_0} \frac{1}{2}\lVert\beta\rVert^2\]

\[\text{s.t.} \quad y_i(\mathbf{\beta}^T\mathbf{x}_i + \beta_0) \geq 1 \quad \forall \quad i = 1, \dots, n\]

<p>This is the same formulation given in Elements of Statistical Learning (they arrive here in a very confusing way imho). The solution to this problem is far from trivial and requires going rapidly over some optimization theory.</p>

<h2 id="optimization-process">Optimization process.</h2>
<h1 id="constrained-optimization-lagrangian">Constrained optimization (Lagrangian).</h1>

<p>The idea of the Lagrangian is based on the fact that the gradient of the function we are optimizing and the gradient of the constraint are proportional at the optimum. The proportionality constant is called the Lagrange multiplier. The Lagrangian is just the way of packing up that information in a way that, when optimizing the Lagrangian w.r.t the original variables and the Lagrange multiplier, we are just finding the proportionality constant and satisfying the constraint. Nevertheless, for inequality constraints, as is the case here, the Lagrangian is not (directly) enough. We need to introduce the idea of the KKT conditions.</p>

<h1 id="kkt-conditions">KKT conditions.</h1>

<p>This is a generalization of the method of Lagrange multipliers (Lagrangian). The Karush-Kuhn-Tucker (KKT) conditions are just first-order necessary conditions for a constrained problem, they follow relatively intuitively. They tell you that the Lagrangian must be at a stationary point and the way this has to happen. Equality constraints must be satisfied, inequality constraints that are not active must have a 0 Lagrange multiplier and therefore the “second” part of the Lagrangian is going to add to 0 (either a restriction is active or the Lagrange multiplier is 0). The KKT conditions come as follows:</p>

\[\nabla f(x^*) + \sum_{i \in A} \lambda_i^* \nabla c_i(x^*) = 0\]

<p>This just tells us that the Lagrangian must be equal to 0, this means that the gradient of the objective
function and constraint function must be proportional (kind of as before). And now, how this must happen:</p>

\[c_i(x^*) = 0, i \in E\]

<p>For E the set of equality constraints.</p>

\[c_i(x^*) \leq 0, i \in I\]

<p>For I the set of inequality constraints.</p>

\[\lambda_i^* \geq 0, i \in I\]

\[\lambda_i^* c_i(x^*) = 0, i \in I \cup E\]

<p>Satisfying this is enough to have a first-order optimality condition. Which should be enough for convex problems. Importantly, the satisfied constraints will have a non-cero Lagrange multiplier. In the case of optimal separating hyperplanes, those will be the points corresponding to the support vectors. As we will see later, \(\beta\) is entirely defined as a weighted combination of the support vectors.</p>

<p>Now the issue is, how do we solve this? We have defined some optimality conditions but this is harder than unconstrained problems, what can we do about this?</p>

<p>In Elements of Statistical Learning, they propose to solve this by first simplifying the problem through the dual form and then using a “standard” constrained optimization algorithm. Let’s see how this dual form helps.</p>

<h1 id="dual-primal-forms">Dual-Primal forms.</h1>

<p>The idea here is to approximate an infinite penalty for breaking a constraint by a finite, linear penalty. This is done by introducing a new variable \(\mathbf{\alpha}\), which is the Lagrange multiplier for the inequality constraints. In essence, we would have the original problem if \(\alpha = \infty\), and we have a lower bound when \(\alpha \leq \infty\). We can then write the Lagrangian as:</p>

\[\min_{\mathbf{\beta}, \beta_0} \max_{\mathbf{\alpha}} \frac{1}{2}\lVert\beta\rVert^2 - \sum_{i=1}^n \alpha_i (y_i[\mathbf{\beta}^T\mathbf{x}_i + \beta_0] - 1)\]

<p>The sign before the Lagrange multipliers here comes from the fact that if we allow for values equal or bigger than 1, then \(\alpha = 0\), but we have to infinitely penalize values smaller than 1, which are elements either wrong classified or between the two hyperplanes. For some intuition, note that in the latter case, \(y_i[\mathbf{\beta}^T\mathbf{x}_i + \beta_0] - 1\) becomes negative, so big positive values of \(\alpha\) will make the Lagrangian big.</p>

<p>That is hard. But if we reverse the order to:</p>

\[\max_{\mathbf{\alpha}} \min_{\mathbf{\beta}, \beta_0} \frac{1}{2}\lVert\beta\rVert^2 - \sum_{i=1}^n \alpha_i (y_i[\mathbf{\beta}^T\mathbf{x}_i + \beta_0] - 1)\]

<p>This is what is known as the dual form, which is much more tractable. Now, this will only be the same in some cases (strong duality). Luckily, this is the case for the optimal separating hyperplane problem (and for SVMs).</p>

<p>We can solve the minimization bit of the problem by taking the gradient w.r.t \(\mathbf{\beta}\) and \(\beta_0\) and setting it to 0 (first-order optimality condition). This will (easily) give us the following:</p>

\[\mathbf{\beta} = \sum_{i=1}^n \alpha_i y_i \mathbf{x}_i\]

\[\sum_{i=1}^n \alpha_i y_i = 0\]

<p>And then plugging this in:</p>

\[\max_{\mathbf{\alpha}} \sum_{i=1}^n \alpha_i - \frac{1}{2} \sum_{i=1}^n \sum_{k=1}^n \alpha_i \alpha_j y_i y_k \mathbf{x}_i^T \mathbf{x}_k\]

\[\text{s.t.} \quad \sum_{i=1}^n \alpha_i y_i = 0 \text{ and } \alpha_i \geq 0 \quad \forall \quad i = 1, \dots, n\]

<p>(Constraints to satisfy KKT conditions)</p>

<p>Which is easy to solve.</p>

<p>The derivatives of that function, let’s call it \(Ld\) are easy to compute if we reframe the above problem in terms of
outer products. For our current notation, this boils down to:</p>

\[\frac{dL_D}{d\alpha_j} = 1-\sum_{i=1}^N\alpha_iy_jy_ix^T_jx_i\]

<p>In code this would be:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="syntax"><code><span class="k">def</span> <span class="nf">Ld</span><span class="p">(</span><span class="n">alpha</span><span class="p">,</span> <span class="n">X</span><span class="p">,</span> <span class="n">y</span><span class="p">):</span>
        <span class="s">"""
        Lagrangian dual function
        """</span>
        <span class="n">alpha_outer</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">outer</span><span class="p">(</span><span class="n">alpha</span><span class="p">,</span> <span class="n">alpha</span><span class="p">)</span>
        <span class="n">y_outer</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">outer</span><span class="p">(</span><span class="n">y</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span>
        <span class="n">X_outer</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">dot</span><span class="p">(</span><span class="n">X</span><span class="p">,</span> <span class="n">X</span><span class="p">.</span><span class="n">T</span><span class="p">)</span> <span class="c1"># Dual, kernel!
</span>
        <span class="n">my_sol</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nb">sum</span><span class="p">(</span><span class="n">alpha</span><span class="p">)</span> <span class="o">-</span> <span class="mf">0.5</span> <span class="o">*</span> <span class="n">np</span><span class="p">.</span><span class="nb">sum</span><span class="p">(</span><span class="n">alpha_outer</span> <span class="o">*</span> <span class="n">y_outer</span> <span class="o">*</span> <span class="n">X_outer</span><span class="p">)</span>
        

      
        <span class="k">return</span> <span class="o">-</span><span class="mi">1</span> <span class="o">*</span> <span class="n">my_sol</span>   <span class="c1"># -1 Because the opt. program works with minimization.
</span></code></pre></div></div>
<p>and the gradient:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="syntax"><code><span class="k">def</span> <span class="nf">dLd</span><span class="p">(</span><span class="n">alpha</span><span class="p">,</span> <span class="n">X</span><span class="p">,</span> <span class="n">y</span><span class="p">):</span>
        <span class="s">"""
        Derivative of the Lagrangian dual function
        """</span>  
        <span class="n">y_outer</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">outer</span><span class="p">(</span><span class="n">y</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span>
        <span class="n">X_outer</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">dot</span><span class="p">(</span><span class="n">X</span><span class="p">,</span> <span class="n">X</span><span class="p">.</span><span class="n">T</span><span class="p">)</span> 
        <span class="n">my_grad</span> <span class="o">=</span>  <span class="n">np</span><span class="p">.</span><span class="n">ones</span><span class="p">(</span><span class="n">alpha</span><span class="p">.</span><span class="n">shape</span><span class="p">)</span> <span class="o">-</span> <span class="n">np</span><span class="p">.</span><span class="nb">sum</span><span class="p">(</span><span class="n">alpha</span><span class="p">[</span><span class="n">np</span><span class="p">.</span><span class="n">newaxis</span><span class="p">]</span> <span class="o">*</span> <span class="n">y_outer</span> <span class="o">*</span> <span class="n">X_outer</span><span class="p">,</span> <span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
        
        <span class="k">return</span> <span class="o">-</span><span class="mi">1</span> <span class="o">*</span> <span class="n">my_grad</span> <span class="c1"># Again the minimization issue...
</span></code></pre></div></div>

<p>Which is simpler! Now we can solve this by using an optimizer that can handle inequality constraints. (How these work is actually quite interesting and complicated but it gets quite out of scope right now…)</p>

<h2 id="solving-the-problem">Solving the problem.</h2>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="syntax"><code>  <span class="c1"># Define the constraints
</span>  <span class="c1"># 1. Alphas bigger or equal than 0 (bounds)
</span>  <span class="n">my_bounds</span> <span class="o">=</span> <span class="p">[(</span><span class="mi">0</span><span class="p">,</span> <span class="n">np</span><span class="p">.</span><span class="n">inf</span><span class="p">)]</span> <span class="o">*</span> <span class="nb">len</span><span class="p">(</span><span class="n">y</span><span class="p">)</span>
  <span class="c1"># 2. Sum of alphas times labels equal to 0 (linear constraint)
</span>  <span class="c1"># See: https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.LinearConstraint.html
</span>  <span class="n">my_constraint</span> <span class="o">=</span> <span class="n">LinearConstraint</span><span class="p">(</span><span class="n">y</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>
  <span class="n">alpha0</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">zeros</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">y</span><span class="p">))</span>
    <span class="c1"># Optimize
</span>  <span class="n">res</span> <span class="o">=</span> <span class="n">minimize</span><span class="p">(</span><span class="n">Ld</span><span class="p">,</span> <span class="n">alpha0</span><span class="p">,</span> <span class="n">args</span><span class="o">=</span><span class="p">(</span><span class="n">X</span><span class="p">,</span> <span class="n">y</span><span class="p">),</span> <span class="n">jac</span><span class="o">=</span><span class="n">dLd</span><span class="p">,</span>
                  <span class="n">constraints</span><span class="o">=</span><span class="n">my_constraint</span><span class="p">,</span> 
                  <span class="n">bounds</span><span class="o">=</span><span class="n">my_bounds</span><span class="p">,</span>
                  <span class="n">options</span><span class="o">=</span><span class="p">{</span><span class="s">'disp'</span><span class="p">:</span> <span class="bp">True</span><span class="p">},</span> <span class="n">method</span> <span class="o">=</span> <span class="s">'SLSQP'</span><span class="p">)</span>
  <span class="c1"># Get the support vectors
</span>  <span class="n">idx_support_vectors</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">where</span><span class="p">(</span><span class="n">res</span><span class="p">.</span><span class="n">x</span> <span class="o">&gt;</span> <span class="mf">1e-5</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span>
  <span class="n">alphas</span> <span class="o">=</span> <span class="n">res</span><span class="p">.</span><span class="n">x</span>
  <span class="n">w</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nb">sum</span><span class="p">((</span><span class="n">alphas</span> <span class="o">*</span> <span class="n">y</span><span class="p">).</span><span class="n">reshape</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span> <span class="o">*</span> <span class="n">X</span><span class="p">,</span> <span class="n">axis</span><span class="o">=</span><span class="mi">0</span><span class="p">)</span>
  <span class="c1"># The bias can be extractr from any support vector
</span>  <span class="n">b</span> <span class="o">=</span> <span class="n">y</span><span class="p">[</span><span class="n">idx_support_vectors</span><span class="p">[</span><span class="mi">0</span><span class="p">]]</span> <span class="o">-</span> <span class="n">np</span><span class="p">.</span><span class="n">dot</span><span class="p">(</span><span class="n">w</span><span class="p">,</span> <span class="n">X</span><span class="p">[</span><span class="n">idx_support_vectors</span><span class="p">[</span><span class="mi">0</span><span class="p">]])</span>
</code></pre></div></div>

<p>The definition of \(\beta\) (in the code w), follows from the solution to the Lagrangian, same with the bias (b). We can see how \(\beta\) is just a weighted combination of the support vectors. The bias is just the value of the hyperplane at one of the support vectors. People recommend taking the average of the bias over the support vectors, for numerical stability reasons.</p>

<p>Let’s try some toy data:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="syntax"><code>    <span class="n">np</span><span class="p">.</span><span class="n">random</span><span class="p">.</span><span class="n">seed</span><span class="p">(</span><span class="mi">42</span><span class="p">)</span>
    <span class="n">N</span> <span class="o">=</span> <span class="mi">50</span>
    <span class="c1"># Generate random points for two classes
</span>    <span class="n">class_1_points</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">random</span><span class="p">.</span><span class="n">randn</span><span class="p">(</span><span class="n">N</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span> <span class="o">+</span> <span class="n">np</span><span class="p">.</span><span class="n">array</span><span class="p">([</span><span class="mi">2</span><span class="p">,</span> <span class="mi">2</span><span class="p">])</span>
    <span class="n">class_2_points</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">random</span><span class="p">.</span><span class="n">randn</span><span class="p">(</span><span class="n">N</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span> <span class="o">+</span> <span class="n">np</span><span class="p">.</span><span class="n">array</span><span class="p">([</span><span class="o">-</span><span class="mi">2</span><span class="p">,</span> <span class="o">-</span><span class="mi">2</span><span class="p">])</span>

    <span class="c1"># Combine points and assign labels
</span>    <span class="n">X</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">vstack</span><span class="p">((</span><span class="n">class_1_points</span><span class="p">,</span> <span class="n">class_2_points</span><span class="p">))</span>
    <span class="n">y</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">hstack</span><span class="p">((</span><span class="n">np</span><span class="p">.</span><span class="n">ones</span><span class="p">(</span><span class="n">N</span><span class="p">),</span> <span class="o">-</span><span class="n">np</span><span class="p">.</span><span class="n">ones</span><span class="p">(</span><span class="n">N</span><span class="p">)))</span>

    <span class="c1"># Plot the points with different markers for each class
</span>    <span class="n">plt</span><span class="p">.</span><span class="n">scatter</span><span class="p">(</span><span class="n">X</span><span class="p">[</span><span class="n">y</span> <span class="o">==</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">],</span> <span class="n">X</span><span class="p">[</span><span class="n">y</span> <span class="o">==</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">],</span> <span class="n">label</span><span class="o">=</span><span class="s">'Class 1'</span><span class="p">,</span> <span class="n">marker</span><span class="o">=</span><span class="s">'o'</span><span class="p">)</span>
    <span class="n">plt</span><span class="p">.</span><span class="n">scatter</span><span class="p">(</span><span class="n">X</span><span class="p">[</span><span class="n">y</span> <span class="o">==</span> <span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">],</span> <span class="n">X</span><span class="p">[</span><span class="n">y</span> <span class="o">==</span> <span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">],</span> <span class="n">label</span><span class="o">=</span><span class="s">'Class -1'</span><span class="p">,</span> <span class="n">marker</span><span class="o">=</span><span class="s">'x'</span><span class="p">)</span>

    <span class="n">plt</span><span class="p">.</span><span class="n">xlabel</span><span class="p">(</span><span class="s">'X-axis'</span><span class="p">)</span>
    <span class="n">plt</span><span class="p">.</span><span class="n">ylabel</span><span class="p">(</span><span class="s">'Y-axis'</span><span class="p">)</span>
    <span class="n">plt</span><span class="p">.</span><span class="n">axhline</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="s">'black'</span><span class="p">,</span> <span class="n">linewidth</span><span class="o">=</span><span class="mf">0.5</span><span class="p">)</span>
    <span class="n">plt</span><span class="p">.</span><span class="n">axvline</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="s">'black'</span><span class="p">,</span> <span class="n">linewidth</span><span class="o">=</span><span class="mf">0.5</span><span class="p">)</span>
    <span class="n">plt</span><span class="p">.</span><span class="n">legend</span><span class="p">()</span>
    <span class="n">plt</span><span class="p">.</span><span class="n">grid</span><span class="p">(</span><span class="n">color</span><span class="o">=</span><span class="s">'gray'</span><span class="p">,</span> <span class="n">linestyle</span><span class="o">=</span><span class="s">'--'</span><span class="p">,</span> <span class="n">linewidth</span><span class="o">=</span><span class="mf">0.5</span><span class="p">)</span>
    <span class="c1"># Plot
</span>    <span class="n">idx_support_vectors</span><span class="p">,</span> <span class="n">w</span><span class="p">,</span> <span class="n">b</span> <span class="o">=</span> <span class="n">opt_sep_hyperplane</span><span class="p">(</span><span class="n">X</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span>
    <span class="n">plt</span><span class="p">.</span><span class="n">scatter</span><span class="p">(</span><span class="n">X</span><span class="p">[</span><span class="n">idx_support_vectors</span><span class="p">,</span> <span class="mi">0</span><span class="p">],</span> <span class="n">X</span><span class="p">[</span><span class="n">idx_support_vectors</span><span class="p">,</span> <span class="mi">1</span><span class="p">],</span> <span class="n">c</span><span class="o">=</span><span class="s">'r'</span><span class="p">,</span> <span class="n">marker</span><span class="o">=</span><span class="s">'.'</span><span class="p">,</span> <span class="n">s</span> <span class="o">=</span> <span class="mi">25</span><span class="p">)</span>
    <span class="c1"># print the hyperplane
</span>    
    <span class="c1"># Hyperplane is w[0] * x + w[1] * y + b = 0
</span>    <span class="c1"># Solve for y:
</span>    <span class="c1"># Get the limits of the plot   
</span>    <span class="n">x_min</span><span class="p">,</span> <span class="n">x_max</span> <span class="o">=</span> <span class="n">plt</span><span class="p">.</span><span class="n">xlim</span><span class="p">()</span>
    <span class="n">x</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">linspace</span><span class="p">(</span><span class="n">x_min</span><span class="p">,</span> <span class="n">x_max</span><span class="p">,</span> <span class="mi">100</span><span class="p">)</span>
    <span class="n">y</span> <span class="o">=</span> <span class="p">(</span><span class="o">-</span><span class="n">w</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">*</span> <span class="n">x</span> <span class="o">-</span> <span class="n">b</span><span class="p">)</span> <span class="o">/</span> <span class="n">w</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span>
    <span class="n">plt</span><span class="p">.</span><span class="n">plot</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span>
    <span class="n">y_1</span> <span class="o">=</span> <span class="p">(</span><span class="o">-</span><span class="n">w</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">*</span> <span class="n">x</span> <span class="o">-</span> <span class="n">b</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span> <span class="o">/</span> <span class="n">w</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span>
    <span class="n">plt</span><span class="p">.</span><span class="n">plot</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y_1</span><span class="p">,</span> <span class="s">'--'</span><span class="p">,</span> <span class="n">c</span> <span class="o">=</span> <span class="s">'g'</span><span class="p">)</span>
    <span class="n">y_m_1</span> <span class="o">=</span> <span class="p">(</span><span class="o">-</span><span class="n">w</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">*</span> <span class="n">x</span> <span class="o">-</span> <span class="n">b</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span> <span class="o">/</span> <span class="n">w</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span>
    <span class="n">plt</span><span class="p">.</span><span class="n">plot</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y_m_1</span><span class="p">,</span> <span class="s">'--'</span><span class="p">,</span> <span class="n">c</span> <span class="o">=</span> <span class="s">'g'</span><span class="p">)</span>
</code></pre></div></div>
<p align="center">
  <img src="/assets/images/SVMs/opt_sep_hyperplane.png" alt="Result, optimal separating hyperplane" />
</p>

<p>A key point is that \(\mathbf{\alpha}\) will be a sparse vector since there are only two points that are taken into consideration to construct the hyperplane.</p>

<h1 id="kernels">Kernels.</h1>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="syntax"><code><span class="k">def</span> <span class="nf">_RBF_kernel</span><span class="p">(</span><span class="n">X1</span><span class="p">,</span> <span class="n">X2</span><span class="p">,</span> <span class="n">gamma</span> <span class="o">=</span> <span class="mi">1</span><span class="p">):</span>
        <span class="n">m</span><span class="p">,</span> <span class="n">d1</span> <span class="o">=</span> <span class="n">X1</span><span class="p">.</span><span class="n">shape</span>
        <span class="n">n</span><span class="p">,</span> <span class="n">d2</span> <span class="o">=</span> <span class="n">X2</span><span class="p">.</span><span class="n">shape</span>

        <span class="c1"># Compute pairwise squared Euclidean distances
</span>        <span class="c1"># This comes from the fact that ||x - y||^2 = ||x||^2 + ||y||^2 - 2&lt;x, y&gt;
</span>        <span class="c1"># Which is the analogous to the elemental identity (a - b)^2 = a^2 + b^2 - 2ab
</span>        <span class="n">dist_sq</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nb">sum</span><span class="p">(</span><span class="n">X1</span><span class="o">**</span><span class="mi">2</span><span class="p">,</span> <span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="p">).</span><span class="n">reshape</span><span class="p">((</span><span class="n">m</span><span class="p">,</span> <span class="mi">1</span><span class="p">))</span> <span class="o">+</span> <span class="n">np</span><span class="p">.</span><span class="nb">sum</span><span class="p">(</span><span class="n">X2</span><span class="o">**</span><span class="mi">2</span><span class="p">,</span> <span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span> <span class="o">-</span> <span class="mi">2</span> <span class="o">*</span> <span class="n">np</span><span class="p">.</span><span class="n">dot</span><span class="p">(</span><span class="n">X1</span><span class="p">,</span> <span class="n">X2</span><span class="p">.</span><span class="n">T</span><span class="p">)</span>

        <span class="c1"># Compute RBF kernel matrix
</span>        <span class="n">K</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">exp</span><span class="p">(</span><span class="o">-</span><span class="n">gamma</span> <span class="o">*</span> <span class="n">dist_sq</span><span class="p">)</span>

        <span class="k">return</span> <span class="n">K</span>

    <span class="k">def</span> <span class="nf">kernel</span><span class="p">(</span><span class="n">X1</span><span class="p">,</span> <span class="n">X2</span><span class="p">):</span>
        <span class="s">"""
        RBF kernel
        """</span>
        <span class="k">return</span> <span class="n">_RBF_kernel</span><span class="p">(</span><span class="n">X1</span><span class="p">,</span> <span class="n">X2</span><span class="p">)</span>
</code></pre></div></div>
<p>We can go one step further and enrich the basis of our data with an infinite dimensional basis, using, for example, the Radial Basis Function (RBF) (check <a href="https://www.youtube.com/watch?v=Q0ExqOphnW0">this</a> out). Since we construct a “similarity” matrix with the inner products of the data points, we could have any number of dimensions in those data points if we can find a way of computing the inner product (which is the definition of a <a href="https://en.wikipedia.org/wiki/Hilbert_space">Hilbert space</a>). In the case of the RBF this is given by:</p>

\[K(\mathbf{x}_i, \mathbf{x}_j) = \exp(-\gamma ||\mathbf{x}_i - \mathbf{x}_j||^2)\]

<p>Where \(\gamma\) is a hyperparameter. Which results in:</p>

<p align="center">
  <img src="/assets/images/SVMs/opt_sep_hyperplane_decision_boundary_kernel.png" alt="Result, optimal separating hyperplane with RBF" />
</p>

<p>Some comments on how this is made are relevant since it is not immediate how to make predictions when using a kernel. It implies that we can only work with \(\mathbf{X}\mathbf{X}^T\) forms. We cannot explicitly compute the weights of the hyperplane (we can, and need, compute the bias, however). Luckily this is not a problem since, from the definition of the weights:</p>

\[\mathbf{\beta} = \sum_{i=1}^n \alpha_i y_i \mathbf{x}_i\]

<p>we can see that this is just a sum over the rows of the dataset weighted by the Lagrange multipliers. This means that \(\mathbf{\beta}\) is just a weighted sum of the support vectors (\(\alpha_i \neq 0\)). Since then we can compute the predictions as:</p>

\[\hat{y} = \text{sign}(\beta \mathbf{x}_j^T + \beta_0)\]

<p>Plugging in the definition of \(\beta\) we get:</p>

\[\hat{y} = \text{sign}(\sum_{i=1}^n \alpha_i y_i \mathbf{x}_i \mathbf{x}_j^T + \beta_0)\]

<p>which we can turn into:</p>

\[\hat{y} = \text{sign}(\sum_{i=1}^n \alpha_i y_i K(\mathbf{x}_i, \mathbf{x}_j) + \beta_0)\]

<p>for \(K\) being the kernel function. So we can calculate the bias term as:</p>

\[\beta_0 = \frac{1}{N_S} \sum_{i \in S} (y_i - \sum_{j \in S} \alpha_j y_j K(\mathbf{x}_i, \mathbf{x}_j))\]

<p>using the mean over the support vectors (or just using one support vector, as in the code). The class predictions then;</p>

\[\hat{y} = \text{sign}(\sum_{i=1}^n \alpha_i y_i K(\mathbf{x}_i, \mathbf{x}_j) + \beta_0)\]

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="syntax"><code>    
    <span class="n">b_kernel</span> <span class="o">=</span> <span class="n">y</span><span class="p">[</span><span class="n">idx_support_vectors</span><span class="p">[</span><span class="mi">0</span><span class="p">]]</span> <span class="o">-</span> <span class="n">np</span><span class="p">.</span><span class="nb">sum</span><span class="p">((</span><span class="n">alphas</span> <span class="o">*</span> <span class="n">y</span><span class="p">).</span><span class="n">reshape</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span> <span class="o">*</span> <span class="n">kernel</span><span class="p">(</span><span class="n">X</span><span class="p">,</span> <span class="n">X</span><span class="p">),</span> <span class="n">axis</span><span class="o">=</span><span class="mi">0</span><span class="p">)[</span><span class="n">idx_support_vectors</span><span class="p">[</span><span class="mi">0</span><span class="p">]]</span>
    <span class="n">prediction_fun</span> <span class="o">=</span> <span class="k">lambda</span> <span class="n">X_test</span><span class="p">:</span> <span class="n">np</span><span class="p">.</span><span class="n">sign</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="nb">sum</span><span class="p">((</span><span class="n">alphas</span> <span class="o">*</span> <span class="n">y</span><span class="p">).</span><span class="n">reshape</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span> <span class="o">*</span> <span class="n">kernel</span><span class="p">(</span><span class="n">X</span><span class="p">,</span> <span class="n">X_test</span><span class="p">),</span> <span class="n">axis</span><span class="o">=</span><span class="mi">0</span><span class="p">)</span> <span class="o">+</span> <span class="n">b_kernel</span><span class="p">)</span>
</code></pre></div></div>

<h1 id="final-comments">Final comments.</h1>

<p>The reflection about “modern” ML and traditional ML, representation learning vs kernels will have to wait until the end of the SVMs blog.</p>

<p>Here we learned about the optimal separating hyperplane problem, which is the basis of SVMs. We saw how to formulate the problem and how to solve it. We also saw how to use kernels to enrich the basis of our data and how to make predictions with them. That’s it for now!</p>]]></content><author><name>Jordi Alonso</name></author><category term="ML" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Diffusion Models I. Approximating the gradient of the data distribution.</title><link href="https://jordialonsoesteve.github.io/2023/02/16/Diffusion-Models.html" rel="alternate" type="text/html" title="Diffusion Models I. Approximating the gradient of the data distribution." /><published>2023-02-16T00:00:00+00:00</published><updated>2023-02-16T00:00:00+00:00</updated><id>https://jordialonsoesteve.github.io/2023/02/16/Diffusion-Models</id><content type="html" xml:base="https://jordialonsoesteve.github.io/2023/02/16/Diffusion-Models.html"><![CDATA[<link href="/css/syntax.css" rel="stylesheet" />

<script src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML" type="text/javascript"></script>

<p>Diffusion models are another approach to generative modelling. The algorithm became popular with the release of <a href="https://arxiv.org/abs/2204.06125">Dalle-2</a> and <a href="https://arxiv.org/abs/2112.10752">Stable Diffusion</a>. However, the underlying idea has been around for some time already.</p>

<p>In these notes, I will not focus on the details of the current SOTA algorithms but on the mathematical foundations of the idea. I will not describe conditional (image) generation. I will focus on sampling from the <strong>unknown</strong> probability distribution of a given dataset, first 2D and finally MNIST. My idea here is to provide <strong>an intuition</strong> of how this stuff works. For a more rigorous treatment check the references!</p>

<p>As an exercise I translated the <em>pytorch</em> code I found to <em>tensorflow</em> so all code here is in the latter. (I am using python 3.9.13)</p>

<p>These notes are (mostly) based on:</p>

<p><strong>[1]</strong> This excellent repository: <a href="https://github.com/acids-ircam/diffusion_models"> https://github.com/acids-ircam/diffusion_models </a></p>

<p><strong>[2]</strong> This awesome article: <a href="https://arxiv.org/abs/2208.11970"> https://arxiv.org/abs/2208.11970 </a></p>

<p><strong>[3]</strong> The work from Yang Song, who accompanies his research with super helpful blog posts: <a href="https://yang-song.net/blog/2019/ssm/">https://yang-song.net/blog/2019/ssm/</a>, <a href="https://yang-song.net/blog/2021/score/">https://yang-song.net/blog/2021/score/</a> with the paper <a href="https://arxiv.org/abs/1907.05600">https://arxiv.org/abs/1907.05600</a>.</p>

<p><strong>[4]</strong> Some classic papers on the topic: <a href="https://arxiv.org/pdf/2006.11239.pdf">https://arxiv.org/pdf/2006.11239.pdf</a>, <a href="https://www.jmlr.org/papers/volume6/hyvarinen05a/hyvarinen05a.pdf">https://www.jmlr.org/papers/volume6/hyvarinen05a/hyvarinen05a.pdf</a>, <a href="https://www.iro.umontreal.ca/~vincentp/Publications/smdae_techreport.pdf">https://www.iro.umontreal.ca/~vincentp/Publications/smdae_techreport.pdf</a>, <a href="https://arxiv.org/abs/1505.04597">https://arxiv.org/abs/1505.04597</a>, <a href="https://www.deeplearningbook.org/contents/partition.html">chapter 5 Deep Learning book</a>, <a href="https://arxiv.org/abs/1611.06612">RefineNet</a></p>

<p><strong>[5]</strong> Some other blogposts like: <a href="https://lilianweng.github.io/posts/2021-07-11-diffusion-models/"> https://lilianweng.github.io/posts/2021-07-11-diffusion-models/ </a></p>

<h3 id="introduction">Introduction</h3>
<hr />
<p>The name <em>diffusion</em> already gives a clue about the underlying idea: Reversing a <em>diffusion</em> process. To do so, we construct a function that can go from pure noise (the endpoint of the diffusion process) to the original coherently structured substance (the original point). In this sense, going from noise to coherent data, diffusion models are similar to GAN, but the similarities end there.</p>

<p>Another way of looking at this, motivated by the <em>score based modelling</em> point of view, is the idea of “navigating” a high dimensional space towards the areas where the coherence (w.r.t our data) within the dimensions is maximized. Or, what is the same, climbing a high dimensional probability distribution towards the peak areas. To clarify, high probability regions (the peaks) in this space are where the combination of the dimensions is more likely to render an observation belonging to our dataset. This is, <strong>gradient ascent w.r.t. the data distribution</strong>, starting from noise (random initialization) and generating an image. This is illustrated in the next GIF (which will be generated from scratch in these notes):</p>

<p align="center">
  <img src="/assets/images/DiffusionModels/MNIST_SAMPLING_FAST.gif" alt="MNIST diffusion gif" />
</p>

<p>Surprisingly enough, it is possible to estimate gradients of a dataset even when we do not have an explicit probability distribution (if we had it there would be no point in doing this anyway).</p>

<p>As a metaphor, what we are going to train here is the compass of the “navigators”, a model that gives us the gradient w.r.t the data distribution at any given point such that we can find our way towards high probability regions.</p>

<p>How can we estimate the gradient of a dataset? Let’s get to it!</p>

<h3 id="estimating-the-gradient-of-a-dataset">Estimating the gradient of a dataset:</h3>
<hr />
<p>Imports and the data:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="syntax"><code><span class="kn">import</span> <span class="nn">numpy</span> <span class="k">as</span> <span class="n">np</span>
<span class="kn">import</span> <span class="nn">matplotlib</span> <span class="k">as</span> <span class="n">mpl</span>
<span class="kn">import</span> <span class="nn">matplotlib.pyplot</span> <span class="k">as</span> <span class="n">plt</span>
<span class="kn">from</span> <span class="nn">sklearn</span> <span class="kn">import</span> <span class="n">datasets</span>
<span class="kn">import</span> <span class="nn">tensorflow</span> <span class="k">as</span> <span class="n">tf</span>
<span class="kn">from</span> <span class="nn">tensorflow</span> <span class="kn">import</span> <span class="n">keras</span>
<span class="kn">from</span> <span class="nn">tensorflow.keras</span> <span class="kn">import</span> <span class="n">layers</span>
<span class="kn">from</span> <span class="nn">tqdm</span> <span class="kn">import</span> <span class="n">tqdm</span>
<span class="kn">import</span> <span class="nn">tensorflow_addons</span> <span class="k">as</span> <span class="n">tfa</span>
<span class="n">plt</span><span class="p">.</span><span class="n">style</span><span class="p">.</span><span class="n">use</span><span class="p">(</span><span class="s">'Solarize_Light2'</span><span class="p">)</span>

<span class="k">def</span> <span class="nf">get_batch</span><span class="p">(</span><span class="n">size</span><span class="p">,</span> <span class="n">noise</span> <span class="o">=</span> <span class="mf">0.05</span><span class="p">,</span> <span class="nb">type</span> <span class="o">=</span> <span class="s">'moons'</span><span class="p">):</span>
    <span class="k">if</span> <span class="nb">type</span> <span class="o">==</span> <span class="s">'moons'</span><span class="p">:</span>
        <span class="n">sample</span> <span class="o">=</span> <span class="n">datasets</span><span class="p">.</span><span class="n">make_moons</span><span class="p">(</span><span class="n">n_samples</span><span class="o">=</span><span class="n">size</span><span class="p">,</span> <span class="n">noise</span><span class="o">=</span><span class="n">noise</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span>
    <span class="k">else</span><span class="p">:</span>
        <span class="n">sample</span> <span class="o">=</span> <span class="n">datasets</span><span class="p">.</span><span class="n">make_circles</span><span class="p">(</span><span class="n">n_samples</span><span class="o">=</span><span class="n">size</span><span class="p">,</span> <span class="n">factor</span><span class="o">=</span><span class="mf">0.5</span><span class="p">,</span> <span class="n">noise</span><span class="o">=</span> <span class="n">noise</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span>
    <span class="k">return</span> <span class="n">sample</span>

<span class="n">data</span> <span class="o">=</span> <span class="n">get_batch</span><span class="p">(</span><span class="mi">10</span><span class="o">**</span><span class="mi">4</span><span class="p">,</span> <span class="nb">type</span> <span class="o">=</span> <span class="s">''</span><span class="p">)</span>
<span class="n">fig</span><span class="p">,</span> <span class="p">(</span><span class="n">ax1</span><span class="p">,</span> <span class="n">ax2</span><span class="p">)</span> <span class="o">=</span> <span class="n">plt</span><span class="p">.</span><span class="n">subplots</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">25</span><span class="p">,</span> <span class="mi">15</span><span class="p">))</span>
<span class="n">ax1</span><span class="p">.</span><span class="n">scatter</span><span class="p">(</span><span class="o">*</span><span class="n">data</span><span class="p">.</span><span class="n">T</span><span class="p">,</span> <span class="n">alpha</span> <span class="o">=</span> <span class="mf">0.5</span><span class="p">,</span> <span class="n">color</span> <span class="o">=</span> <span class="s">'green'</span><span class="p">,</span> <span class="n">edgecolor</span> <span class="o">=</span> <span class="s">'white'</span><span class="p">,</span> <span class="n">s</span> <span class="o">=</span> <span class="mi">40</span><span class="p">)</span>
<span class="n">ax2</span><span class="p">.</span><span class="n">hist2d</span><span class="p">(</span><span class="o">*</span><span class="n">data</span><span class="p">.</span><span class="n">T</span><span class="p">,</span> <span class="n">bins</span> <span class="o">=</span> <span class="mi">50</span><span class="p">);</span>
</code></pre></div></div>
<p align="center">
  <img src="/assets/images/DiffusionModels/DensityApproximation.png" alt="Approximate a density" />
</p>

<p>For an <strong>unknown</strong> probability distribution \(p(x)\) we want to estimate \(\nabla \log p(x)\). So we can frame this as a regression problem with something like:</p>
<p align="center">
    $$\frac{1}{2}\mathop{\mathbb{E}}_{x \sim p(x)}[|| \mathcal{F}_{\theta} - \nabla \log p(x)||²]$$
</p>

<p>Being \(\mathcal{F}_{\theta}\) a very flexible function, like a neural network with parameters \(\theta\).</p>

<p>Yeah, but we don’t know \(p(x)\)! It <a href="https://www.jmlr.org/papers/volume6/hyvarinen05a/hyvarinen05a.pdf">turns out</a> (using integration by parts and some reasonable assumptions) that the above equation can be reformulated as:</p>

<p align="center">
    $$\mathop{\mathbb{E}}_{x \sim p(x)} \bigg[\text{tr}(\nabla_x \mathcal{F}_{\theta}(x)) + \frac{1}{2}||\mathcal{F}_{\theta}(x)||^2 \bigg]$$
</p>

<p>(The trace arises in the multidimensional version.)</p>

<p>Which does not have any \(p(x)\) inside. Now, this, called (vanilla) score based generative modelling can already be used.</p>

<p>Let’s check if we can approximate the gradient of the above 2D circle’s dataset.</p>

<p>First, let’s define our \(\mathcal{F}_{\theta}(x)\) using a fully conected network:</p>

<div class="language-py highlighter-rouge"><div class="highlight"><pre class="syntax"><code><span class="c1"># The gradient
# Now, this takes the point (x_1, x_2) and returns the gradient w.r.t. (x_1, x_2) at that point.
</span><span class="n">F_model</span> <span class="o">=</span> <span class="n">tf</span><span class="p">.</span><span class="n">keras</span><span class="p">.</span><span class="n">Sequential</span><span class="p">([</span>
    <span class="n">layers</span><span class="p">.</span><span class="n">Dense</span><span class="p">(</span><span class="mi">128</span><span class="p">,</span> <span class="n">input_shape</span> <span class="o">=</span> <span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="p">),</span> <span class="n">activation</span><span class="o">=</span> <span class="s">'linear'</span><span class="p">),</span>
    <span class="n">layers</span><span class="p">.</span><span class="n">Dense</span><span class="p">(</span><span class="mi">128</span><span class="p">,</span> <span class="n">activation</span><span class="o">=</span> <span class="s">'gelu'</span><span class="p">),</span>
    <span class="n">layers</span><span class="p">.</span><span class="n">Dense</span><span class="p">(</span><span class="mi">64</span><span class="p">,</span> <span class="n">activation</span><span class="o">=</span> <span class="s">'gelu'</span><span class="p">),</span>
    <span class="n">layers</span><span class="p">.</span><span class="n">Dense</span><span class="p">(</span><span class="mi">32</span><span class="p">,</span> <span class="n">activation</span><span class="o">=</span> <span class="s">'gelu'</span><span class="p">),</span>
    <span class="n">layers</span><span class="p">.</span><span class="n">Dense</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="n">activation</span><span class="o">=</span> <span class="s">'linear'</span><span class="p">)</span> <span class="c1"># TWO DIMENSIONS!
</span><span class="p">])</span>
</code></pre></div></div>

<p>And \(\nabla_x \mathcal{F}_{\theta}(x)\):</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="syntax"><code><span class="c1"># Generate the Hessian
</span><span class="o">@</span><span class="n">tf</span><span class="p">.</span><span class="n">function</span> 
<span class="k">def</span> <span class="nf">Hessian</span><span class="p">(</span><span class="n">F</span><span class="p">,</span> <span class="n">x</span><span class="p">):</span>
    <span class="s">'''
    Computes jacobian of the gradient (F_model) w.r.t x.
    :param F: function R^N -&gt; R^N
    :param x: tensor of shape [B, N]
    :return: Jacobian matrix of shape [B, N, N]

    '''</span>
    <span class="k">with</span> <span class="n">tf</span><span class="p">.</span><span class="n">GradientTape</span><span class="p">()</span> <span class="k">as</span> <span class="n">tape</span><span class="p">:</span>
        <span class="n">tape</span><span class="p">.</span><span class="n">watch</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>
        <span class="n">my_gradient</span> <span class="o">=</span> <span class="n">F</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>
        <span class="n">hessian</span> <span class="o">=</span> <span class="n">tape</span><span class="p">.</span><span class="n">batch_jacobian</span><span class="p">(</span><span class="n">my_gradient</span><span class="p">,</span> <span class="n">x</span><span class="p">)</span>
    
    <span class="k">return</span> <span class="n">hessian</span>
</code></pre></div></div>
<p>Notice that the derivatives are w.r.t the data x (not the parameters of F_model network):</p>

<p>Nice! then we only need to compute the loss, as we specified above:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="syntax"><code><span class="c1"># Now, I have the Jacobian (the gradient) (B, 2) and the Hessian (B, 2, 2)
</span><span class="k">def</span> <span class="nf">score_matching</span><span class="p">(</span><span class="n">F</span><span class="p">,</span> <span class="n">x</span><span class="p">):</span>
    <span class="n">gradient</span> <span class="o">=</span> <span class="n">F</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>

    <span class="c1"># Jacobian part
</span>    <span class="n">norm_gradient</span> <span class="o">=</span> <span class="p">(</span><span class="n">tf</span><span class="p">.</span><span class="n">norm</span><span class="p">(</span><span class="n">gradient</span><span class="p">,</span> <span class="n">axis</span> <span class="o">=</span> <span class="mi">1</span><span class="p">)</span> <span class="o">**</span> <span class="mi">2</span><span class="p">)</span> <span class="o">/</span><span class="mi">2</span>
    <span class="c1"># Hessian part
</span>    <span class="n">hessian</span> <span class="o">=</span> <span class="n">Hessian</span><span class="p">(</span><span class="n">F</span><span class="p">,</span> <span class="n">x</span><span class="p">)</span>

    <span class="n">tr_hessian</span> <span class="o">=</span> <span class="n">tf</span><span class="p">.</span><span class="n">cast</span><span class="p">(</span><span class="n">tf</span><span class="p">.</span><span class="n">linalg</span><span class="p">.</span><span class="n">trace</span><span class="p">(</span><span class="n">hessian</span><span class="p">),</span> <span class="n">dtype</span> <span class="o">=</span> <span class="n">tf</span><span class="p">.</span><span class="n">float32</span><span class="p">)</span>

    <span class="k">return</span> <span class="n">tf</span><span class="p">.</span><span class="n">math</span><span class="p">.</span><span class="n">reduce_mean</span><span class="p">(</span><span class="n">tr_hessian</span> <span class="o">+</span> <span class="n">norm_gradient</span><span class="p">,</span> <span class="n">axis</span> <span class="o">=</span> <span class="o">-</span><span class="mi">1</span><span class="p">)</span>
</code></pre></div></div>
<p>And training loop:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="syntax"><code><span class="c1"># Now training loop
</span><span class="n">optimizer</span> <span class="o">=</span> <span class="n">keras</span><span class="p">.</span><span class="n">optimizers</span><span class="p">.</span><span class="n">Adam</span><span class="p">(</span><span class="n">learning_rate</span><span class="o">=</span> <span class="mf">1e-4</span><span class="p">)</span>

<span class="n">loss_l</span> <span class="o">=</span> <span class="p">[]</span>

<span class="k">for</span> <span class="n">t</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">2500</span><span class="p">):</span> <span class="c1">#Epochs
</span>    <span class="k">with</span> <span class="n">tf</span><span class="p">.</span><span class="n">GradientTape</span><span class="p">()</span> <span class="k">as</span> <span class="n">tape</span><span class="p">:</span>
        <span class="n">loss</span> <span class="o">=</span> <span class="n">score_matching</span><span class="p">(</span><span class="n">F_model</span><span class="p">,</span> <span class="n">data</span><span class="p">)</span>
        
        <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">loss_l</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">1</span><span class="p">:</span>
            <span class="k">if</span> <span class="n">loss_l</span><span class="p">[</span><span class="o">-</span><span class="mi">2</span><span class="p">]</span> <span class="o">&lt;</span> <span class="n">loss_l</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">]:</span> <span class="c1"># trivial early stopping
</span>                <span class="k">break</span>

        <span class="n">model_grads</span> <span class="o">=</span> <span class="n">tape</span><span class="p">.</span><span class="n">gradient</span><span class="p">(</span><span class="n">loss</span><span class="p">,</span> <span class="n">F_model</span><span class="p">.</span><span class="n">trainable_weights</span><span class="p">)</span>

        <span class="n">optimizer</span><span class="p">.</span><span class="n">apply_gradients</span><span class="p">(</span><span class="nb">zip</span><span class="p">(</span><span class="n">model_grads</span><span class="p">,</span> <span class="n">F_model</span><span class="p">.</span><span class="n">trainable_weights</span><span class="p">))</span>
        

        <span class="n">loss_l</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">loss</span><span class="p">)</span>
        <span class="k">if</span> <span class="p">((</span><span class="n">t</span> <span class="o">%</span> <span class="mi">100</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="p">):</span>
            <span class="k">print</span><span class="p">(</span><span class="n">loss_l</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">])</span>

<span class="n">F_model</span><span class="p">.</span><span class="n">save_weights</span><span class="p">(</span><span class="s">'./Vanilla_score_weights.h5'</span><span class="p">)</span>
</code></pre></div></div>
<p>We now can plot the gradients that out F_model estimates:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="syntax"><code><span class="k">def</span> <span class="nf">plot_gradients</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="n">data</span><span class="p">,</span> <span class="n">plot_scatter</span> <span class="o">=</span> <span class="bp">True</span><span class="p">):</span>
    <span class="n">xx</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">stack</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">meshgrid</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">linspace</span><span class="p">(</span><span class="o">-</span><span class="mf">1.5</span><span class="p">,</span> <span class="mf">2.0</span><span class="p">,</span> <span class="mi">50</span><span class="p">),</span> <span class="n">np</span><span class="p">.</span><span class="n">linspace</span><span class="p">(</span><span class="o">-</span><span class="mf">1.5</span><span class="p">,</span> <span class="mf">2.0</span><span class="p">,</span> <span class="mi">50</span><span class="p">)),</span> <span class="n">axis</span> <span class="o">=</span> <span class="o">-</span><span class="mi">1</span><span class="p">).</span><span class="n">reshape</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span>
    <span class="n">scores</span> <span class="o">=</span> <span class="n">model</span><span class="p">(</span><span class="n">xx</span><span class="p">).</span><span class="n">numpy</span><span class="p">()</span> <span class="c1"># the gradients w.r.t each data point! 
</span>    <span class="c1"># This is, how much the DENSITY OF THE DATA increases at a given point of the xx (meshgrid)
</span>
    <span class="c1"># Now that stuff is not good to visualize. Some scaling to make a nice plot:
</span>    <span class="n">scores_nrom</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">linalg</span><span class="p">.</span><span class="n">norm</span><span class="p">(</span><span class="n">scores</span><span class="p">,</span> <span class="n">axis</span> <span class="o">=</span> <span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="nb">ord</span> <span class="o">=</span> <span class="mi">2</span><span class="p">,</span> <span class="n">keepdims</span> <span class="o">=</span> <span class="bp">True</span><span class="p">)</span>
    <span class="n">scores_log1p</span> <span class="o">=</span> <span class="n">scores</span> <span class="o">/</span> <span class="p">(</span><span class="n">scores_nrom</span> <span class="o">+</span> <span class="mf">1e-9</span><span class="p">)</span> <span class="o">*</span> <span class="n">np</span><span class="p">.</span><span class="n">log1p</span><span class="p">(</span><span class="n">scores_nrom</span><span class="p">)</span>
    <span class="n">plt</span><span class="p">.</span><span class="n">figure</span><span class="p">(</span><span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">16</span><span class="p">,</span><span class="mi">12</span><span class="p">))</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">plot_scatter</span><span class="p">):</span>
        <span class="n">plt</span><span class="p">.</span><span class="n">scatter</span><span class="p">(</span><span class="o">*</span><span class="n">data</span><span class="p">.</span><span class="n">T</span><span class="p">,</span> <span class="n">alpha</span><span class="o">=</span><span class="mf">0.3</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="s">'red'</span><span class="p">,</span> <span class="n">edgecolor</span><span class="o">=</span><span class="s">'white'</span><span class="p">,</span> <span class="n">s</span><span class="o">=</span><span class="mi">40</span><span class="p">)</span>
    <span class="n">plt</span><span class="p">.</span><span class="n">quiver</span><span class="p">(</span><span class="o">*</span><span class="n">xx</span><span class="p">.</span><span class="n">T</span><span class="p">,</span> <span class="o">*</span><span class="n">scores_log1p</span><span class="p">.</span><span class="n">T</span><span class="p">,</span> <span class="n">width</span><span class="o">=</span><span class="mf">0.002</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="s">'black'</span><span class="p">)</span>
    <span class="n">plt</span><span class="p">.</span><span class="n">xlim</span><span class="p">(</span><span class="o">-</span><span class="mf">1.5</span><span class="p">,</span> <span class="mf">2.0</span><span class="p">)</span>
    <span class="n">plt</span><span class="p">.</span><span class="n">ylim</span><span class="p">(</span><span class="o">-</span><span class="mf">1.5</span><span class="p">,</span> <span class="mf">2.0</span><span class="p">)</span>

<span class="n">plot_gradients</span><span class="p">(</span><span class="n">F_model</span><span class="p">,</span> <span class="n">data</span><span class="p">)</span>
</code></pre></div></div>
<p align="center">
  <img src="/assets/images/DiffusionModels/gradients.png" alt="visualize the gradients" />
</p>

<p>As we can see, this surprisingly works! That’s far from evident just looking at the loss function! To sample we can use a combination of the gradient and some noise: <em>langevin dynamics</em>.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="syntax"><code><span class="k">def</span> <span class="nf">langevin_dynamics</span><span class="p">(</span><span class="n">F</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">n_steps</span><span class="p">,</span> <span class="n">eps</span> <span class="o">=</span> <span class="mf">0.7e-2</span><span class="p">,</span> <span class="n">decay</span> <span class="o">=</span> <span class="p">.</span><span class="mi">9</span><span class="p">,</span> <span class="n">temperature</span> <span class="o">=</span> <span class="mi">1</span><span class="p">):</span>
    <span class="c1"># Just a naive langevin dynamics "sampler"
</span>    <span class="n">x_sequence</span> <span class="o">=</span> <span class="p">[</span><span class="n">x</span><span class="p">]</span>
    <span class="k">for</span> <span class="n">s</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">n_steps</span><span class="p">):</span>
        <span class="n">z_t</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">random</span><span class="p">.</span><span class="n">normal</span><span class="p">(</span><span class="n">size</span> <span class="o">=</span> <span class="n">x</span><span class="p">.</span><span class="n">shape</span><span class="p">)</span>
        <span class="n">gradient</span> <span class="o">=</span> <span class="n">F</span><span class="p">(</span><span class="n">x</span><span class="p">).</span><span class="n">numpy</span><span class="p">()</span>

        <span class="n">x</span> <span class="o">=</span> <span class="n">x</span> <span class="o">+</span> <span class="n">eps</span> <span class="o">*</span> <span class="p">(</span><span class="n">gradient</span> <span class="o">+</span>  <span class="p">(</span><span class="n">temperature</span> <span class="o">*</span> <span class="n">z_t</span> <span class="p">))</span>
        <span class="n">x_sequence</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>
        <span class="n">eps</span> <span class="o">*=</span> <span class="n">decay</span>
        
    <span class="k">return</span> <span class="n">np</span><span class="p">.</span><span class="n">array</span><span class="p">(</span><span class="n">x_sequence</span><span class="p">).</span><span class="n">reshape</span><span class="p">(</span><span class="n">n_steps</span> <span class="o">+</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span>

</code></pre></div></div>
<p align="center">
  <img src="/assets/images/DiffusionModels/gradients_sample.png" alt="visualize the gradients" />
</p>

<p>Now, there is a very important detail. This method does not work well in high-dimensional spaces, where most of the space is empty. This is especially relevant for images, which live in a low-dimensional manifold. In that case \(\log p(x)\) may become \(-\infty\). A way to solve this is to <strong>add noise</strong> to the data to fill the empty space and have reliable gradients everywhere. Solving the data sparsity problem in a high dimensional space through noise is one of the key ideas in Diffusion Models.</p>

<p>Now, while this is cool and I think helps in understanding the connection with the unknown \(p(x)\), there is even a more surprising, simpler and efficient approach. We can approximate the log “gradient” of the data distribution as the gradient of a Gaussian density</p>

\[q_{\sigma}(\tilde{x}|x)\]

<p>with mean at \(x\) and standard deviation \(\sigma\) w.r.t. a noisy data point \(\tilde{x}\).</p>

<p>This makes intuitive sense, the “gradient” should point us towards a combination of parameters which renders a less noisy data point. This is called <strong>denoising score matching</strong>. Using Gaussian noise and a Gaussian density as the kernel, the loss function looks like:</p>

<p align="center">
    $$Loss(\theta| \sigma) = \mathop{\mathbb{E}}_{x \sim p(x)} \bigg[\frac{1}{2}\bigg|\bigg|\mathcal{F}_{\theta}(\tilde{x}, \sigma) - \frac{x - \tilde{x}}{\sigma^2} \bigg|\bigg|^2_2 \bigg]$$
</p>

<p>Because:</p>
<p align="center">
 $$\frac{\mathbb{d}\log q_{\sigma}(\tilde{x}|x)}{\mathbb{d}\tilde{x}} = \frac{x- \tilde{x}}{\sigma^2}$$.
</p>

<p>Same as before, however, we want a reliable “gradient” everywhere. The solution here is to add different levels of noise \(\sigma\):</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="syntax"><code><span class="c1"># Different sigma values
</span><span class="n">sigma_begin</span> <span class="o">=</span> <span class="mi">1</span>
<span class="n">sigma_end</span> <span class="o">=</span> <span class="mf">0.01</span>
<span class="n">num_noises</span> <span class="o">=</span> <span class="mi">10</span>
<span class="n">sigmas</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">exp</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">linspace</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">log</span><span class="p">(</span><span class="n">sigma_begin</span><span class="p">),</span> <span class="n">np</span><span class="p">.</span><span class="n">log</span><span class="p">(</span><span class="n">sigma_end</span><span class="p">),</span> <span class="n">num_noises</span><span class="p">))</span>
<span class="n">plt</span><span class="p">.</span><span class="n">plot</span><span class="p">(</span><span class="n">sigmas</span><span class="p">)</span>
</code></pre></div></div>
<p align="center">
  <img src="/assets/images/DiffusionModels/sigma_values.png" alt="Sigma values" />
</p>

<p>And this looks like:</p>

<p align="center">
  <img src="/assets/images/DiffusionModels/Added_noise.png" alt="Noise" />
</p>

<p>Here we have the noise added to the initial data and plotted on top in different color/shape.</p>

<p>This works best if we let our model know (our approximation to the gradient) at which noise level \(\sigma\) we are. So this time our model (our fully connected NN) will also take an embedding of the \(\sigma\) level. And we will insist on it! Feeding a representation of the noise level index at every layer.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="syntax"><code><span class="n">Input_data</span> <span class="o">=</span> <span class="n">keras</span><span class="p">.</span><span class="n">Input</span><span class="p">(</span><span class="n">shape</span><span class="o">=</span><span class="p">(</span><span class="mi">2</span><span class="p">,))</span>
<span class="n">labels_input</span> <span class="o">=</span> <span class="n">keras</span><span class="p">.</span><span class="n">Input</span><span class="p">(</span><span class="n">shape</span><span class="o">=</span><span class="p">(</span><span class="mi">1</span><span class="p">,))</span>

<span class="n">d</span> <span class="o">=</span> <span class="n">layers</span><span class="p">.</span><span class="n">Dense</span><span class="p">(</span><span class="mi">128</span><span class="p">,</span> <span class="n">activation</span><span class="o">=</span> <span class="s">'linear'</span><span class="p">)(</span><span class="n">Input_data</span><span class="p">)</span>
<span class="n">l</span> <span class="o">=</span> <span class="n">tf</span><span class="p">.</span><span class="n">keras</span><span class="p">.</span><span class="n">layers</span><span class="p">.</span><span class="n">Embedding</span><span class="p">(</span><span class="n">num_noises</span><span class="p">,</span> <span class="mi">128</span><span class="p">)(</span><span class="n">labels_input</span><span class="p">)</span>

<span class="n">d</span> <span class="o">=</span> <span class="n">keras</span><span class="p">.</span><span class="n">layers</span><span class="p">.</span><span class="n">Multiply</span><span class="p">()([</span><span class="n">d</span><span class="p">,</span> <span class="n">l</span><span class="p">])</span>
<span class="n">d</span> <span class="o">=</span> <span class="n">tf</span><span class="p">.</span><span class="n">keras</span><span class="p">.</span><span class="n">activations</span><span class="p">.</span><span class="n">gelu</span><span class="p">(</span><span class="n">d</span><span class="p">)</span>

<span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="p">(</span><span class="mi">128</span><span class="p">,</span> <span class="mi">128</span><span class="p">):</span>
    <span class="n">d</span> <span class="o">=</span> <span class="n">layers</span><span class="p">.</span><span class="n">Dense</span><span class="p">(</span><span class="n">i</span><span class="p">,</span> <span class="n">activation</span><span class="o">=</span> <span class="s">'linear'</span><span class="p">)(</span><span class="n">d</span><span class="p">)</span>
    <span class="n">l</span> <span class="o">=</span>  <span class="n">tf</span><span class="p">.</span><span class="n">keras</span><span class="p">.</span><span class="n">layers</span><span class="p">.</span><span class="n">Embedding</span><span class="p">(</span><span class="n">num_noises</span><span class="p">,</span> <span class="n">i</span><span class="p">)(</span><span class="n">labels_input</span><span class="p">)</span>
    
    <span class="n">d</span> <span class="o">=</span> <span class="n">keras</span><span class="p">.</span><span class="n">layers</span><span class="p">.</span><span class="n">Multiply</span><span class="p">()([</span><span class="n">d</span><span class="p">,</span> <span class="n">l</span><span class="p">])</span>
    <span class="n">d</span> <span class="o">=</span> <span class="n">tf</span><span class="p">.</span><span class="n">keras</span><span class="p">.</span><span class="n">activations</span><span class="p">.</span><span class="n">gelu</span><span class="p">(</span><span class="n">d</span><span class="p">)</span>

<span class="n">output</span> <span class="o">=</span> <span class="n">keras</span><span class="p">.</span><span class="n">layers</span><span class="p">.</span><span class="n">Dense</span><span class="p">(</span><span class="mi">2</span><span class="p">)(</span><span class="n">d</span><span class="p">)</span>
<span class="n">output</span> <span class="o">=</span> <span class="n">keras</span><span class="p">.</span><span class="n">layers</span><span class="p">.</span><span class="n">Flatten</span><span class="p">()(</span><span class="n">output</span><span class="p">)</span>

<span class="n">F_model</span> <span class="o">=</span> <span class="n">keras</span><span class="p">.</span><span class="n">Model</span><span class="p">([</span><span class="n">Input_data</span><span class="p">,</span> <span class="n">labels_input</span><span class="p">],</span> <span class="n">output</span><span class="p">)</span>
</code></pre></div></div>
<p>Which is already looking relatively fancy! And this is just 2D!</p>

<p align="center">
  <img src="/assets/images/DiffusionModels/NoiseConditionalModel.png" alt="NoiseModel" />
</p>

<p>And we generate some \(\sigma\) levels (indexes) to use during training:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="syntax"><code><span class="n">labels</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">random</span><span class="p">.</span><span class="n">randint</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">num_noises</span><span class="p">,</span> <span class="n">data</span><span class="p">.</span><span class="n">shape</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span>
</code></pre></div></div>
<p>Now we are ready to write down our Noise Conditional Loss function:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="syntax"><code><span class="k">def</span> <span class="nf">conditional_noise_loss</span><span class="p">(</span><span class="n">F</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">labels</span> <span class="o">=</span> <span class="n">labels</span><span class="p">,</span> <span class="n">sigmas</span> <span class="o">=</span> <span class="n">sigmas</span><span class="p">):</span>
    
    <span class="n">used_sigmas</span> <span class="o">=</span> <span class="n">sigmas</span><span class="p">[</span><span class="n">labels</span><span class="p">][...,</span> <span class="n">np</span><span class="p">.</span><span class="n">newaxis</span><span class="p">]</span>

    <span class="c1"># Generate noise for a given level (label)
</span>    <span class="n">noise</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">random</span><span class="p">.</span><span class="n">normal</span><span class="p">(</span><span class="n">size</span> <span class="o">=</span> <span class="n">x</span><span class="p">.</span><span class="n">shape</span><span class="p">)</span> <span class="o">*</span> <span class="n">used_sigmas</span>
    
    <span class="n">perturbed_x</span> <span class="o">=</span> <span class="n">x</span> <span class="o">+</span> <span class="n">noise</span>

    <span class="c1"># \frac{x - \tilde{x}}{\sigma^2}
</span>    <span class="n">target</span> <span class="o">=</span>  <span class="n">tf</span><span class="p">.</span><span class="n">constant</span><span class="p">((</span><span class="n">data</span> <span class="o">-</span> <span class="n">perturbed_x</span><span class="p">)</span> <span class="o">/</span> <span class="p">(</span><span class="n">used_sigmas</span> <span class="o">**</span> <span class="mi">2</span><span class="p">),</span> <span class="n">dtype</span> <span class="o">=</span><span class="n">tf</span><span class="p">.</span><span class="n">float32</span><span class="p">)</span>

    <span class="c1"># Our approximation to the gradient now takes 2 inputs:
</span>        <span class="c1"># The noisy x.
</span>        <span class="c1"># The noise (index) level.
</span>    <span class="n">gradient</span> <span class="o">=</span> <span class="n">F</span><span class="p">([</span><span class="n">perturbed_x</span><span class="p">,</span> <span class="n">labels</span><span class="p">])</span> <span class="c1"># takes the label as embedding!
</span>
    <span class="n">loss</span> <span class="o">=</span> <span class="mi">1</span><span class="o">/</span><span class="mi">2</span> <span class="o">*</span> <span class="p">(</span><span class="n">tf</span><span class="p">.</span><span class="n">norm</span><span class="p">(</span><span class="n">gradient</span> <span class="o">-</span> <span class="n">target</span><span class="p">,</span> <span class="n">axis</span> <span class="o">=</span> <span class="mi">1</span><span class="p">))</span> <span class="o">*</span> <span class="n">used_sigmas</span> <span class="o">**</span> <span class="mi">2</span>
    
    <span class="k">return</span> <span class="n">tf</span><span class="p">.</span><span class="n">math</span><span class="p">.</span><span class="n">reduce_mean</span><span class="p">(</span><span class="n">loss</span><span class="p">)</span>
</code></pre></div></div>

<p>And we can train this:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="syntax"><code><span class="c1"># Now training loop
</span><span class="n">optimizer</span> <span class="o">=</span> <span class="n">keras</span><span class="p">.</span><span class="n">optimizers</span><span class="p">.</span><span class="n">Adam</span><span class="p">(</span><span class="n">learning_rate</span><span class="o">=</span> <span class="mf">1e-3</span><span class="p">)</span>

<span class="n">loss_l</span> <span class="o">=</span> <span class="p">[]</span>
<span class="n">epochs</span> <span class="o">=</span> <span class="n">tqdm</span><span class="p">(</span><span class="nb">range</span><span class="p">(</span><span class="mi">5000</span><span class="p">))</span>
<span class="k">for</span> <span class="n">t</span> <span class="ow">in</span> <span class="n">epochs</span><span class="p">:</span> 
    <span class="k">with</span> <span class="n">tf</span><span class="p">.</span><span class="n">GradientTape</span><span class="p">()</span> <span class="k">as</span> <span class="n">tape</span><span class="p">:</span>
        <span class="n">loss</span> <span class="o">=</span> <span class="n">conditional_noise_loss</span><span class="p">(</span><span class="n">F_model</span><span class="p">,</span> <span class="n">data</span><span class="p">)</span>

        <span class="n">model_grads</span> <span class="o">=</span> <span class="n">tape</span><span class="p">.</span><span class="n">gradient</span><span class="p">(</span><span class="n">loss</span><span class="p">,</span> <span class="n">F_model</span><span class="p">.</span><span class="n">trainable_weights</span><span class="p">)</span>

        <span class="n">optimizer</span><span class="p">.</span><span class="n">apply_gradients</span><span class="p">(</span><span class="nb">zip</span><span class="p">(</span><span class="n">model_grads</span><span class="p">,</span> <span class="n">F_model</span><span class="p">.</span><span class="n">trainable_weights</span><span class="p">))</span>
        

        <span class="n">loss_l</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">loss</span><span class="p">)</span>
        <span class="n">epochs</span><span class="p">.</span><span class="n">set_description</span><span class="p">(</span><span class="s">"Loss: %s"</span> <span class="o">%</span> <span class="n">loss_l</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">].</span><span class="n">numpy</span><span class="p">())</span>
        
<span class="n">F_model</span><span class="p">.</span><span class="n">save_weights</span><span class="p">(</span><span class="s">'./Denoising_conditional_weights.h5'</span><span class="p">)</span>
</code></pre></div></div>
<p>And once it is done we can again visualize the gradients:</p>

<p align="center">
  <img src="/assets/images/DiffusionModels/gradients_noise.png" alt="NoiseModel" />
</p>

<p>And to sample, we can use a fancier version of the ‘langevin_dynamics’ function we used before. It does the same but looping over different noise levels.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="syntax"><code><span class="k">def</span> <span class="nf">ald_sampling</span><span class="p">(</span><span class="n">F</span><span class="p">,</span> <span class="n">sigmas</span><span class="p">,</span> <span class="n">num_noises</span><span class="p">,</span> <span class="nb">iter</span><span class="p">,</span> <span class="n">step_size</span><span class="p">):</span>
    <span class="s">'''
    Sampling and visualization.

    '''</span>
    <span class="n">plot_gradients</span><span class="p">(</span><span class="n">F_model</span><span class="p">,</span> <span class="n">data</span><span class="p">)</span> <span class="c1"># Plot distribution landscape
</span>
    <span class="n">x_t</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">random</span><span class="p">.</span><span class="n">normal</span><span class="p">(</span><span class="n">size</span> <span class="o">=</span> <span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">))</span> <span class="c1"># Initial sample
</span>
    <span class="n">samples</span> <span class="o">=</span> <span class="p">[]</span> <span class="c1"># Placeholder
</span>
    <span class="c1"># Loop over noise levels:
</span>    <span class="k">for</span> <span class="n">noise_level</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">num_noises</span><span class="p">):</span>
        <span class="n">alpha</span> <span class="o">=</span> <span class="n">step_size</span> <span class="o">*</span> <span class="p">(</span><span class="n">sigmas</span><span class="p">[</span><span class="n">noise_level</span><span class="p">]</span><span class="o">**</span><span class="mi">2</span> <span class="o">/</span> <span class="n">sigmas</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span><span class="o">**</span><span class="mi">2</span><span class="p">)</span>
        <span class="c1"># noise level inner sampling:
</span>        <span class="k">for</span> <span class="n">t</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="nb">iter</span><span class="p">):</span>
            <span class="n">z</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">random</span><span class="p">.</span><span class="n">normal</span><span class="p">(</span><span class="n">size</span> <span class="o">=</span> <span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">))</span>
            <span class="n">gradient</span> <span class="o">=</span> <span class="n">F</span><span class="p">([</span><span class="n">x_t</span><span class="p">,</span> <span class="n">np</span><span class="p">.</span><span class="n">array</span><span class="p">([[</span><span class="n">noise_level</span><span class="p">]])]).</span><span class="n">numpy</span><span class="p">()</span>
            
            <span class="n">x_t</span> <span class="o">=</span> <span class="n">x_t</span> <span class="o">+</span> <span class="p">(</span><span class="n">alpha</span><span class="o">/</span><span class="mi">2</span><span class="p">)</span> <span class="o">*</span> <span class="n">gradient</span> <span class="o">+</span> <span class="n">np</span><span class="p">.</span><span class="n">sqrt</span><span class="p">(</span><span class="n">alpha</span><span class="p">)</span> <span class="o">*</span> <span class="n">z</span>
            <span class="n">samples</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">ravel</span><span class="p">(</span><span class="n">x_t</span><span class="p">))</span>

    <span class="c1"># Plot (given noise level) samples
</span>    <span class="n">color</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">array</span><span class="p">([[</span><span class="n">i</span><span class="p">]</span> <span class="o">*</span> <span class="nb">iter</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="n">sigmas</span><span class="p">]).</span><span class="n">ravel</span><span class="p">()</span>

    <span class="n">plt</span><span class="p">.</span><span class="n">scatter</span><span class="p">(</span><span class="o">*</span><span class="n">np</span><span class="p">.</span><span class="n">array</span><span class="p">(</span><span class="n">samples</span><span class="p">).</span><span class="n">T</span><span class="p">,</span> <span class="n">s</span><span class="o">=</span><span class="mi">250</span><span class="p">,</span> <span class="n">c</span> <span class="o">=</span> <span class="n">color</span><span class="p">)</span>

    <span class="n">samples</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">array</span><span class="p">(</span><span class="n">samples</span><span class="p">)</span>
    <span class="c1"># Draw arrrows
</span>    <span class="n">deltas</span> <span class="o">=</span> <span class="p">(</span><span class="n">samples</span><span class="p">[</span><span class="mi">1</span><span class="p">:]</span> <span class="o">-</span> <span class="n">samples</span><span class="p">[:</span><span class="o">-</span><span class="mi">1</span><span class="p">])</span> <span class="c1"># Difference
</span>    
    <span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="n">arrow</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">deltas</span><span class="p">):</span>
        <span class="n">plt</span><span class="p">.</span><span class="n">arrow</span><span class="p">(</span><span class="n">samples</span><span class="p">[</span><span class="n">i</span><span class="p">,</span><span class="mi">0</span><span class="p">],</span> <span class="n">samples</span><span class="p">[</span><span class="n">i</span><span class="p">,</span><span class="mi">1</span><span class="p">],</span> <span class="n">arrow</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="n">arrow</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span>
                    <span class="n">width</span><span class="o">=</span><span class="mf">1e-4</span><span class="p">,</span> <span class="n">head_width</span><span class="o">=</span><span class="mf">2e-2</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="s">"green"</span><span class="p">,</span> <span class="n">linewidth</span><span class="o">=</span><span class="mf">0.2</span><span class="p">)</span>
    <span class="n">plt</span><span class="p">.</span><span class="n">colorbar</span><span class="p">(</span><span class="n">fraction</span><span class="o">=</span><span class="mf">0.01</span><span class="p">,</span> <span class="n">pad</span><span class="o">=</span><span class="mf">0.01</span><span class="p">)</span>
    <span class="n">plt</span><span class="p">.</span><span class="n">show</span><span class="p">()</span>
    <span class="k">return</span> <span class="n">samples</span>

<span class="n">samples</span> <span class="o">=</span> <span class="n">ald_sampling</span><span class="p">(</span><span class="n">F_model</span><span class="p">,</span> <span class="n">sigmas</span><span class="p">,</span> <span class="n">num_noises</span><span class="p">,</span> <span class="mi">20</span><span class="p">,</span> <span class="mf">0.0001</span><span class="p">)</span>
</code></pre></div></div>

<p>Now we can see how when the level of noise decreases the samples converge to the actual data distribution. The colour indicates the amount of noise, from maximum (yellow) to minimum (purple).</p>

<p align="center">
  <img src="/assets/images/DiffusionModels/Sampling_noise_levels.png" alt="NoiseModel" />
</p>

<h3 id="estimating-the-gradient-for-mnist">Estimating the gradient for MNIST:</h3>

<p>MNIST is way more challenging, but the underlying principles are the same!</p>

<p>Data loading and scaling:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="syntax"><code><span class="k">def</span> <span class="nf">scale_image</span><span class="p">(</span><span class="n">image</span><span class="p">):</span>
    <span class="k">return</span> <span class="p">(</span><span class="n">image</span> <span class="o">-</span> <span class="p">(</span><span class="mi">255</span><span class="o">/</span><span class="mi">2</span><span class="p">))</span> <span class="o">/</span> <span class="p">(</span><span class="mi">255</span><span class="o">/</span><span class="mi">2</span><span class="p">)</span> <span class="c1"># -1 to 1 to make it easier
</span>
<span class="n">data_mnist</span> <span class="o">=</span> <span class="n">keras</span><span class="p">.</span><span class="n">datasets</span><span class="p">.</span><span class="n">mnist</span><span class="p">.</span><span class="n">load_data</span><span class="p">(</span><span class="n">path</span><span class="o">=</span><span class="s">"mnist.npz"</span><span class="p">)[</span><span class="mi">0</span><span class="p">][</span><span class="mi">0</span><span class="p">]</span>
<span class="n">data_mnist</span> <span class="o">=</span> <span class="n">scale_image</span><span class="p">(</span><span class="n">data_mnist</span><span class="p">[...,</span> <span class="n">np</span><span class="p">.</span><span class="n">newaxis</span><span class="p">])</span>
</code></pre></div></div>

<p>And now… we really need to go fancy with the model. An arbitrary network does not work, we need something with a proper inductive bias. Since we are concerned with the gradient at the pixel level (each of our dimensions) but still need to take information over the whole picture, a network designed for image segmentation is ideal. An option is RefineNet (the images are from the paper).</p>

<p align="center">
  <img src="/assets/images/DiffusionModels/Refine_net.PNG" alt="RefineNet" />
</p>

<p>The idea is to first downsample the data using a ResNet to 1/4, 1/8, 1/16 and 1/32 (in our case we begin in 1/1). The stride is typically set to 2, thus reducing the feature map resolution to one-half when passing from one block to the next.</p>

<p>After, it applies a multi path refinement (as shown in the image). The key point here is that the downsampling allows us to get general information about the picture while at the same time, eventually, focusing at the pixel level. Each RefineNet block takes a representation of the downsampled version and a higher resolution version until, in our case, reaching the pixel level</p>

<p>Nevertheless, there is still the issue of how to encode the \(\sigma\) level information. One option is to use <a href="https://paperswithcode.com/method/conditional-instance-normalization">“conditional instance normalization”</a>. Instance normalization consists basically on normalizing the feature maps per image.</p>

<p>Now, what this does is:
Let \(\mu_k\) and \(s_k\) denote mean and std of the k-th feature map of x (an image).</p>

\[z_k = \gamma[i, k]\frac{x_k - \mu_k}{s_k} + \beta[i, k]\]

<p>Where \(\gamma\) and \(\beta\) are learnable parameters. These parameters are embeddings of the noise level. The dimensionality of the embedding is such that there is a scalar for each channel. Hence, given k,  \(\gamma\) and \(\beta\) are scalars. Basically, we are doing sort of the same as before, scaling the output of the convolutional layers based on the embedding of the noise level.</p>

<p>Let’s code it. First instance normalization:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="syntax"><code><span class="k">class</span> <span class="nc">CIN</span><span class="p">(</span><span class="n">tf</span><span class="p">.</span><span class="n">keras</span><span class="p">.</span><span class="n">layers</span><span class="p">.</span><span class="n">Layer</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">num_noises</span><span class="p">,</span> <span class="n">num_features</span><span class="p">):</span>
        <span class="nb">super</span><span class="p">().</span><span class="n">__init__</span><span class="p">()</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">num_features</span> <span class="o">=</span> <span class="n">num_features</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">num_noises</span> <span class="o">=</span> <span class="n">num_noises</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">instance_norm</span> <span class="o">=</span> <span class="n">tfa</span><span class="p">.</span><span class="n">layers</span><span class="p">.</span><span class="n">InstanceNormalization</span><span class="p">()</span>
        
        <span class="bp">self</span><span class="p">.</span><span class="n">gamma</span> <span class="o">=</span> <span class="n">tf</span><span class="p">.</span><span class="n">keras</span><span class="p">.</span><span class="n">layers</span><span class="p">.</span><span class="n">Embedding</span><span class="p">(</span><span class="n">input_dim</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">num_noises</span><span class="p">,</span>
                                            <span class="n">output_dim</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">num_features</span><span class="p">,</span>
                                            <span class="n">embeddings_initializer</span> <span class="o">=</span> <span class="n">tf</span><span class="p">.</span><span class="n">keras</span><span class="p">.</span><span class="n">initializers</span><span class="p">.</span><span class="n">RandomNormal</span><span class="p">(</span><span class="mf">1.</span><span class="p">,</span> <span class="mf">0.02</span><span class="p">))</span>

        <span class="bp">self</span><span class="p">.</span><span class="n">beta</span> <span class="o">=</span> <span class="n">tf</span><span class="p">.</span><span class="n">keras</span><span class="p">.</span><span class="n">layers</span><span class="p">.</span><span class="n">Embedding</span><span class="p">(</span><span class="n">input_dim</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">num_noises</span><span class="p">,</span>
                                            <span class="n">output_dim</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">num_features</span><span class="p">,</span>
                                            <span class="n">embeddings_initializer</span> <span class="o">=</span> <span class="s">'Zeros'</span><span class="p">)</span>
        
    <span class="k">def</span> <span class="nf">call</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">image</span><span class="p">,</span> <span class="n">noise_level</span><span class="p">):</span>
        
        <span class="n">image_norm</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">instance_norm</span><span class="p">(</span><span class="n">image</span><span class="p">)</span> <span class="c1"># (B, height, width, num_features)
</span>
        <span class="c1"># Scalars
</span>        <span class="n">my_gamma</span> <span class="o">=</span> <span class="n">tf</span><span class="p">.</span><span class="n">expand_dims</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">gamma</span><span class="p">(</span><span class="n">noise_level</span><span class="p">),</span> <span class="n">axis</span> <span class="o">=</span> <span class="mi">1</span><span class="p">)</span> <span class="c1"># (B, 1, 1  num_features)
</span>        
        <span class="n">my_beta</span> <span class="o">=</span> <span class="n">tf</span><span class="p">.</span><span class="n">expand_dims</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">beta</span><span class="p">(</span><span class="n">noise_level</span><span class="p">),</span> <span class="n">axis</span> <span class="o">=</span> <span class="mi">1</span><span class="p">)</span><span class="c1"># (B, 1, 1, num_features)
</span>
        <span class="n">z</span> <span class="o">=</span> <span class="n">my_gamma</span> <span class="o">*</span> <span class="n">image_norm</span> <span class="o">+</span> <span class="n">my_beta</span> <span class="c1"># (B, height, width, num_features)
</span>
        <span class="k">return</span> <span class="n">z</span>
</code></pre></div></div>

<p>Let’s construct a ResNet:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="syntax"><code><span class="c1"># norm -&gt; non-linear -&gt; conv -&gt; norm -&gt; non-linear -&gt; conv -&gt; Downsample by 2
</span>
<span class="k">class</span> <span class="nc">ResNetBlock</span><span class="p">(</span><span class="n">tf</span><span class="p">.</span><span class="n">keras</span><span class="p">.</span><span class="n">layers</span><span class="p">.</span><span class="n">Layer</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">output_features</span><span class="p">,</span> <span class="n">num_noises</span><span class="p">,</span> <span class="n">downsampling</span> <span class="o">=</span> <span class="bp">True</span><span class="p">):</span>
        <span class="nb">super</span><span class="p">().</span><span class="n">__init__</span><span class="p">()</span>

        <span class="bp">self</span><span class="p">.</span><span class="n">downsampling</span> <span class="o">=</span> <span class="n">downsampling</span>
        
        <span class="bp">self</span><span class="p">.</span><span class="n">act</span> <span class="o">=</span> <span class="n">tf</span><span class="p">.</span><span class="n">keras</span><span class="p">.</span><span class="n">layers</span><span class="p">.</span><span class="n">ELU</span><span class="p">()</span>

        <span class="bp">self</span><span class="p">.</span><span class="n">embd1</span> <span class="o">=</span> <span class="n">CIN</span><span class="p">(</span><span class="n">num_noises</span><span class="p">,</span> <span class="n">output_features</span><span class="p">)</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">embd2</span> <span class="o">=</span> <span class="n">CIN</span><span class="p">(</span><span class="n">num_noises</span><span class="p">,</span> <span class="n">output_features</span><span class="p">)</span>

        <span class="bp">self</span><span class="p">.</span><span class="n">conv1</span> <span class="o">=</span> <span class="n">tf</span><span class="p">.</span><span class="n">keras</span><span class="p">.</span><span class="n">layers</span><span class="p">.</span><span class="n">Conv2D</span><span class="p">(</span><span class="n">output_features</span><span class="p">,</span>
                                                        <span class="n">kernel_size</span> <span class="o">=</span> <span class="mi">3</span><span class="p">,</span>
                                                        <span class="n">padding</span> <span class="o">=</span> <span class="s">'SAME'</span><span class="p">)</span>

        <span class="bp">self</span><span class="p">.</span><span class="n">conv2</span> <span class="o">=</span> <span class="n">tf</span><span class="p">.</span><span class="n">keras</span><span class="p">.</span><span class="n">layers</span><span class="p">.</span><span class="n">Conv2D</span><span class="p">(</span><span class="n">output_features</span><span class="p">,</span>
                                                        <span class="n">kernel_size</span> <span class="o">=</span> <span class="mi">3</span><span class="p">,</span>
                                                        <span class="n">padding</span> <span class="o">=</span> <span class="s">'SAME'</span><span class="p">)</span>

        <span class="bp">self</span><span class="p">.</span><span class="n">down</span> <span class="o">=</span>  <span class="n">tf</span><span class="p">.</span><span class="n">keras</span><span class="p">.</span><span class="n">layers</span><span class="p">.</span><span class="n">Conv2D</span><span class="p">(</span><span class="n">output_features</span><span class="p">,</span>
                                                        <span class="n">kernel_size</span> <span class="o">=</span> <span class="mi">3</span><span class="p">,</span>
                                                        <span class="n">strides</span> <span class="o">=</span> <span class="mi">2</span><span class="p">,</span>
                                                        <span class="n">padding</span> <span class="o">=</span> <span class="s">'SAME'</span><span class="p">)</span>
       
    <span class="k">def</span> <span class="nf">call</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">image</span><span class="p">,</span> <span class="n">noise_level</span><span class="p">):</span>
        
        <span class="n">h</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">embd1</span><span class="p">(</span><span class="n">image</span><span class="p">,</span> <span class="n">noise_level</span><span class="p">)</span>
        <span class="n">h</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">act</span><span class="p">(</span><span class="n">h</span><span class="p">)</span>
        <span class="n">h</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">conv1</span><span class="p">(</span><span class="n">h</span><span class="p">)</span>

        <span class="n">h</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">embd2</span><span class="p">(</span><span class="n">h</span><span class="p">,</span> <span class="n">noise_level</span><span class="p">)</span>
        <span class="n">h</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">act</span><span class="p">(</span><span class="n">h</span><span class="p">)</span>
        <span class="n">h</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">conv2</span><span class="p">(</span><span class="n">h</span><span class="p">)</span>

        <span class="k">if</span> <span class="bp">self</span><span class="p">.</span><span class="n">downsampling</span><span class="p">:</span>
            <span class="k">return</span> <span class="bp">self</span><span class="p">.</span><span class="n">down</span><span class="p">(</span><span class="n">image</span> <span class="o">+</span> <span class="n">h</span><span class="p">)</span>
        <span class="k">else</span><span class="p">:</span>
            <span class="k">return</span> <span class="n">image</span> <span class="o">+</span> <span class="n">h</span>
</code></pre></div></div>
<p>Ok, now let’s deal with RefineNet:</p>

<p align="center">
  <img src="/assets/images/DiffusionModels/Refine_parts.PNG" alt="RefineParts" />
</p>

<p>It consists of a residual convolutional unit, multi-resolution fusion and chained residual pooling.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="syntax"><code><span class="c1"># Residual convolutional Unit
</span><span class="k">class</span> <span class="nc">RCU</span><span class="p">(</span><span class="n">tf</span><span class="p">.</span><span class="n">keras</span><span class="p">.</span><span class="n">layers</span><span class="p">.</span><span class="n">Layer</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">input_features</span><span class="p">,</span> <span class="n">num_noises</span><span class="p">):</span>
        <span class="nb">super</span><span class="p">().</span><span class="n">__init__</span><span class="p">()</span>
        
        <span class="bp">self</span><span class="p">.</span><span class="n">Embedding1</span> <span class="o">=</span> <span class="n">CIN</span><span class="p">(</span><span class="n">num_noises</span><span class="p">,</span> <span class="n">input_features</span><span class="p">)</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">Convolution1</span> <span class="o">=</span> <span class="n">tf</span><span class="p">.</span><span class="n">keras</span><span class="p">.</span><span class="n">layers</span><span class="p">.</span><span class="n">Conv2D</span><span class="p">(</span><span class="n">input_features</span><span class="p">,</span>
                                                    <span class="n">kernel_size</span> <span class="o">=</span> <span class="mi">3</span><span class="p">,</span>
                                                    <span class="n">activation</span> <span class="o">=</span> <span class="s">'ELU'</span><span class="p">,</span>
                                                    <span class="n">padding</span> <span class="o">=</span> <span class="s">'SAME'</span><span class="p">)</span>

        <span class="bp">self</span><span class="p">.</span><span class="n">Embedding2</span> <span class="o">=</span> <span class="n">CIN</span><span class="p">(</span><span class="n">num_noises</span><span class="p">,</span> <span class="n">input_features</span><span class="p">)</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">Convolution2</span> <span class="o">=</span> <span class="n">tf</span><span class="p">.</span><span class="n">keras</span><span class="p">.</span><span class="n">layers</span><span class="p">.</span><span class="n">Conv2D</span><span class="p">(</span><span class="n">input_features</span><span class="p">,</span> 
                                                    <span class="n">kernel_size</span> <span class="o">=</span> <span class="mi">3</span><span class="p">,</span>
                                                    <span class="n">padding</span> <span class="o">=</span> <span class="s">'SAME'</span><span class="p">)</span>

        <span class="bp">self</span><span class="p">.</span><span class="n">first_act</span> <span class="o">=</span> <span class="n">tf</span><span class="p">.</span><span class="n">keras</span><span class="p">.</span><span class="n">layers</span><span class="p">.</span><span class="n">ELU</span><span class="p">()</span>

    <span class="k">def</span> <span class="nf">call</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">image</span><span class="p">,</span> <span class="n">noise_level</span><span class="p">):</span>
        
        <span class="n">res</span> <span class="o">=</span> <span class="n">image</span>

        <span class="n">x</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">first_act</span><span class="p">(</span><span class="n">image</span><span class="p">)</span>
        <span class="n">x</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">Embedding1</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">noise_level</span><span class="p">)</span>
        <span class="n">x</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">Convolution1</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>
        <span class="n">x</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">Embedding2</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">noise_level</span><span class="p">)</span>
        <span class="n">x</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">Convolution2</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>
        
       
        <span class="k">return</span> <span class="n">res</span> <span class="o">+</span> <span class="n">x</span> 

<span class="c1"># Now the multi resolution thing:
</span><span class="k">class</span> <span class="nc">MRF</span><span class="p">(</span><span class="n">tf</span><span class="p">.</span><span class="n">keras</span><span class="p">.</span><span class="n">layers</span><span class="p">.</span><span class="n">Layer</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">im_in</span><span class="p">,</span> <span class="n">input_features</span><span class="p">,</span> <span class="n">num_noises</span><span class="p">,</span> <span class="n">shape_target</span><span class="p">):</span>
        <span class="nb">super</span><span class="p">().</span><span class="n">__init__</span><span class="p">()</span>
        
        <span class="bp">self</span><span class="p">.</span><span class="n">shape_target</span> <span class="o">=</span> <span class="n">shape_target</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">im_in</span> <span class="o">=</span> <span class="n">im_in</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">embeddings</span> <span class="o">=</span> <span class="p">[]</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">Conv</span> <span class="o">=</span> <span class="p">[]</span>


        <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">im_in</span><span class="p">):</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">embeddings</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">CIN</span><span class="p">(</span><span class="n">num_noises</span><span class="p">,</span> <span class="n">input_features</span><span class="p">))</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">Conv</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">tf</span><span class="p">.</span><span class="n">keras</span><span class="p">.</span><span class="n">layers</span><span class="p">.</span><span class="n">Conv2D</span><span class="p">(</span><span class="n">input_features</span><span class="p">,</span>
                                                        <span class="n">kernel_size</span> <span class="o">=</span> <span class="mi">3</span><span class="p">,</span>
                                                        <span class="n">padding</span> <span class="o">=</span> <span class="s">'SAME'</span><span class="p">))</span>
            
    
    <span class="k">def</span> <span class="nf">call</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">images</span><span class="p">,</span> <span class="n">noise_level</span><span class="p">):</span>

        
        <span class="k">if</span>  <span class="bp">self</span><span class="p">.</span><span class="n">im_in</span> <span class="o">==</span> <span class="mi">1</span><span class="p">:</span>
            <span class="n">h</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">embeddings</span><span class="p">[</span><span class="mi">0</span><span class="p">](</span><span class="n">images</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="n">noise_level</span><span class="p">)</span>
            <span class="n">h</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">Conv</span><span class="p">[</span><span class="mi">0</span><span class="p">](</span><span class="n">h</span><span class="p">)</span>
            
            <span class="n">h</span> <span class="o">=</span> <span class="n">tf</span><span class="p">.</span><span class="n">image</span><span class="p">.</span><span class="n">resize</span><span class="p">(</span><span class="n">h</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">shape_target</span><span class="p">[:</span><span class="mi">2</span><span class="p">])</span> 
            
            <span class="k">return</span> <span class="n">h</span>

        <span class="k">else</span><span class="p">:</span>
            
            <span class="n">h1</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">embeddings</span><span class="p">[</span><span class="mi">0</span><span class="p">](</span><span class="n">images</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="n">noise_level</span><span class="p">)</span>
            <span class="n">h1</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">Conv</span><span class="p">[</span><span class="mi">0</span><span class="p">](</span><span class="n">h1</span><span class="p">)</span>
            
            <span class="n">h1</span> <span class="o">=</span> <span class="n">tf</span><span class="p">.</span><span class="n">image</span><span class="p">.</span><span class="n">resize</span><span class="p">(</span><span class="n">h1</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">shape_target</span><span class="p">[:</span><span class="mi">2</span><span class="p">])</span> <span class="c1"># Resizes, if needed, to target
</span>            <span class="c1">#Upsmaples using bilinear interpolation
</span>            <span class="n">h2</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">embeddings</span><span class="p">[</span><span class="mi">1</span><span class="p">](</span><span class="n">images</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span> <span class="n">noise_level</span><span class="p">)</span>
            <span class="n">h2</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">Conv</span><span class="p">[</span><span class="mi">1</span><span class="p">](</span><span class="n">h2</span><span class="p">)</span>
            
            <span class="n">h2</span> <span class="o">=</span> <span class="n">tf</span><span class="p">.</span><span class="n">image</span><span class="p">.</span><span class="n">resize</span><span class="p">(</span><span class="n">h2</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">shape_target</span><span class="p">[:</span><span class="mi">2</span><span class="p">])</span> 
            <span class="n">sums</span> <span class="o">=</span> <span class="n">h1</span> <span class="o">+</span> <span class="n">h2</span>

            <span class="k">return</span> <span class="n">sums</span>

<span class="c1"># Chained residual pooling
</span><span class="k">class</span> <span class="nc">CRP</span><span class="p">(</span><span class="n">tf</span><span class="p">.</span><span class="n">keras</span><span class="p">.</span><span class="n">layers</span><span class="p">.</span><span class="n">Layer</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">input_features</span><span class="p">,</span> <span class="n">num_noises</span><span class="p">,</span> <span class="n">n_blocks</span> <span class="o">=</span> <span class="mi">2</span><span class="p">):</span>
        <span class="nb">super</span><span class="p">().</span><span class="n">__init__</span><span class="p">()</span>
       
        <span class="bp">self</span><span class="p">.</span><span class="n">embeddings</span> <span class="o">=</span> <span class="p">[]</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">conv</span> <span class="o">=</span> <span class="p">[]</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">avg_pool</span> <span class="o">=</span> <span class="p">[]</span>

        <span class="bp">self</span><span class="p">.</span><span class="n">n_blocks</span> <span class="o">=</span> <span class="n">n_blocks</span>
        <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">n_blocks</span><span class="p">):</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">embeddings</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">CIN</span><span class="p">(</span><span class="n">num_noises</span><span class="p">,</span> <span class="n">input_features</span><span class="p">))</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">avg_pool</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">tf</span><span class="p">.</span><span class="n">keras</span><span class="p">.</span><span class="n">layers</span><span class="p">.</span><span class="n">AveragePooling2D</span><span class="p">(</span><span class="n">pool_size</span> <span class="o">=</span> <span class="mi">5</span><span class="p">,</span>
                                                                  <span class="n">padding</span> <span class="o">=</span> <span class="s">'SAME'</span><span class="p">,</span>
                                                                  <span class="n">strides</span> <span class="o">=</span> <span class="mi">1</span><span class="p">))</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">conv</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">tf</span><span class="p">.</span><span class="n">keras</span><span class="p">.</span><span class="n">layers</span><span class="p">.</span><span class="n">Conv2D</span><span class="p">(</span><span class="n">input_features</span><span class="p">,</span>
                                                        <span class="n">kernel_size</span> <span class="o">=</span> <span class="mi">3</span><span class="p">,</span>
                                                        <span class="n">padding</span> <span class="o">=</span> <span class="s">'SAME'</span><span class="p">))</span>

            <span class="bp">self</span><span class="p">.</span><span class="n">first_act</span> <span class="o">=</span> <span class="n">tf</span><span class="p">.</span><span class="n">keras</span><span class="p">.</span><span class="n">layers</span><span class="p">.</span><span class="n">ELU</span><span class="p">()</span>
    
    <span class="k">def</span> <span class="nf">call</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">image</span><span class="p">,</span> <span class="n">noise_level</span><span class="p">):</span>

        <span class="n">x</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">first_act</span><span class="p">(</span><span class="n">image</span><span class="p">)</span>
        
        <span class="nb">sum</span> <span class="o">=</span> <span class="n">x</span>

        <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">n_blocks</span><span class="p">):</span>
            <span class="n">x</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">embeddings</span><span class="p">[</span><span class="n">i</span><span class="p">](</span><span class="n">x</span><span class="p">,</span> <span class="n">noise_level</span><span class="p">)</span>
            <span class="n">x</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">avg_pool</span><span class="p">[</span><span class="n">i</span><span class="p">](</span><span class="n">x</span><span class="p">)</span>
            <span class="n">x</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">conv</span><span class="p">[</span><span class="n">i</span><span class="p">](</span><span class="n">x</span><span class="p">)</span>

            <span class="nb">sum</span> <span class="o">=</span> <span class="n">x</span> <span class="o">+</span> <span class="nb">sum</span>

        <span class="k">return</span> <span class="nb">sum</span>

</code></pre></div></div>

<p>So a block of RefineNet:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="syntax"><code><span class="k">class</span> <span class="nc">RefineNetBlock</span><span class="p">(</span><span class="n">tf</span><span class="p">.</span><span class="n">keras</span><span class="p">.</span><span class="n">layers</span><span class="p">.</span><span class="n">Layer</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">im_in</span><span class="p">,</span> <span class="n">input_features</span><span class="p">,</span> <span class="n">num_noises</span><span class="p">,</span> <span class="n">shape_target</span><span class="p">):</span>
        <span class="nb">super</span><span class="p">().</span><span class="n">__init__</span><span class="p">()</span>

        <span class="bp">self</span><span class="p">.</span><span class="n">RCUBig1</span> <span class="o">=</span> <span class="n">RCU</span><span class="p">(</span><span class="n">input_features</span><span class="p">,</span> <span class="n">num_noises</span><span class="p">)</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">RCUBig2</span> <span class="o">=</span> <span class="n">RCU</span><span class="p">(</span><span class="n">input_features</span><span class="p">,</span> <span class="n">num_noises</span><span class="p">)</span>

        <span class="k">if</span> <span class="n">im_in</span> <span class="o">==</span> <span class="mi">2</span><span class="p">:</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">RCUSmall1</span> <span class="o">=</span> <span class="n">RCU</span><span class="p">(</span><span class="n">input_features</span><span class="p">,</span> <span class="n">num_noises</span><span class="p">)</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">RCUSmall2</span> <span class="o">=</span> <span class="n">RCU</span><span class="p">(</span><span class="n">input_features</span><span class="p">,</span> <span class="n">num_noises</span><span class="p">)</span>

        <span class="bp">self</span><span class="p">.</span><span class="n">MRF</span> <span class="o">=</span> <span class="n">MRF</span><span class="p">(</span><span class="n">im_in</span><span class="p">,</span> <span class="n">input_features</span><span class="p">,</span> <span class="n">num_noises</span><span class="p">,</span> <span class="n">shape_target</span><span class="p">)</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">CRP</span> <span class="o">=</span> <span class="n">CRP</span><span class="p">(</span><span class="n">input_features</span><span class="p">,</span> <span class="n">num_noises</span><span class="p">)</span>

        <span class="bp">self</span><span class="p">.</span><span class="n">final_conv</span> <span class="o">=</span> <span class="n">RCU</span><span class="p">(</span><span class="n">input_features</span><span class="p">,</span> <span class="n">num_noises</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">call</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">image_big</span><span class="p">,</span> <span class="n">image_small</span><span class="p">,</span> <span class="n">noise_level</span><span class="p">):</span>

        <span class="n">image_big_processed</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">RCUBig1</span><span class="p">(</span><span class="n">image_big</span><span class="p">,</span> <span class="n">noise_level</span><span class="p">)</span>
        <span class="n">image_big_processed</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">RCUBig2</span><span class="p">(</span><span class="n">image_big_processed</span><span class="p">,</span> <span class="n">noise_level</span><span class="p">)</span>
        
        <span class="k">if</span> <span class="n">image_small</span> <span class="ow">is</span> <span class="ow">not</span> <span class="bp">None</span><span class="p">:</span>
            <span class="n">image_small_processed</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">RCUSmall1</span><span class="p">(</span><span class="n">image_small</span><span class="p">,</span> <span class="n">noise_level</span><span class="p">)</span>
            <span class="n">image_small_processed</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">RCUSmall2</span><span class="p">(</span><span class="n">image_small_processed</span><span class="p">,</span> <span class="n">noise_level</span><span class="p">)</span>

            <span class="n">x</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">MRF</span><span class="p">([</span><span class="n">image_big_processed</span><span class="p">,</span> <span class="n">image_small_processed</span><span class="p">],</span> <span class="n">noise_level</span><span class="p">)</span>
        <span class="k">else</span><span class="p">:</span>
            <span class="n">x</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">MRF</span><span class="p">([</span><span class="n">image_big_processed</span><span class="p">],</span> <span class="n">noise_level</span><span class="p">)</span>
        
        <span class="n">x</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">CRP</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">noise_level</span><span class="p">)</span>
        <span class="n">x</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">final_conv</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">noise_level</span><span class="p">)</span>

        <span class="k">return</span> <span class="n">x</span>
</code></pre></div></div>

<p>Indeed, the network is quite complex (and this is nothing!). Let’s put everything together.</p>

<p>Naturally, we do not want to work with the image in 1/4 but in 1/1 so in the first ResNet we do not downsample the output. Hence, the downsampling process goes (28, 28) -&gt; (14, 14) -&gt; (7, 7) -&gt; (4, 4). We do not have to worry about the upsampling process since it is taken care of by the bilinear interpolation, which is much more flexible than deconvolutions.</p>

<p>The only thing left is to construct the model and train! All the rest is the same as before in the 2D case!</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="syntax"><code><span class="k">def</span> <span class="nf">make_model</span><span class="p">(</span><span class="n">n_filters</span><span class="p">,</span> <span class="n">num_noises</span><span class="p">):</span>
    <span class="n">Input_image</span> <span class="o">=</span> <span class="n">keras</span><span class="p">.</span><span class="n">Input</span><span class="p">(</span><span class="n">shape</span><span class="o">=</span><span class="p">(</span><span class="mi">28</span><span class="p">,</span> <span class="mi">28</span><span class="p">,</span> <span class="mi">1</span><span class="p">))</span>
    <span class="n">Input_label</span> <span class="o">=</span> <span class="n">keras</span><span class="p">.</span><span class="n">Input</span><span class="p">(</span><span class="n">shape</span><span class="o">=</span><span class="p">(</span><span class="mi">1</span><span class="p">,))</span>
   
    <span class="n">res1</span> <span class="o">=</span> <span class="n">ResNetBlock</span><span class="p">(</span><span class="n">n_filters</span><span class="p">,</span> <span class="n">num_noises</span><span class="o">=</span> <span class="n">num_noises</span><span class="p">,</span> <span class="n">downsampling</span> <span class="o">=</span> <span class="bp">False</span><span class="p">)(</span><span class="n">Input_image</span><span class="p">,</span> <span class="n">Input_label</span><span class="p">)</span>
    <span class="n">res2</span> <span class="o">=</span> <span class="n">ResNetBlock</span><span class="p">(</span><span class="n">n_filters</span><span class="p">,</span> <span class="n">num_noises</span><span class="o">=</span> <span class="n">num_noises</span><span class="p">)(</span><span class="n">res1</span><span class="p">,</span> <span class="n">Input_label</span><span class="p">)</span>
    <span class="n">res3</span> <span class="o">=</span> <span class="n">ResNetBlock</span><span class="p">(</span><span class="n">n_filters</span><span class="p">,</span> <span class="n">num_noises</span><span class="o">=</span> <span class="n">num_noises</span><span class="p">)(</span><span class="n">res2</span><span class="p">,</span> <span class="n">Input_label</span><span class="p">)</span>
    <span class="n">res4</span> <span class="o">=</span> <span class="n">ResNetBlock</span><span class="p">(</span><span class="n">n_filters</span><span class="p">,</span> <span class="n">num_noises</span><span class="o">=</span> <span class="n">num_noises</span><span class="p">)(</span><span class="n">res3</span><span class="p">,</span> <span class="n">Input_label</span><span class="p">)</span>

    <span class="n">RefineNet_4</span> <span class="o">=</span> <span class="n">RefineNetBlock</span><span class="p">(</span><span class="n">im_in</span><span class="o">=</span> <span class="mi">1</span><span class="p">,</span>
                                <span class="n">input_features</span> <span class="o">=</span> <span class="n">n_filters</span><span class="p">,</span>
                                <span class="n">num_noises</span><span class="o">=</span> <span class="n">num_noises</span><span class="p">,</span>
                                <span class="n">shape_target</span><span class="o">=</span> <span class="p">(</span><span class="mi">4</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">1</span><span class="p">))(</span><span class="n">image_big</span> <span class="o">=</span> <span class="n">res4</span><span class="p">,</span>
                                                        <span class="n">image_small</span> <span class="o">=</span> <span class="bp">None</span><span class="p">,</span>
                                                        <span class="n">noise_level</span> <span class="o">=</span> <span class="n">Input_label</span><span class="p">)</span>

    <span class="n">RefineNet_3</span> <span class="o">=</span> <span class="n">RefineNetBlock</span><span class="p">(</span><span class="n">im_in</span><span class="o">=</span> <span class="mi">2</span><span class="p">,</span>
                                <span class="n">input_features</span> <span class="o">=</span> <span class="n">n_filters</span><span class="p">,</span>
                                <span class="n">num_noises</span><span class="o">=</span> <span class="n">num_noises</span><span class="p">,</span>
                                <span class="n">shape_target</span><span class="o">=</span> <span class="p">(</span><span class="mi">7</span><span class="p">,</span> <span class="mi">7</span><span class="p">,</span> <span class="mi">1</span><span class="p">))(</span><span class="n">image_big</span> <span class="o">=</span> <span class="n">res3</span><span class="p">,</span>
                                                        <span class="n">image_small</span> <span class="o">=</span> <span class="n">RefineNet_4</span><span class="p">,</span>
                                                        <span class="n">noise_level</span> <span class="o">=</span> <span class="n">Input_label</span><span class="p">)</span>

    <span class="n">RefineNet_2</span> <span class="o">=</span> <span class="n">RefineNetBlock</span><span class="p">(</span><span class="n">im_in</span><span class="o">=</span> <span class="mi">2</span><span class="p">,</span>
                                <span class="n">input_features</span> <span class="o">=</span> <span class="n">n_filters</span><span class="p">,</span>
                                <span class="n">num_noises</span><span class="o">=</span> <span class="n">num_noises</span><span class="p">,</span>
                                <span class="n">shape_target</span><span class="o">=</span> <span class="p">(</span><span class="mi">14</span><span class="p">,</span> <span class="mi">14</span><span class="p">,</span> <span class="mi">1</span><span class="p">))(</span><span class="n">image_big</span> <span class="o">=</span> <span class="n">res2</span><span class="p">,</span>
                                                        <span class="n">image_small</span> <span class="o">=</span> <span class="n">RefineNet_3</span><span class="p">,</span>
                                                        <span class="n">noise_level</span> <span class="o">=</span> <span class="n">Input_label</span><span class="p">)</span>

    <span class="n">RefineNet_1</span> <span class="o">=</span> <span class="n">RefineNetBlock</span><span class="p">(</span><span class="n">im_in</span><span class="o">=</span> <span class="mi">2</span><span class="p">,</span>
                                <span class="n">input_features</span> <span class="o">=</span> <span class="n">n_filters</span><span class="p">,</span>
                                <span class="n">num_noises</span><span class="o">=</span> <span class="n">num_noises</span><span class="p">,</span>
                                <span class="n">shape_target</span><span class="o">=</span> <span class="p">(</span><span class="mi">28</span><span class="p">,</span> <span class="mi">28</span><span class="p">,</span> <span class="mi">1</span><span class="p">))(</span><span class="n">image_big</span> <span class="o">=</span> <span class="n">res1</span><span class="p">,</span>
                                                        <span class="n">image_small</span> <span class="o">=</span> <span class="n">RefineNet_2</span><span class="p">,</span>
                                                        <span class="n">noise_level</span> <span class="o">=</span> <span class="n">Input_label</span><span class="p">)</span>


    <span class="c1">#And eventually just a linear combination of the features to map the dimensionality of the input:
</span>
    <span class="n">final_conv</span> <span class="o">=</span> <span class="n">tf</span><span class="p">.</span><span class="n">keras</span><span class="p">.</span><span class="n">layers</span><span class="p">.</span><span class="n">Conv2D</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="n">strides</span><span class="o">=</span> <span class="mi">1</span><span class="p">)(</span><span class="n">RefineNet_1</span><span class="p">)</span>

    <span class="n">F_model</span> <span class="o">=</span> <span class="n">tf</span><span class="p">.</span><span class="n">keras</span><span class="p">.</span><span class="n">Model</span><span class="p">([</span><span class="n">Input_image</span><span class="p">,</span> <span class="n">Input_label</span><span class="p">],</span> <span class="n">final_conv</span><span class="p">)</span>

    <span class="k">return</span> <span class="n">F_model</span>

<span class="n">F_model</span> <span class="o">=</span> <span class="n">make_model</span><span class="p">(</span><span class="mi">64</span><span class="p">,</span> <span class="mi">10</span><span class="p">)</span>
</code></pre></div></div>

<p>This takes a bit more to train so the weights are available <a href="/assets/weights/DiffusionModels/Denoising_conditional_weights_MNIST_RefineNet_18_10.h5">here</a></p>

<p>The training loop:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="syntax"><code><span class="c1"># Now training loop 
</span><span class="n">optimizer</span> <span class="o">=</span> <span class="n">keras</span><span class="p">.</span><span class="n">optimizers</span><span class="p">.</span><span class="n">Adam</span><span class="p">(</span><span class="n">learning_rate</span><span class="o">=</span> <span class="mf">1e-3</span><span class="p">)</span>

<span class="n">loss_l</span> <span class="o">=</span> <span class="p">[]</span>
<span class="n">batch_size</span> <span class="o">=</span> <span class="mi">32</span>

<span class="c1"># Epochs loop
</span><span class="k">for</span> <span class="n">t</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">50</span><span class="p">):</span> 
    <span class="n">epoch_loss</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="c1"># Batches loop:
</span>    <span class="k">for</span> <span class="n">b</span> <span class="ow">in</span> <span class="n">tqdm</span><span class="p">(</span><span class="nb">range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="p">(</span><span class="n">data_mnist</span><span class="p">.</span><span class="n">shape</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">-</span> <span class="n">batch_size</span><span class="p">),</span> <span class="n">batch_size</span><span class="p">)):</span>
        
        <span class="n">data</span> <span class="o">=</span> <span class="n">data_mnist</span><span class="p">[</span><span class="n">b</span><span class="p">:</span> <span class="n">b</span> <span class="o">+</span> <span class="n">batch_size</span><span class="p">]</span>
        <span class="n">loss_batch</span> <span class="o">=</span> <span class="p">[]</span>

        <span class="n">labels</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">random</span><span class="p">.</span><span class="n">randint</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">num_noises</span><span class="p">,</span> <span class="n">data</span><span class="p">.</span><span class="n">shape</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span>
        
        <span class="k">with</span> <span class="n">tf</span><span class="p">.</span><span class="n">GradientTape</span><span class="p">()</span> <span class="k">as</span> <span class="n">tape</span><span class="p">:</span>
            <span class="n">loss</span> <span class="o">=</span> <span class="n">conditional_noise_loss</span><span class="p">(</span><span class="n">F_model</span><span class="p">,</span> <span class="n">data</span><span class="p">,</span> <span class="n">labels</span> <span class="o">=</span> <span class="n">labels</span><span class="p">)</span>
            <span class="n">loss_batch</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">loss</span><span class="p">)</span>
         

            <span class="n">model_grads</span> <span class="o">=</span> <span class="n">tape</span><span class="p">.</span><span class="n">gradient</span><span class="p">(</span><span class="n">loss</span><span class="p">,</span> <span class="n">F_model</span><span class="p">.</span><span class="n">trainable_weights</span><span class="p">)</span>

            <span class="n">optimizer</span><span class="p">.</span><span class="n">apply_gradients</span><span class="p">(</span><span class="nb">zip</span><span class="p">(</span><span class="n">model_grads</span><span class="p">,</span> <span class="n">F_model</span><span class="p">.</span><span class="n">trainable_weights</span><span class="p">))</span>
            
    
    <span class="n">loss_l</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">mean</span><span class="p">(</span><span class="n">loss_batch</span><span class="p">))</span>
</code></pre></div></div>

<p>And a sampler adapted to the MNIST:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="syntax"><code><span class="k">def</span> <span class="nf">ald_sampling_mnist</span><span class="p">(</span><span class="n">F</span><span class="p">,</span> <span class="n">sigmas</span><span class="p">,</span> <span class="n">num_noises</span><span class="p">,</span> <span class="nb">iter</span><span class="p">,</span> <span class="n">step_size</span><span class="p">,</span> <span class="n">num_samples</span> <span class="o">=</span> <span class="mi">10</span><span class="p">):</span>

    <span class="n">x_t</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">random</span><span class="p">.</span><span class="n">uniform</span><span class="p">(</span><span class="n">low</span> <span class="o">=</span> <span class="o">-</span><span class="mf">1.</span><span class="p">,</span> <span class="n">high</span> <span class="o">=</span> <span class="mi">1</span><span class="p">,</span> <span class="n">size</span> <span class="o">=</span> <span class="p">(</span><span class="n">num_samples</span><span class="p">,</span> <span class="mi">28</span><span class="p">,</span> <span class="mi">28</span><span class="p">,</span> <span class="mi">1</span><span class="p">))</span> <span class="c1"># Initial sample
</span>
    <span class="n">samples</span> <span class="o">=</span> <span class="p">[]</span> <span class="c1"># Placeholder
</span>
    <span class="c1"># Loop over noise levels:
</span>    <span class="k">for</span> <span class="n">noise_level</span> <span class="ow">in</span> <span class="n">tqdm</span><span class="p">(</span><span class="nb">range</span><span class="p">(</span><span class="n">num_noises</span><span class="p">)):</span>
        <span class="c1">#print(f'noise level {noise_level}')
</span>        <span class="n">alpha</span> <span class="o">=</span> <span class="n">step_size</span> <span class="o">*</span> <span class="p">(</span><span class="n">sigmas</span><span class="p">[</span><span class="n">noise_level</span><span class="p">]</span><span class="o">**</span><span class="mi">2</span> <span class="o">/</span> <span class="n">sigmas</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span><span class="o">**</span><span class="mi">2</span><span class="p">)</span>
        <span class="c1"># noise level inner sampling:
</span>        <span class="k">for</span> <span class="n">t</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="nb">iter</span><span class="p">):</span>
            <span class="n">z</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">random</span><span class="p">.</span><span class="n">normal</span><span class="p">(</span><span class="n">size</span> <span class="o">=</span> <span class="p">(</span><span class="n">num_samples</span><span class="p">,</span> <span class="mi">28</span><span class="p">,</span> <span class="mi">28</span><span class="p">,</span> <span class="mi">1</span><span class="p">))</span>
            <span class="n">gradient</span> <span class="o">=</span> <span class="n">F</span><span class="p">([</span><span class="n">x_t</span><span class="p">,</span> <span class="n">np</span><span class="p">.</span><span class="n">array</span><span class="p">([[</span><span class="n">noise_level</span><span class="p">]</span> <span class="o">*</span> <span class="n">num_samples</span><span class="p">]).</span><span class="n">T</span><span class="p">]).</span><span class="n">numpy</span><span class="p">()</span>
            
            <span class="n">x_t</span> <span class="o">=</span> <span class="n">x_t</span> <span class="o">+</span> <span class="p">(</span><span class="n">alpha</span><span class="o">/</span><span class="mi">2</span><span class="p">)</span> <span class="o">*</span> <span class="n">gradient</span> <span class="o">+</span> <span class="n">np</span><span class="p">.</span><span class="n">sqrt</span><span class="p">(</span><span class="n">alpha</span><span class="p">)</span> <span class="o">*</span> <span class="n">z</span>
            <span class="n">samples</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">x_t</span><span class="p">)</span>

    <span class="k">return</span> <span class="n">samples</span>
</code></pre></div></div>

<p>Run it:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="syntax"><code><span class="nb">iter</span> <span class="o">=</span> <span class="mi">100</span>
<span class="n">num_samples</span> <span class="o">=</span> <span class="mi">64</span>
<span class="n">samples</span> <span class="o">=</span> <span class="n">ald_sampling_mnist</span><span class="p">(</span><span class="n">F_model</span><span class="p">,</span> <span class="n">sigmas</span><span class="p">,</span> <span class="n">num_noises</span><span class="p">,</span> <span class="nb">iter</span> <span class="o">=</span> <span class="nb">iter</span><span class="p">,</span> <span class="n">step_size</span> <span class="o">=</span> <span class="mf">2e-5</span><span class="p">,</span> <span class="n">num_samples</span> <span class="o">=</span> <span class="n">num_samples</span><span class="p">)</span>
</code></pre></div></div>

<p>And now some plotting…</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="syntax"><code><span class="n">plt</span><span class="p">.</span><span class="n">figure</span><span class="p">(</span><span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span> <span class="nb">int</span><span class="p">(</span><span class="n">num_samples</span> <span class="o">*</span> <span class="mf">1.2</span><span class="p">)))</span>

<span class="k">for</span> <span class="n">row</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">samples</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">shape</span><span class="p">[</span><span class="mi">0</span><span class="p">]):</span>
    <span class="k">for</span> <span class="n">j</span><span class="p">,</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="nb">range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">samples</span><span class="p">),</span> <span class="nb">iter</span><span class="p">)):</span>
        <span class="n">plt</span><span class="p">.</span><span class="n">subplot</span><span class="p">(</span><span class="n">samples</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">shape</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="nb">len</span><span class="p">(</span><span class="n">samples</span><span class="p">)</span> <span class="o">//</span> <span class="nb">iter</span><span class="p">,</span> <span class="mi">1</span> <span class="o">+</span> <span class="n">j</span> <span class="o">+</span> <span class="n">row</span> <span class="o">*</span> <span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">samples</span><span class="p">)</span> <span class="o">//</span> <span class="nb">iter</span><span class="p">))</span>
        <span class="n">plt</span><span class="p">.</span><span class="n">imshow</span><span class="p">(</span><span class="n">samples</span><span class="p">[</span><span class="n">i</span><span class="p">][</span><span class="n">row</span><span class="p">]</span> <span class="o">*</span> <span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="n">interpolation</span><span class="o">=</span><span class="s">'nearest'</span><span class="p">,</span> <span class="n">cmap</span><span class="o">=</span><span class="s">'Greys'</span><span class="p">)</span>
        <span class="n">plt</span><span class="p">.</span><span class="n">grid</span><span class="p">(</span><span class="n">b</span><span class="o">=</span><span class="bp">None</span><span class="p">)</span>
        <span class="n">plt</span><span class="p">.</span><span class="n">axis</span><span class="p">(</span><span class="s">'off'</span><span class="p">)</span>

<span class="n">plt</span><span class="p">.</span><span class="n">subplots_adjust</span><span class="p">(</span><span class="n">wspace</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">hspace</span><span class="o">=</span><span class="mi">0</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="n">show</span><span class="p">()</span>
</code></pre></div></div>
<p>What you see here is the sampling process. From noise to coherent images, following the process that we have described here. This is not different from the 2D example but is way cooler since you can see the sampler “navigating” this 28*28 dimensional space until it reaches a peak in the probability distribution of our data. Or what is the same, a coherent handwritten number? Having understood this, it is even more staggering the outputs of SOTA algorithms like Stable Diffusion. Amazing time to be alive isn’t it?</p>

<p align="center">
  <img src="/assets/images/DiffusionModels/Image_MNIST_generated.png" alt="RefineParts" />
</p>

<p>and this to generate the GIF!</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="syntax"><code><span class="kn">from</span> <span class="nn">moviepy.editor</span> <span class="kn">import</span> <span class="n">ImageSequenceClip</span>

<span class="n">images</span> <span class="o">=</span> <span class="p">[]</span>
<span class="n">height</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">sqrt</span><span class="p">(</span><span class="n">samples</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">shape</span><span class="p">[</span><span class="mi">0</span><span class="p">]))</span>

<span class="k">for</span> <span class="n">step</span> <span class="ow">in</span> <span class="n">tqdm</span><span class="p">(</span><span class="nb">range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">samples</span><span class="p">),</span> <span class="mi">5</span><span class="p">)):</span>
    <span class="n">plt</span><span class="p">.</span><span class="n">figure</span><span class="p">(</span><span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">8</span><span class="p">,</span> <span class="mi">8</span><span class="p">))</span>
    <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">samples</span><span class="p">[</span><span class="n">step</span><span class="p">].</span><span class="n">shape</span><span class="p">[</span><span class="mi">0</span><span class="p">]):</span>
        <span class="n">plt</span><span class="p">.</span><span class="n">subplot</span><span class="p">(</span><span class="n">height</span><span class="p">,</span> <span class="n">height</span><span class="p">,</span> <span class="mi">1</span> <span class="o">+</span> <span class="n">i</span><span class="p">)</span>
        <span class="n">plt</span><span class="p">.</span><span class="n">imshow</span><span class="p">(</span><span class="n">samples</span><span class="p">[</span><span class="n">step</span><span class="p">][</span><span class="n">i</span><span class="p">]</span> <span class="o">*</span> <span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="n">interpolation</span><span class="o">=</span><span class="s">'nearest'</span><span class="p">,</span> <span class="n">cmap</span><span class="o">=</span><span class="s">'Greys'</span><span class="p">)</span>
        <span class="n">plt</span><span class="p">.</span><span class="n">grid</span><span class="p">(</span><span class="n">b</span><span class="o">=</span><span class="bp">None</span><span class="p">)</span>
        <span class="n">plt</span><span class="p">.</span><span class="n">axis</span><span class="p">(</span><span class="s">'off'</span><span class="p">)</span>
    <span class="n">plt</span><span class="p">.</span><span class="n">subplots_adjust</span><span class="p">(</span><span class="n">wspace</span><span class="o">=</span><span class="mf">0.05</span><span class="p">,</span> <span class="n">hspace</span><span class="o">=</span><span class="mf">0.05</span><span class="p">)</span>
    <span class="n">plt</span><span class="p">.</span><span class="n">savefig</span><span class="p">(</span><span class="sa">f</span><span class="s">'./Gif_folder/</span><span class="si">{</span><span class="n">step</span><span class="si">}</span><span class="s">.png'</span><span class="p">)</span>
    <span class="n">images</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="sa">f</span><span class="s">'./Gif_folder/</span><span class="si">{</span><span class="n">step</span><span class="si">}</span><span class="s">.png'</span><span class="p">)</span>
    <span class="n">plt</span><span class="p">.</span><span class="n">close</span><span class="p">()</span>

<span class="n">clip</span> <span class="o">=</span> <span class="n">ImageSequenceClip</span><span class="p">(</span><span class="n">images</span><span class="p">,</span> <span class="n">fps</span> <span class="o">=</span> <span class="mi">50</span><span class="p">)</span>
<span class="n">clip</span><span class="p">.</span><span class="n">write_gif</span><span class="p">(</span><span class="s">'MNIST_SAMPLING.gif'</span><span class="p">)</span>
</code></pre></div></div>

<p>And that’s all for part 1. The next part is on the actual diffusion model, which is really similar but still has some differences.</p>]]></content><author><name>Jordi Alonso</name></author><category term="ML" /><summary type="html"><![CDATA[]]></summary></entry></feed>