<?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>Wed, 21 Oct 2026 12:16:35 GMT</pubDate>
    <generator>Quarto + legacy Blogofile feed bridge</generator>
    <sy:updatePeriod>hourly</sy:updatePeriod>
    <sy:updateFrequency>1</sy:updateFrequency>
    
    <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>One differentiable model, many tasks</title><link>https://kitchingroup.cheme.cmu.edu/posts/differentiable-models/</link><pubDate>Sun, 18 Oct 2026 00:00:00 +0000</pubDate><category>optimization</category><category>jax</category><category>discopt</category><category>difflow</category><category>digital twins</category><guid isPermaLink="true">https://kitchingroup.cheme.cmu.edu/posts/differentiable-models/</guid><description>One differentiable model, many tasks</description><content:encoded><![CDATA[ 

Skip to main content




<iframe src="https://kitchingroup.cheme.cmu.edu/posts/differentiable-models/tasks.html" title="One differentiable model, many tasks" style="width:100%; aspect-ratio:1240/920; border:0;" loading="lazy"></iframe>
<p><a href="https://kitchingroup.cheme.cmu.edu/posts/differentiable-models/tasks.html">Open the graphic full-size</a>.</p>
<section id="why-differentiable-models" class="level1">
<h1>Why differentiable models?</h1>
<p>If there is one nearly unifying principle in optimization, it is that derivatives play a central role in much of it (I know not all of it; integers are not differentiable, and you often have to estimate derivatives, etc.). Optimization means many things as illustrated above. We can use it to fit parameters, simulate, reconcile data, plan, control, design experiments and more! These all rely on the central model, and different combinations of what know, what you want, and how you get it. What we aim for in our ecosystem shown above is for a single model to be reusable across many tasks.</p>
<p>The graphic above is interactive, and you can see how the task is implemented in many of the tasks through notebook examples. We have not fully integrated everything seamlessly yet, but we are working on it and making good progress. If you find bugs or have suggestions, please let us know by opening an issue on GitHub!</p>
<!--
Draft notes, not rendered.

The idea: the ecosystem gives you a differentiable model f(x; θ). Each task attaches
a different objective and frees a different subset of x, θ and y:

- simulation: x, θ given; solve for y
- parameter estimation: x, y from data; min over θ of a loss
- data reconciliation: noisy y from data; min weighted adjustment s.t. the model
- planning: data plus Δ = ∂y/∂x replaces hand-maintained delta vectors
- control: data plus a goal; re-solve over a horizon
- design of experiments: max information over x
- optimization: min g(x)
- sensitivity and UQ: the "?" in the sketch; ∂y/∂θ and Σ_y ≈ J Σ_θ Jᵀ

One model reused across all of them is what makes it a digital twin.

Candidates for the dashed satellites: state estimation (MHE), model discrimination,
optimization under uncertainty, hybrid models, surrogates, flexibility, fault detection.

Notebooks to write (no example yet):
- discopt: data reconciliation, planning, MPC (receding horizon), state estimation,
  flexibility, fault detection
- difflow: control, design of experiments, state estimation, model discrimination,
  hybrid models, flexibility (docs pages exist for DOE, MHE, flexibility, operability)
-->


</section>

 ]]></content:encoded></item><item><title>The differentiable optimization landscape</title><link>https://kitchingroup.cheme.cmu.edu/posts/differentiable-optimization-landscape/</link><pubDate>Sat, 17 Oct 2026 00:00:00 +0000</pubDate><category>optimization</category><category>jax</category><category>discopt</category><category>difflow</category><category>pounce</category><guid isPermaLink="true">https://kitchingroup.cheme.cmu.edu/posts/differentiable-optimization-landscape/</guid><description>The differentiable optimization landscape</description><content:encoded><![CDATA[ 

Skip to main content




