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

Skip to main content




<p>For more than 25 years I have built the tools that make the environment I want to work in: Python packages for molecular simulation, Emacs and org-mode, org-ref, scimax, pycse, and most recently scimax for VS Code and a stack of optimization software. AI has not changed that for me. You might think of me as a software beaver, and it changed how big a dam one person can build. In the past year there were about 10,400 commits across scimax, discopt, pounce, feral and difflow, and about 76% of them were co-authored with an AI coding agent. I wrote far fewer of the lines than I used to. I wrote the specifications, I reviewed the changes, and I wrote or approved the tests that decide whether a change is accepted.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://kitchingroup.cheme.cmu.edu/posts/agentic-science/img/software-beaver.svg" class="img-fluid quarto-figure quarto-figure-center figure-img" style="width:55.0%"></p>
</figure>
</div>
<p>That shift is the subject of this post. When an agent can write code, read papers and run calculations, the bottleneck moves. It used to be typing code; now it is deciding what to build and verifying that it is right. I have written about this before in the context of self-driving laboratories <a href="https://kitchingroup.cheme.cmu.edu/papers/kitchin2025b/index.html">(Kitchin, 2025)</a>, and with Hongliang Xin and Heather Kulik about what agentic science could look like more broadly <a href="https://kitchingroup.cheme.cmu.edu/papers/xin2025/index.html">(Xin, Kitchin and Kulik, 2025)</a>. Here I want to be concrete in how we are implementing these ideas. Our approach is mostly embodied in software, so this is a tour of that software and of the ideas that hold it together.</p>
<section id="an-agent-is-only-as-good-as-its-environment" class="level2">
<h2 class="anchored" data-anchor-id="an-agent-is-only-as-good-as-its-environment">An agent is only as good as its environment</h2>
<p>The simplest way to describe our approach is that we spend our effort on the environments that we work in and that the agent works in, rather than on the agent itself. Three ideas run through everything below.</p>
<p>The first is that plain text is ready for agents. Our notes, code, data and manuscripts live in org-mode and Markdown files that an agent can read, search and edit with the same tools it uses on source code. Nothing has to be exported or scraped first.</p>
<p>The second is that abilities should be installable. When an agent needs to know how to set up a VASP calculation or diagnose a stalled interior point solve, that knowledge should not live in a prompt someone pasted once. It should ship as a skill or an MCP server, versioned alongside the code it describes, and be installable with one command.</p>
<p>The third, and the most important, is that software should report its work. Agents are fluent, but fluency is not correctness. The tools we build for agents return structured results, validate their input, raise typed errors instead of returning a plausible wrong answer, and are tested against references we trust. That gives us something to check the agent’s claims against.</p>
<p>The diagram shows how the pieces fit together. You set the goals and check the results. The agent does the work through abilities that knowhow installs, using four groups of tools. Underneath is plain text, and around all of it are checks the agent cannot talk its way past. The rest of the post follows the diagram from top to bottom.</p>
<style>
.stack-fig { --ink:#142033; --muted:#5b6b82; --line:#c9d6e8; --card:#f3f7fd; --band:#dbe8f8; --accent:#2563b8; --guard:#fdf3e7; --guard-line:#d9822b; }
body.quarto-dark .stack-fig { --ink:#dbe5f3; --muted:#94a6c0; --line:#2b3d5a; --card:#101b2e; --band:#16243b; --accent:#7cb4ff; --guard:#2a1f12; --guard-line:#e0a060; }
.stack-fig svg { width:100%; height:auto; font-family:"IBM Plex Sans",system-ui,sans-serif; }
.stack-fig .box { fill:var(--card); stroke:var(--line); stroke-width:1.2; }
.stack-fig .band { fill:var(--band); stroke:var(--line); stroke-width:1.2; }
.stack-fig .agent { fill:var(--card); stroke:var(--accent); stroke-width:2; }
.stack-fig .guard { fill:var(--guard); stroke:var(--guard-line); stroke-width:1.5; }
.stack-fig text { fill:var(--ink); font-size:13px; }
.stack-fig .h { font-weight:600; font-size:15px; }
.stack-fig .pkg { font-weight:600; fill:var(--accent); }
.stack-fig .m { fill:var(--muted); font-size:12.5px; }
.stack-fig .code { font-family:"IBM Plex Mono",ui-monospace,monospace; font-size:11.5px; fill:var(--muted); }
.stack-fig .arrow { stroke:var(--muted); stroke-width:1.5; fill:none; marker-end:url(#stack-arrow); }
.stack-fig .arrowhead { fill:var(--muted); }
</style>
<figure class="stack-fig figure">
<svg viewbox="0 0 760 470" aria-labelledby="stack-title stack-desc">
<title id="stack-title">Our agentic science stack</title>
<desc id="stack-desc">A person sets goals and checks results. An AI agent works through capabilities installed by knowhow, using four groups of tools: scimax for VS Code, litdb and crucible, fairchem-mcp and discopt-calphad, and difflow, pounce, discopt and plugins. All of it rests on plain-text files and is checked by tests, benchmarks, schema validation and citation checks.</desc>
<defs><marker id="stack-arrow" viewbox="0 0 10 10" refx="9" refy="5" markerwidth="7" markerheight="7" orient="auto-start-reverse"><path class="arrowhead" d="M0,0 L10,5 L0,10 z"></path></marker></defs>

<rect class="box" x="10" y="8" width="740" height="40" rx="8"></rect>
<text class="h" x="380" y="33" text-anchor="middle">You: set goals, decide what matters, check results</text>

<path class="arrow" d="M330,50 L330,72"></path>
<text class="m" x="322" y="65" text-anchor="end">specs and goals</text>
<path class="arrow" d="M430,72 L430,50"></path>
<text class="m" x="438" y="65">results and evidence</text>

<rect class="agent" x="10" y="76" width="740" height="40" rx="8"></rect>
<text class="h" x="380" y="101" text-anchor="middle">AI agent (Claude Code, Codex, Gemini, …)</text>

<rect class="band" x="10" y="124" width="740" height="36" rx="8"></rect>
<text x="380" y="147" text-anchor="middle"><tspan class="pkg">knowhow</tspan>  installs skills · subagents · commands · hooks · MCP servers</text>

<path class="arrow" d="M98,160 L98,180"></path>
<path class="arrow" d="M286,160 L286,180"></path>
<path class="arrow" d="M474,160 L474,180"></path>
<path class="arrow" d="M662,160 L662,180"></path>

<rect class="box" x="10" y="184" width="176" height="150" rx="8"></rect>
<text class="h" x="98" y="208" text-anchor="middle">Workbench</text>
<text class="pkg" x="98" y="230" text-anchor="middle">scimax for VS Code</text>
<text class="m" x="98" y="254" text-anchor="middle">notes · agenda · code</text>
<text class="m" x="98" y="272" text-anchor="middle">cite · publish · slides</text>
<text class="code" x="98" y="314" text-anchor="middle">CLI + 4 skills</text>

<rect class="box" x="198" y="184" width="176" height="150" rx="8"></rect>
<text class="h" x="286" y="208" text-anchor="middle">Knowledge</text>
<text class="pkg" x="286" y="230" text-anchor="middle">litdb · crucible</text>
<text class="m" x="286" y="254" text-anchor="middle">search the literature</text>
<text class="m" x="286" y="272" text-anchor="middle">papers → linked org wiki</text>
<text class="code" x="286" y="314" text-anchor="middle">MCP + JSON CLI</text>

<rect class="box" x="386" y="184" width="176" height="150" rx="8"></rect>
<text class="h" x="474" y="208" text-anchor="middle">Computation</text>
<text class="pkg" x="474" y="230" text-anchor="middle">fairchem-mcp</text>
<text class="pkg" x="474" y="248" text-anchor="middle">discopt-calphad</text>
<text class="m" x="474" y="272" text-anchor="middle">steerable simulations</text>
<text class="m" x="474" y="290" text-anchor="middle">certified phase diagrams</text>
<text class="code" x="474" y="314" text-anchor="middle">MCP + certificates</text>

<rect class="box" x="574" y="184" width="176" height="150" rx="8"></rect>
<text class="h" x="662" y="208" text-anchor="middle">Optimization</text>
<text class="pkg" x="662" y="230" text-anchor="middle">difflow · pounce</text>
<text class="pkg" x="662" y="248" text-anchor="middle">discopt + plugins</text>
<text class="m" x="662" y="272" text-anchor="middle">flowsheets · NLP · MINLP</text>
<text class="code" x="662" y="314" text-anchor="middle">MCP + solve reports</text>

<rect class="band" x="10" y="346" width="740" height="40" rx="8"></rect>
<text x="380" y="371" text-anchor="middle"><tspan font-weight="600">Plain text underneath:</tspan> org · Markdown · BibTeX · JSON reports</text>

<rect class="guard" x="10" y="398" width="740" height="62" rx="8"></rect>
<text class="h" x="380" y="422" text-anchor="middle">Checks the agent cannot talk its way past</text>
<text class="m" x="380" y="444" text-anchor="middle">tests · oracle comparisons · public benchmarks · schema validation · citation checks</text>
</svg>
</figure>

</section>
<section id="the-workbench-scimax-for-vs-code" class="level2">
<h2 class="anchored" data-anchor-id="the-workbench-scimax-for-vs-code">The workbench: scimax for VS Code</h2>
<p><a href="https://github.com/jkitchin/scimax_vscode">scimax for VS Code</a> is where the work happens. It brings the scimax environment I built in Emacs to VS Code, and it is one plain-text environment for the whole research cycle. I think in it, with a journal, notes, an agenda, and a searchable database of every org file I have. I compute in it, with runnable Python and Jupyter code blocks inside the document. I write in it, with org-ref citations, BibTeX tools and manuscript publishing for journal submission. I publish from it, and I present from it, using Marp decks with a pen, a laser pointer, a timer and live Python.</p>
<p>Because all of this is plain text, an agent can work with it as easily as I can. A TODO is just a headline in an org file, and scimax gathers those headlines into an agenda. Claude can read that agenda and update it too.</p>
<div class="quarto-layout-panel" data-layout-ncol="2">
<div class="quarto-layout-row">
<div class="quarto-layout-cell" style="flex-basis: 50.0%;justify-content: flex-start;">
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://kitchingroup.cheme.cmu.edu/posts/agentic-science/img/scimax-org-todo.png" class="img-fluid figure-img"></p>
<figcaption>TODOs are plain-text headlines in an org file…</figcaption>
</figure>
</div>
</div>
<div class="quarto-layout-cell" style="flex-basis: 50.0%;justify-content: flex-start;">
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://kitchingroup.cheme.cmu.edu/posts/agentic-science/img/scimax-agenda.png" class="img-fluid figure-img"></p>
<figcaption>…that scimax gathers into an agenda (demo data).</figcaption>
</figure>
</div>
</div>
</div>
</div>
<p>The agent-facing part of scimax is a command-line tool and four skills. The <code>scimax</code> CLI covers the agenda, search, the org database, the journal, tasks and projects, citation checking, export, tangling, publishing and slide builds. <code>scimax skill install</code> adds skills that teach an agent how to use it, so it can act as an executive assistant over my notes: a morning briefing, triaging tasks, finding what is blocked, or working out the critical path through a project. Sites built with <code>scimax publish</code> can include an “Ask” chat that finds the relevant sections and writes an answer with citations, using a language model that runs in the reader’s browser, so there is no server to maintain. This site has one. scimax for VS Code has had 838 commits since January 2026, about 72% of them co-authored with AI.</p>
</section>
<section id="giving-agents-abilities-knowhow" class="level2">
<h2 class="anchored" data-anchor-id="giving-agents-abilities-knowhow">Giving agents abilities: knowhow</h2>
<p>The skills that come with scimax are one example of a broader pattern, and <a href="https://github.com/jkitchin/knowhow">knowhow</a> is how we manage it. knowhow is a package manager for agent capabilities. <code>uvx knowhow add owner/repo --skill name</code> installs skills, slash commands, subagents, hooks and MCP servers from any git repository. It works with Claude Code, Codex, OpenCode, Gemini, Cursor and Copilot, and it records what it installed in a lock file so a project’s agent setup can be reproduced. knowhow replaces an earlier tool of ours called skillz.</p>
<p>knowhow also carries a library of our own: 44 skills for scientific, laboratory and programming work (ASE, VASP, pymatgen, Opentrons robots, design of experiments, electronic lab notebooks and scientific review, among others), along with subagents, commands and hooks. Some of the hooks are guardrails rather than abilities. One keeps an automatic lab-notebook record of what the agent did, and another keeps it from writing secrets into files.</p>
<p>The most useful thing about knowhow may be the convention it set. Every package in the rest of this post ships its own skill in <code>skills/&lt;name&gt;/SKILL.md</code>, next to the code it describes. The instructions change when the code changes, and <code>knowhow add</code> can install any of them.</p>
</section>
<section id="knowing-what-we-know-litdb-and-crucible" class="level2">
<h2 class="anchored" data-anchor-id="knowing-what-we-know-litdb-and-crucible">Knowing what we know: litdb and crucible</h2>
<p>Research starts with the literature, and agents are notorious for inventing it. Our answer is to give the agent a library that we curate, so it searches real papers instead of what it half-remembers.</p>
<p><a href="https://github.com/jkitchin/litdb">litdb</a> is a local database of the papers I care about, with natural-language search. Its MCP server is read-only by design. An agent can run semantic and full-text searches, find papers similar to one it already has, pull new work from OpenAlex, and return citations and BibTeX entries that point to real records. It can even generate the collaborator list an NSF proposal requires, which is the kind of tedious, checkable task that suits an agent well.</p>
<p><a href="https://github.com/jkitchin/crucible">crucible</a> works one level up. An LLM distills papers, notebooks and data into an interlinked org-mode wiki, one article per concept, with an SQLite graph of the links behind it. Ingesting happens in two steps: the agent first proposes which articles to write or update, then writes them. Once the wiki exists, an agent can use the crucible CLI to search it, follow backlinks, find related articles, and look for gaps such as orphan articles and sources that have not been distilled yet. <code>crucible publish</code> turns the wiki into a website using scimax.</p>
<p>These tools are not wired together in code, but they make one workflow. litdb finds the papers, crucible distills them into concepts, and scimax is where I write with them and publish the result. org-ref citation keys run through all three, so a claim in a draft can be traced back to a paper in the database.</p>
</section>
<section id="computing-with-agents-fairchem-mcp-and-discopt-calphad" class="level2">
<h2 class="anchored" data-anchor-id="computing-with-agents-fairchem-mcp-and-discopt-calphad">Computing with agents: fairchem-mcp and discopt-calphad</h2>
<p>Most atomistic simulation workflows were designed for people. You write a script, submit it, wait, and read the output. When an agent drives that workflow, it does the same thing in a loop: write a script, run it, wait, read a log file, and guess what went wrong. Every step reloads the model, and the agent finds out that a calculation went badly only after it is over. We wanted computation that an agent could work with interactively, and results that carry their own evidence of correctness.</p>
<section id="watching-and-steering-simulations" class="level3">
<h3 class="anchored" data-anchor-id="watching-and-steering-simulations">Watching and steering simulations</h3>
<p><a href="https://github.com/jkitchin/fairchem-mcp">fairchem-mcp</a> is an MCP server for the FAIRChem UMA and eSEN <a href="https://kitchingroup.cheme.cmu.edu/papers/topics/neural-network-potentials/index.html">machine-learned potentials</a>, built on ASE. It loads a model once and keeps it in memory, so every later call reuses it instead of spending seconds to minutes loading it again. The agent builds or loads a structure and starts a job: a relaxation, a molecular dynamics run, an NEB calculation, an equation-of-state or elastic scan, phonons, a formation-energy convex hull, or a saddle-point or minima search. Each job runs in the background.</p>
<p>The interesting part is what happens while a job runs. <code>get_status</code> reports the step, the energy and the maximum force, along with a trend label: decreasing, plateaued, stuck or diverging. That label is written for an agent. It turns a column of numbers into a decision about whether to intervene. If a relaxation is stuck, the agent can switch the optimizer partway through, and the atoms keep their positions; only the driver is rebuilt. It can also tighten the force criterion, change the temperature of an MD run, turn on climbing in an NEB, or pause or abort the job. The skill that comes with the server teaches this start, watch and steer loop, including how to respond to each trend label and which job answers which question.</p>
<p>Two other design choices matter. The high-level tools and an <code>execute</code> escape hatch share one Python namespace, so when no tool fits, the agent can drop into Python on the same live <code>Atoms</code> object the job is using. And <code>introspect</code> reads the signatures and docstrings of the code that is actually installed, rather than documentation the model may remember from an older version. That removes a common source of agent errors: calling functions with arguments they no longer take. Because <code>execute</code> runs arbitrary code, this is a tool for a trusted local machine.</p>
<p>The examples in the repository are end-to-end workflows, each written both as a runnable script and as a sequence of MCP tool calls: adsorption energies, adsorption-site searches, diffusion barriers, surface phonons, and an alloy design loop that screens candidates for stability on the convex hull and then ranks the survivors by stiffness. Three routes to transition states, the dimer method, Sella, and eigenvector following in pounce, run on the same system so they can be compared. Installing all of it is one command:</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;">uvx</span> knowhow add jkitchin/fairchem-mcp <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--skill</span> fairchem-mcp <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--mcp</span> fairchem</span></code></pre></div></div>
</section>
<section id="from-energies-to-certified-phase-diagrams" class="level3">
<h3 class="anchored" data-anchor-id="from-energies-to-certified-phase-diagrams">From energies to certified phase diagrams</h3>
<p>A machine-learned potential gives energies of atomic configurations at 0 K. Materials questions are often about <a href="https://kitchingroup.cheme.cmu.edu/papers/topics/alloy-phase-diagrams/index.html">phase diagrams</a>, which need much more: the Gibbs energy of every competing phase as a function of composition and temperature, including the liquid. <a href="https://github.com/jkitchin/discopt-calphad">discopt-calphad</a> connects the two. It is a discopt plugin that computes CALPHAD phase equilibria, and it can compute formation and mixing enthalpies from any ASE calculator, UMA included, and refit the enthalpy terms of an existing thermodynamic database to them. It does not replace the database’s entropy and heat-capacity terms, which a 0 K calculation cannot supply. The agent can then map the phase diagram from the refitted database, compare it with the original, and see what the potential changed.</p>
<p>What makes discopt-calphad suitable for agents is that its answers are certified. A conventional equilibrium calculation finds a local minimum of the Gibbs energy and cannot tell you whether a more stable phase or a miscibility gap was missed. discopt-calphad solves the equilibrium locally, then uses discopt’s global solver to check every phase against the result. If any phase could lower the energy, it is added and the equilibrium is solved again. If none can, the result is certified to within a stated tolerance. Each result carries a certificate level, and when the search cannot finish, the result says so instead of reporting an uncertified answer as if it were certified. In Cu–O, it finds a liquid miscibility gap that a grid-based calculation misses. An agent does not have to judge whether a phase diagram looks plausible. It can read the certificate.</p>
<p>Both of these tools use the optimization software described next: pounce for one of the transition-state searches, and discopt for the certification.</p>
</section>
</section>
<section id="solvers-that-explain-themselves" class="level2">
<h2 class="anchored" data-anchor-id="solvers-that-explain-themselves">Solvers that explain themselves</h2>
<p><img src="https://kitchingroup.cheme.cmu.edu/posts/agentic-science/img/optimization-landscape.png" class="img-fluid"></p>
<p>Since December 2025 we have started 15 public repositories in <a href="https://kitchingroup.cheme.cmu.edu/papers/kitchin2025/index.html">differentiable programming</a> and optimization. Together they hold about 1.8 million lines of source and tests, roughly 1.1 million in Python and 0.66 million in Rust, and they have had about 63,000 downloads from PyPI in the last 180 days. Projects like these usually take a team years. The map above, from an <a href="https://kitchingroup.cheme.cmu.edu/posts/differentiable-optimization-landscape/">earlier post</a>, shows how they relate.</p>
<p>Optimization turns out to be a good domain for AI-assisted development because the answers can be checked. A reported optimum either satisfies the optimality conditions and matches the reference solution, or it does not, and there are large public benchmark sets that say what the right answer is. The same property makes these solvers good tools for agents to use, as long as the solvers explain what they did. That was a design goal from the start.</p>
<p><a href="https://github.com/jkitchin/differentiable-flowsheets">difflow</a> is a JAX framework for differentiable chemical process flowsheets, for simulation, optimization and technoeconomic analysis. Its MCP server lets an agent build a flowsheet one step at a time: choose the species, add units, connect streams and set feeds, with undo and redo when it makes a mistake. Then it can solve the flowsheet, run sensitivity analyses and sweeps, optimize it, propagate uncertainty, and estimate costs. When a flowsheet does not converge, which is the normal state of a new flowsheet, the diagnosis tools return structured findings and suggested remedies rather than a stack trace, and <code>converge</code> tries the remedies in turn. Each tool is labeled as reading, editing or executing code, and one server configuration removes arbitrary Python execution entirely, for when you want the agent to stay inside the modeling tools. difflow can use pounce and discopt as its solvers.</p>
<p><a href="https://github.com/jkitchin/pounce">pounce</a> is a family of interior point solvers written in Rust. It began as a port of Ipopt and now covers nonlinear, conic and quadratic programs. Every solve writes a JSON solve report, so an agent reads structured data instead of parsing an iteration log. The pounce-studio MCP server has about 30 tools built on those reports. Before a solve, an agent can analyze the problem and check the starting point. After a solve, it can diagnose the result, find where progress stalled, look at the convergence trace and the restoration phases, compare two runs, and verify the solution independently. If that is not enough, it can step through a solve in a live debugger. pounce also includes commands for adversarial testing and for reviewing sensitivity results.</p>
<p><a href="https://github.com/jkitchin/discopt">discopt</a> is a modeling language and solver for mixed-integer nonlinear programs, and it has more agent-facing features than anything else we have built. It ships 17 expert subagents (for MINLP solving, convexity detection, Ipopt, benchmarking and more) and commands for each stage of modeling: formulating, diagnosing, debugging, reformulating, explaining a model, converting it between formats, and reporting on benchmarks. It also has a tool-call interface for building models from a natural-language description. The agent creates variables and adds constraints and logical conditions through tool calls, and the expressions it writes go through a safe parser instead of being executed as code. When a model is infeasible or a solve goes wrong, discopt produces reports on infeasibility, convexity and provenance that an agent can act on.</p>
<p>Around discopt is a growing set of <a href="https://kitchingroup.cheme.cmu.edu/software.html#discopt-plugins">plugins</a> that bring the same approach to particular domains. Each one checks its input against a schema, solves the problem, and reports the result, through a skill or an MCP server. discopt-mkm does microkinetic modeling for catalysis, including <a href="https://kitchingroup.cheme.cmu.edu/papers/yang2022b/index.html">degree of rate control</a> and limiting potentials. discopt-grid does power systems optimization, and it was built largely by an autonomous agent loop working from a written specification and implementation plan. discopt-doe adds design of experiments, with agents for estimability and identifiability analysis. discopt-flowsheet raises typed errors instead of returning silently wrong answers. discopt-course is an optimization course in which Claude Code is the teaching assistant, grading against rubrics. discopt-calphad, covered in the computing section, is one more plugin.</p>
</section>
<section id="where-this-is-going" class="level2">
<h2 class="anchored" data-anchor-id="where-this-is-going">Where this is going</h2>
<p>The direction is agents that run the whole loop, from literature to computation to experiment to paper, with people setting the goals and checking the results. The recent roadmap for AI in heterogeneous catalysis describes that future for one field <a href="https://kitchingroup.cheme.cmu.edu/papers/xin2026/index.html">(Xin et al., 2026)</a>. The software here is our attempt to build the environment it needs.</p>
<p>If I had to compress the approach into advice for students, it would be four things. Own the verification: you are responsible for every number, line of code and citation, so learn enough of the fundamentals to know when an answer is wrong. Write things down in plain text, where an agent can read them. Specify, then delegate: the skill that matters now is stating a problem precisely, with tests or checks that define when it is done. And be ambitious, and disclose. Attempt projects that were out of reach, and take responsibility for what you generate.</p>
<p>AI does not reduce the need for expertise. It makes judgment more valuable.</p>
<p>All of the software is on our <a href="https://kitchingroup.cheme.cmu.edu/software.html">software page</a> and at <a href="https://github.com/jkitchin">github.com/jkitchin</a>.</p>


</section>

 ]]></content:encoded></item><item><title>Surrogate models you can optimize globally</title><link>https://kitchingroup.cheme.cmu.edu/posts/mip-surrogates/</link><pubDate>Wed, 28 Oct 2026 00:00:00 +0000</pubDate><category>python</category><category>pycse</category><category>optimization</category><category>surrogates</category><guid isPermaLink="true">https://kitchingroup.cheme.cmu.edu/posts/mip-surrogates/</guid><description>Surrogate models you can optimize globally</description><content:encoded><![CDATA[ 

Skip to main content




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


</section>

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

Skip to main content




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


</section>

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

Skip to main content




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


</section>
</section>

 ]]></content:encoded></item><item><title>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>
    </channel>
</rss>