<?xml version='1.0' encoding='UTF-8'?>
<rss xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:sy="http://purl.org/rss/1.0/modules/syndication/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
  <channel>
    <atom:link href="http://kitchingroup.cheme.cmu.edu/blog/feed/index.xml" rel="self" type="application/rss+xml"/>
    <title>The Kitchin Research Group</title>
    <link>https://kitchingroup.cheme.cmu.edu/blog</link>
    <description>Chemical Engineering at Carnegie Mellon University</description>
    <pubDate>Fri, 30 Oct 2026 23:57:56 GMT</pubDate>
    <generator>Quarto + legacy Blogofile feed bridge</generator>
    <sy:updatePeriod>hourly</sy:updatePeriod>
    <sy:updateFrequency>1</sy:updateFrequency>
    
    <item><title>Surrogate models you can optimize globally</title><link>https://kitchingroup.cheme.cmu.edu/posts/mip-surrogates/</link><pubDate>Wed, 28 Oct 2026 00:00:00 +0000</pubDate><category>python</category><category>pycse</category><category>optimization</category><category>surrogates</category><guid isPermaLink="true">https://kitchingroup.cheme.cmu.edu/posts/mip-surrogates/</guid><description>Surrogate models you can optimize globally</description><content:encoded><![CDATA[ 

Skip to main content




<p>We often fit surrogate models so that we can use them in optimization problems. We want the conditions that maximize a predicted yield, the design that minimizes a predicted cost, or the next experiment to run. With a black-box model that usually means a local optimizer and some restarts, and at the end you have an answer with no way to know whether a better one exists somewhere you did not look. This is a well-known weak point of surrogate-based optimization <span class="citation" data-cites="bhosekar2018surrogate cozad2014alamo">(Bhosekar and Ierapetritou 2018; Cozad et al. 2014)</span>.</p>
<p>There is a class of models where you can do much better. Some surrogates are <em>piecewise linear</em> once they are trained: networks built from affine maps and ReLUs, Kolmogorov-Arnold networks with linear splines, regression trees with linear leaves, and sparse regressions on hinge functions. A piecewise-linear function can be written <em>exactly</em> as a set of mixed-integer linear constraints. The trained model then becomes part of a mixed-integer linear program (MILP), and a MILP solver returns the <strong>global</strong> optimum of the model together with a proof that nothing better exists inside the input box. Better still, the surrogate is now just one block in an optimization model, so you can add your own constraints, integer decisions and other models around it.</p>
<p>Over the past few months we have added this capability to four of our packages: <a href="https://github.com/jkitchin/pycse">pycse</a> (most of the <code>pycse.sklearn</code> models), <a href="https://github.com/jkitchin/jax-ldt">jax-ldt</a> (linear and hyperplane decision trees), <a href="https://github.com/jkitchin/jax-difftree">jax-difftree</a> (differentiable soft trees), and <a href="https://github.com/jkitchin/jaxsr">jaxsr</a> (symbolic regression). This post explains why you would want it, how the models get into a solver, and what each package offers, including the limitations.</p>
<p>The figure below shows the problem and the solution on a small example. The surrogate is a ReLU network with one input and 17 hidden units, so it is piecewise linear with one kink per unit, and it has four valleys. The deepest one is narrow. Press <em>5 random starts</em> to run local search (here, exact steepest descent along the curve): each start slides into the nearest valley and stops, and with these starts none of them finds the deepest one. Nothing in the result tells you that a better answer exists. Then <em>Step</em> through branch and bound. In the MILP, each ReLU has a binary variable, and in one dimension deciding a binary is the same as deciding which side of that unit’s kink <img src="https://latex.codecogs.com/png.latex?x"> lies on, so each branch splits the interval at a kink. On each piece, the units whose kink is outside the piece are fixed and the rest are relaxed, which gives a lower bound (the green segments). Any piece whose bound is above the best point found so far cannot contain the minimum, so it is discarded. When no pieces are left, the best point is proved to be the global minimum. Real MILP solvers bound with LP relaxations, which are tighter than the interval bounds used here, but the logic is the same.</p>
<iframe id="bnb-fig" src="https://kitchingroup.cheme.cmu.edu/posts/mip-surrogates/bnb.html" title="Local search versus branch and bound on a piecewise-linear surrogate" style="width:100%; height:720px; border:0;" loading="lazy"></iframe>
<script>
  window.addEventListener("message", e => {
    const h = e.data && e.data.mipBnbHeight;
    if (h) document.getElementById("bnb-fig").style.height = (h + 4) + "px";
  });
