<?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>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>A zoo of differentiable surrogates that know their uncertainty</title><link>https://kitchingroup.cheme.cmu.edu/posts/differentiable-surrogate-zoo/</link><pubDate>Tue, 20 Oct 2026 00:00:00 +0000</pubDate><category>machine learning</category><category>uncertainty quantification</category><category>jax</category><category>surrogates</category><guid isPermaLink="true">https://kitchingroup.cheme.cmu.edu/posts/differentiable-surrogate-zoo/</guid><description>A zoo of differentiable surrogates that know their uncertainty</description><content:encoded><![CDATA[ 

Skip to main content




<iframe src="https://kitchingroup.cheme.cmu.edu/posts/differentiable-surrogate-zoo/zoo.html" title="A zoo of differentiable surrogates" style="width:100%; aspect-ratio:1240/1044; border:0;" loading="lazy"></iframe>
<p><a href="https://kitchingroup.cheme.cmu.edu/posts/differentiable-surrogate-zoo/zoo.html">Open the graphic full-size</a>.</p>
<p>In the <a href="https://kitchingroup.cheme.cmu.edu/posts/differentiable-models/">last post</a> I argued that one differentiable model can be reused for simulation, estimation, design of experiments, optimization and control. Often that model is not built solely from first principles. It may also contain a surrogate fitted to data or to an expensive simulation. For a surrogate to fit that picture it has to do two things: give derivatives, so the tasks can work through it, and say how sure it is, so uncertainty quantification, experiment design and active learning have something to use. Over the past year we have built quite a few of these across five packages: <a href="https://github.com/jkitchin/jaxsr">jaxsr</a>, <a href="https://github.com/jkitchin/jax-ibart">jax-ibart</a>, <a href="https://github.com/jkitchin/jax-ldt">jax-ldt</a>, <a href="https://github.com/jkitchin/jax-difftree">jax-difftree</a> and <a href="https://github.com/jkitchin/pycse/tree/master/src/pycse/sklearn">pycse.sklearn</a>. Nearly all of them follow the scikit-learn API: you call <code>fit</code>, then <code>predict(X, return_std=True)</code> for a prediction with a standard deviation. This post is the guide to the map above.</p>
<section id="how-to-choose" class="level1">
<h1>How to choose</h1>
<p>There are many things you should consider in choosing a surrogate:</p>
<ol type="1">
<li>Accuracy: can it fit the data well enough for your task?</li>
<li>Mathematical / optimization properties: is it convex, monotonic, periodic, piecewise-linear, etc.?</li>
<li>Interpretability: can you read the model, check it against physics, and publish it?</li>
<li>Uncertainty: does it give honest intervals, and do they cover the truth?</li>
</ol>
<p>A lot depends on what you do with the model after you make it. Here are some guidelines for picking a surrogate, and the packages that implement them.</p>
<table class="caption-top table">
<thead>
<tr class="header">
<th>If you need…</th>
<th>Reach for</th>
<th>Why</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>An equation you can read, check against physics, and publish</td>
<td>Symbolic regression (jaxsr), iBART, SISSO, regress / nlinfit</td>
<td>The model is a short algebraic expression, not a black box.</td>
</tr>
<tr class="even">
<td>Sparse models: few terms, few inputs</td>
<td>jaxsr, iBART, SISSO</td>
<td>All three select a small number of terms from a large library.</td>
</tr>
<tr class="odd">
<td>Exact representation in a mixed-integer program</td>
<td>Linear and hyperplane trees, hardened differentiable trees</td>
<td>Piecewise-linear models embed in a MILP with no approximation, so a solver can certify a global optimum of the surrogate.</td>
</tr>
<tr class="even">
<td>Convexity, so a local optimum is the global one</td>
<td>ICNN</td>
<td>Convex in x by construction.</td>
</tr>
<tr class="odd">
<td>Monotonicity in some inputs</td>
<td>Monotonic NN (guaranteed), jaxsr constraints (enforced at chosen points)</td>
<td>Stops a surrogate from inventing physics you know is wrong.</td>
</tr>
<tr class="even">
<td>Periodicity: angles, time of day, seasons</td>
<td>Periodic NN</td>
<td>Exactly periodic in the inputs you choose.</td>
</tr>
<tr class="odd">
<td>Uncertainty for a network you already trust</td>
<td>LLPR</td>
<td>Computed from the trained last layer, with no retraining.</td>
</tr>
<tr class="even">
<td>Noise that changes across the input space</td>
<td>Differentiable trees, DPOSE, NN + GMM</td>
<td>The variance is learned as a function of x.</td>
</tr>
<tr class="odd">
<td>Several modes, or the whole distribution of y</td>
<td>Normalizing flow, NN + GMM, differentiable trees</td>
<td>A mean and a σ are not enough when p(y</td>
</tr>
<tr class="even">
<td>To run the model backwards: which x gives this y?</td>
<td>Normalizing flow, differentiable trees</td>
<td>Conditional density and gradient-based inverse design.</td>
</tr>
<tr class="odd">
<td>Very little data</td>
<td>regress / nlinfit, response surfaces, symbolic regression</td>
<td>A few parameters can be estimated well from a few points, with honest intervals.</td>
</tr>
<tr class="even">
<td>Classification</td>
<td>Symbolic classifier, differentiable tree classifier, ZENN</td>
<td>Readable boundary, uncertainty split into its parts, or a thermodynamic model.</td>
</tr>
</tbody>
</table>
<p>A few rules of thumb sit behind the table.</p>
<ul>
<li><strong>Structure you can guarantee beats structure you hope the data teach.</strong> If you know a response is monotonic or convex, use a model that cannot be otherwise. Constraints enforced only at training points can still be violated between them.</li>
<li><strong>If the surrogate goes into an optimizer, pick it for the optimizer.</strong> For a MILP use piecewise-linear trees. For a convex problem use an ICNN. For a nonlinear program, any smooth model with good derivatives will do, and a short symbolic expression is easiest on the solver.</li>
<li><strong>Interpretability and flexibility trade off.</strong> Symbolic models and linear trees can be read. Networks usually fit more but explain less. KANs sit in between, because you can plot each learned edge function.</li>
<li><strong>Check the uncertainty, not just the fit.</strong> Each model gets its σ a different way, and they fail differently. The UQ comparison is a good place to start.</li>
</ul>
</section>
<section id="the-symbolic-models" class="level1">
<h1>The symbolic models</h1>
<section id="symbolic-regression" class="level2">
<h2 class="anchored" data-anchor-id="symbolic-regression">Symbolic regression</h2>
<p><code>SymbolicRegressor</code> in <a href="https://kitchingroup.cheme.cmu.edu/jaxsr/">jaxsr</a> builds a library of candidate basis functions of your inputs (powers, logs, exponentials, products, and any you add), then selects a sparse subset and fits the coefficients. The result is an equation of the form y = Σ βᵢ φᵢ(x) that you can read, differentiate, and write straight into an optimization model. Because the final fit is linear in the coefficients, the uncertainty is well grounded. You get prediction intervals from least squares, and also Pareto-front ensembles, conformal intervals and residual bootstraps when you trust the model form less. You can add domain knowledge as constraints: bounds, signs on coefficients, monotonicity, convexity. These are enforced at the training points and, if you ask, on a grid, which is not the same as a guarantee everywhere. <code>MultiOutputSymbolicRegressor</code> fits several targets at once.</p>
<p>Examples: <a href="https://github.com/jkitchin/jaxsr/blob/main/docs/examples/basic_usage.ipynb">basic usage</a>, <a href="https://github.com/jkitchin/jaxsr/blob/main/docs/examples/uncertainty_quantification.ipynb">uncertainty quantification</a>, <a href="https://github.com/jkitchin/jaxsr/blob/main/docs/examples/constraint_enforcement.ipynb">constraint enforcement</a>, <a href="https://github.com/jkitchin/jaxsr/blob/main/docs/examples/multi_output.ipynb">multi-output</a>, <a href="https://github.com/jkitchin/jaxsr/blob/main/docs/examples/chemical_kinetics.ipynb">chemical kinetics</a>, <a href="https://github.com/jkitchin/jaxsr/blob/main/docs/examples/ode_discovery.ipynb">ODE discovery</a>, and the <a href="https://github.com/jkitchin/jaxsr/blob/main/docs/examples/comprehensive_tutorial.ipynb">comprehensive tutorial</a>.</p>
</section>
<section id="additive-symbolic-models" class="level2">
<h2 class="anchored" data-anchor-id="additive-symbolic-models">Additive symbolic models</h2>
<p>Sometimes the best description is a sum of many small, simple terms rather than one clever expression. The stagewise, backfitting and recursive regressors in jaxsr build that sum one piece at a time. They do not report uncertainty yet. Example: <a href="https://github.com/jkitchin/jaxsr/blob/main/docs/examples/additive_symbolic_regression.ipynb">additive symbolic regression</a>.</p>
</section>
<section id="superposition" class="level2">
<h2 class="anchored" data-anchor-id="superposition">Superposition</h2>
<p>Many experiments produce a family of curves, one per temperature or concentration, and the claim is that shifting the axes collapses them onto one master curve. That is time-temperature superposition in rheology, Larson-Miller curves in creep, isoconversional methods in kinetics. Usually you shift the curves by hand and then assume a form for the shift, such as Arrhenius or WLF. <code>SuperpositionRegressor</code> learns the shift law symbolically from a library of candidates, which lets you extrapolate to conditions you never measured. It gives intervals on the shift-law parameters, such as an activation energy; the predictions are point values. Example: <a href="https://github.com/jkitchin/jaxsr/blob/main/docs/examples/superposition_master_curves.ipynb">superposition master curves</a>.</p>
</section>
<section id="response-surfaces" class="level2">
<h2 class="anchored" data-anchor-id="response-surfaces">Response surfaces</h2>
<p><code>ResponseSurface</code> is classical response-surface methodology on top of the symbolic regressor: design generators, coded variables, ANOVA, and canonical analysis of the stationary point. It also lets the model leave the quadratic when the data clearly want something else. If you are running a designed experiment with a handful of factors, start here. Examples: <a href="https://github.com/jkitchin/jaxsr/blob/main/docs/examples/rsm_formulation.ipynb">RSM formulation</a>, <a href="https://github.com/jkitchin/jaxsr/blob/main/docs/examples/doe_study_workflow.ipynb">DOE study workflow</a>.</p>
</section>
<section id="model-averaging" class="level2">
<h2 class="anchored" data-anchor-id="model-averaging">Model averaging</h2>
<p>When several equations fit about equally well, picking one hides real uncertainty about the model form. <code>BayesianModelAverage</code> keeps them all, weighted by how well the data support each one, so the disagreement between plausible models shows up in the prediction. Examples: <a href="https://github.com/jkitchin/jaxsr/blob/main/docs/examples/uncertainty_quantification.ipynb">uncertainty quantification</a>, <a href="https://github.com/jkitchin/jaxsr/blob/main/docs/examples/model_comparison_isotherms.ipynb">comparing isotherm models</a>.</p>
</section>
<section id="symbolic-classifier" class="level2">
<h2 class="anchored" data-anchor-id="symbolic-classifier">Symbolic classifier</h2>
<p><code>SymbolicClassifier</code> applies the same sparse selection to a logistic model, fitted by iteratively reweighted least squares, with one-vs-rest for more than two classes. You get a decision boundary you can write down. Example: <a href="https://github.com/jkitchin/jaxsr/blob/main/docs/examples/symbolic_classification.ipynb">symbolic classification</a>.</p>
</section>
<section id="ibart" class="level2">
<h2 class="anchored" data-anchor-id="ibart">iBART</h2>
<p><a href="https://kitchingroup.cheme.cmu.edu/jax-ibart/">jax-ibart</a> is a JAX version of iBART, iterative Bayesian additive regression trees, from Liu et al.&nbsp;(J. Chem. Phys. 156, 164105, 2022). It goes after the same thing as SISSO, a compact descriptor built from the inputs, but it never writes down the whole candidate space. Each round applies the operators (products, ratios, powers, logs, exponentials and so on) to the features that survived the last round, and BART screening keeps only the few that matter. That keeps the pool small, where full enumeration can reach 10¹⁷ candidates. A sparse linear model is fitted on the survivors at the end. Every descriptor keeps its SymPy expression, and a unit filter throws out ones like volume + speed before they are ever screened. It is not on PyPI; install it from GitHub.</p>
<p>Selection is the largest source of uncertainty here, because a different sample of data can pick a different descriptor. <code>jax_ibart.uncertainty</code> has split-conformal intervals that wrap the whole fit, selection included, and selection-stability counts across bootstrap resamples, which tell you whether the data pin the descriptor down. It also has OLS intervals on the final model, which are optimistic because they condition on the selection, and the BART posterior. These are functions you call, not <code>predict(X, return_std=True)</code>.</p>
<p>Because the model is algebra, <code>model.to_discopt(m)</code> writes it into a <a href="https://github.com/jkitchin/discopt">discopt</a> problem and you can optimize over it globally with a certificate. Bounds on the inputs default to the training range, and guards keep the solver away from poles and logs of negative numbers that the data never visited. Examples: <a href="https://github.com/jkitchin/jax-ibart/blob/main/docs/examples/01_quickstart.ipynb">quickstart</a>, <a href="https://github.com/jkitchin/jax-ibart/blob/main/docs/examples/02_catalysis.ipynb">single-atom catalysis</a>, <a href="https://github.com/jkitchin/jax-ibart/blob/main/docs/examples/06_reaction_kinetics.ipynb">reaction kinetics</a>, <a href="https://github.com/jkitchin/jax-ibart/blob/main/docs/examples/08_uncertainty.ipynb">uncertainty</a>, <a href="https://github.com/jkitchin/jax-ibart/blob/main/docs/examples/09_optimization.ipynb">optimizing over a model</a>.</p>
</section>
<section id="sisso" class="level2">
<h2 class="anchored" data-anchor-id="sisso">SISSO</h2>
<p>SISSO (sure independence screening and sparsifying operator) takes a different route to the same goal. It composes the inputs into a very large space of candidate descriptors, screens them, and keeps the few that explain the data. It is popular in materials science for finding descriptors. The pycse wrapper sits on TorchSISSO. <code>SISSO</code> gives hat-matrix intervals for one equation, and <code>SISSOEnsemble</code> trains a shallow ensemble of equations with an NLL or CRPS loss. Examples: <a href="https://github.com/jkitchin/pycse/blob/master/src/pycse/examples/SISSO_Demo.ipynb">SISSO</a>, <a href="https://github.com/jkitchin/pycse/blob/master/src/pycse/examples/SISSOEnsemble_Demo.ipynb">SISSO ensemble</a>.</p>
</section>
<section id="regress-and-nlinfit" class="level2">
<h2 class="anchored" data-anchor-id="regress-and-nlinfit">regress and nlinfit</h2>
<p>Do not skip the simplest option. If you know the form of the model, <code>pycse.regress</code> and <code>nlinfit</code> fit it by linear or nonlinear least squares and give confidence intervals on the parameters. <code>predict</code> and <code>nlpredict</code> carry those intervals through to the predictions. With few data points this is often the most honest model you can have. <code>LinearRegressionUQ</code> wraps the linear case as an sklearn estimator. Example: <a href="https://github.com/jkitchin/pycse/blob/master/src/pycse/examples/Regression_Functions_Demo.ipynb">regression functions</a>.</p>
</section>
</section>
<section id="the-trees" class="level1">
<h1>The trees</h1>
<section id="linear-and-hyperplane-trees" class="level2">
<h2 class="anchored" data-anchor-id="linear-and-hyperplane-trees">Linear and hyperplane trees</h2>
<p><a href="https://github.com/jkitchin/jax-ldt">jax-ldt</a> grows decision trees with a linear model in each leaf. <code>LinearTreeRegressor</code> splits on one input at a time. <code>HyperplaneTreeRegressor</code> splits on oblique hyperplanes w·x ≤ b, so it needs far fewer leaves when the real boundaries are not aligned with an axis. The model is piecewise linear, which is the point: it embeds exactly in a mixed-integer linear program, for example in <a href="https://github.com/jkitchin/discopt">discopt</a>, and the solver then optimizes the surrogate globally. Uncertainty comes from each leaf’s parameter covariance, propagated linearly or to second order, or from conformal intervals computed per leaf. Trees also export to ONNX. Examples: <a href="https://github.com/jkitchin/jax-ldt/blob/main/examples/01_quickstart.ipynb">quickstart</a>, <a href="https://github.com/jkitchin/jax-ldt/blob/main/examples/02_uncertainty.ipynb">uncertainty</a>, <a href="https://github.com/jkitchin/jax-ldt/blob/main/examples/03_active_learning.ipynb">active learning</a>, <a href="https://github.com/jkitchin/jax-ldt/blob/main/examples/04_mip_optimization.ipynb">MIP optimization</a>, <a href="https://github.com/jkitchin/jax-ldt/blob/main/examples/05_onnx_export.ipynb">ONNX export</a>, <a href="https://github.com/jkitchin/jax-ldt/blob/main/examples/06_steam_tables.ipynb">steam tables</a>.</p>
</section>
<section id="differentiable-trees" class="level2">
<h2 class="anchored" data-anchor-id="differentiable-trees">Differentiable trees</h2>
<p><a href="https://kitchingroup.cheme.cmu.edu/jax-difftree/">jax-difftree</a> makes the splits soft, so the whole tree trains end to end by gradient descent. Each input follows every path with some probability. Each leaf holds a Gaussian, multivariate if there are several outputs, so a prediction is a mixture of Gaussians. That gives a variance that changes with x, and it splits into a within-leaf (aleatoric) part and a between-leaf (routing) part. With several outputs you can condition on the ones you have measured to predict the rest, or ask which x would most likely give a target y. For optimization, affine leaves can be hardened into a crisp piecewise-affine model and exported for a MILP, so this sits between the smooth world and the mixed-integer world. <code>DifferentiableTreeClassifier</code> splits its predictive entropy into aleatoric and epistemic parts. It does not have a notebook yet. Examples: <a href="https://github.com/jkitchin/jax-difftree/blob/main/docs/examples/heteroscedastic_demo.ipynb">heteroscedastic regression</a>, <a href="https://github.com/jkitchin/jax-difftree/blob/main/docs/examples/multivariate_inverse_design.ipynb">multivariate inverse design</a>, <a href="https://github.com/jkitchin/jax-difftree/blob/main/docs/examples/piecewise_linear_milp.ipynb">piecewise-linear MILP</a>, <a href="https://github.com/jkitchin/jax-difftree/blob/main/docs/examples/failure_modes.ipynb">failure modes</a>, <a href="https://github.com/jkitchin/jax-difftree/blob/main/docs/examples/scaling.ipynb">scaling</a>.</p>
</section>
<section id="leaf-model-trees" class="level2">
<h2 class="anchored" data-anchor-id="leaf-model-trees">Leaf-model trees</h2>
<p><code>LeafModelRegressor</code> in pycse is the simplest version of the idea. An ordinary sklearn decision tree splits the data, then any sklearn model you like is fitted in each leaf, and its uncertainty is used if it has one. By default the splits are chosen as if the leaves were constant. <code>refine_splits=True</code> re-chooses them for the leaf model, in the style of M5 model trees. I think of it as a proof of concept and a quick baseline next to jax-ldt. Example: <a href="https://github.com/jkitchin/pycse/blob/master/src/pycse/examples/LeafModel_Demo.ipynb">leaf models</a>.</p>
</section>
</section>
<section id="the-neural-networks" class="level1">
<h1>The neural networks</h1>
<p>The networks fall into two groups. Some build a property into the architecture so it holds everywhere. Others add a cheap uncertainty estimate to an otherwise ordinary network.</p>
<section id="convex-neural-networks-icnn" class="level2">
<h2 class="anchored" data-anchor-id="convex-neural-networks-icnn">Convex neural networks (ICNN)</h2>
<p>An input-convex network keeps the weights on its hidden path non-negative and uses convex, non-decreasing activations, which makes f convex in x. If you minimize the surrogate, any local minimum you find is the global one. The <a href="https://github.com/jkitchin/pycse/blob/master/src/pycse/examples/JAXICNNRegressor_Demo.ipynb">demo</a> also shows how to use one as a convex lower bound of a non-convex function. <code>JAXICNNRegressor</code> gives point predictions only, so pair it with another model when you need intervals.</p>
</section>
<section id="monotonic-and-periodic-networks" class="level2">
<h2 class="anchored" data-anchor-id="monotonic-and-periodic-networks">Monotonic and periodic networks</h2>
<p><code>JAXMonotonicRegressor</code> is guaranteed increasing or decreasing in the inputs you name. Use it when you know the sign of an effect, such as a rate that rises with temperature, and do not want the surrogate to wiggle against it where data are sparse. <code>JAXPeriodicRegressor</code> puts Fourier features on chosen inputs so the model is exactly periodic in them. Both get uncertainty from last-layer prediction rigidity (see LLPR). Examples: <a href="https://github.com/jkitchin/pycse/blob/master/src/pycse/examples/JAXMonotonicRegressor_Demo.ipynb">monotonic</a>, <a href="https://github.com/jkitchin/pycse/blob/master/src/pycse/examples/JAXPeriodicRegressor_Demo.ipynb">periodic</a>.</p>
</section>
<section id="kans" class="level2">
<h2 class="anchored" data-anchor-id="kans">KANs</h2>
<p>Kolmogorov-Arnold networks put learnable one-dimensional functions, here B-splines, on the edges instead of fixed activations on the nodes. You can plot each edge function, which makes them easier to inspect than an MLP. <code>KAN</code> gets uncertainty from an ensemble of output heads calibrated on a validation set. <code>KANLLPR</code> gets it from prediction rigidity instead. Examples: <a href="https://github.com/jkitchin/pycse/blob/master/src/pycse/examples/KAN_Demo.ipynb">KAN</a>, <a href="https://github.com/jkitchin/pycse/blob/master/src/pycse/examples/KANLLPR_Demo.ipynb">KAN + LLPR</a>.</p>
</section>
<section id="zenn" class="level2">
<h2 class="anchored" data-anchor-id="zenn">ZENN</h2>
<p>The zentropy-enhanced neural network is a thermodynamics-inspired model for heterogeneous data from several sources. It predicts an energy at a temperature and can reconstruct the energy landscape. It reports epistemic and aleatoric uncertainty, and the NLL variant calibrates the aleatoric part to the noise. It is described in PNAS 123(1), e2511227122. The <a href="https://github.com/jkitchin/pycse/tree/master/src/pycse/sklearn/zenn">code</a> is in pycse, but it has no example notebook yet.</p>
</section>
<section id="llpr" class="level2">
<h2 class="anchored" data-anchor-id="llpr">LLPR</h2>
<p>Last-layer prediction rigidity is the cheapest way I know to add uncertainty to a network you have already trained. It uses the last-layer features and a covariance computed from them, with no retraining and no ensemble, and is calibrated on held-out data. <code>LLPRRegressor</code> is the standalone version, and the same idea gives the uncertainty in the monotonic, periodic and KAN-LLPR models. Examples: <a href="https://github.com/jkitchin/pycse/blob/master/src/pycse/examples/llpr_demo.ipynb">LLPR demo</a>, <a href="https://github.com/jkitchin/pycse/blob/master/src/pycse/examples/llpr_study.ipynb">LLPR study</a>.</p>
</section>
<section id="dpose" class="level2">
<h2 class="anchored" data-anchor-id="dpose">DPOSE</h2>
<p>DPOSE (direct propagation of shallow ensembles) shares the whole network across ensemble members except the last layer, and trains with a CRPS or NLL loss so the spread is calibrated. It costs little more than a single network. Because you have the members, you can push any function of the prediction through them and get its uncertainty directly. Example: <a href="https://github.com/jkitchin/pycse/blob/master/src/pycse/examples/DPOSE_Demo.ipynb">DPOSE</a>.</p>
</section>
<section id="k-fold-networks" class="level2">
<h2 class="anchored" data-anchor-id="k-fold-networks">k-fold networks</h2>
<p><code>KfoldNN</code> trains each output neuron on a different fold of the data, so one network carries a k-member ensemble, and the spread across outputs is the uncertainty. Example: <a href="https://github.com/jkitchin/pycse/blob/master/src/pycse/examples/KfoldNN_Demo.ipynb">k-fold NN</a>.</p>
</section>
<section id="nn-bayesian-linear-and-nn-gmm" class="level2">
<h2 class="anchored" data-anchor-id="nn-bayesian-linear-and-nn-gmm">NN + Bayesian linear and NN + GMM</h2>
<p>These two use an sklearn MLP as a feature generator and put a probabilistic model on top. <code>NeuralNetworkBLR</code> uses Bayesian linear regression as the last layer. <code>NeuralNetworkGMM</code> uses Gaussian mixture regression, which can represent noise that changes with x and predictions with more than one mode. They are not JAX models, so they do not fit the differentiable story as neatly, but they are simple and robust. Examples: <a href="https://github.com/jkitchin/pycse/blob/master/src/pycse/examples/NNBR_Demo.ipynb">NN + Bayesian linear</a>, <a href="https://github.com/jkitchin/pycse/blob/master/src/pycse/examples/NNGMM_Demo.ipynb">NN + GMM</a>.</p>
</section>
</section>
<section id="distributions" class="level1">
<h1>Distributions</h1>
<section id="normalizing-flows" class="level2">
<h2 class="anchored" data-anchor-id="normalizing-flows">Normalizing flows</h2>
<p><code>NFlowsRegressor</code> learns the whole conditional density p(y | x) as an invertible transformation of a simple distribution. You can sample from it and read off quantiles. Because the transformation is invertible, it also runs backwards to infer which inputs are consistent with an observed output. It is built on PyTorch (nflows), not JAX. Example: <a href="https://github.com/jkitchin/pycse/blob/master/src/pycse/examples/nflows_regressor_examples.ipynb">normalizing flow regression</a>, which includes a CSTR design problem.</p>
</section>
</section>
<section id="across-the-zoo" class="level1">
<h1>Across the zoo</h1>
<section id="active-learning" class="level2">
<h2 class="anchored" data-anchor-id="active-learning">Active learning</h2>
<p>Uncertainty is not only for error bars. It tells you where to sample next. Any model above that returns a standard deviation can drive an active-learning loop that adds data where the surrogate is least sure. Examples: jaxsr <a href="https://github.com/jkitchin/jaxsr/blob/main/docs/examples/active_learning.ipynb">active learning</a> and <a href="https://github.com/jkitchin/jaxsr/blob/main/docs/examples/langmuir_doe_active_learning.ipynb">Langmuir DOE</a>, jax-ldt <a href="https://github.com/jkitchin/jax-ldt/blob/main/examples/03_active_learning.ipynb">active learning</a>, pycse <a href="https://github.com/jkitchin/pycse/blob/master/src/pycse/examples/ActiveLearning_pycse_Demo.ipynb">active learning</a> and <a href="https://github.com/jkitchin/pycse/blob/master/src/pycse/examples/Active_Surrogate_Demo.ipynb">active surrogates</a>.</p>
</section>
<section id="comparing-uncertainty" class="level2">
<h2 class="anchored" data-anchor-id="comparing-uncertainty">Comparing uncertainty</h2>
<p>The methods do not agree, and they fail in different ways. The pycse <a href="https://github.com/jkitchin/pycse/blob/master/src/pycse/examples/UQ_Comparison_Demo.ipynb">UQ comparison</a> runs conformal intervals (MAPIE), NN + Bayesian linear, NN + GMM and DPOSE on the same data and checks coverage and heteroscedastic noise. The jax-difftree <a href="https://github.com/jkitchin/jax-difftree/blob/main/docs/examples/failure_modes.ipynb">failure modes</a> notebook shows where soft trees go wrong.</p>
</section>
</section>
<section id="finishing-thoughts" class="level1">
<h1>Finishing thoughts</h1>
<p>Hopefully this gives you some insights into the breadth of differentiable surrogates that know their uncertainty, and where to find an example of each. The zoo is still growing, and we welcome feedback. If you find bugs, please report them!</p>


</section>

 ]]></content:encoded></item><item>
      <title>Lies, damn lies, statistics and Bayesian statistics</title>
      <link>https://kitchingroup.cheme.cmu.edu/blog/2025/06/22/Lies-damn-lies-statistics-and-Bayesian-statistics</link>
      <pubDate>Sun, 22 Jun 2025 11:14:23 EDT</pubDate>
      <category><![CDATA[machine-learning]]></category>
      <guid isPermaLink="false">14mSE18aHhiNdMq3g69gXgG5w2o=</guid>
      <description>Lies, damn lies, statistics and Bayesian statistics</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="#org035da70"&gt;1. The data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#org28ad175"&gt;2. GPR with a RBF kernel&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#orga7fb619"&gt;3. a better kernel solves these issues&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#orgca7200a"&gt;4. How about with feature engineering?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#orgdd16d18"&gt;5. Summary&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;