<iframe src="https://kitchingroup.cheme.cmu.edu/posts/differentiable-optimization-landscape/landscape.html" title="The differentiable optimization landscape" style="width:100%; aspect-ratio:1320/930; border:0;" loading="lazy"></iframe>
<p><a href="https://kitchingroup.cheme.cmu.edu/posts/differentiable-optimization-landscape/landscape.html">Open the graphic full-size</a>.</p>
<section id="the-big-pivot-into-optimization" class="level1">
<h1>The big pivot into optimization</h1>
<p>Over the past year I have been making a big pivot in research into optimization. I have always learned by doing, and this post documents some of that journey. This transition has been happening for a while, really starting about a decade ago when I started using machine learning to build surrogate models for DFT. That work led to this <a href="https://kitchingroup.cheme.cmu.edu/papers/kitchin2025/">perspective</a> on differentiable programming in scientific programming. That has led to this year’s work in differentiable optimization. My goal is to build an end-to-end differentiable optimization stack that integrates differentiable programming, differentiable solvers, and differentiable surrogates.</p>
<section id="automatic-differentiation-and-program-based-optimization" class="level2">
<h2 class="anchored" data-anchor-id="automatic-differentiation-and-program-based-optimization">Automatic differentiation and program-based optimization</h2>
<p>Coming from the <a href="https://kitchingroup.cheme.cmu.edu/papers/topics/neural-network-potentials/">machine learned potential</a> field, which uses pytorch and jax with regular Python to solve their optimization problems, that is the approach I started first in <a href="https://github.com/jkitchin/differentiable-flowsheets">difflow</a>. The idea was pretty simple, you write regular Python code, and use jax solvers to solve the problem. I found this appealing because I have decades of programming experience in Python. Of course I was familiar with Pyomo, and although it is written in Python, it is very unPythonic with its operator overloading and heavy object-oriented programming. A year ago I did not fully understand how the solves that Pyomo uses actually have automatic differentiation and I thought jax would enable something they can’t do in cases where you don’t have equations. Agentic coding played a huge role in getting difflow off the ground, I started it shortly after Opus 4.5 was released in Nov 2025 and quickly got a proof of concept working. I learned about sequential modular solves, and that I could reformulate a residual approach that also allows a simultaneous solve. difflow is still being actively developed and fills a part of the landscape that is not easily filled by other tools.</p>
</section>
<section id="interior-point-solvers-and-differentiable-programming" class="level2">
<h2 class="anchored" data-anchor-id="interior-point-solvers-and-differentiable-programming">Interior point solvers and differentiable programming</h2>
<p>In February 2026 I set out a challenge at a GRC conference that as a community we were not thinking big enough about agentic coding. I suggested building a new version of Ipopt in Rust, with a new linear algebra library. I actually thought at the time it would probably not work since I was not familiar with Rust, or interior point methods, and that it would establish the boundaries of what is possible with agentic coding. Many people did not think this would work either, but I tried it anyway, and week after week it kept working better. I did have to abandon the first attempt (ripopt) and start over, but pounce emerged from this as one of the best open source interior point solvers, and built a small community of contributors. pounce is where I learned about the way automatic differentiation works in equation-oriented solvers, and that we can use sensitivity analysis to get differentiable solvers in Python.</p>
</section>
<section id="algebraic-modeling-languages-and-mixed-integer-programming" class="level2">
<h2 class="anchored" data-anchor-id="algebraic-modeling-languages-and-mixed-integer-programming">Algebraic modeling languages and mixed integer programming</h2>
<p>At that same GRC, I build <a href="https://github.com/jkitchin/jaxsr">jaxsr</a> as a symbolic regression modeling tool. This got me interested in mixed integer programming and global optimization, and I started <a href="https://github.com/jkitchin/discopt">discopt</a>. This forced me to get comfortable with the need for an algebraic modeling language and the kinds of analysis that are possible. My original goal in discopt was to also build a full Rust solver for mixed integer programming that leveraged pounce for the NLP parts. I tried that for a while, but currently outsource the mixed integer programming to the open source solver HiGHS. Several people have asked why didn’t I just use Pyomo? The simple answer is I learn by doing, and this is how I did it. I also wanted to try to address some limitations I have heard about Pyomo, primarily performance. For large problems it can take longer to build your model than to solve it. discopt addresses some of those issues, and let me develop part of the ecosystem in the landscape above.</p>
</section>
<section id="differentiable-surrogate-models-and-uncertainty-quantification" class="level2">
<h2 class="anchored" data-anchor-id="differentiable-surrogate-models-and-uncertainty-quantification">Differentiable surrogate models and uncertainty quantification</h2>
<p>In between these efforts we had a need for differentiable surrogate models. I learned in <a href="https://kitchingroup.cheme.cmu.edu/papers/sunshine2025/">this paper</a> that some surrogate models are MIP representable which has desirable properties for optimization. In parallel efforts in molecular simulation we were developing <a href="https://kitchingroup.cheme.cmu.edu/papers/topics/uncertainty-quantification/">uncertainty quantification</a> and this started making its way into our efforts in differentiable surrogates with uncertainty quantification. This means we can build surrogates that have properties we want, e.g.&nbsp;monotonicity, convexity, and uncertainty quantification, MIP representable, etc. and use them in optimization. This is a new paradigm that is not easy with other tools.</p>
</section>
</section>
<section id="why-do-we-need-new-solvers-at-all" class="level1">
<h1>Why do we need new solvers at all?</h1>
<p>There are a plethora of options, very good commercial ones, and open-source ones. I work in higher education, and I think it is important that free, open-source solvers exist for students and researchers. I got into computational catalysis as a student because there was a free code I could use, read, modify and learn from. It is often not possible to do this with closed codes. Even if my codes aren’t as good as other codes, if they open the door for others to learn then a part of the mission is accomplished.</p>
<p>New starts mean new things are possible. pounce is more comprehensive than Ipopt in interior point methods, with integrated sensitivity and other great features that are 1) pip-installable, 2) capable of running in your browser, and with interfaces to use in other interfaces. discopt led to new integrations of ideas, and eventually to the development of jax-go which bridges the gap between program and equation-based optimization. difflow is a new way to think about optimization, and the differentiable surrogates are a new way to think about optimization with uncertainty quantification.</p>
<p>Agentic optimization is a new thing with these solvers. The tight integration of agentic coding with differentiable programming and differentiable solvers is a new paradigm that is more possible with these new solvers because they were designed with this in mind. We have integrated agentic debugging, and <a href="https://youtube.com/live/9JW6elWTYhk">agentic optimization</a>.</p>
</section>
<section id="what-is-next" class="level1">
<h1>What is next?</h1>
<p>I have laid out an ambitious vision for the end-to-end differentiable optimization stack, and I think we are making good progress on it. It is a lot, and we don’t use all of them every day. Some of these tools are stabilizing. Feral is pretty stable now. I probably work on pounce once a week or so. discopt and difflow are still changing fast. It would probably be a lie to say there won’t be new packages coming. I still have many private projects that haven’t seen the light of day yet, but will eventually.</p>
<p>We (the Kitchin group) are beginning to enter a new era of applications. Inspired by the algebraic modeling language we made <a href="https://github.com/jkitchin/discopt-mkm">discopt-mkm</a> for microkinetic modeling, and <a href="https://github.com/jkitchin/discopt-calphad">discopt-calphad</a> for alloy phase behavior. <a href="https://github.com/jkitchin/discopt-doe">discopt-doe</a> is a new direction in design of experiments. We have projects in process modeling, alloy design, rare earth separation, and more.</p>
<p>It is always dangerous to predict the future, but I think 5 years from now we will have a much more integrated landscape of differentiable optimization tools. Right now programmatic automatic differentiation is mostly only available as addons like jax, pytorch in Python, and something like that in Julia. AD is coming to Rust as a native feature in a few years, and predict this will be a game changer for optimization. The pace of development I have demonstrated this year means it will probably only take a year or so to transition from a Python or Julia focused solution space to Rust if we decide to. These changes come with 20/20 hindsight and lessons learned from everything we know now, so they happen very quickly. I think the next 5 years will be very exciting for differentiable optimization.</p>
<p>I still think we are not thinking big enough as a community. I am grateful to a few people who made substantial contributions to these codes: Devin Griffith for major contributions to sensitivity in pounce, Germán Heim for integrating pounce into his modeling language oximo. Milan Rother forked feral into <a href="https://milanrother.com/stack/rslab/">RSLAB</a> and was the inspiration for pounce in the browser. Victor Alves for using pounce before anyone else. David Bernal for major contributions in global optimization in discopt, and ideas linking it to pounce. All this work was done by a very small number of people, and I think we can do better as a community.</p>
<p>It is an open challenge to figure out how to sustain these efforts. Pyomo and IDAES were built over two decades, with generous federal funding. This is a challenge today as that funding becomes more scarce. We need new models to support this kind of work that balances keeping open-access to the code, and generating revenue to support the people who build it.</p>
<p>Along this journey I have come to appreciate that 1) I still have a lot to learn in this space, and 2) there is not one tool to rule in optimization; there are many ways to solve problems, and some ways are better than others for certain problems. That means you have to have a landscape of pathways to choose, and historically I have chosen the roads less traveled. I have learned a lot along the way, and I hope this post helps others to learn from my journey.</p>