</script>
<section id="why-global-optimization-of-the-surrogate-matters" class="level2">
<h2 class="anchored" data-anchor-id="why-global-optimization-of-the-surrogate-matters">Why global optimization of the surrogate matters</h2>
<p>The usual way to optimize a neural network or a smooth tree is gradient-based: start somewhere, follow the gradient, restart a few times. That works when the surrogate is nearly convex. When the surrogate has several basins, which is common for exactly the response surfaces we care about (selectivity versus temperature and pressure, cost versus a handful of design variables) you have little control over which one you find. The jax-difftree docs say it plainly: inverse design over a soft tree is local, and multistart is the practical fallback.</p>
<p>A MIP solver works differently. It bounds the objective over regions of the input space and discards regions that cannot contain the optimum. When it finishes, the optimality gap is zero (or a tolerance you chose), and that is a statement about the whole box, not about the neighborhood of a starting point. For smooth networks you can get the same guarantee from deterministic global MINLP solvers <span class="citation" data-cites="schweidtmann2019global">(Schweidtmann and Mitsos 2019)</span>, but for piecewise-linear models the problem is a MILP, and MILP solvers are fast, mature and free (HiGHS <span class="citation" data-cites="huangfu2018highs">(Huangfu and Hall 2018)</span> solves every MILP in this post).</p>
<p>There is a second reason that matters just as much in practice. A surrogate embedded as constraints composes with everything else. You can put two unit-operation surrogates in one flowsheet, add a budget constraint, choose between discrete equipment options, and still get a global optimum of the combined problem. A <code>predict</code> function cannot do that.</p>
</section>
<section id="what-mip-representable-means" class="level2">
<h2 class="anchored" data-anchor-id="what-mip-representable-means">What “MIP-representable” means</h2>
<p>The building block is the ReLU, <img src="https://latex.codecogs.com/png.latex?y%20=%20%5Cmax(0,%20a)">. If you know bounds <img src="https://latex.codecogs.com/png.latex?L%20%5Cle%20a%20%5Cle%20U"> with <img src="https://latex.codecogs.com/png.latex?L%20%3C%200%20%3C%20U">, one binary variable <img src="https://latex.codecogs.com/png.latex?d"> encodes it exactly:</p>
<p><img src="https://latex.codecogs.com/png.latex?%0Ay%20%5Cge%200,%20%5Cqquad%20y%20%5Cge%20a,%20%5Cqquad%20y%20%5Cle%20a%20-%20L(1%20-%20d),%20%5Cqquad%20y%20%5Cle%20U%20d.%0A"></p>
<p>When <img src="https://latex.codecogs.com/png.latex?d%20=%201"> the constraints force <img src="https://latex.codecogs.com/png.latex?y%20=%20a">, and when <img src="https://latex.codecogs.com/png.latex?d%20=%200"> they force <img src="https://latex.codecogs.com/png.latex?y%20=%200">. This is the big-M formulation, and it has been used for trained ReLU networks for some time <span class="citation" data-cites="fischetti2018deep tjeng2019evaluating grimstad2019relu">(Fischetti and Jo 2018; Tjeng et al. 2019; Grimstad and Andersson 2019)</span>. Stronger formulations exist <span class="citation" data-cites="anderson2020strong">(Anderson et al. 2020)</span>, and <span class="citation" data-cites="vielma2015mixed">Vielma (2015)</span> is a good general reference. The same idea covers hinge terms <img src="https://latex.codecogs.com/png.latex?%5Cmax(0,%20x%20-%20k)">, absolute values, linear splines (with SOS2 or segment binaries), and tree routing (one binary per leaf).</p>
<p>Two consequences follow from that formulation and show up in every section below. First, <strong>the bounds <img src="https://latex.codecogs.com/png.latex?L"> and <img src="https://latex.codecogs.com/png.latex?U"> come from bounds on the inputs</strong>, propagated through the model. Every input must have finite bounds, and tight bounds give small big-M constants, which gives fast solves. Loose bounds are the most common cause of a slow MILP. Second, the formulation is exact only for piecewise-linear pieces. A SiLU, tanh or softplus activation, a cubic spline, or a polynomial term is not piecewise linear, and you either retrain with a piecewise-linear choice or move to a nonlinear solver.</p>
<p>It helps to name the levels of representability, since not every model lands in the same place.</p>
<table class="caption-top table">
<colgroup>
<col style="width: 11%">
<col style="width: 42%">
<col style="width: 46%">
</colgroup>
<thead>
<tr class="header">
<th>Level</th>
<th>What it means</th>
<th>Examples</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Exact LP</td>
<td>No binaries needed</td>
<td>Linear models; input-convex networks when the output is minimized</td>
</tr>
<tr class="even">
<td>Exact MILP</td>
<td>Reproduces the model to solver precision</td>
<td>ReLU networks, linear-spline KANs, linear-leaf trees, hinge regressions</td>
</tr>
<tr class="odd">
<td>Approximate MILP</td>
<td>Some 1D nonlinearity interpolated on a grid, with an error bound</td>
<td>pycse’s periodic network (sin/cos features)</td>
</tr>
<tr class="even">
<td>Exact MINLP</td>
<td>The model is written as its own nonlinear equation</td>
<td>SISSO equations, jaxsr models with <code>allow_nonlinear=True</code></td>
</tr>
</tbody>
</table>
<p>Uncertainty adds one more ingredient. For several pycse models the predicted standard deviation is a second-order cone in the network’s internal variables, so robust objectives like <img src="https://latex.codecogs.com/png.latex?%5Cmu%20+%202%5Csigma"> become mixed-integer second-order cone programs. We come back to this in the pycse section.</p>
</section>
<section id="getting-a-surrogate-into-a-solver" class="level2">
<h2 class="anchored" data-anchor-id="getting-a-surrogate-into-a-solver">Getting a surrogate into a solver</h2>
<p>There are three routes from a trained model to an optimization problem, and our packages use all of them.</p>
<p><strong>Direct Pyomo embedding.</strong> The model writes its own variables and constraints into a <a href="https://www.pyomo.org/">Pyomo</a> <code>Block</code> <span class="citation" data-cites="bynum2021pyomo">(Bynum et al. 2021)</span>. You pass in your own Pyomo variables as inputs, and the block links them to the surrogate. This is the most flexible route and the one every package here supports (<code>to_pyomo</code> in pycse, jaxsr and jax-difftree, <code>embed_in_pyomo_model</code> in jax-ldt). Any Pyomo MILP solver works: HiGHS, Gurobi, CPLEX, SCIP.</p>
<p><strong>OMLT.</strong> <a href="https://github.com/cog-imperial/OMLT">OMLT</a> <span class="citation" data-cites="ceccon2022omlt">(Ceccon et al. 2022)</span> reads a neural network (usually from ONNX) and builds a Pyomo block with a choice of formulations, such as <code>ReluBigMFormulation</code>. OMLT also has tree formulations, including linear model decision trees <span class="citation" data-cites="ammari2023linear">(Ammari et al. 2023)</span>. In our packages, pycse’s <code>LLPRRegressor</code> exports to ONNX and directly to an OMLT <code>NetworkDefinition</code> (<code>to_onnx()</code>, <code>to_omlt()</code>). One caveat: jax-ldt also writes ONNX, but OMLT cannot read it. OMLT’s ONNX reader understands neural-network layers (Gemm, MatMul, ReLU), and the tree-routing operations (Gather, LessOrEqual) are rejected. That is why the tree packages ship their own adapters.</p>
<p><strong>discopt.</strong> <a href="https://github.com/jkitchin/discopt">discopt</a> is our hybrid MINLP solver with a JAX backend. jax-ldt (<code>embed_in_discopt_model</code>) and jaxsr (<code>to_discopt</code>) write directly into a <code>discopt.Model</code>, and jax-difftree reaches discopt through the jax-ldt spec (more on that below). The advantage of discopt is that the same model can hold MILP surrogate blocks and nonlinear constraints, which is where jaxsr’s <code>allow_nonlinear=True</code> and pycse’s SISSO export are headed. pycse itself writes Pyomo, but the ONNX file from <code>LLPRRegressor.to_onnx()</code> can also be read by <code>discopt.ml.load_onnx</code>.</p>
<p>Whatever the route, the pattern is the same: declare bounded input variables, embed the surrogate, add your constraints, set an objective on the surrogate output, solve, and then check the answer against the model’s own <code>predict</code>.</p>
</section>
<section id="pycse.sklearn-models" class="level2">
<h2 class="anchored" data-anchor-id="pycse.sklearn-models">pycse.sklearn models</h2>
<p>pycse has the broadest coverage. A new module in pycse 2.13.0, <code>pycse.sklearn.mip</code>, holds the formulation machinery once, and each estimator only describes its trained weights. A model implements <code>mip_network()</code>, which returns its weights as plain numpy layer specs (<code>Affine</code>, <code>ReLU</code>, <code>PiecewiseLinear</code>), a <code>TreeModel</code> of leaf boxes, or a symbolic expression. A single function, <code>add_to_pyomo</code>, turns any of these into constraints, so the bound propagation, the big-M formulation and the tests are shared across models. For the user it is one call:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb1-1"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> pyomo.environ <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">as</span> pyo</span>
<span id="cb1-2"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> pycse.sklearn <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> DPOSE</span>
<span id="cb1-3"></span>
<span id="cb1-4">model <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> DPOSE(layers<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">32</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">16</span>)).fit(X, y)       <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># ReLU by default</span></span>
<span id="cb1-5"></span>
<span id="cb1-6">m <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> pyo.ConcreteModel()</span>
<span id="cb1-7">m.T <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> pyo.Var(bounds<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">300</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">500</span>))</span>
<span id="cb1-8">m.P <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> pyo.Var(bounds<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">10</span>))</span>
<span id="cb1-9">m.budget <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> pyo.Constraint(expr<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>m.T <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">100</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> m.P <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;=</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">12</span>)   <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># your own constraints</span></span>
<span id="cb1-10">m.nn <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> pyo.Block()</span>
<span id="cb1-11">model.to_pyomo(block<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>m.nn, inputs<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>[m.T, m.P])        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># input bounds come from your vars</span></span>
<span id="cb1-12">m.obj <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> pyo.Objective(expr<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>m.nn.y[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>], sense<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>pyo.maximize)</span>
<span id="cb1-13">pyo.SolverFactory(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"highs"</span>).solve(m)</span></code></pre></div></div>
<p>Internal input and output scaling is folded into the exported weights, so <code>m.T</code>, <code>m.P</code> and <code>m.nn.y</code> are in your units.</p>
<section id="which-models-and-what-they-need" class="level3">
<h3 class="anchored" data-anchor-id="which-models-and-what-they-need">Which models, and what they need</h3>
<table class="caption-top table">
<colgroup>
<col style="width: 20%">
<col style="width: 20%">
<col style="width: 20%">
<col style="width: 20%">
<col style="width: 20%">
</colgroup>
<thead>
<tr class="header">
<th>Model</th>
<th>Requirement</th>
<th>Level</th>
<th>Binaries</th>
<th>Std exported</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>DPOSE</code> <span class="citation" data-cites="kellner2024dpose">(Kellner and Ceriotti 2024)</span></td>
<td><code>activation=nn.relu</code> (default)</td>
<td>MILP</td>
<td>≤ 1 per ReLU unit</td>
<td>yes (ensemble)</td>
</tr>
<tr class="even">
<td><code>LLPRRegressor</code> <span class="citation" data-cites="bigi2024llpr">(Bigi et al. 2024)</span></td>
<td><code>activation="relu"</code></td>
<td>MILP</td>
<td>≤ 1 per ReLU unit</td>
<td>yes (LLPR)</td>
</tr>
<tr class="odd">
<td><code>NNBR</code></td>
<td><code>MLPRegressor(activation="relu")</code></td>
<td>MILP</td>
<td>≤ 1 per ReLU unit</td>
<td>yes (Bayesian ridge)</td>
</tr>
<tr class="even">
<td><code>KfoldNN</code></td>
<td><code>activation="relu"</code> (default is swish)</td>
<td>MILP</td>
<td>≤ 1 per ReLU unit</td>
<td>yes (ensemble)</td>
</tr>
<tr class="odd">
<td><code>JAXICNNRegressor</code> <span class="citation" data-cites="amos2017icnn">(Amos et al. 2017)</span></td>
<td><code>activation="relu"</code>, <code>strong_convexity_mu=0</code></td>
<td>MILP, or LP</td>
<td>0 with <code>formulation="convex"</code></td>
<td>no</td>
</tr>
<tr class="even">
<td><code>JAXMonotonicRegressor</code></td>
<td><code>activation="relu"</code></td>
<td>MILP; LP if <code>convexity="convex"</code></td>
<td>≤ 1 per ReLU unit</td>
<td>yes (LLPR)</td>
</tr>
<tr class="odd">
<td><code>KAN</code> <span class="citation" data-cites="liu2024kan">(Liu et al. 2024)</span></td>
<td><code>spline_order=1</code>, <code>base_activation="linear"</code></td>
<td>MILP</td>
<td>~<code>grid_size</code> per layer input</td>
<td>with <code>n_ensemble &gt; 1</code></td>
</tr>
<tr class="even">
<td><code>KANLLPR</code></td>
<td><code>spline_order=1</code>, <code>base_activation="linear"</code></td>
<td>MILP</td>
<td>~<code>grid_size</code> per layer input</td>
<td>yes (LLPR)</td>
</tr>
<tr class="odd">
<td><code>LeafModelRegressor</code></td>
<td>a linear <code>leaf_model</code></td>
<td>MILP</td>
<td>1 per leaf in the box</td>
<td>no</td>
</tr>
<tr class="even">
<td><code>LinearRegressionUQ</code></td>
<td>none</td>
<td>LP</td>
<td>0</td>
<td>yes</td>
</tr>
<tr class="odd">
<td><code>JAXPeriodicRegressor</code></td>
<td><code>activation="relu"</code></td>
<td>approximate MILP</td>
<td>grid segments + ReLUs</td>
<td>yes</td>
</tr>
<tr class="even">
<td><code>SISSO</code> <span class="citation" data-cites="ouyang2018sisso">(Ouyang et al. 2018)</span></td>
<td>none (TorchSisso to fit)</td>
<td>MINLP</td>
<td>0, nonlinear</td>
<td>yes</td>
</tr>
</tbody>
</table>
<p>“One binary per ReLU unit” is an upper bound. Units that are always on or always off over the input box need no binary, and interval bound propagation finds them. A DPOSE network with 32 hidden units needed 30 binaries in the docs notebook.</p>
<p>A few of the formulations are worth a sentence each. KAN layers apply a 1D spline to every edge, but all the edges leaving one input share its breakpoints, so the MIP uses one SOS2 set per <em>input</em> rather than per edge. Trees use one binary to pick a leaf, and leaves that lie outside the input bounds are dropped before the MIP is built. Leaf models are recovered by probing <code>predict</code> and then checked at random points, so any linear scikit-learn pipeline works and a nonlinear one raises an error.</p>
<p>Input-convex networks get special treatment. In an input-convex ReLU network every hidden unit reaches the output through nonnegative weights, so when the output is <em>minimized</em>, the two lower inequalities <img src="https://latex.codecogs.com/png.latex?y%20%5Cge%200,%20y%20%5Cge%20a"> are enough: the solver pushes each unit down to <img src="https://latex.codecogs.com/png.latex?%5Cmax(0,%20a)"> on its own. <code>formulation="convex"</code> drops all the binaries and the problem becomes an LP. pycse checks the weights first and refuses networks that are not convex.</p>
</section>
<section id="how-it-performed" class="level3">
<h3 class="anchored" data-anchor-id="how-it-performed">How it performed</h3>
<p>To test the exports we fitted six models to the same 400 noisy samples of <img src="https://latex.codecogs.com/png.latex?f(x)%20=%20%5Ctfrac14%5ClVert%20x%5CrVert%5E2%20+%20%5Csin%203x_1%20+%20%5Ccos%202x_2%20+%200.15x_2"> on the square <img src="https://latex.codecogs.com/png.latex?%5B-2,%202%5D%5E2">, then minimized each model with HiGHS. The function has one global minimum, <img src="https://latex.codecogs.com/png.latex?f%5E*%20=%20-1.599"> at <img src="https://latex.codecogs.com/png.latex?(-0.50,%20-1.43)">, and several local ones. The deepest of these is a mirror-image well at <img src="https://latex.codecogs.com/png.latex?(-0.50,%201.36)"> with <img src="https://latex.codecogs.com/png.latex?f%20=%20-1.181">, the obvious place for a model to go wrong. The results answer three questions. (They come from one run. Different training runs shift the numbers a little, but not the conclusions.)</p>
<p><strong>Is the MIP exact?</strong> Yes. For every model, the MIP minimum was equal to or slightly below the minimum of a 301×301 grid search on the same model, which is what a true global minimum must do. Evaluating the model with its own <code>predict</code> at the MIP’s answer reproduced the MIP objective to <img src="https://latex.codecogs.com/png.latex?10%5E%7B-11%7D"> or better.</p>
<p><strong>Is the answer right?</strong> Yes, when the model is good. These fits have test RMSE between 0.07 and 0.11, and every one of their MIP optima landed in the global well, within 0.14 of <img src="https://latex.codecogs.com/png.latex?f%5E*"> when judged by the true function. None of them fell into the second well. The figure below shows where each one landed. A model has to be good enough for this. With 300 training epochs <code>LLPRRegressor</code> underfit (RMSE 0.24), and its optimum missed <img src="https://latex.codecogs.com/png.latex?f%5E*"> by 0.5, so we trained it for 1000. All six models predict a minimum slightly <em>deeper</em> than the true one (between <img src="https://latex.codecogs.com/png.latex?-1.69"> and <img src="https://latex.codecogs.com/png.latex?-1.83">). That is expected: a global optimizer seeks out the places where the model’s error happens to be most negative, so the predicted value at the optimum is biased low. Check the optimum against the real function before you trust the number.</p>
<p><strong>What does it cost?</strong> The number of binaries grows with the size of the network, and the solve time grows faster than that:</p>
<table class="caption-top table">
<thead>
<tr class="header">
<th>Model</th>
<th>Binaries</th>
<th>Solve (s)</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>KfoldNN (32, 16)</td>
<td>29</td>
<td>0.06</td>
</tr>
<tr class="even">
<td>DPOSE (32 hidden units)</td>
<td>29</td>
<td>0.10</td>
</tr>
<tr class="odd">
<td>KAN (2, 4, 1), grid 10</td>
<td>68</td>
<td>0.29</td>
</tr>
<tr class="even">
<td>KANLLPR (2, 4, 1), grid 10</td>
<td>66</td>
<td>0.55</td>
</tr>
<tr class="odd">
<td>LLPRRegressor (32, 32)</td>
<td>63</td>
<td>8.1</td>
</tr>
<tr class="even">
<td>NNBR (32, 32)</td>
<td>59</td>
<td>9.3</td>
</tr>
</tbody>
</table>
<p>The two networks with two hidden layers of 32 units are the slow ones. KANs have about as many binaries but solve much faster, because their binaries come from splines on the inputs rather than from stacked ReLU layers. The convex formulation is cheaper still: on a convex test function, <code>JAXICNNRegressor</code> (64, 64) took 0.47 s with 122 binaries as a MILP and 0.06 s with no binaries as an LP, and both found the same minimum.</p>
<p>We left the tree (<code>LeafModelRegressor</code>) out of this comparison. On a smooth function like this one, 400 points are not enough for a tree to fit well (test RMSE 0.29 or more), and its optimum missed <img src="https://latex.codecogs.com/png.latex?f%5E*"> by 0.9 to 2.7 depending on the settings. The MIP still finds the tree’s minimum exactly; the tree is just a poor model of <img src="https://latex.codecogs.com/png.latex?f"> here.</p>
<p>Hover a point or its label in the figure to see which model it belongs to, and click it to show that model’s surface.</p>
<iframe id="optima-fig" src="https://kitchingroup.cheme.cmu.edu/posts/mip-surrogates/optima.html?v=2" title="Where each model puts the minimum" style="width:100%; height:640px; border:0;" loading="lazy"></iframe>
<script>
  window.addEventListener("message", e => {
    const h = e.data && e.data.mipOptimaHeight;
    if (h) document.getElementById("optima-fig").style.height = (h + 4) + "px";
  });
</script>
</section>
<section id="optimizing-with-uncertainty" class="level3">
<h3 class="anchored" data-anchor-id="optimizing-with-uncertainty">Optimizing with uncertainty</h3>
<p>For every pycse model with a standard deviation, the std has the form <img src="https://latex.codecogs.com/png.latex?%5Csigma(x)%20=%20%5Csqrt%7B%5ClVert%20A%20z(x)%20+%20c%20%5CrVert%5E2%20+%20d%7D">, where <img src="https://latex.codecogs.com/png.latex?z"> is one layer of the network (the ensemble members for DPOSE and KfoldNN, the last-layer features for the LLPR models). That is a second-order cone. <code>to_pyomo(include_std=True)</code> adds a variable with <img src="https://latex.codecogs.com/png.latex?s%20%5Cge%20%5Csigma(x)">, which is convex and exact whenever the objective <em>penalizes</em> the std, as in robust design (<img src="https://latex.codecogs.com/png.latex?%5Cmin%5C%20%5Cmu%20+%202%5Csigma">). <code>include_std="exact"</code> enforces <img src="https://latex.codecogs.com/png.latex?s%5E2%20=%20%5Csigma(x)%5E2"> instead. It is nonconvex, and it is needed when the objective <em>rewards</em> the std, as in exploration for active learning (<img src="https://latex.codecogs.com/png.latex?%5Cmin%5C%20%5Cmu%20-%202%5Csigma">), because with the cone alone that problem is unbounded.</p>
<table class="caption-top table">
<thead>
<tr class="header">
<th>Objective</th>
<th>Mean at x*</th>
<th>Std at x*</th>
<th>MIP objective</th>
<th>Grid</th>
<th>Solve</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>mean</td>
<td>-1.7826</td>
<td>0.1024</td>
<td>-1.7826</td>
<td>-1.7800</td>
<td>0.36 s</td>
</tr>
<tr class="even">
<td>robust: mean + 2 std</td>
<td>-1.7693</td>
<td>0.0932</td>
<td>-1.5830</td>
<td>-1.5792</td>
<td>0.21 s</td>
</tr>
<tr class="odd">
<td>explore: mean - 2 std</td>
<td>-1.7769</td>
<td>0.1076</td>
<td>-1.9923</td>
<td>-1.9900</td>
<td>4.2 s</td>
</tr>
</tbody>
</table>
<p>Penalizing the std trades a little mean for less uncertainty, and rewarding it does the opposite. Both std options need a solver with quadratic constraints (Gurobi, CPLEX, SCIP). These numbers are a separate DPOSE fit from the docs notebook, solved with Gurobi; <code>pip install gurobipy</code> includes a size-limited license that covers small examples like this.</p>
</section>
<section id="caveats" class="level3">
<h3 class="anchored" data-anchor-id="caveats">Caveats</h3>
<p>You have to choose piecewise-linear options at training time. Several pycse defaults are smooth (LLPR, ICNN, monotonic, periodic and KfoldNN, plus KAN’s cubic splines), and <code>to_pyomo()</code> raises a <code>ValueError</code> naming the setting to change. The accuracy cost of ReLU or linear splines is usually small, but it is a retraining step.</p>
<p>MIP size grows with the network. Two hidden layers of 32 units solved in 7 to 15 s here, and much larger networks may not solve in reasonable time. KANs (binaries per input, not per unit) and the convex LP scale better.</p>
<p>The convex LP is one-sided: it is exact only when the output is minimized or bounded above. To maximize an ICNN, use the MILP.</p>
<p>Periodic models are approximate. The sin/cos features are interpolated on a grid, with a per-feature error of at most <img src="https://latex.codecogs.com/png.latex?(2%5Cpi/n)%5E2/8"> for <img src="https://latex.codecogs.com/png.latex?n"> points per period (4.8e-3 at the default of 32). The MIP finds the global optimum of the <em>interpolated</em> model, which in the notebook landed 2.4e-3 above the grid-search minimum of the un-interpolated model at 8 to 32 points. At 64 points it beat the grid search, at the cost of 288 binaries instead of 160.</p>
<p>Trees have a boundary problem. A tree sends <img src="https://latex.codecogs.com/png.latex?x%20%5Cle%20t"> left, and a MIP cannot express the strict inequality on the other side, so a point exactly on a split can take either leaf’s value. <code>split_gap=1e-4</code> excludes a thin slab above each split. The gap has to be well above the solver tolerances; at 1e-6 HiGHS still picked the wrong leaf.</p>
<p>SISSO exports exactly, but the equation is nonlinear (division, exp, log), so it needs a global MINLP solver such as BARON, SCIP or Gurobi’s nonlinear mode. Ipopt will only find a local optimum.</p>
<p><code>ZENNRegressor</code> and <code>ZENNClassifier</code> are not representable (their output is a log-sum-exp over sub-networks). <code>NNGMM</code>’s mean network could be exported but is not wired up yet, and <code>JAXICNNRegressor</code> with <code>strong_convexity_mu &gt; 0</code> would be a MIQP but is not exported.</p>
<p>The walkthrough is the pycse docs page <a href="https://kitchingroup.cheme.cmu.edu/pycse/docs/mip.html">Optimizing over machine-learned models with MIP</a>. The code is in <a href="https://github.com/jkitchin/pycse/blob/master/src/pycse/sklearn/mip.py"><code>mip.py</code></a> (its module docstring has a worked example), with tests in <a href="https://github.com/jkitchin/pycse/blob/master/src/pycse/tests/test_sklearn_mip.py"><code>test_sklearn_mip.py</code></a>, and a KAN versus ReLU size comparison in <a href="https://github.com/jkitchin/pycse/blob/master/src/pycse/examples/KAN_Demo.ipynb"><code>KAN_Demo.ipynb</code></a>.</p>
</section>
</section>
<section id="jax-ldt-linear-and-hyperplane-decision-trees" class="level2">
<h2 class="anchored" data-anchor-id="jax-ldt-linear-and-hyperplane-decision-trees">jax-ldt: linear and hyperplane decision trees</h2>
<p><a href="https://github.com/jkitchin/jax-ldt">jax-ldt</a> fits linear-model decision trees, where every leaf holds a linear regression instead of a constant, and hyperplane trees, where the splits are oblique hyperplanes chosen from sparse combinations of the inputs. Both are piecewise linear by construction, so they were designed from the start to be embedded. The package was motivated by our work with the Laird group on hyperplane trees as surrogates for chemical process design <span class="citation" data-cites="sunshine2025hyperplane">(Sunshine et al. 2025)</span>, which builds on linear model decision trees in OMLT <span class="citation" data-cites="ammari2023linear">(Ammari et al. 2023)</span>. Optimization over tree models more generally goes back to <span class="citation" data-cites="misic2020optimization">Mišić (2020)</span> and ENTMOOT <span class="citation" data-cites="thebelt2021entmoot">(Thebelt et al. 2021)</span>.</p>
<p>The MILP formulation lifts the features into the transformed (hyperplane) space, uses one binary per leaf, writes the routing as big-M constraints, and linearizes the leaf outputs with big-M as well. The result is an exact MILP. The same formulation is available in Pyomo and in discopt:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb2-1"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> discopt</span>
<span id="cb2-2"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> jax_ldt <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> HyperplaneTreeRegressor</span>
<span id="cb2-3"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> jax_ldt.export <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> embed_in_discopt_model</span>
<span id="cb2-4"></span>
<span id="cb2-5">model <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> HyperplaneTreeRegressor(max_depth<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">4</span>, max_bins<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">6</span>, min_samples_leaf<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">15</span>)</span>
<span id="cb2-6">model.fit(X, y)</span>
<span id="cb2-7"></span>
<span id="cb2-8">m <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> discopt.Model(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"min-y"</span>)</span>
<span id="cb2-9">x <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> m.continuous(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"x"</span>, shape<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>,), lb<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=-</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>, ub<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>)</span>
<span id="cb2-10">y_expr <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> embed_in_discopt_model(model, m, x)</span>
<span id="cb2-11">m.minimize(y_expr[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>])</span>
<span id="cb2-12">result <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> m.solve()</span></code></pre></div></div>
<p>For Pyomo, <code>embed_in_pyomo_model(model, m, m.x)</code> builds the same formulation on a sub-block, so several trees can share one model. For axis-aligned trees with constant leaves there is also a lighter adapter, <code>to_discopt_decision_tree</code>, which uses discopt’s built-in tree formulation.</p>
<p>The canonical export is a JSON spec (<code>to_json</code> / <code>from_json</code>), and every other format is regenerated from it. That turns out to be useful beyond jax-ldt, because jax-difftree writes the same spec.</p>
<p><strong>Caveats.</strong> Big-M constants are derived per node and per leaf from the input bounds, so bounds are required. You can override them with <code>big_m=</code>, and they are validated when the inputs are bounded. The ONNX export is for inference with onnxruntime, not for OMLT, as noted above. The tree’s uncertainty estimates are stored in the spec but are not part of the MILP.</p>
<p>Docs: <a href="https://github.com/jkitchin/jax-ldt/blob/main/docs/mip_export.md">MIP export</a>, <a href="https://github.com/jkitchin/jax-ldt/blob/main/docs/discopt_integration.md">discopt integration</a>. Examples: <a href="https://github.com/jkitchin/jax-ldt/blob/main/examples/04_mip_optimization.ipynb"><code>04_mip_optimization.ipynb</code></a>, <a href="https://github.com/jkitchin/jax-ldt/blob/main/examples/07_pyomo_embedding.ipynb"><code>07_pyomo_embedding.ipynb</code></a>.</p>
</section>
<section id="jax-difftree-differentiable-soft-trees" class="level2">
<h2 class="anchored" data-anchor-id="jax-difftree-differentiable-soft-trees">jax-difftree: differentiable soft trees</h2>
<p><a href="https://github.com/jkitchin/jax-difftree">jax-difftree</a> takes a different route to a tree. Instead of growing it greedily, it fits a fixed-depth <em>soft</em> tree end to end with gradients <span class="citation" data-cites="hehn2020end">(Hehn et al. 2020)</span>, with oblique splits and sigmoid routing. Soft trees are smooth and train well, and they can match derivative data (Jacobians from a simulator) as well as values. They are not piecewise linear, though, so getting a MILP takes one extra step.</p>
<p>The workflow is to train with affine leaves, harden the tree so that each point routes to exactly one leaf, and then export the crisp piecewise-affine model:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb3-1"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> difftree <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> DifferentiableTreeRegressor, export</span>
<span id="cb3-2"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> pyomo.environ <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">as</span> pyo</span>
<span id="cb3-3"></span>
<span id="cb3-4">model <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> DifferentiableTreeRegressor(</span>
<span id="cb3-5">    depth<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>, leaf_type<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"affine"</span>, jacobian_weight<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">1.0</span>, hardening<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">True</span></span>
<span id="cb3-6">)</span>
<span id="cb3-7">model.fit(X_tr, Y_tr, dydx<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>J_tr)</span>
<span id="cb3-8"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(model.hardening_report_)            <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># soft vs hard error, before and after</span></span>
<span id="cb3-9"></span>
<span id="cb3-10">m <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> pyo.ConcreteModel()</span>
<span id="cb3-11">m.x <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> pyo.Var(<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">range</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>))</span>
<span id="cb3-12">m.y <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> pyo.Var(<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">range</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>))</span>
<span id="cb3-13">tree <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> export.to_pyomo(model, m, x<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>m.x, y<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>m.y, bounds<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>(lo, hi), method<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"bigm"</span>)</span>
<span id="cb3-14">m.obj <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> pyo.Objective(expr<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>m.y[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>])</span>
<span id="cb3-15">pyo.SolverFactory(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"appsi_highs"</span>).solve(m)</span></code></pre></div></div>
<p>The Pyomo export is written as a generalized disjunctive program: each leaf is a <code>Disjunct</code> holding its polytope and its affine model, and a <code>Disjunction</code> makes exactly one leaf active, which Pyomo then transforms (here with big-M). For discopt, <code>export.to_json</code> writes the jax-ldt v1 spec, so jax-ldt’s <code>from_json</code> and <code>embed_in_discopt_model</code> consume a difftree model unchanged. All outputs share one partition, which means a multi-output tree for a unit operation needs only one set of binaries.</p>
<p><strong>Caveats.</strong> The <em>soft/hard gap</em> is the main one. The MILP optimizes the hardened tree, not the soft one you trained, so check <code>soft_hard_gap</code> (and train with <code>hardening=True</code>) before trusting the optimum. The leaf polytopes are closed, as usual in big-M formulations, so an optimum can sit on a split boundary using either neighbor’s model; <code>export.repair_solution</code> moves such a point into the chosen leaf. Continuity across oblique splits needs <code>tied_splits=True</code>, which gives up flexibility (a depth-<img src="https://latex.codecogs.com/png.latex?D"> tree then has <img src="https://latex.codecogs.com/png.latex?D"> hyperplanes instead of <img src="https://latex.codecogs.com/png.latex?2%5ED%20-%201">). The fixed full-binary structure caps practical depth at around 10 to 12. A pruned export matches <code>predict(hard=True)</code> only inside the export box.</p>
<p>Docs: <a href="https://github.com/jkitchin/jax-difftree/blob/main/docs/surrogates.md">surrogates for MILP</a>, <a href="https://github.com/jkitchin/jax-difftree/blob/main/docs/limitations.md">limitations</a>. Notebooks: <a href="https://github.com/jkitchin/jax-difftree/blob/main/notebooks/piecewise_linear_milp.ipynb"><code>piecewise_linear_milp</code></a>, <a href="https://github.com/jkitchin/jax-difftree/blob/main/notebooks/pyomo_embedding.ipynb"><code>pyomo_embedding</code></a>, <a href="https://github.com/jkitchin/jax-difftree/blob/main/notebooks/multivariate_inverse_design.ipynb"><code>multivariate_inverse_design</code></a>.</p>
</section>
<section id="jaxsr-symbolic-regression-with-hinge-functions" class="level2">
<h2 class="anchored" data-anchor-id="jaxsr-symbolic-regression-with-hinge-functions">jaxsr: symbolic regression with hinge functions</h2>
<p><a href="https://github.com/jkitchin/jaxsr">jaxsr</a> fits sparse symbolic models, a weighted sum of terms chosen from a basis library by an information criterion, in the spirit of ALAMO <span class="citation" data-cites="cozad2014alamo">(Cozad et al. 2014)</span>. The new capability is a family of piecewise-linear basis terms, most usefully hinges <img src="https://latex.codecogs.com/png.latex?%5Cmax(0,%20x%20-%20k)"> and <img src="https://latex.codecogs.com/png.latex?%5Cmax(0,%20k%20-%20x)">, which give MARS-style fits <span class="citation" data-cites="friedman1991mars breiman1993hinging">(Friedman 1991; Breiman 1993)</span>. If every selected term is piecewise linear, the fitted model is an exact MILP, and unlike the other models in this post it is also a short, readable equation.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb4" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb4-1"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> jaxsr <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> BasisLibrary, SymbolicRegressor, milp_incompatible_terms</span>
<span id="cb4-2"></span>
<span id="cb4-3">library <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> (</span>
<span id="cb4-4">    BasisLibrary(n_features<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>, feature_names<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"T"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"P"</span>], feature_bounds<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>[(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">4</span>), (<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">4</span>)])</span>
<span id="cb4-5">    .add_constant()</span>
<span id="cb4-6">    .add_linear()</span>
<span id="cb4-7">    .add_hinges(X<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>X, n_knots<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">7</span>)</span>
<span id="cb4-8">    .add_transcendental([<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"abs"</span>])</span>
<span id="cb4-9">)</span>
<span id="cb4-10">model <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> SymbolicRegressor(basis_library<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>library, max_terms<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">6</span>, information_criterion<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"bic"</span>)</span>
<span id="cb4-11">model.fit(X, cost)</span>
<span id="cb4-12"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(model.expression_)</span>
<span id="cb4-13"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(milp_incompatible_terms(model))   <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># [] means a pure MILP</span></span></code></pre></div></div>
<p>The MILP-representable terms are constants and linear terms (written directly), hinges and <code>abs</code> (one binary each, big-M), categorical indicators (one-hot binaries), and categorical-continuous interactions (an exact McCormick product). The model then goes to either backend with one call, linked to your own variables:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb5" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb5-1">model.to_pyomo(m.sr, inputs<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>{<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"T"</span>: m.T, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"P"</span>: m.P}, output<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>m.cost)   <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Pyomo block</span></span>
<span id="cb5-2"></span>
<span id="cb5-3">emb <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> model.to_discopt(dm_model)                                    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># discopt</span></span>
<span id="cb5-4">dm_model.subject_to(emb.inputs[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"T"</span>] <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> emb.inputs[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"P"</span>] <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;=</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5</span>)</span>
<span id="cb5-5">dm_model.minimize(emb.output)</span></code></pre></div></div>
<p>Polynomials, interactions, <code>log</code>, <code>exp</code> and the rest are not piecewise linear. You can still embed them with <code>allow_nonlinear=True</code>, which gives an MIQCP or MINLP for discopt, BARON, SCIP or Gurobi.</p>
<p><strong>Caveats.</strong> Bounds are required and are <em>enforced</em> on the embedded inputs, on purpose: outside the training range the model is extrapolating, and the right fix is more data, not wider bounds. Knot placement matters more than knot count, since the fit can only bend where a knot exists, so pass known breakpoints (a phase boundary, a saturation point) explicitly with <code>knots=</code>. Parametric and custom basis functions, rational forms and SISSO-style features cannot be embedded. <code>MultiOutputSymbolicRegressor</code> is not supported directly, but each <code>estimators_[i]</code> can be embedded separately. The embedding is built from the fitted coefficients, so rebuild it after a refit.</p>
<p>Guide: <a href="https://github.com/jkitchin/jaxsr/blob/main/docs/guides/mip-embedding.md">Embedding models in MILPs</a>. Example: <a href="https://github.com/jkitchin/jaxsr/blob/main/docs/examples/mip_surrogate_optimization.ipynb"><code>mip_surrogate_optimization.ipynb</code></a>, which includes a refine-and-resolve loop and a design problem solved in both backends.</p>
</section>
<section id="choosing-a-model" class="level2">
<h2 class="anchored" data-anchor-id="choosing-a-model">Choosing a model</h2>
<!-- TODO: optionally run one shared benchmark through all four packages for a fair
comparison of fit error, MILP size and solve time. -->
<p>The packages overlap, but they are good at different things. If you want an interpretable equation, or you know where the response bends, jaxsr’s hinge models are the natural choice, and they stay small. If you want a flexible fit with uncertainty and the option of a robust or exploratory objective, the pycse ReLU networks are the most complete, and the input-convex networks are the cheapest to optimize when convexity is physically justified. Trees are attractive when the response has genuinely different regimes: jax-ldt grows them greedily and exports directly, while jax-difftree trains them end to end, can use derivative data, and needs the hardening step before export.</p>
<table class="caption-top table">
<colgroup>
<col style="width: 16%">
<col style="width: 16%">
<col style="width: 16%">
<col style="width: 16%">
<col style="width: 16%">
<col style="width: 16%">
</colgroup>
<thead>
<tr class="header">
<th></th>
<th>pycse ReLU nets</th>
<th>pycse KAN</th>
<th>jax-ldt</th>
<th>jax-difftree</th>
<th>jaxsr hinges</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Binaries scale with</td>
<td>hidden units</td>
<td>grid × inputs</td>
<td>leaves</td>
<td>leaves (<img src="https://latex.codecogs.com/png.latex?2%5ED">)</td>
<td>hinge/abs terms</td>
</tr>
<tr class="even">
<td>Interpretable</td>
<td>no</td>
<td>partly</td>
<td>partly</td>
<td>partly</td>
<td>yes</td>
</tr>
<tr class="odd">
<td>Std in the MIP</td>
<td>yes (SOCP)</td>
<td>with LLPR/ensemble</td>
<td>no</td>
<td>no</td>
<td>no</td>
</tr>
<tr class="even">
<td>Pyomo</td>
<td>yes</td>
<td>yes</td>
<td>yes</td>
<td>yes (GDP)</td>
<td>yes</td>
</tr>
<tr class="odd">
<td>discopt</td>
<td>LLPR via ONNX</td>
<td>no</td>
<td>yes</td>
<td>via jax-ldt spec</td>
<td>yes</td>
</tr>
<tr class="even">
<td>OMLT</td>
<td>LLPR via ONNX</td>
<td>no</td>
<td>no</td>
<td>no</td>
<td>no</td>
</tr>
</tbody>
</table>
</section>
<section id="the-caveat-that-applies-to-all-of-them" class="level2">
<h2 class="anchored" data-anchor-id="the-caveat-that-applies-to-all-of-them">The caveat that applies to all of them</h2>
<p>A global optimizer finds your model’s mistakes. Our first run of the pycse benchmark used 150 samples and shorter training. Two of the models came out poorly fitted, and their global optima sat on the edge of the box, where there was little data and the models were extrapolating: one predicted <img src="https://latex.codecogs.com/png.latex?-1.58"> at a point where the true <img src="https://latex.codecogs.com/png.latex?f"> is <img src="https://latex.codecogs.com/png.latex?+1.07">. A local optimizer started in the middle might never go there, and the MIP always will. The global optimum of the <em>surrogate</em> is only as good as the surrogate is in that region. So validate the optimum against the true model, and then either add data there and refit (jaxsr’s notebook has a refine-and-resolve loop, and our earlier <a href="https://kitchingroup.cheme.cmu.edu/posts/surrogate-active-learning/">active-learning post</a> covers the data side), penalize the std, or tighten the bounds to the region you trust.</p>
</section>
<section id="installing" class="level2">
<h2 class="anchored" data-anchor-id="installing">Installing</h2>
<p>The MIP export is in pycse 2.13.0 and later. Pyomo and HiGHS come with the <code>mip</code> extra; the ONNX and OMLT exports of <code>LLPRRegressor</code> also need <code>onnx</code> and <code>omlt</code>.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb6" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb6-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">pip</span> install <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"pycse[mip]"</span></span></code></pre></div></div>
<p>jaxsr 0.3.0 is on PyPI, but the hinge terms and MILP embedding are newer than that release, so install from GitHub for now. jax-ldt and jax-difftree are not on PyPI. (The <code>difftree</code> package on PyPI is an unrelated project, so install jax-difftree from GitHub as shown.)</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb7" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb7-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">pip</span> install <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"jaxsr[mip] @ git+https://github.com/jkitchin/jaxsr.git"</span></span>
<span id="cb7-2"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">pip</span> install <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"jax-ldt[pyomo] @ git+https://github.com/jkitchin/jax-ldt.git"</span></span>
<span id="cb7-3"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">pip</span> install <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"difftree[pyomo] @ git+https://github.com/jkitchin/jax-difftree.git"</span></span>
<span id="cb7-4"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">pip</span> install discopt omlt onnx</span></code></pre></div></div>
</section>
<section id="references" class="level2">
<h2 class="anchored" data-anchor-id="references">References</h2>
<div id="refs" class="references csl-bib-body hanging-indent">
<div id="ref-ammari2023linear" class="csl-entry">
Ammari, Bashar L., Emma S. Johnson, Georgia Stinchfield, et al. 2023. <span>“Linear Model Decision Trees as Surrogates in Optimization of Engineering Applications.”</span> <em>Computers &amp; Chemical Engineering</em> 178: 108347. <a href="https://doi.org/10.1016/j.compchemeng.2023.108347">https://doi.org/10.1016/j.compchemeng.2023.108347</a>.
</div>
<div id="ref-amos2017icnn" class="csl-entry">
Amos, Brandon, Lei Xu, and J. Zico Kolter. 2017. <span>“Input Convex Neural Networks.”</span> <em>Proceedings of the 34th International Conference on Machine Learning</em>. <a href="https://arxiv.org/abs/1609.07152">https://arxiv.org/abs/1609.07152</a>.
</div>
<div id="ref-anderson2020strong" class="csl-entry">
Anderson, Ross, Joey Huchette, Will Ma, Christian Tjandraatmadja, and Juan Pablo Vielma. 2020. <span>“Strong Mixed-Integer Programming Formulations for Trained Neural Networks.”</span> <em>Mathematical Programming</em> 183: 3–39. <a href="https://doi.org/10.1007/s10107-020-01474-5">https://doi.org/10.1007/s10107-020-01474-5</a>.
</div>
<div id="ref-bhosekar2018surrogate" class="csl-entry">
Bhosekar, Atharv, and Marianthi Ierapetritou. 2018. <span>“Advances in Surrogate Based Modeling, Feasibility Analysis, and Optimization: A Review.”</span> <em>Computers &amp; Chemical Engineering</em> 108: 250–67. <a href="https://doi.org/10.1016/j.compchemeng.2017.09.017">https://doi.org/10.1016/j.compchemeng.2017.09.017</a>.
</div>
<div id="ref-bigi2024llpr" class="csl-entry">
Bigi, Filippo, Sanggyu Chong, Michele Ceriotti, and Federico Grasselli. 2024. <span>“A Prediction Rigidity Formalism for Low-Cost Uncertainties in Trained Neural Networks.”</span> <em>Machine Learning: Science and Technology</em> 5 (4): 045018. <a href="https://doi.org/10.1088/2632-2153/ad805f">https://doi.org/10.1088/2632-2153/ad805f</a>.
</div>
<div id="ref-breiman1993hinging" class="csl-entry">
Breiman, Leo. 1993. <span>“Hinging Hyperplanes for Regression, Classification, and Function Approximation.”</span> <em>IEEE Transactions on Information Theory</em> 39 (3): 999–1013. <a href="https://doi.org/10.1109/18.256506">https://doi.org/10.1109/18.256506</a>.
</div>
<div id="ref-bynum2021pyomo" class="csl-entry">
Bynum, Michael L., Gabriel A. Hackebeil, William E. Hart, et al. 2021. <em>Pyomo: Optimization Modeling in Python</em>. 3rd ed. Vol. 67. Springer Optimization and Its Applications. Springer. <a href="https://doi.org/10.1007/978-3-030-68928-5">https://doi.org/10.1007/978-3-030-68928-5</a>.
</div>
<div id="ref-ceccon2022omlt" class="csl-entry">
Ceccon, Francesco, Jordan Jalving, Joshua Haddad, et al. 2022. <span>“<span>OMLT</span>: Optimization &amp; Machine Learning Toolkit.”</span> <em>Journal of Machine Learning Research</em> 23 (349): 1–8. <a href="https://jmlr.org/papers/v23/22-0277.html">https://jmlr.org/papers/v23/22-0277.html</a>.
</div>
<div id="ref-cozad2014alamo" class="csl-entry">
Cozad, Alison, Nikolaos V. Sahinidis, and David C. Miller. 2014. <span>“Learning Surrogate Models for Simulation-Based Optimization.”</span> <em>AIChE Journal</em> 60 (6): 2211–27. <a href="https://doi.org/10.1002/aic.14418">https://doi.org/10.1002/aic.14418</a>.
</div>
<div id="ref-fischetti2018deep" class="csl-entry">
Fischetti, Matteo, and Jason Jo. 2018. <span>“Deep Neural Networks and Mixed Integer Linear Optimization.”</span> <em>Constraints</em> 23: 296–309. <a href="https://doi.org/10.1007/s10601-018-9285-6">https://doi.org/10.1007/s10601-018-9285-6</a>.
</div>
<div id="ref-friedman1991mars" class="csl-entry">
Friedman, Jerome H. 1991. <span>“Multivariate Adaptive Regression Splines.”</span> <em>The Annals of Statistics</em> 19 (1): 1–67. <a href="https://doi.org/10.1214/aos/1176347963">https://doi.org/10.1214/aos/1176347963</a>.
</div>
<div id="ref-grimstad2019relu" class="csl-entry">
Grimstad, Bjarne, and Henrik Andersson. 2019. <span>“<span>ReLU</span> Networks as Surrogate Models in Mixed-Integer Linear Programs.”</span> <em>Computers &amp; Chemical Engineering</em> 131: 106580. <a href="https://doi.org/10.1016/j.compchemeng.2019.106580">https://doi.org/10.1016/j.compchemeng.2019.106580</a>.
</div>
<div id="ref-hehn2020end" class="csl-entry">
Hehn, Thomas M., Julian F. P. Kooij, and Fred A. Hamprecht. 2020. <span>“End-to-End Learning of Decision Trees and Forests.”</span> <em>International Journal of Computer Vision</em> 128 (4): 997–1011. <a href="https://doi.org/10.1007/s11263-019-01237-6">https://doi.org/10.1007/s11263-019-01237-6</a>.
</div>
<div id="ref-huangfu2018highs" class="csl-entry">
Huangfu, Qi, and J. A. Julian Hall. 2018. <span>“Parallelizing the Dual Revised Simplex Method.”</span> <em>Mathematical Programming Computation</em> 10: 119–42. <a href="https://doi.org/10.1007/s12532-017-0130-5">https://doi.org/10.1007/s12532-017-0130-5</a>.
</div>
<div id="ref-kellner2024dpose" class="csl-entry">
Kellner, Matthias, and Michele Ceriotti. 2024. <span>“Uncertainty Quantification by Direct Propagation of Shallow Ensembles.”</span> <em>Machine Learning: Science and Technology</em> 5 (3): 035006. <a href="https://doi.org/10.1088/2632-2153/ad594a">https://doi.org/10.1088/2632-2153/ad594a</a>.
</div>
<div id="ref-liu2024kan" class="csl-entry">
Liu, Ziming, Yixuan Wang, Sachin Vaidya, et al. 2024. <span>“<span>KAN</span>: Kolmogorov-Arnold Networks.”</span> <em>arXiv Preprint arXiv:2404.19756</em>, ahead of print. <a href="https://doi.org/10.48550/arXiv.2404.19756">https://doi.org/10.48550/arXiv.2404.19756</a>.
</div>
<div id="ref-misic2020optimization" class="csl-entry">
Mišić, Velibor V. 2020. <span>“Optimization of Tree Ensembles.”</span> <em>Operations Research</em> 68 (5): 1605–24. <a href="https://doi.org/10.1287/opre.2019.1928">https://doi.org/10.1287/opre.2019.1928</a>.
</div>
<div id="ref-ouyang2018sisso" class="csl-entry">
Ouyang, Runhai, Stefano Curtarolo, Emre Ahmetcik, Matthias Scheffler, and Luca M. Ghiringhelli. 2018. <span>“<span>SISSO</span>: A Compressed-Sensing Method for Identifying the Best Low-Dimensional Descriptor in an Immensity of Offered Candidates.”</span> <em>Physical Review Materials</em> 2 (8): 083802. <a href="https://doi.org/10.1103/PhysRevMaterials.2.083802">https://doi.org/10.1103/PhysRevMaterials.2.083802</a>.
</div>
<div id="ref-schweidtmann2019global" class="csl-entry">
Schweidtmann, Artur M., and Alexander Mitsos. 2019. <span>“Deterministic Global Optimization with Artificial Neural Networks Embedded.”</span> <em>Journal of Optimization Theory and Applications</em> 180: 925–48. <a href="https://doi.org/10.1007/s10957-018-1396-0">https://doi.org/10.1007/s10957-018-1396-0</a>.
</div>
<div id="ref-sunshine2025hyperplane" class="csl-entry">
Sunshine, Ethan M., Carolina Colombo Tedesco, Sneha A. Akhade, Matthew J. McNenly, John R. Kitchin, and Carl D. Laird. 2025. <span>“Hyperplane Decision Trees as Piecewise Linear Surrogate Models for Chemical Process Design.”</span> <em>Computers &amp; Chemical Engineering</em> 202: 109204. <a href="https://doi.org/10.1016/j.compchemeng.2025.109204">https://doi.org/10.1016/j.compchemeng.2025.109204</a>.
</div>
<div id="ref-thebelt2021entmoot" class="csl-entry">
Thebelt, Alexander, Jan Kronqvist, Miten Mistry, Robert M. Lee, Nathan Sudermann-Merx, and Ruth Misener. 2021. <span>“<span>ENTMOOT</span>: A Framework for Optimization over Ensemble Tree Models.”</span> <em>Computers &amp; Chemical Engineering</em> 151: 107343. <a href="https://doi.org/10.1016/j.compchemeng.2021.107343">https://doi.org/10.1016/j.compchemeng.2021.107343</a>.
</div>
<div id="ref-tjeng2019evaluating" class="csl-entry">
Tjeng, Vincent, Kai Xiao, and Russ Tedrake. 2019. <span>“Evaluating Robustness of Neural Networks with Mixed Integer Programming.”</span> <em>International Conference on Learning Representations</em>. <a href="https://arxiv.org/abs/1711.07356">https://arxiv.org/abs/1711.07356</a>.
</div>
<div id="ref-vielma2015mixed" class="csl-entry">
Vielma, Juan Pablo. 2015. <span>“Mixed Integer Linear Programming Formulation Techniques.”</span> <em>SIAM Review</em> 57 (1): 3–57. <a href="https://doi.org/10.1137/130915303">https://doi.org/10.1137/130915303</a>.
</div>
</div>


</section>

 ]]></content:encoded></item><item><title>How a neural network builds features</title><link>https://kitchingroup.cheme.cmu.edu/posts/neural-network-features/</link><pubDate>Fri, 23 Oct 2026 00:00:00 +0000</pubDate><category>machine learning</category><category>python</category><guid isPermaLink="true">https://kitchingroup.cheme.cmu.edu/posts/neural-network-features/</guid><description>How a neural network builds features</description><content:encoded><![CDATA[ 

Skip to main content




<link rel="stylesheet" href="https://kitchingroup.cheme.cmu.edu/posts/neural-network-features/feat.css">
<script src="https://kitchingroup.cheme.cmu.edu/posts/neural-network-features/feat.js" defer=""></script>
<p>Feature engineering is an old habit in science and engineering. We fit <img src="https://latex.codecogs.com/png.latex?%5Cln%20k"> against <img src="https://latex.codecogs.com/png.latex?1/T"> instead of <img src="https://latex.codecogs.com/png.latex?k"> against <img src="https://latex.codecogs.com/png.latex?T">, we correlate heat transfer data with the Reynolds and Prandtl numbers instead of velocity and viscosity, and we model rate laws as products of concentrations raised to powers. In each case we transform the raw measurements into quantities that make the model simple.</p>
<p>Neural networks are often described as doing this automatically: “the network learns the features.” In the <a href="https://kitchingroup.cheme.cmu.edu/posts/neural-network-interpretations/">last post</a> we saw that a network really is a learned feature transform followed by linear regression. In this post we look at what kind of features a network can actually build, and how it builds them. The short answer is that every feature is made with only two operations, a linear combination and a fixed one-dimensional bend. Neither operation multiplies or divides two inputs, and that has consequences for products, ratios and physical units.</p>
<p>A note on notation: in this post an input is a column vector <img src="https://latex.codecogs.com/png.latex?%5Cmathbf%7Bx%7D"> and a layer computes <img src="https://latex.codecogs.com/png.latex?%5Cmathbf%7BW%7D%5Cmathbf%7Bx%7D%20+%20%5Cmathbf%7Bb%7D">. The last post used row vectors (<img src="https://latex.codecogs.com/png.latex?%5Cmathbf%7Bx%7D%5Cmathbf%7BW%7D">); it is the same thing transposed.</p>
<section id="one-layer-combine-then-activate" class="level1">
<h1>One layer: combine, then activate</h1>
<p>A single neuron <img src="https://latex.codecogs.com/png.latex?j"> in the first layer does two things. It forms a weighted sum of the inputs plus a bias, and then it applies an activation function <img src="https://latex.codecogs.com/png.latex?%5Csigma">:</p>
<p><img src="https://latex.codecogs.com/png.latex?%0Az_j%20=%20%5Csum_i%20W_%7Bji%7D%20x_i%20+%20b_j,%0A%5Cqquad%0Ah_j%20=%20%5Csigma(z_j).%0A"></p>
<p>Let’s draw that. In the figures below the colored squares are input features and the grey squares are weights. A grey square next to a colored square means “multiply,” and the plus signs mean “add.”</p>
<div class="fe-fig" data-fefig="neuron"></div>
<p><em>One neuron. Every colored square is multiplied by a grey square, the products are added to a bias, and the sum is bent by σ.</em></p>
<p>A layer is many neurons that look at the same inputs with different weights. In matrix form <img src="https://latex.codecogs.com/png.latex?%5Cmathbf%7Bz%7D%20=%20%5Cmathbf%7BW%7D%5Cmathbf%7Bx%7D%20+%20%5Cmathbf%7Bb%7D">, and <img src="https://latex.codecogs.com/png.latex?%5Cmathbf%7Bh%7D%20=%20%5Csigma(%5Cmathbf%7Bz%7D)"> is applied element by element. Each <img src="https://latex.codecogs.com/png.latex?z_j"> is a mixture of the inputs, and we draw it as a square striped with the colors of the inputs in proportion to how strongly it weights each one.</p>
<div class="fe-fig" data-fefig="layer"></div>
<p><em>A layer with three inputs and four neurons. Hover over (or tap) a neuron to see the row of weights it uses. Each pre-activation zⱼ is a different blend of the same three inputs.</em></p>
<p>That is everything a layer can do. Notice what is <em>not</em> in the picture. There is no square where <img src="https://latex.codecogs.com/png.latex?x_1"> meets <img src="https://latex.codecogs.com/png.latex?x_2">, so nothing like <img src="https://latex.codecogs.com/png.latex?x_1%20x_2">, <img src="https://latex.codecogs.com/png.latex?x_1/x_2"> or <img src="https://latex.codecogs.com/png.latex?x_1%5E2"> is ever formed by the linear part. The pre-activations are exactly linear in the inputs, and so is any linear combination of them. We can check that with a random layer.</p>
<div id="814b3892" class="cell" data-execution_count="1">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb1-1"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> numpy <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">as</span> np</span>
<span id="cb1-2"></span>
<span id="cb1-3">rng <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.random.default_rng(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>)</span>
<span id="cb1-4">X <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> rng.uniform(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>, size<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">500</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>))  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 500 samples, 3 inputs</span></span>
<span id="cb1-5">W <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> rng.normal(size<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">4</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>))            <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 4 neurons</span></span>
<span id="cb1-6">b <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> rng.normal(size<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">4</span>)</span>
<span id="cb1-7"></span>
<span id="cb1-8">Z <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> X <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">@</span> W.T <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> b                        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># pre-activations</span></span>
<span id="cb1-9">H <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.tanh(Z)                         <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># features</span></span>
<span id="cb1-10"></span>
<span id="cb1-11"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> r2(A, y):</span>
<span id="cb1-12">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">"R² of a least squares fit of y on A plus a constant."</span></span>
<span id="cb1-13">    A <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.column_stack([A, np.ones(<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>(y))])</span>
<span id="cb1-14">    c <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.linalg.lstsq(A, y, rcond<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">None</span>)[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>]</span>
<span id="cb1-15">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> (y <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> A <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">@</span> c).var() <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> y.var()</span>
<span id="cb1-16"></span>
<span id="cb1-17">x1, x2 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> X[:, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>], X[:, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>]</span>
<span id="cb1-18">tests <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"each z_j from x1, x2, x3"</span>, <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">min</span>(r2(X, z) <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> z <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> Z.T)),</span>
<span id="cb1-19">         (<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"x1*x2 from the z's"</span>, r2(Z, x1 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> x2)),</span>
<span id="cb1-20">         (<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"x1/(x2+2) from the z's"</span>, r2(Z, x1 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> (x2 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>))),</span>
<span id="cb1-21">         (<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"x1*x2 from the h's"</span>, r2(H, x1 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> x2))]</span>
<span id="cb1-22"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> name, val <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> tests:</span>
<span id="cb1-23">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>name<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:24s}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;"> R² = </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>val<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:.6f}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span>)</span></code></pre></div></div>
<div class="cell-output cell-output-stdout">
<pre><code>each z_j from x1, x2, x3 R² = 1.000000
x1*x2 from the z's       R² = 0.004774
x1/(x2+2) from the z's   R² = 0.912157
x1*x2 from the h's       R² = 0.142195</code></pre>
</div>
</div>
<p>The pre-activations are perfectly linear in <img src="https://latex.codecogs.com/png.latex?%5Cmathbf%7Bx%7D">. No combination of them explains <em>any</em> of the variance of <img src="https://latex.codecogs.com/png.latex?x_1%20x_2">. (The ratio scores well only because <img src="https://latex.codecogs.com/png.latex?x_1/(x_2%20+%202)"> is nearly linear on this range.) After the activation, the features explain a little of the product, and that little bit is the subject of the next section.</p>
<p>There is a geometric way to say the same thing. A neuron’s output <img src="https://latex.codecogs.com/png.latex?%5Csigma(%5Cmathbf%7Bw%7D_j%20%5Ccdot%20%5Cmathbf%7Bx%7D%20+%20b_j)"> depends on <img src="https://latex.codecogs.com/png.latex?%5Cmathbf%7Bx%7D"> only through the single number <img src="https://latex.codecogs.com/png.latex?%5Cmathbf%7Bw%7D_j%20%5Ccdot%20%5Cmathbf%7Bx%7D">. It is constant along every line (in 3D, every plane) perpendicular to <img src="https://latex.codecogs.com/png.latex?%5Cmathbf%7Bw%7D_j">. Functions like that are called <em>ridge functions</em>: they look like a wave, a step or a ramp that is perfectly straight in one direction. A product and a ratio are not ridge functions. Their contours are hyperbolas and rays.</p>
<div class="fe-fig" data-fefig="ridge"></div>
<p><em>Left: one neuron σ(w₁x₁ + w₂x₂ + b). Its contours are always parallel straight lines, whatever the weights, bias and activation. Middle and right: the contours of x₁x₂ are hyperbolas and the contours of x₁/x₂ are rays from the origin. No single neuron can look like either one.</em></p>
</section>
<section id="where-the-nonlinearity-comes-from" class="level1">
<h1>Where the nonlinearity comes from</h1>
<p>If the linear part never multiplies inputs, a network must get products from the activation function. We can make that precise. For a network with one hidden layer,</p>
<p><img src="https://latex.codecogs.com/png.latex?%0Af(%5Cmathbf%7Bx%7D)%20=%20%5Csum_j%20c_j%20%5C,%20%5Csigma(z_j)%20+%20c_0,%0A%5Cqquad%0Az_j%20=%20%5Csum_i%20W_%7Bji%7D%20x_i%20+%20b_j,%0A"></p>
<p>take two derivatives. The chain rule gives</p>
<p><img src="https://latex.codecogs.com/png.latex?%0A%5Cfrac%7B%5Cpartial%20f%7D%7B%5Cpartial%20x_k%7D=%20%5Csum_j%20c_j%5C,%20%5Csigma'(z_j)%5C,%20W_%7Bjk%7D,%0A%5Cqquad%0A%5Cfrac%7B%5Cpartial%5E2%20f%7D%7B%5Cpartial%20x_k%20%5Cpartial%20x_l%7D%20=%20%5Csum_j%20c_j%5C,%20%5Csigma''(z_j)%5C,%20W_%7Bjk%7D%5C,%20W_%7Bjl%7D.%0A"></p>
<p>The mixed partial derivative <img src="https://latex.codecogs.com/png.latex?%5Cpartial%5E2%20f%20/%20%5Cpartial%20x_1%20%5Cpartial%20x_2"> is a good measure of how much “product” there is in a function. It is exactly 1 everywhere for <img src="https://latex.codecogs.com/png.latex?f%20=%20x_1%20x_2">, and exactly 0 for anything of the form <img src="https://latex.codecogs.com/png.latex?g(x_1)%20+%20h(x_2)">, and in particular for anything linear. The formula says that every bit of it in a neural network comes from <img src="https://latex.codecogs.com/png.latex?%5Csigma''">, the curvature of the activation function. The weights only decide where that curvature is applied and how it is shared between the inputs.</p>
<p>There are two common ways for <img src="https://latex.codecogs.com/png.latex?%5Csigma''"> to be nonzero, and they lead to quite different networks.</p>
<section id="mechanism-1-kinks-relu" class="level2">
<h2 class="anchored" data-anchor-id="mechanism-1-kinks-relu">Mechanism 1: kinks (ReLU)</h2>
<p>The rectified linear unit <img src="https://latex.codecogs.com/png.latex?%5Csigma(z)%20=%20%5Cmax(0,%20z)"> is made of two straight lines. Its second derivative is zero everywhere except at <img src="https://latex.codecogs.com/png.latex?z%20=%200">, where all of its curvature is concentrated in a kink (formally, <img src="https://latex.codecogs.com/png.latex?%5Csigma''"> is a delta function there). A ReLU network is therefore piecewise linear. In one dimension it is a polyline, and each neuron contributes one kink at <img src="https://latex.codecogs.com/png.latex?x%20=%20-b_j%20/%20w_j"> whose size is the change in slope there.</p>
<p>Here is the simplest curved function, <img src="https://latex.codecogs.com/png.latex?x%5E2">, built from ReLUs. Each unit adds a kink, and the polyline gets closer to the parabola as the kinks get closer together.</p>
<div class="fe-fig" data-fefig="relu"></div>
<p><em>Top: x² (thick grey) and a sum of ReLU units (dark), with the individual weighted units in color. Bottom: the slopes. The slope of x² changes smoothly; the network’s slope is a staircase, and each step is one kink. Move the slider to add units.</em></p>
<p>With <img src="https://latex.codecogs.com/png.latex?n"> units, and so <img src="https://latex.codecogs.com/png.latex?n"> evenly spaced pieces on <img src="https://latex.codecogs.com/png.latex?%5B-1,%201%5D">, the largest error is <img src="https://latex.codecogs.com/png.latex?1/n%5E2">. The error falls quickly, but the curvature is never smooth: the slope changes only in jumps, and between the jumps <img src="https://latex.codecogs.com/png.latex?%5Csigma''%20=%200">.</p>
<p>Once you can make a square, you can make a product, because of the polarization identity</p>
<p><img src="https://latex.codecogs.com/png.latex?%0Ax_1%20x_2%20=%20%5Ctfrac%7B1%7D%7B4%7D%5Cleft%5B(x_1%20+%20x_2)%5E2%20-%20(x_1%20-%20x_2)%5E2%5Cright%5D.%0A"></p>
<p>The right side is a difference of two squares of <em>linear combinations of the inputs</em>. That is exactly what neurons can see. Neurons with weights proportional to <img src="https://latex.codecogs.com/png.latex?(1,%201)"> build the first square as a function of <img src="https://latex.codecogs.com/png.latex?x_1%20+%20x_2">, neurons with weights proportional to <img src="https://latex.codecogs.com/png.latex?(1,%20-1)"> build the second, and the output layer subtracts them. The network never multiplies <img src="https://latex.codecogs.com/png.latex?x_1"> by <img src="https://latex.codecogs.com/png.latex?x_2">. It approximates two one-dimensional parabolas along two diagonal directions, and their difference happens to be a product.</p>
<p>This should give you pause about what a neural network “learns”. It is learning some equivalent looking representation of a function that goes through the data.</p>
</section>
<section id="mechanism-2-curvature-tanh-sigmoid-softplus-gelu" class="level2">
<h2 class="anchored" data-anchor-id="mechanism-2-curvature-tanh-sigmoid-softplus-gelu">Mechanism 2: curvature (tanh, sigmoid, softplus, GELU)</h2>
<p>Smooth activations like <img src="https://latex.codecogs.com/png.latex?%5Ctanh"> bend everywhere, so <img src="https://latex.codecogs.com/png.latex?%5Csigma''"> is nonzero almost everywhere. How much a neuron bends depends on where it operates on the curve, and the bias sets that operating point. Expand <img src="https://latex.codecogs.com/png.latex?%5Ctanh"> around an operating point <img src="https://latex.codecogs.com/png.latex?b">:</p>
<p><img src="https://latex.codecogs.com/png.latex?%0A%5Ctanh(b%20+%20%5Cdelta)%20%5Capprox%20%5Ctanh%20b%20+%20%5Coperatorname%7Bsech%7D%5E2%20b%20%5C;%5Cdelta%20-%20%5Ctanh%20b%20%5C,%5Coperatorname%7Bsech%7D%5E2%20b%20%5C;%5Cdelta%5E2%20+%20%5Ccdots%0A"></p>
<p>At <img src="https://latex.codecogs.com/png.latex?b%20=%200"> the quadratic term vanishes (tanh is odd, so it is locally a straight line with a cubic correction). On the shoulders of the curve the quadratic term is largest; <img src="https://latex.codecogs.com/png.latex?%7C%5Ctanh''%7C"> peaks near <img src="https://latex.codecogs.com/png.latex?b%20%5Capprox%20%5Cpm%200.66">. Now let <img src="https://latex.codecogs.com/png.latex?%5Cdelta%20=%20w_1%20x_1%20+%20w_2%20x_2"> be the neuron’s input. Then</p>
<p><img src="https://latex.codecogs.com/png.latex?%0A%5Cdelta%5E2%20=%20w_1%5E2%20x_1%5E2%20+%202%20w_1%20w_2%20%5C,%20x_1%20x_2%20+%20w_2%5E2%20x_2%5E2,%0A"></p>
<p>and there is the product. The curvature of the activation turns a sum into a square, and the square of a sum contains a cross term. It always comes bundled with <img src="https://latex.codecogs.com/png.latex?x_1%5E2"> and <img src="https://latex.codecogs.com/png.latex?x_2%5E2"> terms, though, and with all the higher order terms of the series. To isolate the cross term we use the polarization identity again. With <img src="https://latex.codecogs.com/png.latex?g(s)%20=%20%5Ctanh(b%20+%20%5Cvarepsilon%20s)">, four neurons give</p>
<p><img src="https://latex.codecogs.com/png.latex?%0Ax_1%20x_2%20%5Capprox%20%5Cfrac%7Bg(x_1%20+%20x_2)%20+%20g(-x_1%20-%20x_2)%20-%20g(x_1%20-%20x_2)%20-%20g(-x_1%20+%20x_2)%7D%7B4%20%5Cvarepsilon%5E2%20%5Ctanh''(b)%7D%20.%0A"></p>
<p>Adding <img src="https://latex.codecogs.com/png.latex?g(s)"> and <img src="https://latex.codecogs.com/png.latex?g(-s)"> cancels the odd terms (including the linear one), and subtracting the two pairs cancels the squares, leaving <img src="https://latex.codecogs.com/png.latex?4%5Cvarepsilon%5E2%20%5Ctanh''(b)%5C,x_1%20x_2"> plus terms of order <img src="https://latex.codecogs.com/png.latex?%5Cvarepsilon%5E4">.</p>
<div class="fe-fig" data-fefig="tanh"></div>
<p><em>Left: tanh, the operating point set by the bias, and the local parabola there. The curvature is what makes products possible. Right: four tanh neurons combined as above, compared with the hyperbolic contours of x₁x₂ (green dashes). Small ε is accurate but needs enormous output weights.</em></p>
<p>There is a catch, and the right panel shows it. The approximation is only good when <img src="https://latex.codecogs.com/png.latex?%5Cvarepsilon"> is small, so that each neuron stays in the region where the parabola describes it. But the output weights are <img src="https://latex.codecogs.com/png.latex?1/(4%5Cvarepsilon%5E2%20%5Ctanh''(b))">, which become huge. The network makes a product by taking the difference of four large, nearly equal numbers. Here is the same thing in code.</p>
<div id="e599db7b" class="cell" data-execution_count="2">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb3-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> g(s, eps, b<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.66</span>):</span>
<span id="cb3-2">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> np.tanh(b <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> eps <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> s)</span>
<span id="cb3-3"></span>
<span id="cb3-4"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> tanh2(b):</span>
<span id="cb3-5">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">"Second derivative of tanh."</span></span>
<span id="cb3-6">    t <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.tanh(b)</span>
<span id="cb3-7">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> t <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> (<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> t<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">**</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>)</span>
<span id="cb3-8"></span>
<span id="cb3-9">grid <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.linspace(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">101</span>)</span>
<span id="cb3-10">x1, x2 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.meshgrid(grid, grid)</span>
<span id="cb3-11">u, v <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> x1 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> x2, x1 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> x2</span>
<span id="cb3-12"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> eps <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> [<span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">1.0</span>, <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.3</span>, <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.1</span>, <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.03</span>]:</span>
<span id="cb3-13">    c <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> (<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">4</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> eps<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">**</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> tanh2(<span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.66</span>))</span>
<span id="cb3-14">    approx <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> c <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> (g(u, eps) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> g(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span>u, eps)</span>
<span id="cb3-15">                  <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> g(v, eps) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> g(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span>v, eps))</span>
<span id="cb3-16">    err <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">abs</span>(approx <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> x1 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> x2).<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">max</span>()</span>
<span id="cb3-17">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"eps = </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>eps<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:4.2f}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">  |output weight| = </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">abs</span>(c)<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:6.1f}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb3-18">          <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"  max error = </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>err<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:.1e}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span>)</span></code></pre></div></div>
<div class="cell-output cell-output-stdout">
<pre><code>eps = 1.00  |output weight| =    0.3  max error = 6.6e-01
eps = 0.30  |output weight| =    3.6  max error = 1.1e-01
eps = 0.10  |output weight| =   32.5  max error = 1.3e-02
eps = 0.03  |output weight| =  360.8  max error = 1.2e-03</code></pre>
</div>
</div>
<p>This is typical of what a trained network does too: hidden units with large outputs of opposite sign that mostly cancel. We saw that cancellation in the learned basis functions of the last post.</p>
<p>Ratios are harder than products. Over a range where <img src="https://latex.codecogs.com/png.latex?x_2"> stays away from zero, <img src="https://latex.codecogs.com/png.latex?1/x_2"> is just another smooth curved function of one linear combination, and it can be approximated with kinks or curvature like <img src="https://latex.codecogs.com/png.latex?x%5E2"> was. Near <img src="https://latex.codecogs.com/png.latex?x_2%20=%200"> it has a pole, and <img src="https://latex.codecogs.com/png.latex?%5Ctanh"> units are bounded and ReLU units grow only linearly, so no finite network gets close there. Then the ratio has to be assembled from that and a product, so it costs at least two of the tricks above.</p>
<p>One activation function would make all of this easy. If the inputs were <img src="https://latex.codecogs.com/png.latex?%5Cln%20x_1"> and <img src="https://latex.codecogs.com/png.latex?%5Cln%20x_2"> and the activation were <img src="https://latex.codecogs.com/png.latex?%5Cexp">, a single neuron would compute <img src="https://latex.codecogs.com/png.latex?%5Cexp(a%20%5Cln%20x_1%20+%20b%20%5Cln%20x_2)%20=%20x_1%5Ea%20x_2%5Eb">, with products, ratios and powers all exact. We will come back to that at the end, because it is really a statement about features.</p>
</section>
</section>
<section id="the-sin-of-adding-kelvins-to-pascals" class="level1">
<h1>The sin of adding kelvins to pascals</h1>
<p>Let’s give the inputs physical meaning. Suppose a neuron sees a temperature <img src="https://latex.codecogs.com/png.latex?T"> in kelvin, a pressure <img src="https://latex.codecogs.com/png.latex?P"> in pascal and a concentration <img src="https://latex.codecogs.com/png.latex?C"> in mol/L:</p>
<p><img src="https://latex.codecogs.com/png.latex?%0Az%20=%20W_T%5C,%20T%20+%20W_P%5C,%20P%20+%20W_C%5C,%20C%20+%20b%20.%0A"></p>
<p>In physics you cannot add quantities with different units. “300 K + 101325 Pa” is not a number; it is an error. A neural network adds them anyway, because all it sees are the numbers 300 and 101325. The only way the sum makes sense is if each weight carries units that convert its term into a common unit:</p>
<p><img src="https://latex.codecogs.com/png.latex?%0A%5BW_T%5D%20=%20%5Cfrac%7B%5Bz%5D%7D%7B%5Cmathrm%7BK%7D%7D,%20%5Cqquad%20%5BW_P%5D%20=%20%5Cfrac%7B%5Bz%5D%7D%7B%5Cmathrm%7BPa%7D%7D,%20%5Cqquad%20%5BW_C%5D%20=%20%5Cfrac%7B%5Bz%5D%7D%7B%5Cmathrm%7Bmol/L%7D%7D,%20%5Cqquad%20%5Bb%5D%20=%20%5Bz%5D.%0A"></p>
<p>What is <img src="https://latex.codecogs.com/png.latex?%5Bz%5D">? It is the argument of <img src="https://latex.codecogs.com/png.latex?%5Csigma">. A transcendental function like <img src="https://latex.codecogs.com/png.latex?%5Ctanh"> needs a dimensionless argument, which you can see from its series <img src="https://latex.codecogs.com/png.latex?z%20-%20z%5E3/3%20+%20%5Cldots">: you cannot add <img src="https://latex.codecogs.com/png.latex?z"> and <img src="https://latex.codecogs.com/png.latex?z%5E3"> unless <img src="https://latex.codecogs.com/png.latex?z"> is a pure number. So <img src="https://latex.codecogs.com/png.latex?z"> is dimensionless, the bias is dimensionless, and every weight has the inverse units of its input:</p>
<p><img src="https://latex.codecogs.com/png.latex?%0AW_T%20=%20%5Cfrac%7B1%7D%7BT%5E*%7D,%20%5Cqquad%20W_P%20=%20%5Cfrac%7B1%7D%7BP%5E*%7D,%20%5Cqquad%20W_C%20=%20%5Cfrac%7B1%7D%7BC%5E*%7D%20.%0A"></p>
<p>Each weight is a reciprocal <em>characteristic scale</em>. A neuron is implicitly asking “how many <img src="https://latex.codecogs.com/png.latex?T%5E*"> is this temperature, how many <img src="https://latex.codecogs.com/png.latex?P%5E*"> is this pressure,” and adding the answers. Learning the weights means learning those scales, one set per neuron.</p>
<div class="fe-fig" data-fefig="units"></div>
<p><em>The neuron from before with units. Each grey square must carry the inverse units of the colored square it multiplies, so that every product is dimensionless and the sum is legal.</em></p>
<p>The same bookkeeping goes through the whole network. The features <img src="https://latex.codecogs.com/png.latex?h_j"> are dimensionless, so the output weights <img src="https://latex.codecogs.com/png.latex?c_j"> carry the units of <img src="https://latex.codecogs.com/png.latex?y">. In the mixed partial derivative <img src="https://latex.codecogs.com/png.latex?%5Csum_j%20c_j%20%5Csigma''(z_j)%20W_%7BjT%7D%20W_%7BjP%7D">, the units are <img src="https://latex.codecogs.com/png.latex?%5By%5D/(%5Cmathrm%7BK%7D%5Ccdot%5Cmathrm%7BPa%7D)">, exactly the units of <img src="https://latex.codecogs.com/png.latex?%5Cpartial%5E2%20y%20/%20%5Cpartial%20T%20%5Cpartial%20P">. The network is dimensionally consistent, but only because the weights silently carry units. ReLU is slightly more forgiving because <img src="https://latex.codecogs.com/png.latex?%5Cmax(0,%20%5Clambda%20z)%20=%20%5Clambda%20%5Cmax(0,%20z)"> for <img src="https://latex.codecogs.com/png.latex?%5Clambda%20%3E%200">, so its argument may have units, but the terms of the sum still have to agree, so the weights still convert between input units.</p>
<p>The software does not know any of this, and three things go wrong when we forget it.</p>
<p><strong>Initialization.</strong> Weights are usually initialized as random numbers of order one. With raw inputs, that means <img src="https://latex.codecogs.com/png.latex?T%5E*%20%5Csim%201"> K and <img src="https://latex.codecogs.com/png.latex?P%5E*%20%5Csim%201"> Pa. A pressure of a few bar is then hundreds of thousands of “characteristic pressures,” and every neuron is saturated before training starts.</p>
<div id="e85a82dd" class="cell" data-execution_count="3">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb5" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb5-1">n <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">500</span></span>
<span id="cb5-2">T <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> rng.uniform(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">300</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">500</span>, n)    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># K</span></span>
<span id="cb5-3">P <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> rng.uniform(<span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">1e5</span>, <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">1e6</span>, n)    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Pa</span></span>
<span id="cb5-4">X <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.column_stack([T, P])</span>
<span id="cb5-5"></span>
<span id="cb5-6">W <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> rng.normal(size<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">100</span>))   <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 100 neurons, weights ~ 1</span></span>
<span id="cb5-7">b <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> rng.normal(size<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">100</span>)</span>
<span id="cb5-8"></span>
<span id="cb5-9">Xs <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> (X <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> X.mean(axis<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>)) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> X.std(axis<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>)</span>
<span id="cb5-10"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> name, A <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> [(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"raw (K, Pa)"</span>, X), (<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"standardized"</span>, Xs)]:</span>
<span id="cb5-11">    sat <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.mean(np.<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">abs</span>(A <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">@</span> W <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> b) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>)</span>
<span id="cb5-12">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>name<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:13s}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;"> saturated (|z| &gt; 3): </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>sat<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:.2f}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span>)</span></code></pre></div></div>
<div class="cell-output cell-output-stdout">
<pre><code>raw (K, Pa)   saturated (|z| &gt; 3): 1.00
standardized  saturated (|z| &gt; 3): 0.09</code></pre>
</div>
</div>
<p>A saturated tanh unit has <img src="https://latex.codecogs.com/png.latex?%5Csigma'%20%5Capprox%200"> and <img src="https://latex.codecogs.com/png.latex?%5Csigma''%20%5Capprox%200">: it contributes no gradient and none of the curvature we needed for products.</p>
<p><strong>Regularization.</strong> Weight decay adds <img src="https://latex.codecogs.com/png.latex?%5Clambda%20%5Csum%20W%5E2"> to the loss. With mixed inputs that is a sum of terms with units of <img src="https://latex.codecogs.com/png.latex?1/%5Cmathrm%7BK%7D%5E2"> and <img src="https://latex.codecogs.com/png.latex?1/%5Cmathrm%7BPa%7D%5E2">, which is another sin. In practice it means the answer depends on your choice of units. Here is ridge regression (the linear model is enough to show it) fitted to the same data with pressure in Pa, bar and MPa, and with temperature in K and °C.</p>
<div id="177e1bda" class="cell" data-execution_count="4">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb7" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb7-1">y <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.01</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> T <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">3e-6</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> P <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> rng.normal(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>, <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.1</span>, n)</span>
<span id="cb7-2"></span>
<span id="cb7-3"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> ridge(X, y, lam<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">100.0</span>):</span>
<span id="cb7-4">    A <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.column_stack([X, np.ones(<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>(y))])</span>
<span id="cb7-5">    I <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.eye(A.shape[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>])</span>
<span id="cb7-6">    I[<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>, <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>] <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>                  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># do not penalize the bias</span></span>
<span id="cb7-7">    theta <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.linalg.solve(A.T <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">@</span> A <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> lam <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> I, A.T <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">@</span> y)</span>
<span id="cb7-8">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> A <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">@</span> theta</span>
<span id="cb7-9"></span>
<span id="cb7-10"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> name, Tu, Pu <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> [(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"K, Pa"</span>, T, P),</span>
<span id="cb7-11">                     (<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"K, bar"</span>, T, P <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">1e5</span>),</span>
<span id="cb7-12">                     (<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"°C, bar"</span>, T <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">273.15</span>, P <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">1e5</span>),</span>
<span id="cb7-13">                     (<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"K, MPa"</span>, T, P <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">1e6</span>)]:</span>
<span id="cb7-14">    yp <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> ridge(np.column_stack([Tu, Pu]), y)</span>
<span id="cb7-15">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>name<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:8s}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;"> first predictions:"</span>, np.<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">round</span>(yp[:<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>], <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>))</span></code></pre></div></div>
<div class="cell-output cell-output-stdout">
<pre><code>K, Pa    first predictions: [9.133 8.565 6.603]
K, bar   first predictions: [9.102 8.54  6.63 ]
°C, bar  first predictions: [9.102 8.54  6.63 ]
K, MPa   first predictions: [8.301 7.925 7.324]</code></pre>
</div>
</div>
<p>The same data and the same model give different predictions when we change the pressure units. Changing kelvin to Celsius does nothing, because a shift of origin is absorbed by the bias, which is not penalized. A change of <em>scale</em> is not absorbed by anything.</p>
<p><strong>Standardization is a units fix.</strong> Replacing each input by <img src="https://latex.codecogs.com/png.latex?(x%20-%20%5Cmu)/s"> divides it by a scale with the same units, so the network sees dimensionless numbers and the weights become dimensionless too. That is why standardizing inputs helps so much. It is worth being clear about what it does, though: it chooses the characteristic scales from the spread of the training data, not from the physics. A dimensionless group chosen from the physics, such as <img src="https://latex.codecogs.com/png.latex?T/T_c">, <img src="https://latex.codecogs.com/png.latex?P/P_c"> or a Reynolds number, does the same job with scales that mean something and that transfer to new conditions.</p>
<p>There is one more consequence. Physical laws are full of products and ratios (<img src="https://latex.codecogs.com/png.latex?PV%20=%20nRT">, <img src="https://latex.codecogs.com/png.latex?k%20=%20A%20e%5E%7B-E_a/RT%7D">, a rate <img src="https://latex.codecogs.com/png.latex?r%20=%20k%20C_A%20C_B">), and products and ratios are dimensionally fine: K·Pa is a perfectly good unit. Sums are what require matching units. So the network’s single native operation, the sum, is the one operation physics constrains most, and the operations physics uses most freely are the ones the network has to approximate.</p>
</section>
<section id="composition-features-of-features" class="level1">
<h1>Composition: features of features</h1>
<p>A deep network stacks layers. The second layer does exactly what the first one did, with the first layer’s features as its inputs:</p>
<p><img src="https://latex.codecogs.com/png.latex?%0A%5Cmathbf%7Bz%7D%5E%7B(2)%7D%20=%20%5Cmathbf%7BW%7D%5E%7B(2)%7D%20%5Cmathbf%7Bh%7D%5E%7B(1)%7D%20+%20%5Cmathbf%7Bb%7D%5E%7B(2)%7D,%0A%5Cqquad%0A%5Cmathbf%7Bh%7D%5E%7B(2)%7D%20=%20%5Csigma%5Cbig(%5Cmathbf%7Bz%7D%5E%7B(2)%7D%5Cbig),%0A%5Cqquad%0A%5Cmathbf%7Bh%7D%5E%7B(1)%7D%20=%20%5Csigma%5Cbig(%5Cmathbf%7BW%7D%5E%7B(1)%7D%20%5Cmathbf%7Bx%7D%20+%20%5Cmathbf%7Bb%7D%5E%7B(1)%7D%5Cbig).%0A"></p>
<p>In the square picture, each colored input square of the second layer is a whole first-layer neuron. The structure repeats: grey times feature, add, bend.</p>
<div class="fe-fig" data-fefig="compose"></div>
<p><em>A second-layer neuron (top). Its colored squares are first-layer features, and each of those is itself a bent sum of grey-times-color terms (below). However deep the nesting goes, every colored square sits next to a grey square and never next to another colored square. The product and ratio at the bottom are the patterns that never appear.</em></p>
<p>Written out, a second-layer pre-activation is</p>
<p><img src="https://latex.codecogs.com/png.latex?%0Az%5E%7B(2)%7D_k%20=%20%5Csum_j%20W%5E%7B(2)%7D_%7Bkj%7D%20%5C,%20%5Csigma%5CBig(%5Csum_i%20W%5E%7B(1)%7D_%7Bji%7D%20x_i%20+%20b%5E%7B(1)%7D_j%5CBig)%20+%20b%5E%7B(2)%7D_k%20.%0A"></p>
<p>There is still no <img src="https://latex.codecogs.com/png.latex?x_1%20x_2"> anywhere in it. Depth does not add a new operation; it applies the same two operations again. What depth changes is how the bends combine. Take two derivatives of a two-layer network:</p>
<p><img src="https://latex.codecogs.com/png.latex?%0A%5Cfrac%7B%5Cpartial%5E2%20z%5E%7B(2)%7D_k%7D%7B%5Cpartial%20x_1%20%5Cpartial%20x_2%7D%20=%20%5Csum_j%20W%5E%7B(2)%7D_%7Bkj%7D%5C,%20%5Csigma''%5Cbig(z%5E%7B(1)%7D_j%5Cbig)%5C,%20W%5E%7B(1)%7D_%7Bj1%7D%20W%5E%7B(1)%7D_%7Bj2%7D,%0A"></p>
<p><img src="https://latex.codecogs.com/png.latex?%0A%5Cfrac%7B%5Cpartial%5E2%20f%7D%7B%5Cpartial%20x_1%20%5Cpartial%20x_2%7D%20=%20%5Csum_k%20c_k%20%5Cleft%5B%20%5Csigma''%5Cbig(z%5E%7B(2)%7D_k%5Cbig)%20%5Cfrac%7B%5Cpartial%20z%5E%7B(2)%7D_k%7D%7B%5Cpartial%20x_1%7D%20%5Cfrac%7B%5Cpartial%20z%5E%7B(2)%7D_k%7D%7B%5Cpartial%20x_2%7D%20+%20%5Csigma'%5Cbig(z%5E%7B(2)%7D_k%5Cbig)%20%5Cfrac%7B%5Cpartial%5E2%20z%5E%7B(2)%7D_k%7D%7B%5Cpartial%20x_1%20%5Cpartial%20x_2%7D%20%5Cright%5D.%0A"></p>
<p>Every term still contains a <img src="https://latex.codecogs.com/png.latex?%5Csigma''">, either from the second layer acting on its (now curved) inputs, or passed through from the first layer. Depth gives the network more ways to place curvature, but curvature is still the only source of interaction between inputs. Let’s check the formula against finite differences for a random network, and also try the same network with ReLU.</p>
<div id="4c5391a2" class="cell" data-execution_count="5">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb9" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb9-1">W1, b1 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> rng.normal(size<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>)), rng.normal(size<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5</span>)  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 2 -&gt; 5</span></span>
<span id="cb9-2">W2, b2 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> rng.normal(size<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">4</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5</span>)), rng.normal(size<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">4</span>)  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 5 -&gt; 4</span></span>
<span id="cb9-3">c <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> rng.normal(size<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">4</span>)</span>
<span id="cb9-4"></span>
<span id="cb9-5">relu <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">lambda</span> z: np.maximum(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>, z)</span>
<span id="cb9-6"></span>
<span id="cb9-7"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> f(x, act<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>np.tanh):</span>
<span id="cb9-8">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> c <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">@</span> act(W2 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">@</span> act(W1 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">@</span> x <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> b1) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> b2)</span>
<span id="cb9-9"></span>
<span id="cb9-10"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> mixed_fd(x, act<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>np.tanh, h<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">1e-4</span>):</span>
<span id="cb9-11">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">"∂²f/∂x1∂x2 by central differences."</span></span>
<span id="cb9-12">    e1, e2 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.array([h, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>]), np.array([<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>, h])</span>
<span id="cb9-13">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> (f(x <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> e1 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> e2, act) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> f(x <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> e1 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> e2, act)</span>
<span id="cb9-14">            <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> f(x <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> e1 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> e2, act) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> f(x <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> e1 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> e2, act)</span>
<span id="cb9-15">            ) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> (<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">4</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> h<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">**</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>)</span>
<span id="cb9-16"></span>
<span id="cb9-17"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> mixed_formula(x):</span>
<span id="cb9-18">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">"∂²f/∂x1∂x2 for tanh from the chain rule above."</span></span>
<span id="cb9-19">    a1 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.tanh(W1 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">@</span> x <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> b1)</span>
<span id="cb9-20">    s1, s1pp <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> a1<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">**</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>, <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> a1 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> (<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> a1<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">**</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>)   <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># σ', σ''</span></span>
<span id="cb9-21">    a2 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.tanh(W2 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">@</span> a1 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> b2)</span>
<span id="cb9-22">    s2, s2pp <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> a2<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">**</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>, <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> a2 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> (<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> a2<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">**</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>)</span>
<span id="cb9-23">    dz2 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> W2 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">@</span> (s1[:, <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">None</span>] <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> W1)              <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># ∂z2/∂x</span></span>
<span id="cb9-24">    d2z2 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> W2 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">@</span> (s1pp <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> W1[:, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>] <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> W1[:, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>])   <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># ∂²z2/∂x1∂x2</span></span>
<span id="cb9-25">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> c <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">@</span> (s2pp <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> dz2[:, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>] <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> dz2[:, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>] <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> s2 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> d2z2)</span>
<span id="cb9-26"></span>
<span id="cb9-27"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"x               tanh formula  tanh FD    relu FD"</span>)</span>
<span id="cb9-28"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> x <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> rng.uniform(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>, size<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>)):</span>
<span id="cb9-29">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>np<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">round</span>(x, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>)<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">!s:14s}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">  </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>mixed_formula(x)<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:+.6f}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">  "</span></span>
<span id="cb9-30">          <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>mixed_fd(x)<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:+.6f}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">  </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>mixed_fd(x, relu)<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:+.6f}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span>)</span></code></pre></div></div>
<div class="cell-output cell-output-stdout">
<pre><code>x               tanh formula  tanh FD    relu FD
[-0.51  0.89]   -0.072855  -0.072855  +0.000000
[ 0.82 -0.74]   +0.057736  +0.057736  +0.000000
[-0.9   0.42]   +0.276423  +0.276423  +0.000000</code></pre>
</div>
</div>
<p>For tanh the formula and the finite differences agree. For ReLU the mixed partial is zero, at these points and almost everywhere else, because <img src="https://latex.codecogs.com/png.latex?%5Csigma''%20=%200"> between the kinks. A ReLU network of any depth is still piecewise linear. What depth buys it is the number of pieces: each layer can fold the regions created by the layer before, so the number of linear regions can grow much faster with depth than with width. A ReLU network that approximates <img src="https://latex.codecogs.com/png.latex?x_1%20x_2"> does so by tiling the plane with many small flat facets that bend along creases.</p>
</section>
<section id="feature-engineering-revisited" class="level1">
<h1>Feature engineering, revisited</h1>
<p>Put the pieces together. A neuron can only add scaled inputs and bend the sum. Products and ratios have to be approximated with kinks or curvature, using many neurons, large canceling weights, or both, and only over the range where the network was trained. Units are carried silently by the weights, and the software does not check them.</p>
<p>So what does “the network learns the features” mean? It learns features in the vocabulary of sums and bends. If the relationship you are modeling is naturally written in that vocabulary, the network will find it efficiently. If it is naturally a product of powers, which describes a lot of physical science, you can change the vocabulary for it. Take logarithms of positive inputs (after dividing by reference values, so the logarithms are dimensionless):</p>
<p><img src="https://latex.codecogs.com/png.latex?%0A%5Cln%5Cfrac%7By%7D%7By%5E%5Ccirc%7D%20=%20a%20%5Cln%5Cfrac%7Bx_1%7D%7Bx_1%5E%5Ccirc%7D%20+%20b%20%5Cln%5Cfrac%7Bx_2%7D%7Bx_2%5E%5Ccirc%7D%20+%20%5Cldots%0A%5Cquad%5CLongleftrightarrow%5Cquad%0Ay%20%5Cpropto%20x_1%5Ea%5C,%20x_2%5Eb%20%5Ccdots%0A"></p>
<p>A product of powers is a <em>linear combination</em> of logarithms, which is the one thing the network does exactly. Here is a small test: learn <img src="https://latex.codecogs.com/png.latex?y%20=%20x_1%20x_2%20/%20x_3"> from 300 samples with <img src="https://latex.codecogs.com/png.latex?x_i%20%5Cin%20%5B1,%203%5D">, once with a tanh network on the (standardized) raw inputs and once with plain linear regression on <img src="https://latex.codecogs.com/png.latex?%5Cln%20x_i">. Then test both outside the training range, on <img src="https://latex.codecogs.com/png.latex?x_i%20%5Cin%20%5B3,%205%5D">.</p>
<div id="3106f524" class="cell" data-execution_count="6">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb11" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb11-1"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> scipy.optimize <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> minimize</span>
<span id="cb11-2"></span>
<span id="cb11-3"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> sample(lo, hi, n):</span>
<span id="cb11-4">    X <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> rng.uniform(lo, hi, size<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>(n, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>))</span>
<span id="cb11-5">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> X, X[:, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>] <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> X[:, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>] <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> X[:, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>]</span>
<span id="cb11-6"></span>
<span id="cb11-7">Xtr, ytr <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> sample(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">300</span>)</span>
<span id="cb11-8">Xte, yte <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> sample(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">200</span>)</span>
<span id="cb11-9">mu, sd <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> Xtr.mean(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>), Xtr.std(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>)</span>
<span id="cb11-10">ym, ys <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> ytr.mean(), ytr.std()</span>
<span id="cb11-11"></span>
<span id="cb11-12">m <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">10</span>                                  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># hidden neurons</span></span>
<span id="cb11-13"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> net(p, X):</span>
<span id="cb11-14">    W, b <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> p[:<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> m].reshape(m, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>), p[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> m:<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">4</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> m]</span>
<span id="cb11-15">    c, c0 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> p[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">4</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> m:<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> m], p[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> m]</span>
<span id="cb11-16">    H <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.tanh((X <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> mu) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> sd <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">@</span> W.T <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> b)</span>
<span id="cb11-17">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> (H <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">@</span> c <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> c0) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> ys <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> ym</span>
<span id="cb11-18"></span>
<span id="cb11-19">loss <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">lambda</span> p: np.mean(((net(p, Xtr) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> ytr) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> ys)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">**</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>)</span>
<span id="cb11-20">p0 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.5</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> rng.normal(size<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> m <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>)</span>
<span id="cb11-21">p <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> minimize(loss, p0, method<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"BFGS"</span>,</span>
<span id="cb11-22">             options<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>{<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"maxiter"</span>: <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3000</span>}).x</span>
<span id="cb11-23"></span>
<span id="cb11-24">A <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">lambda</span> X: np.column_stack([np.log(X), np.ones(<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>(X))])</span>
<span id="cb11-25">theta <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.linalg.lstsq(A(Xtr), np.log(ytr), rcond<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">None</span>)[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>]</span>
<span id="cb11-26">loglin <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">lambda</span> X: np.exp(A(X) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">@</span> theta)</span>
<span id="cb11-27"></span>
<span id="cb11-28">rmse <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">lambda</span> a, b: np.sqrt(np.mean((a <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> b)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">**</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>))</span>
<span id="cb11-29"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"model           params  train RMSE  test RMSE"</span>)</span>
<span id="cb11-30">models <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"tanh network"</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> m <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>, <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">lambda</span> X: net(p, X)),</span>
<span id="cb11-31">          (<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"log-linear"</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">4</span>, loglin)]</span>
<span id="cb11-32"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> name, k, model <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> models:</span>
<span id="cb11-33">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>name<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:14s}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">  </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>k<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:6d}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">  </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>rmse(model(Xtr), ytr)<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:10.1e}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb11-34">          <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"  </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>rmse(model(Xte), yte)<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:9.1e}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span>)</span>
<span id="cb11-35"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"exponents:"</span>, np.<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">round</span>(theta[:<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>], <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">6</span>))</span></code></pre></div></div>
<div class="cell-output cell-output-stdout">
<pre><code>model           params  train RMSE  test RMSE
tanh network        51     3.9e-03    1.6e-01
log-linear           4     5.7e-16    2.6e-15
exponents: [ 1.  1. -1.]</code></pre>
</div>
</div>
<p>Both models fit the training data well. The network, with 51 parameters, built an approximation to the product out of curvature, and it degrades as soon as it leaves the region where that approximation was built. The log-linear model has four parameters, recovers the exponents <img src="https://latex.codecogs.com/png.latex?(1,%201,%20-1)"> exactly, and extrapolates perfectly, because in log space the true function is a linear combination of the features. Of course this example was chosen to make the point, and a real problem will not be an exact power law. The lesson is not “always use logarithms.” It is that feature engineering and neural networks are not alternatives. The network builds features from sums and bends, and a good input representation (log transforms for multiplicative effects, dimensionless groups for units, physically meaningful scales) puts the structure you already know into the one operation the network does exactly. Then the network can spend its neurons and its curvature on the parts you do not know.</p>


</section>

 ]]></content:encoded></item><item><title>Interpretations of what a neural network is</title><link>https://kitchingroup.cheme.cmu.edu/posts/neural-network-interpretations/</link><pubDate>Wed, 21 Oct 2026 00:00:00 +0000</pubDate><category>machine learning</category><category>python</category><guid isPermaLink="true">https://kitchingroup.cheme.cmu.edu/posts/neural-network-interpretations/</guid><description>Interpretations of what a neural network is</description><content:encoded><![CDATA[ 

Skip to main content




<link rel="stylesheet" href="https://kitchingroup.cheme.cmu.edu/posts/neural-network-interpretations/nn.css">
<script src="https://kitchingroup.cheme.cmu.edu/posts/neural-network-interpretations/nn.js" defer=""></script>
<p>There are many ways to think about what a neural network is. In this series we look at a few of them. Each one is a little different, and each one makes some property of neural networks easier to understand.</p>
<section id="part-1-a-basis-set-expansion" class="level1">
<h1>Part 1: a basis set expansion</h1>
<p>Let’s start with something familiar: the Fourier series. On the interval <img src="https://latex.codecogs.com/png.latex?%5B-%5Cpi,%20%5Cpi%5D"> we can write a function as</p>
<p><img src="https://latex.codecogs.com/png.latex?%0Af(x)%20%5Capprox%20%5Cfrac%7Ba_0%7D%7B2%7D%20+%20%5Csum_%7Bn=1%7D%5E%7BN%7D%20%5Cleft%5B%20a_n%20%5Ccos(n%20x)%20+%20b_n%20%5Csin(n%20x)%20%5Cright%5D%0A"></p>
<p>This is a <em>basis set expansion</em>: <img src="https://latex.codecogs.com/png.latex?f"> is approximated by a weighted sum of fixed functions <img src="https://latex.codecogs.com/png.latex?%5C%7B1,%20%5Ccos%20x,%20%5Csin%20x,%20%5Ccos%202x,%20%5Csin%202x,%20%5Cldots%5C%7D">. The only unknowns are the coefficients <img src="https://latex.codecogs.com/png.latex?a_n"> and <img src="https://latex.codecogs.com/png.latex?b_n">.</p>
<section id="computing-the-coefficients" class="level2">
<h2 class="anchored" data-anchor-id="computing-the-coefficients">Computing the coefficients</h2>
<p>In general, if we expand <img src="https://latex.codecogs.com/png.latex?f"> in some basis <img src="https://latex.codecogs.com/png.latex?%5Cphi_k(x)">,</p>
<p><img src="https://latex.codecogs.com/png.latex?%0Af(x)%20%5Capprox%20%5Csum_%7Bk%7D%20c_k%20%5Cphi_k(x),%0A"></p>
<p>we find the coefficients by minimizing the squared error <img src="https://latex.codecogs.com/png.latex?%5Cint%20(f%20-%20%5Csum_k%20c_k%20%5Cphi_k)%5E2%20dx">. Setting the derivative with respect to each <img src="https://latex.codecogs.com/png.latex?c_j"> to zero gives the normal equations</p>
<p><img src="https://latex.codecogs.com/png.latex?%0A%5Csum_k%20%5Clangle%20%5Cphi_j,%20%5Cphi_k%20%5Crangle%20%5C,%20c_k%20=%20%5Clangle%20%5Cphi_j,%20f%20%5Crangle%0A%5Cqquad%20%5Ctext%7Bor%7D%20%5Cqquad%0A%5Cmathbf%7BG%7D%5Cmathbf%7Bc%7D%20=%20%5Cmathbf%7Bb%7D,%0A"></p>
<p>where <img src="https://latex.codecogs.com/png.latex?%5Clangle%20g,%20h%20%5Crangle%20=%20%5Cint_%7B-%5Cpi%7D%5E%7B%5Cpi%7D%20g(x)%20h(x)%20dx"> is the inner product, <img src="https://latex.codecogs.com/png.latex?G_%7Bjk%7D%20=%20%5Clangle%20%5Cphi_j,%20%5Cphi_k%20%5Crangle"> is the Gram matrix, and <img src="https://latex.codecogs.com/png.latex?b_j%20=%20%5Clangle%20%5Cphi_j,%20f%20%5Crangle">. For an arbitrary basis this is a linear regression problem: every coefficient depends on every other one through <img src="https://latex.codecogs.com/png.latex?%5Cmathbf%7BG%7D">.</p>
<p>The sines and cosines are special because they are <em>orthogonal</em> on <img src="https://latex.codecogs.com/png.latex?%5B-%5Cpi,%20%5Cpi%5D">:</p>
<p><img src="https://latex.codecogs.com/png.latex?%0A%5Cbegin%7Baligned%7D%0A%5Cint_%7B-%5Cpi%7D%5E%7B%5Cpi%7D%20%5Ccos(m%20x)%20%5Ccos(n%20x)%20%5C,%20dx%20&amp;=%20%5Cpi%20%5C,%20%5Cdelta_%7Bmn%7D%20%5Cquad%20(m,%20n%20%5Cge%201)%20%5C%5C%0A%5Cint_%7B-%5Cpi%7D%5E%7B%5Cpi%7D%20%5Csin(m%20x)%20%5Csin(n%20x)%20%5C,%20dx%20&amp;=%20%5Cpi%20%5C,%20%5Cdelta_%7Bmn%7D%20%5C%5C%0A%5Cint_%7B-%5Cpi%7D%5E%7B%5Cpi%7D%20%5Csin(m%20x)%20%5Ccos(n%20x)%20%5C,%20dx%20&amp;=%200%0A%5Cend%7Baligned%7D%0A"></p>
<p>That makes <img src="https://latex.codecogs.com/png.latex?%5Cmathbf%7BG%7D"> diagonal, and the normal equations decouple into one independent equation per coefficient, <img src="https://latex.codecogs.com/png.latex?c_k%20=%20%5Clangle%20%5Cphi_k,%20f%20%5Crangle%20/%20%5Clangle%20%5Cphi_k,%20%5Cphi_k%20%5Crangle">. For the Fourier series that is</p>
<p><img src="https://latex.codecogs.com/png.latex?%0Aa_n%20=%20%5Cfrac%7B1%7D%7B%5Cpi%7D%20%5Cint_%7B-%5Cpi%7D%5E%7B%5Cpi%7D%20f(x)%20%5Ccos(n%20x)%20%5C,%20dx,%0A%5Cqquad%0Ab_n%20=%20%5Cfrac%7B1%7D%7B%5Cpi%7D%20%5Cint_%7B-%5Cpi%7D%5E%7B%5Cpi%7D%20f(x)%20%5Csin(n%20x)%20%5C,%20dx%20.%0A"></p>
<p>No regression is required. Each coefficient is a <em>projection</em> of <img src="https://latex.codecogs.com/png.latex?f"> onto one basis function, and it is computed with an integral. There is a second benefit hiding in that formula: <img src="https://latex.codecogs.com/png.latex?a_n"> does not depend on <img src="https://latex.codecogs.com/png.latex?N">. If we add more terms to the series, the coefficients we already have do not change. We just compute the new ones.</p>
</section>
<section id="an-example" class="level2">
<h2 class="anchored" data-anchor-id="an-example">An example</h2>
<p>Here is a lumpy function that is not obviously made of sines and cosines.</p>
<div id="c8a62408" class="cell" data-execution_count="1">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb1-1"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> numpy <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">as</span> np</span>
<span id="cb1-2"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> scipy.integrate <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> quad</span>
<span id="cb1-3"></span>
<span id="cb1-4"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> f(x):</span>
<span id="cb1-5">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> np.exp(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> (x <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">**</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.6</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> np.exp(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">4</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> (x <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">1.5</span>)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">**</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>)</span>
<span id="cb1-6"></span>
<span id="cb1-7"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> a(n):</span>
<span id="cb1-8">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> quad(<span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">lambda</span> x: f(x) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> np.cos(n <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> x), <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span>np.pi, np.pi)[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>] <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> np.pi</span>
<span id="cb1-9"></span>
<span id="cb1-10"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> b(n):</span>
<span id="cb1-11">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> quad(<span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">lambda</span> x: f(x) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> np.sin(n <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> x), <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span>np.pi, np.pi)[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>] <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> np.pi</span>
<span id="cb1-12"></span>
<span id="cb1-13"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> fourier(x, N):</span>
<span id="cb1-14">    y <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> a(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> np.ones_like(x)</span>
<span id="cb1-15">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> n <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">range</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>, N <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>):</span>
<span id="cb1-16">        y <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+=</span> a(n) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> np.cos(n <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> x) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> b(n) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> np.sin(n <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> x)</span>
<span id="cb1-17">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> y</span></code></pre></div></div>
</div>
<script type="application/json" id="fourier-data">{"x": [-3.1416, -3.1258, -3.1101, -3.0944, -3.0786, -3.0629, -3.0471, -3.0314, -3.0156, -2.9999, -2.9841, -2.9684, -2.9526, -2.9369, -2.9211, -2.9054, -2.8896, -2.8739, -2.8581, -2.8424, -2.8266, -2.8109, -2.7952, -2.7794, -2.7637, -2.7479, -2.7322, -2.7164, -2.7007, -2.6849, -2.6692, -2.6534, -2.6377, -2.6219, -2.6062, -2.5904, -2.5747, -2.5589, -2.5432, -2.5274, -2.5117, -2.496, -2.4802, -2.4645, -2.4487, -2.433, -2.4172, -2.4015, -2.3857, -2.37, -2.3542, -2.3385, -2.3227, -2.307, -2.2912, -2.2755, -2.2597, -2.244, -2.2282, -2.2125, -2.1968, -2.181, -2.1653, -2.1495, -2.1338, -2.118, -2.1023, -2.0865, -2.0708, -2.055, -2.0393, -2.0235, -2.0078, -1.992, -1.9763, -1.9605, -1.9448, -1.929, -1.9133, -1.8976, -1.8818, -1.8661, -1.8503, -1.8346, -1.8188, -1.8031, -1.7873, -1.7716, -1.7558, -1.7401, -1.7243, -1.7086, -1.6928, -1.6771, -1.6613, -1.6456, -1.6298, -1.6141, -1.5984, -1.5826, -1.5669, -1.5511, -1.5354, -1.5196, -1.5039, -1.4881, -1.4724, -1.4566, -1.4409, -1.4251, -1.4094, -1.3936, -1.3779, -1.3621, -1.3464, -1.3306, -1.3149, -1.2992, -1.2834, -1.2677, -1.2519, -1.2362, -1.2204, -1.2047, -1.1889, -1.1732, -1.1574, -1.1417, -1.1259, -1.1102, -1.0944, -1.0787, -1.0629, -1.0472, -1.0315, -1.0157, -1.0, -0.9842, -0.9685, -0.9527, -0.937, -0.9212, -0.9055, -0.8897, -0.874, -0.8582, -0.8425, -0.8267, -0.811, -0.7952, -0.7795, -0.7637, -0.748, -0.7323, -0.7165, -0.7008, -0.685, -0.6693, -0.6535, -0.6378, -0.622, -0.6063, -0.5905, -0.5748, -0.559, -0.5433, -0.5275, -0.5118, -0.496, -0.4803, -0.4645, -0.4488, -0.4331, -0.4173, -0.4016, -0.3858, -0.3701, -0.3543, -0.3386, -0.3228, -0.3071, -0.2913, -0.2756, -0.2598, -0.2441, -0.2283, -0.2126, -0.1968, -0.1811, -0.1653, -0.1496, -0.1339, -0.1181, -0.1024, -0.0866, -0.0709, -0.0551, -0.0394, -0.0236, -0.0079, 0.0079, 0.0236, 0.0394, 0.0551, 0.0709, 0.0866, 0.1024, 0.1181, 0.1339, 0.1496, 0.1653, 0.1811, 0.1968, 0.2126, 0.2283, 0.2441, 0.2598, 0.2756, 0.2913, 0.3071, 0.3228, 0.3386, 0.3543, 0.3701, 0.3858, 0.4016, 0.4173, 0.4331, 0.4488, 0.4645, 0.4803, 0.496, 0.5118, 0.5275, 0.5433, 0.559, 0.5748, 0.5905, 0.6063, 0.622, 0.6378, 0.6535, 0.6693, 0.685, 0.7008, 0.7165, 0.7323, 0.748, 0.7637, 0.7795, 0.7952, 0.811, 0.8267, 0.8425, 0.8582, 0.874, 0.8897, 0.9055, 0.9212, 0.937, 0.9527, 0.9685, 0.9842, 1.0, 1.0157, 1.0315, 1.0472, 1.0629, 1.0787, 1.0944, 1.1102, 1.1259, 1.1417, 1.1574, 1.1732, 1.1889, 1.2047, 1.2204, 1.2362, 1.2519, 1.2677, 1.2834, 1.2992, 1.3149, 1.3306, 1.3464, 1.3621, 1.3779, 1.3936, 1.4094, 1.4251, 1.4409, 1.4566, 1.4724, 1.4881, 1.5039, 1.5196, 1.5354, 1.5511, 1.5669, 1.5826, 1.5984, 1.6141, 1.6298, 1.6456, 1.6613, 1.6771, 1.6928, 1.7086, 1.7243, 1.7401, 1.7558, 1.7716, 1.7873, 1.8031, 1.8188, 1.8346, 1.8503, 1.8661, 1.8818, 1.8976, 1.9133, 1.929, 1.9448, 1.9605, 1.9763, 1.992, 2.0078, 2.0235, 2.0393, 2.055, 2.0708, 2.0865, 2.1023, 2.118, 2.1338, 2.1495, 2.1653, 2.181, 2.1968, 2.2125, 2.2282, 2.244, 2.2597, 2.2755, 2.2912, 2.307, 2.3227, 2.3385, 2.3542, 2.37, 2.3857, 2.4015, 2.4172, 2.433, 2.4487, 2.4645, 2.4802, 2.496, 2.5117, 2.5274, 2.5432, 2.5589, 2.5747, 2.5904, 2.6062, 2.6219, 2.6377, 2.6534, 2.6692, 2.6849, 2.7007, 2.7164, 2.7322, 2.7479, 2.7637, 2.7794, 2.7952, 2.8109, 2.8266, 2.8424, 2.8581, 2.8739, 2.8896, 2.9054, 2.9211, 2.9369, 2.9526, 2.9684, 2.9841, 2.9999, 3.0156, 3.0314, 3.0471, 3.0629, 3.0786, 3.0944, 3.1101, 3.1258, 3.1416], "f": [1e-05, 2e-05, 2e-05, 2e-05, 3e-05, 3e-05, 4e-05, 5e-05, 6e-05, 7e-05, 9e-05, 0.00011, 0.00013, 0.00016, 0.00019, 0.00022, 0.00027, 0.00032, 0.00037, 0.00044, 0.00053, 0.00062, 0.00073, 0.00086, 0.00101, 0.00118, 0.00138, 0.00161, 0.00188, 0.00218, 0.00253, 0.00293, 0.00339, 0.0039, 0.00449, 0.00516, 0.00591, 0.00676, 0.00772, 0.0088, 0.01, 0.01135, 0.01285, 0.01453, 0.01639, 0.01845, 0.02073, 0.02325, 0.02602, 0.02906, 0.0324, 0.03604, 0.04002, 0.04435, 0.04904, 0.05413, 0.05963, 0.06555, 0.07192, 0.07875, 0.08606, 0.09386, 0.10217, 0.11099, 0.12034, 0.13021, 0.14061, 0.15155, 0.16301, 0.17499, 0.18747, 0.20045, 0.21391, 0.22781, 0.24214, 0.25686, 0.27193, 0.28732, 0.30298, 0.31885, 0.3349, 0.35105, 0.36726, 0.38345, 0.39956, 0.41552, 0.43126, 0.44671, 0.4618, 0.47646, 0.4906, 0.50416, 0.51707, 0.52926, 0.54067, 0.55122, 0.56087, 0.56955, 0.57723, 0.58385, 0.58937, 0.59376, 0.59701, 0.59908, 0.59997, 0.59967, 0.59818, 0.59551, 0.59168, 0.58671, 0.58062, 0.57347, 0.56527, 0.55609, 0.54598, 0.53499, 0.52318, 0.51062, 0.49737, 0.48351, 0.4691, 0.45423, 0.43895, 0.42335, 0.40749, 0.39146, 0.37531, 0.35912, 0.34295, 0.32686, 0.31091, 0.29516, 0.27966, 0.26446, 0.24959, 0.2351, 0.22102, 0.20739, 0.19422, 0.18155, 0.16938, 0.15774, 0.14662, 0.13605, 0.12602, 0.11652, 0.10757, 0.09915, 0.09125, 0.08387, 0.077, 0.07061, 0.0647, 0.05925, 0.05425, 0.04968, 0.04552, 0.04176, 0.03837, 0.03535, 0.03267, 0.03032, 0.02829, 0.02656, 0.02512, 0.02396, 0.02306, 0.02242, 0.02202, 0.02187, 0.02194, 0.02224, 0.02277, 0.02352, 0.02448, 0.02566, 0.02705, 0.02867, 0.0305, 0.03256, 0.03484, 0.03735, 0.0401, 0.0431, 0.04634, 0.04985, 0.05362, 0.05766, 0.06199, 0.06662, 0.07154, 0.07678, 0.08235, 0.08825, 0.09449, 0.10108, 0.10804, 0.11538, 0.12309, 0.1312, 0.13972, 0.14864, 0.15797, 0.16773, 0.17792, 0.18854, 0.1996, 0.21111, 0.22305, 0.23544, 0.24827, 0.26154, 0.27524, 0.28938, 0.30394, 0.31892, 0.33431, 0.35009, 0.36625, 0.38278, 0.39966, 0.41687, 0.43439, 0.45219, 0.47027, 0.48857, 0.50709, 0.52579, 0.54463, 0.56359, 0.58264, 0.60173, 0.62083, 0.6399, 0.6589, 0.6778, 0.69654, 0.7151, 0.73342, 0.75146, 0.76918, 0.78655, 0.8035, 0.82001, 0.83603, 0.85151, 0.86643, 0.88073, 0.89437, 0.90733, 0.91957, 0.93104, 0.94173, 0.95159, 0.9606, 0.96874, 0.97597, 0.98229, 0.98766, 0.99208, 0.99554, 0.99801, 0.9995, 1.0, 0.99951, 0.99802, 0.99555, 0.99211, 0.98769, 0.98232, 0.97601, 0.96878, 0.96065, 0.95164, 0.94178, 0.9311, 0.91963, 0.9074, 0.89445, 0.8808, 0.86651, 0.8516, 0.83612, 0.8201, 0.8036, 0.78664, 0.76928, 0.75156, 0.73352, 0.7152, 0.69665, 0.6779, 0.65901, 0.64001, 0.62094, 0.60184, 0.58275, 0.5637, 0.54474, 0.52589, 0.50719, 0.48868, 0.47037, 0.4523, 0.43449, 0.41697, 0.39975, 0.38287, 0.36634, 0.35018, 0.33439, 0.319, 0.30402, 0.28946, 0.27531, 0.2616, 0.24833, 0.2355, 0.2231, 0.21115, 0.19965, 0.18858, 0.17795, 0.16775, 0.15798, 0.14863, 0.1397, 0.13117, 0.12304, 0.1153, 0.10794, 0.10095, 0.09432, 0.08804, 0.08209, 0.07647, 0.07117, 0.06616, 0.06145, 0.05702, 0.05285, 0.04894, 0.04527, 0.04184, 0.03863, 0.03563, 0.03283, 0.03022, 0.02779, 0.02553, 0.02343, 0.02148, 0.01968, 0.01801, 0.01646, 0.01503, 0.01371, 0.0125, 0.01138, 0.01035, 0.00941, 0.00854, 0.00775, 0.00702, 0.00635, 0.00574, 0.00519, 0.00468, 0.00422, 0.0038, 0.00342, 0.00307, 0.00276, 0.00248, 0.00222, 0.00199, 0.00178, 0.00159, 0.00142, 0.00126, 0.00113, 0.001, 0.00089, 0.00079, 0.0007, 0.00062, 0.00055, 0.00049, 0.00043, 0.00038, 0.00034, 0.0003, 0.00026, 0.00023, 0.0002, 0.00018, 0.00015, 0.00014, 0.00012, 0.0001], "a": [0.568195, 0.201473, -0.231197, -0.148547, 0.024492, 0.017273, -0.012002, -0.003104, 0.002594, 0.000625, -0.000251, -6e-05, 1.2e-05], "b": [0.0, 0.137649, 0.201421, 0.112551, -0.023464, -0.050085, -0.008592, 0.007539, 0.001794, -0.000853, -0.000215, 6.4e-05, 1.4e-05]}</script>
<div class="nn-fig" data-nnfig="fourierSeries"></div>
<p><em>Top: f(x) and its Fourier partial sum. Bottom: the coefficients, each one computed from a single integral. Move the slider and the faded bars join the sum without any of the bars changing height.</em></p>
<p>With eight terms the series is indistinguishable from the function (the largest error is about 0.0015). It converges this quickly partly because <img src="https://latex.codecogs.com/png.latex?f"> almost vanishes at <img src="https://latex.codecogs.com/png.latex?%5Cpm%5Cpi">, so its periodic extension is smooth. A function that does not match up at the ends would show Gibbs ringing near them and converge much more slowly. Every coefficient came from a single integral, and adding a term just adds a bar to the chart; it never changes the ones already there.</p>
</section>
<section id="adding-terms-does-not-change-the-coefficients" class="level2">
<h2 class="anchored" data-anchor-id="adding-terms-does-not-change-the-coefficients">Adding terms does not change the coefficients</h2>
<p>To make the second point concrete, let’s fit the same function by least squares on a grid of points, first with 3 harmonics and then with 6. We do the same with a polynomial basis <img src="https://latex.codecogs.com/png.latex?%5C%7B1,%20x,%20x%5E2,%20%5Cldots%5C%7D">, which is not orthogonal.</p>
<div id="3da0192e" class="cell" data-execution_count="3">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb2-1">xg <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.linspace(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span>np.pi, np.pi, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2001</span>)[:<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>]   <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># one period, evenly spaced</span></span>
<span id="cb2-2">yg <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> f(xg)</span>
<span id="cb2-3"></span>
<span id="cb2-4"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> fourier_basis(x, N):</span>
<span id="cb2-5">    cols <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [np.ones_like(x)]</span>
<span id="cb2-6">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> n <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">range</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>, N <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>):</span>
<span id="cb2-7">        cols <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+=</span> [np.cos(n <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> x), np.sin(n <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> x)]</span>
<span id="cb2-8">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> np.column_stack(cols)</span>
<span id="cb2-9"></span>
<span id="cb2-10"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> poly_basis(x, N):</span>
<span id="cb2-11">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> np.column_stack([x<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">**</span>k <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> k <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">range</span>(N <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>)])</span>
<span id="cb2-12"></span>
<span id="cb2-13"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> name, basis, Ns <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> [(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Fourier"</span>, fourier_basis, (<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">6</span>)),</span>
<span id="cb2-14">                        (<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"polynomial"</span>, poly_basis, (<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">6</span>))]:</span>
<span id="cb2-15">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(name)</span>
<span id="cb2-16">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> N <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> Ns:</span>
<span id="cb2-17">        c <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.linalg.lstsq(basis(xg, N), yg, rcond<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">None</span>)[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>]</span>
<span id="cb2-18">        <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"  N = </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>N<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">: first four coefficients"</span>, np.<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">round</span>(c[:<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">4</span>], <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">4</span>))</span></code></pre></div></div>
<div class="cell-output cell-output-stdout">
<pre><code>Fourier
  N = 3: first four coefficients [ 0.2841  0.2015  0.1376 -0.2312]
  N = 6: first four coefficients [ 0.2841  0.2015  0.1376 -0.2312]
polynomial
  N = 3: first four coefficients [ 0.4682  0.1336 -0.056  -0.0188]
  N = 6: first four coefficients [ 0.2641  0.3645  0.3232 -0.1281]</code></pre>
</div>
</div>
<p>The first four Fourier coefficients are the same whether we keep 3 harmonics or 6 (and they are the projections <img src="https://latex.codecogs.com/png.latex?a_0/2">, <img src="https://latex.codecogs.com/png.latex?a_1">, <img src="https://latex.codecogs.com/png.latex?b_1">, <img src="https://latex.codecogs.com/png.latex?a_2"> from before). The first four polynomial coefficients all change when we add <img src="https://latex.codecogs.com/png.latex?x%5E4,%20x%5E5,%20x%5E6">, because those functions overlap with the ones already in the expansion. With a non-orthogonal basis, every time you add a function you have to solve for all the coefficients again.</p>
<p>One caveat: the grid matters. The sines and cosines are orthogonal as integrals, and they stay orthogonal as sums only on a uniform grid that covers exactly one period. That is why the last point is dropped with <code>[:-1]</code>, since <img src="https://latex.codecogs.com/png.latex?-%5Cpi"> and <img src="https://latex.codecogs.com/png.latex?%5Cpi"> are the same point on the circle. On that grid the largest off-diagonal entry of <img src="https://latex.codecogs.com/png.latex?%5Cmathbf%7BH%7D%5ET%5Cmathbf%7BH%7D"> is below <img src="https://latex.codecogs.com/png.latex?10%5E%7B-12%7D">. On a non-uniform or non-periodic grid the discrete Fourier basis is not orthogonal, and the least squares coefficients would shift when you add terms too.</p>
</section>
<section id="the-fourier-series-as-a-network" class="level2">
<h2 class="anchored" data-anchor-id="the-fourier-series-as-a-network">The Fourier series as a network</h2>
<p>Now let’s draw the Fourier series the way we usually draw a neural network. There is one input, <img src="https://latex.codecogs.com/png.latex?x">. It feeds a layer of “hidden units”, each of which computes <img src="https://latex.codecogs.com/png.latex?%5Ccos(n%20x)"> or <img src="https://latex.codecogs.com/png.latex?%5Csin(n%20x)">. The outputs of those units are multiplied by the coefficients and summed to give <img src="https://latex.codecogs.com/png.latex?f(x)">.</p>
<div class="nn-fig" data-nnfig="fourierNetwork"></div>
<p><em>A truncated Fourier series drawn as a neural network with one hidden layer. The input weights are fixed integer frequencies; the output weights are the Fourier coefficients, drawn with a width proportional to their size.</em></p>
<p>It looks a lot like a neural network, and it really is one. Since <img src="https://latex.codecogs.com/png.latex?%5Csin(nx)%20=%20%5Ccos(nx%20-%20%5Cpi/2)">, every hidden unit computes</p>
<p><img src="https://latex.codecogs.com/png.latex?%0Ah_k(x)%20=%20%5Ccos(w_k%20x%20+%20%5Cbeta_k),%0A"></p>
<p>with weight <img src="https://latex.codecogs.com/png.latex?w_k%20=%20n"> and bias <img src="https://latex.codecogs.com/png.latex?%5Cbeta_k%20%5Cin%20%5C%7B0,%20-%5Cpi/2%5C%7D">. The output is a linear combination <img src="https://latex.codecogs.com/png.latex?f(x)%20=%20%5Csum_k%20c_k%20h_k(x)">. That is exactly a one-hidden-layer network with a cosine activation function. It is a special one in two ways:</p>
<ol type="1">
<li>The first-layer weights and biases are <em>fixed</em>. We chose them ahead of time (integer frequencies, two phases) rather than learning them.</li>
<li>Because of that choice the hidden units are orthogonal, so the output weights can be computed one at a time by projection, and they never change when we add more hidden units.</li>
</ol>
<p>A typical neural network uses something like <img src="https://latex.codecogs.com/png.latex?%5Ctanh"> or ReLU instead of <img src="https://latex.codecogs.com/png.latex?%5Ccos">. The logistic sigmoid, and later <img src="https://latex.codecogs.com/png.latex?%5Ctanh">, came from the picture of a neuron firing: each is a smooth step. ReLU is a more modern choice, made because it makes deep networks easier to optimize. We also give up the integer frequencies of the Fourier basis. This gives us even more flexibility, but it comes at a cost: the hidden units are no longer orthogonal, and the first-layer weights now have to be fitted instead of computed. We have to find all the weights together by regression, and when we add more hidden units the old weights change.</p>
<p>There is a middle ground that makes the point even more clearly. In random-feature models and extreme learning machines, the first-layer weights are drawn at random and then frozen, and only the output weights are fitted. It is the same picture as the Fourier series, with a random basis in place of an orthogonal one. Because the basis is not orthogonal, the output weights come from least squares instead of projection.</p>
</section>
</section>
<section id="part-2-a-feature-generator-for-linear-regression" class="level1">
<h1>Part 2: a feature generator for linear regression</h1>
<p>Here is another way to read the same picture. A neural network is a nonlinear transformation of the input into a new, usually higher dimensional, vector space, followed by linear regression in that space. The hidden layers generate features. The last layer is plain old linear regression on those features.</p>
<p>We have actually already done both of these in Part 1. Let’s look at the Fourier network again, one step at a time, for a single input <img src="https://latex.codecogs.com/png.latex?x">.</p>
<p><strong>Step 1: expand the dimension.</strong> The first layer maps the scalar <img src="https://latex.codecogs.com/png.latex?x"> to a vector with one element per hidden unit. For <img src="https://latex.codecogs.com/png.latex?N%20=%203"> that is a vector in <img src="https://latex.codecogs.com/png.latex?%5Cmathbb%7BR%7D%5E7">:</p>
<p><img src="https://latex.codecogs.com/png.latex?%0A%5Cmathbf%7Bz%7D%20=%20%5Cmathbf%7Bw%7D%20x%20+%20%5Cboldsymbol%7B%5Cbeta%7D,%0A%5Cqquad%0A%5Cmathbf%7Bw%7D%20=%20%5Cbegin%7Bbmatrix%7D%200%20%5C%5C%201%20%5C%5C%201%20%5C%5C%202%20%5C%5C%202%20%5C%5C%203%20%5C%5C%203%20%5Cend%7Bbmatrix%7D,%0A%5Cquad%0A%5Cboldsymbol%7B%5Cbeta%7D%20=%20%5Cbegin%7Bbmatrix%7D%200%20%5C%5C%200%20%5C%5C%20-%5Cpi/2%20%5C%5C%200%20%5C%5C%20-%5Cpi/2%20%5C%5C%200%20%5C%5C%20-%5Cpi/2%20%5Cend%7Bbmatrix%7D.%0A"></p>
<p>This step is linear. It does not add any information; it just makes copies of <img src="https://latex.codecogs.com/png.latex?x"> that are stretched and shifted by different amounts. (The constant unit is <img src="https://latex.codecogs.com/png.latex?%5Ccos(0%20%5Ccdot%20x%20+%200)%20=%201">.)</p>
<p><strong>Step 2: transform nonlinearly.</strong> Apply the activation function to each element:</p>
<p><img src="https://latex.codecogs.com/png.latex?%0A%5Cmathbf%7Bh%7D%20=%20%5Csigma(%5Cmathbf%7Bz%7D)%20=%20%5Ccos(%5Cmathbf%7Bz%7D)%20=%20%5Cbegin%7Bbmatrix%7D%201%20&amp;%20%5Ccos%20x%20&amp;%20%5Csin%20x%20&amp;%20%5Ccos%202x%20&amp;%20%5Csin%202x%20&amp;%20%5Ccos%203x%20&amp;%20%5Csin%203x%20%5Cend%7Bbmatrix%7D%5ET.%0A"></p>
<p>Now the seven elements are genuinely different functions of <img src="https://latex.codecogs.com/png.latex?x">. These are the features.</p>
<p><strong>Step 3: linear regression.</strong> The output is linear in the features,</p>
<p><img src="https://latex.codecogs.com/png.latex?%0Af(x)%20=%20%5Cmathbf%7Bc%7D%20%5Ccdot%20%5Cmathbf%7Bh%7D(x).%0A"></p>
<p>If we have <img src="https://latex.codecogs.com/png.latex?n"> data points, we stack the feature vectors as rows of a matrix <img src="https://latex.codecogs.com/png.latex?%5Cmathbf%7BH%7D"> (<img src="https://latex.codecogs.com/png.latex?n%20%5Ctimes%207">), and finding <img src="https://latex.codecogs.com/png.latex?%5Cmathbf%7Bc%7D"> is the linear least squares problem <img src="https://latex.codecogs.com/png.latex?%5Cmin_%5Cmathbf%7Bc%7D%20%5C%7C%20%5Cmathbf%7BH%7D%5Cmathbf%7Bc%7D%20-%20%5Cmathbf%7By%7D%20%5C%7C%5E2">, with solution <img src="https://latex.codecogs.com/png.latex?%5Cmathbf%7Bc%7D%20=%20(%5Cmathbf%7BH%7D%5ET%5Cmathbf%7BH%7D)%5E%7B-1%7D%5Cmathbf%7BH%7D%5ET%5Cmathbf%7By%7D">. That is the problem <code>np.linalg.lstsq</code> solved with <code>fourier_basis</code> in Part 1. It uses an SVD rather than forming that inverse, but the answer is the same. In the Fourier case <img src="https://latex.codecogs.com/png.latex?%5Cmathbf%7BH%7D%5ET%5Cmathbf%7BH%7D"> happens to be diagonal, which is why the integrals were enough.</p>
<p>Here are the three steps written as a network layer, with matrices instead of loops.</p>
<div id="21ebb0d0" class="cell" data-execution_count="4">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb4" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb4-1">w <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.array([<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>])</span>
<span id="cb4-2">beta <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.array([<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>, <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span>np.pi <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>, <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span>np.pi <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>, <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span>np.pi <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>])</span>
<span id="cb4-3"></span>
<span id="cb4-4">Z <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> xg[:, <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">None</span>] <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> w <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> beta    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># step 1: (n, 1) -&gt; (n, 7)</span></span>
<span id="cb4-5">H <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.cos(Z)                 <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># step 2: nonlinear activation</span></span>
<span id="cb4-6">c <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.linalg.lstsq(H, yg, rcond<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">None</span>)[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>]   <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># step 3: linear regression</span></span>
<span id="cb4-7"></span>
<span id="cb4-8"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(H.shape)</span>
<span id="cb4-9"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(np.<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">round</span>(c, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">4</span>))</span></code></pre></div></div>
<div class="cell-output cell-output-stdout">
<pre><code>(2000, 7)
[ 0.2841  0.2015  0.1376 -0.2312  0.2014 -0.1485  0.1126]</code></pre>
</div>
</div>
<p>These are the same coefficients as before.</p>
<section id="the-output-is-linear-in-the-new-space" class="level2">
<h2 class="anchored" data-anchor-id="the-output-is-linear-in-the-new-space">The output is linear in the new space</h2>
<p>The point of the transformation is that a function which is nonlinear in <img src="https://latex.codecogs.com/png.latex?x"> becomes linear in <img src="https://latex.codecogs.com/png.latex?%5Cmathbf%7Bh%7D">. It is the same trick as fitting <img src="https://latex.codecogs.com/png.latex?y%20=%20a%20+%20b/T">: the model is nonlinear in <img src="https://latex.codecogs.com/png.latex?T">, but it is linear in the feature <img src="https://latex.codecogs.com/png.latex?1/T">, so we compute that feature and fit a line. (Linearizing the Arrhenius equation, <img src="https://latex.codecogs.com/png.latex?%5Cln%20k%20=%20%5Cln%20k_0%20-%20%5Cfrac%7BE_a%7D%7BR%7D%5Cfrac%7B1%7D%7BT%7D">, is a close cousin. There we transform the output to <img src="https://latex.codecogs.com/png.latex?%5Cln%20k"> as well, while a network only transforms the input.)</p>
<p>We can see this geometrically with only one harmonic, so the feature space is small enough to draw. Take <img src="https://latex.codecogs.com/png.latex?y%20=%200.5%20+%200.8%5Ccos%20x%20-%200.6%20%5Csin%20x">. As a function of <img src="https://latex.codecogs.com/png.latex?x"> it is a wave. In the feature space <img src="https://latex.codecogs.com/png.latex?(h_1,%20h_2)%20=%20(%5Ccos%20x,%20%5Csin%20x)">, the input interval is mapped onto a circle, and the data lie exactly on a flat plane <img src="https://latex.codecogs.com/png.latex?y%20=%20c_0%20+%20c_1%20h_1%20+%20c_2%20h_2"> above that circle.</p>
<div class="nn-fig" data-nnfig="featureSpace"></div>
<p><em>Left: the data are nonlinear in x. Right: the same data in the feature space (cos x, sin x) lie on a plane, so finding the coefficients is linear regression. Hover a point to find it in both panels.</em></p>
<p>The grey circle on the floor is where the inputs land in feature space. The points sit on the plane, and fitting a plane is linear regression.</p>
</section>
<section id="deep-networks" class="level2">
<h2 class="anchored" data-anchor-id="deep-networks">Deep networks</h2>
<p>So far there is one hidden layer. What changes when we stack more of them? Let’s write a network with two hidden layers using matrices, for a batch of <img src="https://latex.codecogs.com/png.latex?n"> inputs <img src="https://latex.codecogs.com/png.latex?%5Cmathbf%7BX%7D"> (<img src="https://latex.codecogs.com/png.latex?n%20%5Ctimes%20d">):</p>
<p><img src="https://latex.codecogs.com/png.latex?%0A%5Cbegin%7Baligned%7D%0A%5Cmathbf%7BH%7D_1%20&amp;=%20%5Csigma(%5Cmathbf%7BX%7D%5Cmathbf%7BW%7D_1%20+%20%5Cmathbf%7Bb%7D_1)%20&amp;&amp;%20(n%20%5Ctimes%20d)%20%5Ccdot%20(d%20%5Ctimes%20m_1)%20%5Cto%20(n%20%5Ctimes%20m_1)%20%5C%5C%0A%5Cmathbf%7BH%7D_2%20&amp;=%20%5Csigma(%5Cmathbf%7BH%7D_1%5Cmathbf%7BW%7D_2%20+%20%5Cmathbf%7Bb%7D_2)%20&amp;&amp;%20(n%20%5Ctimes%20m_1)%20%5Ccdot%20(m_1%20%5Ctimes%20m_2)%20%5Cto%20(n%20%5Ctimes%20m_2)%20%5C%5C%0A%5Cmathbf%7By%7D%20&amp;=%20%5Cmathbf%7BH%7D_2%20%5Cmathbf%7Bc%7D%20+%20c_0%20&amp;&amp;%20(n%20%5Ctimes%20m_2)%20%5Ccdot%20(m_2%20%5Ctimes%201)%20%5Cto%20(n%20%5Ctimes%201)%0A%5Cend%7Baligned%7D%0A"></p>
<p>Each hidden layer is a linear map to a new dimension followed by an elementwise nonlinearity, just like Step 1 and Step 2 above. Stacking them is function composition. Call the layer maps <img src="https://latex.codecogs.com/png.latex?%5Cmathbf%7Bg%7D_1(%5Cmathbf%7Bx%7D)%20=%20%5Csigma(%5Cmathbf%7Bx%7D%5Cmathbf%7BW%7D_1%20+%20%5Cmathbf%7Bb%7D_1)"> and <img src="https://latex.codecogs.com/png.latex?%5Cmathbf%7Bg%7D_2(%5Cmathbf%7Bu%7D)%20=%20%5Csigma(%5Cmathbf%7Bu%7D%5Cmathbf%7BW%7D_2%20+%20%5Cmathbf%7Bb%7D_2)">. The features at the end are</p>
<p><img src="https://latex.codecogs.com/png.latex?%0A%5Cmathbf%7Bh%7D_2(%5Cmathbf%7Bx%7D)%20=%20%5Csigma%5Cbig(%5Csigma(%5Cmathbf%7Bx%7D%5Cmathbf%7BW%7D_1%20+%20%5Cmathbf%7Bb%7D_1)%5Cmathbf%7BW%7D_2%20+%20%5Cmathbf%7Bb%7D_2%5Cbig)%20=%20(%5Cmathbf%7Bg%7D_2%20%5Ccirc%20%5Cmathbf%7Bg%7D_1)(%5Cmathbf%7Bx%7D),%0A"></p>
<p>and the output is still linear in them,</p>
<p><img src="https://latex.codecogs.com/png.latex?%0Af(%5Cmathbf%7Bx%7D)%20=%20%5Cmathbf%7Bc%7D%20%5Ccdot%20%5Cmathbf%7Bh%7D_2(%5Cmathbf%7Bx%7D)%20+%20c_0%20.%0A"></p>
<p>However many layers there are, the last one is a linear model on the features, and with a squared-error loss, fitting it is linear regression. The columns of <img src="https://latex.codecogs.com/png.latex?%5Cmathbf%7BH%7D_2"> are <img src="https://latex.codecogs.com/png.latex?m_2"> basis functions, each one a composition of the layers before it. The only difference from the Fourier series is that these basis functions are not chosen in advance; their shapes are set by <img src="https://latex.codecogs.com/png.latex?%5Cmathbf%7BW%7D_1,%20%5Cmathbf%7Bb%7D_1,%20%5Cmathbf%7BW%7D_2,%20%5Cmathbf%7Bb%7D_2">, which are fitted along with <img src="https://latex.codecogs.com/png.latex?%5Cmathbf%7Bc%7D">.</p>
<div class="nn-fig" data-nnfig="deepNetwork"></div>
<p><em>A network with two hidden layers in graph form (top) and matrix form (bottom). The hidden layers transform n inputs into n rows of 3 features, the columns of H₂. The output layer is linear regression on them. (Three features keep the drawing small; the trained example below uses six.)</em></p>
</section>
<section id="checking-that-the-last-layer-is-linear-regression" class="level2">
<h2 class="anchored" data-anchor-id="checking-that-the-last-layer-is-linear-regression">Checking that the last layer is linear regression</h2>
<p>Let’s train a small network on the function from Part 1 and look inside it. This one has two hidden layers of six <img src="https://latex.codecogs.com/png.latex?%5Ctanh"> units. To keep it to numpy and scipy, we pack all the weights into one vector and minimize the mean squared error with BFGS.</p>
<div id="11311cd7" class="cell" data-execution_count="5">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb6" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb6-1"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> scipy.optimize <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> minimize</span>
<span id="cb6-2"></span>
<span id="cb6-3">sizes <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">6</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">6</span>]   <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># input, hidden layer 1, hidden layer 2</span></span>
<span id="cb6-4">shapes <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> []</span>
<span id="cb6-5"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> m, k <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">zip</span>(sizes[:<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>], sizes[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>:]):</span>
<span id="cb6-6">    shapes <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+=</span> [(m, k), (k,)]      <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># W, b for each hidden layer</span></span>
<span id="cb6-7">shapes <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+=</span> [(sizes[<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>],), ()]      <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># c, c0 for the output layer</span></span>
<span id="cb6-8"></span>
<span id="cb6-9"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> unpack(p):</span>
<span id="cb6-10">    out, i <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [], <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span></span>
<span id="cb6-11">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> s <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> shapes:</span>
<span id="cb6-12">        k <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">int</span>(np.prod(s))</span>
<span id="cb6-13">        out.append(p[i:i <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> k].reshape(s))</span>
<span id="cb6-14">        i <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+=</span> k</span>
<span id="cb6-15">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> out</span>
<span id="cb6-16"></span>
<span id="cb6-17"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> features(p, X):</span>
<span id="cb6-18">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">"The hidden layers: H = tanh(... tanh(X W1 + b1) ... W + b)."</span></span>
<span id="cb6-19">    <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span>hidden, c, c0 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> unpack(p)</span>
<span id="cb6-20">    H <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> X</span>
<span id="cb6-21">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> W, b <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">zip</span>(hidden[::<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>], hidden[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>::<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>]):</span>
<span id="cb6-22">        H <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.tanh(H <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">@</span> W <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> b)</span>
<span id="cb6-23">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> H</span>
<span id="cb6-24"></span>
<span id="cb6-25"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> nn(p, X):</span>
<span id="cb6-26">    <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span>_, c, c0 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> unpack(p)</span>
<span id="cb6-27">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> features(p, X) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">@</span> c <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> c0</span>
<span id="cb6-28"></span>
<span id="cb6-29">xt <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.linspace(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span>np.pi, np.pi, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">200</span>)</span>
<span id="cb6-30">Xt, yt <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> xt[:, <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">None</span>], f(xt)</span>
<span id="cb6-31"></span>
<span id="cb6-32"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> loss(p):</span>
<span id="cb6-33">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> np.mean((nn(p, Xt) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> yt)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">**</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>)</span>
<span id="cb6-34"></span>
<span id="cb6-35">p0 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.random.default_rng(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">42</span>).normal(size<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">sum</span>(<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">int</span>(np.prod(s)) <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> s <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> shapes))</span>
<span id="cb6-36">sol <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> minimize(loss, p0, method<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"BFGS"</span>, options<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>{<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"maxiter"</span>: <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5000</span>})</span>
<span id="cb6-37"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>(p0)<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;"> parameters, MSE = </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>sol<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>fun<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:.2e}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span>)</span></code></pre></div></div>
<div class="cell-output cell-output-stdout">
<pre><code>61 parameters, MSE = 4.81e-06</code></pre>
</div>
</div>
<p>Now take the trained hidden layers, compute the features <img src="https://latex.codecogs.com/png.latex?%5Cmathbf%7BH%7D_2"> for the training inputs, and do ordinary linear regression of <img src="https://latex.codecogs.com/png.latex?y"> on them. If the last layer is linear regression, we should get back the output weights the network learned.</p>
<div id="d80178c0" class="cell" data-execution_count="6">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb8" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb8-1">H2 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> features(sol.x, Xt)</span>
<span id="cb8-2">A <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.column_stack([H2, np.ones(<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>(xt))])</span>
<span id="cb8-3">c_lstsq <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.linalg.lstsq(A, yt, rcond<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">None</span>)[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>]</span>
<span id="cb8-4"></span>
<span id="cb8-5"><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span>_, c_nn, c0_nn <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> unpack(sol.x)</span>
<span id="cb8-6"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"network:"</span>, np.<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">round</span>(np.append(c_nn, c0_nn), <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>))</span>
<span id="cb8-7"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"lstsq:  "</span>, np.<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">round</span>(c_lstsq, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>))</span>
<span id="cb8-8"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"cond(A) = </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>np<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>linalg<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>cond(A)<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:.0f}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span>)</span></code></pre></div></div>
<div class="cell-output cell-output-stdout">
<pre><code>network: [-1.044  0.962 -1.522  1.387 -0.859  0.774 -0.994]
lstsq:   [-1.044  0.962 -1.523  1.387 -0.859  0.776 -0.994]
cond(A) = 159</code></pre>
</div>
</div>
<p>If you run this yourself, expect different numbers. The path BFGS takes depends on the numpy, scipy and BLAS versions, so the trained weights will differ. What should reproduce is that the two rows agree.</p>
<p>They agree to within the tolerance the optimizer stopped at, and that is not a coincidence. Write <img src="https://latex.codecogs.com/png.latex?%5Cmathbf%7BA%7D%20=%20%5B%5Cmathbf%7BH%7D_2%20%5C;%5C;%20%5Cmathbf%7B1%7D%5D"> and <img src="https://latex.codecogs.com/png.latex?%5Cboldsymbol%7B%5Ctheta%7D%20=%20(%5Cmathbf%7Bc%7D,%20c_0)">. With the hidden weights held fixed, the loss <img src="https://latex.codecogs.com/png.latex?%5Cfrac%7B1%7D%7Bn%7D%5C%7C%5Cmathbf%7BA%7D%5Cboldsymbol%7B%5Ctheta%7D%20-%20%5Cmathbf%7By%7D%5C%7C%5E2"> is a convex quadratic in <img src="https://latex.codecogs.com/png.latex?%5Cboldsymbol%7B%5Ctheta%7D">. Its gradient, <img src="https://latex.codecogs.com/png.latex?%5Cfrac%7B2%7D%7Bn%7D%5Cmathbf%7BA%7D%5ET(%5Cmathbf%7BA%7D%5Cboldsymbol%7B%5Ctheta%7D%20-%20%5Cmathbf%7By%7D)">, vanishes at a stationary point of the full training problem, and that condition is exactly the normal equations. As long as <img src="https://latex.codecogs.com/png.latex?%5Cmathbf%7BA%7D"> has full rank (its condition number is modest here), the network’s output weights are the least squares solution on its own features.</p>
<p>Finally, here are the six basis functions the network built, the columns of <img src="https://latex.codecogs.com/png.latex?%5Cmathbf%7BH%7D_2">. Compare them to the sines and cosines in Part 1: these are not orthogonal and they are not pretty, but a weighted sum of them reproduces <img src="https://latex.codecogs.com/png.latex?f(x)">.</p>
<script type="application/json" id="basis-data">{"x": [-3.1416, -3.11, -3.0784, -3.0469, -3.0153, -2.9837, -2.9521, -2.9206, -2.889, -2.8574, -2.8259, -2.7943, -2.7627, -2.7311, -2.6996, -2.668, -2.6364, -2.6048, -2.5733, -2.5417, -2.5101, -2.4785, -2.447, -2.4154, -2.3838, -2.3522, -2.3207, -2.2891, -2.2575, -2.226, -2.1944, -2.1628, -2.1312, -2.0997, -2.0681, -2.0365, -2.0049, -1.9734, -1.9418, -1.9102, -1.8786, -1.8471, -1.8155, -1.7839, -1.7523, -1.7208, -1.6892, -1.6576, -1.6261, -1.5945, -1.5629, -1.5313, -1.4998, -1.4682, -1.4366, -1.405, -1.3735, -1.3419, -1.3103, -1.2787, -1.2472, -1.2156, -1.184, -1.1524, -1.1209, -1.0893, -1.0577, -1.0261, -0.9946, -0.963, -0.9314, -0.8999, -0.8683, -0.8367, -0.8051, -0.7736, -0.742, -0.7104, -0.6788, -0.6473, -0.6157, -0.5841, -0.5525, -0.521, -0.4894, -0.4578, -0.4262, -0.3947, -0.3631, -0.3315, -0.3, -0.2684, -0.2368, -0.2052, -0.1737, -0.1421, -0.1105, -0.0789, -0.0474, -0.0158, 0.0158, 0.0474, 0.0789, 0.1105, 0.1421, 0.1737, 0.2052, 0.2368, 0.2684, 0.3, 0.3315, 0.3631, 0.3947, 0.4262, 0.4578, 0.4894, 0.521, 0.5525, 0.5841, 0.6157, 0.6473, 0.6788, 0.7104, 0.742, 0.7736, 0.8051, 0.8367, 0.8683, 0.8999, 0.9314, 0.963, 0.9946, 1.0261, 1.0577, 1.0893, 1.1209, 1.1524, 1.184, 1.2156, 1.2472, 1.2787, 1.3103, 1.3419, 1.3735, 1.405, 1.4366, 1.4682, 1.4998, 1.5313, 1.5629, 1.5945, 1.6261, 1.6576, 1.6892, 1.7208, 1.7523, 1.7839, 1.8155, 1.8471, 1.8786, 1.9102, 1.9418, 1.9734, 2.0049, 2.0365, 2.0681, 2.0997, 2.1312, 2.1628, 2.1944, 2.226, 2.2575, 2.2891, 2.3207, 2.3522, 2.3838, 2.4154, 2.447, 2.4785, 2.5101, 2.5417, 2.5733, 2.6048, 2.6364, 2.668, 2.6996, 2.7311, 2.7627, 2.7943, 2.8259, 2.8574, 2.889, 2.9206, 2.9521, 2.9837, 3.0153, 3.0469, 3.0784, 3.11, 3.1416], "f": [1e-05, 2e-05, 3e-05, 4e-05, 6e-05, 9e-05, 0.00013, 0.00019, 0.00027, 0.00038, 0.00053, 0.00074, 0.00102, 0.0014, 0.0019, 0.00256, 0.00343, 0.00455, 0.00599, 0.00782, 0.01013, 0.01302, 0.01661, 0.02101, 0.02637, 0.03284, 0.04057, 0.04971, 0.06043, 0.07289, 0.08721, 0.10351, 0.12189, 0.14239, 0.16502, 0.18972, 0.21639, 0.24485, 0.27485, 0.30607, 0.33814, 0.37059, 0.40294, 0.43463, 0.46508, 0.49372, 0.51996, 0.54324, 0.56305, 0.57896, 0.59058, 0.59765, 0.6, 0.59758, 0.59044, 0.57875, 0.56279, 0.54292, 0.51959, 0.49332, 0.46466, 0.4342, 0.40251, 0.37019, 0.33776, 0.30574, 0.27458, 0.24467, 0.21631, 0.18977, 0.16523, 0.14279, 0.12252, 0.10442, 0.08845, 0.07453, 0.06257, 0.05243, 0.044, 0.03713, 0.03169, 0.02755, 0.02461, 0.02276, 0.02193, 0.02204, 0.02307, 0.02497, 0.02774, 0.03139, 0.03595, 0.04144, 0.04793, 0.05547, 0.06414, 0.07401, 0.08515, 0.09766, 0.11161, 0.12708, 0.14415, 0.16287, 0.18331, 0.2055, 0.22947, 0.25521, 0.28272, 0.31195, 0.34282, 0.37526, 0.40913, 0.44429, 0.48054, 0.51769, 0.55548, 0.59367, 0.63195, 0.67003, 0.70757, 0.74424, 0.7797, 0.8136, 0.84559, 0.87534, 0.90253, 0.92687, 0.94807, 0.96589, 0.98014, 0.99064, 0.99727, 0.99994, 0.99863, 0.99336, 0.98418, 0.9712, 0.95459, 0.93452, 0.91123, 0.88499, 0.85608, 0.82482, 0.79154, 0.75658, 0.72029, 0.68301, 0.64508, 0.60683, 0.56858, 0.53061, 0.49322, 0.45663, 0.42108, 0.38675, 0.3538, 0.32237, 0.29257, 0.26446, 0.2381, 0.21352, 0.19071, 0.16966, 0.15034, 0.13268, 0.11664, 0.10212, 0.08906, 0.07735, 0.06692, 0.05767, 0.04949, 0.04231, 0.03602, 0.03055, 0.02581, 0.02171, 0.01819, 0.01519, 0.01262, 0.01045, 0.00862, 0.00708, 0.00579, 0.00472, 0.00383, 0.0031, 0.00249, 0.002, 0.0016, 0.00127, 0.00101, 0.0008, 0.00063, 0.00049, 0.00038, 0.0003, 0.00023, 0.00018, 0.00014, 0.0001], "H": [[-0.91844, -0.91839, -0.91834, -0.91829, -0.91824, -0.91819, -0.91815, -0.91811, -0.91806, -0.91803, -0.91799, -0.91796, -0.91793, -0.9179, -0.91788, -0.91787, -0.91785, -0.91785, -0.91785, -0.91786, -0.91787, -0.9179, -0.91793, -0.91797, -0.91802, -0.91807, -0.91814, -0.91822, -0.91832, -0.91842, -0.91854, -0.91867, -0.91881, -0.91897, -0.91914, -0.91933, -0.91953, -0.91975, -0.91999, -0.92024, -0.92051, -0.9208, -0.92111, -0.92143, -0.92178, -0.92214, -0.92253, -0.92294, -0.92337, -0.92382, -0.9243, -0.9248, -0.92533, -0.92588, -0.92647, -0.92708, -0.92772, -0.9284, -0.92911, -0.92986, -0.93064, -0.93146, -0.93232, -0.93321, -0.93415, -0.93512, -0.93614, -0.93719, -0.93829, -0.93942, -0.94058, -0.94177, -0.94299, -0.94424, -0.9455, -0.94678, -0.94807, -0.94936, -0.95064, -0.95191, -0.95317, -0.9544, -0.95559, -0.95675, -0.95786, -0.95891, -0.95991, -0.96083, -0.96169, -0.96246, -0.96315, -0.96375, -0.96425, -0.96465, -0.96494, -0.96513, -0.96519, -0.96513, -0.96494, -0.96462, -0.96414, -0.96351, -0.9627, -0.96172, -0.96054, -0.95915, -0.95752, -0.95564, -0.95348, -0.95101, -0.9482, -0.94502, -0.94141, -0.93735, -0.93277, -0.92762, -0.92184, -0.91535, -0.90807, -0.89993, -0.89083, -0.88067, -0.86934, -0.85672, -0.8427, -0.82715, -0.80994, -0.79093, -0.77001, -0.74704, -0.7219, -0.69451, -0.66476, -0.6326, -0.598, -0.56095, -0.52148, -0.47969, -0.43567, -0.38961, -0.3417, -0.29219, -0.24135, -0.18951, -0.13698, -0.0841, -0.03123, 0.02132, 0.07321, 0.12415, 0.17386, 0.22213, 0.26874, 0.31354, 0.3564, 0.39723, 0.43599, 0.47265, 0.50721, 0.53971, 0.57017, 0.59868, 0.62529, 0.6501, 0.67318, 0.69464, 0.71456, 0.73304, 0.75017, 0.76604, 0.78074, 0.79435, 0.80695, 0.81861, 0.82941, 0.83941, 0.84866, 0.85724, 0.86519, 0.87255, 0.87939, 0.88573, 0.89161, 0.89708, 0.90217, 0.9069, 0.9113, 0.9154, 0.91922, 0.92278, 0.9261, 0.92921, 0.93211, 0.93482, 0.93736, 0.93974, 0.94197, 0.94406, 0.94602, 0.94787], [-0.19298, -0.19487, -0.19688, -0.19903, -0.2013, -0.20372, -0.2063, -0.20903, -0.21194, -0.21502, -0.21828, -0.22175, -0.22542, -0.2293, -0.23341, -0.23775, -0.24234, -0.24718, -0.25228, -0.25765, -0.2633, -0.26923, -0.27547, -0.282, -0.28883, -0.29598, -0.30344, -0.31121, -0.3193, -0.32771, -0.33642, -0.34544, -0.35475, -0.36435, -0.37423, -0.38437, -0.39475, -0.40536, -0.41618, -0.42718, -0.43834, -0.44963, -0.46103, -0.4725, -0.48402, -0.49556, -0.50708, -0.51857, -0.52998, -0.54128, -0.55246, -0.56349, -0.57433, -0.58497, -0.59539, -0.60556, -0.61548, -0.62512, -0.63447, -0.64352, -0.65226, -0.66069, -0.6688, -0.67659, -0.68404, -0.69116, -0.69795, -0.70441, -0.71053, -0.71631, -0.72176, -0.72687, -0.73163, -0.73606, -0.74013, -0.74386, -0.74723, -0.75023, -0.75286, -0.7551, -0.75695, -0.75838, -0.75939, -0.75994, -0.76003, -0.75962, -0.75868, -0.75718, -0.75508, -0.75235, -0.74894, -0.7448, -0.73987, -0.73409, -0.72741, -0.71976, -0.71105, -0.70123, -0.6902, -0.67789, -0.66423, -0.64913, -0.63253, -0.61437, -0.5946, -0.57318, -0.55011, -0.52539, -0.49906, -0.47119, -0.44187, -0.41123, -0.37943, -0.34665, -0.3131, -0.27901, -0.24461, -0.21013, -0.17581, -0.14188, -0.10854, -0.07598, -0.04436, -0.01381, 0.01555, 0.04365, 0.07042, 0.09584, 0.11989, 0.14258, 0.16393, 0.18397, 0.20275, 0.22031, 0.23671, 0.25202, 0.26628, 0.27957, 0.29194, 0.30345, 0.31417, 0.32414, 0.33341, 0.34205, 0.3501, 0.3576, 0.36459, 0.37112, 0.37722, 0.38292, 0.38825, 0.39325, 0.39794, 0.40235, 0.40649, 0.41039, 0.41407, 0.41755, 0.42084, 0.42395, 0.4269, 0.42971, 0.43238, 0.43492, 0.43735, 0.43967, 0.44189, 0.44401, 0.44605, 0.44801, 0.4499, 0.45171, 0.45346, 0.45514, 0.45677, 0.45834, 0.45986, 0.46134, 0.46276, 0.46414, 0.46548, 0.46678, 0.46805, 0.46927, 0.47046, 0.47162, 0.47274, 0.47383, 0.4749, 0.47593, 0.47693, 0.47791, 0.47886, 0.47978, 0.48068, 0.48156, 0.48241, 0.48324, 0.48404, 0.48483], [-0.8942, -0.89332, -0.89237, -0.89135, -0.89025, -0.88906, -0.88778, -0.88641, -0.88492, -0.88332, -0.88159, -0.87972, -0.87771, -0.87553, -0.87319, -0.87065, -0.86791, -0.86494, -0.86174, -0.85827, -0.85453, -0.85047, -0.84608, -0.84134, -0.83619, -0.83063, -0.8246, -0.81808, -0.81102, -0.80338, -0.79511, -0.78617, -0.7765, -0.76606, -0.75479, -0.74263, -0.72954, -0.71545, -0.70031, -0.68407, -0.6667, -0.64813, -0.62835, -0.60732, -0.58503, -0.56147, -0.53665, -0.5106, -0.48335, -0.45495, -0.42547, -0.39499, -0.36362, -0.33145, -0.2986, -0.2652, -0.23138, -0.19728, -0.16301, -0.12872, -0.09452, -0.06052, -0.02683, 0.00647, 0.03929, 0.07157, 0.10325, 0.1343, 0.16469, 0.1944, 0.22342, 0.25175, 0.27939, 0.30637, 0.33269, 0.35837, 0.38343, 0.4079, 0.43179, 0.45512, 0.4779, 0.50016, 0.52191, 0.54314, 0.56388, 0.58411, 0.60385, 0.62309, 0.64182, 0.66004, 0.67774, 0.69491, 0.71154, 0.72762, 0.74314, 0.75808, 0.77244, 0.78621, 0.79938, 0.81194, 0.82389, 0.83523, 0.84595, 0.85606, 0.86557, 0.87448, 0.88281, 0.89056, 0.89777, 0.90444, 0.91059, 0.91626, 0.92146, 0.92622, 0.93057, 0.93453, 0.93812, 0.94138, 0.94432, 0.94698, 0.94936, 0.9515, 0.95341, 0.95511, 0.95662, 0.95795, 0.95912, 0.96015, 0.96103, 0.96179, 0.96244, 0.96298, 0.96342, 0.96377, 0.96404, 0.96423, 0.96435, 0.9644, 0.96439, 0.96432, 0.96419, 0.96402, 0.9638, 0.96353, 0.96322, 0.96287, 0.96249, 0.96207, 0.96162, 0.96114, 0.96063, 0.96009, 0.95953, 0.95895, 0.95834, 0.95771, 0.95706, 0.9564, 0.95572, 0.95502, 0.95432, 0.95359, 0.95286, 0.95212, 0.95137, 0.9506, 0.94984, 0.94906, 0.94829, 0.9475, 0.94672, 0.94593, 0.94515, 0.94436, 0.94357, 0.94278, 0.942, 0.94122, 0.94044, 0.93967, 0.9389, 0.93814, 0.93738, 0.93663, 0.93589, 0.93515, 0.93443, 0.93371, 0.933, 0.9323, 0.93161, 0.93093, 0.93027, 0.92961, 0.92896, 0.92832, 0.9277, 0.92708, 0.92648, 0.92589], [-0.83024, -0.8279, -0.82537, -0.82263, -0.81966, -0.81645, -0.81297, -0.8092, -0.80511, -0.80067, -0.79585, -0.79061, -0.78491, -0.77871, -0.77196, -0.7646, -0.75657, -0.74782, -0.73826, -0.72782, -0.71641, -0.70393, -0.69029, -0.67537, -0.65905, -0.64119, -0.62167, -0.60033, -0.57703, -0.55161, -0.52391, -0.4938, -0.46113, -0.42578, -0.38764, -0.34667, -0.30283, -0.25615, -0.20673, -0.15473, -0.10039, -0.04403, 0.01397, 0.07314, 0.13297, 0.19292, 0.2524, 0.31085, 0.36775, 0.42261, 0.47501, 0.52461, 0.57117, 0.61452, 0.65458, 0.69136, 0.7249, 0.75533, 0.7828, 0.80748, 0.82958, 0.8493, 0.86686, 0.88245, 0.89627, 0.90851, 0.91933, 0.92888, 0.93732, 0.94477, 0.95134, 0.95713, 0.96224, 0.96674, 0.97071, 0.97421, 0.97729, 0.98, 0.98239, 0.98449, 0.98633, 0.98795, 0.98938, 0.99063, 0.99172, 0.99268, 0.99352, 0.99426, 0.9949, 0.99546, 0.99595, 0.99638, 0.99675, 0.99708, 0.99736, 0.99761, 0.99783, 0.99802, 0.99818, 0.99832, 0.99845, 0.99856, 0.99865, 0.99874, 0.99881, 0.99887, 0.99893, 0.99897, 0.99901, 0.99905, 0.99908, 0.99911, 0.99913, 0.99915, 0.99916, 0.99917, 0.99918, 0.99919, 0.99919, 0.9992, 0.9992, 0.9992, 0.9992, 0.9992, 0.99919, 0.99919, 0.99918, 0.99917, 0.99917, 0.99916, 0.99915, 0.99914, 0.99913, 0.99911, 0.9991, 0.99909, 0.99907, 0.99906, 0.99904, 0.99903, 0.99901, 0.99899, 0.99897, 0.99895, 0.99893, 0.99891, 0.99889, 0.99887, 0.99885, 0.99882, 0.9988, 0.99877, 0.99875, 0.99872, 0.9987, 0.99867, 0.99864, 0.99861, 0.99858, 0.99855, 0.99852, 0.99849, 0.99846, 0.99843, 0.9984, 0.99837, 0.99833, 0.9983, 0.99827, 0.99823, 0.9982, 0.99816, 0.99813, 0.9981, 0.99806, 0.99803, 0.99799, 0.99796, 0.99792, 0.99788, 0.99785, 0.99781, 0.99778, 0.99774, 0.99771, 0.99767, 0.99764, 0.99761, 0.99757, 0.99754, 0.9975, 0.99747, 0.99744, 0.99741, 0.99737, 0.99734, 0.99731, 0.99728, 0.99725, 0.99722], [-0.86207, -0.86115, -0.86017, -0.85912, -0.85799, -0.85678, -0.85549, -0.8541, -0.85262, -0.85103, -0.84933, -0.8475, -0.84555, -0.84346, -0.84121, -0.83881, -0.83623, -0.83347, -0.83052, -0.82735, -0.82396, -0.82033, -0.81644, -0.81228, -0.80783, -0.80307, -0.79798, -0.79255, -0.78675, -0.78056, -0.77397, -0.76696, -0.7595, -0.75158, -0.74318, -0.7343, -0.72491, -0.71501, -0.7046, -0.69367, -0.68224, -0.67032, -0.65791, -0.64506, -0.63179, -0.61815, -0.60418, -0.58995, -0.57554, -0.561, -0.54644, -0.53194, -0.51762, -0.50356, -0.48988, -0.47671, -0.46414, -0.4523, -0.44131, -0.43127, -0.4223, -0.4145, -0.40796, -0.40277, -0.39902, -0.39678, -0.39611, -0.39707, -0.39968, -0.40398, -0.40997, -0.41766, -0.42702, -0.43802, -0.4506, -0.46469, -0.4802, -0.49703, -0.51505, -0.53411, -0.55407, -0.57474, -0.59597, -0.61756, -0.63933, -0.66109, -0.68268, -0.70393, -0.72468, -0.74481, -0.76419, -0.78273, -0.80036, -0.81701, -0.83266, -0.84729, -0.86089, -0.87349, -0.88511, -0.89578, -0.90554, -0.91445, -0.92255, -0.9299, -0.93656, -0.94257, -0.94799, -0.95287, -0.95726, -0.96121, -0.96474, -0.96791, -0.97076, -0.9733, -0.97558, -0.97762, -0.97945, -0.98109, -0.98255, -0.98387, -0.98504, -0.9861, -0.98704, -0.98789, -0.98866, -0.98934, -0.98996, -0.99051, -0.99101, -0.99146, -0.99186, -0.99222, -0.99255, -0.99284, -0.9931, -0.99334, -0.99355, -0.99373, -0.9939, -0.99405, -0.99418, -0.99429, -0.99439, -0.99448, -0.99455, -0.99461, -0.99466, -0.9947, -0.99473, -0.99474, -0.99475, -0.99476, -0.99475, -0.99474, -0.99472, -0.99469, -0.99466, -0.99462, -0.99457, -0.99452, -0.99446, -0.9944, -0.99433, -0.99426, -0.99418, -0.9941, -0.99402, -0.99393, -0.99383, -0.99374, -0.99364, -0.99353, -0.99343, -0.99332, -0.99321, -0.99309, -0.99298, -0.99286, -0.99274, -0.99262, -0.99249, -0.99237, -0.99224, -0.99211, -0.99199, -0.99186, -0.99173, -0.9916, -0.99147, -0.99134, -0.99121, -0.99108, -0.99095, -0.99082, -0.99069, -0.99056, -0.99043, -0.9903, -0.99018, -0.99005], [-0.93797, -0.93761, -0.93722, -0.9368, -0.93636, -0.93589, -0.93538, -0.93484, -0.93426, -0.93364, -0.93298, -0.93227, -0.93151, -0.9307, -0.92983, -0.92891, -0.92791, -0.92685, -0.92571, -0.9245, -0.9232, -0.9218, -0.92031, -0.91871, -0.917, -0.91517, -0.91322, -0.91112, -0.90888, -0.90649, -0.90393, -0.9012, -0.89828, -0.89516, -0.89184, -0.8883, -0.88453, -0.88051, -0.87624, -0.87171, -0.8669, -0.8618, -0.85641, -0.85071, -0.8447, -0.83836, -0.8317, -0.82471, -0.81738, -0.80972, -0.80172, -0.7934, -0.78474, -0.77577, -0.76649, -0.75691, -0.74704, -0.7369, -0.72651, -0.71587, -0.70501, -0.69395, -0.68271, -0.6713, -0.65975, -0.64806, -0.63627, -0.62439, -0.61243, -0.60041, -0.58833, -0.57622, -0.56408, -0.5519, -0.53971, -0.52749, -0.51525, -0.50298, -0.49068, -0.47834, -0.46594, -0.45347, -0.44092, -0.42827, -0.4155, -0.40259, -0.38951, -0.37625, -0.36278, -0.34907, -0.33512, -0.32088, -0.30636, -0.29152, -0.27635, -0.26084, -0.24498, -0.22876, -0.21218, -0.19524, -0.17794, -0.1603, -0.14232, -0.12402, -0.10541, -0.08653, -0.06739, -0.04802, -0.02845, -0.00872, 0.01116, 0.03113, 0.05119, 0.07127, 0.09137, 0.11144, 0.13145, 0.15138, 0.17119, 0.19087, 0.21038, 0.2297, 0.24882, 0.26771, 0.28636, 0.30475, 0.32287, 0.34071, 0.35826, 0.37551, 0.39246, 0.40909, 0.42541, 0.44142, 0.4571, 0.47245, 0.48748, 0.50219, 0.51656, 0.53061, 0.54433, 0.55772, 0.57079, 0.58352, 0.59593, 0.60802, 0.61978, 0.63122, 0.64234, 0.65314, 0.66362, 0.67379, 0.68365, 0.69321, 0.70246, 0.71141, 0.72007, 0.72844, 0.73653, 0.74434, 0.75187, 0.75914, 0.76614, 0.77289, 0.77939, 0.78565, 0.79167, 0.79746, 0.80303, 0.80839, 0.81353, 0.81848, 0.82322, 0.82778, 0.83215, 0.83635, 0.84038, 0.84425, 0.84796, 0.85151, 0.85492, 0.8582, 0.86133, 0.86434, 0.86722, 0.86999, 0.87264, 0.87518, 0.87761, 0.87995, 0.88219, 0.88433, 0.88639, 0.88836, 0.89026, 0.89207, 0.89381, 0.89548, 0.89708, 0.89861]], "c": [-1.04353, 0.96244, -1.52183, 1.38705, -0.85904, 0.77446], "c0": -0.9937}</script>
<div class="nn-fig" data-nnfig="learnedBasis"></div>
<p><em>Left: the columns of H₂, the basis functions learned by the hidden layers. Switch to cₖ·hₖ to see them scaled by their output weights. Right: their linear combination. Toggle basis functions off to see that the fit needs all of them.</em></p>
<p>So a neural network does the same two things as the Fourier series: it transforms the input into a new vector space with a nonlinear function, and it does linear regression in that space. The Fourier series fixes the transformation in advance and gets the regression for free. A neural network learns the transformation, and pays for it with a nonlinear optimization over all the weights at once.</p>
<p>You can see from the learned basis functions that there is a lot of cancellation: each value of the output is a sum of positive and negative contributions from the hidden units. That is a common property of learned features. They are not orthogonal, and they are rarely interpretable on their own; individually they mean little, and only their linear combination reproduces the output.</p>
<p>We would never ask “What did the Fourier series learn about our data?” The Fourier basis is fixed, and all the data-dependence is in the coefficients. A neural network is different in exactly one place: the features are learned too, so that is where most of what it “learned” lives. That is also why transfer learning works, since features learned on one problem are often a good basis for another. So these viewpoints do not make a network fully transparent, but they do make its structure transparent. It is a learned nonlinear feature transform followed by a linear model. The structure is simple; the learned features usually are not.</p>
<p>We used a simple dense network with a squared-error loss here, which makes the last layer linear regression. The same reading applies to other heads with a small change. A classifier with a softmax output is doing multinomial logistic regression on its features, so in general the last layer is a generalized linear model. It also applies to convolutional networks, recurrent networks, transformers, graph neural networks and many other architectures. What differs is how the features are generated.</p>


</section>
</section>

 ]]></content:encoded></item><item><title>pycse 2.12 is out</title><link>https://kitchingroup.cheme.cmu.edu/posts/pycse-2-12/</link><pubDate>Thu, 15 Oct 2026 00:00:00 +0000</pubDate><category>python</category><category>pycse</category><guid isPermaLink="true">https://kitchingroup.cheme.cmu.edu/posts/pycse-2-12/</guid><description>pycse 2.12 is out</description><content:encoded><![CDATA[ 

Skip to main content




<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://kitchingroup.cheme.cmu.edu/assets/software/pycse.svg" class="img-fluid quarto-figure quarto-figure-center figure-img" alt="pycse logo" width="160"></p>
</figure>
</div>
<p><a href="https://github.com/jkitchin/pycse">pycse</a> 2.12.0 is on PyPI. This is a bigger release than usual, and a few of the changes will alter results or break existing code, so it is worth reading the first section before upgrading.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb1-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">pip</span> install <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--upgrade</span> pycse</span></code></pre></div></div>
<section id="changes-that-may-affect-your-code" class="level2">
<h2 class="anchored" data-anchor-id="changes-that-may-affect-your-code">Changes that may affect your code</h2>
<p>pycse now requires Python 3.12 or newer, and the JAX stack moves up to <code>jax&gt;=0.10</code>, <code>flax&gt;=0.12.7</code> and <code>optax&gt;=0.2.5</code>.</p>
<p>Prediction intervals from <code>predict()</code> and <code>polyval()</code> now have shape <code>(n, 2)</code> instead of <code>(2, n)</code>, the same as <code>nlpredict()</code> has always returned. Column 0 is the lower bound and column 1 the upper bound. If you unpack the intervals, the fix is one transpose:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb2-1">y, yint, se <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> predict(X, y, pars, XX)</span>
<span id="cb2-2">lower, upper <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> yint.T    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># was: lower, upper = yint</span></span></code></pre></div></div>
<p>The <code>optimizer="bfgs"</code> and <code>"lbfgs"</code> options in the JAX models (<code>DPOSE</code>, <code>KAN</code>, <code>KANLLPR</code>, <code>KfoldNN</code> and <code>SISSOEnsemble</code>) now run a real L-BFGS with a line search from optax. Before, they ran Adam with a cosine schedule. Your fits will be different, and they should actually be converged now. If you need the old behavior, use <code>optimizer="adam_cosine"</code>.</p>
<p><code>JAXMonotonicRegressor</code> is now only monotone by default. It used to be convex as well, which meant concave or S-shaped monotone data could not be fitted. Pass <code>convexity="convex"</code> to get the old behavior, or <code>convexity="concave"</code> for the other case.</p>
</section>
<section id="what-is-new" class="level2">
<h2 class="anchored" data-anchor-id="what-is-new">What is new</h2>
<p>The largest addition is <code>pycse.sklearn.zenn</code>, an independent JAX/Flax implementation of Zentropy-Enhanced Neural Networks from Wang, Shang, Liu and Hao, “<a href="https://doi.org/10.1073/pnas.2511227122">ZENN: A thermodynamics-inspired computational framework for heterogeneous data-driven modeling</a>,” <em>PNAS</em> 123(1), e2511227122 (2026). It provides <code>ZENNRegressor</code>, <code>ZENNRegressorNLL</code> and <code>ZENNClassifier</code>. Like the other models in <code>pycse.sklearn</code>, they follow the scikit-learn estimator API.</p>
<p><code>LeafModelRegressor(refine_splits=True)</code> chooses each split threshold for the leaf model instead of for constant leaves, so the breakpoints land in the right place for piecewise-linear data. The active learning tools picked up a <code>penalty_scale</code> option for penalized batches, and <code>ActiveSurrogate.build()</code> and <code>LatinSquare</code> take a <code>random_state</code> so results are reproducible.</p>
<p>The MCP server works with both the 1.x and 2.x versions of <code>mcp</code>, and the Claude Code skill installed by <code>pycse skill install</code> was rewritten. It now covers the regression functions, all of the sklearn models, design of experiments and active learning.</p>
</section>
<section id="fixes" class="level2">
<h2 class="anchored" data-anchor-id="fixes">Fixes</h2>
<p><code>pycse.__version__</code> finally reports the installed version. <code>nlpredict()</code> works for a single prediction point, <code>NNGMM</code> works on Python 3.12, and <code>NNBR</code> reports the correct standard deviation of the intercept. In active learning, penalized batches no longer contain duplicate points and <code>ThompsonSampling</code> makes a new draw on every call. The hashcache backends return NumPy arrays and tuples with their original types, and <code>pycse.sklearn</code> now imports its models lazily, so <code>import pycse.sklearn</code> no longer loads every backend.</p>
<p>The full list is in the <a href="https://github.com/jkitchin/pycse/blob/master/CHANGELOG.md">changelog</a>. As always, issues and pull requests are welcome on <a href="https://github.com/jkitchin/pycse">GitHub</a>.</p>


</section>

 ]]></content:encoded></item><item><title>A new home for the blog</title><link>https://kitchingroup.cheme.cmu.edu/posts/new-site/</link><pubDate>Wed, 14 Oct 2026 00:00:00 +0000</pubDate><category>blog</category><category>python</category><guid isPermaLink="true">https://kitchingroup.cheme.cmu.edu/posts/new-site/</guid><description>A new home for the blog</description><content:encoded><![CDATA[ 

Skip to main content




<!-- SAMPLE POST: shows how code-heavy posts work. Edit or delete it. -->
<p>The blog ran on blogofile for many years, and that software is long past maintained. The 623 posts written with it are still at their original addresses, with their original look, in the <a href="https://kitchingroup.cheme.cmu.edu/blog/">archive</a>. New posts live here under <code>/posts/</code>.</p>
<p>Posts are Quarto documents (<code>.qmd</code>) or Jupyter notebooks (<code>.ipynb</code>). Code runs on my machine when I render, and the results are saved with the post, so the site builds without re-running anything.</p>
<section id="a-small-example" class="level2">
<h2 class="anchored" data-anchor-id="a-small-example">A small example</h2>
<p>Here is the first-order reaction <img src="https://latex.codecogs.com/png.latex?A%20%5Crightarrow%20B"> in a batch reactor, <img src="https://latex.codecogs.com/png.latex?%5Cfrac%7BdC_A%7D%7Bdt%7D%20=%20-k%20C_A">, solved numerically and compared with the analytical solution.</p>
<div id="8738bae6" class="cell" data-execution_count="1">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb1-1"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> numpy <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">as</span> np</span>
<span id="cb1-2"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> scipy.integrate <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> solve_ivp</span>
<span id="cb1-3"></span>
<span id="cb1-4">k <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.23</span>   <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 1/min</span></span>
<span id="cb1-5">Ca0 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">2.0</span>  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># mol/L</span></span>
<span id="cb1-6"></span>
<span id="cb1-7">sol <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> solve_ivp(<span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">lambda</span> t, Ca: <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span>k <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> Ca, (<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">20</span>), [Ca0], dense_output<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">True</span>)</span>
<span id="cb1-8">t <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.linspace(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">20</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">200</span>)</span>
<span id="cb1-9">Ca <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> sol.sol(t)[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>]</span>
<span id="cb1-10"></span>
<span id="cb1-11"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"C_A(20 min) = </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>Ca[<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>]<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:.4f}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;"> mol/L (exact </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>Ca0 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> np<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>exp(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span>k <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">20</span>)<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:.4f}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">)"</span>)</span></code></pre></div></div>
<div class="cell-output cell-output-stdout">
<pre><code>C_A(20 min) = 0.0201 mol/L (exact 0.0201)</code></pre>
</div>
</div>
<div id="cell-fig-batch" class="cell" data-execution_count="2">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb3-1"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> matplotlib.pyplot <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">as</span> plt</span>
<span id="cb3-2"></span>
<span id="cb3-3">fig, ax <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> plt.subplots(figsize<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">6</span>, <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">3.5</span>))</span>
<span id="cb3-4">ax.plot(t, Ca, label<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"solve_ivp"</span>)</span>
<span id="cb3-5">ax.plot(t[::<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">15</span>], Ca0 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> np.exp(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span>k <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> t[::<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">15</span>]), <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"o"</span>, label<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"exact"</span>)</span>
<span id="cb3-6">ax.set_xlabel(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"t (min)"</span>)</span>
<span id="cb3-7">ax.set_ylabel(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"$C_A$ (mol/L)"</span>)</span>
<span id="cb3-8">ax.legend()</span>
<span id="cb3-9">plt.tight_layout()</span></code></pre></div></div>
<div class="cell-output cell-output-display">
<div id="fig-batch" class="quarto-float quarto-figure quarto-figure-center anchored" alt="Line plot of the concentration of A falling exponentially from 2 mol/L toward zero over 20 minutes, with the numerical solution passing through the exact-solution markers.">
<figure class="quarto-float quarto-float-fig figure">
<div aria-describedby="fig-batch-caption-0ceaefa1-69ba-4598-a22c-09a6ac19f8ca">
<img src="https://kitchingroup.cheme.cmu.edu/posts/new-site/index_files/figure-html/fig-batch-output-1.png" alt="Line plot of the concentration of A falling exponentially from 2 mol/L toward zero over 20 minutes, with the numerical solution passing through the exact-solution markers." width="568" height="326" class="figure-img">
</div>
<figcaption class="quarto-float-caption-bottom quarto-float-caption quarto-float-fig" id="fig-batch-caption-0ceaefa1-69ba-4598-a22c-09a6ac19f8ca">
Figure&nbsp;1: Concentration of A in a first-order batch reaction.
</figcaption>
</figure>
</div>
</div>
</div>
<p>Figure&nbsp;1 shows the two agree. Equations, figures and cross-references all come from Quarto, and code blocks have a copy button.</p>


</section>

 ]]></content:encoded></item><item>
      <title>Caching expensive function calls so you don't have to rerun them</title>
      <link>https://kitchingroup.cheme.cmu.edu/blog/2023/02/01/Caching-expensive-function-calls-so-you-don-t-have-to-rerun-them</link>
      <pubDate>Wed, 01 Feb 2023 20:09:44 EST</pubDate>
      <category><![CDATA[python]]></category>
      <guid isPermaLink="false">TuAI-nnf5JY8R8O-DrQ6Ra7u8hI=</guid>
      <description>Caching expensive function calls so you don't have to rerun them</description>
      <content:encoded><![CDATA[


&lt;div id="table-of-contents" role="doc-toc"&gt;
&lt;h2&gt;Table of Contents&lt;/h2&gt;
&lt;div id="text-table-of-contents" role="doc-toc"&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#org07769a2"&gt;1. where things start to go wrong&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#org45a5a5a"&gt;1.1. Global variables&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#org81a44de"&gt;1.2. running functions with mutable arguments&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#orgeecc951"&gt;1.3. If you run the same function different ways, the cache is not reused&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#orgb8d7f30"&gt;1.4. Fragile cache invalidation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#orgd5fbbc2"&gt;2. Some partial solutions with pycse.hashcache&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#orge7c0587"&gt;2.1. No known problem with global variables&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#org857113f"&gt;2.2. hashcache and mutable arguments&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#org4925e4a"&gt;2.3. Reuse the cache when you run different ways&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#org8461f88"&gt;2.4. Insensitivity to unimportant changes&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#orgfb0f2ed"&gt;3. Is it the answer?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;
Check out the video at:
&lt;/p&gt;
&lt;iframe width="560" height="315" src="https://www.youtube.com/embed/Sp0qebuYsZU" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen&gt;&lt;/iframe&gt;

&lt;p&gt;
Nobody likes to run expensive jobs more than necessary, so cache solutions are often used where you save the results, and look them up later. There is functools.cache in Python, but it is memory only, and not persistent, so you start over in a new session.
&lt;/p&gt;

&lt;p&gt;
For persistent cache, joblib (&lt;a href="https://joblib.readthedocs.io/en/latest/"&gt;https://joblib.readthedocs.io/en/latest/&lt;/a&gt;) is a standard tool for this. Here is a simple example: 
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-jupyter-python"&gt;&lt;span style="color: #0000FF;"&gt;from&lt;/span&gt; joblib &lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; Memory
&lt;span style="color: #BA36A5;"&gt;location&lt;/span&gt; = &lt;span style="color: #008000;"&gt;'/Users/jkitchin/Dropbox/emacs/journal/2023/02/01/joblib_cache/joblib_cache'&lt;/span&gt;
&lt;span style="color: #BA36A5;"&gt;memory&lt;/span&gt; = Memory(location, verbose=0)

&lt;span style="color: #6434A3;"&gt;@memory.cache&lt;/span&gt;
&lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;fun&lt;/span&gt;(x=1.0):
    &lt;span style="color: #006FE0;"&gt;print&lt;/span&gt;(&lt;span style="color: #008000;"&gt;'If you see this, go get a coffee while it runs &amp;#128012;.'&lt;/span&gt;)
    &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; x**2

&lt;span style="color: #006FE0;"&gt;print&lt;/span&gt;(fun(2)) &lt;span style="color: #8D8D84;"&gt;# &lt;/span&gt;&lt;span style="color: #8D8D84; font-style: italic;"&gt;Runs the function&lt;/span&gt;
&lt;span style="color: #006FE0;"&gt;print&lt;/span&gt;(fun(2)) &lt;span style="color: #8D8D84;"&gt;# &lt;/span&gt;&lt;span style="color: #8D8D84; font-style: italic;"&gt;Looks up the cached value&lt;/span&gt;
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
If you see this, go get a coffee while it runs 🐌.
4
4
&lt;/p&gt;

&lt;p&gt;
That works because joblib saves the results in a file in the location you specify.
&lt;/p&gt;

&lt;p&gt;
Here is another example with another arg.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-jupyter-python"&gt;&lt;span style="color: #6434A3;"&gt;@memory.cache&lt;/span&gt;
&lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;f2&lt;/span&gt;(x=1.0, a=3):
    &lt;span style="color: #006FE0;"&gt;print&lt;/span&gt;(f&lt;span style="color: #008000;"&gt;'If you see this, go get a coffee while it runs. a=&lt;/span&gt;{"&amp;#128012;"*a}&lt;span style="color: #008000;"&gt;'&lt;/span&gt;)
    &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; a*x**2

(f2(2),       &lt;span style="color: #8D8D84;"&gt;# &lt;/span&gt;&lt;span style="color: #8D8D84; font-style: italic;"&gt;Runs function&lt;/span&gt;
 f2(2, a=3),  &lt;span style="color: #8D8D84;"&gt;# &lt;/span&gt;&lt;span style="color: #8D8D84; font-style: italic;"&gt;calls cache&lt;/span&gt;
 f2(2, 4))    &lt;span style="color: #8D8D84;"&gt;# &lt;/span&gt;&lt;span style="color: #8D8D84; font-style: italic;"&gt;Runs another function because a changed&lt;/span&gt;
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
If you see this, go get a coffee while it runs. a=🐌🐌🐌
If you see this, go get a coffee while it runs. a=🐌🐌🐌🐌
&lt;/p&gt;
&lt;table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides"&gt;


&lt;colgroup&gt;
&lt;col  class="org-right" /&gt;

&lt;col  class="org-right" /&gt;

&lt;col  class="org-right" /&gt;
&lt;/colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class="org-right"&gt;12&lt;/td&gt;
&lt;td class="org-right"&gt;12&lt;/td&gt;
&lt;td class="org-right"&gt;16&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;
Here, we look up from the cache each time.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-jupyter-python"&gt;(f2(2), f2(2, a=3), f2(2, 4))
&lt;/pre&gt;
&lt;/div&gt;

&lt;table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides"&gt;


&lt;colgroup&gt;
&lt;col  class="org-right" /&gt;

&lt;col  class="org-right" /&gt;

&lt;col  class="org-right" /&gt;
&lt;/colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class="org-right"&gt;12&lt;/td&gt;
&lt;td class="org-right"&gt;12&lt;/td&gt;
&lt;td class="org-right"&gt;16&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;


&lt;div id="outline-container-org07769a2" class="outline-2"&gt;
&lt;h2 id="org07769a2"&gt;&lt;span class="section-number-2"&gt;1.&lt;/span&gt; where things start to go wrong&lt;/h2&gt;
&lt;div class="outline-text-2" id="text-1"&gt;
&lt;/div&gt;
&lt;div id="outline-container-org45a5a5a" class="outline-3"&gt;
&lt;h3 id="org45a5a5a"&gt;&lt;span class="section-number-3"&gt;1.1.&lt;/span&gt; Global variables&lt;/h3&gt;
&lt;div class="outline-text-3" id="text-1-1"&gt;
&lt;p&gt;
First, we look at an uncached version of a function that uses a global variable.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-jupyter-python"&gt;&lt;span style="color: #BA36A5;"&gt;a&lt;/span&gt; = 3

&lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;f3&lt;/span&gt;(x=1.0):
    &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; a*x**2

f3(2)
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
12
&lt;/p&gt;

&lt;p&gt;
We can change &lt;code&gt;a&lt;/code&gt; and see the change.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-jupyter-python"&gt;&lt;span style="color: #BA36A5;"&gt;a&lt;/span&gt;=0
f3(2)
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
0
&lt;/p&gt;

&lt;p&gt;
Now we look at a cached version.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-jupyter-python"&gt;&lt;span style="color: #BA36A5;"&gt;a&lt;/span&gt; = 3
&lt;span style="color: #6434A3;"&gt;@memory.cache&lt;/span&gt;
&lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;f4&lt;/span&gt;(x=1.0):
    &lt;span style="color: #006FE0;"&gt;print&lt;/span&gt;(&lt;span style="color: #008000;"&gt;'If you see this, go get a coffee while it runs &amp;#128012;.'&lt;/span&gt;)
    &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; a*x**2

(f4(2), f4(2), f4(2))
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
If you see this, go get a coffee while it runs 🐌.
&lt;/p&gt;
&lt;table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides"&gt;


&lt;colgroup&gt;
&lt;col  class="org-right" /&gt;

&lt;col  class="org-right" /&gt;

&lt;col  class="org-right" /&gt;
&lt;/colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class="org-right"&gt;12&lt;/td&gt;
&lt;td class="org-right"&gt;12&lt;/td&gt;
&lt;td class="org-right"&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;
Changing the global variable does not change the cache though. uh oh. This is just wrong. The answers should clearly be 0. Incorrect cache invalidation strikes.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-jupyter-python"&gt;&lt;span style="color: #BA36A5;"&gt;a&lt;/span&gt; = 0
(f4(2), f4(2), f4(2))
&lt;/pre&gt;
&lt;/div&gt;

&lt;table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides"&gt;


&lt;colgroup&gt;
&lt;col  class="org-right" /&gt;

&lt;col  class="org-right" /&gt;

&lt;col  class="org-right" /&gt;
&lt;/colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class="org-right"&gt;12&lt;/td&gt;
&lt;td class="org-right"&gt;12&lt;/td&gt;
&lt;td class="org-right"&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;

&lt;div id="outline-container-org81a44de" class="outline-3"&gt;
&lt;h3 id="org81a44de"&gt;&lt;span class="section-number-3"&gt;1.2.&lt;/span&gt; running functions with mutable arguments&lt;/h3&gt;
&lt;div class="outline-text-3" id="text-1-2"&gt;
&lt;p&gt;
Using mutable arguments is a recipe for trouble and unanticipated problems, but it is easy to unintentionally do, and not always obvious, as I show here.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-jupyter-python"&gt;&lt;span style="color: #0000FF;"&gt;from&lt;/span&gt; ase.build &lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; bulk
&lt;span style="color: #0000FF;"&gt;from&lt;/span&gt; ase.calculators.emt &lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; EMT

&lt;span style="color: #BA36A5;"&gt;atoms&lt;/span&gt; = bulk(&lt;span style="color: #008000;"&gt;'Pd'&lt;/span&gt;)
atoms.set_calculator(EMT())

&lt;span style="color: #6434A3;"&gt;@memory.cache&lt;/span&gt;
&lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;f&lt;/span&gt;(atoms):
    &lt;span style="color: #006FE0;"&gt;print&lt;/span&gt;(&lt;span style="color: #008000;"&gt;'If you see this, go get a coffee.'&lt;/span&gt;)
    &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; atoms.get_potential_energy()

(f(atoms), f(atoms))

&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
If you see this, go get a coffee.
If you see this, go get a coffee.
&lt;/p&gt;
&lt;table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides"&gt;


&lt;colgroup&gt;
&lt;col  class="org-right" /&gt;

&lt;col  class="org-right" /&gt;
&lt;/colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class="org-right"&gt;0.0003422625372841992&lt;/td&gt;
&lt;td class="org-right"&gt;0.0003422625372841992&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;
You can see this ran twice. The reason is that the atoms object was mutated by adding data onto it. Here is how I know:
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-jupyter-python"&gt;&lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; joblib
&lt;span style="color: #BA36A5;"&gt;atoms&lt;/span&gt; = bulk(&lt;span style="color: #008000;"&gt;'Pd'&lt;/span&gt;)
atoms.set_calculator(EMT())
joblib.&lt;span style="color: #006FE0;"&gt;hash&lt;/span&gt;(atoms), atoms._calc.results
&lt;/pre&gt;
&lt;/div&gt;

&lt;table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides"&gt;


&lt;colgroup&gt;
&lt;col  class="org-left" /&gt;

&lt;col  class="org-left" /&gt;
&lt;/colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class="org-left"&gt;ee2ed2eb9fdb4b3d6416803a33f43a22&lt;/td&gt;
&lt;td class="org-left"&gt;nil&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;
Here you can see that simply running the get energy function the hash changes because the results dictionary on the calculator changes. That means subsequent uses of the atoms object will have a different hash, and you cannot rely on that to look up the results. In this case the results should not change the output of the function, but since they are included in the hash, it incorrectly invalidates the hash.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-jupyter-python"&gt;atoms.get_potential_energy()
joblib.&lt;span style="color: #006FE0;"&gt;hash&lt;/span&gt;(atoms), atoms._calc.results
&lt;/pre&gt;
&lt;/div&gt;

&lt;table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides"&gt;


&lt;colgroup&gt;
&lt;col  class="org-left" /&gt;

&lt;col  class="org-left" /&gt;
&lt;/colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class="org-left"&gt;d37ef0a5761f499060b4f55bdf644814&lt;/td&gt;
&lt;td class="org-left"&gt;(energy : 0.0003422625372841992 energies : array ((0.00034226)) free_energy : 0.0003422625372841992 forces : array (((0 0 0))))&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;
Suffice to say, this is non-obvious, but having seen it, not a surprise; mutable arguments are frequently a source of problems.
&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;

&lt;div id="outline-container-orgeecc951" class="outline-3"&gt;
&lt;h3 id="orgeecc951"&gt;&lt;span class="section-number-3"&gt;1.3.&lt;/span&gt; If you run the same function different ways, the cache is not reused&lt;/h3&gt;
&lt;div class="outline-text-3" id="text-1-3"&gt;
&lt;p&gt;
Some aspects of this are specific to org-mode and how scripts are run in it. Here we have to use an absolute path to make sure we use the right cache. That still doesn't solve the problem though as we will see.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-python"&gt;&lt;span style="color: #0000FF;"&gt;from&lt;/span&gt; joblib &lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; Memory
&lt;span style="color: #BA36A5;"&gt;location&lt;/span&gt; = &lt;span style="color: #008000;"&gt;'/Users/jkitchin/Dropbox/emacs/journal/2023/02/01/joblib_cache/joblib_cache'&lt;/span&gt;
&lt;span style="color: #BA36A5;"&gt;memory&lt;/span&gt; = Memory(location, verbose=0)

a = 3
&lt;span style="color: #6434A3;"&gt;@memory.cache&lt;/span&gt;
&lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;f4&lt;/span&gt;(x=1.0):
    &lt;span style="color: #006FE0;"&gt;print&lt;/span&gt;(&lt;span style="color: #008000;"&gt;'If you see this, go get a coffee while it runs'&lt;/span&gt;)
    &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; a*x**2

&lt;span style="color: #006FE0;"&gt;print&lt;/span&gt;((f4(2), f4(2), f4(2)))
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
The issue is that joblib uses the file name it thinks the function is from in the path it saves the results. The filename is different
&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;

&lt;div id="outline-container-orgb8d7f30" class="outline-3"&gt;
&lt;h3 id="orgb8d7f30"&gt;&lt;span class="section-number-3"&gt;1.4.&lt;/span&gt; Fragile cache invalidation&lt;/h3&gt;
&lt;div class="outline-text-3" id="text-1-4"&gt;
&lt;p&gt;
joblib uses the function source in its hash. That means &lt;i&gt;any&lt;/i&gt; change to the source, including the function name, renaming variables, whitespace, comments or docstring changes invalidates the hash even though they may have no change in the output. That is an overabundance of caution, but simple to implement.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-jupyter-python"&gt;&lt;span style="color: #6434A3;"&gt;@memory.cache&lt;/span&gt;
&lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;f4&lt;/span&gt;(x=1.0):
    &lt;span style="color: #036A07;"&gt;'add a ds.'&lt;/span&gt;
    &lt;span style="color: #8D8D84;"&gt;# &lt;/span&gt;&lt;span style="color: #8D8D84; font-style: italic;"&gt;comment&lt;/span&gt;
    &lt;span style="color: #006FE0;"&gt;print&lt;/span&gt;(&lt;span style="color: #008000;"&gt;'If you see this, go get a coffee while it runs'&lt;/span&gt;)
    &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; a*x**2

&lt;span style="color: #006FE0;"&gt;print&lt;/span&gt;((f4(2), f4(2), f4(2)))
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
If you see this, go get a coffee while it runs
(0, 0, 0)
&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;


&lt;div id="outline-container-orgd5fbbc2" class="outline-2"&gt;
&lt;h2 id="orgd5fbbc2"&gt;&lt;span class="section-number-2"&gt;2.&lt;/span&gt; Some partial solutions with pycse.hashcache&lt;/h2&gt;
&lt;div class="outline-text-2" id="text-2"&gt;
&lt;p&gt;
I wrote &lt;code&gt;hashcache&lt;/code&gt; to solve some of these problems. It is actually built on top of joblib.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-jupyter-python"&gt;&lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; pycse
pycse.__version__, pycse.__file__
&lt;/pre&gt;
&lt;/div&gt;

&lt;table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides"&gt;


&lt;colgroup&gt;
&lt;col  class="org-right" /&gt;

&lt;col  class="org-left" /&gt;
&lt;/colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class="org-right"&gt;2.2.1&lt;/td&gt;
&lt;td class="org-left"&gt;/Users/jkitchin/Dropbox/python/pycse/pycse/__init__.py&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-jupyter-python"&gt;&lt;span style="color: #0000FF;"&gt;from&lt;/span&gt; pycse.hashcache &lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; hashcache
  
hashcache.&lt;span style="color: #BA36A5;"&gt;location&lt;/span&gt; = &lt;span style="color: #008000;"&gt;"/Users/jkitchin/Dropbox/emacs/journal/2023/02/01/cache"&lt;/span&gt;
hashcache.&lt;span style="color: #BA36A5;"&gt;verbose&lt;/span&gt; = &lt;span style="color: #D0372D;"&gt;False&lt;/span&gt;

&lt;span style="color: #6434A3;"&gt;@hashcache&lt;/span&gt;
&lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;h1&lt;/span&gt;(x):
    &lt;span style="color: #006FE0;"&gt;print&lt;/span&gt;(&lt;span style="color: #008000;"&gt;'This runs soo slow... Go get a coffee'&lt;/span&gt;)
    &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; x**2

h1(2), h1(2)
&lt;/pre&gt;
&lt;/div&gt;

&lt;table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides"&gt;


&lt;colgroup&gt;
&lt;col  class="org-right" /&gt;

&lt;col  class="org-right" /&gt;
&lt;/colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class="org-right"&gt;4&lt;/td&gt;
&lt;td class="org-right"&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;

&lt;div id="outline-container-orge7c0587" class="outline-3"&gt;
&lt;h3 id="orge7c0587"&gt;&lt;span class="section-number-3"&gt;2.1.&lt;/span&gt; No known problem with global variables&lt;/h3&gt;
&lt;div class="outline-text-3" id="text-2-1"&gt;
&lt;div class="org-src-container"&gt;
&lt;pre class="src src-jupyter-python"&gt;&lt;span style="color: #BA36A5;"&gt;a&lt;/span&gt; = 3
&lt;span style="color: #6434A3;"&gt;@hashcache&lt;/span&gt;
&lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;h4&lt;/span&gt;(x=1.0):
    &lt;span style="color: #006FE0;"&gt;print&lt;/span&gt;(&lt;span style="color: #008000;"&gt;'If you see this, go get a coffee while it runs'&lt;/span&gt;)
    &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; a*x**2

(h4(2), h4(2), h4(2))

&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
If you see this, go get a coffee while it runs
&lt;/p&gt;
&lt;table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides"&gt;


&lt;colgroup&gt;
&lt;col  class="org-right" /&gt;

&lt;col  class="org-right" /&gt;

&lt;col  class="org-right" /&gt;
&lt;/colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class="org-right"&gt;12&lt;/td&gt;
&lt;td class="org-right"&gt;12&lt;/td&gt;
&lt;td class="org-right"&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-jupyter-python"&gt;&lt;span style="color: #BA36A5;"&gt;a&lt;/span&gt;=0
(h4(2), h4(2), h4(2))
&lt;/pre&gt;
&lt;/div&gt;

&lt;table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides"&gt;


&lt;colgroup&gt;
&lt;col  class="org-right" /&gt;

&lt;col  class="org-right" /&gt;

&lt;col  class="org-right" /&gt;
&lt;/colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class="org-right"&gt;0&lt;/td&gt;
&lt;td class="org-right"&gt;0&lt;/td&gt;
&lt;td class="org-right"&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;
Whew!!! we got the right answers. hashcache does a better job detecting the external change.
&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;

&lt;div id="outline-container-org857113f" class="outline-3"&gt;
&lt;h3 id="org857113f"&gt;&lt;span class="section-number-3"&gt;2.2.&lt;/span&gt; hashcache and mutable arguments&lt;/h3&gt;
&lt;div class="outline-text-3" id="text-2-2"&gt;
&lt;p&gt;
hashcache does not solve the mutable argument problem, but, it does warn you it detected it.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-jupyter-python"&gt;&lt;span style="color: #0000FF;"&gt;from&lt;/span&gt; ase.build &lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; bulk
&lt;span style="color: #0000FF;"&gt;from&lt;/span&gt; ase.calculators.emt &lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; EMT

&lt;span style="color: #BA36A5;"&gt;atoms&lt;/span&gt; = bulk(&lt;span style="color: #008000;"&gt;'Pd'&lt;/span&gt;)
atoms.set_calculator(EMT())

&lt;span style="color: #6434A3;"&gt;@hashcache&lt;/span&gt;
&lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;h&lt;/span&gt;(atoms):
    &lt;span style="color: #006FE0;"&gt;print&lt;/span&gt;(&lt;span style="color: #008000;"&gt;'If you see this, go get a coffee.'&lt;/span&gt;)
    &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; atoms.get_potential_energy()

(h(atoms), h(atoms), h(atoms))
&lt;/pre&gt;
&lt;/div&gt;

&lt;table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides"&gt;


&lt;colgroup&gt;
&lt;col  class="org-right" /&gt;

&lt;col  class="org-right" /&gt;

&lt;col  class="org-right" /&gt;
&lt;/colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class="org-right"&gt;0.0003422625372841992&lt;/td&gt;
&lt;td class="org-right"&gt;0.0003422625372841992&lt;/td&gt;
&lt;td class="org-right"&gt;0.0003422625372841992&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;

&lt;div id="outline-container-org4925e4a" class="outline-3"&gt;
&lt;h3 id="org4925e4a"&gt;&lt;span class="section-number-3"&gt;2.3.&lt;/span&gt; Reuse the cache when you run different ways&lt;/h3&gt;
&lt;div class="outline-text-3" id="text-2-3"&gt;
&lt;p&gt;
hashcache uses the same cache at the function and function environment level, so it avoids reruns even from different places. It is a judgement call by you to say if this is the right thing to do.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-jupyter-python"&gt;&lt;span style="color: #006FE0;"&gt;print&lt;/span&gt;(h1(2), h1(2))
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
4 4
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-python"&gt;&lt;span style="color: #0000FF;"&gt;from&lt;/span&gt; pycse.hashcache &lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; hashcache
hashcache.&lt;span style="color: #BA36A5;"&gt;location&lt;/span&gt; = &lt;span style="color: #008000;"&gt;"/Users/jkitchin/Dropbox/emacs/journal/2023/02/01/cache"&lt;/span&gt;

&lt;span style="color: #6434A3;"&gt;@hashcache&lt;/span&gt;
&lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;h1&lt;/span&gt;(x):
    &lt;span style="color: #006FE0;"&gt;print&lt;/span&gt;(&lt;span style="color: #008000;"&gt;'This runs soo slow... Go get a coffee'&lt;/span&gt;)
    &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; x**2

&lt;span style="color: #006FE0;"&gt;print&lt;/span&gt;(h1(2), h1(2))
&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;

&lt;div id="outline-container-org8461f88" class="outline-3"&gt;
&lt;h3 id="org8461f88"&gt;&lt;span class="section-number-3"&gt;2.4.&lt;/span&gt; Insensitivity to unimportant changes&lt;/h3&gt;
&lt;div class="outline-text-3" id="text-2-4"&gt;
&lt;p&gt;
Instead of hashing the source of the function, in hashcache I hash the bytecode instead. This is certainly less sensitive to unimportant changes like docstrings, comments or whitespace. I do use the function name in the hash, so even though that does not affect the output, I thought it might be confusing in the future.
&lt;/p&gt;

&lt;p&gt;
Here, small changes like comments, docstrings, etc, don't affect the hash.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-jupyter-python"&gt;&lt;span style="color: #BA36A5;"&gt;a&lt;/span&gt; = 3
&lt;span style="color: #6434A3;"&gt;@hashcache&lt;/span&gt;
&lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;h4&lt;/span&gt;(x=1.0):
    &lt;span style="color: #036A07;"&gt;'doc string'&lt;/span&gt;
    &lt;span style="color: #8D8D84;"&gt;# &lt;/span&gt;&lt;span style="color: #8D8D84; font-style: italic;"&gt;comments&lt;/span&gt;
    &lt;span style="color: #006FE0;"&gt;print&lt;/span&gt;(&lt;span style="color: #008000;"&gt;'If you see this, go get a coffee while it runs'&lt;/span&gt;)
    &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; a*x**2    

(h4(2), h4(2), h4(2))
&lt;/pre&gt;
&lt;/div&gt;

&lt;table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides"&gt;


&lt;colgroup&gt;
&lt;col  class="org-right" /&gt;

&lt;col  class="org-right" /&gt;

&lt;col  class="org-right" /&gt;
&lt;/colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class="org-right"&gt;12&lt;/td&gt;
&lt;td class="org-right"&gt;12&lt;/td&gt;
&lt;td class="org-right"&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;

&lt;div id="outline-container-orgfb0f2ed" class="outline-2"&gt;
&lt;h2 id="orgfb0f2ed"&gt;&lt;span class="section-number-2"&gt;3.&lt;/span&gt; Is it the answer?&lt;/h2&gt;
&lt;div class="outline-text-2" id="text-3"&gt;
&lt;p&gt;
Probably not completely. It is almost certain I have not captured all the ways the cache should be invalidated, or when a new cache should be used. hashcache is for now, a proof of concept in understanding why this is a hard problem to solve. I prefer its behavior over the defaults in joblib so far though.
&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Copyright (C) 2023 by John Kitchin. See the &lt;a href="/copying.html"&gt;License&lt;/a&gt; for information about copying.&lt;p&gt;
&lt;p&gt;&lt;a href="/org/2023/02/01/Caching-expensive-function-calls-so-you-don't-have-to-rerun-them.org"&gt;org-mode source&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Org-mode version = 9.5.5&lt;/p&gt;]]></content:encoded>
    </item>
    <item>
      <title>Line integrals in Python with autograd</title>
      <link>https://kitchingroup.cheme.cmu.edu/blog/2018/11/16/Line-integrals-in-Python-with-autograd</link>
      <pubDate>Fri, 16 Nov 2018 08:39:44 EST</pubDate>
      <category><![CDATA[autograd]]></category>
      <category><![CDATA[integration]]></category>
      <category><![CDATA[python]]></category>
      <guid isPermaLink="false">8pnl2uSxymXf56S8frpp6psrwys=</guid>
      <description>Line integrals in Python with autograd</description>
      <content:encoded><![CDATA[


&lt;div id="table-of-contents"&gt;
&lt;h2&gt;Table of Contents&lt;/h2&gt;
&lt;div id="text-table-of-contents"&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#org107f13d"&gt;1. Summary&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;
A line integral is an integral of a function along a curve in space. We usually represent the curve by a parametric equation, e.g. \(\mathbf{r}(t) = [x(t), y(t), z(t)] = x(t)\mathbf{i} + y(t)\mathbf{j} + z(t)\mathbf{k}\).  So, in general the curve will be a vector function, and the function we want to integrate will also be a vector function.
&lt;/p&gt;

&lt;p&gt;
Then, we can write the line integral definition as:
&lt;/p&gt;

&lt;p&gt;
\(\int_C \mathbf{F(r)}\cdot d\mathbf{r} = \int_a^b \mathbf{F}({\mathbf{r}(t)) \cdot \mathbf{r'}(t) dt\) where \(\mathbf{r'}(t) = \frac{d\mathbf{r}}{dt}\). This integrand is a scalar function, because of the dot product.
&lt;/p&gt;

&lt;p&gt;
The following examples are adapted from Chapter 10 in Advanced Engineering Mathematics by Kreysig.
&lt;/p&gt;

&lt;p&gt;
The first example is the evaluation of  a line integral in the plane. We want to evaluate the integral of \(\mathbf{F(r)}=[-y, -xy]\) on the curve \(\mathbf{r(t)}=[-sin(t), cos(t)]\) from t=0 to t = &amp;pi;/2. The answer in the book is given as 0.4521. Here we evaluate this numerically, using autograd for the relevant derivative. Since the curve has multiple outputs, we have to use the jacobian function to get the derivatives. After that, it is a simple bit of matrix multiplication, and a call to the quad function.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-ipython"&gt;&lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; autograd.numpy &lt;span style="color: #0000FF;"&gt;as&lt;/span&gt; np
&lt;span style="color: #0000FF;"&gt;from&lt;/span&gt; autograd &lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; jacobian
&lt;span style="color: #0000FF;"&gt;from&lt;/span&gt; scipy.integrate &lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; quad

&lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;F&lt;/span&gt;(X):
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #BA36A5;"&gt;x&lt;/span&gt;, &lt;span style="color: #BA36A5;"&gt;y&lt;/span&gt; = X
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; -y, -x * y

&lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;r&lt;/span&gt;(t):
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; np.array([-np.sin(t), np.cos(t)])

&lt;span style="color: #BA36A5;"&gt;drdt&lt;/span&gt; = jacobian(r)

&lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;integrand&lt;/span&gt;(t):
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; F(r(t)) @ drdt(t)

&lt;span style="color: #BA36A5;"&gt;I&lt;/span&gt;, &lt;span style="color: #BA36A5;"&gt;e&lt;/span&gt; = quad(integrand, 0.0, np.pi / 2)
&lt;span style="color: #0000FF;"&gt;print&lt;/span&gt;(f&lt;span style="color: #008000;"&gt;'The integral is {I:1.4f}.'&lt;/span&gt;)
&lt;/pre&gt;
&lt;/div&gt;

&lt;pre class="example"&gt;
The integral is 0.4521.


&lt;/pre&gt;

&lt;p&gt;
We get the same result as the analytical solution.
&lt;/p&gt;


&lt;p&gt;
The next example is in three dimensions. Find the line integral along \(\mathbf{r}(t)=[cos(t), sin(t), 3t]\) of the function \(\mathbf{F(r)}=[z, x, y]\) from t=0 to t=2 &amp;pi;. The solution is given as 21.99.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-ipython"&gt;&lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; autograd.numpy &lt;span style="color: #0000FF;"&gt;as&lt;/span&gt; np
&lt;span style="color: #0000FF;"&gt;from&lt;/span&gt; autograd &lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; elementwise_grad, grad, jacobian

&lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;F&lt;/span&gt;(X):
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #BA36A5;"&gt;x&lt;/span&gt;, &lt;span style="color: #BA36A5;"&gt;y&lt;/span&gt;, &lt;span style="color: #BA36A5;"&gt;z&lt;/span&gt; = X
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; [z, x, y]

&lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;C&lt;/span&gt;(t):
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; np.array([np.cos(t), np.sin(t), 3 * t])

&lt;span style="color: #BA36A5;"&gt;dCdt&lt;/span&gt; = jacobian(C, 0)

&lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;integrand&lt;/span&gt;(t):
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; F(C(t)) @ dCdt(t)

&lt;span style="color: #BA36A5;"&gt;I&lt;/span&gt;, &lt;span style="color: #BA36A5;"&gt;e&lt;/span&gt; = quad(integrand, 0, 2 * np.pi)
&lt;span style="color: #0000FF;"&gt;print&lt;/span&gt;(f&lt;span style="color: #008000;"&gt;'The integral is {I:1.2f}.'&lt;/span&gt;)
&lt;/pre&gt;
&lt;/div&gt;

&lt;pre class="example"&gt;
The integral is 21.99.


&lt;/pre&gt;

&lt;p&gt;
That is also the same as the analytical solution. Note the real analytical solution was 7 &amp;pi;, which is nearly equivalent to our answer.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-ipython"&gt;7 * np.pi - I
&lt;/pre&gt;
&lt;/div&gt;

&lt;pre class="example"&gt;
3.552713678800501e-15

&lt;/pre&gt;


&lt;p&gt;
As a final example, we consider an alternate form of the line integral. In this form we do not use a dot product, so the integral results in a vector. This doesn't require anything from autograd, but does require us to be somewhat clever in how to do the integrals since quad can only integrate scalar functions. We need to integrate each component of the integrand independently. Here is one approach where we use lambda functions for each component. You could also manually separate the components.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-ipython"&gt;&lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;F&lt;/span&gt;(r):
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #BA36A5;"&gt;x&lt;/span&gt;, &lt;span style="color: #BA36A5;"&gt;y&lt;/span&gt;, &lt;span style="color: #BA36A5;"&gt;z&lt;/span&gt; = r
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; x * y, y * z, z

&lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;r&lt;/span&gt;(t):
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; np.array([np.cos(t), np.sin(t), 3 * t])

&lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;integrand&lt;/span&gt;(t):
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; F(r(t))

[quad(&lt;span style="color: #0000FF;"&gt;lambda&lt;/span&gt; t: integrand(t)[i], 0, 2 * np.pi)[0] &lt;span style="color: #0000FF;"&gt;for&lt;/span&gt; i &lt;span style="color: #0000FF;"&gt;in&lt;/span&gt; [0, 1, 2]]
&lt;/pre&gt;
&lt;/div&gt;

&lt;pre class="example"&gt;
[-6.9054847581172525e-18, -18.849555921538755, 59.21762640653615]

&lt;/pre&gt;

&lt;p&gt;
The analytical solution in this case was given as:
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-ipython"&gt;[0, -6 * np.pi, 6 * np.pi**2]
&lt;/pre&gt;
&lt;/div&gt;

&lt;pre class="example"&gt;
[0, -18.84955592153876, 59.21762640653615]

&lt;/pre&gt;

&lt;p&gt;
which is evidently the same as our numerical solution.
&lt;/p&gt;

&lt;p&gt;
Maybe an alternative, but more verbose is this vectorized integrate function. We still make temporary functions for integrating, and the vectorization is essentially like the list comprehension above, but we avoid the lambda functions.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-ipython"&gt;&lt;span style="color: #6434A3;"&gt;@np.vectorize&lt;/span&gt;
&lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;integrate&lt;/span&gt;(i):
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;integrand&lt;/span&gt;(t):
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; F(r(t))[i]
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #BA36A5;"&gt;I&lt;/span&gt;, &lt;span style="color: #BA36A5;"&gt;e&lt;/span&gt; = quad(integrand, 0, 2 * np.pi)
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; I

integrate([0, 1, 2])
&lt;/pre&gt;
&lt;/div&gt;

&lt;pre class="example"&gt;
array([ -6.90548476e-18,  -1.88495559e+01,   5.92176264e+01])

&lt;/pre&gt;

&lt;div id="outline-container-org107f13d" class="outline-2"&gt;
&lt;h2 id="org107f13d"&gt;&lt;span class="section-number-2"&gt;1&lt;/span&gt; Summary&lt;/h2&gt;
&lt;div class="outline-text-2" id="text-1"&gt;
&lt;p&gt;
Once again, autograd provides a convenient way to compute function jacobians which make it easy to evaluate line integrals in Python.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Copyright (C) 2018 by John Kitchin. See the &lt;a href="/copying.html"&gt;License&lt;/a&gt; for information about copying.&lt;p&gt;
&lt;p&gt;&lt;a href="/org/2018/11/16/Line-integrals-in-Python-with-autograd.org"&gt;org-mode source&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Org-mode version = 9.1.14&lt;/p&gt;]]></content:encoded>
    </item>
    <item>
      <title>Autograd and the derivative of an integral function</title>
      <link>https://kitchingroup.cheme.cmu.edu/blog/2018/10/10/Autograd-and-the-derivative-of-an-integral-function</link>
      <pubDate>Wed, 10 Oct 2018 18:24:12 EDT</pubDate>
      <category><![CDATA[autograd]]></category>
      <category><![CDATA[python]]></category>
      <guid isPermaLink="false">cO6xXl0wefWmv_shWZaPoagKeJs=</guid>
      <description>Autograd and the derivative of an integral function</description>
      <content:encoded><![CDATA[


&lt;div id="table-of-contents"&gt;
&lt;h2&gt;Table of Contents&lt;/h2&gt;
&lt;div id="text-table-of-contents"&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#org4ec4dc0"&gt;1. Example 1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#org079e9a6"&gt;2. Example 2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#org7aa394c"&gt;3. Summary&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;
There are many functions that are defined by integrals. The &lt;a href="https://en.wikipedia.org/wiki/Error_function"&gt;error function&lt;/a&gt;, for example is defined by \(erf(x) = \frac{2}{\sqrt{\pi}}\int_0^x e^{-t^2}dt\).
&lt;/p&gt;

&lt;p&gt;
Another &lt;a href="https://en.wikipedia.org/wiki/Leibniz_integral_rule#Example_1"&gt;example&lt;/a&gt; is:
&lt;/p&gt;

&lt;p&gt;
\(\phi(\alpha) = \int_0^1 \frac{\alpha}{x^2 + \alpha^2} dx\).
&lt;/p&gt;

&lt;p&gt;
We have reasonable ways to evaluate these functions numerically, e.g. &lt;code&gt;scipy.integrate.quad&lt;/code&gt;, or &lt;code&gt;numpy.trapz&lt;/code&gt;, but what about the derivatives of these functions? The analytical way to do this is to use the &lt;a href="https://en.wikipedia.org/wiki/Leibniz_integral_rule"&gt;Leibniz rule&lt;/a&gt;, which involves integrating a derivative and evaluating it at the limits. For some functions, this may also lead to new integrals you have to numerically evaluate. Today, we consider the role that automatic differentiation can play in this.
&lt;/p&gt;

&lt;p&gt;
The idea is simple, we define a function in Python as usual, and in the function body calculate the integral in a program. Then we use autograd to get the derivative of the function.
&lt;/p&gt;

&lt;p&gt;
In this case, we have an analytical derivative to compare the answers to:
&lt;/p&gt;

&lt;p&gt;
\(\frac{d\phi}{d\alpha} = -\frac{1}{1 + \alpha^2}\).
&lt;/p&gt;

&lt;div id="outline-container-org4ec4dc0" class="outline-2"&gt;
&lt;h2 id="org4ec4dc0"&gt;&lt;span class="section-number-2"&gt;1&lt;/span&gt; Example 1&lt;/h2&gt;
&lt;div class="outline-text-2" id="text-1"&gt;
&lt;p&gt;
For simplicity, I am going to approximate the integral with the trapezoid method in vectorized form.  Here is our program to define \(\phi(\alpha)\). I found we need a pretty dense grid on the x value so that we have a pretty accurate integral, especially near \(x=0\) where there is a singularity as &amp;alpha; goes to zero. That doesn't worry me too much, there are better integral approximations to use, including Simpson's method, adaptive methods and perhaps quadrature. If you define them so autograd can use them, they should all work. I chose the trapezoidal method because it is simple to implement here. Note, however, the autograd.numpy wrappers don't have a definition for &lt;code&gt;numpy.trapz&lt;/code&gt; to use it directly. You could add one, or just do this.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-ipython"&gt;&lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; autograd.numpy &lt;span style="color: #0000FF;"&gt;as&lt;/span&gt; np
&lt;span style="color: #0000FF;"&gt;from&lt;/span&gt; autograd &lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; grad

&lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;trapz&lt;/span&gt;(y, x):
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #BA36A5;"&gt;d&lt;/span&gt; = np.diff(x)
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; np.&lt;span style="color: #006FE0;"&gt;sum&lt;/span&gt;((y[0:-1] + y[1:]) * d / 2)


&lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;phi&lt;/span&gt;(alpha):
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #BA36A5;"&gt;x&lt;/span&gt; = np.linspace(0, 1, 1000)
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #BA36A5;"&gt;y&lt;/span&gt; = alpha / (x**2 + alpha**2)
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; trapz(y, x)


&lt;span style="color: #8D8D84;"&gt;# &lt;/span&gt;&lt;span style="color: #8D8D84; font-style: italic;"&gt;This is the derivative here!&lt;/span&gt;
&lt;span style="color: #BA36A5;"&gt;adphi&lt;/span&gt; = grad(phi, 0)
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
Now, we can plot the derivatives. I will plot both the analytical and automatic differentiated results.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-ipython"&gt;%matplotlib inline
&lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; matplotlib.pyplot &lt;span style="color: #0000FF;"&gt;as&lt;/span&gt; plt

&lt;span style="color: #8D8D84;"&gt;# &lt;/span&gt;&lt;span style="color: #8D8D84; font-style: italic;"&gt;results from AD&lt;/span&gt;
&lt;span style="color: #BA36A5;"&gt;alpha&lt;/span&gt; = np.linspace(0.01, 1)

&lt;span style="color: #8D8D84;"&gt;# &lt;/span&gt;&lt;span style="color: #8D8D84; font-style: italic;"&gt;The AD derivative function is not vectorized, so we use this list comprehension.&lt;/span&gt;
&lt;span style="color: #BA36A5;"&gt;dphidalpha&lt;/span&gt; = [adphi(a) &lt;span style="color: #0000FF;"&gt;for&lt;/span&gt; a &lt;span style="color: #0000FF;"&gt;in&lt;/span&gt; alpha]

&lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;analytical_dphi&lt;/span&gt;(alpha):
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; -1 / (1 + alpha**2)

plt.plot(alpha, analytical_dphi(alpha), label=&lt;span style="color: #008000;"&gt;'analytical'&lt;/span&gt;)
plt.plot(alpha, dphidalpha, &lt;span style="color: #008000;"&gt;'r--'&lt;/span&gt;, label=&lt;span style="color: #008000;"&gt;'AD'&lt;/span&gt;)
plt.xlabel(r&lt;span style="color: #008000;"&gt;'$\alpha$'&lt;/span&gt;)
plt.ylabel(r&lt;span style="color: #008000;"&gt;'$frac{d\phi}{d\alpha}$'&lt;/span&gt;)
plt.legend()
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
&lt;img src="/media/7fed41ae1651a1b7a7f52674a2164226-90490jWu.png"&gt; 
&lt;/p&gt;

&lt;p&gt;
Visually, these are indistinguishable from each other. We can look at the errors too, and here we see they are negligible, and probably we can attribute them to the approximation we use for the integral, and not due to automatic differentiation.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-ipython"&gt;&lt;span style="color: #BA36A5;"&gt;perr&lt;/span&gt; = (analytical_dphi(alpha) - dphidalpha) / analytical_dphi(alpha) * 100
plt.plot(alpha, perr, label=&lt;span style="color: #008000;"&gt;'analytical'&lt;/span&gt;)
plt.xlabel(r&lt;span style="color: #008000;"&gt;'$\alpha$'&lt;/span&gt;)
plt.ylabel(&lt;span style="color: #008000;"&gt;'%error'&lt;/span&gt;)
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
&lt;img src="/media/7fed41ae1651a1b7a7f52674a2164226-90490wg0.png"&gt; 
&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;


&lt;div id="outline-container-org079e9a6" class="outline-2"&gt;
&lt;h2 id="org079e9a6"&gt;&lt;span class="section-number-2"&gt;2&lt;/span&gt; Example 2&lt;/h2&gt;
&lt;div class="outline-text-2" id="text-2"&gt;
&lt;p&gt;
In &lt;a href="https://en.wikipedia.org/wiki/Leibniz_integral_rule#Example_2"&gt;example 2&lt;/a&gt; there is this function, which has variable limits:
&lt;/p&gt;

&lt;p&gt;
\(f(x) = \int_{\sin x}^{\cos x} \cosh t^2 dt\)
&lt;/p&gt;

&lt;p&gt;
What is \(f'(x)\) here? It can be derived with some effort and it is:
&lt;/p&gt;

&lt;p&gt;
\(f'(x) = -\cosh(\cos^2 x) \sin x - \cosh(\sin^2 x) \cos x\)
&lt;/p&gt;

&lt;p&gt;
This function was kind of fun to code up, I hadn't thought about how to represent variable limits, but here it is.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-ipython"&gt;&lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;f&lt;/span&gt;(x):
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #BA36A5;"&gt;a&lt;/span&gt; = np.sin(x)
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #BA36A5;"&gt;b&lt;/span&gt; = np.cos(x)
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #BA36A5;"&gt;t&lt;/span&gt; = np.linspace(a, b, 1000)
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #BA36A5;"&gt;y&lt;/span&gt; = np.cosh(t**2)
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; trapz(y, t)

&lt;span style="color: #8D8D84;"&gt;# &lt;/span&gt;&lt;span style="color: #8D8D84; font-style: italic;"&gt;Here is our derivative!&lt;/span&gt;
&lt;span style="color: #BA36A5;"&gt;dfdx&lt;/span&gt; = grad(f, 0)
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
Here is a graphical comparison of the two:
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-ipython"&gt;&lt;span style="color: #BA36A5;"&gt;x&lt;/span&gt; = np.linspace(0, 2 * np.pi)

&lt;span style="color: #BA36A5;"&gt;analytical&lt;/span&gt; = -np.cosh(np.cos(x)**2) * np.sin(x) - \
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   np.cosh(np.sin(x)**2) * np.cos(x)
&lt;span style="color: #BA36A5;"&gt;ad&lt;/span&gt; = [dfdx(_x) &lt;span style="color: #0000FF;"&gt;for&lt;/span&gt; _x &lt;span style="color: #0000FF;"&gt;in&lt;/span&gt; x]

plt.plot(x, analytical, label=&lt;span style="color: #008000;"&gt;'analytical'&lt;/span&gt;)
plt.plot(x, ad, &lt;span style="color: #008000;"&gt;'r--'&lt;/span&gt;, label=&lt;span style="color: #008000;"&gt;'AD'&lt;/span&gt;)
plt.xlabel(&lt;span style="color: #008000;"&gt;'x'&lt;/span&gt;)
plt.ylabel(&lt;span style="color: #008000;"&gt;'df/dx'&lt;/span&gt;)
plt.legend()
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
&lt;img src="/media/7fed41ae1651a1b7a7f52674a2164226-90490iqD.png"&gt; 
&lt;/p&gt;


&lt;p&gt;
These are once again indistinguishable.
&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;

&lt;div id="outline-container-org7aa394c" class="outline-2"&gt;
&lt;h2 id="org7aa394c"&gt;&lt;span class="section-number-2"&gt;3&lt;/span&gt; Summary&lt;/h2&gt;
&lt;div class="outline-text-2" id="text-3"&gt;
&lt;p&gt;
These are amazing results to me. Before trying it, I would not have thought it would be so easy to evaluate the derivative of these functions. These work of course because all the operations involved in computing the integral are differentiable and defined in autograd. It certainly opens the door to all kinds of new approaches to solving engineering problems that need the derivatives for various purposes like optimization, sensitivity analysis, etc.
&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Copyright (C) 2018 by John Kitchin. See the &lt;a href="/copying.html"&gt;License&lt;/a&gt; for information about copying.&lt;p&gt;
&lt;p&gt;&lt;a href="/org/2018/10/10/Autograd-and-the-derivative-of-an-integral-function.org"&gt;org-mode source&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Org-mode version = 9.1.13&lt;/p&gt;]]></content:encoded>
    </item>
    <item>
      <title>Compressibility variation from an implicit equation of state</title>
      <link>https://kitchingroup.cheme.cmu.edu/blog/2018/10/09/Compressibility-variation-from-an-implicit-equation-of-state</link>
      <pubDate>Tue, 09 Oct 2018 09:21:06 EDT</pubDate>
      <category><![CDATA[autograd]]></category>
      <category><![CDATA[python]]></category>
      <guid isPermaLink="false">5_ZW_tD3wR0tLozwLCnpfsH_myk=</guid>
      <description>Compressibility variation from an implicit equation of state</description>
      <content:encoded><![CDATA[


&lt;div id="table-of-contents"&gt;
&lt;h2&gt;Table of Contents&lt;/h2&gt;
&lt;div id="text-table-of-contents"&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#org56ce300"&gt;1. Summary thoughts&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;
In this &lt;a href="http://kitchingroup.cheme.cmu.edu/blog/2018/10/07/Compressibility-factor-variation-from-the-van-der-Waals-equation-by-three-different-approaches/"&gt;post&lt;/a&gt; I explored using automatic differentiation to compute how the compressibility of a gas defined by the van der Waal equation varies with the reduced pressure. In that example we had an explicit function of the pressure as a function of the volume and temperature, and we could derive a differential equation that defines the variation we were interested in.
&lt;/p&gt;

&lt;p&gt;
I thought we should be able to derive the differential equation more directly, still using automatic differentiation and we explore that idea here. The general strategy to compute the compressibility as a function of pressure is to integrate \(dV / dP_r\) over a range of \(P_r\) to get the molar volume as a function of \(P_r\), and then to directly compute the compressibility from \(Z = PV/(RT)\).
&lt;/p&gt;

&lt;p&gt;
To use this approach we need to get \(dV / dP_r\) from the van der Waal equation. Previously, we derived this in a round about way from the explicit form of the van der Waal equation. Here, we follow the work in this &lt;a href="http://kitchingroup.cheme.cmu.edu/blog/2018/10/08/Getting-derivatives-from-implicit-functions-with-autograd/"&gt;post&lt;/a&gt; to get the derivative from the implicit form of the van der Waal equation:
&lt;/p&gt;

&lt;p&gt;
\(f(V, P_r, T_r) = \frac{R Tr * Tc}{V - b} - \frac{a}{V^2} - P_r Pc = 0\)
&lt;/p&gt;

&lt;p&gt;
Based on the work in this &lt;a href="http://kitchingroup.cheme.cmu.edu/blog/2018/10/08/Getting-derivatives-from-implicit-functions-with-autograd/"&gt;post&lt;/a&gt;, we can get
&lt;/p&gt;

&lt;p&gt;
\(dV/dP_r = (-df/dP_r) / (df/dV)\)
&lt;/p&gt;

&lt;p&gt;
and the two derivatives on the right can be found easily by automatic differentiation. First, we express the van der Waal equation in implicit form, with the variables as \(V, P_r, T_r\). Only two of those variables are independent; if you define two of them you can compute the third one using a tool like fsolve.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-ipython"&gt;&lt;span style="color: #BA36A5;"&gt;R&lt;/span&gt; = 0.08206
&lt;span style="color: #BA36A5;"&gt;Pc&lt;/span&gt; = 72.9
&lt;span style="color: #BA36A5;"&gt;Tc&lt;/span&gt; = 304.2

&lt;span style="color: #BA36A5;"&gt;a&lt;/span&gt; = 27 * R**2 * Tc**2 / (Pc * 64)
&lt;span style="color: #BA36A5;"&gt;b&lt;/span&gt; = R * Tc / (8 * Pc)

&lt;span style="color: #BA36A5;"&gt;Tr&lt;/span&gt; = 1.1  &lt;span style="color: #8D8D84;"&gt;# &lt;/span&gt;&lt;span style="color: #8D8D84; font-style: italic;"&gt;Constant for this example&lt;/span&gt;

&lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;f&lt;/span&gt;(V, Pr, Tr):
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; R * Tr * Tc / (V - b) - a / V**2 - Pr * Pc
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
Now, if we want to know how does the volume vary with \(P_r\), we need to derive the derivative \(dV/dP_r\), and then integrate it. Here we use autograd to define the derivatives, and then we define a function that uses them. Note the arguments in the function dVdPr are in an order that anticipates we want to integrate it in solve_ivp, to get a function \(V(P_r)\).
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-ipython"&gt;&lt;span style="color: #0000FF;"&gt;from&lt;/span&gt; autograd &lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; grad

&lt;span style="color: #BA36A5;"&gt;dfdPr&lt;/span&gt; = grad(f, 1)  &lt;span style="color: #8D8D84;"&gt;# &lt;/span&gt;&lt;span style="color: #8D8D84; font-style: italic;"&gt;derivative of f with respect to arg at index=1: Pr&lt;/span&gt;
&lt;span style="color: #BA36A5;"&gt;dfdV&lt;/span&gt; = grad(f, 0)  &lt;span style="color: #8D8D84;"&gt;# &lt;/span&gt;&lt;span style="color: #8D8D84; font-style: italic;"&gt;derivative of f with respect to arg at index=0: V&lt;/span&gt;

&lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;dVdPr&lt;/span&gt;(Pr, V):
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; -dfdPr(V, Pr, Tr) / dfdV(V, Pr, Tr)  &lt;span style="color: #8D8D84;"&gt;# &lt;/span&gt;&lt;span style="color: #8D8D84; font-style: italic;"&gt;Tr is a constant in here&lt;/span&gt;
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
Now, we need an initial condition to start the integration from. We want the volume at \(P_r=0.1\). We have to use fsolve for this, or some other method that tells you want is the volume at \(P_r=0.1\). We can pass the values of \(P_r\) and \(T_R\) as arguments to our implicit function. Since \(V\) is the first argument, we can directly solve our implicit function. Otherwise you would have to define a helper objective function to use with fsolve.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-ipython"&gt;&lt;span style="color: #0000FF;"&gt;from&lt;/span&gt; scipy.optimize &lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; fsolve

V0, = fsolve(f, 3.5, args=(0.1, 1.1))
V0
&lt;/pre&gt;
&lt;/div&gt;

&lt;pre class="example"&gt;
3.6764763125625435

&lt;/pre&gt;

&lt;p&gt;
Finally, we are ready to integrate the ODE, and plot the solution.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-ipython"&gt;&lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; numpy &lt;span style="color: #0000FF;"&gt;as&lt;/span&gt; np
&lt;span style="color: #0000FF;"&gt;from&lt;/span&gt; scipy.integrate &lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; solve_ivp

&lt;span style="color: #BA36A5;"&gt;Pr_span&lt;/span&gt; = (0.1, 10)
&lt;span style="color: #BA36A5;"&gt;Pr_eval&lt;/span&gt;, &lt;span style="color: #BA36A5;"&gt;h&lt;/span&gt; = np.linspace(*Pr_span, retstep=&lt;span style="color: #D0372D;"&gt;True&lt;/span&gt;)

&lt;span style="color: #BA36A5;"&gt;sol&lt;/span&gt; = solve_ivp(dVdPr, Pr_span, (V0,), max_step=h)
&lt;span style="color: #0000FF;"&gt;print&lt;/span&gt;(sol.message)

%matplotlib inline
&lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; matplotlib.pyplot &lt;span style="color: #0000FF;"&gt;as&lt;/span&gt; plt

&lt;span style="color: #BA36A5;"&gt;Pr&lt;/span&gt; = sol.t  &lt;span style="color: #8D8D84;"&gt;# &lt;/span&gt;&lt;span style="color: #8D8D84; font-style: italic;"&gt;the P_r steps used in the solution&lt;/span&gt;
&lt;span style="color: #BA36A5;"&gt;V&lt;/span&gt; = sol.y[0]  &lt;span style="color: #8D8D84;"&gt;# &lt;/span&gt;&lt;span style="color: #8D8D84; font-style: italic;"&gt;V(P_r) from the solution&lt;/span&gt;

&lt;span style="color: #BA36A5;"&gt;Z&lt;/span&gt; = Pr * Pc * V / (R * Tr * Tc)  &lt;span style="color: #8D8D84;"&gt;# &lt;/span&gt;&lt;span style="color: #8D8D84; font-style: italic;"&gt;Compressibility Z(P_r)&lt;/span&gt;

plt.plot(Pr, Z)
plt.xlabel(&lt;span style="color: #008000;"&gt;'$P_r$'&lt;/span&gt;)
plt.ylabel(&lt;span style="color: #008000;"&gt;'Z'&lt;/span&gt;)
plt.xlim([0, 10])
plt.ylim([0, 2])
&lt;/pre&gt;
&lt;/div&gt;

&lt;pre class="example"&gt;
The solver successfully reached the end of the integration interval.


&lt;/pre&gt;

&lt;pre class="example"&gt;
(0, 2)

&lt;/pre&gt;



&lt;p&gt;
&lt;img src="/media/0a06a9507e7d4f809f61d49b8988e2d1-90490gTZ.png"&gt; 
&lt;/p&gt;

&lt;p&gt;
That is the same result as we got before.
&lt;/p&gt;

&lt;div id="outline-container-org56ce300" class="outline-2"&gt;
&lt;h2 id="org56ce300"&gt;&lt;span class="section-number-2"&gt;1&lt;/span&gt; Summary thoughts&lt;/h2&gt;
&lt;div class="outline-text-2" id="text-1"&gt;
&lt;p&gt;
This method also worked successfully to solve this problem. In most ways, this method has less algebraic manipulations required to get to the solution. In &lt;a href="http://kitchingroup.cheme.cmu.edu/blog/2018/10/07/Compressibility-factor-variation-from-the-van-der-Waals-equation-by-three-different-approaches/#orge63b16e"&gt;method 3&lt;/a&gt;, we had to do some calculus that relied on a particular explicit form of the van der Waal equation. While those manipulations were not particularly difficulty, the leave opportunities for mistakes, and they will be more difficult for an implicit equation of state (e.g. if there was a \(P\) on the right hand side).
&lt;/p&gt;

&lt;p&gt;
This approach also required some manipulation, but it is a standard one and that is how do you get a derivative from an implicit function. After that, it is straightforward to define the desired derivative as a function and then integrate it to get the solution. So, we still don't get a free pass on calculus, but we do reduce the number of manipulations required to get to the solution. I consider that a plus.
&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Copyright (C) 2018 by John Kitchin. See the &lt;a href="/copying.html"&gt;License&lt;/a&gt; for information about copying.&lt;p&gt;
&lt;p&gt;&lt;a href="/org/2018/10/09/Compressibility-variation-from-an-implicit-equation-of-state.org"&gt;org-mode source&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Org-mode version = 9.1.13&lt;/p&gt;]]></content:encoded>
    </item>
    <item>
      <title>Getting derivatives from implicit functions with autograd</title>
      <link>https://kitchingroup.cheme.cmu.edu/blog/2018/10/08/Getting-derivatives-from-implicit-functions-with-autograd</link>
      <pubDate>Mon, 08 Oct 2018 19:53:21 EDT</pubDate>
      <category><![CDATA[autograd]]></category>
      <category><![CDATA[python]]></category>
      <guid isPermaLink="false">vzdJUvksnYiFe3TCZNWRgZOmbqc=</guid>
      <description>Getting derivatives from implicit functions with autograd</description>
      <content:encoded><![CDATA[


&lt;div id="table-of-contents"&gt;
&lt;h2&gt;Table of Contents&lt;/h2&gt;
&lt;div id="text-table-of-contents"&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#org1e7bbe1"&gt;1. Example 1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#org30374e2"&gt;2. Example 2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#orgc49e13d"&gt;3. Example 3&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#org452c660"&gt;4. Example 4&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#org67383a5"&gt;5. Example 5&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#org0081ece"&gt;6. Example 6&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#orgedd1f4b"&gt;7. Example 7&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#orgb2b9394"&gt;8. Conclusions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;
If we have an implicit function: \(f(x, y(x)) = 0\), but we want to compute the derivative \(dy/dx\) we can use the chain rule to derive:
&lt;/p&gt;

&lt;p&gt;
\(df/dx + df/dy dy/dx = 0\)
&lt;/p&gt;

&lt;p&gt;
We can then solve for \(dy/dx\):
&lt;/p&gt;

&lt;p&gt;
\(dy/dx = -df/dx / df/dy\) to get the desired derivative. The interesting point of this blog post is that we can get the two derivatives on the right hand side of this equation using automatic differentiation of the function \(f(x, y)\)! There are a few examples of analytical approaches to derivatives from implicit functions &lt;a href="https://www.math.ucdavis.edu/~kouba/CalcOneDIRECTORY/implicitdiffdirectory/ImplicitDiff.html"&gt;here&lt;/a&gt;, and I wanted to explore them with autograd in this post.
&lt;/p&gt;

&lt;p&gt;
In the following examples, we will assume that \(y\) is a function of \(x\) and that \(x\) is independent. We will consider a series of implicit equations, compute \(dy/dx\) using autograd from the formula above, and compare them to the analytical results in the web page referenced above.
&lt;/p&gt;

&lt;p&gt;
The \(dy/dx\) functions generally depend on both \(x\), and \(y\). Technically, these are the derivatives along the curve \(y(x)\), but since we can evaluate them at any points, we will use some random points for \(x\) and \(y\) to test for equality between the analytical derivatives and the autograd derivatives. This isn't a rigorous proof of equality, but it is the only thing that makes sense to do for now. It is assumed that if these points are ok, all the others are too. That might be a broad claim, since we only sample \(x\) and \(y\) from 0 to 1 here but the approach is general. Here are the imports and the random test points for all the examples that follow.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-ipython"&gt;&lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; autograd.numpy &lt;span style="color: #0000FF;"&gt;as&lt;/span&gt; np
&lt;span style="color: #0000FF;"&gt;from&lt;/span&gt; autograd &lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; grad

&lt;span style="color: #BA36A5;"&gt;xr&lt;/span&gt; = np.random.random(50)
&lt;span style="color: #BA36A5;"&gt;yr&lt;/span&gt; = np.random.random(50)
&lt;/pre&gt;
&lt;/div&gt;


&lt;div id="outline-container-org1e7bbe1" class="outline-2"&gt;
&lt;h2 id="org1e7bbe1"&gt;&lt;span class="section-number-2"&gt;1&lt;/span&gt; Example 1&lt;/h2&gt;
&lt;div class="outline-text-2" id="text-1"&gt;
&lt;p&gt;
\(x^3 + y^3 = 4\)
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-ipython"&gt;&lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;f1&lt;/span&gt;(x, y):
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; x**3 + y**3 - 4

&lt;span style="color: #BA36A5;"&gt;D1x&lt;/span&gt; = grad(f1, 0)
&lt;span style="color: #BA36A5;"&gt;D1y&lt;/span&gt; = grad(f1, 1)

&lt;span style="color: #BA36A5;"&gt;dydx_1&lt;/span&gt; = &lt;span style="color: #0000FF;"&gt;lambda&lt;/span&gt; x, y: -D1x(x, y) / D1y(x, y)
&lt;span style="color: #BA36A5;"&gt;dydx_1a&lt;/span&gt; = &lt;span style="color: #0000FF;"&gt;lambda&lt;/span&gt; x, y: -x**2 / y**2

np.allclose(dydx_1a(xr, yr),
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;[dydx_1(_xr, _yr) &lt;span style="color: #0000FF;"&gt;for&lt;/span&gt; _xr, _yr &lt;span style="color: #0000FF;"&gt;in&lt;/span&gt; &lt;span style="color: #006FE0;"&gt;zip&lt;/span&gt;(xr, yr)])
&lt;/pre&gt;
&lt;/div&gt;

&lt;pre class="example"&gt;
True

&lt;/pre&gt;

&lt;p&gt;
The output of True means the autograd results and the analytical results are "all close", i.e. within a tolerance the results are the same. The required derivatives of this are not that difficult to derive, but it is nice to see a simple example that "just works". A subtle point of the dydx function is that it is not &lt;i&gt;vectorized&lt;/i&gt; which is why I used a list comprehension to evaluate all the points. It might be possible to make a pseudo-vectorized version with the np.vectorize decorator, but that is not of interest here.
&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;

&lt;div id="outline-container-org30374e2" class="outline-2"&gt;
&lt;h2 id="org30374e2"&gt;&lt;span class="section-number-2"&gt;2&lt;/span&gt; Example 2&lt;/h2&gt;
&lt;div class="outline-text-2" id="text-2"&gt;
&lt;p&gt;
\((x - y)^2 = x + y - 1\)
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-ipython"&gt;&lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;f2&lt;/span&gt;(x, y):
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; (x - y)**2 - x - y + 1

&lt;span style="color: #BA36A5;"&gt;D2x&lt;/span&gt; = grad(f2, 0)
&lt;span style="color: #BA36A5;"&gt;D2y&lt;/span&gt; = grad(f2, 1)

&lt;span style="color: #BA36A5;"&gt;dydx_2&lt;/span&gt; = &lt;span style="color: #0000FF;"&gt;lambda&lt;/span&gt; x, y: -D2x(x, y) / D2y(x, y)
&lt;span style="color: #BA36A5;"&gt;dydx2_a&lt;/span&gt; = &lt;span style="color: #0000FF;"&gt;lambda&lt;/span&gt; x, y: (2 * y - 2 * x + 1) / (2 * y - 2 * x - 1)

np.allclose(dydx2_a(xr, yr),
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   [dydx_2(_xr, _yr) &lt;span style="color: #0000FF;"&gt;for&lt;/span&gt; _xr, _yr &lt;span style="color: #0000FF;"&gt;in&lt;/span&gt; &lt;span style="color: #006FE0;"&gt;zip&lt;/span&gt;(xr, yr)])
&lt;/pre&gt;
&lt;/div&gt;

&lt;pre class="example"&gt;
True

&lt;/pre&gt;

&lt;p&gt;
This also works.
&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;

&lt;div id="outline-container-orgc49e13d" class="outline-2"&gt;
&lt;h2 id="orgc49e13d"&gt;&lt;span class="section-number-2"&gt;3&lt;/span&gt; Example 3&lt;/h2&gt;
&lt;div class="outline-text-2" id="text-3"&gt;
&lt;p&gt;
\(y = sin(3x + 4y)\)
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-ipython"&gt;&lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;f3&lt;/span&gt;(x, y):
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; y - np.sin(3 * x + 4 * y)


&lt;span style="color: #BA36A5;"&gt;D3x&lt;/span&gt; = grad(f3, 0)
&lt;span style="color: #BA36A5;"&gt;D3y&lt;/span&gt; = grad(f3, 1)

&lt;span style="color: #BA36A5;"&gt;dydx_3&lt;/span&gt; = &lt;span style="color: #0000FF;"&gt;lambda&lt;/span&gt; x, y: -D3x(x, y) / D3y(x, y)
&lt;span style="color: #BA36A5;"&gt;dydx3_a&lt;/span&gt; = &lt;span style="color: #0000FF;"&gt;lambda&lt;/span&gt; x, y: (3 * np.cos(3 * x + 4 * y)) / (1 - 4 * np.cos(3 * x + 4 * y))

np.allclose(dydx3_a(xr, yr),
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   [dydx_3(_xr, _yr) &lt;span style="color: #0000FF;"&gt;for&lt;/span&gt; _xr, _yr &lt;span style="color: #0000FF;"&gt;in&lt;/span&gt; &lt;span style="color: #006FE0;"&gt;zip&lt;/span&gt;(xr, yr)])
&lt;/pre&gt;
&lt;/div&gt;

&lt;pre class="example"&gt;
True

&lt;/pre&gt;

&lt;p&gt;
Check.
&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;

&lt;div id="outline-container-org452c660" class="outline-2"&gt;
&lt;h2 id="org452c660"&gt;&lt;span class="section-number-2"&gt;4&lt;/span&gt; Example 4&lt;/h2&gt;
&lt;div class="outline-text-2" id="text-4"&gt;
&lt;p&gt;
\(y = x^2 y^3 + x^3 y^2\)
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-ipython"&gt;&lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;f4&lt;/span&gt;(x, y):
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; y - x**2 * y**3 - x**3 * y**2


&lt;span style="color: #BA36A5;"&gt;D4x&lt;/span&gt; = grad(f4, 0)
&lt;span style="color: #BA36A5;"&gt;D4y&lt;/span&gt; = grad(f4, 1)

&lt;span style="color: #BA36A5;"&gt;dydx_4&lt;/span&gt; = &lt;span style="color: #0000FF;"&gt;lambda&lt;/span&gt; x, y: -D4x(x, y) / D4y(x, y)
&lt;span style="color: #BA36A5;"&gt;dydx4_a&lt;/span&gt; = &lt;span style="color: #0000FF;"&gt;lambda&lt;/span&gt; x, y: (2 * x * y**3 + 3 * x**2 * y**2) / (1 - 3 * x**2 * y**2 - 2 * x**3 * y)

np.allclose(dydx4_a(xr, yr),
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   [dydx_4(_xr, _yr) &lt;span style="color: #0000FF;"&gt;for&lt;/span&gt; _xr, _yr &lt;span style="color: #0000FF;"&gt;in&lt;/span&gt; &lt;span style="color: #006FE0;"&gt;zip&lt;/span&gt;(xr, yr)])
&lt;/pre&gt;
&lt;/div&gt;

&lt;pre class="example"&gt;
True

&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;

&lt;div id="outline-container-org67383a5" class="outline-2"&gt;
&lt;h2 id="org67383a5"&gt;&lt;span class="section-number-2"&gt;5&lt;/span&gt; Example 5&lt;/h2&gt;
&lt;div class="outline-text-2" id="text-5"&gt;
&lt;p&gt;
\(e^{xy} = e^{4x} - e^{5y}\)
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-ipython"&gt;&lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;f5&lt;/span&gt;(x, y):
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; np.exp(4 * x) - np.exp(5 * y) - np.exp(x * y)

&lt;span style="color: #BA36A5;"&gt;D5x&lt;/span&gt; = grad(f5, 0)
&lt;span style="color: #BA36A5;"&gt;D5y&lt;/span&gt; = grad(f5, 1)

&lt;span style="color: #BA36A5;"&gt;dydx_5&lt;/span&gt; = &lt;span style="color: #0000FF;"&gt;lambda&lt;/span&gt; x, y: -D5x(x, y) / D5y(x, y)
&lt;span style="color: #BA36A5;"&gt;dydx5_a&lt;/span&gt; = &lt;span style="color: #0000FF;"&gt;lambda&lt;/span&gt; x, y: (4 * np.exp(4 * x) - y * np.exp(x * y)) / (x * np.exp(x * y) + 5 * np.exp(5 * y))

np.allclose(dydx5_a(xr, yr),
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   [dydx_5(_xr, _yr) &lt;span style="color: #0000FF;"&gt;for&lt;/span&gt; _xr, _yr &lt;span style="color: #0000FF;"&gt;in&lt;/span&gt; &lt;span style="color: #006FE0;"&gt;zip&lt;/span&gt;(xr, yr)])
&lt;/pre&gt;
&lt;/div&gt;

&lt;pre class="example"&gt;
True

&lt;/pre&gt;

&lt;p&gt;
Also check.
&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;

&lt;div id="outline-container-org0081ece" class="outline-2"&gt;
&lt;h2 id="org0081ece"&gt;&lt;span class="section-number-2"&gt;6&lt;/span&gt; Example 6&lt;/h2&gt;
&lt;div class="outline-text-2" id="text-6"&gt;
&lt;p&gt;
\(\cos^2 x + cos^2 y = cos(2x + 2y)\)
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-ipython"&gt;&lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;f6&lt;/span&gt;(x, y):
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; np.cos(x)**2 + np.cos(y)**2 - np.cos(2 * x + 2 * y)

&lt;span style="color: #BA36A5;"&gt;D6x&lt;/span&gt; = grad(f6, 0)
&lt;span style="color: #BA36A5;"&gt;D6y&lt;/span&gt; = grad(f6, 1)

&lt;span style="color: #BA36A5;"&gt;dydx_6&lt;/span&gt; = &lt;span style="color: #0000FF;"&gt;lambda&lt;/span&gt; x, y: -D6x(x, y) / D6y(x, y)
&lt;span style="color: #BA36A5;"&gt;dydx6_a&lt;/span&gt; = &lt;span style="color: #0000FF;"&gt;lambda&lt;/span&gt; x, y: (np.cos(x) * np.sin(x) - np.sin(2 * x + 2 * y)) / (np.sin(2 * x + 2 * y) - np.cos(y) * np.sin(y))

np.allclose(dydx6_a(xr, yr),
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   [dydx_6(_xr, _yr) &lt;span style="color: #0000FF;"&gt;for&lt;/span&gt; _xr, _yr &lt;span style="color: #0000FF;"&gt;in&lt;/span&gt; &lt;span style="color: #006FE0;"&gt;zip&lt;/span&gt;(xr, yr)])
&lt;/pre&gt;
&lt;/div&gt;

&lt;pre class="example"&gt;
True

&lt;/pre&gt;

&lt;p&gt;
Check.
&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;

&lt;div id="outline-container-orgedd1f4b" class="outline-2"&gt;
&lt;h2 id="orgedd1f4b"&gt;&lt;span class="section-number-2"&gt;7&lt;/span&gt; Example 7&lt;/h2&gt;
&lt;div class="outline-text-2" id="text-7"&gt;
&lt;p&gt;
\(x = 3 + \sqrt{x^2 + y^2}\)
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-ipython"&gt;&lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;f7&lt;/span&gt;(x, y):
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; 3 + np.sqrt(x**2 + y**2) - x

&lt;span style="color: #BA36A5;"&gt;D7x&lt;/span&gt; = grad(f7, 0)
&lt;span style="color: #BA36A5;"&gt;D7y&lt;/span&gt; = grad(f7, 1)

&lt;span style="color: #BA36A5;"&gt;dydx_7&lt;/span&gt; = &lt;span style="color: #0000FF;"&gt;lambda&lt;/span&gt; x, y: -D7x(x, y) / D7y(x, y)
&lt;span style="color: #BA36A5;"&gt;dydx7_a&lt;/span&gt; = &lt;span style="color: #0000FF;"&gt;lambda&lt;/span&gt; x, y: (np.sqrt(x**2 + y**2) - x) / y

np.allclose(dydx7_a(xr, yr),
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   [dydx_7(_xr, _yr) &lt;span style="color: #0000FF;"&gt;for&lt;/span&gt; _xr, _yr &lt;span style="color: #0000FF;"&gt;in&lt;/span&gt; &lt;span style="color: #006FE0;"&gt;zip&lt;/span&gt;(xr, yr)])
&lt;/pre&gt;
&lt;/div&gt;

&lt;pre class="example"&gt;
True

&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;


&lt;div id="outline-container-orgb2b9394" class="outline-2"&gt;
&lt;h2 id="orgb2b9394"&gt;&lt;span class="section-number-2"&gt;8&lt;/span&gt; Conclusions&lt;/h2&gt;
&lt;div class="outline-text-2" id="text-8"&gt;
&lt;p&gt;
There are a handful of other examples at the website referenced in the beginning, but I am stopping here. After seven examples of quantitative agreement between the easy to derive autograd derivatives, and the easy to moderately difficult analytical derivatives, it seems like it is autograd for the win here. This technique has some important implications for engineering calculations that I will explore in a future post. Until then, this is yet another interesting thing we can do with automatic differentiation!
&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Copyright (C) 2018 by John Kitchin. See the &lt;a href="/copying.html"&gt;License&lt;/a&gt; for information about copying.&lt;p&gt;
&lt;p&gt;&lt;a href="/org/2018/10/08/Getting-derivatives-from-implicit-functions-with-autograd.org"&gt;org-mode source&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Org-mode version = 9.1.13&lt;/p&gt;]]></content:encoded>
    </item>
    </channel>
</rss>