This post on LinkedIn (&lt;a href="https://www.linkedin.com/posts/activity-7341134401705041920-gaEd?utm_source=share&amp;amp;utm_medium=member_desktop&amp;amp;rcm=ACoAAAfqmO0BzyXpJw8w7yyHwkoMSiaKfGg-sKI"&gt;https://www.linkedin.com/posts/activity-7341134401705041920-gaEd?utm_source=share&amp;amp;utm_medium=member_desktop&amp;amp;rcm=ACoAAAfqmO0BzyXpJw8w7yyHwkoMSiaKfGg-sKI&lt;/a&gt;) reminded me of a quip I often make of "Lies, damn lies, statistics, and Bayesian statistics". I am frequently skeptical of claims about "Bayesian something something", especially when the claim is about uncertainty quantification. That skepticism comes from practical experience of mine that "Bayesian something something" is rarely as well behaved and informative as advertised (in my hands of course).
&lt;/p&gt;

&lt;p&gt;
To illustrate, I will use some noisy, 1d data from a Lennard-Jones function and Gaussian process regression to fit the data.
&lt;/p&gt;
&lt;div id="outline-container-org035da70" class="outline-2"&gt;
&lt;h2 id="org035da70"&gt;&lt;span class="section-number-2"&gt;1.&lt;/span&gt; The data&lt;/h2&gt;
&lt;div class="outline-text-2" id="text-1"&gt;
&lt;p&gt;
We get our data by sampling a Lennard-Jones function, adding some noise, and removing a gap in the data. The gap in the middle might be classically considered an interpolation region.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-jupyter-python"&gt;&lt;span style="color: #0000FF; font-weight: bold;"&gt;import&lt;/span&gt; numpy &lt;span style="color: #0000FF; font-weight: bold;"&gt;as&lt;/span&gt; np
&lt;span style="color: #0000FF; font-weight: bold;"&gt;import&lt;/span&gt; matplotlib.pyplot &lt;span style="color: #0000FF; font-weight: bold;"&gt;as&lt;/span&gt; plt

&lt;span style="color: #BA36A5;"&gt;r&lt;/span&gt; = np.linspace(0.95, 3, 200)

&lt;span style="color: #BA36A5;"&gt;eps&lt;/span&gt;, &lt;span style="color: #BA36A5;"&gt;sig&lt;/span&gt; = 1, 1
&lt;span style="color: #BA36A5;"&gt;y&lt;/span&gt; = 4 * eps * ((1 / r)**12 - (1 / r)**6) + np.random.normal(0, 0.03, size=r.shape)


&lt;span style="color: #BA36A5;"&gt;ind&lt;/span&gt; = ((r &amp;gt; 1) &amp;amp; (r &amp;lt; 1.25)) | ((r &amp;gt; 2) &amp;amp; (r &amp;lt; 2.5))
&lt;span style="color: #BA36A5;"&gt;_R&lt;/span&gt; = r[ind][:, &lt;span style="color: #D0372D;"&gt;None&lt;/span&gt;]
&lt;span style="color: #BA36A5;"&gt;_y&lt;/span&gt; = y[ind]
plt.plot(_R, _y, &lt;span style="color: #008000;"&gt;'.'&lt;/span&gt;)
plt.xlabel(&lt;span style="color: #008000;"&gt;'R'&lt;/span&gt;)
plt.ylabel(&lt;span style="color: #008000;"&gt;'E'&lt;/span&gt;);
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
&lt;figure&gt;&lt;img src="/media/653165863df7654b10ddaca2f7645560768bd870.png"&gt;&lt;/figure&gt; 
&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id="outline-container-org28ad175" class="outline-2"&gt;
&lt;h2 id="org28ad175"&gt;&lt;span class="section-number-2"&gt;2.&lt;/span&gt; GPR with a RBF kernel&lt;/h2&gt;
&lt;div class="outline-text-2" id="text-2"&gt;
&lt;p&gt;
The RBF kernel is the most standard kernel. It does an ok job fitting here, although I see evidence of overfitting (the wiggles are caused by the noise). You can reduce the overfitting by using a larger &lt;code&gt;alpha&lt;/code&gt; value in the gpr, but that requires you to know in advance how smooth it should be.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-jupyter-python"&gt;&lt;span style="color: #0000FF; font-weight: bold;"&gt;from&lt;/span&gt; sklearn.gaussian_process &lt;span style="color: #0000FF; font-weight: bold;"&gt;import&lt;/span&gt; GaussianProcessRegressor
&lt;span style="color: #0000FF; font-weight: bold;"&gt;from&lt;/span&gt; sklearn.gaussian_process.kernels &lt;span style="color: #0000FF; font-weight: bold;"&gt;import&lt;/span&gt; RBF, WhiteKernel
&lt;span style="color: #BA36A5;"&gt;kernel&lt;/span&gt; = RBF() + WhiteKernel()
&lt;span style="color: #BA36A5;"&gt;gpr&lt;/span&gt; = GaussianProcessRegressor(kernel=kernel,
                               random_state=0, normalize_y=&lt;span style="color: #D0372D;"&gt;True&lt;/span&gt;).fit(_R, _y)

plt.plot(_R, _y, &lt;span style="color: #008000;"&gt;'b.'&lt;/span&gt;)
plt.plot(r, y, &lt;span style="color: #008000;"&gt;'b.'&lt;/span&gt;, alpha=0.2)

&lt;span style="color: #BA36A5;"&gt;yp&lt;/span&gt;, &lt;span style="color: #BA36A5;"&gt;se&lt;/span&gt; = gpr.predict(r[:, &lt;span style="color: #D0372D;"&gt;None&lt;/span&gt;], return_std=&lt;span style="color: #D0372D;"&gt;True&lt;/span&gt;)
plt.plot(r, yp)
plt.plot(r, yp + 2 * se, &lt;span style="color: #008000;"&gt;'k--'&lt;/span&gt;, r, yp - 2 * se, &lt;span style="color: #008000;"&gt;'k--'&lt;/span&gt;);
plt.plot(_R, _y, &lt;span style="color: #008000;"&gt;'.'&lt;/span&gt;)
plt.xlabel(&lt;span style="color: #008000;"&gt;'R'&lt;/span&gt;)
plt.ylabel(&lt;span style="color: #008000;"&gt;'E'&lt;/span&gt;);

gpr.kernel_
&lt;/pre&gt;
&lt;/div&gt;

&lt;pre class="example"&gt;
RBF(length_scale=0.0948) + WhiteKernel(noise_level=0.00635)
&lt;/pre&gt;

&lt;p&gt;
&lt;figure&gt;&lt;img src="/media/6acc7dccc37ee773aeb4c97c62929401733a02f6.png"&gt;&lt;/figure&gt; 
&lt;/p&gt;

&lt;p&gt;
The uncertainty here is primarily related to the model, i.e. it is constrained to be correct where there is data, but with no data, the model is not the right one.
&lt;/p&gt;

&lt;p&gt;
The model does well in the region where there is data, but is qualitatively wrong in the gap (even though classically this would be considered interpolation), and overestimates the uncertainty in this region. The problem is the covariance kernel decays to 0 about two length scales away from the last point, which means there is no data to inform what the weights in that region should look like.  That causes the model to revert to the mean of the data.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-jupyter-python"&gt;gpr.predict([[1.8]]), gpr.predict([[3.0]]), np.mean(_y)
&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;col  class="org-left" /&gt;

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

&lt;col  class="org-right" /&gt;
&lt;/colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class="org-left"&gt;array&lt;/td&gt;
&lt;td class="org-left"&gt;((-0.2452041))&lt;/td&gt;
&lt;td class="org-left"&gt;array&lt;/td&gt;
&lt;td class="org-left"&gt;((-0.29363654))&lt;/td&gt;
&lt;td class="org-right"&gt;-0.2936364964541409&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;
Why is this happening? It is not that tricky. You can think of the GP as an expansion of the data in basis functions. The kernel trick effectively makes this expansion in the infinite limit. What are those basis functions? We can draw samples of them, which we show here. You can see that where there is no data, the basis functions are "wiggly". That means they are simply not good at making predictions here.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-jupyter-python"&gt;&lt;span style="color: #BA36A5;"&gt;y_samples&lt;/span&gt; = gpr.sample_y(r[:, &lt;span style="color: #D0372D;"&gt;None&lt;/span&gt;], n_samples=15, random_state=0)

plt.plot(r, yp)
plt.plot(r, yp + 2 * se, &lt;span style="color: #008000;"&gt;'k--'&lt;/span&gt;, r, yp - 2 * se, &lt;span style="color: #008000;"&gt;'k--'&lt;/span&gt;);
plt.plot(_R, _y, &lt;span style="color: #008000;"&gt;'.'&lt;/span&gt;)

plt.plot(r, y_samples, &lt;span style="color: #008000;"&gt;'k'&lt;/span&gt;, alpha=0.2);

plt.xlabel(&lt;span style="color: #008000;"&gt;'R'&lt;/span&gt;)
plt.ylabel(&lt;span style="color: #008000;"&gt;'E'&lt;/span&gt;);
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
&lt;figure&gt;&lt;img src="/media/cff4cfa5cacedcef6ecde8ec2b63dcee659949fb.png"&gt;&lt;/figure&gt; 
&lt;/p&gt;


&lt;p&gt;
This kernel simply cannot be used for extrapolation, or any predictions more than about two length scales away from the nearest point. Calling it Bayesian doesn't make it better. For similar reasons, this model will not work well outside the data range.
&lt;/p&gt;

&lt;p&gt;
A practical person would still consider using this model, and might even rely on the uncertainty being too large to identify regions of low reliability.
&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id="outline-container-orga7fb619" class="outline-2"&gt;
&lt;h2 id="orga7fb619"&gt;&lt;span class="section-number-2"&gt;3.&lt;/span&gt; a better kernel solves these issues&lt;/h2&gt;
&lt;div class="outline-text-2" id="text-3"&gt;
&lt;p&gt;
Not all is lost, if we know more. In this case we can construct a kernel that reflects our understanding that the data came from a Lennard Jones like interaction model. You can construct kernels by adding and multiplying kernels. Here we consider a linear kernel, the &lt;code&gt;DotProduct&lt;/code&gt; kernel, and construct a new kernel that is a sum of the linear kernel to the 12&lt;sup&gt;th&lt;/sup&gt; power, a linear kernel to the 6&lt;sup&gt;th&lt;/sup&gt; power, and a &lt;code&gt;WhiteKernel&lt;/code&gt; for the noise. It is a little subtle that this kernel should work in \(1 / r\) space, so in addition to kernel engineering, we also do feature engineering.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-jupyter-python"&gt;&lt;span style="color: #0000FF; font-weight: bold;"&gt;from&lt;/span&gt; sklearn.gaussian_process.kernels &lt;span style="color: #0000FF; font-weight: bold;"&gt;import&lt;/span&gt; DotProduct

&lt;span style="color: #BA36A5;"&gt;kernel&lt;/span&gt; = DotProduct()**12 + DotProduct()**6 +  WhiteKernel()
&lt;span style="color: #BA36A5;"&gt;gpr&lt;/span&gt; = GaussianProcessRegressor(kernel=kernel, normalize_y=&lt;span style="color: #D0372D;"&gt;True&lt;/span&gt;).fit(1 / _R, _y)

plt.plot(_R, _y, &lt;span style="color: #008000;"&gt;'b.'&lt;/span&gt;)
plt.plot(r, y, &lt;span style="color: #008000;"&gt;'b.'&lt;/span&gt;, alpha=0.2)


&lt;span style="color: #BA36A5;"&gt;yp&lt;/span&gt;, &lt;span style="color: #BA36A5;"&gt;se&lt;/span&gt; = gpr.predict(1 / r[:, &lt;span style="color: #D0372D;"&gt;None&lt;/span&gt;], return_std=&lt;span style="color: #D0372D;"&gt;True&lt;/span&gt;)
plt.plot(r, yp)
plt.plot(r, yp + 2 * se, &lt;span style="color: #008000;"&gt;'k--'&lt;/span&gt;, r, yp - 2 * se, &lt;span style="color: #008000;"&gt;'k--'&lt;/span&gt;);

plt.xlabel(&lt;span style="color: #008000;"&gt;'R'&lt;/span&gt;)
plt.ylabel(&lt;span style="color: #008000;"&gt;'E'&lt;/span&gt;);

gpr.kernel_
&lt;/pre&gt;
&lt;/div&gt;

&lt;pre class="example"&gt;
DotProduct(sigma_0=0.0281) ** 12 + DotProduct(sigma_0=0.936) ** 6 + WhiteKernel(noise_level=0.0077)
&lt;/pre&gt;

&lt;p&gt;
&lt;figure&gt;&lt;img src="/media/70e91f8419a473ed578a14442694e67a3409bd1e.png"&gt;&lt;/figure&gt; 
&lt;/p&gt;

&lt;p&gt;
Note that this GPR does fine in the gap, including the right level of uncertainty there. This model is better because we used the kernel to constrain what forms the model can have. This model actually extrapolates correctly outside the data. It is worth noting that although this model has great predictive and UQ properties, it does not tell us anything about the values of &amp;epsilon; and &amp;sigma; in the Lennard Jones model. Although we might say the kernel is physics-based, i.e. it is based on the relevant features and equation, it does not have physical parameters in it.
&lt;/p&gt;

&lt;p&gt;
How about those basis functions here? You can see that all of them basically look like the LJ potential. That means they are good basis functions to expand this data set in.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-jupyter-python"&gt;&lt;span style="color: #BA36A5;"&gt;y_samples&lt;/span&gt; = gpr.sample_y(1 / r[:, &lt;span style="color: #D0372D;"&gt;None&lt;/span&gt;], n_samples=15, random_state=0)

plt.plot(_R, _y, &lt;span style="color: #008000;"&gt;'.'&lt;/span&gt;)

plt.plot(r, y_samples, &lt;span style="color: #008000;"&gt;'k'&lt;/span&gt;, alpha=0.2);

plt.xlabel(&lt;span style="color: #008000;"&gt;'R'&lt;/span&gt;)
plt.ylabel(&lt;span style="color: #008000;"&gt;'E'&lt;/span&gt;);
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
&lt;figure&gt;&lt;img src="/media/e7fe34a01c52cb228cbbcde85e5f334e7f8237a1.png"&gt;&lt;/figure&gt; 
&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id="outline-container-orgca7200a" class="outline-2"&gt;
&lt;h2 id="orgca7200a"&gt;&lt;span class="section-number-2"&gt;4.&lt;/span&gt; How about with feature engineering?&lt;/h2&gt;
&lt;div class="outline-text-2" id="text-4"&gt;
&lt;p&gt;
Can we do even better with feature engineering here? Motivated by &lt;a href="https://www.linkedin.com/feed/update/urn:li:activity:7342573774774386688?commentUrn=urn%3Ali%3Acomment%3A%28activity%3A7342573774774386688%2C7342949865590530052%29&amp;amp;dashCommentUrn=urn%3Ali%3Afsd_comment%3A%287342949865590530052%2Curn%3Ali%3Aactivity%3A7342573774774386688%29"&gt;this comment&lt;/a&gt; by Cory Simon, we cast the problem as a linear regression in [1 / r&lt;sup&gt;6&lt;/sup&gt;, 1 / r&lt;sup&gt;12&lt;/sup&gt;] feature space. This is also a perfectly reasonable thing to do. Since our output is linear in these features, we simply use a linear kernel (aka the DotProduct kernel in sklearn).
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-jupyter-python"&gt;&lt;span style="color: #BA36A5;"&gt;r6&lt;/span&gt; = 1 / _R**6
&lt;span style="color: #BA36A5;"&gt;r12&lt;/span&gt; = r6**2

&lt;span style="color: #BA36A5;"&gt;kernel&lt;/span&gt; = DotProduct() + WhiteKernel()

&lt;span style="color: #BA36A5;"&gt;gpr&lt;/span&gt; = GaussianProcessRegressor(kernel=kernel, normalize_y=&lt;span style="color: #D0372D;"&gt;True&lt;/span&gt;).fit(np.hstack([r6, r12]), _y)

plt.plot(_R, _y, &lt;span style="color: #008000;"&gt;'b.'&lt;/span&gt;)
plt.plot(r, y, &lt;span style="color: #008000;"&gt;'b.'&lt;/span&gt;, alpha=0.2)

&lt;span style="color: #BA36A5;"&gt;fr6&lt;/span&gt; = 1 / r[:, &lt;span style="color: #D0372D;"&gt;None&lt;/span&gt;]**6
&lt;span style="color: #BA36A5;"&gt;fr12&lt;/span&gt; = fr6**2

&lt;span style="color: #BA36A5;"&gt;yp&lt;/span&gt;, &lt;span style="color: #BA36A5;"&gt;se&lt;/span&gt; = gpr.predict(np.hstack([fr6, fr12]), return_std=&lt;span style="color: #D0372D;"&gt;True&lt;/span&gt;)
plt.plot(r, yp)
plt.plot(r, yp + 2 * se, &lt;span style="color: #008000;"&gt;'k--'&lt;/span&gt;, r, yp - 2 * se, &lt;span style="color: #008000;"&gt;'k--'&lt;/span&gt;);

plt.xlabel(&lt;span style="color: #008000;"&gt;'R'&lt;/span&gt;)
plt.ylabel(&lt;span style="color: #008000;"&gt;'E'&lt;/span&gt;);

gpr.kernel_
&lt;/pre&gt;
&lt;/div&gt;

&lt;pre class="example"&gt;
DotProduct(sigma_0=0.74) + WhiteKernel(noise_level=0.00654)
&lt;/pre&gt;

&lt;p&gt;
&lt;figure&gt;&lt;img src="/media/d8769fe652b9e902e3d349ce26cdbd7d8050b190.png"&gt;&lt;/figure&gt; 
&lt;/p&gt;

&lt;p&gt;
We can't easily plot these basis functions the same way, so we reduce them to a 1-d plot. You can see here that these basis functions practically the same as the one with the advanced kernel.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-jupyter-python"&gt;&lt;span style="color: #BA36A5;"&gt;y_samples&lt;/span&gt; = gpr.sample_y(np.hstack([fr6, fr12]),
                         n_samples=15, random_state=0)