</section>

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

Skip to main content




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


</section>

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

Skip to main content




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


</section>

 ]]></content:encoded></item><item>
      <title>New publication - Multiscale Perturbation Methods for Dynamic/Programmable Catalysis</title>
      <link>https://kitchingroup.cheme.cmu.edu/blog/2025/10/31/New-publication-Multiscale-Perturbation-Methods-for-Dynamic-Programmable-Catalysis</link>
      <pubDate>Fri, 31 Oct 2025 10:44:03 EDT</pubDate>
      <category><![CDATA[publication]]></category>
      <category><![CDATA[news]]></category>
      <guid isPermaLink="false">hM2Y5YylQb-PGcHjOGprZ0EXSnY=</guid>
      <description>New publication - Multiscale Perturbation Methods for Dynamic/Programmable Catalysis</description>
      <content:encoded><![CDATA[


&lt;p&gt;
&lt;figure&gt;&lt;img src="/media/date-31-10-2025-time-10-43-49.png"&gt;&lt;/figure&gt; 
&lt;/p&gt;


&lt;p&gt;
In our recent paper, Multiscale Perturbation Methods for Dynamic/Programmable Catalysis, Carolina Colombo Tedesco, Carl Laird, Aditya Khair, and I developed an analytical framework for modeling dynamic catalysis—where catalyst binding energies are periodically modulated to overcome Sabatier limitations and enhance reaction rates. Building on our previous boundary value problem approach for simulating cyclic steady states, we applied multiscale perturbation theory to decompose the system response into slow (average) and fast (oscillatory) components. This allowed us to derive closed-form expressions for surface coverages and limit cycles without costly numerical integration. The method accurately reproduces results from full simulations for linear systems when the forcing frequency is high and amplitudes are small to moderate, providing insight into how oscillating catalytic systems behave and when they deviate from quasi-static assumptions. Beyond dynamic catalysis, this analytical framework may be useful for understanding other periodically driven systems where time-scale separation enables simple yet powerful approximations.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-bibtex"&gt;&lt;span style="color: #006699;"&gt;@article&lt;/span&gt;{&lt;span style="color: #D0372D;"&gt;tedesco-2025-multis-pertur&lt;/span&gt;,
  &lt;span style="color: #BA36A5;"&gt;author&lt;/span&gt; =       {Carolina Colombo Tedesco and Carl D. Laird and John R. Kitchin
                  and Aditya S. Khair},
  &lt;span style="color: #BA36A5;"&gt;title&lt;/span&gt; =        {Multiscale Perturbation Methods for Dynamic/programmable
                  Catalysis},
  &lt;span style="color: #BA36A5;"&gt;journal&lt;/span&gt; =      {Industrial \&amp;amp; Engineering Chemistry Research},
  &lt;span style="color: #BA36A5;"&gt;volume&lt;/span&gt; =       {nil},
  &lt;span style="color: #BA36A5;"&gt;number&lt;/span&gt; =       {nil},
  &lt;span style="color: #BA36A5;"&gt;pages&lt;/span&gt; =        {acs.iecr.5c03023},
  &lt;span style="color: #BA36A5;"&gt;year&lt;/span&gt; =         2025,
  &lt;span style="color: #BA36A5;"&gt;doi&lt;/span&gt; =          {&lt;span style="color: #006DAF; text-decoration: underline;"&gt;10.1021/acs.iecr.5c03023&lt;/span&gt;},
  &lt;span style="color: #BA36A5;"&gt;url&lt;/span&gt; =          {&lt;span style="color: #006DAF; text-decoration: underline;"&gt;http://dx.doi.org/10.1021/acs.iecr.5c03023&lt;/span&gt;},
  &lt;span style="color: #BA36A5;"&gt;DATE_ADDED&lt;/span&gt; =   {Fri Oct 31 10:39:19 2025},
}
&lt;/pre&gt;
&lt;/div&gt;


&lt;p&gt;
&lt;script type='text/javascript' src='https://d1bxh8uas1mnw7.cloudfront.net/assets/embed.js'&gt;&lt;/script&gt;
&lt;div data-badge-type='medium-donut' class='altmetric-embed' data-badge-details='right' data-doi='10.1021/acs.iecr.5c03023'&gt;&lt;/div&gt;
&lt;/p&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/10/31/New-publication---Multiscale-Perturbation-Methods-for-Dynamic/Programmable-Catalysis.org"&gt;org-mode source&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Org-mode version = 9.7.34&lt;/p&gt;]]></content:encoded>
    </item>
    <item>
      <title>New publication - How Electrolyte pH Affects the Oxygen Reduction Reaction</title>
      <link>https://kitchingroup.cheme.cmu.edu/blog/2025/10/02/New-publication-How-Electrolyte-pH-Affects-the-Oxygen-Reduction-Reaction</link>
      <pubDate>Thu, 02 Oct 2025 07:26:59 EDT</pubDate>
      <category><![CDATA[publication]]></category>
      <category><![CDATA[news]]></category>
      <guid isPermaLink="false">V91eOL6-GSC6b5WspdCcQoA-NCg=</guid>
      <description>New publication - How Electrolyte pH Affects the Oxygen Reduction Reaction</description>
      <content:encoded><![CDATA[


&lt;p&gt;
&lt;figure&gt;&lt;img src="/media/orr-ph.png"&gt;&lt;/figure&gt; 
&lt;/p&gt;

&lt;p&gt;
In our recent work published in the Journal of the American Chemical Society, we investigated how electrolyte pH affects the oxygen reduction reaction (ORR) - a critical process in fuel cells and batteries. Working with an outstanding team including Jay Bender, Rohan Sanspeur, and colleagues from UT Austin, we tackled a long-standing puzzle: why does changing pH dramatically affect ORR rates on some catalysts (like Au) but barely affect others (like Pt)?
&lt;/p&gt;

&lt;p&gt;
Through careful electrochemical experiments, we measured ORR activity across different metals in both acidic and alkaline conditions. The results were striking - Au catalysts showed dramatically increased activity when moving from acid to base, while Pt remained essentially unchanged. Our kinetic analysis revealed that the rate-determining steps don't actually change with pH, challenging previous explanations.
&lt;/p&gt;

&lt;p&gt;
The breakthrough came from combining these experiments with density functional theory (DFT) calculations. We discovered that electric field effects provide a unifying explanation. When pH increases, the interfacial electric field becomes more negative. This field change strongly stabilizes the key reaction intermediate (*O₂) on weakly binding metals like Au, dramatically lowering activation barriers. On strongly binding metals like Pt, the reaction intermediates are much less sensitive to electric fields, explaining their pH-independent behavior.
&lt;/p&gt;

&lt;p&gt;
This computational insight allowed us to extend our understanding to other metals (Ag, Ir, Ru, Pd), confirming the general principle: pH effects depend on how field-sensitive the rate-determining intermediates are.
&lt;/p&gt;

&lt;p&gt;
Our work demonstrates the power of combining rigorous experimental kinetics with advanced computational modeling. By understanding these fundamental electric field effects, we can now predict and potentially engineer pH-dependent catalytic behavior - opening new avenues for optimizing electrochemical energy conversion devices.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-bibtex"&gt;&lt;span style="color: #006699;"&gt;@article&lt;/span&gt;{&lt;span style="color: #D0372D;"&gt;bender-2025-how-elect&lt;/span&gt;,
  &lt;span style="color: #BA36A5;"&gt;author&lt;/span&gt; = {Jay T. Bender and Rohan Yuri Sanspeur and Nicolas Bueno Ponce and Angel E. Valles and Alyssa K. Uvodich and Delia J. Milliron and John R. Kitchin and Joaquin Resasco},
  &lt;span style="color: #BA36A5;"&gt;title&lt;/span&gt; = {How Electrolyte Ph Affects the Oxygen Reduction Reaction},
  &lt;span style="color: #BA36A5;"&gt;journal&lt;/span&gt; = {Journal of the American Chemical Society},
  &lt;span style="color: #BA36A5;"&gt;volume&lt;/span&gt; = {nil},
  &lt;span style="color: #BA36A5;"&gt;number&lt;/span&gt; = {nil},
  &lt;span style="color: #BA36A5;"&gt;pages&lt;/span&gt; = {jacs.5c14208},
  &lt;span style="color: #BA36A5;"&gt;year&lt;/span&gt; = {2025},
  &lt;span style="color: #BA36A5;"&gt;doi&lt;/span&gt; = {&lt;span style="color: #006DAF; text-decoration: underline;"&gt;10.1021/jacs.5c14208&lt;/span&gt;},
  &lt;span style="color: #BA36A5;"&gt;url&lt;/span&gt; = {&lt;span style="color: #006DAF; text-decoration: underline;"&gt;http://dx.doi.org/10.1021/jacs.5c14208&lt;/span&gt;},
  &lt;span style="color: #BA36A5;"&gt;DATE_ADDED&lt;/span&gt; = {Thu Oct 2 07:22:51 2025},
}
&lt;/pre&gt;
&lt;/div&gt;


&lt;p&gt;
&lt;script type='text/javascript' src='https://d1bxh8uas1mnw7.cloudfront.net/assets/embed.js'&gt;&lt;/script&gt;
&lt;div data-badge-type='medium-donut' class='altmetric-embed' data-badge-details='right' data-doi='10.1021/jacs.5c14208'&gt;&lt;/div&gt;
&lt;/p&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/10/02/New-publication---How-Electrolyte-pH-Affects-the-Oxygen-Reduction-Reaction.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>New publication - Uncertainty Quantification in Graph Neural Networks With    Shallow Ensembles</title>
      <link>https://kitchingroup.cheme.cmu.edu/blog/2025/09/27/New-publication-Uncertainty-Quantification-in-Graph-Neural-Networks-With-Shallow-Ensembles</link>
      <pubDate>Sat, 27 Sep 2025 11:50:48 EDT</pubDate>
      <category><![CDATA[publication]]></category>
      <category><![CDATA[news]]></category>
      <guid isPermaLink="false">s_fSx3dppxsWwS3QKPjeYvxDS7A=</guid>
      <description>New publication - Uncertainty Quantification in Graph Neural Networks With    Shallow Ensembles</description>
      <content:encoded><![CDATA[


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

&lt;p&gt;
In our latest work, we tackled a critical challenge in materials modeling: ensuring that AI models don't just make predictions, but also tell us &lt;i&gt;how confident&lt;/i&gt; they are in those predictions. When Graph Neural Networks (GNNs) encounter new, "out-of-domain" materials they haven't seen during training, their predictions can become unreliable, and it's tough to know when that's happening. So, we integrated a clever, lightweight technique called Direct Propagation of Shallow Ensembles (DPOSE) into a GNN model called SchNet. Essentially, DPOSE allows the model to estimate its own uncertainty efficiently. Our findings showed that this approach is really effective at flagging when the model is venturing into unfamiliar territory, giving us higher uncertainty for novel molecules or material structures across various datasets. While it performed well, we also learned about its limitations in distinguishing very subtle structural differences. Ultimately, this work is a step towards building more trustworthy AI for materials discovery, paving the way for smarter active learning strategies where the AI itself helps decide what new data to explore.
&lt;/p&gt;


&lt;div class="org-src-container"&gt;
&lt;pre class="src src-bibtex"&gt;&lt;span style="color: #006699;"&gt;@article&lt;/span&gt;{&lt;span style="color: #D0372D;"&gt;vinchurkar-2025-uncer-quant&lt;/span&gt;,
  &lt;span style="color: #BA36A5;"&gt;author&lt;/span&gt; =       {Tirtha Vinchurkar and Kareem Abdelmaqsoud and John R Kitchin},
  &lt;span style="color: #BA36A5;"&gt;title&lt;/span&gt; =        {Uncertainty Quantification in Graph Neural Networks With
                  Shallow Ensembles},
  &lt;span style="color: #BA36A5;"&gt;journal&lt;/span&gt; =      {Machine Learning: Science and Technology},
  &lt;span style="color: #BA36A5;"&gt;volume&lt;/span&gt; =       {nil},
  &lt;span style="color: #BA36A5;"&gt;number&lt;/span&gt; =       {nil},
  &lt;span style="color: #BA36A5;"&gt;pages&lt;/span&gt; =        {nil},
  &lt;span style="color: #BA36A5;"&gt;year&lt;/span&gt; =         2025,
  &lt;span style="color: #BA36A5;"&gt;doi&lt;/span&gt; =          {&lt;span style="color: #006DAF; text-decoration: underline;"&gt;10.1088/2632-2153/ae0bf0&lt;/span&gt;},
  &lt;span style="color: #BA36A5;"&gt;url&lt;/span&gt; =          {&lt;span style="color: #006DAF; text-decoration: underline;"&gt;https://doi.org/10.1088/2632-2153/ae0bf0&lt;/span&gt;},
  &lt;span style="color: #BA36A5;"&gt;DATE_ADDED&lt;/span&gt; =   {Sat Sep 27 11:42:36 2025},
}

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


&lt;p&gt;
&lt;script type='text/javascript' src='https://d1bxh8uas1mnw7.cloudfront.net/assets/embed.js'&gt;&lt;/script&gt;
&lt;div data-badge-type='medium-donut' class='altmetric-embed' data-badge-details='right' data-doi='10.1088/2632-2153/ae0bf0'&gt;&lt;/div&gt;
&lt;/p&gt;

&lt;p&gt;
Curious about using an LLM to interact with this paper? Check out
&lt;/p&gt;

&lt;iframe width="560" height="315" src="https://www.youtube.com/embed/kHsbUjgCGBY?si=bejDCZJqdXTExL0J" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen&gt;&lt;/iframe&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/09/27/New-publication---Uncertainty-Quantification-in-Graph-Neural-Networks-With-Shallow-Ensembles.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>New publication - Towards Agentic Science for Advancing Scientific Discovery</title>
      <link>https://kitchingroup.cheme.cmu.edu/blog/2025/09/16/New-publication-Towards-Agentic-Science-for-Advancing-Scientific-Discovery</link>
      <pubDate>Tue, 16 Sep 2025 13:38:54 EDT</pubDate>
      <category><![CDATA[publication]]></category>
      <category><![CDATA[news]]></category>
      <guid isPermaLink="false">L2nYbbOG0rMQ9_4L2K7M3nJNrlI=</guid>
      <description>New publication - Towards Agentic Science for Advancing Scientific Discovery</description>
      <content:encoded><![CDATA[


&lt;p&gt;
In our new paper published in Nature Machine Intelligence, my colleagues Hongliang Xin, Heather Kulik, and I explore what we call "agentic science" – a new paradigm where AI agents can (semi-)autonomously conduct scientific research.
&lt;/p&gt;

&lt;p&gt;
Scientific discovery has evolved through distinct eras: from early empirical observations and theoretical frameworks like Newtonian mechanics, through the computational modeling revolution, to today's data science approaches. We argue that we're now entering the age of agentic science, where AI systems don't just analyze data but can independently reason, plan experiments, and interact with both digital tools and physical laboratory equipment.
&lt;/p&gt;

&lt;p&gt;
What makes these AI agents special is their capacity for independent agency. Built around large language models that can process text, images, and structured data, they can actively learn, integrate with external tools, and think strategically about long-term research goals. Systems like Coscientist can already interpret natural language requests and autonomously operate lab equipment, while A-Lab represents a fully autonomous materials synthesis laboratory.
&lt;/p&gt;

&lt;p&gt;
However, we're honest about the challenges. These systems can "hallucinate" – producing convincing but incorrect information – and they're sensitive to how questions are phrased. We also lack standardized ways to evaluate their performance, and they consume significant computational resources.
&lt;/p&gt;

&lt;p&gt;
The key to success lies in maintaining human oversight while leveraging AI for high-throughput tasks. With proper safeguards, transparent documentation, and ethical considerations, agentic AI could dramatically accelerate scientific discovery while actually improving reproducibility by systematically analyzing literature and identifying research gaps.
&lt;/p&gt;

&lt;p&gt;
We believe this represents a fundamental shift in how science gets done – not replacing human scientists, but creating powerful human-AI partnerships that could unlock new pathways to discovery.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-bibtex"&gt;&lt;span style="color: #006699;"&gt;@article&lt;/span&gt;{&lt;span style="color: #D0372D;"&gt;xin-2025-towar-agent&lt;/span&gt;,
  &lt;span style="color: #BA36A5;"&gt;author&lt;/span&gt; =       {Hongliang Xin and John R. Kitchin and Heather J. Kulik},
  &lt;span style="color: #BA36A5;"&gt;title&lt;/span&gt; =        {Towards Agentic Science for Advancing Scientific Discovery},
  &lt;span style="color: #BA36A5;"&gt;journal&lt;/span&gt; =      {Nature Machine Intelligence},
  &lt;span style="color: #BA36A5;"&gt;volume&lt;/span&gt; =       {nil},
  &lt;span style="color: #BA36A5;"&gt;number&lt;/span&gt; =       {nil},
  &lt;span style="color: #BA36A5;"&gt;pages&lt;/span&gt; =        {nil},
  &lt;span style="color: #BA36A5;"&gt;year&lt;/span&gt; =         2025,
  &lt;span style="color: #BA36A5;"&gt;doi&lt;/span&gt; =          {&lt;span style="color: #006DAF; text-decoration: underline;"&gt;10.1038/s42256-025-01110-x&lt;/span&gt;},
  &lt;span style="color: #BA36A5;"&gt;url&lt;/span&gt; =          {&lt;span style="color: #006DAF; text-decoration: underline;"&gt;https://doi.org/10.1038/s42256-025-01110-x&lt;/span&gt;},
  &lt;span style="color: #BA36A5;"&gt;DATE_ADDED&lt;/span&gt; =   {Tue Sep 16 13:36:03 2025},
}
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
&lt;script type='text/javascript' src='https://d1bxh8uas1mnw7.cloudfront.net/assets/embed.js'&gt;&lt;/script&gt;
&lt;div data-badge-type='medium-donut' class='altmetric-embed' data-badge-details='right' data-doi='10.1038/s42256-025-01110-x'&gt;&lt;/div&gt;
&lt;/p&gt;

&lt;iframe width="560" height="315" src="https://www.youtube.com/embed/t_A_TAa3t1A?si=mYM2UTghkiL0nUJO" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen&gt;&lt;/iframe&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/09/16/New-publication---Towards-Agentic-Science-for-Advancing-Scientific-Discovery.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>
    </channel>
</rss>