plt.plot(_R, _y, &lt;span style="color: #008000;"&gt;'.'&lt;/span&gt;)

plt.plot(r, y_samples, &lt;span style="color: #008000;"&gt;'k'&lt;/span&gt;, alpha=0.2);

plt.xlabel(&lt;span style="color: #008000;"&gt;'R'&lt;/span&gt;)
plt.ylabel(&lt;span style="color: #008000;"&gt;'E'&lt;/span&gt;);
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
&lt;figure&gt;&lt;img src="/media/f777586bb8e17bac5ca3dadbfba97119addeb46b.png"&gt;&lt;/figure&gt; 
&lt;/p&gt;



&lt;p&gt;
This also works quite well, and is another way to leverage knowledge about what we are building a model for.
&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id="outline-container-orgdd16d18" class="outline-2"&gt;
&lt;h2 id="orgdd16d18"&gt;&lt;span class="section-number-2"&gt;5.&lt;/span&gt; Summary&lt;/h2&gt;
&lt;div class="outline-text-2" id="text-5"&gt;
&lt;p&gt;
Naive use of GPR can provide useful models when you have enough data, but these models likely do not accurately capture uncertainty outside that data, nor is it likely they are reliable in extrapolation. It is possible to do better than this, when you know what to do. Through feature and kernel engineering, you can sometimes create situations where the problem essentially becomes linear regression, where a simple linear kernel is what you want, or you develop a kernel that represents the underlying model. Kernel engineering is generally hard, with limited opportunities to be flexible. See &lt;a href="https://www.cs.toronto.edu/~duvenaud/cookbook/"&gt;https://www.cs.toronto.edu/~duvenaud/cookbook/&lt;/a&gt; for examples of kernels and combining them.
&lt;/p&gt;

&lt;p&gt;
You can see it is not adequate to say "we used Gaussian process regression". That is about as informative as saying linear regression without identifying the features, or nonlinear regression and not saying what model. You have to be specific about the kernel, and thoughtful about how you know if a prediction is reliable or not. Just because you get an uncertainty prediction doesn't mean its right.
&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Copyright (C) 2025 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/2025/06/22/Lies,-damn-lies,-statistics-and-Bayesian-statistics.org"&gt;org-mode source&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Org-mode version = 9.8-pre&lt;/p&gt;]]></content:encoded>
    </item>
    <item>
      <title>Modeling a Cu dimer by EMT, nonlinear regression and neural networks</title>
      <link>https://kitchingroup.cheme.cmu.edu/blog/2017/03/18/Modeling-a-Cu-dimer-by-EMT-nonlinear-regression-and-neural-networks</link>
      <pubDate>Sat, 18 Mar 2017 15:47:55 EDT</pubDate>
      <category><![CDATA[molecular-simulation]]></category>
      <category><![CDATA[python]]></category>
      <category><![CDATA[neural-network]]></category>
      <category><![CDATA[machine-learning]]></category>
      <guid isPermaLink="false">YF7pYoEPLUP4Of_3TO50RTAKQhM=</guid>
      <description>Modeling a Cu dimer by EMT, nonlinear regression and neural networks</description>
      <content:encoded><![CDATA[


&lt;p&gt;
In this post we consider a Cu&lt;sub&gt;2&lt;/sub&gt; dimer and how its energy varies with the separation of the atoms. We assume we have a way to calculate this, but that it is expensive, and that we want to create a simpler model that is as accurate, but cheaper to run. A simple way to do that is to regress a physical model, but we will illustrate some challenges with that. We then show a neural network can be used as an accurate regression function without needing to know more about the physics.
&lt;/p&gt;

&lt;p&gt;
We will use an &lt;a href="https://wiki.fysik.dtu.dk/ase/ase/calculators/emt.html"&gt;effective medium theory&lt;/a&gt; calculator to demonstrate this. The calculations are not expected to be very accurate or relevant to any experimental data, but they are fast, and will illustrate several useful points that are independent of that. We will take as our energy zero the energy of two atoms at a large separation, in this case about 10 angstroms. Here we plot the energy as a function of the distance between the two atoms, which is the only degree of freedom that matters in this example.
&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
%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: #0000FF;"&gt;from&lt;/span&gt; ase.calculators.emt &lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; EMT
&lt;span style="color: #0000FF;"&gt;from&lt;/span&gt; ase &lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; Atoms

&lt;span style="color: #BA36A5;"&gt;atoms&lt;/span&gt; = Atoms(&lt;span style="color: #008000;"&gt;'Cu2'&lt;/span&gt;,[[0, 0, 0], [10, 0, 0]], pbc=[&lt;span style="color: #D0372D;"&gt;False&lt;/span&gt;, &lt;span style="color: #D0372D;"&gt;False&lt;/span&gt;, &lt;span style="color: #D0372D;"&gt;False&lt;/span&gt;])
atoms.set_calculator(EMT())

&lt;span style="color: #BA36A5;"&gt;e0&lt;/span&gt; = atoms.get_potential_energy()

&lt;span style="color: #8D8D84;"&gt;# &lt;/span&gt;&lt;span style="color: #8D8D84; font-style: italic;"&gt;Array of bond lengths to get the energy for&lt;/span&gt;
&lt;span style="color: #BA36A5;"&gt;d&lt;/span&gt; = np.linspace(1.7, 3, 30)

&lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;get_e&lt;/span&gt;(distance):
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #BA36A5;"&gt;a&lt;/span&gt; = atoms.copy()
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   a[1]&lt;span style="color: #BA36A5;"&gt;.x&lt;/span&gt; = distance
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   a.set_calculator(EMT())
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #BA36A5;"&gt;e&lt;/span&gt; = a.get_potential_energy()
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; e

&lt;span style="color: #BA36A5;"&gt;e&lt;/span&gt; = np.array([get_e(dist) &lt;span style="color: #0000FF;"&gt;for&lt;/span&gt; dist &lt;span style="color: #0000FF;"&gt;in&lt;/span&gt; d])
&lt;span style="color: #BA36A5;"&gt;e&lt;/span&gt; -=  e0  &lt;span style="color: #8D8D84;"&gt;# &lt;/span&gt;&lt;span style="color: #8D8D84; font-style: italic;"&gt;set the energy zero&lt;/span&gt;

plt.plot(d, e, &lt;span style="color: #008000;"&gt;'bo '&lt;/span&gt;)
plt.xlabel(&lt;span style="color: #008000;"&gt;'d (&amp;#197;)'&lt;/span&gt;)
plt.ylabel(&lt;span style="color: #008000;"&gt;'energy (eV)'&lt;/span&gt;)
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
&lt;img src="/media/ob-ipython-82aeda9421056689d5212f9033da900a.png"&gt; 
&lt;/p&gt;


&lt;p&gt;
We see there is a minimum, and the energy is asymmetric about the minimum. We have no functional form for the energy here, just the data in the plot. So to get another energy, we have to run another calculation. If that was expensive, we might prefer an analytical equation to evaluate instead.  We will get an analytical form by fitting a function to the data. A classic one is the &lt;a href="https://en.wikipedia.org/wiki/Buckingham_potential"&gt;Buckingham potential&lt;/a&gt;: \(E = A \exp(-B r) - \frac{C}{r^6}\). Here we perform the regression.
&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;model&lt;/span&gt;(r, A, B, C):
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; A * np.exp(-B * r) - C / r**6

&lt;span style="color: #0000FF;"&gt;from&lt;/span&gt; pycse &lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; nlinfit
&lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; pprint

&lt;span style="color: #BA36A5;"&gt;p0&lt;/span&gt; = [-80, 1, 1]
&lt;span style="color: #BA36A5;"&gt;p&lt;/span&gt;, &lt;span style="color: #BA36A5;"&gt;pint&lt;/span&gt;, &lt;span style="color: #BA36A5;"&gt;se&lt;/span&gt; = nlinfit(model, d, e, p0, 0.05)
&lt;span style="color: #0000FF;"&gt;print&lt;/span&gt;(&lt;span style="color: #008000;"&gt;'Parameters = '&lt;/span&gt;, p)
&lt;span style="color: #0000FF;"&gt;print&lt;/span&gt;(&lt;span style="color: #008000;"&gt;'Confidence intervals = '&lt;/span&gt;)
pprint.pprint(pint)
plt.plot(d, e, &lt;span style="color: #008000;"&gt;'bo '&lt;/span&gt;, label=&lt;span style="color: #008000;"&gt;'calculations'&lt;/span&gt;)

&lt;span style="color: #BA36A5;"&gt;x&lt;/span&gt; = np.linspace(&lt;span style="color: #006FE0;"&gt;min&lt;/span&gt;(d), &lt;span style="color: #006FE0;"&gt;max&lt;/span&gt;(d))
plt.plot(x, model(x, *p), label=&lt;span style="color: #008000;"&gt;'fit'&lt;/span&gt;)
plt.legend(loc=&lt;span style="color: #008000;"&gt;'best'&lt;/span&gt;)
plt.xlabel(&lt;span style="color: #008000;"&gt;'d (&amp;#197;)'&lt;/span&gt;)
plt.ylabel(&lt;span style="color: #008000;"&gt;'energy (eV)'&lt;/span&gt;)
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
Parameters =  [ -83.21072545    1.18663393 -266.15259507]
Confidence intervals =
array([[ -93.47624687,  -72.94520404],
       [   1.14158438,    1.23168348],
       [-280.92915682, -251.37603331]])
&lt;img src="/media/ob-ipython-a05811588d06f090a2462ba9f48dccb3.png"&gt; 
&lt;/p&gt;

&lt;p&gt;
That fit is ok, but not great. We would be better off with a spline for this simple system! The trouble is how do we get anything better? If we had a better equation to fit to we might get better results.  While one might come up with one for this dimer, how would you extend it to more complex systems, even just a trimer? There have been decades of research dedicated to that, and we are not smarter than those researchers so, it is time for a new approach.
&lt;/p&gt;

&lt;p&gt;
We will use a Neural Network regressor. The input will be \(d\) and we want to regress a function to predict the energy.
&lt;/p&gt;

&lt;p&gt;
There are a couple of important points to make here.
&lt;/p&gt;

&lt;ol class="org-ol"&gt;
&lt;li&gt;This is just another kind of regression.&lt;/li&gt;
&lt;li&gt;We need a lot more data to do the regression. Here we use 300 data points.&lt;/li&gt;
&lt;li&gt;We need to specify a network architecture. Here we use one hidden layer with 10 neurons, and the tanh activation function on each neuron. The last layer is just the output layer. I do not claim this is any kind of optimal architecture. It is just one that works to illustrate the idea.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;
Here is the code that uses a neural network regressor, which is lightly adapted from &lt;a href="http://scikit-neuralnetwork.readthedocs.io/en/latest/guide_model.html"&gt;http://scikit-neuralnetwork.readthedocs.io/en/latest/guide_model.html&lt;/a&gt;.
&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; sknn.mlp &lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; Regressor, Layer

&lt;span style="color: #BA36A5;"&gt;D&lt;/span&gt; = np.linspace(1.7, 3, 300)

&lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;get_e&lt;/span&gt;(distance):
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #BA36A5;"&gt;a&lt;/span&gt; = atoms.copy()
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   a[1]&lt;span style="color: #BA36A5;"&gt;.x&lt;/span&gt; = distance
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   a.set_calculator(EMT())
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #BA36A5;"&gt;e&lt;/span&gt; = a.get_potential_energy()
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; e

&lt;span style="color: #BA36A5;"&gt;E&lt;/span&gt; = np.array([get_e(dist) &lt;span style="color: #0000FF;"&gt;for&lt;/span&gt; dist &lt;span style="color: #0000FF;"&gt;in&lt;/span&gt; D])
&lt;span style="color: #BA36A5;"&gt;E&lt;/span&gt; -=  e0  &lt;span style="color: #8D8D84;"&gt;# &lt;/span&gt;&lt;span style="color: #8D8D84; font-style: italic;"&gt;set the energy zero&lt;/span&gt;

&lt;span style="color: #BA36A5;"&gt;X_train&lt;/span&gt; = np.row_stack(np.array(D))

&lt;span style="color: #BA36A5;"&gt;N&lt;/span&gt; = 10
&lt;span style="color: #BA36A5;"&gt;nn&lt;/span&gt; = Regressor(layers=[Layer(&lt;span style="color: #008000;"&gt;"Tanh"&lt;/span&gt;, units=N),
&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;   &lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;  Layer(&lt;span style="color: #008000;"&gt;'Linear'&lt;/span&gt;)])
nn.fit(X_train, E)

&lt;span style="color: #BA36A5;"&gt;dfit&lt;/span&gt; = np.linspace(&lt;span style="color: #006FE0;"&gt;min&lt;/span&gt;(d), &lt;span style="color: #006FE0;"&gt;max&lt;/span&gt;(d))

&lt;span style="color: #BA36A5;"&gt;efit&lt;/span&gt; = nn.predict(np.row_stack(dfit))

plt.plot(d, e, &lt;span style="color: #008000;"&gt;'bo '&lt;/span&gt;)
plt.plot(dfit, efit)
plt.legend([&lt;span style="color: #008000;"&gt;'calculations'&lt;/span&gt;, &lt;span style="color: #008000;"&gt;'neural network'&lt;/span&gt;])
plt.xlabel(&lt;span style="color: #008000;"&gt;'d (&amp;#197;)'&lt;/span&gt;)
plt.ylabel(&lt;span style="color: #008000;"&gt;'energy (eV)'&lt;/span&gt;)
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
&lt;img src="/media/ob-ipython-025c1b30f565c5806510804582e91242.png"&gt; 
&lt;/p&gt;

&lt;p&gt;
This fit looks pretty good, better than we got for the Buckingham potential. Well, it probably should look better, we have many more parameters that were fitted! It is not perfect, but it could be systematically improved by increasing the number of hidden layers, and neurons in each layer. I am being a little loose here by relying on a visual assessment of the fit. To systematically improve it you would need a quantitative analysis of the errors. I also note though, that if I run the block above several times in succession, I get different fits each time. I suppose that is due to some random numbers used to initialize the fit, but sometimes the fit is about as good as the result you see above, and sometimes it is terrible.
&lt;/p&gt;

&lt;p&gt;
Ok, what is the point after all? We developed a neural network that pretty accurately captures the energy of a Cu dimer &lt;i&gt;with no knowledge&lt;/i&gt; of the physics involved. Now, EMT is not that expensive, but suppose this required 300 DFT calculations at 1 minute or more a piece? That is five hours just to get the data! With this neural network, we can quickly compute energies. For example, this shows we get about 10000 energy calculations in just 287 ms.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-ipython"&gt;%%timeit

&lt;span style="color: #BA36A5;"&gt;dfit&lt;/span&gt; = np.linspace(&lt;span style="color: #006FE0;"&gt;min&lt;/span&gt;(d), &lt;span style="color: #006FE0;"&gt;max&lt;/span&gt;(d), 10000)
&lt;span style="color: #BA36A5;"&gt;efit&lt;/span&gt; = nn.predict(np.row_stack(dfit))
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
1 loop, best of 3: 287 ms per loop
&lt;/p&gt;

&lt;p&gt;
Compare that to the time it took to compute the 300 energies with EMT
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-ipython"&gt;%%timeit
&lt;span style="color: #BA36A5;"&gt;E&lt;/span&gt; = np.array([get_e(dist) &lt;span style="color: #0000FF;"&gt;for&lt;/span&gt; dist &lt;span style="color: #0000FF;"&gt;in&lt;/span&gt; D])
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
1 loop, best of 3: 230 ms per loop
&lt;/p&gt;

&lt;p&gt;
The neural network is a lot faster than the way we get the EMT energies!
&lt;/p&gt;

&lt;p&gt;
It is true in this case we could have used a spline, or interpolating function and it would likely be even better than this Neural Network. We are aiming to get more complicated soon though. For a trimer, we will have three dimensions to worry about, and that can still be worked out in a similar fashion I think. Past that, it becomes too hard to reduce the dimensions, and this approach breaks down. Then we have to try something else. We will get to that in another post.
&lt;/p&gt;
&lt;p&gt;Copyright (C) 2017 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/2017/03/18/Modeling-a-Cu-dimer-by-EMT,-nonlinear-regression-and-neural-networks.org"&gt;org-mode source&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Org-mode version = 9.0.5&lt;/p&gt;]]></content:encoded>
    </item>
  </channel>
</rss>