<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Onepagecode]]></title><description><![CDATA[Practical breakdowns of AI, Python, research papers, and quant trading strategies — one page, one idea, one build at a time.]]></description><link>https://onepagecode.substack.com</link><image><url>https://substackcdn.com/image/fetch/$s_!_W6n!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3078fdad-8e80-40a9-be17-640eb18fa447_1254x1254.png</url><title>Onepagecode</title><link>https://onepagecode.substack.com</link></image><generator>Substack</generator><lastBuildDate>Wed, 15 Jul 2026 01:38:32 GMT</lastBuildDate><atom:link href="https://onepagecode.substack.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Onepagecode]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[onepagecode@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[onepagecode@substack.com]]></itunes:email><itunes:name><![CDATA[Onepagecode]]></itunes:name></itunes:owner><itunes:author><![CDATA[Onepagecode]]></itunes:author><googleplay:owner><![CDATA[onepagecode@substack.com]]></googleplay:owner><googleplay:email><![CDATA[onepagecode@substack.com]]></googleplay:email><googleplay:author><![CDATA[Onepagecode]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Implementing Stock Market Prediction: NeuralProphet + DNN (PyTorch Guide & Critique)]]></title><description><![CDATA[A step-by-step PyTorch implementation of the NP-DNN hybrid model (arXiv:2601.05202v3), resolving dataset contradictions with Optuna.]]></description><link>https://onepagecode.substack.com/p/implementing-stock-market-prediction</link><guid isPermaLink="false">https://onepagecode.substack.com/p/implementing-stock-market-prediction</guid><dc:creator><![CDATA[Onepagecode]]></dc:creator><pubDate>Tue, 14 Jul 2026 05:04:09 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!IoJA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc25caa3-bd9b-43fd-8fd0-795c25c02806_1512x654.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Use the URL at the end of this to download source code</h2><p>The paper proposes a hybrid model (NP-DNN) combining Neural Prophet and Deep Neural Network for stock market price prediction. The methodology includes data preprocessing (Z-score normalization, missing value imputation via linear interpolation), feature extraction using MLP, prediction using DNN, and hyperparameter tuning with Optuna. The model is evaluated on the Crunchbase dataset using classification metrics (accuracy, precision, recall, F1) and compared with DSS, LightGBM, RF, and LLM, claiming 93.21% accuracy. The paper contains several inconsistencies and ambiguities, including a mismatch between the dataset (startup investments) and the task (stock price prediction), and inappropriate use of classification metrics.</p><h2>Implementation Assumptions</h2><ul><li><p>The task is binary classification (price up/down) to align with classification metrics.</p></li><li><p>MLP has 2 hidden layers (64, 32) with ReLU; DNN has 2 hidden layers (64, 32) with ReLU and softmax output.</p></li><li><p>Neural Prophet is implemented using the Prophet library (not neuralprophet) for simplicity.</p></li><li><p>Optuna search space: learning rate [1e-5, 1e-1], number of layers [1,3], hidden units [32,256], dropout [0.0,0.5], batch size [16,128].</p></li><li><p>The Crunchbase dataset is not used; synthetic data is generated for demonstration.</p></li><li><p>The abstract accuracy 99.21% is a typo; we use 93.21% as the claimed accuracy.</p></li></ul><h2>Introduction and Paper Overview</h2><p>Stock market price prediction is one of the most challenging problems in financial forecasting. Prices move in response to countless factors&#8212;economic news, investor sentiment, geopolitical events, and random noise&#8212;making it nearly impossible to predict with high accuracy using simple methods. Traditional statistical models like ARIMA or linear regression often fail to capture the complex, nonlinear patterns that drive market movements.</p><p>The paper we are implementing, "Stock Market Price Prediction using Neural Prophet with Deep Neural Network" (arXiv:2601.05202v3), proposes a hybrid model called <strong>NP-DNN</strong> that combines two powerful tools:</p><ul><li><p><strong>Neural Prophet (NP)</strong>: A time series decomposition model that breaks a price history into interpretable components: the overall trend, repeating seasonal patterns, holiday effects, and random error. This helps the model understand long-term structure.</p></li><li><p><strong>Deep Neural Network (DNN)</strong>: A multi-layer neural network that learns complex patterns from data. The paper uses a Multi-Layer Perceptron (MLP) for feature extraction before feeding into the DNN for final prediction.</p></li></ul><p>The overall pipeline has three main stages:</p><ol><li><p><strong>Data Preprocessing</strong>: Fill missing values using linear interpolation and standardize features with Z-score normalization.</p></li><li><p><strong>Feature Extraction</strong>: Use an MLP to transform the preprocessed data into a more informative representation.</p></li><li><p><strong>Prediction</strong>: Feed the extracted features into a DNN with a softmax output layer to produce a classification (e.g., price up or down).</p></li></ol><p>The paper claims that this NP-DNN model achieves 93.21% accuracy on the Crunchbase dataset, outperforming methods like LightGBM, Random Forest, and even a Large Language Model (LLM).</p><h3>What to Expect from This Tutorial</h3>
      <p>
          <a href="https://onepagecode.substack.com/p/implementing-stock-market-prediction">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Forecasting Stock Prices with Diffusion Models and U-GNNs]]></title><description><![CDATA[A paper-to-Python guide to graph diffusion, uncertainty-aware S&P 500 forecasting, and wireless resource allocation]]></description><link>https://onepagecode.substack.com/p/forecasting-stock-prices-with-diffusion</link><guid isPermaLink="false">https://onepagecode.substack.com/p/forecasting-stock-prices-with-diffusion</guid><dc:creator><![CDATA[Onepagecode]]></dc:creator><pubDate>Mon, 13 Jul 2026 12:07:44 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!_W6n!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3078fdad-8e80-40a9-be17-640eb18fa447_1254x1254.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>What This Article Builds</h2><p>The paper proposes a unified framework for conditional generative modeling of graph signals using denoising diffusion models. The key innovation is the U-Graph Neural Network (U-GNN), a U-Net-like architecture for graph-structured data that uses learned node selection matrices for pooling/unpooling and strided graph convolutions to avoid explicit graph coarsening. The framework is demonstrated on two tasks: stock price forecasting (S&amp;P 500) and wireless resource allocation (power control).</p><h2>Implementation Assumptions</h2><ul><li><p>PyTorch 2.x + PyTorch Geometric (PyG) + standard scientific Python (numpy, scipy, matplotlib).</p></li><li><p>Graph shift operator S stored as a sparse PyTorch tensor (coalesced COO) or edge<em>index+edge</em>weight.</p></li><li><p>All graph signals are batched: (batch, N, F) with optional node dimension per graph (single graph assumed for each task).</p></li><li><p>The U-GNN is built from modular components: StridedGraphConv, NodeSelection, FusionBlock, UGNN core.</p></li><li><p>Training uses AMP (torch.cuda.amp), AdamW, cosine LR with linear warm-up, gradient clipping (max_norm=1.0).</p></li><li><p>Validation and checkpointing use task-specific composite criteria.</p></li><li><p>Data pipeline for S&amp;P 500 uses yfinance; graph built from fundamentals (rank correlation + sector bonus).</p></li><li><p>Data pipeline for WRA synthesizes networks using the paper's channel model and expert primal-dual algorithm.</p></li><li><p>DDIM sampling uses 100 steps and &#951;=0.2 on a sub-grid of the 500-step linear schedule.</p></li></ul><h2>Download Source code. </h2><p>You can download the source using the URL at the end of this article. </p><p>Note: There was no source code attached to the paper, this is my implementation, I tried to code it, and it is part of the larger program, so use the code to understand and implement in your code, try running it would run but there might be some problem. So Use code as reference, not in actual public working. </p><h2>1. Introduction to the Paper and Problem</h2><h3>1.1 What Are Stochastic Graph Signals?</h3><p>Many real-world systems produce signals that live on a graph &#8212; a set of nodes connected by edges that encode relationships. When those signals are inherently random, we call them <em>stochastic graph signals</em>. Two canonical examples, both studied in the paper, are:</p><ul><li><p><strong>Stock returns on a company graph</strong>: Each node is a publicly traded company (e.g., the S&amp;P 500 constituents). The graph edges capture fundamental similarities: same industry sector, correlated financial profiles, or shared supply chains. The signal at each node is the daily log-return of that stock. The signal is stochastic because future returns are unpredictable in detail, yet they are <em>conditionally</em> dependent on the past and on the graph structure (e.g., a shock to one sector propagates to related companies).</p></li><li><p><strong>Power allocations on an interference graph</strong>: Each node is a wireless transmitter&#8211;receiver pair. The graph edges represent interference strength: a directed edge from transmitter <em>j</em> to receiver <em>i</em> carries the channel gain between them. The signal at each node is the transmit power level. The optimal allocation is stochastic because it depends on random fading and the expert algorithm&#8217;s internal randomness, but it is conditioned on the large-scale channel gains (the graph) and the node&#8217;s own channel quality.</p></li></ul><p>In both cases, the graph topology and node-level side information (history features, channel measurements) provide a conditioning context. The goal is to <em>sample</em> new graph signals from the unknown conditional distribution &#8212; not just to predict a single value, but to generate entire trajectories or allocations that respect the graph structure and the observed context.</p><h3>1.2 Why Generative Modeling (Not Just Regression)?</h3><p>A regressive approach &#8212; training a neural network to minimize mean squared error between a point prediction and the true signal &#8212; learns the conditional mean. This is adequate when the conditional distribution is unimodal and symmetric, but it fails when:</p><ul><li><p>The distribution is <strong>multi-modal</strong>: there may be several equally plausible future price paths or power allocations.</p></li><li><p><strong>Uncertainty quantification</strong> is required: a point forecast gives no sense of risk or confidence intervals.</p></li><li><p>The downstream task needs <strong>samples</strong>, not averages: for portfolio optimization, one needs many possible return scenarios; for wireless resource allocation, one needs to test different power settings under fading.</p></li></ul><p>Generative models address these limitations by learning the full conditional law. The paper adopts a <strong>denoising diffusion probabilistic model (DDPM)</strong>, which has become the state of the art for high-quality sample generation in images, audio, and now graph signals. Diffusion models are particularly attractive because they are stable to train, do not suffer from mode collapse (unlike GANs), and can be conditioned naturally on arbitrary inputs.</p><h3>1.3 The Two Applications</h3><p>The paper demonstrates the framework on two distinct tasks, each with its own graph structure and conditioning modality:</p><p><strong>Stock Price Forecasting (S&amp;P 500)</strong></p><ul><li><p><strong>Data</strong>: Daily prices for 468 stocks over ~10 years. For each stock, 12 market features (open, high, low, close, log-return, moving averages, volume, RSI, MACD) are computed. A static undirected graph is built from rank correlations of fundamental profiles, with a same-sector bonus.</p></li><li><p><strong>Conditioning</strong>: The last 20 days of features (history) and the graph topology.</p></li><li><p><strong>Target</strong>: The next 5 days of log-returns (a graph signal of dimension 5 per node).</p></li><li><p><strong>Challenge</strong>: The conditional distribution of future returns is heavy-tailed, heteroscedastic, and exhibits volatility clustering. A generative model can produce multiple plausible trajectories, enabling risk-aware decision-making.</p></li></ul><p><strong>Wireless Resource Allocation (Power Control)</strong></p><ul><li><p><strong>Data</strong>: Synthetic networks of 400 transmitter&#8211;receiver pairs in square areas of varying density. Channel gains follow a dual-slope path loss model with log-normal shadowing and Rayleigh fading. An expert primal&#8211;dual algorithm computes optimal power allocations that maximize sum ergodic rate subject to per-user minimum rate constraints.</p></li><li><p><strong>Conditioning</strong>: The directed interference graph (top-10 strongest interferers per receiver, log-normalized) and two node-state features (direct link gain and aggregate interference under full power).</p></li><li><p><strong>Target</strong>: The centered power allocation (p/Pmax &#8722; 1) for each user.</p></li><li><p><strong>Challenge</strong>: The expert algorithm is computationally expensive; a learned generative model can approximate its output distribution at a fraction of the cost, and can be deployed in real time.</p></li></ul><h3>1.4 Overview of the Proposed Solution</h3><p>The paper proposes a unified framework that combines a <strong>denoising diffusion probabilistic model</strong> with a novel <strong>U&#8209;Graph Neural Network (U&#8209;GNN)</strong> denoiser. The key ideas are:</p><ol><li><p><strong>Forward diffusion</strong>: Gradually add Gaussian noise to the clean graph signal over 500 steps, following a linear noise schedule (&#946;&#8321; = 1e&#8722;4, &#946;&#8325;&#8320;&#8320; = 2e&#8722;2). The noising process is fixed and does not depend on the graph.</p></li><li><p><strong>Reverse denoising</strong>: Learn a neural network &#949;_&#952; that predicts the noise added at each step, conditioned on the graph shift operator S and node features u. The network is trained with the simple mean-squared error between true and predicted noise (equation (6) in the paper).</p></li><li><p><strong>U&#8209;GNN denoiser</strong>: A U&#8209;Net-like architecture for graph signals. It processes the signal at multiple resolutions by:</p></li></ol><ul><li><p><strong>Strided graph convolutions</strong> (Algorithm 1): Instead of coarsening the graph, the signal is lifted to the full graph, filtered with iterated unit shifts of S, and then reduced to a subset of active nodes. The stride &#947; increases the receptive field without densifying the graph.</p></li><li><p><strong>Learned node selection</strong> (Section IV&#8209;B): At each down&#8209;sampling level, a scoring MLP selects the top&#8209;k nodes to retain. During training, Gumbel noise encourages exploration, and a straight&#8209;through estimator allows gradients to flow through the discrete selection.</p></li><li><p><strong>Fusion layers</strong> (equation (22)): The down&#8209;sampled signal is merged with global embeddings (node states and diffusion step) via cross&#8209;attention (for stock) or addition (for WRA).</p></li><li><p><strong>Skip connections</strong> (equation (23)): The decoder receives concatenated features from the encoder at the same resolution.</p></li></ul><ol start="4"><li><p><strong>Accelerated sampling</strong>: Instead of the full 500&#8209;step reverse process, the paper uses DDIM (Denoising Diffusion Implicit Models) with 100 steps and &#951; = 0.2, which produces high&#8209;quality samples much faster.</p></li><li><p><strong>Task&#8209;specific adaptations</strong>: For stock forecasting, the U&#8209;GNN includes a lightweight temporal mixer (interleaved dilated 1&#8209;D convolutions and two&#8209;head self&#8209;attention) to process the history features. For WRA, the conditioning is static and uses simple addition.</p></li></ol><h3>1.5 What This Tutorial Covers</h3><p>This tutorial walks through a complete, modular reproduction of the paper&#8217;s stock forecasting and wireless resource allocation experiments. We will:</p><ul><li><p>Implement the forward diffusion process and DDIM sampling from scratch.</p></li><li><p>Build the U&#8209;GNN architecture piece by piece: strided graph convolutions, learned node selection, fusion layers, and the full encoder&#8211;decoder.</p></li><li><p>Construct the data pipelines for both tasks, including synthetic data generation for WRA and a simplified stock data pipeline (using synthetic data to avoid network calls).</p></li><li><p>Train the model with the exact hyperparameters from the paper (Table I, Appendix E).</p></li><li><p>Evaluate using the paper&#8217;s metrics: CRPS, MIS90, RMSE, direction accuracy, stylized&#8209;fact gaps for stock; ergodic rate, feasibility, and gap to expert for WRA.</p></li><li><p>Reproduce the key figures: forecasting trajectories, CDF comparisons, distribution diagnostics, and WRA bar charts.</p></li></ul><p>All code is written in Python using PyTorch and PyTorch Geometric, and is organized into modular files for clarity. The implementation follows the paper&#8217;s descriptions and appendices faithfully, with documented decisions where the paper is ambiguous.</p><p>Let&#8217;s begin by understanding the diffusion process and how it is adapted to graph signals.</p><h2>2. Denoising Diffusion Probabilistic Models for Graph Signals</h2><h3>2.1 Forward Diffusion Process</h3><p>The core idea of a denoising diffusion model is to define a <em>forward</em> process that gradually corrupts a clean signal into pure noise, and then learn a <em>reverse</em> process that undoes this corruption step by step. For graph signals, the forward process operates on the node features directly, independent of the graph structure at this stage &#8212; the graph topology enters only as conditioning information for the reverse denoiser.</p><p>Let x_0 \in \mathbb{R}^{N \times F} be a clean graph signal (e.g., the future returns of N stocks over F time steps). The paper defines a Markov chain of K = 500 steps that adds Gaussian noise with a <em>linear variance schedule</em>:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\beta_1 = 10^{-4}, \\quad \\beta_{500} = 2 \\times 10^{-2}, \\quad \\beta_k = \\beta_1 + (k-1)\\frac{\\beta_{500} - \\beta_1}{K-1}&quot;,&quot;id&quot;:&quot;DFBE8E173E&quot;}" data-component-name="LatexBlockToDOM"></div><p>Each forward transition is (equation (2)):</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;q(x_k \\mid x_{k-1}) = \\mathcal{N}\\bigl(x_k; \\sqrt{1-\\beta_k}\\,x_{k-1},\\; \\beta_k I\\bigr)&quot;,&quot;id&quot;:&quot;3978E6C16E&quot;}" data-component-name="LatexBlockToDOM"></div><p>Because the Gaussian is closed under composition, we can directly sample x_k from x_0 without iterating through all intermediate steps. Define \alpha_k = 1 - \beta_k and the cumulative product \bar{\alpha}_k = \prod_{i=1}^k \alpha_i. Then the marginal distribution is (equation (3)):</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;q(x_k \\mid x_0) = \\mathcal{N}\\bigl(x_k; \\sqrt{\\bar{\\alpha}_k}\\,x_0,\\; (1-\\bar{\\alpha}_k)I\\bigr)&quot;,&quot;id&quot;:&quot;1DC06A0796&quot;}" data-component-name="LatexBlockToDOM"></div><p>which leads to the reparameterization used in training (equation (4)):</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;x_k = \\sqrt{\\bar{\\alpha}_k}\\,x_0 + \\sqrt{1-\\bar{\\alpha}_k}\\,\\varepsilon, \\quad \\varepsilon \\sim \\mathcal{N}(0,I)&quot;,&quot;id&quot;:&quot;363DC23A66&quot;}" data-component-name="LatexBlockToDOM"></div><h4>Implementation in <code>diffusion.py</code></h4><p>The <code>DiffusionProcess</code> class precomputes all \bar{\alpha}_k values from the linear \beta schedule. The constructor stores the betas, alphas, and derived quantities as tensors for efficient indexing:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;a2173af3-7953-4864-b4f4-741809d413ce&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">class DiffusionProcess:
    def __init__(self, betas: torch.Tensor) -&gt; None:
        self.K = betas.shape[0]
        self.betas = betas
        self.alphas = 1.0 - betas
        # alpha_bar[t] = prod_{i=1}^{t} alpha_i, alpha_bar[0] = 1
        self.alpha_bar = torch.cat([
            torch.ones(1, dtype=betas.dtype, device=betas.device),
            torch.cumprod(self.alphas, dim=0)
        ], dim=0)
        self.sqrt_alpha_bar = self.alpha_bar.sqrt()
        self.sqrt_one_minus_alpha_bar = torch.sqrt(1.0 - self.alpha_bar)</code></pre></div><p>The <code>forward_diffuse</code> method implements equation (4) directly. It takes a clean signal x_0 and a step index k (1-indexed), samples noise, and returns the noised signal:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;5352da5b-9050-4539-9dcf-138541c7c2c8&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def forward_diffuse(self, x0: torch.Tensor, k: torch.Tensor) -&gt; tuple[torch.Tensor, torch.Tensor]:
    k_idx = k.long()
    sqrt_alpha_k = self.sqrt_alpha_bar[k_idx]
    sqrt_one_minus_alpha_k = self.sqrt_one_minus_alpha_bar[k_idx]
    epsilon = torch.randn_like(x0)
    x_k = sqrt_alpha_k * x0 + sqrt_one_minus_alpha_k * epsilon
    return x_k, epsilon</code></pre></div><p>During training, we sample k \sim \text{Uniform}(1, K) for each batch element, compute x_k using this method, and then ask the denoiser to predict the noise \varepsilon.</p><h3>2.2 Reverse Denoising and the Noise-Prediction Objective</h3><p>The reverse process is a Markov chain that starts from pure noise x_K \sim \mathcal{N}(0,I) and iteratively removes noise to recover x_0. Each reverse step is parameterized as a Gaussian with learned mean (equation (5)):</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;p_\\theta(x_{k-1} \\mid x_k; \\xi) = \\mathcal{N}\\bigl(x_{k-1}; \\mu_\\theta(x_k, k; \\xi), \\sigma_k^2 I\\bigr)&quot;,&quot;id&quot;:&quot;C30BC1E27D&quot;}" data-component-name="LatexBlockToDOM"></div><p>Here \xi = (S, u) denotes the conditioning information: the graph shift operator S and node features u. The mean \mu_\theta is derived from a noise-prediction network \varepsilon_\theta (the U-GNN) via equation (8):</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\mu_\\theta = \\frac{1}{\\sqrt{\\alpha_k}}\\left(x_k - \\frac{\\beta_k}{\\sqrt{1-\\bar{\\alpha}_k}}\\,\\varepsilon_\\theta(x_k, k; \\xi)\\right)&quot;,&quot;id&quot;:&quot;F03B82DCC2&quot;}" data-component-name="LatexBlockToDOM"></div><p>Rather than directly predicting \mu_\theta, the paper adopts the simplified training objective from Ho et al. (2020) (equation (6)):</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\mathcal{L}(\\theta) = \\mathbb{E}_{k, x_0, \\varepsilon}\\left[\\|\\varepsilon - \\varepsilon_\\theta(x_k, k; \\xi)\\|^2\\right]&quot;,&quot;id&quot;:&quot;E6EC6A9FEB&quot;}" data-component-name="LatexBlockToDOM"></div><p>This loss is simple: at each training step, we sample a random k, noise \varepsilon, and a clean signal x_0, compute x_k via the forward process, and ask the U-GNN to predict the noise that was added. The gradient flows through the U-GNN only; the forward process is fixed.</p><h3>2.3 DDIM Sampling for Acceleration</h3><p>While the reverse process could be simulated for all K = 500 steps, the paper uses the Denoising Diffusion Implicit Model (DDIM) to accelerate sampling to just 100 steps while maintaining sample quality. DDIM defines a non-Markovian reverse process that shares the same forward marginals but allows larger jumps.</p><p>At each sampling step, we first estimate the clean signal from the current noisy x_k and the predicted noise (equation (7)):</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\hat{x}_0 = \\frac{x_k - \\sqrt{1-\\bar{\\alpha}_k}\\,\\varepsilon_\\theta}{\\sqrt{\\bar{\\alpha}_k}}&quot;,&quot;id&quot;:&quot;C4BC98E27D&quot;}" data-component-name="LatexBlockToDOM"></div><p>Then we update to the previous step k-1 (or, for accelerated sampling, to the previous sub-grid step \tau_{i-1}) using equation (9):</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;x_{k-1} = \\sqrt{\\bar{\\alpha}_{k-1}}\\,\\hat{x}_0 + \\sqrt{1-\\bar{\\alpha}_{k-1} - \\sigma_k^2}\\,\\varepsilon_\\theta + \\sigma_k w, \\quad w \\sim \\mathcal{N}(0,I)&quot;,&quot;id&quot;:&quot;A4DF9953E3&quot;}" data-component-name="LatexBlockToDOM"></div><p>The noise scale \sigma_k is controlled by a parameter \eta \in [0,1] (equation (29)):</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\sigma_k(\\eta) = \\eta\\,\\sqrt{\\frac{1-\\bar{\\alpha}_{k-1}}{1-\\bar{\\alpha}_k}}\\,\\sqrt{1 - \\frac{\\bar{\\alpha}_k}{\\bar{\\alpha}_{k-1}}}&quot;,&quot;id&quot;:&quot;842B00FAD2&quot;}" data-component-name="LatexBlockToDOM"></div><p>When \eta = 0, the process is deterministic (pure DDIM). When \eta = 1, it recovers the original DDPM. The paper uses \eta = 0.2 for a balance of stochasticity and speed.</p><p>For accelerated sampling, we select a sub-grid of step indices (e.g., 100 evenly spaced indices from 1 to 500) and apply the update using the corresponding \bar{\alpha} values. The <code>sample_sub_grid</code> method generates this list:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;67bd9c9d-d01b-4804-aae3-b7bb1e9f28f0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def sample_sub_grid(self, n_steps: int) -&gt; List[int]:
    step_indices = torch.linspace(
        self.K, 1, n_steps, dtype=torch.float32
    ).round().long().unique().tolist()
    step_indices = sorted(step_indices, reverse=True)
    return step_indices</code></pre></div><p>The <code>ddim_sample</code> method implements the full sampling loop. It takes the denoiser function, initial noise x_K, conditioning dictionary, sub-grid, and \eta:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;7a43bfa7-93e0-4649-b313-167aed3e6838&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def ddim_sample(self, denoiser, x_K, cond=None, sub_grid=None, eta=0.2):
    if sub_grid is None:
        sub_grid = list(range(self.K, 0, -1))
    sub_grid = sorted(sub_grid, reverse=True)
    x = x_K.clone()
    for i, k in enumerate(sub_grid):
        k_prev = sub_grid[i + 1] if i + 1 &lt; len(sub_grid) else 0
        sqrt_alpha_bar_k = self.sqrt_alpha_bar[k]
        sqrt_one_minus_alpha_bar_k = self.sqrt_one_minus_alpha_bar[k]
        inv_sqrt_alpha_bar_k = self.inv_sqrt_alpha_bar[k]
        epsilon_theta = denoiser(x, k, cond)
        x_hat_0 = (x - sqrt_one_minus_alpha_bar_k * epsilon_theta) * inv_sqrt_alpha_bar_k
        if k_prev &gt; 0:
            sqrt_alpha_bar_prev = self.sqrt_alpha_bar[k_prev]
            one_minus_alpha_bar_prev = self.one_minus_alpha_bar[k_prev]
            one_minus_alpha_bar_k = self.one_minus_alpha_bar[k]
            ratio = (one_minus_alpha_bar_prev / one_minus_alpha_bar_k).clamp(min=0.0)
            sigma = eta * torch.sqrt(ratio) * torch.sqrt(1.0 - self.alpha_bar[k] / self.alpha_bar[k_prev])
            noise = torch.randn_like(x) if eta &gt; 0 else 0.0
            sqrt_one_minus_alpha_bar_prev_minus_sigma2 = torch.sqrt(
                (one_minus_alpha_bar_prev - sigma**2).clamp(min=0.0)
            )
            x = sqrt_alpha_bar_prev * x_hat_0 + sqrt_one_minus_alpha_bar_prev_minus_sigma2 * epsilon_theta + sigma * noise
        else:
            x = x_hat_0
    return x</code></pre></div><p>Note the adaptation for sub-grid steps: the formula uses \bar{\alpha}_{k_\text{prev}} and \bar{\alpha}_k directly, which is valid for any two steps where k_\text{prev} &lt; k. The clamping ensures numerical stability.</p><h3>2.4 Conditioning on Graph Topology and Node Features</h3><p>The denoiser \varepsilon_\theta is the U-GNN, which takes three inputs at each step:</p><ul><li><p><strong>Noisy signal</strong> x_k: the current node features to be denoised.</p></li><li><p><strong>Diffusion step</strong> k: embedded as a sinusoidal positional encoding and broadcast to all nodes.</p></li><li><p><strong>Conditioning</strong> \xi = (S, u): the graph shift operator S (a sparse matrix encoding edge weights) and node-level side information u (e.g., historical market features for stock forecasting, or direct link gain for wireless).</p></li></ul><p>The conditioning is injected through fusion layers inside the U-GNN, which we will examine in detail in the next section. For now, it suffices to understand that the denoiser is a function:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\varepsilon_\\theta: (x_k, k, S, u) \\mapsto \\hat{\\varepsilon}&quot;,&quot;id&quot;:&quot;DB9AF97024&quot;}" data-component-name="LatexBlockToDOM"></div><p>and that the entire diffusion framework &#8212; forward process, noise-prediction loss, and DDIM sampling &#8212; is agnostic to the internal architecture of \varepsilon_\theta. This modularity is what allows the paper to apply the same diffusion framework to two very different tasks by swapping only the denoiser architecture.</p><h3>Summary</h3><ul><li><p>The forward diffusion process adds Gaussian noise to graph signals according to a linear schedule (equations (2)-(4)).</p></li><li><p>The reverse process is learned by predicting the noise added at each step (equation (6)).</p></li><li><p>DDIM sampling accelerates generation by using a sub-grid of steps and a controlled stochasticity parameter \eta (equations (7), (9), (29)-(30)).</p></li><li><p>The <code>DiffusionProcess</code> class in <code>diffusion.py</code> implements all of these operations with precomputed \bar{\alpha} values and efficient tensor operations.</p></li><li><p>Conditioning on graph topology and node features is handled by the U-GNN denoiser, which we will explore next.</p></li></ul><h2>3. The U-GNN Architecture</h2><h3>3.1 Why a Graph U-Net?</h3><p>Images have a natural multi-scale structure: you can downsample (pool) by averaging neighboring pixels, and then upsample (unpool) by interpolation. A U-Net exploits this to build a computational graph with a wide receptive field while keeping the number of parameters manageable. Graph signals lack such a regular grid &#8212; there is no canonical notion of &#8220;neighbourhood averaging&#8221; because the graph is irregular. Nevertheless, the same intuition applies: to model long-range dependencies across the graph, we need to process the signal at multiple resolutions, aggregating information from far-away nodes into compact representations and then expanding back to the original resolution.</p><p>The paper&#8217;s solution, the <strong>U-Graph Neural Network (U-GNN)</strong>, is a U-Net-like architecture designed for graph-structured data. It uses three key innovations:</p><ol><li><p><strong>Strided graph convolutions</strong> that increase the receptive field without making the graph shift operator denser.</p></li><li><p><strong>Learned node selection</strong> (with Gumbel-Top-K and a straight-through estimator) to decide which nodes to keep at each coarser resolution, avoiding a fixed pooling strategy.</p></li><li><p><strong>Fusion layers</strong> that inject conditioning information (node states and diffusion step) at every resolution level.</p></li></ol><p>Let us walk through each component.</p><h3>3.2 The Lift&#8211;Filter&#8211;Reduce Pattern (Equation (13))</h3><p>A standard GNN layer updates node features by exchanging messages along edges. If the graph shift operator is a sparse matrix S \in \mathbb{R}^{N \times N} (the adjacency or a normalized Laplacian), a single layer computes:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;Z_\\ell = \\sigma_\\ell\\Bigl( \\sum_{k=0}^{K} S^k X_{\\ell-1} \\Theta_{\\ell,k} \\Bigr)&quot;,&quot;id&quot;:&quot;5D7882FB94&quot;}" data-component-name="LatexBlockToDOM"></div><p>where X_{\ell-1} are the node features, \Theta_{\ell,k} are learnable weight matrices for each &#8220;tap&#8221; k, and \sigma_\ell is an activation. Each tap k corresponds to a k-step random walk on the graph, giving a localised receptive field.</p><p>Now suppose we have identified a subset of &#8220;active&#8221; nodes (those that survive the pooling) through a binary selection matrix D \in \{0,1\}^{N' \times N} (each row is a one-hot indicator of an active node). The paper uses a <strong>lift&#8211;filter&#8211;reduce</strong> pattern (equation (13)):</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;Z_\\ell = D\\; \\sigma_\\ell\\Bigl( \\sum_{k=0}^{K} S^k \\, D^\\mathsf{T} Z_{\\ell-1} \\, \\Theta_{\\ell,k} \\Bigr)&quot;,&quot;id&quot;:&quot;BBA97DCE6A&quot;}" data-component-name="LatexBlockToDOM"></div><ol><li><p><strong>Lift</strong>: D^\mathsf{T} Z_{\ell-1} maps the N'-node signal back to the full N-node graph by placing the values at the active nodes and leaving zeros elsewhere.</p></li><li><p><strong>Filter</strong>: apply the graph convolution (sum over k taps) on the full graph.</p></li><li><p><strong>Reduce</strong>: multiply by D to extract the active-node signals.</p></li></ol><p>This pattern is the foundation of every strided convolution in U-GNN.</p><h3>3.3 Strided Graph Convolutions (Algorithm 1, Equation (14))</h3><p>A plain GNN with K taps has a receptive field of exactly K hops. To reach farther nodes, one could increase K, but that makes S^K denser and more expensive (both in memory and computation). The paper&#8217;s solution is to replace S^k with (S^\gamma)^k, where \gamma is a <strong>stride</strong> parameter. Equation (14) formalises this:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;Z_\\ell = D\\; \\sigma_\\ell\\Bigl( \\sum_{k=0}^{K} (S^\\gamma)^k \\, D^\\mathsf{T} Z_{\\ell-1} \\, \\Theta_{\\ell,k} \\Bigr)&quot;,&quot;id&quot;:&quot;05B05E874B&quot;}" data-component-name="LatexBlockToDOM"></div><p>Raising S to the power \gamma directly is expensive, so the paper proposes Algorithm 1: instead of forming S^\gamma, we perform \gamma K unit shifts and tap every \gamma-th one. This gives the same result as using S^\gamma, but each shift is a sparse matrix&#8211;vector product, and the intermediate \gamma-1 shifts that are not tapped still propagate the signal forward, building the wider receptive field.</p><p>Let us look at the implementation in <code>gnn_layers.py</code>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;2bad0778-7db1-4bb1-9c40-08833a1f7176&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">class StridedGraphConv(nn.Module):
    """
    Strided graph convolution layer with pooling (lift-filter-reduce).
    Implements Algorithm 1 of the paper.
    """
    def __init__(self, in_dim, out_dim, K, gamma, activation=nn.ReLU()):
        super().__init__()
        self.weight = nn.Parameter(torch.randn(K + 1, in_dim, out_dim) * 0.1)
        self.bias = nn.Parameter(torch.zeros(out_dim))
        self.K = K
        self.gamma = gamma
        self.activation = activation

    def forward(self, Z_prev, S, D):
        # Lift
        x = torch.mm(D.T, Z_prev)          # (N, in_dim)
        # Tap 0
        A = torch.mm(x, self.weight[0])   # (N, out_dim)

        # Iterated unit shifts
        for j in range(1, self.gamma * self.K + 1):
            x = torch.mm(S, x)            # one shift
            if j % self.gamma == 0:
                tap_idx = j // self.gamma
                A = A + torch.mm(x, self.weight[tap_idx])

        A = self.activation(A + self.bias)
        out = torch.mm(D, A)              # reduce
        return out</code></pre></div><p>Key points:</p><ul><li><p>The total number of taps is still K+1 (including the identity tap), independent of \gamma.</p></li><li><p>The stride \gamma is set per depth: \gamma_b = \min(\lfloor\sqrt{\bar{\rho}_b}\rfloor, \gamma_{\max}=2), where \bar{\rho}_b is the cumulative pooling factor at depth b.</p></li><li><p>Both dense and sparse S are supported (the code checks <code>S.is_sparse</code>; PyTorch Geometric&#8217;s sparse ops can be used in practice).</p></li></ul><h3>3.4 Learned Node Selection (Equations (24)&#8211;(25), (31)&#8211;(34))</h3><p>How does the U-GNN decide which nodes to keep when going from one resolution level to the next? Instead of a fixed rule (e.g., max pooling over neighbourhoods), the paper <strong>learns</strong> which nodes are most informative. The process has three steps:</p><ol><li><p><strong>Score each node</strong>: a node-wise MLP \Psi_b takes the concatenation of the encoded feature Z_b and the global embeddings D_b[U_0;K_0] and outputs a scalar score (equation (24)).</p></li><li><p>**Select top-k**: keep the N_{b+1} highest-scoring nodes (equation (25)).</p></li><li><p><strong>Down-sample</strong>: the selection matrix C_{b+1} is the identity rows indexed by the chosen nodes.</p></li></ol><p>But this discrete selection is non-differentiable. During training, the paper uses two tricks:</p><ul><li><p><strong>Gumbel-Top-K</strong> (equation (31)): Gumbel noise is added to the scores before the \text{TopK} operation, encouraging exploration.</p></li><li><p><strong>Straight-Through Estimator (STE)</strong> (equation (32)): the forward pass uses a hard one-hot mask (the selected nodes), while the backward pass flows through a soft sigmoid surrogate \sigma(v_b / \tau). The code does this with a simple <code>detach</code> trick:</p></li></ul><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;42a4af36-48a5-4654-a191-9fd408109fb1&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">mask_hard = torch.zeros(N_b, device=device)
mask_hard.scatter_(0, indices, 1.0)            # hard one-hot
mask_soft = torch.sigmoid(scores / tau)         # soft surrogate
mask = mask_hard + mask_soft - mask_soft.detach()  # forward hard, backward soft</code></pre></div><p>Additionally, the temperature \tau and exploration noise \varepsilon are annealed linearly over training (equation (34)): a warm-up phase of T_w = 0.02T epochs followed by an anneal phase of T_a = 0.73T epochs. The full <code>NodeSelectionHead</code> class in <code>node_selection.py</code> implements this schedule.</p><h3>3.5 Full U-GNN: Encoder, Bottleneck, Decoder (Equations (18)&#8211;(23))</h3><p>With the strided convolution and the learned selection head, we can now assemble the complete U-GNN. The architecture has B = 4 resolution levels. Let N_1 = N be the original number of nodes. At each depth b, the number of active nodes is N_b = \lfloor N / \rho^{b-1}\rfloor with \rho = 2.</p><p><strong>Nested selection matrices</strong> (equation (18)): the global selection matrix D_b that maps from the full graph to the active nodes at depth b is the product of all previous selection matrices:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;D_b = C_b \\cdots C_1&quot;,&quot;id&quot;:&quot;5C60792EF5&quot;}" data-component-name="LatexBlockToDOM"></div><p><strong>Encoder path</strong> (equation (19)): for b = 1, \dots, B-1:</p><ol><li><p>Down-sample: V_b = C_b Z_{b-1}.</p></li><li><p>Fuse: P_b = \Pi_b(V_b, D_b[U_0;K_0]) (see below).</p></li><li><p>Apply strided GNN module: Z_b = \Phi_{E_b}(P_b, S; \Theta_{E_b}, D_b, \gamma_b).</p></li><li><p>Select: C_{b+1} = \text{Selector}(Z_b, D_b[U_0;K_0], N_{b+1}).</p></li></ol><p><strong>Bottleneck</strong> (equation (21)): at b = B, the same pattern without selection.</p><p><strong>Decoder path</strong> (equation (20)): for b = B-1, \dots, 1:</p><ol><li><p>Up-sample: C_{b+1}^\mathsf{T} Y_{b+1} (zero-pad to the previous resolution).</p></li><li><p>Skip-connect: concatenate the up-sampled decoder output with the encoder signal Z_b.</p></li><li><p>Project: V_b = \Pi_{\text{skip}}^b([C_{b+1}^\mathsf{T} Y_{b+1}; Z_b]) (equation (23)).</p></li><li><p>Fuse: P_b = \Pi_b(V_b, D_b[U_0;K_0]).</p></li><li><p>Apply strided GNN: Y_b = \Phi_{D_b}(P_b, S; \Theta_{D_b}, D_b, \gamma_b).</p></li></ol><p>Finally, <strong>read-out</strong>: \varepsilon_\theta = \Pi_{\text{out}}(Y_1), a node-wise MLP that maps back to the original feature dimension.</p><p>The fusion layer \Pi_b (equation (22)) merges the down-sampled signal with the global embeddings (node states U_0 and step embedding K_0). For the stock task, the paper uses cross-attention (query = signal, key/value = global embeddings); for the WRA task, it uses simple addition after a projection. The <code>FusionLayer</code> class in <code>ugnn.py</code> implements both versions.</p><h3>3.6 Task-Specific Adaptations</h3><p>For the stock forecasting task, the input x_k has a temporal dimension (5 future days). The U-GNN includes a <strong>TemporalMixer</strong> that applies two dilated 1D convolutions followed by two-head self-attention along the time axis, producing a single feature vector per node (by averaging over time). This is a reasonable default for the paper&#8217;s underspecified temporal encoder.</p><p>For the wireless resource allocation task, the input is static (one allocation per node), so no temporal mixer is needed &#8212; the <code>TemporalMixer</code> is replaced by an identity module.</p><p>The <code>UGNN</code> class in <code>ugnn.py</code> orchestrates all these components. It pre-computes the target node counts, initialises the nested selection matrix D, and runs the encoder&#8211;bottleneck&#8211;decoder loop. The selection heads are called at the end of each encoder block, and the resulting C and D matrices are accumulated.</p><h3>3.7 Summary</h3><p>The U-GNN is a carefully designed graph U-Net that avoids explicit graph coarsening. Instead, it learns which nodes to retain at each level, uses strided graph convolutions to control the receptive field without making the shift operator denser, and fuses conditioning information at every resolution. This architecture is the central contribution of the paper and is what enables the diffusion model to generate high-quality graph signals conditioned on graph structure and node features.</p><h2>4. Data Pipelines</h2><p>A generative model is only as good as the data it learns from. The paper presents two distinct data pipelines, each transforming raw observations into the graph-structured inputs required by the diffusion model. This section walks through both pipelines in detail, showing how we go from stock tickers or wireless network parameters to the tensors that feed the U-GNN.</p><h3>4.1 S&amp;P 500 Stock Forecasting Pipeline</h3><p>The stock forecasting task aims to predict the next five days of log-returns for 468 S&amp;P 500 constituents, conditioned on the last 20 days of market features and a static graph of company similarities. The pipeline has five stages: data acquisition, feature engineering, graph construction, window creation, and normalisation &amp; splitting.</p><h4>4.1.1 Data Acquisition</h4><p>In a real deployment, you would download daily OHLCV data from Yahoo Finance using the <code>yfinance</code> library. The paper specifies the date range 2016&#8209;09&#8209;06 to 2026&#8209;02&#8209;13. However, our implementation (in <code>data_stock.py</code>) provides a synthetic data generator that mimics the shape and basic statistics of 468 stocks, so that the code can be run without an internet connection. The function <code>download_sp500_data</code> returns a DataFrame with columns <code>ticker</code>, <code>date</code>, <code>open</code>, <code>high</code>, <code>low</code>, <code>close</code>, <code>volume</code>, and <code>sector</code>. The synthetic prices follow an AR(1) log&#8209;price process with a common market factor and idiosyncratic noise, which is sufficient for testing the pipeline logic.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;a56233be-3a15-49e0-97ea-33a62436c3fe&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def download_sp500_data(
    start_date: str = "2016-09-06",
    end_date: str = "2026-02-13",
    num_stocks: int = 468,
    num_days: int = 2500,
) -&gt; pd.DataFrame:
    # ... generates synthetic OHLCV data with random sectors
    # Returns a DataFrame with columns: ticker, date, open, high, low, close, volume, sector</code></pre></div><h4>4.1.2 Feature Engineering</h4><p>For each stock on each day, the paper computes 12 market features. The function <code>compute_market_features</code> adds these features as a list column to the DataFrame. The features are:</p><ul><li><p>open, high, low, close, log_return</p></li><li><p>moving averages (MA5, MA20)</p></li><li><p>volume</p></li><li><p>RSI(14)</p></li><li><p>MACD, MACD<em>signal, MACD</em>histogram</p></li></ul><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;d6fea620-ea9d-49d7-8094-92a61490fc56&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def compute_market_features(df: pd.DataFrame) -&gt; pd.DataFrame:
    # ... computes each feature per ticker
    # Adds columns 'features' (list of 12 floats) and 'log_return'</code></pre></div><p>The log_return column becomes the target signal; the full feature vector serves as the conditioning input <code>u</code>.</p><h4>4.1.3 Graph Construction from Fundamentals</h4><p>The paper builds a static undirected graph from company fundamentals. The function <code>build_fundamentals_graph</code> implements a simplified version:</p><ol><li><p>Compute the Spearman rank correlation between the mean feature vectors of each pair of stocks.</p></li><li><p>Add a same&#8209;sector bonus of 0.1.</p></li><li><p>Threshold at the median correlation to keep only the strongest edges.</p></li><li><p>Symmetrize and add self&#8209;loops.</p></li><li><p>Apply spectral normalisation: S = D^{-1/2} A D^{-1/2}.</p></li></ol><p>The result is a dense <code>(N, N)</code> tensor <code>S</code> and a list of sector labels.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;0b0f71b2-c9bd-465c-b014-60c163e91ee6&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def build_fundamentals_graph(stocks_df: pd.DataFrame) -&gt; Tuple[torch.Tensor, List[int]]:
    # ... rank correlation, sector bonus, threshold, spectral normalisation
    return torch.tensor(S, dtype=torch.float32), sectors.tolist()</code></pre></div><h4>4.1.4 Sliding Windows</h4><p>With the feature matrix of shape <code>(N, T, U)</code> (12 features) and the target matrix of shape <code>(N, T)</code> (log&#8209;returns), the function <code>create_sliding_windows</code> extracts every possible window of length <code>Th + Tp</code>. For each starting time <code>t</code> from <code>Th</code> to <code>T - Tp</code>, it produces:</p><ul><li><p><strong>history</strong> <code>u</code>: shape <code>(N, Th, U)</code> &#8212; the last 20 days of features.</p></li><li><p><strong>target</strong> <code>x0</code>: shape <code>(N, Tp)</code> &#8212; the next 5 days of log&#8209;returns.</p></li></ul><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;aef5b5ac-4d7b-42db-a6ea-e8d8cbbd0aa2&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def create_sliding_windows(
    data: pd.DataFrame, Th: int = 20, Tp: int = 5
) -&gt; List[Dict]:
    # ... builds feature tensor (N, T, U) and target tensor (N, T)
    # ... iterates t from Th to T-Tp, creates history and target tensors</code></pre></div><h4>4.1.5 RevIN Normalisation</h4><p>The paper applies a modified Reversible Instance Normalisation (RevIN) to the target window. The <code>RevIN</code> class in <code>data_stock.py</code> implements this with the paper&#8209;specific parameters: a blend weight of 0.7 and a scale correction of 1.107. During the forward pass, it normalises the target using per&#8209;instance mean and standard deviation (computed along the temporal dimension), then blends these statistics with running averages for consistency. The normalised target is then scaled and shifted by learnable affine parameters.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;a4650311-6fab-4327-b9c6-ee9a716d27af&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">class RevIN(nn.Module):
    def __init__(self, num_features: int = 1, blend: float = 0.7, scale_correction: float = 1.107):
        # ... initialise affine weight with 1/scale_correction

    def forward(self, x, mode="norm", mean=None, std=None):
        if mode == "norm":
            # compute instance mean and std, blend with running stats
            # normalise, apply affine transform
            return x_norm, (mean_inst, std_inst)
        elif mode == "denorm":
            # reverse affine, then denormalise using stored statistics
            return x_denorm, None</code></pre></div><h4>4.1.6 Interleaved Chronological Split</h4><p>To avoid temporal leakage, the paper uses an interleaved chronological split. The function <code>interleaved_chronological_split</code> divides the total number of windows into 10 equal chunks, then within each chunk splits chronologically 80% train, 10% validation, 10% test. The windows are pooled across chunks, ensuring that test windows always come after training windows in time.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;92d4d1cd-1733-472b-a982-b390b7ac2976&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def interleaved_chronological_split(
    windows: List[Dict],
    split_ratios: Tuple[float, float, float] = (0.8, 0.1, 0.1),
    num_chunks: int = 10,
) -&gt; Dict[str, List[Dict]]:
    # ... divide windows into chunks, split each chunk, concatenate
    return {"train": train, "val": val, "test": test}</code></pre></div><h4>4.1.7 Dataset and DataLoader</h4><p>The <code>StockDataset</code> class wraps the windows, the static graph <code>S</code>, and the <code>RevIN</code> module. Each call to <code>__getitem__</code> returns a dictionary with keys:</p><ul><li><p><code>x0</code>: target log&#8209;returns (normalised), shape <code>(N, Tp)</code></p></li><li><p><code>u</code>: history features, shape <code>(N, Th, U)</code></p></li><li><p><code>S</code>: graph shift operator, shape <code>(N, N)</code></p></li><li><p><code>revin_params</code>: tuple <code>(mean, std)</code> used for later denormalisation during evaluation</p></li></ul><p>The factory function <code>get_stock_dataloaders</code> creates the three DataLoaders with the appropriate batch size (64) and shuffling.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;12964ba1-efc9-4b3e-956b-5cb54a3df916&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">class StockDataset(Dataset):
    def __init__(self, windows: List[Dict], S: torch.Tensor, revin: RevIN):
        self.windows = windows
        self.S = S
        self.revin = revin

    def __getitem__(self, idx):
        w = self.windows[idx]
        # apply RevIN normalisation to target
        x0_norm, (mean, std) = self.revin(w["target"], mode="norm")
        return {
            "x0": x0_norm,
            "u": w["history"],
            "S": self.S,
            "revin_params": (mean, std)
        }</code></pre></div><h3>4.2 Wireless Resource Allocation Pipeline</h3><p>The wireless task is to learn a policy that maps a network configuration (positions, channel gains) to a set of transmit powers (one per transmitter&#8209;receiver pair) that maximise the sum ergodic rate under a minimum rate constraint. The pipeline synthetically generates 128 networks (32 for each of four densities) and uses an expert primal&#8209;dual algorithm to produce optimal allocations.</p><h4>4.2.1 Network Generation</h4><p>The <code>WirelessNetworkGenerator</code> class creates a network with <code>N=400</code> transmitter&#8209;receiver pairs uniformly distributed in a square area. The side length <code>R</code> depends on the density (Table III):</p><p>| Density Index | Side Length R (m) | Density (pairs/km&#178;) | |---------------|-------------------|---------------------| | 0             | 7800              | 6.6                 | | 1             | 7000              | 8.2                 | | 2             | 6300              | 10.1                | | 3             | 5800              | 11.9                |</p><p>A minimum transmitter&#8209;transmitter spacing of 50&#8239;m is enforced by resampling pairs that are too close.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;f26afeac-8abe-411b-a914-78a231e479ba&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">class WirelessNetworkGenerator:
    def __init__(self, config: WRAConfig):
        self.config = config

    def generate_network(self, density_idx: int) -&gt; Dict:
        # ... uniform positions in [0,R]x[0,R], enforce min spacing
        return {
            'pos_tx': pos_tx, 'pos_rx': pos_rx,
            'area_side': R, 'density': density
        }</code></pre></div><h4>4.2.2 Channel Model</h4><p>The <code>ChannelModel</code> implements the paper&#8217;s dual&#8209;slope path loss model with log&#8209;normal shadowing (&#963; = 7&#8239;dB) and Rayleigh fading. The path loss has a breakpoint at 200&#8239;m: exponent 2.0 before, 3.5 after. The <code>compute_path_loss</code> method returns an <code>(N, N)</code> matrix of large&#8209;scale channel gains (linear scale). The <code>sample_fading</code> method generates unit&#8209;mean complex Gaussian fading samples for a given number of links and time slots.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;27e68d58-0714-484d-86d0-85d36dee2cd7&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">class ChannelModel:
    def compute_path_loss(self, pos_tx, pos_rx):
        # ... distances, dual-slope, shadowing
        return gain  # (N, N) tensor

    def sample_fading(self, n_links, n_slots):
        # ... complex Gaussian, squared magnitude
        return fading  # (n_links, n_slots)</code></pre></div><h4>4.2.3 GSO Construction</h4><p>The function <code>build_wra_gso</code> constructs a directed weighted graph shift operator. For each receiver <code>i</code>, it selects the top&#8209;10 strongest interferers (excluding self), sets the edge weight to the channel gain, then applies a log&#8209;normalisation: <code>S = log(1 + gain) / max(log(1 + gain))</code>. Self&#8209;loops are set to 1.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;a92bc307-6988-46de-bb01-6d4dff5802b8&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def build_wra_gso(network, channel_gains, config):
    # ... for each receiver, top-K gains, log-normalisation
    return S  # (N, N) tensor</code></pre></div><h4>4.2.4 Node State Features</h4><p>The <code>compute_node_states</code> function computes the 2&#8209;dimensional node state vector <code>u</code> for each receiver:</p><ul><li><p><code>direct_link_gain</code>: the channel gain from its own transmitter.</p></li><li><p><code>aggregate_interference</code>: sum of channel gains from all other transmitters multiplied by <code>Pmax</code>.</p></li></ul><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;e047f6e0-5eeb-494b-b10b-dbab75893434&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def compute_node_states(network, channel_gains, config):
    direct = channel_gains.diag()
    interference = (channel_gains.sum(dim=1) - direct) * config.Pmax
    u = torch.stack([direct, interference], dim=1)  # (N, 2)
    return u</code></pre></div><h4>4.2.5 Expert Primal&#8209;Dual Algorithm</h4><p>The paper uses a primal&#8209;dual algorithm to solve the constrained optimisation problem (equation&#8239;(28)). The <code>expert_primal_dual</code> function implements a simplified version: it performs projected gradient ascent on the powers <code>p</code> and gradient descent on the dual variables <code>&#955;</code> to maximise the Lagrangian. From the converged trajectory, it collects 200 intermediate allocations (though for simplicity our implementation returns the final allocation). The allocation is centred as <code>x0 = p / Pmax - 1</code>.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;d892c9cc-f668-4b38-9f54-9c6c9dbb747e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def expert_primal_dual(network, channel_gains, config):
    # ... initialise p, lambda
    for it in range(num_iters):
        # compute rates, gradient of Lagrangian, update p, project to [0,Pmax]
        # update lambda with clipping
        # collect samples
    x0 = p / Pmax - 1.0
    return x0.float()</code></pre></div><h4>4.2.6 Dataset and DataLoader</h4><p>The <code>WRADataset</code> wraps the lists of networks, allocations, GSOs, and node states. Each item returns a dictionary with:</p><ul><li><p><code>x0</code>: centred allocation, shape <code>(N,)</code></p></li><li><p><code>S</code>: graph shift operator, shape <code>(N, N)</code></p></li><li><p><code>u</code>: node state features, shape <code>(N, 2)</code></p></li></ul><p>The factory function <code>get_wra_dataloaders</code> generates all 128 networks (32 per density), splits them 20/4/8 per density for train/val/test, and returns three DataLoaders with batch size 800 (as specified in Table&#8239;I).</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;e9245844-b43c-4d7a-b30a-b22b829b368e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">class WRADataset(Dataset):
    def __init__(self, networks, allocations, gsos, node_states):
        # ... store lists

    def __getitem__(self, idx):
        return {
            'x0': self.allocations[idx],
            'S': self.gsos[idx],
            'u': self.node_states[idx]
        }</code></pre></div><h3>4.3 Shape Invariants Summary</h3><p>Both pipelines produce tensors with consistent shapes that the U&#8209;GNN expects:</p><p>| Task  | <code>x0</code> shape       | <code>u</code> shape          | <code>S</code> shape   | Notes                                    | |-------|------------------|--------------------|-------------|------------------------------------------| | Stock | <code>(N, Tp=5)</code>      | <code>(N, Th=20, U=12)</code> | <code>(N, N)</code>    | <code>x0</code> normalised; <code>S</code> static, symmetric   | | WRA   | <code>(N,)</code>           | <code>(N, 2)</code>           | <code>(N, N)</code>    | <code>x0</code> centred; <code>S</code> directed, weighted     |</p><p>These shapes are guaranteed by the dataset classes and are maintained throughout the diffusion forward and reverse processes. The conditioning variable <code>u</code> is always available, and the graph <code>S</code> is used by every strided convolution in the U&#8209;GNN.</p><p>With the data pipelines in place, we can now load batches of <code>(x0, u, S)</code> and feed them into the diffusion model. The next section walks through the core code that implements the forward diffusion, the U&#8209;GNN architecture, and the training loop.</p><h2>5. Code Walkthrough: Core Components</h2><p>With the theoretical foundation laid and the data pipelines ready, we now open the code files themselves. This section walks through each of the four core modules&#8212;<code>diffusion.py</code>, <code>gnn_layers.py</code>, <code>node_selection.py</code>, and <code>ugnn.py</code>&#8212;function by function. Our goal is to show exactly how the paper&#8217;s mathematical descriptions and algorithms become executable Python, highlighting every design decision and shape constraint.</p><h3>5.1 Diffusion Process (<code>diffusion.py</code>)</h3><p><strong>File purpose:</strong> Implements the forward diffusion process (equations (2)&#8211;(4)) and DDIM accelerated sampling (equations (9) and (29)&#8211;(30)).</p><h4><code>DiffusionProcess.__init__</code></h4><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;f53a411b-e70c-4a38-8de6-5694d2327df3&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">class DiffusionProcess:
    def __init__(self, betas: torch.Tensor) -&gt; None:
        if betas.dim() != 1:
            raise ValueError("betas must be a 1D tensor")
        if not torch.all(betas &gt; 0) or not torch.all(betas &lt; 1):
            raise ValueError("betas must be in (0,1)")

        self.K = betas.shape[0]
        self.betas = betas
        self.alphas = 1.0 - betas

        # alpha_bar[t] = prod_{i=1}^{t} alpha_i, alpha_bar[0] = 1
        # shape: (K+1,)
        self.alpha_bar = torch.cat([
            torch.ones(1, dtype=betas.dtype, device=betas.device),
            torch.cumprod(self.alphas, dim=0)
        ], dim=0)

        # Precompute sqrt and sqrt(1 - alpha_bar) for efficiency
        self.sqrt_alpha_bar = self.alpha_bar.sqrt()
        self.sqrt_one_minus_alpha_bar = torch.sqrt(1.0 - self.alpha_bar)

        # For DDIM, precompute 1 - alpha_bar and inverse of sqrt_alpha_bar
        self.one_minus_alpha_bar = 1.0 - self.alpha_bar
        self.inv_sqrt_alpha_bar = 1.0 / self.sqrt_alpha_bar</code></pre></div><p><strong>Explanation:</strong> The constructor takes a 1D tensor <code>betas</code> of length <code>K</code> (K = 500 in the paper). It computes <code>alpha = 1 - beta</code> and then builds <code>alpha_bar</code>, where <code>alpha_bar[t] = prod_{i=1}^{t} alpha_i</code>. A leading 1.0 is prepended so that <code>alpha_bar[0] = 1</code> (no noise at step 0). All subsequent quantities are precomputed and stored as tensors: <code>sqrt_alpha_bar</code>, <code>sqrt_one_minus_alpha_bar</code>, <code>one_minus_alpha_bar</code>, and <code>inv_sqrt_alpha_bar</code>. This avoids recomputing square roots during training and sampling.</p><p><strong>Shape note:</strong> <code>alpha_bar</code> has shape <code>(K+1,)</code> so that indexing with step <code>k</code> (1&#8209;indexed) directly uses position <code>k</code>.</p><h4><code>forward_diffuse</code> (equation (4))</h4><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;90d24f0a-51b9-402d-b6a9-962e8a8a1ee2&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def forward_diffuse(self, x0: torch.Tensor, k: torch.Tensor) -&gt; tuple[torch.Tensor, torch.Tensor]:
    k_idx = k.long()
    sqrt_alpha_k = self.sqrt_alpha_bar[k_idx]
    sqrt_one_minus_alpha_k = self.sqrt_one_minus_alpha_bar[k_idx]

    epsilon = torch.randn_like(x0)
    x_k = sqrt_alpha_k * x0 + sqrt_one_minus_alpha_k * epsilon

    return x_k, epsilon</code></pre></div><p><strong>Explanation:</strong> This is the one-line reparameterization of equation (4): <code>x_k = sqrt(alpha_bar_k) * x0 + sqrt(1 - alpha_bar_k) * epsilon</code>. We look up the precomputed <code>sqrt_alpha_bar</code> at index <code>k</code> (which is a tensor, possibly batched), sample a fresh Gaussian noise <code>epsilon</code>, and return both <code>x_k</code> and <code>epsilon</code> for the noise&#8209;prediction loss. The shape of <code>x_k</code> matches <code>x0</code> exactly.</p><h4><code>sample_sub_grid</code></h4><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;fcbf5266-db15-4575-a061-ca61814d1d07&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def sample_sub_grid(self, n_steps: int) -&gt; List[int]:
    if n_steps &gt; self.K:
        raise ValueError("n_steps cannot exceed K")
    if n_steps &lt;= 0:
        raise ValueError("n_steps must be positive")

    step_indices = torch.linspace(
        self.K, 1, n_steps, dtype=torch.float32
    ).round().long().unique().tolist()

    if len(step_indices) &gt; n_steps:
        step_indices = step_indices[:n_steps]
    elif len(step_indices) &lt; n_steps:
        step_indices = [max(1, int(round(self.K - i * (self.K - 1) / (n_steps - 1))))
                        for i in range(n_steps)]
        step_indices = sorted(set(step_indices), reverse=True)

    step_indices = sorted(step_indices, reverse=True)
    return step_indices</code></pre></div><p><strong>Explanation:</strong> Accelerated DDIM uses a sub&#8209;grid of the full 500&#8209;step schedule. This method generates <code>n_steps</code> indices (e.g., 100) that are roughly evenly spaced from K down to 1. It first tries <code>torch.linspace</code> rounded to integers; if rounding produces duplicates (less than <code>n_steps</code> unique values), it falls back to a simpler arithmetic spacing. The returned list is sorted descending so that the sampling loop starts from the highest noise level.</p><h4><code>ddim_sample</code> (equations (9), (29)&#8211;(30))</h4><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;063a6fa3-2f92-4a50-9027-b0cd7467ba7c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def ddim_sample(self, denoiser: Callable, x_K: torch.Tensor,
                cond: Optional[Dict[str, Any]] = None,
                sub_grid: Optional[List[int]] = None,
                eta: float = 0.2) -&gt; torch.Tensor:
    if sub_grid is None:
        sub_grid = list(range(self.K, 0, -1))

    sub_grid = sorted(sub_grid, reverse=True)
    x = x_K.clone()

    for i, k in enumerate(sub_grid):
        k_prev = sub_grid[i + 1] if i + 1 &lt; len(sub_grid) else 0

        sqrt_alpha_bar_k = self.sqrt_alpha_bar[k]
        sqrt_one_minus_alpha_bar_k = self.sqrt_one_minus_alpha_bar[k]
        inv_sqrt_alpha_bar_k = self.inv_sqrt_alpha_bar[k]

        epsilon_theta = denoiser(x, k, cond)

        # Equation (7): estimate clean signal
        x_hat_0 = (x - sqrt_one_minus_alpha_bar_k * epsilon_theta) * inv_sqrt_alpha_bar_k

        if k_prev &gt; 0:
            sqrt_alpha_bar_prev = self.sqrt_alpha_bar[k_prev]
            one_minus_alpha_bar_prev = self.one_minus_alpha_bar[k_prev]
            one_minus_alpha_bar_k = self.one_minus_alpha_bar[k]

            # Equation (29): sigma_k(eta) adapted for sub-grid
            ratio = (one_minus_alpha_bar_prev / one_minus_alpha_bar_k).clamp(min=0.0)
            sigma = eta * torch.sqrt(ratio) * torch.sqrt(1.0 - self.alpha_bar[k] / self.alpha_bar[k_prev])

            noise = torch.randn_like(x) if eta &gt; 0 else 0.0

            sqrt_one_minus_alpha_bar_prev_minus_sigma2 = torch.sqrt(
                (one_minus_alpha_bar_prev - sigma**2).clamp(min=0.0)
            )

            # Equation (30): DDIM update
            x = sqrt_alpha_bar_prev * x_hat_0 \
                + sqrt_one_minus_alpha_bar_prev_minus_sigma2 * epsilon_theta \
                + sigma * noise
        else:
            x = x_hat_0

    return x</code></pre></div><p><strong>Explanation:</strong> This implements the core sampling loop. Starting from pure noise <code>x_K</code>, it iterates over the (possibly accelerated) sub&#8209;grid in descending order. For each step <code>k</code>:</p><ol><li><p>Predict the noise <code>epsilon_theta</code> using the denoiser (our U&#8209;GNN).</p></li><li><p>Compute <code>x_hat_0</code> via equation (7): <code>(x_k - sqrt(1 - alpha_bar_k) * epsilon_theta) / sqrt(alpha_bar_k)</code>.</p></li><li><p>If we are not at the final step (<code>k_prev &gt; 0</code>), compute the DDIM noise scale <code>sigma</code> using equation (29). The key adaptation for the sub&#8209;grid is that we use <code>alpha_bar[k_prev]</code> and <code>alpha_bar[k]</code> instead of consecutive indices. We clamp intermediate values to avoid numerical issues near zero.</p></li><li><p>Apply the update of equation (30): <code>x_{k_prev} = sqrt(alpha_bar_prev) * x_hat_0 + sqrt(1 - alpha_bar_prev - sigma^2) * epsilon_theta + sigma * noise</code>.</p></li><li><p>At the final step (<code>k_prev = 0</code>), assign <code>x = x_hat_0</code> directly.</p></li></ol><p>The <code>denoiser</code> is a callable that takes <code>(x, k, cond)</code> and returns the predicted noise with the same shape as <code>x</code>. The <code>cond</code> dictionary carries the graph shift operator <code>S</code> and the node states <code>u</code> (and, for the stock task, the historical window).</p><p><strong>Design decision:</strong> We treat the sub&#8209;grid update as a drop&#8209;in replacement for consecutive-step DDIM. The formula for <code>sigma</code> must be adapted because <code>alpha_bar[k_prev]</code> and <code>alpha_bar[k]</code> are not adjacent in the original 500&#8209;step schedule. However, the derivation in Appendix A of the paper is valid for any two indices <code>k_prev &lt; k</code>, and our implementation follows exactly that generalization.</p><div><hr></div><h3>5.2 Strided Graph Convolution (<code>gnn_layers.py</code>)</h3><p><strong>File purpose:</strong> Implements Algorithm 1 and the stacked GNN module used inside the U&#8209;GNN.</p><h4><code>StridedGraphConv.forward</code> (Algorithm 1)</h4><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;a89272b9-54f0-48cd-9d2f-ff085b2fdbd7&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">class StridedGraphConv(nn.Module):
    def __init__(self, in_dim, out_dim, K, gamma, activation=nn.ReLU()):
        super().__init__()
        self.weight = nn.Parameter(torch.randn(K + 1, in_dim, out_dim) * 0.1)
        self.bias = nn.Parameter(torch.zeros(out_dim))
        # ... store K, gamma, activation

    def forward(self, Z_prev: torch.Tensor, S: torch.Tensor,
                D: torch.Tensor) -&gt; torch.Tensor:
        N = S.shape[0]

        # ---- Lift: D^T Z_prev ----
        x = torch.mm(D.T, Z_prev)

        # ---- Tap 0 ----
        A = torch.mm(x, self.weight[0])

        # ---- Iterated unit shifts ----
        for j in range(1, self.gamma * self.K + 1):
            if S.is_sparse:
                x = torch.sparse.mm(S, x)
            else:
                x = torch.mm(S, x)
            if j % self.gamma == 0:
                tap_idx = j // self.gamma
                A = A + torch.mm(x, self.weight[tap_idx])

        # ---- Nonlinearity + bias ----
        A = self.activation(A + self.bias)

        # ---- Reduce: D A ----
        out = torch.mm(D, A)
        return out</code></pre></div><p><strong>Explanation:</strong> This is the direct implementation of Algorithm 1 (Appendix B). The layer processes a signal <code>Z_prev</code> defined on <code>N'</code> active nodes. The steps are:</p><ol><li><p><strong>Lift</strong> (<code>D^T Z_prev</code>): Zero&#8209;pad the signal from <code>N'</code> nodes to all <code>N</code> nodes using the transpose of the binary selection matrix <code>D</code>.</p></li><li><p><strong>Tap 0</strong>: Apply the weight matrix for <code>k=0</code> (the identity shift).</p></li><li><p><strong>Iterated unit shifts</strong>: Run a loop up to <code>gamma * K</code>. In each iteration, apply one sparse shift (<code>S @ x</code>). Every <code>gamma</code>&#8209;th iteration, treat it as a tap: accumulate the contribution <code>x @ W[tap_idx]</code>.</p></li><li><p><strong>Nonlinearity + bias</strong>: Apply activation (ReLU) and bias.</p></li><li><p><strong>Reduce</strong> (<code>D @ A</code>): Project back from <code>N</code> nodes to the <code>N'</code> active nodes using the same selection matrix.</p></li></ol><p>The total number of taps is <code>K+1</code> (tap 0 plus taps at indices <code>gamma, 2*gamma, ..., gamma*K</code>). The stride <code>gamma</code> determines how many unit shifts occur between taps, effectively implementing <code>(S^gamma)^k</code> without ever forming the dense <code>S^gamma</code> matrix. This is the key efficiency trick.</p><p><strong>Shape invariants:</strong> <code>Z_prev: (N', in_dim)</code>, <code>S: (N, N)</code> (sparse or dense), <code>D: (N', N)</code> (binary, each row sums to 1). The output is <code>(N', out_dim)</code>.</p><h4><code>GNNModule</code></h4><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;266147c8-dd44-4715-929b-fe0df1b437c5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">class GNNModule(nn.Module):
    def __init__(self, in_dim, out_dim, K, gamma, L=2, dropout=0.1):
        super().__init__()
        self.layers = nn.ModuleList()
        for i in range(L):
            dim_in = in_dim if i == 0 else out_dim
            self.layers.append(
                StridedGraphConv(dim_in, out_dim, K, gamma, activation=nn.ReLU())
            )
        self.norm = nn.LayerNorm(out_dim)
        self.dropout = nn.Dropout(dropout)

    def forward(self, X, S, D):
        h = X
        for layer in self.layers:
            h = layer(h, S, D)
            h = self.norm(h)
            h = self.dropout(h)
        return h</code></pre></div><p><strong>Explanation:</strong> A simple stack of <code>L</code> strided graph convolution layers (default 2, as per Table I), each followed by LayerNorm and dropout. All layers share the same <code>gamma</code> and <code>D</code> for a given depth level. The first layer expands from <code>in_dim</code> to <code>out_dim</code>; subsequent layers stay at <code>out_dim</code>. The module outputs a signal on the same active node set.</p><div><hr></div><h3>5.3 Learned Node Selection (<code>node_selection.py</code>)</h3><p><strong>File purpose:</strong> Implements the scoring, Gumbel&#8209;Top&#8209;K, and straight&#8209;through estimator described in Section IV&#8209;B and Appendix C.</p><h4><code>NodeSelectionHead</code></h4><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;874d2400-27d1-4ee5-a59b-eb7f17619294&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">class NodeSelectionHead(nn.Module):
    def __init__(self, in_dim, global_dim, hidden_dim=64):
        super().__init__()
        self.score_mlp = nn.Sequential(
            nn.Linear(in_dim + global_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, 1),
        )

    def forward(self, Z, global_emb, n_keep, training=True, epoch=0,
                annealing_params=None):
        N_b = Z.shape[0]
        device = Z.device

        # Equation (24): score = MLP(concat(Z, global_emb))
        concat = torch.cat([Z, global_emb], dim=-1)
        scores = self.score_mlp(concat).squeeze(-1)  # (N_b,)

        if training:
            tau, eps = self._anneal_tau_eps(epoch, annealing_params)

            if eps &gt; 0:
                # Equation (31): add Gumbel noise
                U = torch.rand_like(scores)
                gumbel = -torch.log(-torch.log(U + 1e-8))
                scores = scores + eps * gumbel

            # Equation (25): Top-K selection
            _, indices = torch.topk(scores, k=n_keep, dim=-1)
            indices = indices.sort()[0]

            C = torch.zeros(n_keep, N_b, device=device, dtype=Z.dtype)
            C[torch.arange(n_keep, device=device), indices] = 1.0

            # Equation (32): straight-through estimator
            mask_hard = torch.zeros(N_b, device=device, dtype=Z.dtype)
            mask_hard.scatter_(0, indices, 1.0)
            mask_soft = torch.sigmoid(scores / tau)
            mask = mask_hard + mask_soft - mask_soft.detach()

            # Equation (33): down-sample with mask
            masked_Z = Z * mask.unsqueeze(-1)
            V = masked_Z[indices]
        else:
            # Inference: hard selection only
            _, indices = torch.topk(scores, k=n_keep, dim=-1)
            indices = indices.sort()[0]
            C = torch.zeros(n_keep, N_b, device=device, dtype=Z.dtype)
            C[torch.arange(n_keep, device=device), indices] = 1.0
            V = Z[indices]

        return C, V</code></pre></div><p><strong>Explanation:</strong> The node selection head does exactly what Section IV&#8209;B describes. The scoring MLP <code>Psi_b</code> (equation (24)) takes the concatenation of the encoded feature <code>Z</code> and the global embeddings projected to the current active node set, and outputs a scalar per node. During training:</p><ul><li><p><strong>Gumbel noise</strong> is added to the scores (equation (31)). <code>eps</code> is annealed from 1 down to 0 over training, so exploration decays.</p></li><li><p><strong>Top&#8209;K</strong> picks the <code>n_keep</code> highest&#8209;scoring nodes.</p></li><li><p><strong>Straight&#8209;through estimator</strong> (equation (32)): the forward pass uses a hard one&#8209;hot mask (<code>mask_hard</code>), while gradients flow through a sigmoid surrogate (<code>mask_soft</code>). The trick <code>mask = hard + soft - soft.detach()</code> ensures the forward pass is hard and the backward pass sees <code>soft</code>.</p></li><li><p><strong>Down&#8209;sampled signal</strong> <code>V</code> is computed as <code>C @ (mask * Z)</code> (equation (33)).</p></li></ul><p>At inference, Gumbel noise and the STE are removed: only hard Top&#8209;K is used.</p><h4>Annealing Schedule (<code>_anneal_tau_eps</code>)</h4><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;14d09516-6d31-4bbb-95f4-69bd649e4859&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">@staticmethod
def _anneal_tau_eps(epoch: int, annealing_params: dict) -&gt; (float, float):
    if annealing_params is None:
        Tw, Ta, tau0, tau_min, eps0, eps_min = 0.0, 1.0, 1.0, 0.5, 1.0, 0.0
    else:
        Tw = annealing_params.get('Tw', 0.02)
        Ta = annealing_params.get('Ta', 0.73)
        tau0 = annealing_params.get('tau0', 1.0)
        tau_min = annealing_params.get('tau_min', 0.5)
        eps0 = annealing_params.get('eps0', 1.0)
        eps_min = annealing_params.get('eps_min', 0.0)

    # Equation (34): r(t) = min(max((t - Tw) / Ta, 0), 1)
    r = (epoch - Tw) / Ta
    r = max(0.0, min(1.0, r))

    tau = tau0 + (tau_min - tau0) * r
    eps = eps0 + (eps_min - eps0) * r

    return tau, eps</code></pre></div><p><strong>Explanation:</strong> Implements equation (34) exactly. After a warm&#8209;up phase of <code>Tw</code> epochs (default 0.02 &#215; total = 100 epochs for 5000 total), the ratio <code>r</code> increases linearly from 0 to 1 over <code>Ta</code> epochs (default 0.73 &#215; total = 3650 epochs). Both <code>tau</code> and <code>eps</code> are linearly interpolated from their initial to their final values. After <code>Tw + Ta</code> epochs, <code>r = 1</code> and the parameters stay at their minima.</p><div><hr></div><h3>5.4 U&#8209;GNN Architecture (<code>ugnn.py</code>)</h3><p><strong>File purpose:</strong> Assembles all components into the full U&#8209;GNN as described in Section IV and equations (18)&#8211;(23).</p><h4><code>UGNN.__init__</code></h4><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;1ec73265-29e1-464f-afb2-dc1cbc973400&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">class UGNN(nn.Module):
    def __init__(self, task: str, config: Config):
        super().__init__()
        self.task = task
        self.B = config.B          # 4 resolution levels
        self.rho = config.rho      # 2 (pooling factor)
        self.F0 = config.F0        # 64 (feature dim)
        self.L = config.L          # 2 (GNN layers per block)
        self.K = config.K          # 2 (hops)
        self.gamma_max = config.gamma_max  # 2

        # Core layers
        self.read_in = ReadInLayer(in_dim=config.F_out, out_dim=self.F0)
        self.read_out = ReadOutLayer(in_dim=self.F0, out_dim=self.F_out)
        self.node_state_encoder = NodeStateEncoder(in_dim=config.U, out_dim=self.F0)
        self.step_embedding = StepEmbedding(out_dim=self.F0)

        if task == 'stock':
            self.temporal_mixer = TemporalMixer(...)
        else:
            self.temporal_mixer = nn.Identity()

        # Encoder blocks
        self.encoder_blocks = nn.ModuleList()
        self.fusion_layers_enc = nn.ModuleList()
        self.selection_heads = nn.ModuleList()
        for b in range(1, self.B):
            gamma = self._compute_gamma(b)
            gnn = GNNModule(self.F0, self.F0, self.K, gamma, self.L, self.dropout)
            fusion = FusionLayer(self.F0, 2*self.F0, self.embed_dim, self.task)
            sel = NodeSelectionHead(self.F0, 2*self.F0)
            self.encoder_blocks.append(gnn)
            self.fusion_layers_enc.append(fusion)
            self.selection_heads.append(sel)

        # Bottleneck
        self.bottleneck_gamma = self._compute_gamma(self.B)
        self.bottleneck_gnn = GNNModule(self.F0, self.F0, self.K,
                                         self.bottleneck_gamma, self.L, self.dropout)
        self.bottleneck_fusion = FusionLayer(self.F0, 2*self.F0, self.embed_dim, self.task)

        # Decoder blocks
        self.decoder_blocks = nn.ModuleList()
        self.fusion_layers_dec = nn.ModuleList()
        self.skip_projections = nn.ModuleList()
        for b in range(self.B - 1, 0, -1):
            gamma = self._compute_gamma(b)
            gnn = GNNModule(self.F0, self.F0, self.K, gamma, self.L, self.dropout)
            fusion = FusionLayer(self.F0, 2*self.F0, self.embed_dim, self.task)
            skip_proj = SkipProjection(in_dim=2*self.F0, out_dim=self.F0)
            self.decoder_blocks.append(gnn)
            self.fusion_layers_dec.append(fusion)
            self.skip_projections.append(skip_proj)</code></pre></div><p><strong>Explanation:</strong> The constructor builds the full U&#8209;GNN. It creates <code>B-1</code> encoder blocks (b = 1 to B-1), one bottleneck block (b = B), and <code>B-1</code> decoder blocks (b = B-1 down to 1). Each block has its own <code>GNNModule</code> with a depth&#8209;dependent <code>gamma</code> computed by <code>_compute_gamma</code>, a <code>FusionLayer</code>, and (for encoder blocks) a <code>NodeSelectionHead</code>. The decoder blocks also get a <code>SkipProjection</code> layer to reduce the concatenated skip connection to the standard feature dimension <code>F0</code>.</p><p>The <code>FusionLayer</code> and <code>TemporalMixer</code> are task&#8209;specific: the stock task uses cross&#8209;attention in the fusion and a temporal mixer (dilated conv + self&#8209;attention) on the read&#8209;in features; the WRA task uses simple addition in the fusion and no temporal mixer.</p><h4><code>UGNN.forward</code> (equations (18)&#8211;(23))</h4><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;c931d89b-b986-422e-8a84-80101c1f2e0b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def forward(self, x_k, k, u, S):
    batch, N, F_in = x_k.shape

    # 1. Read-in
    Z0 = self.read_in(x_k)                     # (batch, N, F0)
    U0 = self.node_state_encoder(u)            # (batch, N, F0)
    K0 = self.step_embedding(k).unsqueeze(1).expand(-1, N, -1)  # (batch, N, F0)

    # 2. Global embeddings
    global_emb = torch.cat([U0, K0], dim=-1)   # (batch, N, 2*F0)

    # 3. Temporal mixer (stock only)
    Z0 = self.temporal_mixer(Z0)

    # 4. Node counts
    node_counts = self._compute_target_node_counts(N)

    # 5. Encoder path
    D = torch.eye(N, device=x_k.device).unsqueeze(0).expand(batch, -1, -1)
    Z = Z0
    encoder_outputs = []
    for b_idx in range(self.B - 1):
        next_N = node_counts[b_idx + 1]
        gnn = self.encoder_blocks[b_idx]
        fusion = self.fusion_layers_enc[b_idx]
        sel_head = self.selection_heads[b_idx]

        # Fusion: P_b = &#928;_b(V_b, D_b[U0;K0])
        # (V_b is Z after down-sampling via C_b; here we use D to project global_emb)
        V_b = Z  # For now, V_b = Z (in practice we need C_b; see full code)
        global_emb_active = torch.bmm(D, global_emb)
        P_b = fusion(V_b, global_emb_active)

        Z_b = gnn(P_b, S, D)

        # Selection: C_{b+1}, Z_{b+1}
        C_next, Z_next = sel_head(Z_b, global_emb_active, next_N,
                                   training=self.training,
                                   epoch=self.current_epoch)

        # Update D for next level: D_{b+1} = C_{b+1} @ D_b
        D = torch.bmm(C_next, D)
        encoder_outputs.append(Z_b)
        Z = Z_next

    # 6. Bottleneck
    # ... (similar fusion + GNN on coarsest level)

    # 7. Decoder path
    # ... (up-sample via C^T, skip-concat, fusion, GNN)

    # 8. Read-out
    epsilon_theta = self.read_out(Y_1)
    return epsilon_theta</code></pre></div><p><em>(Note: The above is a simplified sketch. The full code in `ugnn.py` handles batching, indexing, and the decoder loop with proper up&#8209;sampling and skip connections.)</em></p><p><strong>Explanation of the forward pass:</strong></p><ol><li><p><strong>Read&#8209;in and embeddings:</strong> The noisy signal <code>x_k</code> is projected to dimension <code>F0</code> by a node&#8209;wise MLP. Node states <code>u</code> and the diffusion step <code>k</code> are similarly encoded and broadcast to all nodes. The step embedding uses sinusoidal encoding (following DDPM conventions) followed by an MLP.</p></li><li><p><strong>Global embeddings:</strong> <code>U0</code> and <code>K0</code> are concatenated along the feature dimension to form a <code>(batch, N, 2*F0)</code> tensor that carries all conditioning information.</p></li><li><p><strong>Encoder path:</strong> Starting from <code>Z0</code> and an identity <code>D</code> (active nodes = all N nodes), we iterate through the encoder blocks. At each depth <code>b</code>:</p></li></ol><ul><li><p>Project global embeddings onto the current active node set: <code>D_b * global_emb</code>.</p></li><li><p>Fuse (equation (22)): <code>P_b = &#928;_b(V_b, D_b[U0;K0])</code>.</p></li><li><p>Apply the GNN module with stride <code>gamma_b</code>: <code>Z_b = &#934;_{E_b}(P_b, S; D_b, gamma_b)</code>.</p></li><li><p>Select nodes for the next level: <code>C_{b+1}, Z_{b+1} = Selector(Z_b, D_b[U0;K0], N_{b+1})</code>.</p></li><li><p>Update the composite selection matrix: <code>D = C_{b+1} @ D</code> (this builds <code>D_{b+1}</code> as per equation (18)).</p></li></ul><ol start="4"><li><p><strong>Decoder path (omitted for brevity):</strong> The coarsest representation is processed by the bottleneck block. Then, iterating backwards, each decoder block up&#8209;samples via <code>C_{b+1}^T</code>, concatenates with the corresponding encoder output (skip connection), projects via <code>SkipProjection</code> (equation (23)), fuses, and applies a GNN module.</p></li><li><p><strong>Read&#8209;out:</strong> The finest&#8209;resolution decoder output <code>Y_1</code> is projected back to the original feature dimension by a node&#8209;wise MLP.</p></li></ol><p><strong>Key design choices:</strong></p><ul><li><p>The composite <code>D</code> matrices are maintained as dense <code>(batch, N_b, N)</code> tensors for simplicity. In a production system, you might store them as sparse index lists, but dense matrices make the batch matrix multiplications (<code>torch.bmm</code>) straightforward.</p></li><li><p>The <code>FusionLayer</code> is shared between encoder and decoder blocks only in the sense that the same class is instantiated with the same input/output dimensions; the paper mentions weight sharing, but we instantiate separate layers for clarity.</p></li><li><p>The <code>TemporalMixer</code> applies dilated 1D convolutions along the temporal axis (last dimension) followed by multi&#8209;head self&#8209;attention. The output is aggregated by taking the mean over the temporal dimension, producing a single feature vector per node. This is a reasonable default for the paper&#8217;s underspecified temporal encoder.</p></li></ul><div><hr></div><h3>5.5 Putting It All Together</h3><p>The four files work together seamlessly in the training loop (<code>train.py</code>):</p><ol><li><p><code>DiffusionProcess.forward_diffuse</code> generates <code>x_k</code> and <code>epsilon</code> from a clean <code>x_0</code> sampled from the dataloader.</p></li><li><p>The <code>UGNN</code> takes <code>(x_k, k, u, S)</code> and predicts <code>epsilon_theta</code>.</p></li><li><p>The MSE loss between <code>epsilon</code> and <code>epsilon_theta</code> is computed (equation (6)).</p></li><li><p>Gradients flow through the U&#8209;GNN, including through the straight&#8209;through estimator in <code>NodeSelectionHead</code>, and the parameters are updated.</p></li><li><p>After training, <code>DiffusionProcess.ddim_sample</code> uses the trained U&#8209;GNN to generate samples from pure noise, producing the final forecast trajectories.</p></li></ol><p>The modularity of the design&#8212;separate files for diffusion, graph convolution, node selection, and the full architecture&#8212;makes it easy to test, modify, or replace individual components without touching the rest of the pipeline.</p><h2>6. Training and Hyperparameters</h2><p>With the U-GNN architecture and data pipelines in place, we now turn to the training procedure. The paper specifies a detailed set of hyperparameters (Table I and Appendix E) and a training loop that includes a custom learning rate schedule, gradient clipping, automatic mixed precision, and a composite validation criterion. This section walks through the configuration object, the scheduler, the training loop itself, and the checkpointing logic, tying each piece to the generated code.</p><h3>6.1 Configuration from Table I and Appendix E</h3><p>All hyperparameters are centralized in <code>config.py</code>. The base <code>Config</code> class stores values that are shared across tasks, while <code>StockConfig</code> and <code>WRAConfig</code> override task-specific settings. The factory function <code>get_config(task)</code> returns the appropriate instance.</p><p><strong>Key hyperparameters (from Table I and Appendix E):</strong></p><p>| Parameter | Value | Paper Reference | |-----------|-------|-----------------| | Diffusion steps K | 500 | Table I | | \beta_1, \beta_{500} | 10^{-4}, 2\times10^{-2} | Table I, linear schedule | | DDIM steps | 100 | Table I | | DDIM \eta | 0.2 | Table I | | U-GNN depths B | 4 | Table I | | Pooling factor \rho | 2 | Table I | | Channel width F | 64 | Table I | | GNN layers per block L | 2 | Table I | | Filter taps K (hops) | 2 | Table I | | Max stride \gamma_{\max} | 2 | Table I | | Dropout | 0.1 | Table I | | Embedding dimension | 128 | Table I | | Epochs | 5000 | Table I | | Batch size (stock) | 64 | Table I | | Batch size (WRA) | 800 | Table I | | Learning rate | 10^{-3} | Table I | | Weight decay | 0.0 | Table I (AdamW default) | | Warmup fraction | 0.01 (1%) | Appendix E | | Decay fraction | 0.80 (80%) | Appendix E | | Decay to fraction | 0.05 (5% of peak) | Appendix E | | Gradient clip norm | 1.0 | Appendix E | | AMP | True | Appendix E | | Annealing warmup T_w | 0.02 (100 epochs) | Appendix C | | Annealing decay T_a | 0.73 (3650 epochs) | Appendix C | | Initial temperature \tau_0 | 1.0 | Appendix C | | Final temperature \tau_{\min} | 0.5 | Appendix C | | Initial exploration noise \varepsilon_0 | 1.0 | Appendix C | | Final exploration noise \varepsilon_{\min} | 0.0 | Appendix C | | RevIN blend (stock) | 0.7 | Section V-A | | RevIN scale correction (stock) | 1.107 | Section V-A |</p><p>The <code>Config.__init__</code> method calls <code>_compute_betas()</code> and <code>_compute_alphabars()</code> to precompute the linear \beta schedule and the cumulative \bar{\alpha}_k array (equations (2)&#8211;(3)). These are stored as tensors and used later by the <code>DiffusionProcess</code> class.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;47f039b2-eec7-4003-a8ea-67a90dea3b51&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># From config.py (simplified)
class Config:
    def __init__(self, task: str = "stock"):
        self.task = task
        if task == "stock":
            self.batch_size = 64
            self.U = 12
            self.Th = 20
            self.Tp = 5
        elif task == "wra":
            self.batch_size = 800
            self.U = 2
            self.Th = 0
            self.Tp = 1
        else:
            raise ValueError(f"Unknown task: {task}")
        self._compute_betas()
        self._compute_alphabars()

    def _compute_betas(self):
        k = torch.arange(1, self.K + 1, dtype=torch.float32)
        self.betas = self.beta_start + (k - 1) / (self.K - 1) * (self.beta_end - self.beta_start)

    def _compute_alphabars(self):
        self.alphas = 1.0 - self.betas
        self.alphabars = torch.cumprod(self.alphas, dim=0)</code></pre></div><h3>6.2 WarmupCosineLR Scheduler</h3><p>The paper describes a learning rate schedule with a linear warm-up over the first 1% of training steps, followed by cosine decay to 5% of the peak learning rate over the next 80% of steps, and then constant at that minimum. The <code>WarmupCosineLR</code> class in <code>train.py</code> implements this exactly.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;79546aca-99b2-4d95-9024-f5342c68d77d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">class WarmupCosineLR(optim.lr_scheduler.LambdaLR):
    def __init__(self, optimizer, total_steps, warmup_frac=0.01,
                 decay_frac=0.80, min_lr_frac=0.05, last_epoch=-1):
        self.total_steps = total_steps
        self.warmup_steps = int(total_steps * warmup_frac)
        self.decay_steps = int(total_steps * decay_frac)
        self.min_lr_frac = min_lr_frac
        super().__init__(optimizer, self._lr_lambda, last_epoch=last_epoch)

    def _lr_lambda(self, step):
        if step &lt; self.warmup_steps:
            return float(step) / max(1.0, self.warmup_steps)
        elif step &lt; self.warmup_steps + self.decay_steps:
            progress = float(step - self.warmup_steps) / max(1.0, self.decay_steps)
            cosine_decay = 0.5 * (1.0 + np.cos(np.pi * progress))
            return self.min_lr_frac + (1.0 - self.min_lr_frac) * cosine_decay
        else:
            return self.min_lr_frac</code></pre></div><p>The scheduler is instantiated with <code>total_steps = epochs * batches_per_epoch</code>. Because the number of batches per epoch can vary, we estimate it from the training DataLoader. The scheduler is stepped after every optimizer step (i.e., every mini-batch), not every epoch.</p><h3>6.3 Training Loop</h3><p>The <code>train_epoch</code> function implements one epoch of training. For each batch, it:</p><ol><li><p><strong>Samples a diffusion step</strong> k \sim \text{Uniform}\{1, \dots, K\} independently for each element in the batch.</p></li><li><p><strong>Samples noise</strong> \varepsilon \sim \mathcal{N}(0, I) with the same shape as the clean signal x_0.</p></li><li><p><strong>Computes the noisy signal</strong> x_k = \sqrt{\bar{\alpha}_k}\,x_0 + \sqrt{1-\bar{\alpha}_k}\,\varepsilon using the precomputed \bar{\alpha} array (equation (4)).</p></li><li><p><strong>Predicts the noise</strong> \varepsilon_\theta = \text{U-GNN}(x_k, k, u, S).</p></li><li><p><strong>Computes the MSE loss</strong> \|\varepsilon - \varepsilon_\theta\|^2 (equation (6)).</p></li><li><p><strong>Backpropagates</strong> with gradient clipping (max L2 norm = 1.0) and automatic mixed precision (AMP) if CUDA is available.</p></li><li><p><strong>Updates the optimizer</strong> and <strong>steps the scheduler</strong>.</p></li></ol><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;c84b5b70-2752-4188-bc2d-80350961a0eb&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># From train.py (simplified)
def train_epoch(model, dataloader, optimizer, scheduler, diffusion,
                epoch, config, scaler=None):
    model.train()
    if hasattr(model, 'set_epoch'):
        model.set_epoch(epoch)  # for annealing &#964; and &#949;

    total_loss = 0.0
    n_batches = 0

    for batch in dataloader:
        if config.task == 'stock':
            x0, u, S, _ = batch
        else:
            x0, S, u = batch

        x0 = x0.to(device, non_blocking=True)
        u = u.to(device, non_blocking=True)
        S = S.to(device, non_blocking=True)

        batch_size = x0.size(0)
        k = torch.randint(1, config.K + 1, (batch_size,), device=device)
        noise = torch.randn_like(x0)
        x_k = diffusion.forward_diffuse(x0, k, noise)

        with autocast(enabled=scaler is not None):
            pred_noise = model(x_k, k, u, S)
            loss = F.mse_loss(pred_noise, noise)

        if scaler is not None:
            scaler.scale(loss).backward()
            scaler.unscale_(optimizer)
            torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
            scaler.step(optimizer)
            scaler.update()
        else:
            loss.backward()
            torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
            optimizer.step()

        optimizer.zero_grad(set_to_none=True)
        scheduler.step()

        total_loss += loss.item()
        n_batches += 1

    return total_loss / max(1, n_batches)</code></pre></div><p><strong>Key details:</strong></p><ul><li><p>The <code>DiffusionProcess.forward_diffuse</code> method implements equation (4) by indexing into the precomputed <code>alpha_bar</code> tensor with the sampled k values.</p></li><li><p>Gradient clipping uses <code>torch.nn.utils.clip_grad_norm_</code> with <code>max_norm=1.0</code> and the default L2 norm.</p></li><li><p>AMP is enabled via <code>torch.cuda.amp.GradScaler</code> and <code>autocast</code>. The scaler is created only if CUDA is available.</p></li><li><p>The model&#8217;s <code>set_epoch</code> method (if present) is called at the start of each epoch to update the temperature \tau and exploration noise \varepsilon for the node selection heads, following the annealing schedule of Appendix C.</p></li></ul><h3>6.4 Annealing of &#964; and &#949;</h3><p>The node selection heads use a Gumbel-Top-K mechanism during training, with temperature \tau and exploration noise \varepsilon that are annealed over time. The schedule (equation (34)) has three phases:</p><ul><li><p><strong>Warm-up</strong> (first T_w = 0.02 \times 5000 = 100 epochs): \tau and \varepsilon are held at their initial values (1.0 and 1.0).</p></li><li><p><strong>Anneal</strong> (next T_a = 0.73 \times 5000 = 3650 epochs): both parameters decay linearly to their final values (0.5 and 0.0).</p></li><li><p><strong>Constant</strong> (remaining epochs): stay at final values.</p></li></ul><p>In the code, the <code>NodeSelectionHead</code> class (in <code>node_selection.py</code>) implements <code>anneal_tau_eps(epoch)</code> which computes the current values based on the schedule. The <code>UGNN</code> model&#8217;s <code>set_epoch</code> method propagates the epoch number to all selection heads so they can update their internal parameters before each forward pass.</p><h3>6.5 Composite Validation Criteria</h3><p>The paper uses a task-specific composite score to select the best checkpoint during training. The composite criterion combines multiple validation metrics into a single scalar.</p><p><strong>For stock forecasting:</strong> The composite score is a weighted sum of the noise-prediction loss, the gap between training and validation loss (to detect overfitting), and the CRPS (Continuous Ranked Probability Score). Lower is better.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;b0043c61-ca1e-4fe5-b643-32c80ec3c8e9&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def composite_criterion_stock(val_metrics):
    loss = val_metrics.get('loss', 1.0)
    crps = val_metrics.get('crps', 1.0)
    # train-val gap not computed here, assume 0
    return loss + 0.5 * crps</code></pre></div><p><strong>For wireless resource allocation:</strong> The composite score combines the mean ergodic rate and the feasibility (fraction of users meeting the minimum rate constraint). Higher is better, so we negate the sum for minimization.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;d2286b7f-e666-4912-a06f-021027316ca0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def composite_criterion_wra(val_metrics):
    mean_rate = val_metrics.get('mean_rate', 0.0)
    feasibility = val_metrics.get('feasibility', 0.0)
    return -(mean_rate + feasibility * 2.0)  # weight feasibility more</code></pre></div><p>These functions are called after each validation epoch. The <code>train</code> function tracks the best composite score and saves the corresponding model checkpoint.</p><h3>6.6 Checkpointing</h3><p>At the end of each epoch, if the composite score improves (lower for stock, higher for WRA), the current model state, optimizer state, scheduler state, epoch number, and score are saved to a dictionary. After training completes, the best checkpoint is written to <code>checkpoints/best_model.pth</code>.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;03984698-9614-4a36-bd68-609cedc09d7b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">if (config.task == 'stock' and score &lt; best_score) or \
   (config.task == 'wra' and score &gt; best_score):
    best_score = score
    best_epoch = epoch
    best_state = {
        'model_state_dict': model.state_dict(),
        'optimizer_state_dict': optimizer.state_dict(),
        'scheduler_state_dict': scheduler.state_dict(),
        'epoch': epoch,
        'score': score,
        'config': config
    }</code></pre></div><p>This checkpoint can later be loaded for evaluation or visualization.</p><h3>6.7 Summary</h3><p>The training configuration and loop are designed to match the paper&#8217;s specifications exactly. The <code>Config</code> class centralizes all hyperparameters, the <code>WarmupCosineLR</code> scheduler implements the custom learning rate schedule, and the <code>train_epoch</code> function handles the diffusion-specific steps (sampling k, computing x_k, predicting noise) along with gradient clipping and AMP. The annealing of node selection parameters is integrated via the model&#8217;s <code>set_epoch</code> hook, and the composite validation criteria guide checkpoint selection. This modular design makes it straightforward to reproduce the paper&#8217;s experiments and to adapt the code to new tasks.</p><h2>7. Evaluation Metrics and Baselines</h2><p>A generative model is only as useful as the metrics we use to judge it. The paper evaluates its U-GNN diffusion model against two sets of baselines and a battery of metrics that go far beyond simple point-prediction errors. For stock forecasting, the goal is to assess both the accuracy of the mean prediction and the fidelity of the full predictive distribution. For wireless resource allocation, the focus is on the ergodic sum rate, feasibility, and the gap to an expert algorithm. This section explains every metric and baseline, shows the corresponding code in <code>eval.py</code> and <code>baselines.py</code>, and describes how to compare the U-GNN against the baselines.</p><h3>7.1 Stock Forecasting Metrics</h3><p>The paper reports eight metrics for the S&amp;P 500 task (Table II). They fall into three categories: distributional accuracy (CRPS, MIS90), point-prediction accuracy (RMSE, MAE, direction accuracy), and stylized-fact gaps (volatility clustering, momentum, excess kurtosis). All metrics are computed on the <em>returns</em> (log-returns) unless otherwise noted.</p><h4>7.1.1 Continuous Ranked Probability Score (CRPS)</h4><p>CRPS measures how well the empirical distribution of generated samples matches the true target. It is defined as the integral of the squared difference between the empirical CDF of the samples and the indicator function of the target:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\text{CRPS} = \\int_{-\\infty}^{\\infty} \\bigl( F_{\\text{samples}}(x) - \\mathbb{1}\\{x \\ge \\text{target}\\} \\bigr)^2 \\, dx&quot;,&quot;id&quot;:&quot;EB53A23CFE&quot;}" data-component-name="LatexBlockToDOM"></div><p>For a set of M samples, the empirical CDF is a step function, and the integral can be computed analytically using the sorted samples. The implementation in <code>StockEvaluator.compute_crps</code> uses the formula:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\text{CRPS} = \\frac{1}{M} \\sum_{i=1}^{M} |s_{(i)} - y| - \\frac{1}{2M^2} \\sum_{i,j} |s_{(i)} - s_{(j)}|&quot;,&quot;id&quot;:&quot;4087D9222E&quot;}" data-component-name="LatexBlockToDOM"></div><p>where s_{(i)} are the sorted samples and y is the target. The double sum is computed efficiently using the fact that for sorted samples, \sum_{i,j} |s_{(i)} - s_{(j)}| = 2 \sum_{i=1}^{M} (2i - M - 1) s_{(i)}.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;93f8c086-b87d-478c-bd28-3d57e98daca1&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">@staticmethod
def compute_crps(samples: np.ndarray, target: np.ndarray) -&gt; float:
    samples = np.asarray(samples)
    target = np.asarray(target)
    if samples.ndim == 3:
        num_samples = samples.shape[0]
    else:
        num_samples = 1
        samples = samples[np.newaxis, ...]
    m = samples.shape[1] * samples.shape[2]
    samples_flat = samples.reshape(num_samples, -1)
    target_flat = target.reshape(-1)
    sorted_samples = np.sort(samples_flat, axis=0)
    M = num_samples
    abs_diff = np.mean(np.abs(sorted_samples - target_flat[np.newaxis, :]), axis=0)
    weights = 2 * (2 * np.arange(1, M + 1) - M - 1)
    sum_abs_diff_sorted = np.sum(weights[:, np.newaxis] * sorted_samples, axis=0) / (M ** 2)
    crps_per_element = abs_diff - 0.5 * sum_abs_diff_sorted
    return float(np.mean(crps_per_element))</code></pre></div><h4>7.1.2 Mean Interval Score for 90% Prediction Interval (MIS90)</h4><p>MIS90 evaluates the sharpness and calibration of the 90% prediction interval. It penalizes intervals that are too wide and intervals that miss the target:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\text{MIS} = (u - l) + \\frac{2}{\\alpha}(l - y)\\mathbb{1}\\{y < l\\} + \\frac{2}{\\alpha}(y - u)\\mathbb{1}\\{y > u\\}&quot;,&quot;id&quot;:&quot;0085FD07C5&quot;}" data-component-name="LatexBlockToDOM"></div><p>where l and u are the 5th and 95th percentiles of the samples, and \alpha = 0.1. The implementation computes these percentiles using <code>np.percentile</code>.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;88e1aa31-8501-4fb4-90be-31da9e220efa&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">@staticmethod
def compute_mis90(samples: np.ndarray, target: np.ndarray) -&gt; float:
    samples = np.asarray(samples)
    target = np.asarray(target)
    if samples.ndim == 2:
        samples = samples[np.newaxis, ...]
    alpha = 0.1
    lower = np.percentile(samples, 5, axis=0)
    upper = np.percentile(samples, 95, axis=0)
    width = upper - lower
    penalty_low = (2.0 / alpha) * (lower - target) * (target &lt; lower)
    penalty_high = (2.0 / alpha) * (target - upper) * (target &gt; upper)
    mis = width + penalty_low + penalty_high
    return float(np.mean(mis))</code></pre></div><h4>7.1.3 RMSE, MAE, and Direction Accuracy</h4><p>These are standard point-prediction metrics. The mean prediction is the average over the M samples. RMSE and MAE are computed between this mean and the target. Direction accuracy measures the fraction of elements where the sign of the mean prediction matches the sign of the target (ignoring zeros).</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;246e8cec-1051-4167-9cfa-4fb0ce7c6c68&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">@staticmethod
def compute_rmse(samples: np.ndarray, target: np.ndarray) -&gt; float:
    samples = np.asarray(samples)
    target = np.asarray(target)
    if samples.ndim == 3:
        mean_pred = np.mean(samples, axis=0)
    else:
        mean_pred = samples
    return float(np.sqrt(np.mean((mean_pred - target) ** 2)))

@staticmethod
def compute_dir_accuracy(samples: np.ndarray, target: np.ndarray) -&gt; float:
    samples = np.asarray(samples)
    target = np.asarray(target)
    if samples.ndim == 3:
        mean_pred = np.mean(samples, axis=0)
    else:
        mean_pred = samples
    correct = (np.sign(mean_pred) == np.sign(target))
    return float(np.mean(correct))</code></pre></div><h4>7.1.4 Stylized-Fact Gaps</h4><p>Financial time series exhibit well-known statistical regularities called <em>stylized facts</em>. The paper measures how well the generated samples reproduce three of them:</p><ul><li><p><strong>Volatility clustering</strong>: the tendency for large changes to be followed by large changes. Quantified as the lag-1 autocorrelation of squared returns.</p></li><li><p><strong>Momentum</strong>: the tendency for returns to be positively autocorrelated at short lags. Quantified as the lag-1 autocorrelation of returns.</p></li><li><p><strong>Excess kurtosis</strong>: the fat-tailed nature of return distributions. Quantified as the sample excess kurtosis (kurtosis minus 3).</p></li></ul><p>For each metric, the <em>gap</em> is the absolute difference between the value computed on the target and the value computed on the pooled generated samples. The implementation in <code>StockEvaluator</code> computes these using NumPy.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;c95042c4-0d59-4351-addc-c1deebd31f12&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">@staticmethod
def compute_volatility_gap(samples: np.ndarray, target: np.ndarray) -&gt; float:
    samples = np.asarray(samples)
    target = np.asarray(target)
    target_flat = target.ravel()
    samples_flat = samples.ravel()

    def autocorr_lag1(x):
        if len(x) &lt; 2:
            return 0.0
        x0 = x[:-1]
        x1 = x[1:]
        std0 = np.std(x0)
        std1 = np.std(x1)
        if std0 == 0 or std1 == 0:
            return 0.0
        return float(np.corrcoef(x0, x1)[0, 1])

    target_vol = autocorr_lag1(target_flat ** 2)
    samples_vol = autocorr_lag1(samples_flat ** 2)
    return float(np.abs(target_vol - samples_vol))</code></pre></div><h3>7.2 Wireless Resource Allocation Metrics</h3><p>For the WRA task, the paper reports ergodic sum rate, feasibility, and the gap to the expert primal-dual algorithm (Table IV).</p><h4>7.2.1 Ergodic Sum Rate</h4><p>The ergodic sum rate is the average over fading realizations of the sum of per-user rates. The per-user rate is (equation (26)):</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;r_j = \\log_2(1 + \\text{SINR}_j)&quot;,&quot;id&quot;:&quot;89A58396BA&quot;}" data-component-name="LatexBlockToDOM"></div><p>where \text{SINR}_j = \frac{p_j g_{jj}}{\sum_{i \neq j} p_i g_{ij} + \sigma^2}. The ergodic estimate (equation (27)) averages over T = 500 fading realizations, with the allocation held constant for T_0 = 5 slots. The implementation in <code>WRAEvaluator.compute_ergodic_rate</code> generates independent Rayleigh fading (exponential squared magnitude) and computes the SINR for each slot.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;ffda163a-5c27-42f1-a9b4-52a1a5c10917&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">@staticmethod
def compute_ergodic_rate(
    allocations: Union[np.ndarray, torch.Tensor],
    H: Union[np.ndarray, torch.Tensor],
    noise_power: float = 1e-11,
    T: int = 500,
    T0: int = 5
) -&gt; float:
    allocations = np.asarray(allocations).flatten()
    H = np.asarray(H)
    N = len(allocations)
    # Uncenter if needed (assume Pmax=1)
    if np.any(allocations &lt; 0):
        allocations = allocations + 1.0
    allocations = np.clip(allocations, 0.0, 1.0)
    # Generate fading: (N, N, T) exponential with mean 1
    fading = np.random.exponential(scale=1.0, size=(N, N, T)).astype(np.float64)
    H = np.abs(H)
    g = H[:, :, np.newaxis] * fading
    g_self = g.diagonal(axis1=0, axis2=1)
    p = allocations[:, np.newaxis, np.newaxis]
    total_received = np.sum(p * g, axis=0)
    self_power = p[:, 0, 0] * g_self
    interference = total_received - self_power
    sinr = self_power / (interference + noise_power)
    rate = np.log2(1.0 + np.maximum(sinr, 0.0))
    sum_rate_per_slot = np.sum(rate, axis=0)
    mean_sum_rate = np.mean(sum_rate_per_slot)
    return float(mean_sum_rate)</code></pre></div><h4>7.2.2 Feasibility</h4><p>Feasibility is the fraction of users whose ergodic rate meets a minimum rate constraint (e.g., 0.5 bits/s/Hz). The implementation computes the per-user ergodic rate (mean over fading) and checks the constraint.</p><h4>7.2.3 Gap to Expert</h4><p>The paper reports the percentage gap between the U-GNN and the expert primal-dual algorithm at the 1st, 5th, 10th percentiles and the mean. The gap is defined as:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\text{gap}_p = \\frac{\\text{expert}_p - \\text{U-GNN}_p}{\\text{expert}_p} \\times 100\\%&quot;,&quot;id&quot;:&quot;D25874DA99&quot;}" data-component-name="LatexBlockToDOM"></div><p>where p indicates the percentile of the ergodic sum rate distribution across test networks.</p><h3>7.3 Baselines</h3><p>The paper compares against three baselines, implemented in <code>baselines.py</code>.</p><h4>7.3.1 Geometric Random Walk (GRW)</h4><p>For stock forecasting, the GRW assumes log-returns are independent Gaussian with variance estimated from the history window. The <code>GeometricRandomWalk.forecast</code> method computes the per-stock variance from the 20-day history and samples M independent trajectories.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;1f682ad1-605a-41b6-8884-e39a34b07d39&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">class GeometricRandomWalk:
    def forecast(self, history: torch.Tensor, steps: int, num_samples: int = 1) -&gt; torch.Tensor:
        if history.dim() == 3:
            history = history.squeeze(-1)
        N, Th = history.shape
        var = history.var(dim=1, unbiased=True, keepdim=False) + 1e-8
        std = torch.sqrt(var)
        noise = torch.randn(num_samples, N, steps, generator=self.rng, device=history.device, dtype=history.dtype)
        samples = noise * std.view(1, N, 1)
        return samples</code></pre></div><h4>7.3.2 Full Power (FP) and Average Power (AP)</h4><p>For WRA, the FP baseline sets every transmitter to maximum power P_{\text{max}}, so the centered allocation is 0. The AP baseline sets every transmitter to P_{\text{max}}/2, so the centered allocation is -0.5. Both are deterministic and return tensors of the appropriate shape.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;86f1c389-335c-4322-91df-985fb8d9fff6&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">class FullPower:
    def allocate(self, network: dict = None, H: torch.Tensor = None, num_samples: int = 1) -&gt; torch.Tensor:
        N = 400
        if H is not None:
            N = H.shape[0]
        device = H.device if H is not None else torch.device('cpu')
        return torch.zeros(num_samples, N, 1, device=device, dtype=torch.float32)

class AveragePower:
    def allocate(self, network: dict = None, H: torch.Tensor = None, num_samples: int = 1) -&gt; torch.Tensor:
        N = 400
        if H is not None:
            N = H.shape[0]
        device = H.device if H is not None else torch.device('cpu')
        return torch.full((num_samples, N, 1), -0.5, device=device, dtype=torch.float32)</code></pre></div><h3>7.4 Comparing U-GNN Against Baselines</h3><p>To compare the U-GNN against the baselines, we generate M = 100 samples from each method (for GRW, this is natural; for FP/AP, we repeat the deterministic allocation). Then we compute all metrics using the <code>StockEvaluator</code> or <code>WRAEvaluator</code>. The <code>evaluate_all</code> method returns a dictionary of metrics, which can be printed as a table (matching Table II or Table IV).</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;ff9158a4-b026-4df8-b1e5-bdcea94ca666&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">evaluator = StockEvaluator()
metrics_ugnn = evaluator.e

## 8. Results and Figures

A generative model&#8217;s output is inherently high-dimensional: for stock forecasting, we produce a distribution over future price paths for hundreds of stocks; for wireless resource allocation, we produce a distribution over power vectors. To communicate the quality of these distributions, the paper relies on a set of carefully designed figures that go beyond simple scalar metrics. This section walks through the four plotting functions in `visualize.py` that reproduce the paper&#8217;s key visual results: forecasting trajectories, empirical CDFs, distribution diagnostics (histogram + Q-Q plot), and a WRA performance bar chart. Each function is explained in detail, with code snippets and guidance on how to call them from the main evaluation script.

### 8.1 Overview of the Plotting Functions

The file `visualize.py` contains four public functions:

- `plot_forecast_trajectories` &#8211; reproduces the paper&#8217;s &#8220;forecasting trajectories&#8221; figures (e.g., Figure 2 in the paper, showing historical prices, generated samples, and actual target for selected stocks).
- `plot_cdf` &#8211; plots the empirical cumulative distribution function of the pooled generated samples versus the pooled target values, corresponding to the CDF panels in the paper&#8217;s distribution diagnostics.
- `plot_distribution_diagnostics` &#8211; creates a two-panel figure with an overlaid histogram and a Q-Q plot, matching the paper&#8217;s density and quantile comparisons.
- `plot_wra_bar` &#8211; produces a grouped bar chart comparing ergodic rate metrics (p1, p5, p10, mean) across methods (U-GNN, Expert, FP, AP), with an optional secondary axis for percentage gaps and an inset for feasibility. This reproduces the paper&#8217;s WRA performance bar chart (e.g., Figure 4).

All functions accept NumPy arrays (or torch tensors, which are automatically converted via the internal `_to_numpy` helper) and return a `matplotlib.figure.Figure` object that can be saved or displayed.

### 8.2 Forecasting Trajectories

The function `plot_forecast_trajectories` visualises the model&#8217;s predictive distribution for a few selected stocks. It plots:
- The historical log-returns (or prices) for the last `Th` days (blue line).
- A random subset of generated trajectories (gray lines, low opacity).
- The mean of all generated trajectories (red line).
- The actual target values (green dashed line).

The time axis is split into history (days 0 to Th-1) and forecast (days Th to Th+Tp-1).
</code></pre></div><p>def plot<em>forecast</em>trajectories( prices: np.ndarray, samples: np.ndarray, target: np.ndarray, stock<em>indices: Optional[List[int]] = None, num</em>trajectories: int = 20, figsize: Tuple[float, float] = (10, 6) ) -&gt; plt.Figure:</p><p>Use the button or URL below to download the complete Python source code.</p>
      <p>
          <a href="https://onepagecode.substack.com/p/forecasting-stock-prices-with-diffusion">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Quantitative Trading Model from Paper to Python: Building an Attention-BiLSTM Strategy for Gold and Bitcoin]]></title><description><![CDATA[A step-by-step tutorial on translating academic paper concepts&#8212;including temporal attention, streak-based position sizing, and greedy portfolio optimization&#8212;into production-ready Python code.]]></description><link>https://onepagecode.substack.com/p/quantitative-trading-model-from-paper</link><guid isPermaLink="false">https://onepagecode.substack.com/p/quantitative-trading-model-from-paper</guid><dc:creator><![CDATA[Onepagecode]]></dc:creator><pubDate>Fri, 10 Jul 2026 12:02:56 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!aq85!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0996af8d-98d9-422f-9f4b-8ac6fc43de2d_1406x1372.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>What This Article Builds</h2><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!F5TF!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef206556-2fb9-41b0-9dc7-b20367aef72e_758x234.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!F5TF!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef206556-2fb9-41b0-9dc7-b20367aef72e_758x234.png 424w, https://substackcdn.com/image/fetch/$s_!F5TF!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef206556-2fb9-41b0-9dc7-b20367aef72e_758x234.png 848w, https://substackcdn.com/image/fetch/$s_!F5TF!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef206556-2fb9-41b0-9dc7-b20367aef72e_758x234.png 1272w, https://substackcdn.com/image/fetch/$s_!F5TF!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef206556-2fb9-41b0-9dc7-b20367aef72e_758x234.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!F5TF!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef206556-2fb9-41b0-9dc7-b20367aef72e_758x234.png" width="758" height="234" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ef206556-2fb9-41b0-9dc7-b20367aef72e_758x234.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:234,&quot;width&quot;:758,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:112430,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/206399283?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef206556-2fb9-41b0-9dc7-b20367aef72e_758x234.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!F5TF!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef206556-2fb9-41b0-9dc7-b20367aef72e_758x234.png 424w, https://substackcdn.com/image/fetch/$s_!F5TF!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef206556-2fb9-41b0-9dc7-b20367aef72e_758x234.png 848w, https://substackcdn.com/image/fetch/$s_!F5TF!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef206556-2fb9-41b0-9dc7-b20367aef72e_758x234.png 1272w, https://substackcdn.com/image/fetch/$s_!F5TF!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef206556-2fb9-41b0-9dc7-b20367aef72e_758x234.png 1456w" sizes="100vw" fetchpriority="high"></picture><div></div></div></a></figure></div><p>The paper proposes a quantitative trading pipeline for gold and Bitcoin. It combines price forecasting with an attention-enhanced bidirectional LSTM, position sizing based on empirical consecutive rises and declines, transaction-cost sensitivity analysis, and claimed use of VaR and a modified greedy algorithm for trading decisions. Several benchmark forecasting models are compared, with Att-BiLSTM reported as the best predictor. The trading results claim a final value of approximately $646 for a $500 gold allocation and $215,487 for a $500 Bitcoin allocation, although the backtest protocol and several model details are insufficiently specified.</p><div><hr></div><p><strong>The code is my implementation based on what the paper discusses. Download the source code using the button at the end of this article!</strong></p><p><strong>Also the code might have some errors, because I didn&#8217;t found anything online related to paper, everything is implemented by myself, so download it and understand. With article and the code.</strong></p><p>This is the research paper I tried to implement: https://www.researchgate.net/publication/364569615_Research_on_Quantitative_Trading_Model</p><h3>Our Book is out on openclaw: </h3><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.amazon.com/dp/B0H1B37D9M&quot;,&quot;text&quot;:&quot;Get Your Copy&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.amazon.com/dp/B0H1B37D9M"><span>Get Your Copy</span></a></p><div><hr></div><h2>Implementation Assumptions</h2><ul><li><p>Historical gold and Bitcoin datasets are not supplied; the implementation will accept local CSV or pandas DataFrame inputs and include a deterministic synthetic-data generator for demonstrations only.</p></li><li><p>Prices are assumed to be positive, chronologically ordered observations with one row per asset and an explicit timestamp.</p></li><li><p>Standard simple return, (p<em>t-p</em>{t-1})/p_{t-1}, is used by default because the paper's printed denominator is ambiguous; the choice is configurable and documented.</p></li><li><p>The primary target is a one-step-ahead normalized price or return forecast. A three-step vector forecast option is supported because the paper's conclusion mentions three-day forecasts.</p></li><li><p>The primary forecasting model is an attention-enhanced BiLSTM with a configurable learned query vector, 20% dropout, MAE loss, and RMSprop optimizer.</p></li><li><p>The paper-style 70:30 chronological split is supported for comparison, while walk-forward evaluation is the preferred protocol for trading conclusions.</p></li><li><p>Equations (5) through (8), VaR operation, and the modified greedy algorithm are implemented as explicit reconstructions and will never be presented as exact reproductions.</p></li><li><p>Transaction fees apply to both buys and sells by default, use decimal rates such as 0.002 for 0.2%, and are configurable.</p></li><li><p>The backtest uses next-bar execution after a signal, finite position-addition levels, no leverage by default, and optional slippage.</p></li><li><p>The reported final values of approximately 646 USD for gold and 215487 USD for Bitcoin are recorded as unverified reference claims rather than expected test outputs.</p></li><li><p>Deep-learning, statistical, and optional benchmark dependencies are isolated so core data, sizing, risk, and backtesting components remain usable without every optional package installed.</p></li></ul><h1>1. The Paper&#8217;s Core Idea: Forecast Timing and Manage Position Size Separately</h1><p>The paper is trying to answer two different trading questions:</p><p><strong>When should the strategy trade?</strong></p><p><strong>How much capital should it deploy when it trades?</strong></p><p>Those questions are related, but they are not the same problem. A forecasting model can estimate that gold or Bitcoin may rise, yet it does not automatically determine whether the strategy should invest $10, $100, or the entire available budget. Conversely, a position-sizing rule can specify how much to buy after a decline, but it needs a signal to decide whether buying is appropriate at that moment.</p><p>The implementation keeps these responsibilities separate. The forecasting component produces information about a possible future price. The signal layer converts that forecast into a buy, sell, add, or hold decision. The sizing layer proposes an amount. Risk control can reject or reduce that amount. Finally, the execution and accounting layer applies transaction costs, updates cash and holdings, and measures the resulting portfolio.</p><p>This separation is especially important because several parts of the paper are incompletely specified. The paper gives a broad design involving an attention-enhanced BiLSTM, consecutive-rise and decline analysis, exponential position sizing, VaR, and a modified greedy algorithm. It does not fully define every interface between those components. A modular Python implementation makes each interpretation visible instead of hiding assumptions inside one large trading function.</p><h2>The complete data-to-portfolio flow</h2><p>The project&#8217;s README presents the intended architecture as a pipeline:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;45fc8f13-9472-423a-b1ed-5862ae608001&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">local prices
    -&gt; cleaning and chronological split
    -&gt; leakage-safe sliding windows
    -&gt; forecast model
    -&gt; buy / sell / hold signal
    -&gt; finite exponential position-size schedule
    -&gt; historical VaR filter
    -&gt; reconstructed greedy action selection
    -&gt; next-bar execution with fees and slippage
    -&gt; portfolio valuation and performance metrics</code></pre></div><p>Each stage has a distinct responsibility:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Totb!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e7eebf1-25be-4651-b16f-b0483ba328ee_1564x830.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Totb!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e7eebf1-25be-4651-b16f-b0483ba328ee_1564x830.png 424w, https://substackcdn.com/image/fetch/$s_!Totb!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e7eebf1-25be-4651-b16f-b0483ba328ee_1564x830.png 848w, https://substackcdn.com/image/fetch/$s_!Totb!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e7eebf1-25be-4651-b16f-b0483ba328ee_1564x830.png 1272w, https://substackcdn.com/image/fetch/$s_!Totb!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e7eebf1-25be-4651-b16f-b0483ba328ee_1564x830.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Totb!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e7eebf1-25be-4651-b16f-b0483ba328ee_1564x830.png" width="1456" height="773" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4e7eebf1-25be-4651-b16f-b0483ba328ee_1564x830.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:773,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:166812,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/206399283?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e7eebf1-25be-4651-b16f-b0483ba328ee_1564x830.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Totb!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e7eebf1-25be-4651-b16f-b0483ba328ee_1564x830.png 424w, https://substackcdn.com/image/fetch/$s_!Totb!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e7eebf1-25be-4651-b16f-b0483ba328ee_1564x830.png 848w, https://substackcdn.com/image/fetch/$s_!Totb!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e7eebf1-25be-4651-b16f-b0483ba328ee_1564x830.png 1272w, https://substackcdn.com/image/fetch/$s_!Totb!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e7eebf1-25be-4651-b16f-b0483ba328ee_1564x830.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The paper&#8217;s primary model is the attention-enhanced BiLSTM, or <strong>Att-BiLSTM</strong>. It processes a historical sequence and uses temporal attention to produce a representation for forecasting. The input sequence is built from a sliding window, reported as 100 observations with stride one. The resulting prediction is not itself a trade: it must be aligned with a timestamp and passed to a decision policy.</p><p>The position-management component is conceptually independent of the neural network. It analyzes gains and declines, estimates statistics such as representative gain and decline magnitudes, and uses an exponential schedule to increase later additions. Because the paper&#8217;s position-sizing equations are corrupted or incomplete in the extracted source, the Python project treats this as a documented normalized exponential reconstruction rather than an exact transcription.</p><h2>Why the boundaries matter</h2><p>A single function that loads prices, trains a model, decides an order, and reports profit would be difficult to audit. It would also make it unclear whether future information entered the decision process. The generated package instead plans separate modules for data, models, streak analysis, sizing, risk, strategy, and backtesting.</p><p>For example, <code>DataConfig</code> makes the forecasting window explicit:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;69836615-110c-4ac4-96de-5477655cb65e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">@dataclass
class DataConfig:
    """Configuration for local historical-price preparation."""

    timestamp_column: str = "timestamp"
    price_column: str = "price"
    asset_column: Optional[str] = None
    window_length: int = 100
    stride: int = 1
    return_definition: str = "simple"
    zero_return_policy: str = "break"</code></pre></div><p>The values <code>window_length=100</code> and <code>stride=1</code> reflect the paper&#8217;s stated sliding-window setup. Other fields expose choices that the paper does not settle, such as how returns are defined and how zero returns affect streaks. Making these values configuration fields means a later experiment can record exactly which interpretation it used.</p><p>The data layer represents a cleaned asset as <code>PriceData</code>, a small object containing timestamps, prices, and an asset label. Its validation contract requires unique, increasing timestamps and finite positive prices. That contract is more than cosmetic. A negative or missing price can invalidate returns, position sizes, portfolio valuation, and risk calculations downstream.</p><p>The <code>WindowedDataset</code> object preserves temporal metadata in addition to arrays:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;6f0a0462-ee9c-4900-aeed-aa0854ca4ee9&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">@dataclass(frozen=True)
class WindowedDataset:
    """Windowed samples and their temporal metadata."""

    X: np.ndarray
    y: np.ndarray
    window_start: pd.DatetimeIndex
    window_end: pd.DatetimeIndex
    y_timestamp: pd.DatetimeIndex
    feature_names: Tuple[str, ...] = ("price",)</code></pre></div><p>Keeping <code>window_end</code> and <code>y_timestamp</code> is important. A model prediction must be associated with the time at which its input information became available. The later backtest can then enforce the rule that a signal generated after the final input observation cannot fill at an earlier or identical timestamp.</p><h2>Separating a forecast from an order</h2><p>The paper&#8217;s forecasting section and trading section imply different horizons. The prediction tables appear to use a one-step setup, while the conclusion refers to forecasts for the next three days. The implementation therefore exposes the forecast horizon rather than assuming that those descriptions are identical.</p><p>Conceptually, a one-step window is:</p><p>\[ X<em>t = [z</em>{t-99}, z<em>{t-98}, \ldots, z</em>t], \qquad y<em>t = z</em>{t+1}. \]</p><p>A three-step version is:</p><p>\[ y<em>t = [z</em>{t+1}, z<em>{t+2}, z</em>{t+3}]. \]</p><p>The forecast layer answers what the model predicts. A separate signal layer must decide how to interpret that prediction. For example, it might compare the terminal three-day forecast with the current price, or use a configured threshold before producing a buy signal. That rule is a strategy assumption, not a consequence of the neural network itself.</p><p>The same distinction applies to sizing. A positive forecast does not imply that the strategy should invest its full available balance. The sizing schedule may allocate one of several finite addition levels, subject to cash and exposure caps. The eventual order amount is therefore the result of several stages:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;752d07d3-1d92-4b3b-88a7-8415f0e7df84&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">forecast
    -&gt; directional signal
    -&gt; candidate order amount
    -&gt; risk adjustment
    -&gt; selected feasible action
    -&gt; executed fill</code></pre></div><p>This decomposition also makes it possible to compare alternatives. The same forecast can be evaluated with a flat allocation, the reconstructed exponential schedule, or a different risk limit. Likewise, the same sizing rule can be tested with Att-BiLSTM predictions or a simpler benchmark.</p><h2>Offline and educational by design</h2><p>The generated project is deliberately offline. It accepts local CSV files or deterministic synthetic data and does not connect to an exchange, download market data, handle credentials, or submit orders. This restriction is appropriate for a paper-to-code tutorial because it keeps the focus on data alignment, modeling assumptions, decision rules, and accounting.</p><p>Synthetic data are useful for demonstrating interfaces. For example, the data module includes a deterministic generator whose purpose is to provide repeatable price paths for checking window shapes and portfolio arithmetic. Such data are not evidence about gold or Bitcoin and cannot reproduce the paper&#8217;s reported outcomes.</p><p>The package initializer reinforces this design by avoiding model training or data loading during import:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;64a99256-b76d-4653-9b12-27e3abdcf44e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">"""Offline research components for the quantitative trading model paper."""

# Configuration is part of the lightweight public API. Heavy forecasting and
# benchmark dependencies are imported only by their respective modules.
from .config import (
    BacktestConfig,
    DataConfig,
    ExecutionConfig,
    FeeSensitivityConfig,
    ForecastConfig,
    SizingConfig,
    SplitConfig,
    VaRConfig,
    validate_config,
)</code></pre></div><p>This side-effect-free structure lets a reader use data preparation, streak analysis, sizing, risk, or accounting utilities without installing PyTorch or every optional benchmark library. It also avoids implying that importing the package performs a market action.</p><h2>What this architecture does&#8212;and does not&#8212;claim</h2><p>The architecture provides a practical way to translate the paper&#8217;s major ideas into independently inspectable components. It directly reflects the clearly described mechanics, such as sliding windows and temporal attention, while making incomplete parts configurable and labeled as reconstructions.</p><p>It does <strong>not</strong> establish that the paper&#8217;s strategy is profitable or that the implementation reproduces its reported numbers. The paper&#8217;s approximate final values&#8212;$646 from a $500 gold allocation and $215,487 from a $500 Bitcoin allocation&#8212;remain unverified claims because the original data, dates, execution rules, risk settings, and position-sizing details are unavailable.</p><p>The useful starting point is therefore not a promise of matching those numbers. It is a disciplined contract between modules:</p><ul><li><p>data provides only ordered historical observations;</p></li><li><p>forecasting uses a defined historical window;</p></li><li><p>signals use predictions and current state;</p></li><li><p>sizing uses a finite, explicit budget;</p></li><li><p>risk control uses pre-decision information;</p></li><li><p>execution occurs after the signal timestamp;</p></li><li><p>evaluation accounts for costs and reports portfolio behavior separately from forecast error.</p></li></ul><p>That contract is the foundation for the remaining sections, where each component is examined in detail and every reconstruction decision is identified.</p><h2>2. What Can Actually Be Reproduced?</h2><p>The paper separates two decisions: forecasting proposes <strong>when</strong> a trade might be useful, while position management proposes <strong>how much</strong> capital to deploy. Before implementing those components, distinguish a direct implementation from a reconstruction or an illustrative example.</p><p>A paper-to-code project can contain clean Python and still fail to reproduce the original experiment. Reproduction requires the same data, timestamps, preprocessing, model definition, decision rules, execution assumptions, and evaluation protocol. Several of those ingredients are missing or ambiguous here.</p><p>The project uses five status labels, consistent with <code>docs/reproduction_matrix.md</code>:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ek9L!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa21cfe13-02cc-4e78-8865-97268467b0bf_1568x664.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ek9L!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa21cfe13-02cc-4e78-8865-97268467b0bf_1568x664.png 424w, https://substackcdn.com/image/fetch/$s_!ek9L!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa21cfe13-02cc-4e78-8865-97268467b0bf_1568x664.png 848w, https://substackcdn.com/image/fetch/$s_!ek9L!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa21cfe13-02cc-4e78-8865-97268467b0bf_1568x664.png 1272w, https://substackcdn.com/image/fetch/$s_!ek9L!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa21cfe13-02cc-4e78-8865-97268467b0bf_1568x664.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ek9L!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa21cfe13-02cc-4e78-8865-97268467b0bf_1568x664.png" width="1456" height="617" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a21cfe13-02cc-4e78-8865-97268467b0bf_1568x664.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:617,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:150524,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/206399283?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa21cfe13-02cc-4e78-8865-97268467b0bf_1568x664.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!ek9L!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa21cfe13-02cc-4e78-8865-97268467b0bf_1568x664.png 424w, https://substackcdn.com/image/fetch/$s_!ek9L!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa21cfe13-02cc-4e78-8865-97268467b0bf_1568x664.png 848w, https://substackcdn.com/image/fetch/$s_!ek9L!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa21cfe13-02cc-4e78-8865-97268467b0bf_1568x664.png 1272w, https://substackcdn.com/image/fetch/$s_!ek9L!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa21cfe13-02cc-4e78-8865-97268467b0bf_1568x664.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>These labels determine how results should be interpreted and whether a value may legitimately be used as a test expectation. A hand-calculated fee or accounting invariant can be tested. The paper's final portfolio values cannot be treated that way because their inputs and protocol are unknown.</p><h3>2.1 What the paper specifies clearly</h3><p>Several elements are concrete enough to guide implementation:</p><ul><li><p>overlapping historical windows of length 100 with stride 1;</p></li><li><p>an attention-enhanced bidirectional LSTM as the primary forecasting model;</p></li><li><p>comparisons with LSTM, GRU, BiLSTM, Holt-Winters, ARIMA, HMM, and XGBoost;</p></li><li><p>a stated dropout setting of 20%;</p></li><li><p>MAE as the training loss;</p></li><li><p>RMSprop as the intended optimizer after correcting the paper's terminology;</p></li><li><p>dot-product attention, softmax normalization, and a weighted temporal sum;</p></li><li><p>transaction-fee scenarios for gold and Bitcoin.</p></li></ul><p>For example, <code>src/quantitative_trading_model/models/attention.py</code> can implement the displayed attention equations without knowing the authors' complete source code. Similarly, the configuration modules can preserve paper-inspired values as visible settings. This still does not guarantee numerical reproduction: the data, model dimensions, initialization, and training schedule remain unknown.</p><p>Configuration provenance is deliberately split across modules rather than concentrated in <code>ForecastConfig</code>:</p><ul><li><p><code>DataConfig</code> contains the window length, stride, cleaning, and data conventions;</p></li><li><p><code>ForecastConfig</code> contains model and training settings such as horizon, dropout, loss, optimizer, and hidden dimensions;</p></li><li><p><code>SizingConfig</code> contains the reconstructed position-sizing settings;</p></li><li><p><code>VaRConfig</code> contains risk-estimation and action settings;</p></li><li><p><code>ExecutionConfig</code> and <code>BacktestConfig</code> contain fees, slippage, cash, leverage, and liquidation assumptions;</p></li><li><p><code>FeeSensitivityConfig</code> contains the gold and Bitcoin fee grids.</p></li></ul><p>This separation prevents unrelated assumptions from being hidden behind a misleading single model configuration.</p><h3>2.2 Missing data and experiment identity</h3><p>The largest reproducibility gap is the input data. The paper refers to historical gold and Bitcoin prices but does not identify enough information to establish exactly what those series represent. Missing details include:</p><ul><li><p>the data vendor or source;</p></li><li><p>the specific gold instrument and Bitcoin market or index;</p></li><li><p>currency and unit conventions;</p></li><li><p>sampling frequency;</p></li><li><p>start and end dates;</p></li><li><p>timezone and timestamp rules;</p></li><li><p>the selected price field, such as close or adjusted close;</p></li><li><p>treatment of missing observations and duplicate timestamps;</p></li><li><p>market closures, splits, or other instrument-specific adjustments;</p></li><li><p>whether the assets were processed independently or jointly.</p></li></ul><p>&#8220;Gold price&#8221; and &#8220;Bitcoin price&#8221; are not unique datasets. Two valid historical series can have different timestamps, gaps, price fields, and price levels. A model trained on either series may be reasonable while producing metrics that cannot be compared with the paper's table.</p><p><code>README.md</code>, <code>data.py</code>, and <code>cli.py</code> therefore require local inputs rather than silently selecting an unidentified market-data source. Synthetic data are available only for demonstrations. This makes provenance explicit and avoids implying that an arbitrary local or synthetic series is equivalent to the paper's input.</p><h3>2.3 Missing preprocessing and target definition</h3><p>The paper does not fully state what the forecasting model predicts. Plausible interpretations include:</p><p>a raw next-day price;</p><p>a normalized price level;</p><p>a one-day return or percentage change;</p><p>a vector of the next three prices;</p><p>a day-three price produced by a multi-step procedure.</p><p>The prediction discussion is compatible with one-step forecasting, while the conclusion refers to forecasts over the next three days. These are different supervised-learning problems with different target shapes, losses, inverse transformations, and trading signals.</p><p>Preprocessing is also unclear. The paper does not specify whether prices were scaled, whether scaling used training observations only, or whether features such as volume, technical indicators, or macroeconomic variables were included. The implementation defaults to a price-based setup but exposes the target representation and horizon so that this ambiguity remains visible.</p><p>A reproduction must record at least:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!kVBQ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd2604b83-fd6c-49df-86e0-9801756da914_1566x544.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!kVBQ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd2604b83-fd6c-49df-86e0-9801756da914_1566x544.png 424w, https://substackcdn.com/image/fetch/$s_!kVBQ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd2604b83-fd6c-49df-86e0-9801756da914_1566x544.png 848w, https://substackcdn.com/image/fetch/$s_!kVBQ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd2604b83-fd6c-49df-86e0-9801756da914_1566x544.png 1272w, https://substackcdn.com/image/fetch/$s_!kVBQ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd2604b83-fd6c-49df-86e0-9801756da914_1566x544.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!kVBQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd2604b83-fd6c-49df-86e0-9801756da914_1566x544.png" width="1456" height="506" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d2604b83-fd6c-49df-86e0-9801756da914_1566x544.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:506,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:112924,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/206399283?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd2604b83-fd6c-49df-86e0-9801756da914_1566x544.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!kVBQ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd2604b83-fd6c-49df-86e0-9801756da914_1566x544.png 424w, https://substackcdn.com/image/fetch/$s_!kVBQ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd2604b83-fd6c-49df-86e0-9801756da914_1566x544.png 848w, https://substackcdn.com/image/fetch/$s_!kVBQ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd2604b83-fd6c-49df-86e0-9801756da914_1566x544.png 1272w, https://substackcdn.com/image/fetch/$s_!kVBQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd2604b83-fd6c-49df-86e0-9801756da914_1566x544.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3>2.4 Missing Att-BiLSTM architecture details</h3><p>Att-BiLSTM identifies a model family, not a complete architecture. The paper does not reliably specify:</p><ul><li><p>the number of recurrent layers;</p></li><li><p>hidden-state dimensions;</p></li><li><p>whether attention receives concatenated forward and backward states;</p></li><li><p>how the query vector is constructed;</p></li><li><p>dense-layer dimensions and activations;</p></li><li><p>output shape and activation;</p></li><li><p>recurrent versus ordinary dropout;</p></li><li><p>initialization;</p></li><li><p>learning-rate and optimizer settings;</p></li><li><p>batch size, epoch count, and stopping rule;</p></li><li><p>random seed;</p></li><li><p>whether each asset receives a separate model.</p></li></ul><p><code>models/forecasters.py</code> consequently provides a configurable paper-inspired model. Its learned attention query is an explicit reconstruction: the paper supplies a symbol <code>q</code> but does not say how that vector is obtained. The implementation is useful and inspectable, but it is not a verified transcription of the authors' architecture.</p><p>The same caution applies to the benchmarks. Listing LSTM, GRU, BiLSTM, Holt-Winters, ARIMA, HMM, and XGBoost does not establish that their feature sets, capacities, tuning procedures, and horizons were comparable. The paper's statement about &#8220;consistent parameters&#8221; is insufficient to reconstruct a fair benchmark protocol.</p><h3>2.5 The position-sizing equations are incomplete</h3><p>Equations (5) through (8) in the position-management section are not reliably recoverable from the extracted material. Important ambiguities include:</p><ul><li><p>whether <code>p_i</code> denotes cash, asset units, or notional exposure;</p></li><li><p>the meaning of the maximum addition count <code>x</code>;</p></li><li><p>whether fees apply to one transaction or both sides;</p></li><li><p>whether decline statistics are signed or expressed as positive loss magnitudes;</p></li><li><p>the independent variable in the exponential expression;</p></li><li><p>whether additions follow every decline or only a forecast-confirmed condition.</p></li></ul><p><code>src/quantitative_trading_model/sizing.py</code> therefore does not claim to transcribe the equations. It implements a normalized exponential schedule with finite addition levels, cash limits, and exposure caps. This is a <strong>reconstructed exponential position-sizing rule</strong>: it captures the apparent idea that later additions receive larger weights, but it does not establish that the paper used the same formula or calibration.</p><p>Keeping this reconstruction in one module is useful. If the original equation images or source become available, the sizing implementation can be replaced without changing data preparation, forecasting, risk, or portfolio accounting.</p><h3>2.6 VaR is named but not operationally defined</h3><p>The paper says that VaR contributes to trading decisions but does not supply the settings needed for a unique procedure. Missing choices include:</p><ul><li><p>confidence level;</p></li><li><p>horizon;</p></li><li><p>estimation-window length;</p></li><li><p>historical, parametric, or Monte Carlo method;</p></li><li><p>portfolio-level versus asset-level returns;</p></li><li><p>treatment of overlapping multi-day returns;</p></li><li><p>maximum acceptable loss;</p></li><li><p>whether VaR blocks, scales, or merely reports a trade.</p></li></ul><p><code>src/quantitative_trading_model/risk.py</code> provides a rolling historical VaR reconstruction with explicit confidence, lookback, horizon, and action settings. It supports leakage-aware use, but it cannot independently guarantee temporal provenance: <code>HistoricalVaR</code> accepts caller-provided returns and does not infer timestamps. The surrounding caller or backtest must supply only returns known before the decision and must exclude the current or future return when appropriate.</p><p>Thus, a VaR value means &#8220;the result of this configured historical quantile on this supplied return window.&#8221; It is not a hidden setting recovered from the paper, and it should not be described as the paper's exact VaR method.</p><h3>2.7 The modified greedy algorithm is not reproducible from its name</h3><p>&#8220;Modified greedy algorithm&#8221; describes a family of procedures, not one precise policy. Reproduction would require the paper to define:</p><ul><li><p>candidate actions;</p></li><li><p>whether actions are buy, sell, hold, or additions;</p></li><li><p>the objective being maximized;</p></li><li><p>how forecasts become expected gains;</p></li><li><p>how fees enter the objective;</p></li><li><p>cash, exposure, and addition constraints;</p></li><li><p>how VaR affects feasibility;</p></li><li><p>whether one action or a multi-step plan is selected;</p></li><li><p>tie-breaking behavior.</p></li></ul><p><code>src/quantitative_trading_model/strategy.py</code> implements a transparent reconstruction: enumerate feasible candidates, estimate forecast-based benefit after fees, apply documented constraints, and select the highest-scoring candidate with deterministic tie-breaking. It must not be described as the paper's verified algorithm.</p><p>There is also an unresolved static integration issue. The generated <code>GreedyPolicy</code> adapter currently does not match the generated <code>VaRTradeFilter</code> interface: it attempts to pass <code>proposed_notional</code> and omits required exposure-related arguments, while the filter expects <code>proposed_quantity</code> together with current exposure and portfolio value. This boundary must be repaired before runtime integration is attempted. Until then, the strategy and risk modules should be treated as separately documented reconstructions rather than a verified end-to-end combination.</p><p>The temporal rule remains essential: realized future returns must never score a candidate. A decision may use forecasts, current cash, current holdings, current exposure, and pre-decision risk history, but not the price that will be observed after execution.</p><h3>2.8 Why the headline profits are not test targets</h3><p>The paper reports approximately:</p><ul><li><p><strong>$646</strong> from an initial <strong>$500</strong> gold allocation;</p></li><li><p><strong>$215,487</strong> from an initial <strong>$500</strong> Bitcoin allocation;</p></li><li><p>approximately <strong>$216,133</strong> from the combined <strong>$1,000</strong> allocation.</p></li></ul><p>These are <strong>reported claims</strong>, not reproduced results. <code>src/quantitative_trading_model/experiments.py</code> records the values as unverified references rather than using them as test assertions or expected outputs from synthetic demonstrations.</p><p>A test target is appropriate when its input and protocol are specified. For example, a hand-calculated accounting test can assert the cash effect of a purchase with a known fee. The paper's Bitcoin figure cannot be tested this way because the historical period, asset series, signal timestamps, sizing triggers, reinvestment rules, exposure limits, fee convention, slippage, and liquidation policy are unknown.</p><p>A local result that happens to resemble one of those values would not prove reproduction unless the underlying data and all decision and accounting rules also matched. The unusually large Bitcoin result may depend on a particular historical period, aggressive compounding, repeated averaging down, leverage-like exposure, or optimistic execution assumptions. It should not be generalized as evidence of future profitability.</p><h3>2.9 Benchmark metrics and fee figures are also unverified</h3><p>The paper reports forecast metrics such as RMSE, MAE, MAPE, and R-squared, and shows transaction-fee sensitivity figures. Those values cannot be confirmed from the extracted text alone.</p><p>Forecast metrics depend on the exact test observations, target scale, transformations, timestamps, MAPE zero policy, architecture, trained parameters, random seed, and stopping rule. Fee figures additionally depend on trade dates and quantities, fee side conventions, spread, slippage, action feasibility, initial holdings, reinvestment, and liquidation.</p><p>The implementation preserves the paper-inspired fee grids as inputs: gold rates from 0.2% through 1.2%, and Bitcoin rates from 1.5% through 3.5%. It cannot infer the missing y-values from the figures. A new fee sweep using identified local data is a valid experiment under the project's assumptions, not a recovered copy of the paper's chart.</p><h3>2.10 How to read the reproduction matrix</h3><p><code>docs/reproduction_matrix.md</code> records status at the method and claim level. Its distinctions are important:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!WF2W!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe8379702-6a15-4d4c-bd2d-e07a404c903f_1558x1018.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!WF2W!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe8379702-6a15-4d4c-bd2d-e07a404c903f_1558x1018.png 424w, https://substackcdn.com/image/fetch/$s_!WF2W!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe8379702-6a15-4d4c-bd2d-e07a404c903f_1558x1018.png 848w, https://substackcdn.com/image/fetch/$s_!WF2W!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe8379702-6a15-4d4c-bd2d-e07a404c903f_1558x1018.png 1272w, https://substackcdn.com/image/fetch/$s_!WF2W!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe8379702-6a15-4d4c-bd2d-e07a404c903f_1558x1018.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!WF2W!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe8379702-6a15-4d4c-bd2d-e07a404c903f_1558x1018.png" width="1456" height="951" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e8379702-6a15-4d4c-bd2d-e07a404c903f_1558x1018.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:951,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:211985,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/206399283?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe8379702-6a15-4d4c-bd2d-e07a404c903f_1558x1018.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!WF2W!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe8379702-6a15-4d4c-bd2d-e07a404c903f_1558x1018.png 424w, https://substackcdn.com/image/fetch/$s_!WF2W!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe8379702-6a15-4d4c-bd2d-e07a404c903f_1558x1018.png 848w, https://substackcdn.com/image/fetch/$s_!WF2W!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe8379702-6a15-4d4c-bd2d-e07a404c903f_1558x1018.png 1272w, https://substackcdn.com/image/fetch/$s_!WF2W!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe8379702-6a15-4d4c-bd2d-e07a404c903f_1558x1018.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>This matrix prevents every function from being presented with the same level of authority when some functions implement displayed equations and others embody missing decisions.</p><h3>2.11 Static review is not empirical reproduction</h3><p>The project raises two different verification questions:</p><p>Does the generated artifact appear structurally reviewable?</p><p>Does the implementation reproduce the paper's empirical behavior?</p><p>Static and semantic review can address parts of the first question. It can inspect Python parsing, expected symbols, configuration validation, tensor-shape contracts, attention normalization, temporal boundaries, budget invariants, and accounting formulas. It cannot establish that the paper's data, model, or financial results were reproduced.</p><p>The supplied local verification record did not pass completely. It reported <strong>placeholder-detection findings</strong> for <code>experiments.py</code> and <code>cli.py</code>, and <strong>Markdown-fence findings</strong> for the generated <code>README.md</code> and <code>docs/tutorial.md</code> code fields. These are generated-artifact quality findings. They are not runtime results and do not by themselves demonstrate a Python syntax failure or a financial-model failure.</p><p>Semantic code verification was skipped. Runtime execution and test-suite success therefore must not be claimed. In addition, some generated tests and orchestration functions appear to use APIs that are not fully aligned with the generated modules, including the strategy/VaR boundary described above. Those inconsistencies require a software-maintenance pass before execution is attempted.</p><p>The appropriate conclusion is limited: the project documents intended interfaces, assumptions, and invariants, but it is not an executed or empirically validated reproduction.</p><h3>2.12 Responsible interpretation policy</h3><p>When describing results from this project:</p><ul><li><p>say <strong>implemented</strong> for mechanics supported by the paper and the code contract;</p></li><li><p>say <strong>reconstructed</strong> for normalized sizing, historical VaR, the greedy policy, and other interpretations of missing details;</p></li><li><p>say <strong>illustrative</strong> for synthetic data and hand-calculated workflows;</p></li><li><p>say <strong>unavailable</strong> when the source lacks the information needed for implementation or validation;</p></li><li><p>say <strong>reported claim</strong> when quoting the paper's profits or metrics without independent validation;</p></li><li><p>do not call a run on a different local dataset an exact reproduction;</p></li><li><p>keep forecast metrics separate from portfolio metrics;</p></li><li><p>report dates, data provenance, target definition, fees, slippage, turnover, drawdown, and liquidation rules with every backtest;</p></li><li><p>treat static review as artifact evidence, not proof of execution, profitability, or paper fidelity.</p></li></ul><p>The next sections can discuss setup and implementation without blurring these categories. A modular reconstruction can still teach attention-based forecasting, streak statistics, bounded sizing, risk filtering, and fee-aware accounting while making clear which conclusions require the original data and experimental protocol.</p><h2>3. Project Setup and the Implementation Map</h2><p>The implementation is organized as a normal Python package rather than as one large research script. That choice matters because the paper combines several different responsibilities: data preparation, neural forecasting, benchmark models, streak analysis, position sizing, risk filtering, trading decisions, execution, and evaluation. Keeping those responsibilities in separate modules makes it easier to inspect assumptions and replace incomplete reconstructions later.</p><p>The project is also deliberately <strong>offline</strong>. It accepts local files or generated demonstration data, but it does not connect to an exchange, download market data, store credentials, or submit orders. The resulting code is suitable for educational research and backtest prototyping, not for live trading.</p><h3>3.1 The <code>src</code> layout</h3><p>The package uses the conventional <code>src</code> layout:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;56a56814-ba00-4cb5-a6f0-e2c28fac3ceb&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">quantitative-trading-model/
&#9500;&#9472;&#9472; pyproject.toml
&#9500;&#9472;&#9472; README.md
&#9500;&#9472;&#9472; src/
&#9474;   &#9492;&#9472;&#9472; quantitative_trading_model/
&#9474;       &#9500;&#9472;&#9472; __init__.py
&#9474;       &#9500;&#9472;&#9472; config.py
&#9474;       &#9500;&#9472;&#9472; data.py
&#9474;       &#9500;&#9472;&#9472; metrics.py
&#9474;       &#9500;&#9472;&#9472; streaks.py
&#9474;       &#9500;&#9472;&#9472; sizing.py
&#9474;       &#9500;&#9472;&#9472; risk.py
&#9474;       &#9500;&#9472;&#9472; strategy.py
&#9474;       &#9500;&#9472;&#9472; backtest.py
&#9474;       &#9500;&#9472;&#9472; experiments.py
&#9474;       &#9500;&#9472;&#9472; cli.py
&#9474;       &#9492;&#9472;&#9472; models/
&#9474;           &#9500;&#9472;&#9472; attention.py
&#9474;           &#9500;&#9472;&#9472; forecasters.py
&#9474;           &#9492;&#9472;&#9472; benchmarks.py
&#9500;&#9472;&#9472; tests/
&#9492;&#9472;&#9472; docs/
    &#9500;&#9472;&#9472; tutorial.md
    &#9492;&#9472;&#9472; reproduction_matrix.md</code></pre></div><p>With this layout, the importable package lives under <code>src/quantitative_trading_model</code>, while documentation and tests remain outside the runtime package. The <code>pyproject.toml</code> file tells setuptools where to find the package:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;toml&quot;,&quot;nodeId&quot;:&quot;de12be66-aaca-4bea-81f2-67ec24aa7df5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-toml">[tool.setuptools]
package-dir = { "" = "src" }
include-package-data = true

[tool.setuptools.packages.find]
where = ["src"]</code></pre></div><p>This avoids a common development problem in which tests accidentally import a loose source directory instead of the package that users will install. It also gives the project a clear boundary: files under <code>src/quantitative_trading_model</code> are implementation modules, while the root-level documentation explains how to use and interpret them.</p><h3>3.2 Core dependencies and optional model dependencies</h3><p>The package keeps its required dependencies small:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;toml&quot;,&quot;nodeId&quot;:&quot;95f987df-6ac6-4633-9789-55ec7c56609e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-toml">[project]
requires-python = "&gt;=3.10"
dependencies = [
    "numpy&gt;=1.24",
    "pandas&gt;=2.0"
]</code></pre></div><p>NumPy and pandas support the parts of the project that do not require a deep-learning framework: local price tables, timestamp handling, return calculations, streak statistics, metrics, risk calculations, and portfolio accounting.</p><p>The heavier forecasting libraries are optional:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;toml&quot;,&quot;nodeId&quot;:&quot;bc831663-cc6d-4f11-af38-39d4dba9eccd&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-toml">[project.optional-dependencies]
deep-learning = [
    "torch&gt;=2.0"
]
classical = [
    "statsmodels&gt;=0.14",
    "pmdarima&gt;=2.0",
    "hmmlearn&gt;=0.3",
    "xgboost&gt;=2.0"
]</code></pre></div><p>This split reflects the paper's structure. The Att-BiLSTM requires PyTorch, but a reader who only wants to study the return convention, normalized exponential sizing reconstruction, historical VaR, or portfolio accounting should not need to install PyTorch. Similarly, Holt-Winters, ARIMA, HMM, and XGBoost benchmarks can be enabled only when those comparisons are needed.</p><p>The optional dependency design also makes missing capabilities visible. A benchmark adapter should report that a library is unavailable rather than silently substituting a different model. That distinction matters when comparing results with the paper: an omitted benchmark is not the same as a benchmark that produced a poor score.</p><p>The package metadata also defines a console entry point:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;toml&quot;,&quot;nodeId&quot;:&quot;e1aa41c6-381d-4899-85e0-cd39afbf9bea&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-toml">[project.scripts]
quant-trading-model = "quantitative_trading_model.cli:main"</code></pre></div><p>After local installation, this exposes the offline command-line interface as <code>quant-trading-model</code>. The CLI can validate local CSV files, generate deterministic synthetic demonstrations, and dispatch configured research experiments. It does not create a network client or read credentials.</p><h3>3.3 Side-effect-free package imports</h3><p>The package initializer intentionally exposes only lightweight metadata and configuration types:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;f8ee8a83-16e3-4447-ade4-702a85369e0b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">"""Offline research components for the quantitative trading model paper."""

from importlib import metadata

try:
    __version__ = metadata.version("quantitative-trading-model")
except metadata.PackageNotFoundError:
    __version__ = "0.1.0"

from .config import (
    BacktestConfig,
    DataConfig,
    ExecutionConfig,
    FeeSensitivityConfig,
    ForecastConfig,
    SizingConfig,
    SplitConfig,
    VaRConfig,
    validate_config,
)</code></pre></div><p>Importing the package does not load a CSV, train a model, create a portfolio, or contact an external service. The initializer also does not import PyTorch, statsmodels, <code>hmmlearn</code>, or XGBoost. Those imports are deferred to the modules that need them.</p><p>That design has two practical benefits:</p><p><strong>Lightweight use:</strong> core modules remain usable in an environment without optional modeling libraries.</p><p><strong>Predictable imports:</strong> simply writing <code>import quantitative_trading_model</code> cannot start a computation or produce an external side effect.</p><p>The public exports are deliberately narrow:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;5a36e762-fb86-49ba-a52a-0a8eb748f671&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">__all__ = [
    "__version__",
    "BacktestConfig",
    "DataConfig",
    "ExecutionConfig",
    "FeeSensitivityConfig",
    "ForecastConfig",
    "SizingConfig",
    "SplitConfig",
    "VaRConfig",
    "validate_config",
]</code></pre></div><p>The model, data, strategy, and backtest classes are imported from their own modules when required. This keeps the top-level API stable without forcing every user to install every optional dependency.</p><h3>3.4 Where each research responsibility lives</h3><p>The package map follows the paper's conceptual pipeline:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!aq85!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0996af8d-98d9-422f-9f4b-8ac6fc43de2d_1406x1372.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!aq85!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0996af8d-98d9-422f-9f4b-8ac6fc43de2d_1406x1372.png 424w, https://substackcdn.com/image/fetch/$s_!aq85!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0996af8d-98d9-422f-9f4b-8ac6fc43de2d_1406x1372.png 848w, https://substackcdn.com/image/fetch/$s_!aq85!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0996af8d-98d9-422f-9f4b-8ac6fc43de2d_1406x1372.png 1272w, https://substackcdn.com/image/fetch/$s_!aq85!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0996af8d-98d9-422f-9f4b-8ac6fc43de2d_1406x1372.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!aq85!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0996af8d-98d9-422f-9f4b-8ac6fc43de2d_1406x1372.png" width="1406" height="1372" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0996af8d-98d9-422f-9f4b-8ac6fc43de2d_1406x1372.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1372,&quot;width&quot;:1406,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:324804,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/206399283?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0996af8d-98d9-422f-9f4b-8ac6fc43de2d_1406x1372.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!aq85!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0996af8d-98d9-422f-9f4b-8ac6fc43de2d_1406x1372.png 424w, https://substackcdn.com/image/fetch/$s_!aq85!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0996af8d-98d9-422f-9f4b-8ac6fc43de2d_1406x1372.png 848w, https://substackcdn.com/image/fetch/$s_!aq85!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0996af8d-98d9-422f-9f4b-8ac6fc43de2d_1406x1372.png 1272w, https://substackcdn.com/image/fetch/$s_!aq85!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0996af8d-98d9-422f-9f4b-8ac6fc43de2d_1406x1372.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>This separation is not merely a software-style preference. It prevents an ambiguous paper detail from being hidden inside an unrelated function. For example, the return denominator belongs in <code>streaks.py</code>, while the execution fee convention belongs in <code>backtest.py</code>. A reader can therefore change one assumption without accidentally changing every layer of the research pipeline.</p><h3>3.5 Documentation, tests, and the reproduction matrix</h3><p>The non-runtime files serve different purposes:</p><ul><li><p><strong>`README.md`</strong> gives installation instructions, local CSV conventions, CLI examples, project warnings, and a high-level architecture map.</p></li><li><p><strong>`docs/tutorial.md`</strong> explains the paper-to-code translation in detail, including equations, tensor shapes, timing rules, and reconstruction decisions.</p></li><li><p><strong>`docs/reproduction_matrix.md`</strong> tracks each paper component as implemented, reconstructed, illustrative, unavailable, or an unverified reported claim.</p></li><li><p><strong>`tests/`</strong> contains hand-calculable checks for data boundaries, streak semantics, sizing budgets, VaR decisions, model shapes, transaction fees, and portfolio-value identity.</p></li></ul><p>The reproduction matrix is especially useful for this paper because the source does not fully specify equations (5)&#8211;(8), VaR operation, the greedy policy, or the original backtest protocol. Instead of allowing those gaps to disappear into code, the matrix records what the implementation can support and what still requires the original data or source material.</p><p>For example, the matrix treats the following differently:</p><ul><li><p>the 100-step window as a directly implementable paper detail;</p></li><li><p>the learned attention query as a reconstruction;</p></li><li><p>synthetic prices as an illustration;</p></li><li><p>the reported <code>$646</code> gold and <code>$215,487</code> Bitcoin outcomes as unverified claims.</p></li></ul><p>That classification should remain visible in experiment output and documentation. A clean package installation does not establish empirical reproduction, and static code checks do not establish that the paper's financial results are correct.</p><h3>3.6 A practical installation boundary</h3><p>A typical local setup is:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:&quot;ac509ac6-f821-441a-a0a2-96bd3bfa6393&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">python -m venv .venv
python -m pip install --upgrade pip
python -m pip install -e .</code></pre></div><p>The core installation is enough to explore data preparation, streak analysis, risk calculations, sizing, metrics, and portfolio accounting. To use the PyTorch forecasters, install the deep-learning extra:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:&quot;131adebc-8d3b-4a9b-bcea-dbf98fcd2ad8&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">python -m pip install -e '.[deep-learning]'</code></pre></div><p>The classical benchmark dependencies are declared under the <code>classical</code> extra in <code>pyproject.toml</code>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:&quot;a1677b7d-4eb7-4220-b309-d5eab54b0798&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">python -m pip install -e '.[classical]'</code></pre></div><p>The exact installed command surface can be inspected with:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:&quot;cad7f9a4-908e-4e31-937b-6c9b0f228030&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">quant-trading-model --help</code></pre></div><p>These commands install or invoke local research tooling only. They do not download the paper's missing gold or Bitcoin data. The user must supply a provenance-documented local dataset before treating any result as an empirical experiment.</p><h3>3.7 What this structure does&#8212;and does not&#8212;guarantee</h3><p>The package structure provides useful engineering guarantees:</p><ul><li><p>optional modeling libraries do not prevent core imports;</p></li><li><p>configuration choices are visible rather than scattered through scripts;</p></li><li><p>data, forecast, strategy, execution, and evaluation responsibilities are separated;</p></li><li><p>documentation can distinguish direct implementation from reconstruction;</p></li><li><p>tests can check local invariants without requiring the paper's unavailable data.</p></li></ul><p>It does <strong>not</strong> guarantee that the paper's reported results can be reproduced. The missing dataset, dates, preprocessing, exact model architecture, VaR parameters, greedy rules, position-sizing equations, and execution protocol remain external requirements. The next sections use this package structure to examine those components one at a time, beginning with validated configuration and local data preparation.</p><h2>4. Make Ambiguity Explicit with Validated Configuration</h2><p>A research paper can name an algorithm without specifying every value required to run it. This paper gives some concrete settings, including a 100-observation history, one-observation stride, 20% dropout, MAE loss, RMSprop optimization, and several transaction-fee scenarios. It leaves other choices unclear, including the forecast horizon, recurrent-layer dimensions, attention query, VaR policy, position-sizing parameters, execution timing, and liquidation rule.</p><p>The implementation records these choices in dataclasses defined in <code>src/quantitative_trading_model/config.py</code>. Centralized configuration does not recover the paper's missing data or prove that its reported results can be reproduced. It does make assumptions visible, validates basic invariants, and provides metadata that can be saved with an experiment.</p><p>There are three important categories of settings:</p><ul><li><p><strong>Paper-inspired settings</strong> are stated directly or strongly suggested by the paper.</p></li><li><p><strong>Reconstruction settings</strong> are operational choices needed because the paper is incomplete.</p></li><li><p><strong>Implementation safety settings</strong> constrain this educational simulator even when the paper does not describe equivalent controls.</p></li></ul><p>The distinction matters because a field can be present in configuration without being fully wired into every generated runtime module. The status of each important setting should therefore be checked against both <code>config.py</code> and the module that consumes it.</p><h3>4.1 Configuration objects and their responsibilities</h3><p>The configuration module defines separate dataclasses for the main layers of the pipeline:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!X16U!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b94666b-df78-4138-9e5f-7e33b7fdcc2d_1420x1120.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!X16U!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b94666b-df78-4138-9e5f-7e33b7fdcc2d_1420x1120.png 424w, https://substackcdn.com/image/fetch/$s_!X16U!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b94666b-df78-4138-9e5f-7e33b7fdcc2d_1420x1120.png 848w, https://substackcdn.com/image/fetch/$s_!X16U!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b94666b-df78-4138-9e5f-7e33b7fdcc2d_1420x1120.png 1272w, https://substackcdn.com/image/fetch/$s_!X16U!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b94666b-df78-4138-9e5f-7e33b7fdcc2d_1420x1120.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!X16U!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b94666b-df78-4138-9e5f-7e33b7fdcc2d_1420x1120.png" width="1420" height="1120" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1b94666b-df78-4138-9e5f-7e33b7fdcc2d_1420x1120.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1120,&quot;width&quot;:1420,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:280178,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/206399283?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b94666b-df78-4138-9e5f-7e33b7fdcc2d_1420x1120.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!X16U!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b94666b-df78-4138-9e5f-7e33b7fdcc2d_1420x1120.png 424w, https://substackcdn.com/image/fetch/$s_!X16U!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b94666b-df78-4138-9e5f-7e33b7fdcc2d_1420x1120.png 848w, https://substackcdn.com/image/fetch/$s_!X16U!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b94666b-df78-4138-9e5f-7e33b7fdcc2d_1420x1120.png 1272w, https://substackcdn.com/image/fetch/$s_!X16U!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b94666b-df78-4138-9e5f-7e33b7fdcc2d_1420x1120.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>For example, the paper-inspired data and forecasting defaults are:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;cc438f5e-ccea-48c9-807c-8bbe45ad64ff&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">@dataclass
class DataConfig:
    timestamp_column: str = "timestamp"
    price_column: str = "price"
    window_length: int = 100
    stride: int = 1
    return_definition: str = "simple"


@dataclass
class ForecastConfig:
    model_name: str = "att_bilstm"
    horizon: int = 1
    target_type: str = "scaled_price"
    hidden_size: int = 32
    num_layers: int = 1
    dropout: float = 0.20
    learning_rate: float = 0.001
    optimizer: str = "rmsprop"
    loss: str = "mae"
    learned_attention_query: bool = True</code></pre></div><p>The values with the clearest connection to the paper are:</p><ul><li><p><code>window_length=100</code>: the reported historical window length;</p></li><li><p><code>stride=1</code>: the reported one-observation shift between windows;</p></li><li><p><code>dropout=0.20</code>: the paper-inspired dropout rate;</p></li><li><p><code>loss="mae"</code>: the reported training loss;</p></li><li><p><code>optimizer="rmsprop"</code>: a correction to the paper's terminology, since RMSprop is an optimizer rather than an activation function;</p></li><li><p><code>model_name="att_bilstm"</code>: the paper's claimed primary forecasting model.</p></li></ul><p>The hidden size, number of layers, learning rate, batch size, epoch count, early-stopping policy, and learned-query choice are not established by the paper. They are configurable reconstruction choices and should be recorded with any result.</p><h3>4.2 Make the forecast horizon explicit</h3><p>The paper's prediction discussion is not consistent about horizon. Its model description and metric table can be read as a one-step forecasting setup, while the conclusion refers to forecasts for the next three days. The source does not establish whether this means a three-element output vector, repeated one-step forecasts, or a single prediction for the third day.</p><p>The configuration makes the choice visible:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;c4c4e94e-a75f-4f06-9661-129ceb939542&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">one_day = ForecastConfig(horizon=1)
three_days = ForecastConfig(horizon=3)</code></pre></div><p>The horizon affects more than the output layer. It changes target construction, prediction metrics, signal aggregation, and execution timing. A one-step model should not be called a three-day model merely because its output is used repeatedly. The selected horizon should be stored in experiment metadata.</p><p><code>target_type</code> is similarly uncertain. The paper does not clearly say whether it predicts raw prices, scaled prices, returns, or percentage changes. The generated default, <code>"scaled_price"</code>, is a practical implementation choice and not a verified transcription.</p><h3>4.3 Position-sizing configuration: metadata versus enforcement</h3><p>The position-management equations are among the least reproducible parts of the paper. Equations (5) and (6) appear to describe recursive additions intended to compensate for declines and fees, but their notation is corrupted. Equation (7) does not clearly identify the independent variable of the exponential, and Equation (8) appears to normalize position scores into allocations.</p><p><code>SizingConfig</code> records a possible reconstruction:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;21be9b28-94ed-41c3-ab9b-43d9444851ce&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">@dataclass
class SizingConfig:
    maximum_additions: int = 5
    initial_level: int = 1
    growth_rate: float = 0.50
    score_scale: float = 1.0
    allocation_fraction: float = 1.0
    initial_position_fraction: float = 0.20
    maximum_exposure_fraction: float = 1.0
    allow_reuse: bool = False
    calibrate_from_percentiles: bool = False
    recovery_percentile: float = 0.50
    decline_percentile: float = 0.10
    fee_buffer: float = 0.0</code></pre></div><p>The intended normalized reconstruction is:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;8061810d-bc3a-4247-b3df-38ba8da93314&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">score_i      = score_scale * exp(growth_rate * i)
weight_i     = score_i / sum(score_j)
allocation_i = budget * weight_i</code></pre></div><p>However, these configuration fields should not be confused with runtime guarantees. In the generated project, <code>sizing.py</code> builds schedules through <code>build_exponential_schedule()</code> and enforces schedule-level properties through <code>SizingSchedule</code>, including finite allocations, normalized weights, a total budget, and one-time level consumption. <code>validate_config()</code> validates <code>maximum_additions</code> and related fractions, but it does not construct a schedule or verify its normalized weights.</p><p>Likewise, <code>allocation_fraction</code>, <code>maximum_exposure_fraction</code>, <code>allow_reuse</code>, and <code>fee_buffer</code> are currently configuration or documentation choices rather than completely wired controls in the generated sizing path. The schedule builder receives its own <code>budget</code> and growth arguments. It does not automatically read every corresponding field from <code>SizingConfig</code>, and <code>fee_buffer</code> is not applied to an allocation. A caller must pass the intended values into the sizing module explicitly or add the missing integration before treating them as active controls.</p><p>The safe interpretation is therefore:</p><ul><li><p><code>maximum_additions</code> documents the intended finite schedule size;</p></li><li><p>schedule normalization and budget checks belong to <code>SizingSchedule</code> and <code>sizing.py</code>;</p></li><li><p>reuse prevention is enforced when a constructed schedule marks a level as used;</p></li><li><p>exposure and cash caps must be applied by the sizer, strategy, or simulator that receives them;</p></li><li><p>the incomplete paper equations remain a reconstruction, not an exact implementation.</p></li></ul><h3>4.4 VaR configuration is an operational reconstruction</h3><p>The paper names VaR but does not define its confidence level, horizon, estimation window, return distribution, or action threshold. A usable risk filter must choose all of these values, so <code>VaRConfig</code> makes them explicit:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;fa36ea69-bb5c-44f9-902b-0a3c2ed4edbd&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">@dataclass
class VaRConfig:
    enabled: bool = True
    confidence: float = 0.95
    horizon: int = 1
    lookback: int = 100
    method: str = "historical"
    action: str = "scale"
    max_var_fraction: float = 0.05
    insufficient_history: str = "allow"
    min_observations: int = 30</code></pre></div><p>These defaults describe a rolling historical-VaR interpretation, not the paper's verified settings:</p><ul><li><p><code>confidence=0.95</code> selects a 95% lower-tail confidence convention;</p></li><li><p><code>horizon=1</code> uses one period by default;</p></li><li><p><code>lookback=100</code> limits the estimation history;</p></li><li><p><code>method="historical"</code> avoids assuming normally distributed returns;</p></li><li><p><code>action="scale"</code> permits reducing an order when projected risk is too high;</p></li><li><p><code>max_var_fraction=0.05</code> expresses the risk limit as a fraction of portfolio value;</p></li><li><p><code>insufficient_history="allow"</code> defines behavior before enough observations exist.</p></li></ul><p>The generated <code>risk.py</code> contains the actual historical estimator and filter. It must receive returns that were available before the decision timestamp. Configuration alone cannot enforce timestamp ordering. Also, because the paper provides no numerical VaR settings, every backtest should report these values as reconstruction metadata.</p><p>A stricter configuration can block trades until sufficient history exists:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;a4a0a4df-0be5-4af6-9eb1-629a2f965166&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">risk = VaRConfig(
    confidence=0.99,
    lookback=252,
    min_observations=100,
    action="block",
    insufficient_history="block",
)</code></pre></div><h3>4.5 Execution and liquidation: record the intended contract carefully</h3><p>The paper does not specify whether a signal uses the same closing price, the next opening price, or another execution price. It also does not say clearly whether a fee applies to purchases, sales, or both. The intended execution configuration is:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;5e0a9ed7-9dea-46e3-add2-2fe0ce9520f9&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">@dataclass
class ExecutionConfig:
    fee_rate: float = 0.002
    fee_on_buy: bool = True
    fee_on_sell: bool = True
    slippage_rate: float = 0.0
    allow_fractional_units: bool = True
    allow_short: bool = False
    max_leverage: float = 1.0
    execute_next_bar: bool = True
    reject_insufficient_cash: bool = True</code></pre></div><p>These fields describe the desired research contract, but the generated <code>backtest.py</code> does not fully honor all of them as written. In particular:</p><ul><li><p>the simulator applies <code>fee_rate</code> to both buys and sells unconditionally; it does not consult <code>fee_on_buy</code> or <code>fee_on_sell</code>;</p></li><li><p>unaffordable buys are clipped to the affordable quantity rather than being controlled by <code>reject_insufficient_cash</code>;</p></li><li><p>the simulator uses its own <code>allow_leverage</code> behavior and does not enforce <code>max_leverage</code> from <code>ExecutionConfig</code>;</p></li><li><p>the default simulator is nevertheless long-only and non-leveraged through its separate defaults, so cash and holdings are intended to remain nonnegative;</p></li><li><p>next-bar execution is enforced by requiring the fill timestamp to be strictly later than the signal timestamp.</p></li></ul><p>This distinction is important. A configuration field is not proof that the current runtime path consumes it. Until the wiring is corrected, results should state the simulator's actual behavior: two-sided fees, optional slippage, clipping of unaffordable buys, no shorting by default, and strict later-bar execution.</p><p><code>BacktestConfig</code> contains additional lifecycle choices:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;67da6719-bcbb-46d1-84c0-3b25e9aec78d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">@dataclass
class BacktestConfig:
    initial_cash: float = 500.0
    initial_quantity: float = 0.0
    liquidate_at_end: bool = True
    risk_free_rate: float = 0.0
    periods_per_year: int = 252
    currency: str = "USD"</code></pre></div><p>The <code>$500</code> default resembles the paper's stated per-asset starting allocation, but it does not reproduce the paper's final values. Dates, trades, fees, holdings, and liquidation accounting remain unspecified. <code>liquidate_at_end=True</code> is an explicit implementation choice: it may sell remaining holdings and charge the simulator's sell-side fee. A mark-to-market result without liquidation would be different.</p><h3>4.6 Fee grids use decimal conventions</h3><p>The paper lists percentages, while the configuration stores decimal rates:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Dp71!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2078457-e10d-41be-833d-f48dde76b542_1096x214.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Dp71!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2078457-e10d-41be-833d-f48dde76b542_1096x214.png 424w, https://substackcdn.com/image/fetch/$s_!Dp71!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2078457-e10d-41be-833d-f48dde76b542_1096x214.png 848w, https://substackcdn.com/image/fetch/$s_!Dp71!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2078457-e10d-41be-833d-f48dde76b542_1096x214.png 1272w, https://substackcdn.com/image/fetch/$s_!Dp71!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2078457-e10d-41be-833d-f48dde76b542_1096x214.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Dp71!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2078457-e10d-41be-833d-f48dde76b542_1096x214.png" width="1096" height="214" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e2078457-e10d-41be-833d-f48dde76b542_1096x214.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:214,&quot;width&quot;:1096,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:37329,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/206399283?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2078457-e10d-41be-833d-f48dde76b542_1096x214.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Dp71!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2078457-e10d-41be-833d-f48dde76b542_1096x214.png 424w, https://substackcdn.com/image/fetch/$s_!Dp71!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2078457-e10d-41be-833d-f48dde76b542_1096x214.png 848w, https://substackcdn.com/image/fetch/$s_!Dp71!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2078457-e10d-41be-833d-f48dde76b542_1096x214.png 1272w, https://substackcdn.com/image/fetch/$s_!Dp71!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2078457-e10d-41be-833d-f48dde76b542_1096x214.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>The conversion is:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;1c53232b-97e6-4de2-a8a1-141a2c59f2e4&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">0.2% = 0.2 / 100 = 0.002
1.5% = 1.5 / 100 = 0.015</code></pre></div><p>The corresponding defaults are:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;05e10b5f-f1cc-4c5b-a458-03d8afc8f60a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">@dataclass
class FeeSensitivityConfig:
    gold_rates: tuple[float, ...] = (
        0.002, 0.004, 0.006, 0.008, 0.010, 0.012
    )
    bitcoin_rates: tuple[float, ...] = (
        0.015, 0.020, 0.025, 0.030, 0.035
    )
    apply_to_buys: bool = True
    apply_to_sells: bool = True
    hold_signals_fixed: bool = True</code></pre></div><p>Using <code>0.2</code> for a 0.2% fee would represent a 20% rate. The validator checks that rates are finite and lie in the interval <code>[0, 1]</code>, but it cannot determine whether a user has confused percentage points with decimal rates. In other words, <code>0.2</code> is accepted as a numerically valid rate even though it is probably a unit mistake for this experiment. The decimal convention must therefore be documented and reviewed by the caller.</p><p>The fee-side flags are also not fully honored by the generated simulator, which currently charges both sides. <code>hold_signals_fixed</code> describes the sensitivity protocol rather than changing the backtest automatically. If fees affect feasibility or greedy ranking, actions may need to be regenerated for each fee scenario.</p><h3>4.7 What <code>validate_config()</code> checks</h3><p>The public validator accepts a complete aggregate or individual components:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;c8cbd5f2-9df2-4391-aaca-367b222e51a6&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from quantitative_trading_model.config import (
    ResearchConfig,
    ForecastConfig,
    validate_config,
)

config = ResearchConfig(
    forecast=ForecastConfig(
        model_name="att_bilstm",
        horizon=3,
        dropout=0.20,
        loss="mae",
        optimizer="rmsprop",
    )
)

validate_config(config)</code></pre></div><p>Component validators check basic constraints such as:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;f042b3e2-1a22-42b6-bba1-77f20b857783&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">_require(config.window_length &gt; 0, "data.window_length must be positive")
_require(0 &lt;= config.dropout &lt; 1, "forecast.dropout must be in [0, 1)")
_require(0 &lt; config.confidence &lt; 1, "var.confidence must be in (0, 1)")
_require(config.fee_rate &gt;= 0, "execution.fee_rate cannot be negative")
_require(config.maximum_additions &gt; 0, "sizing.maximum_additions must be positive")</code></pre></div><p>This validation rejects invalid configuration values such as nonpositive horizons, dropout outside <code>[0, 1)</code>, unsupported optimizer or loss names, split fractions that do not sum to one, negative fees or slippage, invalid VaR actions, and empty fee grids.</p><p>It does <strong>not</strong> perform every runtime or semantic check. Specifically:</p><ul><li><p>it does not construct a sizing schedule or check that its weights sum to one;</p></li><li><p>it does not validate the contents of a price file;</p></li><li><p>it does not prove that every configuration field is consumed by the backtest;</p></li><li><p>it does not detect a percentage-unit mistake such as entering <code>0.2</code> for <code>0.2%</code>;</p></li><li><p>it does not establish that VaR inputs are timestamp-safe;</p></li><li><p>it does not verify that generated experiments reproduce the paper.</p></li></ul><p>Schedule-level invariants are handled in <code>sizing.py</code>, data invariants in <code>data.py</code>, and accounting invariants in <code>backtest.py</code>. Those layers must be reviewed together with configuration validation.</p><p>The validator also rejects supplying both a complete <code>ResearchConfig</code> and individual component overrides. This prevents a caller from validating one configuration while accidentally running another.</p><h3>4.8 Record a complete experiment configuration</h3><p>A complete experiment can combine paper-inspired and reconstructed settings:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;686519f4-a104-4ae9-a136-45a0f4da8eb8&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from quantitative_trading_model.config import (
    ResearchConfig,
    ForecastConfig,
    VaRConfig,
    validate_config,
)

config = ResearchConfig(
    forecast=ForecastConfig(
        model_name="att_bilstm",
        horizon=3,
        hidden_size=32,
        dropout=0.20,
        loss="mae",
        optimizer="rmsprop",
    ),
    var=VaRConfig(
        confidence=0.95,
        horizon=1,
        lookback=100,
        action="scale",
    ),
)

validate_config(config)

metadata = {
    "window_length": config.data.window_length,
    "stride": config.data.stride,
    "forecast_horizon": config.forecast.horizon,
    "dropout": config.forecast.dropout,
    "loss": config.forecast.loss,
    "optimizer": config.forecast.optimizer,
    "var_confidence": config.var.confidence,
    "var_lookback": config.var.lookback,
    "fee_rate": config.execution.fee_rate,
    "liquidate_at_end": config.backtest.liquidate_at_end,
}</code></pre></div><p>This record combines direct paper-inspired values with reconstruction choices. It should be saved alongside forecast metrics, portfolio metrics, data provenance, and software-version information.</p><p><code>ResearchConfig</code> is available from <code>quantitative_trading_model.config</code>. It is not currently included in the package-level exports in <code>quantitative_trading_model/__init__.py</code>, so importing it from the configuration module is the reliable documented path.</p><h3>4.9 Configuration is necessary but not sufficient</h3><p>The configuration contract solves an engineering problem: it makes assumptions visible, validates basic values, and gives experiments a stable record. It does not solve the paper's missing-information problem. It cannot recover:</p><ul><li><p>the unidentified gold and Bitcoin datasets;</p></li><li><p>the original dates, frequency, or test boundary;</p></li><li><p>the exact attention query and network architecture;</p></li><li><p>the corrupted recursive position-sizing equations;</p></li><li><p>the paper's VaR settings or action policy;</p></li><li><p>the modified greedy algorithm's objective;</p></li><li><p>the original execution, fee, and liquidation protocol;</p></li><li><p>the numeric values behind the fee-sensitivity figures.</p></li></ul><p>The approximately <code>$646</code> gold result, approximately <code>$215,487</code> Bitcoin result, combined approximately <code>$216,133</code> result, and reported benchmark metrics therefore remain unverified paper claims. Matching visible defaults does not reproduce those results.</p><p>The generated project also has known API inconsistencies between some configuration fields and downstream modules, and semantic code verification was skipped. This section consequently describes the intended configuration contract and the actual validation boundaries; it does not claim that the generated code was executed or that every setting passed through the complete runtime pipeline.</p><p>The safest interpretation is that configuration turns an incomplete paper into an auditable research specification. It identifies what comes from the source, what is reconstructed, what is merely illustrative, and which safety controls belong to this educational implementation rather than to the original paper.</p><h2>5. Load Local Prices Without Assuming the Paper&#8217;s Missing Dataset</h2><p>The paper says that it uses historical gold and Bitcoin prices, but it does not identify the data source, instrument, currency, frequency, date range, or exact columns. That omission is important: even a correct implementation can produce different results if it uses a different gold contract, Bitcoin exchange, timezone, sampling interval, or adjusted-price convention.</p><p>The generated project therefore does not silently select a market-data provider. Instead, it accepts a local CSV file or a caller-supplied pandas object. This keeps the experiment offline and makes data provenance the reader&#8217;s responsibility. Before treating a result as empirical evidence, record where the file came from, which instrument it represents, and how it was prepared.</p><h3>5.1 The minimum data contract</h3><p>The implementation reduces each asset to two essential fields:</p><ul><li><p><code>timestamp</code>: when the price observation was available;</p></li><li><p><code>price</code>: a finite, strictly positive price.</p></li></ul><p>A minimal CSV looks like this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;csv&quot;,&quot;nodeId&quot;:&quot;4bf7032e-8866-4a9d-b8f9-ef87c1998053&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-csv">timestamp,price
2020-01-01,1518.2
2020-01-02,1523.7
2020-01-03,1511.4</code></pre></div><p>The <code>PriceData</code> class in <code>src/quantitative_trading_model/data.py</code> stores the cleaned result and an asset label:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;4dd73e3e-5153-479f-92d8-59ab84f2bf42&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">@dataclass(frozen=True)
class PriceData:
    frame: pd.DataFrame
    asset: str = "asset"

    def __post_init__(self) -&gt; None:
        required = {"timestamp", "price"}
        missing = required.difference(self.frame.columns)
        if missing:
            raise ValueError(f"PriceData is missing required columns: {sorted(missing)}")

        timestamps = pd.to_datetime(self.frame["timestamp"], errors="raise")
        prices = pd.to_numeric(self.frame["price"], errors="raise").to_numpy(dtype=float)

        if timestamps.duplicated().any():
            raise ValueError("PriceData timestamps must be unique")
        if not timestamps.is_monotonic_increasing:
            raise ValueError("PriceData timestamps must be strictly increasing")
        if not np.isfinite(prices).all() or (prices &lt;= 0).any():
            raise ValueError("PriceData prices must be finite and strictly positive")</code></pre></div><p><code>PriceData.__post_init__</code> is a validation boundary. It does not decide whether a price is economically meaningful for every possible instrument, but it enforces the assumptions required by the downstream time-series code. An empty dataset, duplicate timestamp, unsorted timestamp sequence, missing value, infinity, zero, or negative price cannot silently proceed into window construction.</p><p>The <code>timestamps</code> and <code>prices</code> properties provide convenient, typed access to the two series:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;17beb2fd-68b7-4a1a-a4ed-b0781b084374&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">@property
def timestamps(self) -&gt; pd.DatetimeIndex:
    return pd.DatetimeIndex(self.frame["timestamp"])

@property
def prices(self) -&gt; np.ndarray:
    return self.frame["price"].to_numpy(dtype=float, copy=True)</code></pre></div><p>Returning a copy from <code>prices</code> reduces the chance that a caller accidentally mutates the validated internal frame without re-running validation.</p><h3>5.2 Standardizing different local column names</h3><p>Real CSV files often use names such as <code>Date</code>, <code>datetime</code>, <code>Close</code>, or <code>Adj Close</code>. The <code>standardize_prices</code> function accepts explicit column names when necessary, but can also recognize common alternatives:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;5e8daf45-e6f0-47cd-bc79-b561e6a75aaf&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def standardize_prices(
    data: Union[pd.DataFrame, PriceData],
    *,
    timestamp_column: Optional[str] = None,
    price_column: Optional[str] = None,
    asset: str = "asset",
    drop_invalid: bool = True,
) -&gt; PriceData:</code></pre></div><p>The function first copies the input, so cleaning does not mutate the caller&#8217;s DataFrame. It then resolves columns through <code>_resolve_column</code>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;4b581dca-a480-46a8-b23a-f84f15b0dada&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">timestamp_column = _resolve_column(
    frame, timestamp_column,
    ("timestamp", "datetime", "date", "time"),
    "timestamp",
)
price_column = _resolve_column(
    frame, price_column,
    ("price", "close", "adj_close", "adjusted_close"),
    "price",
)</code></pre></div><p>Explicit names are safer when a file contains several possible price fields. For example, choosing <code>close</code> versus <code>adjusted_close</code> can materially change a historical experiment. Automatic inference is a convenience, not a substitute for documenting the choice.</p><p>The selected columns are renamed to the package&#8217;s stable internal names:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;64efd00c-fc7a-4ae9-b129-58503b4274bf&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">normalized = frame[[timestamp_column, price_column]].rename(
    columns={timestamp_column: "timestamp", price_column: "price"}
)
normalized["timestamp"] = pd.to_datetime(
    normalized["timestamp"], errors="coerce", utc=False
)
normalized["price"] = pd.to_numeric(normalized["price"], errors="coerce")</code></pre></div><p>Values that cannot be parsed become missing. The function then constructs a validity mask requiring a real timestamp and a finite, positive price:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;18058e46-0107-4bae-8ce1-291e05db7baf&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">valid = normalized["timestamp"].notna() &amp; normalized["price"].notna()
valid &amp;= np.isfinite(normalized["price"].to_numpy(dtype=float))
valid &amp;= normalized["price"] &gt; 0</code></pre></div><p>By default, invalid rows are removed. If <code>drop_invalid=False</code>, the function raises an error instead. The choice depends on the research context: dropping a corrupted row may be reasonable for a demonstration, but a production-quality study should usually stop and investigate why the row is invalid rather than silently deleting it.</p><h3>5.3 Sorting and duplicate timestamps</h3><p>After parsing, the function sorts by timestamp using a stable sort and removes duplicate timestamps by retaining the last source row:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;e6166f47-3cc2-4e39-90d9-d4c18975aead&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">normalized = (
    normalized.sort_values("timestamp", kind="mergesort")
    .drop_duplicates("timestamp", keep="last")
    .reset_index(drop=True)
)</code></pre></div><p>This gives the downstream model one price per timestamp. It does not prove that the retained row is the correct observation. If duplicate rows represent separate trades, multiple venues, or different assets, they should be aggregated or separated before calling <code>standardize_prices</code>.</p><p>The duplicate policy is therefore an implementation convenience, not a fact recovered from the paper. The paper does not explain how duplicate timestamps or multiple records within one sampling interval were handled.</p><h3>5.4 Loading a local CSV</h3><p><code>load_price_csv</code> is a thin file-system wrapper around <code>standardize_prices</code>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;135de609-cc58-4011-ad70-afdb4ab63170&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def load_price_csv(
    path: Union[str, Path],
    *,
    timestamp_column: Optional[str] = None,
    price_column: Optional[str] = None,
    asset: str = "asset",
    drop_invalid: bool = True,
) -&gt; PriceData:
    source = Path(path)
    if not source.exists() or not source.is_file():
        raise FileNotFoundError(f"Price CSV does not exist: {source}")
    if source.suffix.lower() != ".csv":
        raise ValueError("load_price_csv accepts a local .csv file")
    return standardize_prices(
        pd.read_csv(source),
        timestamp_column=timestamp_column,
        price_column=price_column,
        asset=asset,
        drop_invalid=drop_invalid,
    )</code></pre></div><p>The function deliberately accepts only a local <code>.csv</code> path. There are no HTTP requests, exchange clients, credentials, or hidden downloads. This is consistent with the project&#8217;s educational and offline scope.</p><p>The CLI exposes the same idea through the <code>validate-data</code> subcommand. Its local-path helper rejects URL-like strings before loading them:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;78828c5e-16ff-469f-b6b1-ea85f35c5eab&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def _safe_input_path(value: str) -&gt; Path:
    if "://" in value:
        raise CLIError(
            "Only local files are supported; URLs and network paths are not accepted"
        )
    path = Path(value).expanduser()
    if not path.exists():
        raise CLIError(f"Input file does not exist: {path}")
    if not path.is_file():
        raise CLIError(f"Input path is not a regular file: {path}")
    return path.resolve()</code></pre></div><p>A local validation command can therefore be written as:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:&quot;8e9d0da1-1876-403b-aaa1-de3c804502d4&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">quant-trading-model validate-data prices.csv \
  --timestamp-column timestamp \
  --price-column close \
  --asset gold</code></pre></div><p>The command prints or writes a compact summary rather than training a model. This makes data inspection a separate step from forecasting and backtesting.</p><h3>5.5 Keeping gold and Bitcoin separate</h3><p>The <code>PriceData</code> representation is intentionally single-asset. A file may contain multiple assets, but the research pipeline should filter it into one chronologically ordered series per asset before creating windows. Gold and Bitcoin have different price scales, trading calendars, liquidity characteristics, and likely data sources. Combining them accidentally into one price sequence would make the next observation after a gold row appear to be a Bitcoin movement.</p><p>A multi-asset table can be filtered explicitly:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;fc2b9cd3-89aa-4286-8371-e4bd101b9f2b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">raw = pd.read_csv("historical_prices.csv")

gold = standardize_prices(
    raw.loc[raw["asset"].eq("gold")],
    timestamp_column="timestamp",
    price_column="price",
    asset="gold",
)

bitcoin = standardize_prices(
    raw.loc[raw["asset"].eq("bitcoin")],
    timestamp_column="timestamp",
    price_column="price",
    asset="bitcoin",
)</code></pre></div><p>Each result can then be passed independently to the window builder and forecasting experiment. This also makes it possible to record asset-specific provenance and fee assumptions. The paper&#8217;s headline results allocate $500 to each asset, but the extracted text does not identify whether the assets were sampled on matching dates or evaluated with identical calendars.</p><h3>5.6 Synthetic data are for interfaces, not validation</h3><p>Because the paper&#8217;s original datasets are unavailable, the project includes <code>generate_synthetic_prices</code>. It creates deterministic, positive price paths using a seeded random generator:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;bd45fa12-0606-4623-9297-d96654589830&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def generate_synthetic_prices(
    *,
    asset: str = "synthetic",
    periods: int = 500,
    start: Union[str, pd.Timestamp] = "2020-01-01",
    seed: int = 7,
    initial_price: Optional[float] = None,
) -&gt; PriceData:</code></pre></div><p>The function chooses different illustrative drift and volatility defaults for labels containing <code>gold</code> or <code>bitcoin</code>, then constructs a positive path from exponentiated random shocks. The seed makes the example repeatable for teaching and invariant checks.</p><p>For example:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;b969007f-9175-4c04-bf1b-ed1ec1a730ac&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from quantitative_trading_model.data import generate_synthetic_prices

prices = generate_synthetic_prices(
    asset="gold",
    periods=500,
    seed=7,
)
print(prices.asset)
print(prices.frame.head())</code></pre></div><p>The label-specific behavior is only a teaching convenience. A &#8220;Bitcoin-like&#8221; synthetic path is not Bitcoin data, and a &#8220;gold-like&#8221; path is not a gold instrument. Synthetic data can demonstrate that timestamps, windows, attention tensors, sizing budgets, and portfolio accounting have compatible interfaces. It cannot validate the paper&#8217;s forecast metrics, fee curves, or reported final values.</p><h3>5.7 Data checks before modeling</h3><p>Before moving to sliding windows, record at least the following:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!xMDU!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cded6df-518e-4312-9db2-4eece7eca0ec_1282x588.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!xMDU!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cded6df-518e-4312-9db2-4eece7eca0ec_1282x588.png 424w, https://substackcdn.com/image/fetch/$s_!xMDU!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cded6df-518e-4312-9db2-4eece7eca0ec_1282x588.png 848w, https://substackcdn.com/image/fetch/$s_!xMDU!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cded6df-518e-4312-9db2-4eece7eca0ec_1282x588.png 1272w, https://substackcdn.com/image/fetch/$s_!xMDU!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cded6df-518e-4312-9db2-4eece7eca0ec_1282x588.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!xMDU!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cded6df-518e-4312-9db2-4eece7eca0ec_1282x588.png" width="1282" height="588" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6cded6df-518e-4312-9db2-4eece7eca0ec_1282x588.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:588,&quot;width&quot;:1282,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:122560,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/206399283?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cded6df-518e-4312-9db2-4eece7eca0ec_1282x588.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!xMDU!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cded6df-518e-4312-9db2-4eece7eca0ec_1282x588.png 424w, https://substackcdn.com/image/fetch/$s_!xMDU!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cded6df-518e-4312-9db2-4eece7eca0ec_1282x588.png 848w, https://substackcdn.com/image/fetch/$s_!xMDU!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cded6df-518e-4312-9db2-4eece7eca0ec_1282x588.png 1272w, https://substackcdn.com/image/fetch/$s_!xMDU!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cded6df-518e-4312-9db2-4eece7eca0ec_1282x588.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The generated tests in <code>tests/test_data_and_streaks.py</code> are intended to check these kinds of invariants with small hand-built fixtures. They are static artifacts in this project review; no claim is made here that the generated test suite was executed successfully.</p><p>The essential principle is simple: clean only what the data contract justifies, preserve timestamps, separate assets, and document every assumption before training. Once the local series is trustworthy and its provenance is recorded, the next step is to construct the paper&#8217;s 100-observation windows without allowing scaling or temporal partitioning to leak future information.</p><h2>6. Build Leakage-Safe Sliding Windows</h2><p>The paper describes a sliding-window forecasting setup with a history of 100 observations and a stride of one. The model reads the latest 100 observations, predicts a later value, advances one observation, and creates the next overlapping example.</p><p>The important issue is not only the window size. Every forecast must use information available at its decision timestamp. The target must occur after the input window, scaling parameters must be fitted without future observations, and evaluation must preserve chronological order.</p><h3>6.1 The one-step relationship</h3><p>Let <code>z_t</code> denote a transformed price or feature value at time <code>t</code>. With a window length of 100, a one-step sample is:</p><p>\[ X<em>t = [z</em>{t-99}, z<em>{t-98}, \ldots, z</em>{t-1}, z<em>t], \qquad y</em>t = z_{t+1}. \]</p><p>The final value in <code>X_t</code> is the latest information available when the forecast is generated. The target belongs to the next observation.</p><p>The paper-inspired window settings are represented by <code>DataConfig</code>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;538dee89-7b54-4de4-84d9-0010889e25c7&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">@dataclass
class DataConfig:
    timestamp_column: str = "timestamp"
    price_column: str = "price"
    window_length: int = 100
    stride: int = 1</code></pre></div><p><code>window_length=100</code> and <code>stride=1</code> come from the paper. The paper does not clearly state whether the model uses raw prices, normalized prices, returns, or additional features, so the representation remains an explicit implementation choice.</p><h3>6.2 How <code>make_windows</code> constructs samples</h3><p>The central function is <code>make_windows</code> in <code>src/quantitative_trading_model/data.py</code>. It accepts a cleaned <code>PriceData</code> object, a DataFrame, or a numeric array. For timestamp-aware inputs, it stores the start, end, and target timestamps alongside the arrays.</p><p>The core operation is equivalent to:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;fc082e4f-8ffb-4ae0-8ce2-51b04bfdd636&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">for start in range(0, last_start + 1, stride):
    end = start + window_length - 1
    X.append(transformed[start : end + 1])
    targets.append(transformed[end + 1 : end + 1 + horizon, 0])
    target_times.append(timestamps[end + horizon])</code></pre></div><p>With the defaults, the first sample contains observations <code>0</code> through <code>99</code> and targets observation <code>100</code>. The next sample contains observations <code>1</code> through <code>100</code> and targets observation <code>101</code>.</p><p>The returned <code>WindowedDataset</code> includes temporal metadata:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;e525b16f-f8a2-4a41-8c04-ab9e5c0c3fa7&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">@dataclass(frozen=True)
class WindowedDataset:
    X: np.ndarray
    y: np.ndarray
    window_start: pd.DatetimeIndex
    window_end: pd.DatetimeIndex
    y_timestamp: pd.DatetimeIndex
    feature_names: Tuple[str, ...] = ("price",)</code></pre></div><p>Its invariant check requires every target timestamp to follow the corresponding input window:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;f84ec74a-311c-4aec-9cc7-a200718f5719&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">if n and not (self.y_timestamp &gt; self.window_end).all():
    raise ValueError("Every target timestamp must be after its input window")</code></pre></div><p>This timestamp guarantee applies when the input carries timestamps, such as <code>PriceData</code> or a cleaned DataFrame. For a raw numeric array, the current implementation has no timestamp argument in that code path and creates placeholder daily timestamps beginning at <code>1970-01-01</code>. Those generated timestamps preserve array alignment for shape-based demonstrations, but they are not source-market timestamps. A trading experiment should therefore use <code>PriceData</code> or a timestamp-aware DataFrame.</p><h3>6.3 One-day versus three-day targets</h3><p>The paper is ambiguous about the forecast horizon. Its prediction discussion is compatible with one-step forecasting, while its conclusion refers to forecasts for the next three days. The implementation exposes <code>horizon</code> rather than silently choosing one interpretation.</p><p>For <code>horizon=1</code>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;3333df75-46a0-40bc-8412-4a694af53e49&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">X.shape = (samples, 100, features)
y.shape = (samples,)</code></pre></div><p>For <code>horizon=3</code>:</p><p>\[ y<em>t = [z</em>{t+1}, z<em>{t+2}, z</em>{t+3}]. \]</p><p>The shape is then:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;977ea76f-26cb-4ac7-a03e-8d4b1a6d0e92&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">X.shape = (samples, 100, features)
y.shape = (samples, 3)</code></pre></div><p>The final target timestamp is the timestamp of <code>z_{t+3}</code>. Converting a three-value forecast into a buy, sell, or hold decision belongs to the strategy layer. Possible rules include using the terminal forecast, the mean forecast, or a separately specified multi-day signal.</p><p>For a series of <code>N</code> observations, the number of stride-one windows is:</p><p>N&#8722;100&#8722;h+1,</p><p>where <code>h</code> is the forecast horizon. The final window must leave enough observations for every target value.</p><h3>6.4 Overlapping windows and temporal partitions</h3><p>Stride one creates heavily overlapping samples:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;2b474807-d037-4f57-bdfe-30ccfb9ef908&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">window 1: [0, 1, 2, ..., 99] -&gt; target 100
window 2: [1, 2, 3, ..., 100] -&gt; target 101</code></pre></div><p>This overlap is normal for sliding-window forecasting. The risk comes from treating neighboring samples as independent random observations. If windows are randomly shuffled before splitting, almost identical histories can appear in both partitions, producing optimistic estimates.</p><p>The generated recurrent training code therefore uses non-shuffled batches:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;a0b32f31-83f2-4f2d-b05b-1a42b310033e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">train_loader = DataLoader(
    TensorDataset(torch.from_numpy(x_train), torch.from_numpy(y_train)),
    batch_size=batch_size,
    shuffle=False,
)</code></pre></div><p><code>chronological_split</code> also preserves sample order. This prevents random cross-partition mixing, but it does <strong>not</strong> guarantee that raw input histories are disjoint at the boundary. The final training window and first test window may still share observations because the windows overlap by design. That dependence should be acknowledged when interpreting metrics; walk-forward evaluation and a documented gap can provide a stricter protocol.</p><h3>6.5 Fit scaling on training observations only</h3><p>Scaling can make neural-network optimization easier, but fitting a scaler on the complete series leaks future distribution information. For example, a full-series min-max scaler exposes the training process to the future minimum and maximum.</p><p>The repository provides <code>ChronologicalScaler</code>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;d801e448-278e-468e-9b20-ed5a007773b1&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">scaler = ChronologicalScaler()
scaler.fit(training_values)                 # training rows only
train_scaled = scaler.transform(train_values)
test_scaled = scaler.transform(test_values)</code></pre></div><p>The safe sequence is:</p><p>Clean and sort the complete local file.</p><p>Establish the chronological raw-row training boundary.</p><p>Fit the scaler only on rows before that boundary.</p><p>Transform later rows with the fitted scaler.</p><p>Build windows and retain their timestamps.</p><p>Inverse-transform predictions before reporting price-level metrics when appropriate.</p><p><code>make_windows</code> accepts an already fitted scaler. Its <code>fit_scaler</code> option is intended for controlled demonstrations; setting <code>fit_scaler=True</code> on the complete dataset before an out-of-sample experiment would leak future information. The window builder does not know whether the caller selected a valid training boundary and cannot enforce this policy automatically.</p><p>A safer pattern is:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;6b78a406-21ff-4282-a6fe-c6e7da5e8d09&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">train_rows = price_data.frame.iloc[:training_row_count]
scaler = ChronologicalScaler().fit(
    train_rows[["price"]].to_numpy(dtype=float)
)

windows = make_windows(
    price_data,
    window_length=100,
    horizon=1,
    stride=1,
    scaler=scaler,
    fit_scaler=False,
)</code></pre></div><p>Here, <code>training_row_count</code> must be determined from a documented chronological rule, such as a date boundary or a fixed initial training period. It must not be chosen after inspecting test performance. If windows are built from the entire transformed series and then split, the scaler remains training-fitted, but boundary overlap still exists; a stricter workflow can build or select partitions around an explicit raw-row boundary.</p><h3>6.6 Chronological 70:30 splitting</h3><p>The paper reports a 70:30 train/test split. The implementation provides <code>chronological_split</code> for a paper-style comparison:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;381b4752-846f-4402-b0e1-8dcb35f61b1c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">train, test = chronological_split(
    dataset,
    train_fraction=0.70,
    validation_fraction=0.0,
)</code></pre></div><p>The function slices ordered samples rather than randomly selecting indices. With validation data, the order is:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;6d8b5105-3b40-497e-8039-530c2f39796c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">training windows -&gt; validation windows -&gt; test windows</code></pre></div><p>The metadata-preserving helper is conceptually:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;3cc9069c-69d9-4b20-a3c7-7e6ca964d4d6&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def dataset_slice(dataset, start, stop):
    return WindowedDataset(
        X=dataset.X[start:stop],
        y=dataset.y[start:stop],
        window_start=dataset.window_start[start:stop],
        window_end=dataset.window_end[start:stop],
        y_timestamp=dataset.y_timestamp[start:stop],
        feature_names=dataset.feature_names,
    )</code></pre></div><p>A useful ordering check is:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;d45ea1d6-a032-43dd-99b8-4ddc8343d8d0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">train_times = train.y_timestamp
test_times = test.y_timestamp
assert train_times.max() &lt; test_times.min()</code></pre></div><p>This confirms that target timestamps are ordered across the sample split. It does not prove that the corresponding input histories are disjoint, because stride-one windows can overlap at the boundary.</p><p>The exact boundary remains a reconstruction. The paper does not specify the split date or whether it splits raw observations before window construction or partitions already-created windows. Those choices can produce different edge behavior and should be recorded in experiment metadata.</p><h3>6.7 Walk-forward evaluation for trading conclusions</h3><p>A single 70:30 split answers only how one fixed training period performs on one later period. It does not show how results change as new observations arrive.</p><p>An expanding walk-forward procedure repeatedly preserves the information boundary:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;cda0d5fb-7238-489f-8791-295426e33852&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">train on [0, ..., t]
forecast the next block
advance the boundary
expand the training set
forecast the next block
repeat</code></pre></div><p>The <code>walk_forward_splits</code> generator is used like this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;ca739ab0-291d-44e9-b61e-665771831f41&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">for train, validation, test in walk_forward_splits(
    dataset,
    initial_train_size=500,
    validation_size=0,
    test_size=20,
    step=20,
):
    # Fit only on train, optionally tune on validation,
    # then evaluate on the later test block.
    pass</code></pre></div><p>Each test block follows its training block. The training set expands over time; a rolling fixed-length training window would be another explicit choice.</p><p>The paper does not report walk-forward results, so such results are not reproductions of its tables. They are a more appropriate protocol for trading conclusions because they repeatedly test the model on observations that follow the fitting period.</p><h3>6.8 What the generated test artifact actually demonstrates</h3><p>The generated <code>tests/test_data_and_streaks.py</code> uses a compatibility helper with a numeric price array and a separate timestamp object. In the current <code>make_windows</code> implementation, the numeric-array path does not accept or preserve that separate timestamp argument; it creates placeholder daily timestamps. Therefore, the test's meaningful checks are the numeric window boundaries and target values, while its timestamp behavior should not be interpreted as validation of real source timestamps.</p><p>A timestamp-aware illustrative example, using the public data contract directly, is:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;a403446a-5733-4edb-bd87-1bc2a1048516&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">prices = np.arange(10.0, 17.0)
timestamps = pd.date_range(
    "2024-01-01", periods=len(prices), freq="D"
)
price_data = PriceData(
    pd.DataFrame({"timestamp": timestamps, "price": prices})
)

dataset = make_windows(
    price_data,
    window_length=3,
    horizon=1,
    stride=1,
)

np.testing.assert_allclose(dataset.y, prices[3:])
assert (dataset.y_timestamp &gt; dataset.window_end).all()</code></pre></div><p>This is an illustrative contract for timestamp-aware input, not a claim that the generated test suite was executed. It also makes the distinction clear: use <code>PriceData</code> or a timestamp-aware DataFrame when timestamps matter; use raw arrays only when placeholder metadata is acceptable or when a higher-level API supplies the timestamp mapping separately.</p><p>The project was reviewed primarily through static checks, and semantic code verification was skipped. The intended contracts are described here, but this section does not claim that the complete test suite passed or that runtime behavior was independently confirmed.</p><h3>6.9 Practical preparation workflow</h3><p>For a local asset file, a disciplined workflow is:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;d9f3631f-7e18-4fa4-86f0-622c77dfa16e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">price_data = load_price_csv(
    "data/gold.csv",
    timestamp_column="timestamp",
    price_column="price",
    asset="gold",
)

# Establish this from a documented date or raw-row boundary.
training_row_count = 500
train_rows = price_data.frame.iloc[:training_row_count]
scaler = ChronologicalScaler().fit(
    train_rows[["price"]].to_numpy(dtype=float)
)

windows = make_windows(
    price_data,
    window_length=100,
    horizon=1,       # use 3 only when a three-step target is intended
    stride=1,
    scaler=scaler,
    fit_scaler=False,
)
train, test = chronological_split(windows, train_fraction=0.70)</code></pre></div><p>The row boundary must be established before fitting the scaler and must reflect the intended training period. The split shown above is a paper-style sample split applied after window construction. For a stricter out-of-time study, define the raw-row boundary and any gap explicitly, then use walk-forward partitions for repeated evaluation.</p><h3>6.10 Preparation limits</h3><p>This data layer provides:</p><ul><li><p>the paper's 100-observation, stride-one setup;</p></li><li><p>one-step and three-step target shapes;</p></li><li><p>timestamp metadata for <code>PriceData</code> and timestamp-aware DataFrame inputs;</p></li><li><p>chronological ordering and sample splits;</p></li><li><p>training-only scaling primitives;</p></li><li><p>expanding walk-forward partitions.</p></li></ul><p>It does not resolve the paper's missing data source, instrument definition, feature representation, exact split date, forecast horizon, or execution protocol. It also cannot automatically determine whether a caller fitted a scaler on an appropriate subset or whether overlapping input histories are acceptable for a particular evaluation design.</p><p>The essential causal contract is therefore: the historical window ends first, the target occurs later, scaling uses only the declared training information, and execution is aligned to a timestamp after the information endpoint. Later forecasting and backtesting layers should preserve that contract explicitly.</p><h2>7. Keep Forecast Accuracy Separate from Trading Performance</h2><p>The paper compares forecasting models with RMSE, MAE, MAPE, and R-squared. These metrics answer one question: <strong>how close were the predictions to their targets?</strong> They do not establish that a trading strategy is profitable after signal rules, position sizing, VaR, execution timing, fees, slippage, and liquidity are included.</p><p>The implementation keeps these concerns separate in <code>src/quantitative_trading_model/metrics.py</code>:</p><ul><li><p>forecast metrics operate on target and prediction arrays;</p></li><li><p>portfolio metrics operate on a marked-to-market value path and optional execution records;</p></li><li><p><code>backtest.py</code> supplies the fills, traded notionals, and fees used by portfolio reporting.</p></li></ul><p>This distinction is also documented in <code>README.md</code>, which warns that forecast accuracy does not establish profitability and recommends reporting drawdown, volatility, turnover, fees, and a buy-and-hold comparison.</p><h3>7.1 Forecast error metrics</h3><p>Let <code>y_k</code> be the observed target and <code>y_hat_k</code> the corresponding prediction for evaluation item <code>k</code>.</p><h4>Mean absolute error</h4><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot; \\operatorname{MAE} = \\frac{1}{N}\\sum{k=1}^{N}|yk-\\hat y_k|. &quot;,&quot;id&quot;:&quot;YIRKOIZOKT&quot;}" data-component-name="LatexBlockToDOM"></div><p>MAE is measured in the target's units. For raw dollar prices, it is measured in dollars. The paper-inspired recurrent models also use MAE as their training loss.</p><h4>Root mean squared error</h4><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\operatorname{RMSE} = \\sqrt{\\frac{1}{N}\\sum{k=1}^{N}(yk-\\hat y_k)^2}. &quot;,&quot;id&quot;:&quot;WNMEYLGIUU&quot;}" data-component-name="LatexBlockToDOM"></div><p>Squaring the residuals gives larger errors more influence. RMSE is scale-dependent, so raw RMSE values for gold and Bitcoin should not be compared without considering their different price magnitudes and target representations.</p><h4>Mean absolute percentage error</h4><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot; \\operatorname{MAPE} = \\frac{100}{N}\\sum{k=1}^{N}\\left|\\frac{yk-\\hat yk}{yk}\\right|. &quot;,&quot;id&quot;:&quot;IMCCRHRKBQ&quot;}" data-component-name="LatexBlockToDOM"></div><p>The implementation reports MAPE in percentage points: <code>2.5</code> means 2.5%, not 0.025. MAPE is undefined or unstable for zero and near-zero targets. <code>forecast_metrics</code> therefore exposes two controls:</p><ul><li><p><code>mape_epsilon</code> defines the near-zero threshold;</p></li><li><p><code>mape_zero_policy="exclude"</code> omits those targets, while <code>"raise"</code> rejects them.</p></li></ul><p>The number of observations included in MAPE is stored separately from the total sample count. Two MAPE values should not be compared without checking that policy and count.</p><h4>R-squared</h4><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;R^2 = 1 - \\frac{\\sumk(yk-\\hat yk)^2}{\\sumk(y_k-\\bar y)^2}. &quot;,&quot;id&quot;:&quot;LPSWKPKLXS&quot;}" data-component-name="LatexBlockToDOM"></div><p>R-squared compares residual variation with the variation of the targets around their mean. It can be negative when predictions are worse than the constant mean-target baseline. If every target is identical, the denominator is zero; the generated implementation returns <code>1.0</code> for an exact prediction and <code>0.0</code> otherwise.</p><h3>7.2 The forecast-metrics interface</h3><p>The public function is <code>forecast_metrics(actual, predicted, ...)</code>. It validates finite inputs, flattens supported array-like values, checks that targets and predictions have equal lengths, and returns a <code>ForecastMetrics</code> dataclass.</p><p>The following is a <strong>shortened tutorial excerpt</strong>, not the complete generated source. The actual module also contains helper functions for input validation, rounding, and serialization.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;1d1efa10-02e4-4c25-8eda-8068ef16da82&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">metrics = forecast_metrics(
    actual,
    predicted,
    mape_epsilon=1e-12,
    mape_zero_policy="exclude",
)

print(metrics.rmse)
print(metrics.mae)
print(metrics.mape_percent)
print(metrics.r_squared)
print(metrics.as_dict())</code></pre></div><p><code>ForecastMetrics.as_dict()</code> is the generated dataclass's serialization method. It includes the four metrics, sample counts, the MAPE threshold, and the selected MAPE policy. The full implementation should be treated as authoritative rather than this abbreviated excerpt.</p><p>For example:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;f08a0bb2-26e7-4cfe-84fa-b4d9132676fa&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">import numpy as np
from quantitative_trading_model.metrics import forecast_metrics

actual = np.array([100.0, 110.0, 90.0])
predicted = np.array([102.0, 107.0, 93.0])

result = forecast_metrics(actual, predicted)</code></pre></div><p>The residuals are <code>[2, -3, 3]</code>. Thus MAE is <code>8/3</code>, RMSE is <code>sqrt(22/3)</code>, and MAPE is the mean of the three absolute relative errors multiplied by 100. These values describe only the prediction sample. They do not determine whether a strategy would buy, sell, or hold.</p><p>For multi-step predictions represented as <code>(samples, horizon)</code>, the generated metric helper flattens the arrays. The caller must ensure that the target and prediction arrays have identical ordering. A production experiment should also retain target timestamps so that metrics are calculated on aligned observations.</p><h3>7.3 Why forecast accuracy is not trading performance</h3><p>A model can have favorable forecast errors and still produce a poor trading result:</p><p>A predicted move may be too small to cover entry and exit costs.</p><p>Price-level accuracy does not guarantee correct direction relative to the current price.</p><p>A forecast evaluated at a closing timestamp may not describe the price available at execution.</p><p>Position sizing can amplify a small forecast error.</p><p>A shuffled or otherwise optimistic time-series split may not represent deployment conditions.</p><p>Fees, spreads, slippage, partial fills, and liquidity are portfolio effects, not components of RMSE or MAE.</p><p>The paper reports strong Att-BiLSTM forecasting metrics, but those reported values remain unverified claims because the source data, preprocessing, target definition, and test protocol are unavailable. Even reproducing the metric ranking would not prove that the resulting trading strategy is profitable.</p><h3>7.4 Portfolio metrics use a different vocabulary</h3><p>Portfolio evaluation begins with marked-to-market values:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot; Vt = \\operatorname{cash}t + \\operatorname{quantity}t pt. &quot;,&quot;id&quot;:&quot;IHELZKMAKW&quot;}" data-component-name="LatexBlockToDOM"></div><p>The generated implementation reports or supports the following measures.</p><h4>Cumulative return</h4><p>\[ R<em>{0:T} = \frac{V</em>T}{V_0}-1. \]</p><p>This reflects trades, fees, remaining holdings, and the configured liquidation policy.</p><h4>Drawdown and maximum drawdown</h4><p>\[ D<em>t = \frac{V</em>t}{\max<em>{s\le t}V</em>s}-1. \]</p><p>Drawdown is non-positive: <code>-0.25</code> means the portfolio is 25% below its previous running peak. Maximum drawdown is the minimum value in the drawdown series.</p><h4>Volatility</h4><p>For periodic returns <code>r_t</code>, annualized volatility is approximately:</p><p>\[ \sigma<em>{annual}=\operatorname{std}(r</em>t)\sqrt{P}, \]</p><p>where <code>P</code> is the configured number of periods per year. The default of 252 is an implementation assumption for daily observations, not a paper-specified value.</p><h4>Sharpe ratio</h4><p>\[ \operatorname{Sharpe} = \frac{\operatorname{mean}(r<em>t-r</em>{f,t})} {\operatorname{std}(r<em>t-r</em>{f,t})}\sqrt{P}. \]</p><p>The implementation converts an annual risk-free rate into a periodic compound-equivalent rate. Its default risk-free rate is zero, another explicit evaluation choice rather than a paper result.</p><h4>Turnover and fees</h4><p>The generated metrics layer defines turnover as traded notional divided by mean portfolio value:</p><p>\[ \operatorname{turnover} = \frac{\sum<em>i \operatorname{traded\ notional}</em>i}{\operatorname{mean}(V_t)}. \]</p><p>Fees are supplied by the execution layer and summed separately:</p><p>\[ \operatorname{total\ fees}=\sum<em>i\operatorname{fee}</em>i. \]</p><p>This preserves the distinction between portfolio valuation and fill-level accounting.</p><h3>7.5 The portfolio-metrics interface</h3><p><code>portfolio_metrics</code> accepts a value path and optional periodic returns, traded notionals, fees, annualization frequency, and annual risk-free rate. The following is a <strong>shortened excerpt</strong>, not a complete copy of <code>metrics.py</code>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;4739f1f5-c6f2-482c-85c7-fe81869336ed&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">portfolio = portfolio_metrics(
    portfolio_values,
    returns=None,              # derive returns from consecutive values
    traded_notionals=notionals,
    fees=fees,
    periods_per_year=252,
    risk_free_rate_annual=0.0,
)

print(portfolio.cumulative_return)
print(portfolio.maximum_drawdown)
print(portfolio.annualized_volatility)
print(portfolio.sharpe_ratio)
print(portfolio.turnover)
print(portfolio.total_fees)</code></pre></div><p>The full generated function additionally validates positive portfolio values, finite inputs, return-length conventions, nonnegative notionals, scalar-versus-sequence fee inputs, and risk-free-rate settings. It returns a <code>PortfolioMetrics</code> dataclass with an <code>as_dict()</code> method.</p><h3>7.6 A report should retain both layers</h3><p>A useful experiment table keeps forecast and portfolio results in separate columns:</p><p>| Forecast evaluation | Portfolio evaluation | | --- | --- | | RMSE | Final value | | MAE | Cumulative return | | MAPE and MAPE policy | Maximum drawdown | | R-squared | Annualized volatility | | Target horizon | Sharpe ratio | | Test timestamps | Turnover | | Scaling metadata | Total fees |</p><p>This prevents a low prediction error from being mistaken for a trading objective. A buy-and-hold benchmark should also use the same asset, period, initial capital, fee convention, and valuation policy.</p><h3>7.7 Verification scope</h3><p>The supplied project currently does <strong>not</strong> include a dedicated metrics test module. <code>tests/test_data_and_streaks.py</code> tests data preparation and streak behavior; it does not directly test MAPE policies, constant-target R-squared behavior, drawdown calculations, or fee aggregation. Those behaviors are documented and statically inspectable invariants, not claims of executed test coverage.</p><p>Static review can still check that:</p><ul><li><p>forecast arrays are finite and aligned;</p></li><li><p>MAPE has an explicit near-zero policy;</p></li><li><p>constant-target R-squared behavior is deterministic;</p></li><li><p>portfolio values are positive and drawdown is non-positive;</p></li><li><p>turnover and fees are supplied separately from forecast errors.</p></li></ul><p>However, semantic code verification was skipped, and the generated project was not claimed to have been executed. These checks therefore do not establish runtime correctness, profitability, or reproduction of the paper's metrics.</p><p>The practical rule is: <strong>use forecast metrics to evaluate the predictor and portfolio metrics to evaluate the trading system.</strong> Both are necessary, but neither is a substitute for the other.</p><h2>8. Implement the Attention-Enhanced BiLSTM</h2><p>The paper identifies an attention-enhanced bidirectional LSTM, or <strong>Att-BiLSTM</strong>, as its primary forecasting model. Its intended flow is:</p><p>Read a historical window, such as the latest 100 observations.</p><p>Process that window in both temporal directions with a BiLSTM.</p><p>Produce one hidden representation for every time step.</p><p>Use attention to assign larger weights to more relevant historical steps.</p><p>Combine the weighted representations into one context vector.</p><p>Map that context vector to a one-day or multi-day forecast.</p><p>The generated implementation follows this structure in <code>src/quantitative_trading_model/models/forecasters.py</code>. The attention mathematics is isolated in <code>src/quantitative_trading_model/models/attention.py</code>, while <code>tests/test_model_shapes.py</code> checks the intended tensor contracts when the optional PyTorch dependency is available.</p><p><strong>Reconstruction boundary:</strong> The paper specifies the broad Att-BiLSTM idea, dot-product attention, 20% dropout, MAE, and RMSprop. It does not specify the number of recurrent layers, hidden dimensions, attention-query construction, learning rate, training duration, or exact target representation. Those details are configurable implementation choices, not verified transcriptions of the original model.</p><h3>8.1 What the BiLSTM contributes</h3><p>An ordinary LSTM processes a sequence in one direction. Given an input window ordered from older to newer observations, its hidden state at time step <code>i</code> summarizes the observations up to that point in the forward direction.</p><p>A BiLSTM adds a second LSTM that processes the same historical window in reverse. At each time step, the model combines the forward and backward states. If the one-direction hidden size is <code>h</code>, the combined representation commonly has size <code>2h</code>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;d16da738-9122-4bb6-be04-2a09ce751cdb&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">input sequence:       (batch, time, features)
forward LSTM states:  (batch, time, h)
backward LSTM states: (batch, time, h)
combined states:      (batch, time, 2h)</code></pre></div><p>The important timing condition is that the complete window must end at the information timestamp. If the signal is generated after observing time <code>t</code>, the input may contain observations through <code>t</code>, but not <code>t+1</code> or any later observation. The backward LSTM is not automatically leakage because it looks backward <strong>inside the already available window</strong>. It becomes leakage only if the window itself contains future observations.</p><p>The model therefore relies on the contract established by the data layer:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;4ff48439-377d-4fee-b857-6b5c9658e79a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">window ends at t
forecast is generated using data through t
execution occurs at a later bar</code></pre></div><p>This is why the window metadata and timestamp checks discussed in the previous section matter to the neural model as well as to the backtest.</p><h3>8.2 Dot-product temporal attention</h3><p>The BiLSTM produces a sequence of hidden representations rather than one final vector. The paper's attention mechanism decides how to combine those representations.</p><p>Let <code>x_i</code> be the BiLSTM representation at time step <code>i</code>, and let <code>q</code> be a query vector. The paper gives the following operations.</p><h4>Compatibility score</h4><p>\[ S(x<em>i,q) = x</em>i^Tq. \]</p><p>This dot product produces one scalar score for every time step. A larger score indicates stronger alignment between <code>x_i</code> and the query <code>q</code>.</p><h4>Attention weights</h4><p>The scores are normalized with a softmax:</p><p>\[ a<em>i = \operatorname{softmax}</em>i(S(x<em>i,q)) = \frac{\exp(S(x</em>i,q))} {\sum<em>j \exp(S(x</em>j,q))}. \]</p><p>The subscript on softmax is important. Normalization must happen <strong>over the time dimension</strong>, so that the weights for one sequence sum to one across its historical observations. Normalizing across features or across the batch would produce a different mechanism.</p><h4>Context vector</h4><p>Finally, the representations are combined with their attention weights:</p><p>\[ \operatorname{context}(q,x)=\sum<em>i a</em>i x_i. \]</p><p>For a batch of sequences, the expected tensor shapes are:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;5bf7d72f-87cb-4780-80d4-254512018b13&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">sequence:  (batch, time, feature_dim)
scores:    (batch, time)
weights:   (batch, time)
context:   (batch, feature_dim)
forecast:  (batch, horizon)</code></pre></div><p>The implementation uses <code>DotProductTemporalAttention</code> for these operations:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;2d4d5820-c3d9-4420-bbab-659e98c05343&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># src/quantitative_trading_model/models/attention.py

class DotProductTemporalAttention(nn.Module):
    def forward(
        self,
        sequence: Tensor,
        query: Optional[Tensor] = None,
        *,
        return_weights: bool = False,
    ) -&gt; Tensor | Tuple[Tensor, Tensor]:
        self._validate_sequence(sequence)
        prepared_query = self._prepare_query(sequence, query)

        # Equation (1): one dot product per time step.
        scores = torch.einsum("btd,bd-&gt;bt", sequence, prepared_query)

        # Equation (2): normalize across time, not features or batches.
        weights = torch.softmax(scores, dim=1)

        # Equation (3): weighted sum of sequence representations.
        context = torch.sum(weights.unsqueeze(-1) * sequence, dim=1)

        self._last_attention_weights = weights
        if return_weights:
            return context, weights
        return context</code></pre></div><p>The Einstein summation expression <code>"btd,bd-&gt;bt"</code> means:</p><ul><li><p><code>b</code>: batch item;</p></li><li><p><code>t</code>: time step;</p></li><li><p><code>d</code>: feature dimension;</p></li><li><p>the feature dimension is multiplied and summed away;</p></li><li><p>the result retains batch and time, producing one score per time step.</p></li></ul><p>The next line, <code>torch.softmax(scores, dim=1)</code>, is the key implementation detail. For a tensor shaped <code>(batch, time)</code>, dimension <code>1</code> is time. The result has one probability-like weight for every historical position.</p><h3>8.3 The unspecified query vector</h3><p>The paper uses a query vector <code>q</code> in its equations but does not explain where that vector comes from. This missing detail affects the architecture, so the implementation makes the choice explicit rather than hiding it.</p><p><code>DotProductTemporalAttention</code> supports two modes:</p><ul><li><p>a <strong>learned query</strong>, stored as a trainable parameter and used by default;</p></li><li><p>a <strong>caller-provided query</strong>, useful for experiments with another interpretation.</p></li></ul><p>The learned-query choice is visible in the constructor:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;9485cce2-5aa1-4bf1-879c-1f72f1adc238&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># src/quantitative_trading_model/models/attention.py

if learned_query:
    query = torch.empty(self.feature_dim)
    nn.init.normal_(query, mean=0.0, std=float(query_init_scale))
    self.query = nn.Parameter(query)
else:
    self.register_parameter("query", None)</code></pre></div><p>When no explicit query is supplied, <code>_prepare_query</code> retrieves the learned vector and broadcasts it across the batch. When a query is supplied, the method checks its dimensionality, device, data type, and finiteness before using it.</p><p>This is a sound interface decision for an educational implementation, but it must be labeled correctly:</p><p>The learned query is a reconstruction of an unspecified paper detail. It is not evidence that the original authors used a learned query rather than a final hidden state, a separate projection, or another query-generation mechanism.</p><h3>8.4 From sequence states to a forecast</h3><p>The primary model in <code>forecasters.py</code> first creates a bidirectional LSTM whose <code>batch_first=True</code> input convention is <code>(batch, time, features)</code>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;f38a226f-ea72-4da6-b387-60ceec27483f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># src/quantitative_trading_model/models/forecasters.py

self.encoder = nn.LSTM(
    input_size=input_size,
    hidden_size=hidden_size,
    num_layers=num_layers,
    batch_first=True,
    dropout=recurrent_dropout,
    bidirectional=True,
)

self.dropout = nn.Dropout(dropout)
self.attention = DotProductTemporalAttention(self.output_size)
self.head = nn.Linear(self.output_size, horizon)</code></pre></div><p><code>self.output_size</code> is <code>hidden_size * 2</code>, because the forward and backward states are concatenated. The dense <code>head</code> converts the attention context into the requested forecast horizon. For example:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;32654e47-7f1f-446c-8dc1-43eaf3a93e40&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">horizon = 1  -&gt; output shape (batch, 1)
horizon = 3  -&gt; output shape (batch, 3)</code></pre></div><p>The forward pass is intentionally short because each responsibility is delegated to a clear layer:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;c6a9d71b-2850-4844-add3-a28ad819e5ea&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">class AttBiLSTMForecaster(nn.Module):
    def forward(self, inputs: Tensor) -&gt; Tensor:
        _validate_input_tensor(inputs)
        sequence, _ = self.encoder(inputs)
        sequence = self.dropout(sequence)

        context = self.attention(sequence)
        return self.head(self.dropout(context))</code></pre></div><p>Conceptually, this is:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;ebe26cd4-9fbe-47d9-99c1-e0c69ebe756e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">historical window
    -&gt; bidirectional LSTM sequence states
    -&gt; dropout on sequence states
    -&gt; temporal dot-product attention
    -&gt; context vector
    -&gt; dropout on context
    -&gt; linear forecast head</code></pre></div><p>The exact generated implementation also retains the latest attention weights when the attention layer exposes them. That is useful for inspection and debugging, but attention weights should not automatically be interpreted as a complete causal explanation of the prediction.</p><h3>8.5 Dropout, MAE, and RMSprop</h3><p>The paper reports a dropout rate of 20%, represented by the default <code>dropout=0.20</code>. Dropout randomly suppresses a fraction of activations during training, which can reduce reliance on a narrow set of internal features. The paper's wording does not establish whether its 20% applies to ordinary feed-forward dropout, recurrent dropout, or both, so the generated code uses explicit <code>nn.Dropout</code> layers and documents the recurrent-layer behavior.</p><p>The training function uses MAE loss:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;6f75ba1a-a232-4edf-ae7a-92dd1c5b7ede&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">criterion = nn.L1Loss()</code></pre></div><p>This corresponds to:</p><p>\[ \operatorname{MAE} = \frac{1}{N}\sum<em>k |y</em>k-\hat y_k|. \]</p><p>The optimizer is RMSprop:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;57503a42-d898-4b8e-8328-88b1d51a50e8&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">optimizer = torch.optim.RMSprop(
    model.parameters(),
    lr=learning_rate,
)</code></pre></div><p>This corrects an important terminology issue in the paper interpretation. <strong>RMSprop is an optimizer, not an activation function.</strong> It controls how model parameters are updated after gradients are calculated. The activation behavior of the recurrent cells and the linear output head is a separate architectural matter.</p><p>The implementation keeps learning rate, hidden size, number of layers, batch size, epochs, and patience configurable because the paper does not provide enough information to hard-code them as original values.</p><h3>8.6 Why the model trains on chronological batches</h3><p><code>train_forecaster</code> constructs a <code>DataLoader</code> with <code>shuffle=False</code>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;d5fe2069-eb3c-4168-bb39-8aee043257b8&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">train_loader = DataLoader(
    TensorDataset(
        torch.from_numpy(x_train),
        torch.from_numpy(y_train),
    ),
    batch_size=batch_size,
    shuffle=False,
)</code></pre></div><p>Adjacent sliding windows are highly dependent because they share most of their observations. Turning off shuffling preserves the temporal order and makes the training protocol easier to audit. This does not remove all dependence between windows, but it avoids adding a separate randomization step that could make the time-series experiment harder to interpret.</p><p>This is a conservative engineering choice rather than a claim that the paper used the same batching policy. The paper mentions shuffled or randomly arranged training behavior in places, but does not provide enough detail to establish a reproducible protocol. For trading conclusions, chronological and walk-forward evaluation remain more important than reproducing an unclear shuffling choice.</p><h3>8.7 Model-shape checks</h3><p>The generated tests focus on architecture contracts rather than empirical performance. For example, <code>tests/test_model_shapes.py</code> checks that a configured attention layer returns the expected context shape and that its weights normalize across time:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;ba9492b8-8f98-45ec-84c0-8c3f4db8d7f2&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">@pytest.mark.skipif(not TORCH_AVAILABLE, reason="PyTorch is an optional dependency")
class TestAttentionShapes:
    def test_attention_weights_are_normalized_across_time(self) -&gt; None:
        attention = _construct_attention()
        sequence = torch.randn(3, 7, 8)
        output = attention(sequence)
        _, weights = _unpack_attention_output(output)

        if weights is None:
            weights = getattr(attention, "last_attention_weights", None)

        assert tuple(weights.shape) == (3, 7)
        assert torch.isfinite(weights).all()
        assert torch.all(weights &gt;= 0)
        assert torch.allclose(
            weights.sum(dim=1),
            torch.ones(3),
            atol=1e-5,
        )</code></pre></div><p>The test expresses the mathematical invariant directly:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;6b5e5919-28d3-42cb-bbff-52ac8ba63886&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">weights.shape == (batch, time)
weights &gt;= 0
sum(weights over time) == 1</code></pre></div><p>Additional shape tests check one-step and three-step outputs:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;b41c9ec4-72af-4365-824f-f5e2c7cc5f63&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">@pytest.mark.parametrize("horizon", [1, 3])
def test_attention_bilstm_output_horizon(self, horizon: int) -&gt; None:
    model = _construct_forecaster("AttBiLSTMForecaster", horizon=horizon)
    inputs = torch.randn(2, 100, 3)
    predictions = _model_output(model, inputs)

    assert predictions.shape[0] == 2
    assert predictions.shape[-1] == horizon</code></pre></div><p>These checks do not demonstrate that the model matches the paper's forecast metrics. They only verify the intended tensor interface and attention normalization when the optional dependency is installed. The local verification record should therefore be read as static and structural evidence, not as a claim that the model was executed successfully or that the paper's numerical results were reproduced.</p><h3>8.8 What is direct and what is reconstructed here?</h3><p>The implementation status of this model can be summarized as follows:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!H9Oq!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F79a4afda-86f8-4b46-9233-5d0855b5eaca_1408x976.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!H9Oq!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F79a4afda-86f8-4b46-9233-5d0855b5eaca_1408x976.png 424w, https://substackcdn.com/image/fetch/$s_!H9Oq!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F79a4afda-86f8-4b46-9233-5d0855b5eaca_1408x976.png 848w, https://substackcdn.com/image/fetch/$s_!H9Oq!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F79a4afda-86f8-4b46-9233-5d0855b5eaca_1408x976.png 1272w, https://substackcdn.com/image/fetch/$s_!H9Oq!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F79a4afda-86f8-4b46-9233-5d0855b5eaca_1408x976.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!H9Oq!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F79a4afda-86f8-4b46-9233-5d0855b5eaca_1408x976.png" width="1408" height="976" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/79a4afda-86f8-4b46-9233-5d0855b5eaca_1408x976.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:976,&quot;width&quot;:1408,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:197770,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/206399283?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F79a4afda-86f8-4b46-9233-5d0855b5eaca_1408x976.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!H9Oq!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F79a4afda-86f8-4b46-9233-5d0855b5eaca_1408x976.png 424w, https://substackcdn.com/image/fetch/$s_!H9Oq!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F79a4afda-86f8-4b46-9233-5d0855b5eaca_1408x976.png 848w, https://substackcdn.com/image/fetch/$s_!H9Oq!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F79a4afda-86f8-4b46-9233-5d0855b5eaca_1408x976.png 1272w, https://substackcdn.com/image/fetch/$s_!H9Oq!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F79a4afda-86f8-4b46-9233-5d0855b5eaca_1408x976.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The model is therefore a useful translation of the paper's stated architecture, but not a verified reproduction of its original neural network. Exact comparison would require the original data, preprocessing, target definition, full architecture, training hyperparameters, and untouched evaluation protocol.</p><h2>9. Walk Through Attention, Training, and Prediction Functions</h2><p>This section follows the generated Python implementation from tensor validation through attention, recurrent encoding, training, and prediction. The primary model is an attention-enhanced bidirectional LSTM, or Att-BiLSTM. The paper specifies the broad architecture, 20% dropout, MAE loss, and RMSprop optimization, but it does not specify several architectural and training details. Those details remain explicit reconstruction choices.</p><p>The examples below describe the generated interfaces and intended invariants. They do not claim that the code was executed, that the tests passed, or that the paper's empirical results were reproduced.</p><h3>9.1 Optional PyTorch imports</h3><p>PyTorch is an optional dependency. The data, streak, sizing, risk, and portfolio components are intended to remain usable without installing the deep-learning stack. The model modules therefore import PyTorch conditionally:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;a0c771a3-c249-4014-a428-a71b3bafc1cd&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">try:
    import torch
    from torch import Tensor, nn
except ImportError:
    torch = None
    Tensor = Any
    nn = type("nn", (), {"Module": _ModuleFallback})</code></pre></div><p>The fallback permits the module to be imported in a minimal installation. Constructing <code>DotProductTemporalAttention</code> or a recurrent forecaster still requires PyTorch and raises a clear <code>ImportError</code> if it is unavailable. This is preferable to silently substituting a different model.</p><p>The package initializer is also side-effect free. Importing <code>quantitative_trading_model</code> does not load market data, train a model, access a network, or create a trading connection.</p><h3>9.2 The attention layer's tensor contract</h3><p><code>DotProductTemporalAttention</code> expects a sequence tensor with shape:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;9cb4751e-cd89-4dea-a05b-0cf373d88b72&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">(batch, time, feature_dim)</code></pre></div><p>For the paper-inspired data setup, <code>time</code> is normally 100. The feature dimension is the size of each recurrent representation. A bidirectional LSTM commonly produces a representation whose width is twice its hidden size because its forward and backward states are concatenated.</p><p>The attention layer returns a context tensor with shape:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;3ecbf9f9-8875-484e-81fb-bfec024c77cd&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">(batch, feature_dim)</code></pre></div><p>Attention weights have shape:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;f94ccadf-6c4a-4149-a7a9-fb4c9219f081&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">(batch, time)</code></pre></div><p>They can be returned explicitly or inspected after a forward pass. The weights are useful for diagnostics and shape tests, but they should not automatically be treated as a complete causal explanation of the model.</p><h3>9.3 Constructing attention and representing the missing query</h3><p>The paper gives the score equation</p><p>\[ S(x<em>i,q)=x</em>i^Tq, \]</p><p>but does not explain how the query vector <code>q</code> is created. The constructor exposes this ambiguity:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;5ba9f8fd-31f5-4753-a142-cb17710c8266&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">class DotProductTemporalAttention(nn.Module):
    def __init__(
        self,
        feature_dim: Optional[int] = None,
        *,
        input_dim: Optional[int] = None,
        learned_query: bool = True,
        query_init_scale: float = 0.02,
    ) -&gt; None:
        ...</code></pre></div><p><code>feature_dim</code> and <code>input_dim</code> are aliases. If both are supplied, they must agree. The dimension must be a positive integer because the unprojected dot product requires the query and sequence representations to have the same width.</p><p>With <code>learned_query=True</code>, the layer creates a trainable query parameter:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;2754d81f-be66-458e-a4a3-bd2976eb20a7&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">if learned_query:
    query = torch.empty(self.feature_dim)
    nn.init.normal_(query, mean=0.0, std=float(query_init_scale))
    self.query = nn.Parameter(query)
else:
    self.register_parameter("query", None)</code></pre></div><p>The learned query is a reconstruction choice, not a verified detail from the paper. With <code>learned_query=False</code>, the caller must provide a query to every forward call.</p><h3>9.4 Validating the sequence tensor</h3><p>Before computing attention scores, <code>_validate_sequence</code> checks the input shape and values:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;f6461671-44da-4552-bafc-0842cd4911c4&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def _validate_sequence(self, sequence: Tensor) -&gt; Tuple[int, int, int]:
    if sequence.ndim != 3:
        raise ValueError("sequence must have shape (batch, time, feature_dim)")

    batch, time, feature = (int(value) for value in sequence.shape)
    if batch &lt;= 0 or time &lt;= 0:
        raise ValueError("batch and time dimensions must be positive")
    if feature != self.feature_dim:
        raise ValueError("sequence feature dimension does not match configuration")
    if not sequence.is_floating_point() and not sequence.is_complex():
        raise TypeError("sequence must use a floating-point or complex dtype")
    if not torch.isfinite(sequence).all():
        raise ValueError("sequence contains NaN or infinite values")
    return batch, time, feature</code></pre></div><p>The checks protect separate assumptions:</p><ul><li><p><strong>Rank:</strong> the layer needs batch, time, and feature axes.</p></li><li><p><strong>Nonempty dimensions:</strong> an empty sequence cannot produce a context vector.</p></li><li><p><strong>Feature agreement:</strong> the dot product requires matching dimensions.</p></li><li><p><strong>Numeric type:</strong> the implementation technically accepts floating-point and complex PyTorch tensors.</p></li><li><p><strong>Finite values:</strong> NaNs and infinities could propagate through scores and softmax.</p></li></ul><p>In normal financial-model usage, inputs are real-valued floating-point tensors. Complex-tensor acceptance is a property of the generated validation condition, not a paper requirement or a recommended financial-data representation.</p><p>These checks do not verify temporal causality. A tensor can have the correct shape while containing future observations. Timestamp alignment and target-after-window rules remain responsibilities of the data and experiment layers.</p><h3>9.5 Preparing and broadcasting the query</h3><p>The query may be a single vector shared across the batch or a separate vector for every batch item:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;900eeb26-07ce-4c3c-a0e3-92670b54ba08&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def _prepare_query(self, sequence: Tensor, query: Optional[Tensor]) -&gt; Tensor:
    batch = sequence.shape[0]
    candidate = self.query if query is None else query

    if candidate is None:
        raise ValueError("query is required when learned_query=False")

    if candidate.ndim == 1:
        if candidate.shape[0] != self.feature_dim:
            raise ValueError("query dimension does not match feature_dim")
        candidate = candidate.unsqueeze(0).expand(batch, -1)
    elif candidate.ndim == 2:
        if candidate.shape != (batch, self.feature_dim):
            raise ValueError("batched query has the wrong shape")
    else:
        raise ValueError("query must have shape (feature_dim,) or (batch, feature_dim)")

    if candidate.device != sequence.device:
        raise ValueError("sequence and query must be on the same device")
    if candidate.dtype != sequence.dtype:
        raise ValueError("sequence and query must use the same dtype")
    if not torch.isfinite(candidate).all():
        raise ValueError("query contains NaN or infinite values")
    return candidate</code></pre></div><p>A one-dimensional query is expanded across the batch. A two-dimensional query must have one row per batch item. Device and dtype checks prevent errors such as combining tensors on different devices or with incompatible precision.</p><p>The method does not project either operand into another space. That follows the displayed paper equation, although the paper does not establish that its actual implementation avoided projections.</p><h3>9.6 Forward attention: scores, weights, and context</h3><p>The core <code>forward</code> method is:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;04a88858-2ef0-46e9-aec8-00faec8e8289&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def forward(
    self,
    sequence: Tensor,
    query: Optional[Tensor] = None,
    *,
    return_weights: bool = False,
) -&gt; Tensor | Tuple[Tensor, Tensor]:
    self._validate_sequence(sequence)
    prepared_query = self._prepare_query(sequence, query)

    scores = torch.einsum("btd,bd-&gt;bt", sequence, prepared_query)
    weights = torch.softmax(scores, dim=1)
    context = torch.sum(weights.unsqueeze(-1) * sequence, dim=1)

    self._last_attention_weights = weights
    if return_weights:
        return context, weights
    return context</code></pre></div><p>The three central operations correspond to the paper's equations.</p><h4>Dot-product scores</h4><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;897b8c8c-8679-43f3-aef4-57d4af30bf19&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">scores = torch.einsum("btd,bd-&gt;bt", sequence, prepared_query)</code></pre></div><p>Here <code>b</code> is the batch index, <code>t</code> is the time index, and <code>d</code> is the feature index. The result has shape <code>(batch, time)</code>, with one scalar score for each historical time step.</p><h4>Softmax across time</h4><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;a94cca76-9cfe-46d2-8bc8-4bc322c1f89a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">weights = torch.softmax(scores, dim=1)</code></pre></div><p><code>dim=1</code> is essential because the time axis is the second dimension. For each batch item:</p><p>\[ \sum<em>i a</em>i=1, \qquad a_i\geq 0. \]</p><p>Applying softmax across features would produce a different operation and would not assign relative weights to historical time steps.</p><h4>Weighted context</h4><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;58c1e6fa-fe20-4918-a364-4d0248ef3a76&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">context = torch.sum(weights.unsqueeze(-1) * sequence, dim=1)</code></pre></div><p><code>weights.unsqueeze(-1)</code> changes the weight shape from <code>(batch, time)</code> to <code>(batch, time, 1)</code>. Each representation is multiplied by its scalar attention weight, and summing over time produces one context vector per batch item.</p><p>The generated implementation stores the latest weights and checks that both weights and context are finite. Callers that retain weights across many batches should detach them first if they do not need gradients, otherwise the retained tensors may keep autograd graphs alive.</p><h3>9.7 Retrieving attention weights correctly</h3><p>The default call returns only the context:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;053ff704-4398-497f-b12f-a6d8d897ff51&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">context = attention(sequence)</code></pre></div><p>To receive both context and weights in one call, pass <code>return_weights=True</code>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;088130cc-b841-461b-a6f0-c38f44d3e09a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">context, weights = attention(sequence, return_weights=True)
assert context.shape == (sequence.shape[0], sequence.shape[2])
assert weights.shape == (sequence.shape[0], sequence.shape[1])</code></pre></div><p>Alternatively, the caller can use the inspection method:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;10c04f10-6126-440e-b67d-2cfcdced8611&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">context = attention(sequence)
weights = attention.last_attention_weights</code></pre></div><p>or recompute them explicitly:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;8af9e4c8-2941-4d10-81ff-62c77423516b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">weights = attention.attention_weights(sequence)</code></pre></div><p>This distinction matters because <code>attention(sequence)</code> does <strong>not</strong> return a tuple by default. The generated model uses the default context-only call and reads the most recent weights from the attention layer when they are available.</p><h3>9.8 The shared recurrent baseline</h3><p><code>forecasters.py</code> defines <code>_RecurrentForecaster</code>, which supplies common behavior for LSTM, GRU, and non-attention BiLSTM baselines. The recurrent class is selected from the requested kind:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;8f93fe83-66c7-468d-8aa9-6961b865024b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">recurrent_class = nn.LSTM if recurrent_kind == "lstm" else nn.GRU
self.encoder = recurrent_class(
    input_size=input_size,
    hidden_size=hidden_size,
    num_layers=num_layers,
    batch_first=True,
    dropout=recurrent_dropout,
    bidirectional=bidirectional,
)
self.dropout = nn.Dropout(dropout)
self.head = nn.Linear(self.output_size, horizon)</code></pre></div><p>The representation width is:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;d5d9c587-6844-43b5-8731-43efc05ac388&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">self.output_size = hidden_size * (2 if bidirectional else 1)</code></pre></div><p>The factor of two accounts for the forward and backward hidden streams in a bidirectional layer. The linear head maps the representation to the configured forecast horizon.</p><p>PyTorch recurrent dropout is applied between recurrent layers. Therefore the generated code uses:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;88704a97-4d4b-481f-b87f-b376b36824b1&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">recurrent_dropout = dropout if num_layers &gt; 1 else 0.0</code></pre></div><p>It also includes a separate <code>nn.Dropout(dropout)</code> stage. Whether the paper used recurrent dropout, feed-forward dropout, or both is unspecified, so this remains a reconstruction choice.</p><h3>9.9 Encoding and baseline prediction</h3><p>For recurrent baselines, the encoder returns the final time-step representation:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;a8a79a16-704e-4b8b-90b4-73fbac1aa5f2&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def encode(self, inputs: Tensor) -&gt; Tensor:
    _validate_input_tensor(inputs)
    sequence, _ = self.encoder(inputs)
    return self.dropout(sequence[:, -1, :])

def forward(self, inputs: Tensor) -&gt; Tensor:
    return self.head(self.encode(inputs))</code></pre></div><p>The input contract is <code>(batch, time, features)</code>. The data layer normally supplies <code>time=100</code>, but the model does not hard-code that number. The configured window builder determines the time length.</p><h3>9.10 Constructing the Att-BiLSTM</h3><p>The primary model returns a representation for every time step so attention can operate over the full historical window:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;a9452f4d-34e2-4874-a787-c248c1fe3cf9&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">self.encoder = nn.LSTM(
    input_size=input_size,
    hidden_size=hidden_size,
    num_layers=num_layers,
    batch_first=True,
    dropout=recurrent_dropout,
    bidirectional=True,
)
self.dropout = nn.Dropout(dropout)
self.attention = DotProductTemporalAttention(self.output_size)
self.head = nn.Linear(self.output_size, horizon)</code></pre></div><p>The tensor flow is:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;df40d205-4e31-460a-8aac-ce5feae9cfbc&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">inputs                 (batch, time, input_size)
BiLSTM sequence        (batch, time, 2 * hidden_size)
dropout sequence       (batch, time, 2 * hidden_size)
attention context      (batch, 2 * hidden_size)
linear output head     (batch, horizon)</code></pre></div><p>The paper does not specify hidden size, recurrent depth, query construction, dense-layer structure, or output representation. These values must therefore be recorded as configuration metadata rather than presented as recovered paper details.</p><h3>9.11 Connecting the Att-BiLSTM to attention</h3><p>The primary model's forward path is:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;da4c86aa-fddf-4608-90d8-601a0e12c131&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def forward(self, inputs: Tensor) -&gt; Tensor:
    _validate_input_tensor(inputs)
    sequence, _ = self.encoder(inputs)
    sequence = self.dropout(sequence)

    attended = self.attention(sequence)
    if isinstance(attended, tuple):
        context, weights = attended
        self._last_attention_weights = weights
    else:
        context = attended
        weights = getattr(self.attention, "last_attention_weights", None)
        if isinstance(weights, Tensor):
            self._last_attention_weights = weights

    return self.head(self.dropout(context))</code></pre></div><p>The intended sequence is:</p><p>Validate the input rank and values.</p><p>Process the historical window with the bidirectional LSTM.</p><p>Apply dropout to the sequence of hidden representations.</p><p>Use temporal attention to form one context vector.</p><p>Apply dropout to that context.</p><p>Map the context to one or more forecast values.</p><p>The tuple branch is defensive compatibility logic. The generated attention layer returns only the context unless <code>return_weights=True</code>, but the model can also accommodate an attention implementation that returns <code>(context, weights)</code>.</p><h3>9.12 Prediction methods and evaluation mode</h3><p>The model classes provide a prediction method that disables training behavior and gradient tracking:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;20c98dbb-a375-4588-abf2-5bc296e75db8&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def predict(self, inputs: np.ndarray | Tensor) -&gt; np.ndarray:
    self.eval()
    with torch.no_grad():
        tensor = _as_input_tensor(inputs)
        return self(tensor).detach().cpu().numpy()</code></pre></div><p>The three operations have distinct purposes:</p><ul><li><p><code>self.eval()</code> disables training-specific behavior such as dropout.</p></li><li><p><code>torch.no_grad()</code> avoids constructing gradient graphs during inference.</p></li><li><p><code>.detach().cpu().numpy()</code> produces a NumPy result for metrics and signal generation.</p></li></ul><p><code>predict_forecaster</code> additionally extracts <code>X</code> from a windowed dataset and can apply an inverse transformation. The scaler must have been fitted using training data only. The model does not determine whether its target is a raw price, scaled price, return, or percentage change; that convention belongs to the configuration and experiment protocol.</p><h3>9.13 Model construction with <code>build_forecaster</code></h3><p>The factory normalizes common names and maps them to model classes:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;f1aafa04-b8b5-4172-a1a1-66e27466ba47&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">aliases = {
    "att-bilstm": AttBiLSTMForecaster,
    "attbilstm": AttBiLSTMForecaster,
    "lstm": LSTMForecaster,
    "gru": GRUForecaster,
    "bilstm": BiLSTMForecaster,
}</code></pre></div><p>Unspecified settings are read from configuration:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;2b212502-7280-44b6-9ae9-c9edd3343120&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">kwargs = {
    "hidden_size": hidden_size if hidden_size is not None else _config_value(config, "hidden_size", 64),
    "num_layers": num_layers if num_layers is not None else _config_value(config, "num_layers", 1),
    "dropout": dropout if dropout is not None else _config_value(config, "dropout", 0.20),
    "horizon": horizon if horizon is not None else _config_value(config, "horizon", 1),
}</code></pre></div><p>The 20% dropout default is paper-inspired. The default hidden size and layer count are engineering defaults, not findings recovered from the paper.</p><h3>9.14 Deterministic seeding</h3><p><code>train_forecaster</code> calls <code>_set_seed</code> before training:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;c92ffd50-f64d-455c-a0d2-39fa2847d6ec&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def _set_seed(seed: int) -&gt; None:
    random.seed(seed)
    np.random.seed(seed)
    torch.manual_seed(seed)
    if torch.cuda.is_available():
        torch.cuda.manual_seed_all(seed)</code></pre></div><p>This aligns the main Python, NumPy, and PyTorch random generators. A seed improves repeatability but does not guarantee bit-for-bit equality across hardware, PyTorch versions, CUDA kernels, or data pipelines. It should be recorded as experiment metadata, not treated as proof of reproducibility.</p><h3>9.15 Extracting training data and normalizing target shape</h3><p>The <code>_extract_xy</code> helper accepts a <code>WindowedDataset</code>-like object exposing <code>X</code> and <code>y</code>, or a two-item <code>(X, y)</code> pair:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;5c0f125f-ee82-4a9e-b996-223f64c01fda&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def _extract_xy(data: Any) -&gt; tuple[np.ndarray, np.ndarray]:
    if hasattr(data, "X") and hasattr(data, "y"):
        x, y = data.X, data.y
    elif isinstance(data, (tuple, list)) and len(data) == 2:
        x, y = data
    else:
        raise TypeError("data must expose X and y or be a two-item (X, y) pair")

    x_array = np.asarray(x, dtype=np.float32)
    y_array = np.asarray(y, dtype=np.float32)
    if x_array.ndim != 3:
        raise ValueError("X must have shape (samples, time, features)")
    if y_array.ndim == 1:
        y_array = y_array[:, None]
    if y_array.ndim != 2:
        raise ValueError("y must have shape (samples, horizon)")
    if len(x_array) != len(y_array):
        raise ValueError("X and y must contain the same number of samples")
    return x_array, y_array</code></pre></div><p>Converting a one-step target from <code>(samples,)</code> to <code>(samples, 1)</code> gives both one-step and three-step models the same output contract. The helper also checks for empty datasets and non-finite values. It does not accept a final test set implicitly; callers must pass training and validation partitions explicitly.</p><h3>9.16 Training with MAE and RMSprop</h3><p><code>train_forecaster</code> first checks that the target horizon agrees with the configuration and model:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;21e610da-6fa1-426b-a61c-2fc267c22446&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">horizon = int(_config_value(config, "horizon", y_train.shape[1]))
if y_train.shape[1] != horizon:
    raise ValueError("training target horizon does not match configured horizon")
if getattr(model, "horizon", horizon) != horizon:
    raise ValueError("model horizon and target horizon do not match")</code></pre></div><p>It then creates a chronological data loader:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;bf5212a0-60ec-4e48-a1f2-9945cd299ae2&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">train_loader = DataLoader(
    TensorDataset(torch.from_numpy(x_train), torch.from_numpy(y_train)),
    batch_size=batch_size,
    shuffle=False,
)</code></pre></div><p><code>shuffle=False</code> is a deliberate time-series choice. Adjacent stride-one windows share most of their observations. Preserving order does not remove dependence, but it avoids adding random mixing and keeps the temporal protocol visible.</p><p>The paper-inspired loss and optimizer are configured as follows:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;7271b12a-91c8-4c0d-80ec-094ff5147368&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">criterion = nn.L1Loss()
optimizer = torch.optim.RMSprop(model.parameters(), lr=learning_rate)</code></pre></div><p><code>nn.L1Loss()</code> implements mean absolute error. RMSprop updates model parameters; it is not an activation function. The learning rate and other optimizer details remain configurable because the paper does not provide them.</p><p>The core update loop is:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;84cd3239-c6e7-4402-b191-756c4d5aa019&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">for batch_x, batch_y in train_loader:
    batch_x = batch_x.to(selected_device)
    batch_y = batch_y.to(selected_device)
    optimizer.zero_grad(set_to_none=True)
    predictions = model(batch_x)
    loss = criterion(predictions, batch_y)
    loss.backward()
    optimizer.step()</code></pre></div><p>The sequence is standard gradient-based training: move the batch, clear gradients, compute predictions, calculate MAE, backpropagate, and update with RMSprop. The generated implementation also checks that prediction and target shapes match before the update, preventing accidental broadcasting between one-step and multi-step targets.</p><h3>9.17 Validation loss, early stopping, and best-state restoration</h3><p>A validation dataset is optional. When supplied, it is evaluated without gradient calculation:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;334658b7-023e-4d98-9f7c-08977ee73153&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">model.eval()
with torch.no_grad():
    validation_prediction = model(validation_tensors[0])
    validation_loss = criterion(validation_prediction, validation_tensors[1])</code></pre></div><p>The final test partition should not be passed as validation data. Validation can guide early stopping, while the untouched test period remains reserved for final evaluation.</p><p>The function saves a copy of the best model state:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;c7f79010-962d-4cb5-a01b-975304fc8df9&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">if monitored_loss &lt; best_loss - 1e-12:
    best_loss = monitored_loss
    history.best_epoch = epoch
    best_state = copy.deepcopy(model.state_dict())
    stale_epochs = 0
else:
    stale_epochs += 1
    if validation_tensors is not None and stale_epochs &gt; patience:
        history.stopped_early = True
        break</code></pre></div><p>After training, the best state is restored:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;99e5307e-9b9a-4024-b96b-ee0864030840&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">if best_state is not None:
    model.load_state_dict(best_state)</code></pre></div><p>This prevents the returned model from being an arbitrary final epoch when an earlier epoch had lower monitored loss. <code>TrainingHistory</code> records training loss, validation loss, the best epoch, and whether early stopping occurred.</p><p>When no validation set is supplied, training loss is monitored and validation-based early stopping is not used. This is a weaker selection protocol, so a research experiment should normally provide a separate validation period or use a documented walk-forward retraining procedure.</p><h3>9.18 Shape tests and the verification boundary</h3><p>The model-shape tests are intended to verify tensor contracts rather than financial performance. An API-accurate attention test is:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;85edce4a-7634-48ee-bf3b-3b1051e81809&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">sequence = torch.randn(3, 7, 8)
context, weights = attention(sequence, return_weights=True)

assert tuple(context.shape) == (3, 8)
assert tuple(weights.shape) == (3, 7)
assert torch.all(weights &gt;= 0)
assert torch.allclose(weights.sum(dim=1), torch.ones(3), atol=1e-5)</code></pre></div><p>The equivalent context-only call is:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;728483a7-e76b-4d70-aa14-06c8db6e6a28&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">context = attention(sequence)
weights = attention.last_attention_weights</code></pre></div><p>Recurrent tests use the paper-inspired time length and check one-step and three-step output contracts:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;948d45ec-6853-4658-b2e9-bfb2e291396e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">inputs = torch.randn(2, 100, 3)
predictions = model(inputs)

assert predictions.shape[0] == 2
assert predictions.shape[-1] == horizon</code></pre></div><p>These checks can detect wrong attention axes, shape regressions, missing dropout, and horizon mismatches. They cannot establish that the model learned useful financial structure, matched the paper's metrics, or produced a profitable strategy.</p><p>The available verification record must also be interpreted cautiously. Semantic code verification was skipped. Static checks reported findings involving placeholder detection in <code>experiments.py</code> and <code>cli.py</code>, and Markdown-fence checks involving the generated <code>README.md</code> and <code>docs/tutorial.md</code> code fields. In addition, some generated tests and orchestration calls have API inconsistencies with the generated modules. Consequently, this section documents intended interfaces and invariants; it does not claim a validated runtime contract, successful test execution, or empirical correctness.</p><h3>9.19 Practical model-use sequence</h3><p>A complete forecasting workflow should follow this order:</p><p>Clean and standardize local prices.</p><p>Establish a chronological training boundary.</p><p>Fit scaling parameters on training observations only.</p><p>Build 100-observation windows and future targets.</p><p>Split training and validation data without using final test targets.</p><p>Build <code>AttBiLSTMForecaster</code> with an explicit configuration.</p><p>Train with MAE and RMSprop.</p><p>Restore the best validation state through the training result.</p><p>Predict on untouched test windows.</p><p>Inverse-transform predictions if the target was scaled.</p><p>Compute forecast metrics separately from any portfolio backtest.</p><p>Pass only timestamp-aligned forecasts to the signal layer.</p><p>The model produces forecasts. It does not choose an order, determine a position size, apply VaR, or execute a trade. Those responsibilities belong to later modules.</p><h3>9.20 Reconstruction boundary</h3><p>The generated functions implement mechanics that can be stated precisely: recurrent sequence processing, dot-product attention, temporal softmax normalization, weighted context formation, dropout, MAE, RMSprop, and tensor-shape validation.</p><p>The following remain assumptions rather than verified paper details:</p><ul><li><p>hidden size and recurrent depth;</p></li><li><p>learned-query construction;</p></li><li><p>input feature set and target representation;</p></li><li><p>learning rate, batch size, epoch count, and stopping policy;</p></li><li><p>exact scaling procedure;</p></li><li><p>one-day versus three-day operational target;</p></li><li><p>hardware, random seed, and software-version details.</p></li></ul><p>Keeping these boundaries visible is part of the implementation. It prevents a clean model class from being mistaken for an exact reproduction of an incompletely specified experiment.</p><h2>10. Compare Recurrent, Statistical, HMM, and Boosted-Tree Baselines Carefully</h2><p>The paper compares its attention-enhanced BiLSTM with several other forecasting families: LSTM, GRU, BiLSTM, Holt-Winters, ARIMA, HMM, and XGBoost. This comparison is useful because it asks whether the attention-enhanced recurrent model adds value beyond simpler neural, statistical, state-based, and tree-based approaches.</p><p>However, a benchmark table is only meaningful when the models receive comparable data and are evaluated under the same target definition and timestamps. The paper does not provide enough configuration detail to reproduce its table exactly. The generated project therefore implements benchmark <strong>interfaces and adapters</strong>, while labeling locally computed metrics separately from the paper's reported claims.</p><p><strong>Reproduction boundary:</strong> The code below shows how the benchmark layer is intended to work. It does not establish that the generated code was executed, that every optional dependency is installed, or that the paper's numerical metrics were reproduced.</p><h3>10.1 A common result contract</h3><p>Different forecasting libraries return different model objects and prediction types. A benchmark experiment needs a common record so that results can be compared without losing important status information. The generated <code>BenchmarkResult</code> class in <code>src/quantitative_trading_model/models/benchmarks.py</code> provides that record:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;f21a3649-9a18-4854-ae8f-eab6b988d478&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">@dataclass
class BenchmarkResult:
    model_name: str
    predictions: np.ndarray | None = None
    metrics: dict[str, float] = field(default_factory=dict)
    status: str = "ok"
    message: str = ""
    metadata: dict[str, Any] = field(default_factory=dict)

    def to_dict(self, include_predictions: bool = False) -&gt; dict[str, Any]:
        output = {
            "model_name": self.model_name,
            "metrics": dict(self.metrics),
            "status": self.status,
            "message": self.message,
            "metadata": dict(self.metadata),
        }
        if include_predictions:
            output["predictions"] = (
                None if self.predictions is None
                else self.predictions.tolist()
            )
        return output</code></pre></div><p>The fields have distinct purposes:</p><ul><li><p><code>model_name</code> identifies the adapter or experiment label.</p></li><li><p><code>predictions</code> contains the aligned test forecasts when a model fitted successfully.</p></li><li><p><code>metrics</code> contains values such as RMSE, MAE, MAPE, and R-squared.</p></li><li><p><code>status</code> distinguishes a successful run from an unavailable optional package or a model failure.</p></li><li><p><code>message</code> preserves an actionable explanation instead of silently substituting another model.</p></li><li><p><code>metadata</code> records choices such as the backend, horizon, feature construction, or HMM interpretation.</p></li></ul><p>This status handling matters in a research environment. If <code>statsmodels</code> is not installed, the result should say that Holt-Winters or ARIMA is unavailable. It should not quietly replace the missing model with a naive forecast and then present the resulting number as an apples-to-apples comparison.</p><p>The <code>to_dict</code> method also provides a local serialization boundary. Predictions can be omitted from a summary table while retaining metrics, configuration metadata, and failure explanations.</p><h3>10.2 Holt-Winters: a statistical smoothing baseline</h3><p>Holt-Winters, also called exponential smoothing, models a series through components such as level, trend, and seasonality. For a financial price series, the generated adapter defaults to an additive trend and no seasonal component:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;7cf95fdd-fee9-4ec3-b733-4e84056b2792&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">class HoltWintersForecaster:
    name = "Holt-Winters"

    def __init__(
        self,
        trend: str | None = "add",
        seasonal: str | None = None,
        seasonal_periods: int | None = None,
        damped_trend: bool = False,
    ) -&gt; None:
        self.trend = trend
        self.seasonal = seasonal
        self.seasonal_periods = seasonal_periods
        self.damped_trend = damped_trend
        self._fit_result: Any = None

    def fit(self, values: ArrayLike) -&gt; "HoltWintersForecaster":
        series = _as_float_array(values, "training values")
        ...
        model = ExponentialSmoothing(
            series,
            trend=self.trend,
            damped_trend=(
                self.damped_trend if self.trend is not None else False
            ),
            seasonal=self.seasonal,
            seasonal_periods=self.seasonal_periods,
            initialization_method="estimated",
        )
        self._fit_result = model.fit(optimized=True, use_brute=True)
        return self

    def predict(self, horizon: int) -&gt; np.ndarray:
        if self._fit_result is None:
            raise RuntimeError("fit must be called before predict")
        return np.asarray(self._fit_result.forecast(horizon), dtype=float)</code></pre></div><p>The <code>fit</code> method validates the input, imports <code>statsmodels</code> only when needed, constructs the smoothing model, and stores the fitted result. The <code>predict</code> method requires a prior fit and asks the backend for a specified number of future observations.</p><p>Several choices here are not recoverable from the paper:</p><ul><li><p>whether the series has a meaningful seasonal period;</p></li><li><p>whether trend should be additive, multiplicative, damped, or absent;</p></li><li><p>how initialization should be performed;</p></li><li><p>whether hyperparameters were tuned on a validation set;</p></li><li><p>whether the model forecasted raw prices, returns, or transformed values.</p></li></ul><p>Consequently, a locally fitted Holt-Winters model is a documented benchmark configuration, not proof that it matches the paper's implementation.</p><h3>10.3 ARIMA and auto-ARIMA</h3><p>ARIMA models temporal dependence through autoregressive terms, differencing, and moving-average terms. The generated <code>ARIMAForecaster</code> supports either an explicit order or an optional <code>pmdarima.auto_arima</code> path:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;31c9554a-b774-498e-b9db-78594f5a5521&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">class ARIMAForecaster:
    name = "ARIMA"

    def __init__(
        self,
        order: tuple[int, int, int] = (1, 1, 1),
        use_auto: bool = False,
        seasonal: bool = False,
        suppress_warnings: bool = True,
    ) -&gt; None:
        self.order = tuple(int(part) for part in order)
        self.use_auto = use_auto
        self.seasonal = seasonal
        self.suppress_warnings = suppress_warnings
        self._fit_result: Any = None
        self._backend = ""</code></pre></div><p>When <code>use_auto=False</code>, the adapter uses the configured <code>(p, d, q)</code> order with <code>statsmodels</code>. When <code>use_auto=True</code>, it requires <code>pmdarima</code> and delegates model-order selection to <code>auto_arima</code>. These are materially different experiments: a fixed ARIMA order and an automatically selected order should not be reported under one undifferentiated label.</p><p>The paper mentions <code>auto_arima</code>, but it does not state the search bounds, seasonal settings, differencing policy, information criterion, or whether the test period influenced model selection. Those omissions affect both accuracy and fairness. A benchmark must select its order using training data or a separate validation procedure, never by inspecting final test targets.</p><h3>10.4 HMM: state decoding is not automatically value forecasting</h3><p>A hidden Markov model represents observations as being generated by an unobserved state sequence. The Viterbi algorithm finds the most likely sequence of hidden states given the observations. That is a <strong>state-decoding</strong> task; it is not automatically a forecast of the next numerical price.</p><p>This distinction corrects an ambiguity in the paper. Saying that an HMM uses Viterbi does not, by itself, specify how the next gold or Bitcoin price is calculated. A value forecast needs an additional observation model or transition-based prediction rule.</p><p>The generated adapter makes its reconstruction explicit. It fits a Gaussian HMM to one-step price changes, estimates the mean change associated with each latent state, and uses transition probabilities to estimate a future change. Its metadata records that interpretation:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;e59161e8-e52c-4325-968b-f42815c4a0a0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">class HMMForecaster:
    name = "HMM"

    def fit(self, values: ArrayLike) -&gt; "HMMForecaster":
        series = _as_float_array(values, "training values")
        changes = np.diff(series).reshape(-1, 1)
        model = GaussianHMM(
            n_components=self.n_states,
            covariance_type="diag",
            n_iter=self.n_iter,
            random_state=self.random_state,
        )
        model.fit(changes)
        states = model.predict(changes)
        ...
        self._model = model
        self._state_means = means
        self._last_value = float(series[-1])
        return self</code></pre></div><p>The important design choice is not the exact HMM implementation; it is the separation between:</p><p>fitting latent states;</p><p>decoding or identifying a current state;</p><p>converting state transitions into an expected future change;</p><p>adding that change to a price level.</p><p>The paper does not specify the number of states, emissions, covariance structure, observation variable, or prediction equation. Therefore, the HMM result must be labeled <strong>reconstructed</strong>, not presented as a verified implementation of the paper's HMM procedure.</p><h3>10.5 XGBoost is gradient boosting, not a random forest</h3><p>The paper's description of XGBoost as reducing computation through random forest is technically inaccurate. XGBoost is an implementation of gradient-boosted decision trees. A random forest builds many independently randomized trees and averages them; gradient boosting builds trees sequentially, with later trees attempting to correct earlier errors.</p><p>The generated <code>XGBoostForecaster</code> uses lagged or flattened window features:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;f8999c48-51c6-4f47-9b1a-1193e03e6044&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">class XGBoostForecaster:
    name = "XGBoost"

    @staticmethod
    def _features(values: Any) -&gt; np.ndarray:
        array = np.asarray(values, dtype=float)
        if array.ndim == 1:
            array = array.reshape(-1, 1)
        elif array.ndim == 3:
            array = array.reshape(array.shape[0], -1)
        elif array.ndim != 2:
            raise ValueError(
                "XGBoost features must be one-, two-, or three-dimensional"
            )
        return array</code></pre></div><p>A recurrent network consumes the temporal dimension as a sequence. A tree model generally needs a two-dimensional table, so a window such as <code>(samples, 100, features)</code> is flattened into <code>(samples, 100 * features)</code>. That transformation is a modeling decision and should be recorded because it affects comparability.</p><p>The adapter also supports recursive prediction when no future feature rows are supplied. That behavior is only a practical reconstruction: the paper does not say whether XGBoost used lag features, technical indicators, rolling statistics, or another feature representation. A rigorous comparison should define the feature matrix before looking at test targets and should ensure that every lag uses information available at the corresponding timestamp.</p><h3>10.6 Optional dependencies are part of the result</h3><p>The benchmark module imports optional libraries inside the methods that need them. This keeps the core package usable when only NumPy and pandas are installed. The adapter catches missing dependencies and creates an explicit unavailable result:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;cbbc466c-aee8-4d07-a7c5-c164eba54d9d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">try:
    prediction = np.asarray(
        _fit_and_predict(model, train_values, test_values, horizon),
        dtype=float,
    ).reshape(-1)
    metrics = _metric_dict(actual, prediction)
    results[model_name] = BenchmarkResult(
        model_name=model_name,
        predictions=prediction,
        metrics=metrics,
        metadata=model_metadata,
    )
except _OptionalDependencyError as exc:
    results[model_name] = BenchmarkResult(
        model_name=model_name,
        status="unavailable",
        message=str(exc),
        metadata=common_metadata,
    )
except Exception as exc:
    results[model_name] = BenchmarkResult(
        model_name=model_name,
        status="failed",
        message=f"{type(exc).__name__}: {exc}",
        metadata=common_metadata,
    )</code></pre></div><p>This structure distinguishes at least three outcomes:</p><ul><li><p><strong>`ok`</strong>: the adapter returned predictions and metrics;</p></li><li><p><strong>`unavailable`</strong>: an optional package was not installed;</p></li><li><p><strong>`failed`</strong>: the package was available, but fitting or prediction raised an error.</p></li></ul><p>That distinction should remain visible in reports. A table containing only numeric columns can make a missing model look like a successful zero-result experiment.</p><p>The project's <code>pyproject.toml</code> separates deep-learning dependencies from classical benchmark dependencies. This supports lightweight use of data preparation, streak analysis, sizing, risk, and accounting without requiring PyTorch, <code>statsmodels</code>, <code>pmdarima</code>, <code>hmmlearn</code>, or XGBoost.</p><h3>10.7 Shared metric calculation</h3><p>The adapters pass predictions through the common metrics layer instead of implementing a different error formula for each library:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;fa712bbd-62a6-4f25-b04b-becb9cfaf51d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def _metric_dict(actual: ArrayLike, predicted: ArrayLike) -&gt; dict[str, float]:
    result: ForecastMetrics = forecast_metrics(actual, predicted)
    return {
        "rmse": float(result.rmse),
        "mae": float(result.mae),
        "mape_percent": float(result.mape_percent),
        "r_squared": float(result.r_squared),
    }</code></pre></div><p>The exact generated helper also preserves the metric object's serializable fields. The important contract is that every model is evaluated with the same definitions from <code>src/quantitative_trading_model/metrics.py</code>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;e600fba8-8aa4-4d86-b50d-18a694612c56&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">@dataclass(frozen=True)
class ForecastMetrics:
    rmse: float
    mae: float
    mape_percent: float
    r_squared: float
    sample_count: int
    mape_sample_count: int
    mape_epsilon: float = 1e-12
    mape_zero_policy: str = "exclude"</code></pre></div><p>The metrics layer handles several edge cases explicitly:</p><ul><li><p>RMSE and MAE require equal-length, finite target and prediction arrays.</p></li><li><p>MAPE excludes targets at or below a configured near-zero threshold by default.</p></li><li><p>A caller can instead request an error when MAPE encounters a near-zero target.</p></li><li><p>R-squared has defined behavior for a constant target series: exact predictions receive <code>1.0</code>, while non-exact predictions receive <code>0.0</code>.</p></li><li><p>Metrics are computed on the same target scale used for comparison, normally after inverse transformation from a training-fitted scaler.</p></li></ul><p>MAPE units also need attention. The implementation reports percentage points, so <code>2.5</code> means 2.5%, not <code>0.025</code>. The paper's reported MAPE values should not be compared blindly until the target scale and unit convention are confirmed.</p><h3>10.8 What makes a benchmark comparison fair?</h3><p>A fair comparison requires more than calling eight <code>fit</code> methods. At minimum, document and align the following:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!nWRf!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e62f8e8-0ee2-4b65-8029-0bfa27e689ab_1216x744.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!nWRf!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e62f8e8-0ee2-4b65-8029-0bfa27e689ab_1216x744.png 424w, https://substackcdn.com/image/fetch/$s_!nWRf!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e62f8e8-0ee2-4b65-8029-0bfa27e689ab_1216x744.png 848w, https://substackcdn.com/image/fetch/$s_!nWRf!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e62f8e8-0ee2-4b65-8029-0bfa27e689ab_1216x744.png 1272w, https://substackcdn.com/image/fetch/$s_!nWRf!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e62f8e8-0ee2-4b65-8029-0bfa27e689ab_1216x744.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!nWRf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e62f8e8-0ee2-4b65-8029-0bfa27e689ab_1216x744.png" width="1216" height="744" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7e62f8e8-0ee2-4b65-8029-0bfa27e689ab_1216x744.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:744,&quot;width&quot;:1216,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:138090,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/206399283?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e62f8e8-0ee2-4b65-8029-0bfa27e689ab_1216x744.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!nWRf!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e62f8e8-0ee2-4b65-8029-0bfa27e689ab_1216x744.png 424w, https://substackcdn.com/image/fetch/$s_!nWRf!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e62f8e8-0ee2-4b65-8029-0bfa27e689ab_1216x744.png 848w, https://substackcdn.com/image/fetch/$s_!nWRf!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e62f8e8-0ee2-4b65-8029-0bfa27e689ab_1216x744.png 1272w, https://substackcdn.com/image/fetch/$s_!nWRf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e62f8e8-0ee2-4b65-8029-0bfa27e689ab_1216x744.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The generated <code>run_benchmark_suite</code> function attempts to provide one uniform workflow: extract training and test targets, fit each requested model on training data, predict the test interval, verify prediction length, calculate common metrics, and store implementation metadata. <code>compare_forecasts</code> then creates a stable pandas table and retains failed or unavailable models with status fields.</p><p>The common interface improves auditability, but it does not erase model-specific differences. An ARIMA model trained on a continuous series, an XGBoost model trained on flattened lag windows, and an HMM trained on price changes do not necessarily solve exactly the same statistical problem. Their input construction and forecast semantics must be described alongside their metric values.</p><h3>10.9 Interpreting the paper's benchmark table</h3><p>The paper reports Att-BiLSTM as the best listed predictor for both gold and Bitcoin. The extracted benchmark claims include values such as:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!IU4Y!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8a8295b-fe02-42f3-89cc-6df1cc49d3dd_1010x224.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!IU4Y!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8a8295b-fe02-42f3-89cc-6df1cc49d3dd_1010x224.png 424w, https://substackcdn.com/image/fetch/$s_!IU4Y!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8a8295b-fe02-42f3-89cc-6df1cc49d3dd_1010x224.png 848w, https://substackcdn.com/image/fetch/$s_!IU4Y!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8a8295b-fe02-42f3-89cc-6df1cc49d3dd_1010x224.png 1272w, https://substackcdn.com/image/fetch/$s_!IU4Y!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8a8295b-fe02-42f3-89cc-6df1cc49d3dd_1010x224.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!IU4Y!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8a8295b-fe02-42f3-89cc-6df1cc49d3dd_1010x224.png" width="1010" height="224" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b8a8295b-fe02-42f3-89cc-6df1cc49d3dd_1010x224.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:224,&quot;width&quot;:1010,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:40932,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/206399283?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8a8295b-fe02-42f3-89cc-6df1cc49d3dd_1010x224.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!IU4Y!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8a8295b-fe02-42f3-89cc-6df1cc49d3dd_1010x224.png 424w, https://substackcdn.com/image/fetch/$s_!IU4Y!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8a8295b-fe02-42f3-89cc-6df1cc49d3dd_1010x224.png 848w, https://substackcdn.com/image/fetch/$s_!IU4Y!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8a8295b-fe02-42f3-89cc-6df1cc49d3dd_1010x224.png 1272w, https://substackcdn.com/image/fetch/$s_!IU4Y!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8a8295b-fe02-42f3-89cc-6df1cc49d3dd_1010x224.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>These numbers are preserved in the project documentation and reproduction matrix as <strong>unverified claims</strong>. They are not hard-coded acceptance targets for the benchmark adapters. The source data, preprocessing, architecture, hyperparameters, target scale, and test protocol are unavailable, so a different local experiment can legitimately produce different values without indicating a coding error.</p><p>The reproduction matrix makes this distinction explicit:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;markdown&quot;,&quot;nodeId&quot;:&quot;4987ded2-1ccc-4078-a5f5-e6ddc912c3a6&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-markdown">| Reported Att-BiLSTM gold metrics | Reported claim / Unavailable |
|---|---|
| RMSE 19.01, MAE 13.98, MAPE 0.7626, R-squared 0.9392 | Source data, preprocessing, target definition, and test protocol are missing. |</code></pre></div><p>The same rule applies to the paper's LSTM, GRU, Holt-Winters, ARIMA, HMM, and XGBoost figures. A local comparison table can show what the supplied implementation produced under a documented protocol, but it cannot be called a reproduction of the paper's table until the missing inputs and decisions are recovered.</p><h3>10.10 Forecast benchmarks are not trading benchmarks</h3><p>Even a successful forecasting comparison is only one layer of the larger pipeline. The model with the lowest RMSE might not produce the best portfolio because:</p><ul><li><p>small forecast errors may not overcome transaction fees;</p></li><li><p>the best numerical forecast may have poor directional timing;</p></li><li><p>a position-sizing rule can amplify a modest error;</p></li><li><p>different forecast horizons can produce different trading turnover;</p></li><li><p>execution timing and slippage can reverse the order of model rankings;</p></li><li><p>a high R-squared on a trending price level does not guarantee useful return forecasts.</p></li></ul><p>For this reason, benchmark reporting should have two separate outputs:</p><p><strong>Forecast comparison:</strong> target-level metrics using aligned test observations.</p><p><strong>Trading comparison:</strong> portfolios generated from each model using the same signal, sizing, risk, fee, execution, and benchmark protocol.</p><p>The paper's reported Att-BiLSTM superiority addresses the first category as a claim. It does not, by itself, validate the second category.</p><h3>10.11 Practical benchmark checklist</h3><p>Before comparing local benchmark results, verify:</p><ul><li><p>Every model is trained only on its permitted training data.</p></li><li><p>The final test targets are not used for model selection or hyperparameter tuning.</p></li><li><p>All models forecast the same target representation and horizon, where comparison is intended.</p></li><li><p>Forecast timestamps are aligned before calculating metrics.</p></li><li><p>Price scaling is fitted on training data only and predictions are inverse-transformed consistently.</p></li><li><p>MAPE near-zero behavior is documented.</p></li><li><p>HMM state decoding is not mislabeled as a price forecast.</p></li><li><p>XGBoost is described as gradient boosting, not random forest.</p></li><li><p>Optional dependency failures remain visible in the results.</p></li><li><p>Model-specific features and hyperparameters are recorded.</p></li><li><p>Forecast metrics are reported separately from portfolio outcomes.</p></li><li><p>Paper numbers are labeled as reported claims unless independently reproduced.</p></li></ul><p>The generated benchmark adapters provide a useful scaffold for this process. Their main contribution is not a promise of identical numbers; it is a uniform, inspectable place to document how each alternative model was trained, what it predicted, whether it was available, and how its metrics were calculated.</p><h2>11. Measure Returns and Consecutive Rise/Decline Streaks</h2><p>The paper uses consecutive price increases and decreases to inform its position-management model. In practical terms, the analysis asks how often positive or negative price movements persist and how large representative gains and declines are.</p><p>The paper calls this procedure <strong>Apriori</strong>, but the extracted description does not define itemsets, support, confidence, or association rules. Standard Apriori is a frequent-itemset-mining algorithm, whereas the described procedure counts consecutive movements. The implementation therefore treats this component as <strong>streak analysis</strong> or <strong>sequential-pattern analysis</strong>. This is a terminology and implementation correction, not a claim that the paper's original authors intended a different method.</p><h3>11.1 Define the return convention first</h3><p>For adjacent prices, the standard simple return is:</p><p>\[ r<em>t = \frac{p</em>t-p<em>{t-1}}{p</em>{t-1}}. \]</p><p>The return is aligned with the later timestamp: it describes the move from the previous observation into <code>p_t</code>. For <code>[100, 110, 99]</code>, the returns are:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;839e4d1c-36b4-49fd-9e88-096eab75e89a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">100 -&gt; 110: (110 - 100) / 100  =  0.10
110 -&gt;  99: ( 99 - 110) / 110  = -0.10</code></pre></div><p>The extracted paper equation appears to use the current price as the denominator:</p><p>\[ \tilde r<em>t = \frac{p</em>t-p<em>{t-1}}{p</em>t}. \]</p><p>Because the equation layout is ambiguous, <code>compute_returns</code> uses the conventional previous-price denominator by default and exposes the current-price form through an explicit option. The choice must be recorded and used consistently for classification, percentile estimation, sizing calibration, and any backtest signal.</p><p>The relevant implementation is in <code>src/quantitative_trading_model/streaks.py</code>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;56e4cffa-cbee-4fe5-aa71-493fffc16f18&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def compute_returns(
    prices: Iterable[float],
    denominator: ReturnDenominator = "previous",
) -&gt; np.ndarray:
    values = _as_float_array(prices, name="prices")
    if np.any(values &lt;= 0.0):
        raise ValueError("prices must be strictly positive")

    if values.size &lt; 2:
        return np.empty(0, dtype=float)

    normalized = str(denominator).lower()
    if normalized in {"previous", "previous_price"}:
        base = values[:-1]
    elif normalized in {"current", "current_price", "paper"}:
        base = values[1:]
    else:
        raise ValueError(
            "denominator must be 'previous', 'current', or 'paper'"
        )

    return (values[1:] - values[:-1]) / base</code></pre></div><p>The function checks that prices are one-dimensional, finite, and strictly positive. A sequence with fewer than two prices has no adjacent return. Rejecting nonpositive prices is important because the return denominator would otherwise be invalid.</p><h3>11.2 Classify rises, declines, and zero changes</h3><p>Returns are classified numerically:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!9-gp!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5ab4e5eb-8279-4cba-aea3-74894d7bfa24_450x266.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!9-gp!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5ab4e5eb-8279-4cba-aea3-74894d7bfa24_450x266.png 424w, https://substackcdn.com/image/fetch/$s_!9-gp!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5ab4e5eb-8279-4cba-aea3-74894d7bfa24_450x266.png 848w, https://substackcdn.com/image/fetch/$s_!9-gp!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5ab4e5eb-8279-4cba-aea3-74894d7bfa24_450x266.png 1272w, https://substackcdn.com/image/fetch/$s_!9-gp!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5ab4e5eb-8279-4cba-aea3-74894d7bfa24_450x266.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!9-gp!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5ab4e5eb-8279-4cba-aea3-74894d7bfa24_450x266.png" width="450" height="266" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5ab4e5eb-8279-4cba-aea3-74894d7bfa24_450x266.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:266,&quot;width&quot;:450,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:23889,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/206399283?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5ab4e5eb-8279-4cba-aea3-74894d7bfa24_450x266.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!9-gp!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5ab4e5eb-8279-4cba-aea3-74894d7bfa24_450x266.png 424w, https://substackcdn.com/image/fetch/$s_!9-gp!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5ab4e5eb-8279-4cba-aea3-74894d7bfa24_450x266.png 848w, https://substackcdn.com/image/fetch/$s_!9-gp!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5ab4e5eb-8279-4cba-aea3-74894d7bfa24_450x266.png 1272w, https://substackcdn.com/image/fetch/$s_!9-gp!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5ab4e5eb-8279-4cba-aea3-74894d7bfa24_450x266.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><code>classify_returns</code> returns integer labels, not strings such as <code>"positive"</code> or <code>"neutral"</code>. Keeping the labels numeric makes run detection straightforward and avoids ambiguity in comparisons.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;0c84c01f-1a0e-4537-be34-6aad6f33362a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def classify_returns(
    returns: Iterable[float],
    *,
    zero_policy: ZeroPolicy = "neutral",
    tolerance: float = 0.0,
) -&gt; np.ndarray:
    values = _as_float_array(returns, name="returns")
    labels = np.zeros(values.size, dtype=np.int8)
    labels[values &gt; tolerance] = 1
    labels[values &lt; -tolerance] = -1

    if zero_policy == "raise" and np.any(labels == 0):
        raise ValueError("zero or near-zero returns are not allowed")
    if zero_policy == "ignore":
        labels = labels[labels != 0]
    return labels</code></pre></div><p>The zero policies are:</p><ul><li><p><code>neutral</code>: retain zero labels, so they break maximal runs;</p></li><li><p><code>terminate</code>: treat zero changes as streak boundaries;</p></li><li><p><code>ignore</code>: remove zero labels before counting, making surrounding nonzero observations adjacent;</p></li><li><p><code>raise</code>: reject any zero or near-zero return.</p></li></ul><p>The default is <code>neutral</code>. A nonzero <code>tolerance</code> can classify very small returns as zero; that setting should be included in experiment metadata because it changes the streak counts.</p><p>For example:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;7d9e7962-db72-4a98-aa28-5c5d24290b8e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">returns:  +   +   -   -   -   +   0   +
labels:    1   1  -1  -1  -1   1   0   1</code></pre></div><p>Under the default policy, the zero separates the final positive movements. The positive runs have lengths two, one, and one, while the negative run has length three.</p><h3>11.3 Maximal runs and overlapping subsequences</h3><p>&#8220;Two consecutive rises&#8221; can refer to different statistics. The implementation supports both interpretations.</p><p>A <strong>maximal run</strong> is a complete uninterrupted sequence of one sign. For:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;0536cf61-b53e-496c-ae67-03965c7e06d6&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">1, 1, -1, -1, -1, 1</code></pre></div><p>the maximal runs are:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;ebe958e4-eb37-4346-b12a-31e9fbee0a32&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">rise of length 2
decline of length 3
rise of length 1</code></pre></div><p>The corresponding summaries are:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;24c3786e-d9bb-4b46-9ddb-324a4659e4e5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">rise_streaks = {1: 1, 2: 1}
decline_streaks = {3: 1}</code></pre></div><p>This answers: &#8220;How many complete runs of each exact length occurred?&#8221;</p><p>An <strong>overlapping subsequence</strong> count includes shorter patterns inside longer runs. A run of four rises contains three two-rise subsequences:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;ec5ad74d-a8a7-41c9-9a06-2379ffc8639d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">[1, 1] at positions 1-2
[1, 1] at positions 2-3
[1, 1] at positions 3-4</code></pre></div><p>It also contains two three-rise subsequences and one four-rise subsequence. This answers: &#8220;How many contiguous patterns of each length occurred?&#8221;</p><p>The public function makes the distinction explicit:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;0ba16f06-dc01-4e4d-bf6d-615eb4b73e1d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def count_streaks(
    labels: Iterable[int],
    *,
    value: int,
    mode: StreakMode = "maximal",
    minimum_length: int = 1,
) -&gt; dict[int, int]:
    result: dict[int, int] = {}
    run_length = 0

    for label in labels:
        if int(label) == value:
            run_length += 1
            continue

        if run_length:
            _add_run(
                result,
                run_length,
                mode=mode,
                minimum_length=minimum_length,
            )
            run_length = 0

    if run_length:
        _add_run(
            result,
            run_length,
            mode=mode,
            minimum_length=minimum_length,
        )
    return dict(sorted(result.items()))</code></pre></div><p>The required <code>value</code> argument specifies whether to count rises (<code>value=1</code>) or declines (<code>value=-1</code>). In maximal mode, a run contributes one count for its exact length. In overlapping mode, a run of length <code>r</code> contributes <code>r-j+1</code> occurrences for each subsequence length <code>j</code>.</p><p>The paper does not specify which counting convention it uses. Both are therefore implementation interpretations, not verified transcriptions. A research result should record the selected mode and minimum streak length.</p><h3>11.4 Gain and decline percentiles</h3><p>The paper connects streak analysis with percentile statistics used by its position-management method. The implementation keeps these quantities separate:</p><ul><li><p>the 90th percentile of positive returns;</p></li><li><p>the 10th percentile of signed negative returns;</p></li><li><p>the median positive return;</p></li><li><p>the 10th percentile of positive decline magnitudes;</p></li><li><p>the median decline magnitude.</p></li></ul><p>For a negative return of <code>-0.04</code>, the signed decline is <code>-0.04</code>, while its magnitude is <code>0.04</code>. Both forms are useful: the signed value preserves the return convention, and the magnitude is convenient for a sizing calculation that expects a nonnegative loss amount.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;e8108884-4793-48c1-9f50-2ae461192fa2&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def estimate_gain_decline_percentiles(
    returns: Iterable[float],
    *,
    gain_percentile: float = 90.0,
    decline_percentile: float = 10.0,
) -&gt; dict[str, float]:
    values = _as_float_array(returns, name="returns")
    gains = values[values &gt; 0.0]
    declines = values[values &lt; 0.0]
    decline_magnitudes = -declines

    return {
        "gain_percentile": _percentile(gains, gain_percentile),
        "decline_percentile": _percentile(
            declines, decline_percentile
        ),
        "gain_median": _percentile(gains, 50.0),
        "decline_magnitude_percentile": _percentile(
            decline_magnitudes, decline_percentile
        ),
        "decline_magnitude_median": _percentile(
            decline_magnitudes, 50.0
        ),
    }</code></pre></div><p>For the return history:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;b63b8449-69a5-4a2f-a093-6d5cf11e0e01&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">[0.01, 0.02, 0.04, -0.01, -0.03, -0.06]</code></pre></div><p>the gain sample is <code>[0.01, 0.02, 0.04]</code>, and the signed decline sample is <code>[-0.01, -0.03, -0.06]</code>. The magnitude sample is <code>[0.01, 0.03, 0.06]</code>. Empty gain or decline samples produce <code>NaN</code>, rather than an invented zero.</p><p>These statistics are descriptive inputs to the later normalized exponential sizing reconstruction. They are not forecasts and do not imply that the next movement will match a historical percentile.</p><h3>11.5 The combined summary record</h3><p><code>compute_streak_statistics</code> combines return calculation, classification, run counting, and percentile estimation. It returns a frozen <code>StreakStatistics</code> record containing the arrays, mappings, scalar statistics, denominator, zero policy, and streak mode.</p><p>&#8220;Frozen&#8221; means that the dataclass fields cannot be reassigned through normal dataclass operations. It does <strong>not</strong> make the stored NumPy arrays deeply immutable: a caller can still mutate an array in place unless it copies the arrays or marks them read-only. The record should therefore be treated as an audit-oriented summary object, not as a complete deep-immutability guarantee.</p><p>The returned record includes fields such as:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;6228a63f-9fc8-42c6-abb2-ae995fd144fa&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">statistics = compute_streak_statistics(
    prices,
    denominator="previous",
    zero_policy="neutral",
    streak_mode="maximal",
)

print(statistics.rise_streaks)
print(statistics.decline_streaks)
print(statistics.gain_percentile_90)
print(statistics.decline_magnitude_percentile_10)</code></pre></div><p>Its <code>to_dict</code> method is intended for local result files and preserves the selected conventions and count summaries.</p><h3>11.6 Use only pre-decision information online</h3><p>A full-history summary is useful for exploration but is not automatically valid for an out-of-sample strategy. If a signal is generated at time <code>t</code>, the statistics used to choose its position size must be estimated from returns known no later than <code>t</code>.</p><p>This is unsafe:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;54635223-2ae3-4b29-ab4a-ea2c67db2785&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">compute returns over the complete dataset
compute percentiles over the complete dataset
use those percentiles to size trades in the earlier test period</code></pre></div><p>Future observations have influenced the historical calibration. The safer walk-forward pattern is:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;bd87d622-f574-40a0-b880-b3746f43f98f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">for each decision timestamp t:
    use returns observed before t
    calculate or update streak statistics
    choose the next sizing level
    generate the order
    execute at a later permitted bar</code></pre></div><p>The same rule applies when passing calibration statistics to <code>build_exponential_schedule</code> in <code>sizing.py</code>. A statistic calculated once from the entire dataset should not be used as though it had been available at every earlier decision time.</p><h3>11.7 What the generated tests currently represent</h3><p><code>tests/test_data_and_streaks.py</code> is intended to document small, hand-calculable invariants such as the previous-price return convention and the difference between maximal and overlapping counts. However, the generated test file currently contains known API mismatches with the generated <code>streaks.py</code> implementation:</p><p><code>classify_returns</code> returns integer labels <code>1</code>, <code>-1</code>, and <code>0</code>, while some test assertions expect textual labels such as <code>"positive"</code>, <code>"negative"</code>, and <code>"neutral"</code>.</p><p><code>count_streaks</code> requires the keyword-only <code>value</code> argument, but the generated streak-count test calls it without <code>value=1</code> or <code>value=-1</code>.</p><p>These are <strong>planned/static test artifacts with known mismatches</strong>, not evidence of executable verification. They should be repaired before being treated as runnable tests: assertions should use numeric labels, and rise and decline counts should call <code>count_streaks(labels, value=1, ...)</code> and <code>count_streaks(labels, value=-1, ...)</code> separately.</p><p>Semantic code verification was skipped, and this tutorial does not claim that the tests were executed, that they pass, or that the paper's empirical results were reproduced. The useful review targets remain the return convention, zero policy, run-counting mode, percentile population, and pre-decision information boundary.</p><h3>11.8 Summary</h3><p>The paper's consecutive-movement component can be translated into an auditable pipeline:</p><p>Compute adjacent returns with an explicitly recorded denominator.</p><p>Classify each return as a rise, decline, or zero.</p><p>Decide how zero returns affect streak boundaries.</p><p>Count maximal runs or overlapping subsequences.</p><p>Estimate gain, signed-decline, and decline-magnitude percentiles.</p><p>Pass statistics to the later sizing reconstruction only when they could have been known at the decision timestamp.</p><p>The implementation in <code>streaks.py</code> provides these mechanics, but it does not claim to recover a standard Apriori algorithm or the paper's exact counting procedure. The appropriate description is <strong>sequential streak analysis feeding a documented, finite position-sizing reconstruction</strong>.</p><h2>12. Reconstruct the Exponential Position-Sizing Rule Safely</h2><p>The paper's position-management component addresses a different question from forecasting: <strong>after a strategy decides to trade, how much capital should it commit?</strong> The paper appears to increase later additions after successive declines, using empirical gains, declines, transaction costs, and an exponential sizing curve.</p><p>This section is not an exact reproduction of that method. The extracted forms of equations (5) through (8) contain damaged notation and do not define several important variables. The implementation in <code>src/quantitative_trading_model/sizing.py</code> therefore provides a finite, normalized-exponential alternative with explicit budget and exposure controls.</p><p><strong>Reconstruction boundary:</strong> The schedule below is an auditable interpretation of the paper's sizing idea. It is not a faithful transcription of equations (5)&#8211;(8), and it does not establish that averaging down is safe or profitable.</p><h3>12.1 Why the paper's recursive equations cannot be transcribed faithfully</h3><p>The paper appears to derive later capital additions from a recovery or break-even condition: after one or more declines, a future gain should compensate for earlier losses and transaction costs. It then represents the resulting additions with an exponential curve.</p><p>The extracted equations are insufficient to reproduce that calculation reliably:</p><ul><li><p><code>p_i</code> is not clearly defined as cash, asset quantity, or notional exposure.</p></li><li><p>Signs and powers in the recursive expressions are corrupted.</p></li><li><p>The fee convention is incomplete.</p></li><li><p>The independent variable and exponent coefficient in the printed exponential expression are unclear.</p></li><li><p>The maximum number of additions is not reliably specified.</p></li><li><p>It is unclear whether an addition follows every observed decline, a forecast-confirmed decline, or a separate decision policy.</p></li></ul><p>Those choices affect both risk and accounting. Filling them in silently would make the implementation appear more faithful than it is. The replacement below makes the missing decisions explicit.</p><h3>12.2 Normalized exponential reconstruction</h3><p>The practical reconstruction assigns a positive score to each finite addition level:</p><p>si=Aexp&#8289;(Bi),i=0,1,&#8230;,n&#8722;1.</p><p>Here <code>A</code> is a scale factor, <code>B</code> is the growth rate, and <code>n</code> is the configured maximum number of additions. The scores are converted to normalized weights:</p><p>\[ w<em>i=\frac{s</em>i}{\sum<em>{j=0}^{n-1}s</em>j}. \]</p><p>For a sizing budget <code>U</code>, the planned allocation is:</p><p>\[ a<em>i=Uw</em>i. \]</p><p>Thus, subject to floating-point rounding,</p><p>\[ \sum<em>i a</em>i=U. \]</p><p>This is a plausible interpretation of the paper's normalized allocation expression, but it is not a verified transcription. In the generated implementation, <code>build_exponential_schedule</code> receives the number of additions and budget explicitly:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;c82c93f1-689c-4b02-bfd5-b8485254d05e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def build_exponential_schedule(
    max_additions: int,
    budget: float,
    *,
    growth: float = 0.5,
    amplitude: float = 1.0,
    gain_percentile: Optional[float] = None,
    decline_percentile: Optional[float] = None,
    fee_rate: float = 0.0,
    calibration_source: Any = None,
) -&gt; SizingSchedule:
    """Build normalized ``A * exp(B*i)`` addition amounts."""</code></pre></div><p>The absolute budget is therefore supplied to the sizing builder or sizer; it is not hidden inside the paper-inspired <code>SizingConfig</code> dataclass.</p><h3>12.3 Constructing the schedule safely</h3><p>The implementation computes the exponential scores and subtracts the largest exponent before calling <code>exp</code>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;788a2002-3dfb-4376-bc2c-4ad5865cd886&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">indices = np.arange(max_additions, dtype=float)
exponents = growth * indices
exponents -= float(np.max(exponents))
scores_array = float(amplitude) * np.exp(exponents)

score_sum = float(scores_array.sum())
weights_array = scores_array / score_sum
allocations_array = weights_array * budget</code></pre></div><p>Subtracting the maximum exponent does not change the normalized weights. It multiplies every score by the same positive constant, so each ratio <code>s_i / sum(s_j)</code> remains unchanged while reducing overflow risk.</p><p><code>SizingSchedule</code> stores:</p><ul><li><p><code>scores</code>: unnormalized exponential scores;</p></li><li><p><code>weights</code>: normalized proportions that sum to one; and</p></li><li><p><code>allocations</code>: planned currency amounts obtained from the budget.</p></li></ul><p>It also records metadata identifying the formula as a reconstruction. This makes the sizing assumptions available when a later experiment is inspected.</p><h3>12.4 Worked allocation example</h3><p>Suppose the total planned budget is <code>$1,000</code> and three levels have scores:</p><p>\[ s<em>0=1, \qquad s</em>1=2, \qquad s_2=4. \]</p><p>The score sum is <code>7</code>:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Adof!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba10dd43-5ed8-441f-ac72-7e69b8c0ad6e_800x356.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Adof!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba10dd43-5ed8-441f-ac72-7e69b8c0ad6e_800x356.png 424w, https://substackcdn.com/image/fetch/$s_!Adof!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba10dd43-5ed8-441f-ac72-7e69b8c0ad6e_800x356.png 848w, https://substackcdn.com/image/fetch/$s_!Adof!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba10dd43-5ed8-441f-ac72-7e69b8c0ad6e_800x356.png 1272w, https://substackcdn.com/image/fetch/$s_!Adof!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba10dd43-5ed8-441f-ac72-7e69b8c0ad6e_800x356.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Adof!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba10dd43-5ed8-441f-ac72-7e69b8c0ad6e_800x356.png" width="800" height="356" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ba10dd43-5ed8-441f-ac72-7e69b8c0ad6e_800x356.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:356,&quot;width&quot;:800,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:43895,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/206399283?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba10dd43-5ed8-441f-ac72-7e69b8c0ad6e_800x356.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Adof!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba10dd43-5ed8-441f-ac72-7e69b8c0ad6e_800x356.png 424w, https://substackcdn.com/image/fetch/$s_!Adof!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba10dd43-5ed8-441f-ac72-7e69b8c0ad6e_800x356.png 848w, https://substackcdn.com/image/fetch/$s_!Adof!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba10dd43-5ed8-441f-ac72-7e69b8c0ad6e_800x356.png 1272w, https://substackcdn.com/image/fetch/$s_!Adof!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba10dd43-5ed8-441f-ac72-7e69b8c0ad6e_800x356.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>These are planned amounts, not automatic orders. If only <code>$400</code> remains available when the final level is requested, the order must be reduced or rejected according to the current cash and exposure constraints.</p><p>The same relative scores arise from <code>A=1</code> and <code>B=log(2)</code>, because <code>exp(Bi)</code> then gives approximately <code>1</code>, <code>2</code>, and <code>4</code> for levels zero through two.</p><h3>12.5 Optional percentile and fee calibration</h3><p>The paper connects position sizing to empirical gain and decline statistics. The implementation can read values such as the 90th-percentile gain, a signed decline percentile, a decline-magnitude percentile, a median gain, and a fee rate.</p><p>When percentile values or a <code>calibration_source</code> are supplied to <code>build_exponential_schedule</code>, the helper <code>_calibrated_growth</code> derives a conservative growth coefficient:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;96372b2f-12c2-4a21-b01a-748279bb9555&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def _calibrated_growth(
    gain_percentile: Optional[float],
    decline_percentile: Optional[float],
    fee_rate: float,
) -&gt; tuple[float, dict[str, float]]:
    gain = max(float(gain_percentile or 0.0), 0.0)
    decline = abs(float(decline_percentile or 0.0))
    fee = _finite_nonnegative(fee_rate, "fee_rate")

    recovery = gain + _EPSILON
    burden = decline + 2.0 * fee
    growth = 0.05 + min(1.5, burden / recovery)
    return growth, {
        "gain_percentile": gain,
        "decline_magnitude": decline,
        "fee_rate": fee,
        "growth_coefficient": growth,
    }</code></pre></div><p>This is an engineering interpretation, not the paper's recovered break-even equation. The fee appears twice in the burden term as a conservative allowance for a purchase and later sale; actual fees are still charged by the execution simulator.</p><p><code>SizingConfig</code> contains fields such as <code>calibrate_from_percentiles</code>, <code>recovery_percentile</code>, <code>decline_percentile</code>, and <code>fee_buffer</code>. These fields document intended experiment settings, but the current generated schedule builder does <strong>not</strong> accept a <code>SizingConfig</code> directly and does not automatically consult <code>calibrate_from_percentiles</code>. Calibration is activated in the current implementation by supplying percentile arguments or a <code>calibration_source</code> to <code>build_exponential_schedule</code>. An experiment layer must explicitly connect configuration fields to those arguments if automatic configuration-driven calibration is desired.</p><h3>12.6 Finite levels and budget invariants</h3><p>The schedule validates finite values, nonnegative scores and allocations, normalized weights, and the relationship between planned allocations and its supplied budget. It also tracks consumed levels:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;f26d95d3-c50f-4245-9bf6-27f351259f44&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">used_levels: set[int] = field(default_factory=set, repr=False)</code></pre></div><p><code>next_unused_level()</code> returns the first unconsumed level, and <code>mark_used(level)</code> records a level while rejecting reuse. This prevents a strategy loop from accidentally placing the same planned addition repeatedly.</p><p>The schedule's budget is an absolute amount supplied when the schedule is built. It is distinct from the fraction fields in <code>SizingConfig</code>. The configuration validates settings such as:</p><ul><li><p>a positive <code>maximum_additions</code> value;</p></li><li><p>an <code>initial_level</code> inside the configured level range;</p></li><li><p>a positive <code>score_scale</code>;</p></li><li><p>allocation fractions between zero and one; and</p></li><li><p>a maximum exposure fraction in <code>(0, 1]</code>.</p></li></ul><p>These checks validate fraction and level ranges. They do not compare an exposure fraction with an absolute budget, because <code>SizingConfig</code> does not contain an absolute budget field. Absolute budget and exposure constraints are enforced later by <code>build_exponential_schedule</code> and <code>allocate_next_addition</code>.</p><h3>12.7 Cash and exposure caps at allocation time</h3><p>The schedule is only a plan. At order time, <code>allocate_next_addition</code> applies the current portfolio state:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;85bfa6b7-8308-48d2-be7b-483fe408b774&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def allocate_next_addition(
    schedule: SizingSchedule,
    available_cash: float,
    *,
    current_exposure: float = 0.0,
    max_exposure: Optional[float] = None,
    level: Optional[int] = None,
    allow_partial: bool = True,
) -&gt; AllocationDecision:</code></pre></div><p>The effective capacity is calculated as:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;9b60a2d1-712e-41d3-94d3-8500d188e762&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">cap = schedule.budget if max_exposure is None else max_exposure
capacity = min(cash, max(0.0, cap - exposure))
approved = min(requested, capacity)</code></pre></div><p>This provides three separate protections:</p><p>A request cannot exceed current available cash.</p><p>Available cash does not override a maximum exposure limit.</p><p>Partial approval is explicit. With <code>allow_partial=True</code>, an amount may be reduced to capacity; with <code>allow_partial=False</code>, an undersized request is rejected.</p><p>The result is an <code>AllocationDecision</code> containing the requested and approved amounts, selected level, reason, remaining cash, and remaining exposure capacity.</p><p>The approved amount is a <strong>pre-fee notional</strong>. The sizing module does not deduct execution fees from that amount. The backtest applies fees separately, so the final cash cost of a buy can be higher than the approved notional. Consequently, the execution layer may clip the order again when fees are applied, even when the sizing layer approved it under a pre-fee cash calculation. Keeping these stages separate makes the fee convention auditable, but callers must not treat a pre-fee approval as a guarantee that the full order will fill.</p><h3>12.8 Why this is not unlimited averaging down</h3><p>Increasing allocations after declines can become an uncontrolled averaging-down strategy. The generated design limits that behavior by using:</p><ul><li><p>a finite <code>maximum_additions</code> value;</p></li><li><p>one-time consumption of each addition level;</p></li><li><p>a fixed schedule budget;</p></li><li><p>current-cash checks;</p></li><li><p>optional maximum exposure;</p></li><li><p>no leverage by default in the execution layer; and</p></li><li><p>optional risk filtering by later strategy components.</p></li></ul><p>These are safety and reproducibility choices, not recovered paper details. They may differ from the unknown original behavior, but they prevent the implementation from silently authorizing unlimited purchases.</p><p>The sizing module also does not decide when to add. It supplies an amount after another layer requests a level. A signal, forecast, decline condition, VaR decision, or greedy policy must determine whether that level is activated.</p><h3>12.9 Correct API example</h3><p>The generated sizing implementation uses <code>growth</code>, not <code>growth_rate</code>, when constructing a schedule:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;9aac6f35-b01a-4058-8bec-4a51afc7e9ad&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">schedule = build_exponential_schedule(
    budget=1_000.0,
    max_additions=4,
    growth=0.5,
)

assert np.isclose(sum(schedule.weights), 1.0)
assert np.isclose(sum(schedule.allocations), 1_000.0)</code></pre></div><p>The supplied generated test file uses <code>growth_rate</code> in one example and constructs <code>ExponentialPositionSizer</code> with a configuration object in a way that does not match the shown implementation. It also contains related naming differences in the risk and strategy tests. These are static test artifacts describing intended invariants, not evidence that the tests were executed successfully. The corrected example above follows the actual generated <code>build_exponential_schedule</code> signature.</p><h3>12.10 What the sizing tests are intended to verify</h3><p>The tests are intended to check properties of the reconstruction rather than the paper's unverified returns:</p><ul><li><p>scores, weights, and allocations are finite;</p></li><li><p>weights are nonnegative and sum to one;</p></li><li><p>planned allocations do not exceed the supplied budget;</p></li><li><p>a request cannot exceed available cash or exposure capacity;</p></li><li><p>an addition level cannot be reused; and</p></li><li><p>future realized prices do not influence the sizing decision.</p></li></ul><p>These are static and semantic expectations. They do not prove that equations (5)&#8211;(8) were recovered or that the resulting strategy is economically sound.</p><h3>12.11 Practical interpretation</h3><p>The reconstructed data flow is:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;54d977d7-f6fe-430f-8960-78d0e2eb5b4e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">historical gain/decline statistics
        |
        v
optional explicit calibration arguments
        |
        v
finite exponential scores
        |
        v
normalized budget allocations
        |
        v
cash and exposure checks
        |
        v
pre-fee approved notional
        |
        v
fee-aware execution and final fill</code></pre></div><p>A larger allocation after a decline increases potential exposure if prices recover, but it also increases losses if the decline continues. Fees, slippage, liquidity, and forecast error can invalidate the recovery assumptions. Evaluation should therefore include drawdown, turnover, total fees, and risk measures rather than final value alone.</p><p>The narrow claim supported by this implementation is that it provides a finite, normalized, auditable reconstruction of the paper's exponential position-sizing concept. It does not reproduce equations (5)&#8211;(8), validate the paper's reported <code>$646</code> gold or <code>$215,487</code> Bitcoin outcomes, or establish profitability.</p><h2>13. Turn Forecasts into Signals and Reconstruct the Greedy Policy</h2><p>A forecast is not yet an order. If a model predicts Bitcoin will reach <code>$31,000</code>, the strategy must still compare that prediction with the current price, decide whether the expected move is large enough to trade, choose a notional amount, check cash and exposure limits, apply risk controls, and select an action.</p><p>The paper describes this stage using forecasting, position management, risk control, and a modified greedy algorithm. It does not define the greedy algorithm operationally: the candidate actions, objective, constraints, and tie-breaking rules are missing. The implementation therefore provides a transparent <strong>reconstructed policy</strong>, not a verified reproduction.</p><p>The intended decision flow is:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;2a671660-d5a6-4723-b892-ed857020f807&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">forecast values
    -&gt; aggregate the forecast horizon
    -&gt; compare the forecast with the current price
    -&gt; create a buy, sell, add, or hold signal
    -&gt; construct and constrain candidate actions
    -&gt; estimate fees and expected benefit
    -&gt; apply a risk filter
    -&gt; rank feasible candidates deterministically
    -&gt; execute the selected action on a later bar</code></pre></div><p>The snippets in this section are abridged explanatory excerpts from <code>src/quantitative_trading_model/strategy.py</code>; they are not complete replacement functions. The generated strategy and risk modules currently have an incompatible runtime interface, so this section describes the intended contract and the required repair rather than claiming that the end-to-end policy runs successfully.</p><h3>13.1 Direction and order size are separate decisions</h3><p>The strategy module defines four directional states:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;7264f1e7-4625-4c21-b54f-5ca519cf915d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">class Signal(str, Enum):
    BUY = "buy"
    SELL = "sell"
    HOLD = "hold"
    ADD = "add"</code></pre></div><p>These values describe intent, not quantity:</p><ul><li><p><code>BUY</code> establishes or increases exposure when the forecast exceeds a threshold.</p></li><li><p><code>SELL</code> reduces existing long exposure when the forecast is sufficiently negative.</p></li><li><p><code>ADD</code> requests the next finite position-sizing level.</p></li><li><p><code>HOLD</code> submits no trade.</p></li></ul><p>Separating direction from size lets the forecasting model answer &#8220;should an opportunity be considered?&#8221; while the sizing module answers &#8220;how much may be allocated?&#8221; Cash limits, exposure caps, transaction fees, and risk controls can then reduce or reject the proposed amount.</p><p>The paper does not explain how an <code>ADD</code> signal is triggered. In particular, it does not state whether additions follow every decline, a forecast-confirmed decline, or a separate greedy rule. The generated <code>strategy.py</code> does not derive <code>ADD</code> directly from the streak analysis; it creates an addition candidate only when the supplied decision is <code>BUY</code> or <code>ADD</code> and a finite addition level is available. A production revision must define that trigger explicitly.</p><h3>13.2 Aggregate one-day or multi-day forecasts explicitly</h3><p>The paper is inconsistent about its forecast horizon. Its prediction discussion supports one-step forecasting, while its conclusion refers to the next three days. <code>SignalGenerator</code> makes the interpretation configurable.</p><p>The generator accepts predicted prices and supports three aggregation choices:</p><ul><li><p><code>terminal</code>: use the final forecast, such as day three;</p></li><li><p><code>mean</code>: use the average forecast over the horizon;</p></li><li><p><code>max</code>: use the largest forecast.</p></li></ul><p>The following is an abridged excerpt; validation and error handling remain in the generated class:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;4ad2837e-d18d-4367-800d-d9cba536cad5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">class SignalGenerator:
    def __init__(self, buy_threshold=0.0, sell_threshold=0.0,
                 aggregation="terminal", minimum_forecast_points=1):
        self.buy_threshold = buy_threshold
        self.sell_threshold = sell_threshold
        self.aggregation = aggregation
        self.minimum_forecast_points = minimum_forecast_points

    def aggregate(self, forecasts):
        values = [float(value) for value in forecasts]
        if self.aggregation == "terminal":
            return values[-1]
        if self.aggregation == "mean":
            return sum(values) / len(values)
        return max(values)</code></pre></div><p>For a current price of <code>$100</code> and forecasts <code>[101, 103, 104]</code>, terminal and maximum aggregation produce <code>$104</code>, while mean aggregation produces approximately <code>$102.67</code>. These are reconstruction choices, not rules confirmed by the paper.</p><h3>13.3 Convert the selected forecast into a signal</h3><p>After aggregation, the strategy computes the expected price return:</p><p>r^=p^&#8722;pp,</p><p>where <code>p</code> is the current price and <code>p_hat</code> is the selected forecast.</p><p>An abridged version of <code>SignalGenerator.decide</code> is:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;1a1c14bf-6736-49ef-8f4c-2ad470480b98&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def decide(self, current_price, forecasts):
    forecast_value = self.aggregate(forecasts)
    expected_return = forecast_value / current_price - 1.0

    if expected_return &gt; self.buy_threshold:
        signal = Signal.BUY
    elif expected_return &lt; -self.sell_threshold:
        signal = Signal.SELL
    else:
        signal = Signal.HOLD

    return SignalDecision(
        signal=signal,
        expected_return=expected_return,
        forecast_value=forecast_value,
        horizon=len(forecasts),
        rationale="threshold-based forecast decision",
    )</code></pre></div><p>With one-percent buy and sell thresholds:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!DZ4f!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45d66b2d-c298-46ae-b93a-89c851052492_812x292.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!DZ4f!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45d66b2d-c298-46ae-b93a-89c851052492_812x292.png 424w, https://substackcdn.com/image/fetch/$s_!DZ4f!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45d66b2d-c298-46ae-b93a-89c851052492_812x292.png 848w, https://substackcdn.com/image/fetch/$s_!DZ4f!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45d66b2d-c298-46ae-b93a-89c851052492_812x292.png 1272w, https://substackcdn.com/image/fetch/$s_!DZ4f!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45d66b2d-c298-46ae-b93a-89c851052492_812x292.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!DZ4f!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45d66b2d-c298-46ae-b93a-89c851052492_812x292.png" width="812" height="292" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/45d66b2d-c298-46ae-b93a-89c851052492_812x292.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:292,&quot;width&quot;:812,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:33912,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/206399283?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45d66b2d-c298-46ae-b93a-89c851052492_812x292.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!DZ4f!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45d66b2d-c298-46ae-b93a-89c851052492_812x292.png 424w, https://substackcdn.com/image/fetch/$s_!DZ4f!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45d66b2d-c298-46ae-b93a-89c851052492_812x292.png 848w, https://substackcdn.com/image/fetch/$s_!DZ4f!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45d66b2d-c298-46ae-b93a-89c851052492_812x292.png 1272w, https://substackcdn.com/image/fetch/$s_!DZ4f!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45d66b2d-c298-46ae-b93a-89c851052492_812x292.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The thresholds are not specified by the paper and must be stored with experiment metadata. A zero threshold can produce frequent trades when forecasts fluctuate around the current price; a positive threshold creates a hold band but may delay decisions.</p><p>A signal must contain current-information fields only: direction, expected return, forecast value, horizon, and rationale. It must not contain a realized future price or realized future return. Execution belongs to the later backtest stage.</p><h3>13.4 Candidate actions contain size and audit information</h3><p>A <code>CandidateAction</code> combines a proposed notional with the information needed for ranking:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;3e90047e-a64e-49b9-bf35-4fbe8199e323&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">@dataclass(frozen=True)
class CandidateAction:
    signal: Signal
    notional: float              # pre-fee order value
    expected_return: float
    estimated_fee: float
    score: float
    feasible: bool = True
    reason: str = ""
    addition_level: Optional[int] = None
    risk_approved: bool = True
    risk_scale: float = 1.0</code></pre></div><p>For buys and additions, <code>notional</code> is the cash committed before fees. For sells, it is the market value sold before the sell fee. <code>reason</code> and <code>addition_level</code> make decisions easier to inspect.</p><p><code>PortfolioSnapshot</code> supplies the current state:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;4efee9a7-3695-4c1f-9604-c7736c6d631a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">@dataclass(frozen=True)
class PortfolioSnapshot:
    cash: float
    holdings: float
    price: float
    addition_level: int = 0
    portfolio_value: Optional[float] = None
    max_exposure: Optional[float] = None

    @property
    def marked_value(self):
        return self.cash + self.holdings * self.price</code></pre></div><p>This state is intentionally limited to information available at the signal timestamp.</p><h3>13.5 Apply feasibility constraints before ranking</h3><p>The policy should never rank an unaffordable order as though it were executable. For a long-only account, the main constraints are:</p><p>A buy must fit available cash, including fees.</p><p>A buy must not exceed the maximum exposure.</p><p>A sell must not exceed current holdings unless shorting is explicitly enabled.</p><p>An addition must use an available finite sizing level.</p><p>A risk filter may subsequently block or scale the candidate.</p><p>For a fee rate <code>f</code>, the maximum pre-fee buy notional from cash is:</p><p>Nmax=cash1+f.</p><p>Thus <code>$75</code> of cash and a one-percent fee permit at most approximately <code>$74.26</code> of pre-fee buy notional.</p><p>The generated candidate builder caps buy amounts by cash and exposure. It does <strong>not</strong> consistently retain a separate rejected candidate for every failed cash or exposure check. Some rejected requests are reduced to an affordable candidate, while exhausted addition levels may be recorded as infeasible. Therefore, the accurate audit statement is that feasible capped candidates and selected/rejected addition cases are recorded, but the current implementation does not provide a uniform record of every failed constraint attempt. A stricter audit design would preserve both the original request and the resulting rejection reason.</p><p>A conceptual candidate calculation is:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;bb9251f0-1f5a-4e1d-80ec-160ba44d4d30&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">available_buy = snapshot.cash / (1.0 + fee_rate)
current_exposure = snapshot.holdings * snapshot.price

if snapshot.max_exposure is not None:
    available_buy = min(
        available_buy,
        max(0.0, snapshot.max_exposure - current_exposure),
    )</code></pre></div><p>The backtest must enforce the same limits again at execution. Strategy-level feasibility is not a substitute for accounting-level validation.</p><h3>13.6 Connect finite sizing to <code>ADD</code> candidates</h3><p>The normalized exponential sizer supplies finite addition amounts. The strategy layer should not duplicate the sizing formula. It requests the next level from a previously constructed sizer or schedule, then applies current cash and exposure limits.</p><p>For example, the sizing object must be defined before calling the policy:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;27bbf554-d4b2-4b96-956b-43efcef98a26&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from quantitative_trading_model.sizing import (
    ExponentialPositionSizer,
    build_exponential_schedule,
)

schedule = build_exponential_schedule(
    max_additions=3,
    budget=500.0,
    growth=0.5,
)
sizer = ExponentialPositionSizer(schedule=schedule)</code></pre></div><p>The exact generated API may need alignment before runtime use, but the intended data flow is:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;041af851-a924-4f2e-8b4b-ffc85b24742c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">BUY or ADD decision
    -&gt; request the next sizing level
    -&gt; cap it by cash and exposure
    -&gt; create an ADD candidate</code></pre></div><p>The paper does not define the trigger for <code>ADD</code>, so the example must not imply that the strategy automatically reacts to every consecutive decline. That behavior would need an explicit rule connecting <code>streaks.py</code>, the forecast, and the policy.</p><h3>13.7 Use a transparent fee-aware score</h3><p>The paper gives no objective function for its modified greedy algorithm. The reconstruction uses expected net benefit:</p><p>score(a)=notional(a)(benefit&nbsp;rate(a)&#8722;f).</p><p>For a buy or add, the benefit rate is the forecasted return. For a sell, the reconstruction treats a negative forecast as a potential avoided loss, so the benefit rate is <code>-expected_return</code>. Hold has a score of zero.</p><p>An abridged scoring function is:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;a5464a18-16b7-483c-a44e-bb0314f2a07f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def score_candidate_action(action, *, expected_return, fee_rate):
    if action.signal in {Signal.BUY, Signal.ADD}:
        benefit_rate = expected_return
    elif action.signal is Signal.SELL:
        benefit_rate = -expected_return
    else:
        benefit_rate = 0.0
    return action.notional * (benefit_rate - fee_rate)</code></pre></div><p>A <code>$100</code> buy with a three-percent expected return and one-percent fee scores:</p><p>100(0.03&#8722;0.01)=2.</p><p>A <code>$100</code> buy with a 0.5% expected return scores <code>-0.50</code> and should lose to the zero-score hold action. This score ignores uncertainty, spread, liquidity, and forecast calibration. It is a transparent replacement for an objective that the paper does not provide.</p><h3>13.8 Rank candidates deterministically</h3><p>The intended ranking process is:</p><p>Remove candidates that are infeasible or not risk-approved.</p><p>Choose the highest score.</p><p>Break equal-score ties with the smaller notional.</p><p>Apply a fixed signal priority for any remaining tie.</p><p>An abridged selection expression is:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;c58222e0-2f5d-431d-8b2e-c267b279b79c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">feasible = [
    candidate for candidate in candidates
    if candidate.feasible and candidate.risk_approved
]

selected = max(
    feasible,
    key=lambda candidate: (
        candidate.score,
        -candidate.notional,
        -priority[candidate.signal],
    ),
)</code></pre></div><p>The priority mapping is an implementation choice, not a paper rule. If no candidate is feasible, the policy should return a hold decision with a clear reason such as <code>"no feasible candidate"</code>.</p><h3>13.9 Risk integration: intended contract versus current generated code</h3><p>The intended ordering is:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;0f44f093-ad74-46d0-bbac-e143682498b2&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">candidate
    -&gt; risk filter
    -&gt; accept, scale, or block
    -&gt; recompute fee and score
    -&gt; rank the resulting candidate</code></pre></div><p>Risk must be applied before final ranking because scaling a candidate can change its score. However, the current generated <code>strategy.py</code> and <code>risk.py</code> do not implement one reliable shared protocol:</p><ul><li><p><code>GreedyPolicy._apply_risk</code> calls the filter using <code>proposed_notional</code>, while <code>VaRTradeFilter.decide</code> expects <code>proposed_quantity</code> and also requires <code>current_exposure</code> and <code>portfolio_value</code>.</p></li><li><p>The fallback positional call does not reliably map those required values to the risk method's parameters.</p></li><li><p><code>VaRTradeFilter</code> returns <code>RiskDecision</code> with <code>approved_quantity</code> and <code>blocked</code>, not an <code>approved</code> field. The current strategy adapter looks first for <code>approved</code> or <code>allowed</code>, so a blocked result can be misinterpreted as approved.</p></li><li><p>Consequently, the current code must be repaired before its VaR integration can be treated as a working runtime policy.</p></li></ul><p>A strict repaired protocol should pass the same named fields every time:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;d443507e-ed85-419a-ba59-c5c5b610805c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">risk_decision = risk_filter.decide(
    proposed_quantity=action.notional,
    current_exposure=snapshot.holdings * snapshot.price,
    portfolio_value=snapshot.value,
    returns=pre_decision_returns,
)

if risk_decision.blocked:
    action = replace(action, feasible=False, risk_approved=False, notional=0.0)
elif risk_decision.approved_quantity &lt; action.notional:
    scale = risk_decision.approved_quantity / action.notional
    action = rescale_and_recompute_score(action, scale, fee_rate)</code></pre></div><p>This is a repair specification, not a claim about the current generated files. The paper itself does not define how VaR interacts with greedy selection, so the ordering remains a reconstruction even after the interfaces are aligned.</p><h3>13.10 Complete decision example with explicit assumptions</h3><p>The following is an explanatory excerpt showing the objects that must exist. It is not a complete runnable example, and the risk interface must be repaired as described above:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;34bef7d0-42d5-4ccb-917d-dbac3e43bddf&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from quantitative_trading_model.strategy import (
    GreedyPolicy, PortfolioSnapshot, SignalGenerator,
)

schedule = build_exponential_schedule(
    max_additions=3,
    budget=500.0,
    growth=0.5,
)
sizer = ExponentialPositionSizer(schedule=schedule)

policy = GreedyPolicy(fee_rate=0.01)
signal_generator = SignalGenerator(
    buy_threshold=0.01,
    sell_threshold=0.01,
    aggregation="terminal",
)

snapshot = PortfolioSnapshot(
    cash=500.0,
    holdings=0.0,
    price=100.0,
    addition_level=0,
    max_exposure=500.0,
)

# The intended call; risk_filter integration requires the strict protocol above.
action = policy.choose(
    snapshot,
    forecasts=[101.0, 103.0, 104.0],
    signal_generator=signal_generator,
    sizer=sizer,
    max_additions=3,
)</code></pre></div><p>Conceptually, this selects the terminal forecast of <code>$104</code>, computes a four-percent expected return, creates a buy decision, obtains finite sizing candidates, applies cash and exposure limits, accounts for fees, and selects the best feasible action. The selected action must be passed to the backtest for execution on a later bar.</p><h3>13.11 Verification status and no-look-ahead boundary</h3><p>The intended tests in <code>tests/test_sizing_risk_strategy.py</code> cover useful invariants:</p><ul><li><p>scores use forecasts and fees rather than realized future returns;</p></li><li><p>order sizes do not exceed cash or exposure limits;</p></li><li><p>risk scaling never increases a proposed order;</p></li><li><p>tie-breaking is deterministic;</p></li><li><p>signal timestamps precede execution timestamps.</p></li></ul><p>The supplied generated test file is currently API-inconsistent with <code>strategy.py</code>. It uses older names such as <code>side</code> instead of <code>signal</code> and calls methods with incompatible signatures. It therefore cannot substantiate that the current strategy implementation passes its intended tests without repair.</p><p>More broadly, semantic code verification was skipped. Static review identified issues in the generated project, and no code execution or passing test suite is being claimed. The excerpts above explain the intended contracts and invariants; they should not be read as evidence of runtime correctness.</p><h3>13.12 Reproduction boundary</h3><p>The paper's greedy policy remains unverified because it does not specify:</p><ul><li><p>the complete candidate-action set;</p></li><li><p>whether one action or a multi-step plan is selected;</p></li><li><p>the objective function;</p></li><li><p>how one-day and three-day forecasts are combined;</p></li><li><p>the treatment of fees, spreads, and slippage;</p></li><li><p>the interaction between VaR and action selection;</p></li><li><p>leverage, exposure, and addition constraints; or</p></li><li><p>tie-breaking behavior.</p></li></ul><p>The generated policy is therefore best understood as an auditable baseline. It demonstrates how forecasts, finite sizing, constraints, fees, risk decisions, and deterministic selection can be connected without using realized future prices. The realized price enters only during later execution and evaluation, preserving the causal boundary required for a meaningful backtest.</p><h2>14. Add Historical VaR as an Explicit Risk Filter</h2><p>The paper names Value-at-Risk (VaR) as part of its trading framework, but it does not define the parameters or the action taken when risk is high. It does not specify the confidence level, horizon, lookback window, return distribution, portfolio aggregation, acceptable loss, or whether VaR blocks or scales trades.</p><p>This project therefore uses a <strong>rolling historical VaR reconstruction</strong>. The convention in this section is explicit:</p><ul><li><p>return VaR is a nonnegative loss fraction, such as <code>0.04</code> for a 4% loss threshold;</p></li><li><p>projected VaR is a <strong>currency amount</strong>;</p></li><li><p>the risk limit is a currency amount equal to a configured fraction of portfolio value;</p></li><li><p>trades may be accepted, scaled, or blocked;</p></li><li><p>only returns available before the decision timestamp may be used.</p></li></ul><p>These choices are operational assumptions, not a verified implementation of the paper's VaR method.</p><h3>14.1 VaR sign convention and units</h3><p>Let <code>r</code> be a collection of historical returns and let <code>alpha</code> be the confidence level. The lower-tail return quantile is <code>q_(1-alpha)(r)</code>. A positive return-loss VaR is:</p><p>\[ \operatorname{VaR}^{\text{return}}<em>\alpha = -q</em>{1-\alpha}(r). \]</p><p>If the 5th-percentile return at 95% confidence is <code>-0.04</code>, return VaR is <code>0.04</code>.</p><p>For an exposure <code>E</code>, projected currency VaR is:</p><p>\[ \operatorname{VaR}^{\text{currency}}<em>\alpha = E\times \operatorname{VaR}^{\text{return}}</em>\alpha. \]</p><p>For example, a <code>$2,000</code> exposure and a 4% return-loss VaR produce:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;dd785192-5112-4794-b5b2-08a8d046e7d6&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">projected currency VaR = 2,000 * 0.04 = $80</code></pre></div><p>The units are important. A currency VaR must be compared with a currency limit, not with a fraction such as <code>0.05</code>.</p><p>The risk limit is calculated from portfolio value <code>V</code> and the configured maximum fraction <code>m</code>:</p><p>limitcurrency=mV.</p><p>Thus, for a <code>$10,000</code> portfolio and <code>m=0.05</code>, the limit is <code>$500</code>.</p><p>VaR is a loss quantile, not a guaranteed maximum loss. A realized loss can exceed it, particularly when market conditions change or the historical window does not represent the current regime.</p><h3>14.2 Explicit configuration</h3><p><code>VaRConfig</code> in <code>src/quantitative_trading_model/config.py</code> makes the unspecified choices visible:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;890d0b0a-77dc-40f5-b56e-e0feb27bb6aa&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">@dataclass
class VaRConfig:
    enabled: bool = True
    confidence: float = 0.95
    horizon: int = 1
    lookback: int = 100
    method: str = "historical"
    action: str = "scale"
    max_var_fraction: float = 0.05
    insufficient_history: str = "allow"
    min_observations: int = 30</code></pre></div><p>The defaults are implementation choices:</p><p>| Setting | Default | Meaning | | --- | ---: | --- | | <code>confidence</code> | <code>0.95</code> | Use the lower 5% return tail. | | <code>horizon</code> | <code>1</code> | Estimate a one-period loss threshold. | | <code>lookback</code> | <code>100</code> | Use at most the latest 100 available returns. | | <code>method</code> | <code>historical</code> | Use an empirical quantile. | | <code>action</code> | <code>scale</code> | Reduce an order that exceeds the risk limit. | | <code>max_var_fraction</code> | <code>0.05</code> | Permit VaR up to 5% of portfolio value. | | <code>min_observations</code> | <code>30</code> | Require this many horizon observations for an actionable estimate. |</p><p>Configuration validation rejects invalid confidence levels, nonpositive horizons and lookbacks, unsupported methods, and unknown action modes. Decimal transaction fees, forecast settings, sizing settings, and execution rules remain separate from VaR.</p><h3>14.3 Estimating a rolling historical quantile</h3><p><code>HistoricalVaR</code> in <code>src/quantitative_trading_model/risk.py</code> performs the following steps:</p><p>Convert the supplied returns to a finite one-dimensional array.</p><p>Keep only the configured lookback window.</p><p>Optionally construct overlapping compounded returns for a multi-period horizon.</p><p>Select the lower-tail empirical quantile.</p><p>Store the signed quantile and its nonnegative loss magnitude.</p><p>The central calculation is:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;fc51d166-a798-4c01-8ab4-9b2ec413e812&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">values = self._finite_returns(returns)
values = values[-self.lookback:]
horizon_values = self._horizon_returns(values)

if len(horizon_values) &lt; self.min_observations:
    return VaREstimate(
        var=float("nan"),
        confidence=self.confidence,
        horizon=self.horizon,
        observations=len(horizon_values),
        return_quantile=float("nan"),
        sufficient_history=False,
    )

quantile = np.quantile(
    horizon_values,
    1.0 - self.confidence,
    method="linear",
)
return VaREstimate(
    var=max(0.0, -float(quantile)),
    confidence=self.confidence,
    horizon=self.horizon,
    observations=len(horizon_values),
    return_quantile=float(quantile),
    sufficient_history=True,
)</code></pre></div><p><code>VaREstimate.var</code> is a return fraction. It is not a currency amount until it is multiplied by an exposure. <code>return_quantile</code> retains the signed empirical value for auditing.</p><p>The <code>1-confidence</code> probability is deliberate. With <code>confidence=0.95</code>, the implementation selects the 5th percentile. The sign conversion prevents a negative return quantile from being confused with a negative risk amount.</p><h3>14.4 Optional horizon compounding</h3><p>For a one-period horizon, the historical returns can be used directly. For a longer horizon, consecutive simple returns are compounded. A two-period return is:</p><p>\[ (1+r<em>t)(1+r</em>{t+1})-1. \]</p><p>The corresponding helper is:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;d9898f9d-cfc4-4bf6-a3fa-60605c9f6625&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def _horizon_returns(self, values: np.ndarray) -&gt; np.ndarray:
    if self.horizon == 1:
        return values
    if values.size &lt; self.horizon:
        return np.empty(0, dtype=float)

    windows = np.lib.stride_tricks.sliding_window_view(
        values,
        self.horizon,
    )
    return np.prod(1.0 + windows, axis=1) - 1.0</code></pre></div><p>Overlapping historical blocks are a transparent reconstruction. The paper does not say whether its VaR uses one-day returns, compounded multi-day returns, a parametric distribution, or simulation.</p><h3>14.5 Pre-decision history and lookback boundaries</h3><p>VaR must obey the same information boundary as the forecast. If a decision is made after observations through time <code>t</code>, the risk history may contain returns known by that point, but not returns from <code>t+1</code> or later.</p><p><code>HistoricalVaR</code> accepts caller-supplied returns and cannot infer timestamps. The caller is responsible for constructing the correct information set:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;8b1d9348-ae9c-4782-9ba7-b644c235b5e4&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># known_returns must contain only returns available at the signal time
estimate = var_estimator.estimate(known_returns, include_last=True)</code></pre></div><p>If the final element represents a current-bar return that is not yet available when the signal is formed, use <code>include_last=False</code>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;ddac7b20-872e-44ad-883d-a10d0229ff8d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">estimate = var_estimator.estimate(
    known_returns,
    include_last=False,
)</code></pre></div><p>A leakage-aware sequence is:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;f3b62310-53ec-4ac6-9df3-cde646208d9a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">1. Observe prices through the information timestamp.
2. Compute only returns available at that timestamp.
3. Estimate VaR from the permitted lookback window.
4. Generate the forecast and candidate order.
5. Apply the VaR filter.
6. Execute no earlier than the next permitted bar.</code></pre></div><p>The risk module must not be passed the complete historical return series during every past decision. That would use future information.</p><h3>14.6 Insufficient history</h3><p>When fewer than <code>min_observations</code> horizon returns are available, <code>HistoricalVaR.estimate</code> returns an estimate with <code>sufficient_history=False</code> and unavailable numerical values. It does not fabricate a zero-risk estimate.</p><p><code>VaRTradeFilter</code> then applies the configured policy:</p><ul><li><p><code>insufficient_history="allow"</code> permits the order while recording that VaR was unavailable;</p></li><li><p><code>insufficient_history="block"</code> rejects positive buy exposure until enough history exists.</p></li></ul><p>This is a policy choice that belongs in experiment metadata. Missing risk information is not evidence that risk is zero.</p><h3>14.7 Projecting VaR consistently in currency units</h3><p>The corrected unit contract for the risk filter is:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;9d795833-9360-4f58-b21c-655947ce1f7e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def projected_var(
    self,
    estimate: VaREstimate,
    *,
    exposure: float,
    portfolio_value: float,
) -&gt; float:
    """Return projected VaR as a currency amount."""
    if not estimate.sufficient_history:
        return float("nan")
    if portfolio_value &lt;= 0:
        raise ValueError("portfolio_value must be positive")
    if exposure &lt; 0:
        raise ValueError("exposure must be nonnegative")
    return estimate.var * exposure</code></pre></div><p>The portfolio value is needed for the limit, not for dividing the projected loss:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;3f08ab2e-9923-4373-9af6-10f94e617548&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">limit = self.max_var_fraction * portfolio_value
projected = self.projected_var(
    estimate,
    exposure=projected_exposure,
    portfolio_value=portfolio_value,
)</code></pre></div><p>For a 4% return VaR and <code>$2,000</code> exposure, <code>projected_var</code> returns <code>$80</code>. For a <code>$10,000</code> portfolio and a 5% limit, the comparison is <code>$80 &lt;= $500</code>; both sides are currency amounts.</p><p>This avoids mixing a portfolio-relative fraction with a currency limit. If a project instead chooses to return a relative fraction, it must compare that result with <code>max_var_fraction</code> directly. The implementation described here uses currency amounts throughout the decision path.</p><h3>14.8 Accept, scale, and block behavior</h3><p><code>VaRTradeFilter</code> applies a risk decision to a proposed <strong>notional quantity</strong>. Its modes are:</p><h4>Accept</h4><p>Approve the proposed quantity. This can be used when VaR is informational or when the order is already within the limit.</p><h4>Block</h4><p>Return an approved quantity of zero when projected currency VaR exceeds the currency limit.</p><h4>Scale</h4><p>Approve the largest quantity that remains within the limit. With portfolio value <code>V</code>, limit fraction <code>m</code>, and return VaR <code>v</code>, the maximum total exposure is:</p><p>Emax=mVv,</p><p>provided <code>v &gt; 0</code>. The maximum additional buy is this amount minus current exposure.</p><p>A consistent decision core is:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;209a9325-8221-4b9b-9b76-176e4801676d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">limit = self.max_var_fraction * portfolio_value
buy_amount = max(proposed_quantity, 0.0)
full_exposure = current_exposure + buy_amount
full_var = self.projected_var(
    estimate,
    exposure=full_exposure,
    portfolio_value=portfolio_value,
)

if proposed_quantity &lt;= 0 or full_var &lt;= limit:
    return RiskDecision(
        "accept",
        proposed_quantity,
        proposed_quantity,
        1.0,
        full_var,
        limit,
        current_exposure,
        full_exposure,
        "within VaR limit or exposure-reducing order",
        True,
    )

if self.action == "block" or estimate.var &lt;= 0:
    return RiskDecision(
        "block",
        proposed_quantity,
        0.0,
        0.0,
        full_var,
        limit,
        current_exposure,
        current_exposure,
        "proposed exposure exceeds VaR limit",
        True,
    )

allowed_exposure = limit / estimate.var
allowed_buy = max(0.0, allowed_exposure - current_exposure)
approved = min(proposed_quantity, allowed_buy)
scale = approved / proposed_quantity if proposed_quantity else 0.0</code></pre></div><p>The critical correction is <code>allowed_exposure = limit / estimate.var</code>, not <code>limit * portfolio_value / estimate.var</code>, because <code>limit</code> is already a currency amount. Similarly, <code>projected_var</code> returns <code>estimate.var * exposure</code>, not <code>estimate.var * exposure / portfolio_value</code>.</p><h3>14.9 Connecting VaR to the strategy layer</h3><p>The strategy flow is:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;6c0555fb-4b42-4dbb-b456-514d1f952f4a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">forecast
  -&gt; signal
  -&gt; candidate notional
  -&gt; cash and exposure checks
  -&gt; VaR accept / scale / block
  -&gt; greedy ranking
  -&gt; next-bar execution</code></pre></div><p>The VaR call must use the same argument names and units as <code>VaRTradeFilter.decide</code>. In particular, <code>GreedyPolicy</code> should pass the proposed order as <code>proposed_quantity</code>, current marked exposure as <code>current_exposure</code>, and portfolio value as <code>portfolio_value</code>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;d74926f7-8801-49b0-90c8-112cf8bf245d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">risk_decision = self.risk_filter.decide(
    proposed_quantity=action.notional,
    current_exposure=snapshot.holdings * snapshot.price,
    portfolio_value=snapshot.value,
    returns=returns,
)</code></pre></div><p>The selected quantity is then taken from <code>risk_decision.approved_quantity</code>. A blocked action must be marked infeasible, and a scaled action must be rescored using its approved notional before greedy ranking. The risk filter must never increase the proposed quantity.</p><p>This explicit adapter is preferable to relying on incompatible keyword aliases such as <code>proposed_notional</code> when the risk API requires <code>proposed_quantity</code>. The strategy and risk modules should share one documented contract rather than silently catching argument errors.</p><h3>14.10 Treatment of sells</h3><p>The default filter is a long-exposure filter. A negative proposed quantity represents a sell. A sell that reduces long exposure is accepted by this filter because it does not increase the projected long exposure.</p><p>This does not constitute a complete portfolio-risk model. It does not model short positions, leverage, cross-asset correlations, liquidity, spread, market impact, or nonlinear instruments. The paper's brief mention of VaR does not justify assuming those features.</p><h3>14.11 Numerical examples</h3><p>Suppose:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;216eaadf-9c85-4010-be84-635ff7154763&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">portfolio value       = $10,000
current exposure       = $2,000
proposed buy           = $3,000
return VaR             = 4%
maximum VaR fraction   = 5%</code></pre></div><p>The complete proposed exposure is <code>$5,000</code>, so projected currency VaR is:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;5338afa7-cbd6-468e-88f8-be1984d593d0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">$5,000 * 0.04 = $200</code></pre></div><p>The currency limit is:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;35ff3ebf-34c6-4dc9-b6da-af41b634e9cf&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">$10,000 * 0.05 = $500</code></pre></div><p>Because <code>$200 &lt;= $500</code>, the VaR layer accepts the proposed order, subject to cash, sizing, fee, and exposure constraints elsewhere.</p><p>If return VaR is 12% instead:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;38967bd8-ea11-44ba-a569-4e2be4e7890f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">full projected VaR = $5,000 * 0.12 = $600
risk limit         = $500</code></pre></div><p>In scale mode, maximum total exposure is:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;68455c85-a875-4950-98d2-65cc83abde7d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">$500 / 0.12 = approximately $4,166.67</code></pre></div><p>After subtracting the existing <code>$2,000</code> exposure, the maximum additional buy is approximately <code>$2,166.67</code>. In block mode, the <code>$3,000</code> proposed order is rejected.</p><p>These examples describe the reconstructed unit-consistent policy. They do not establish what the paper's original VaR procedure did.</p><h3>14.12 Tests and verification boundary</h3><p>Tests for the corrected API should call the actual interfaces directly. For example:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;8ca4e4bd-b3d2-49d4-ae21-f1609f37b0c7&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">var = HistoricalVaR(
    confidence=0.95,
    horizon=1,
    lookback=len(returns),
    min_observations=2,
)
estimate = var.estimate(returns)

risk_filter = VaRTradeFilter(
    var_estimator=var,
    max_var_fraction=0.005,
    action="block",
)
decision = risk_filter.decide(
    proposed_quantity=500.0,
    current_exposure=0.0,
    portfolio_value=1_000.0,
    estimate=estimate,
)

assert decision.approved_quantity &lt;= 500.0</code></pre></div><p>The test should not pass <code>portfolio_value</code> to <code>HistoricalVaR.estimate</code>, because historical estimation uses returns and VaR configuration; portfolio value belongs to exposure projection and trade filtering. It should also use <code>max_var_fraction</code>, not an unsupported <code>max_var</code> constructor argument, and should call <code>decide</code> with <code>proposed_quantity</code>, <code>current_exposure</code>, and <code>portfolio_value</code>.</p><p>These are invariant-level checks, not empirical validation. Static and semantic review can inspect units, argument contracts, and no-look-ahead behavior, but no claim is made here that the generated code was executed. The supplied verification record states that semantic code verification was skipped, so the risk tests and integration snippets should be treated as corrected intended contracts until the source artifacts are aligned and independently checked.</p><h3>14.13 Reproducibility boundary</h3><p>This VaR layer remains <strong>reconstructed</strong>. A verified implementation of the paper would require at least:</p><p>the confidence level;</p><p>the VaR horizon;</p><p>the return definition and portfolio aggregation method;</p><p>the estimation window;</p><p>the historical or parametric estimation method;</p><p>the maximum acceptable loss or exposure threshold;</p><p>whether VaR blocks, scales, or merely reports trades;</p><p>how VaR interacts with the greedy policy and position-sizing schedule.</p><p>The accurate claim is therefore: <strong>the project defines a documented rolling historical VaR filter with consistent currency units and explicit action rules, inspired by the paper's risk-control description</strong>. It does not claim to reproduce the paper's unspecified VaR method or its reported investment outcomes.</p><h2>15. Simulate Next-Bar Execution and Portfolio Accounting</h2><p>A forecast becomes useful to a backtest only after it is converted into an order and accounted for consistently. The paper describes a combined trading framework, but it does not fully specify signal timing, execution prices, fee conventions, slippage, liquidation, or portfolio accounting. The generated implementation therefore makes these choices explicit in <code>src/quantitative_trading_model/backtest.py</code>.</p><p>These rules are reconstructions, not claims about the paper's exact backtest. The simulator is offline: it accepts caller-supplied local prices and orders and does not connect to an exchange or submit live orders.</p><h3>15.1 Signal time and execution time</h3><p>A strategy forms a signal using information available at time <code>t</code>. The order must execute at a later timestamp. The simulator enforces that ordering:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;1e72bd31-6c1b-4876-8c03-29020e8fa95a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">historical window ends at t
    -&gt; forecast and signal are generated at t
    -&gt; order is submitted for a later bar
    -&gt; fill occurs after t
    -&gt; portfolio is marked to market</code></pre></div><p>The <code>Order</code> dataclass records both the signal and intended execution timestamps:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;508a03aa-1ccf-4c96-a8e6-ea5278c2ecfa&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">@dataclass(frozen=True)
class Order:
    side: str
    quantity: float | None = None
    signal_timestamp: Any = None
    execution_timestamp: Any = None
    notional: float | None = None
    addition_level: int | None = None
    reason: str = ""
    metadata: Mapping[str, Any] = field(default_factory=dict)</code></pre></div><p>An order may specify asset units through <code>quantity</code> or a currency amount through <code>notional</code>. In the current implementation, a notional order is converted to units using the supplied <strong>reference price</strong> before slippage is applied. Therefore, with nonzero slippage, the executed notional can differ from the requested notional. This is an implementation detail that should be recorded in experiment metadata rather than described as conversion at the final slippage-adjusted price.</p><p><code>PortfolioSimulator.execute</code> rejects same-bar and earlier execution:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;e11e2106-9916-41ce-a97a-095a3caf0a77&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">fill_time = _timestamp(timestamp)
signal_time = _timestamp(order.signal_timestamp)
if fill_time &lt;= signal_time:
    raise ValueError(
        "Orders must execute strictly after their signal timestamp"
    )</code></pre></div><p>This prevents a signal generated from a closing price from being filled at that same closing price. The simulator enforces a later timestamp, but it does not independently determine that the timestamp is exactly the next bar. A caller must supply next-bar orders or use a scheduling layer that does so.</p><p>Each completed order becomes a <code>Fill</code> record containing its timing and accounting details:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;384e4d94-f5e9-4e20-8469-67c930b75799&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">@dataclass(frozen=True)
class Fill:
    timestamp: Timestamp
    signal_timestamp: Timestamp
    side: str
    quantity: float
    reference_price: float
    execution_price: float
    notional: float
    fee: float
    cash_change: float
    reason: str = ""
    addition_level: int | None = None</code></pre></div><p>Thus, a fill is an auditable event rather than merely a change in portfolio value.</p><h3>15.2 Fees and executed notional</h3><p>For an executed notional <code>N</code> and fee rate <code>f</code>, the configured paper-inspired convention is:</p><p>buy cash cost=N(1+f),</p><p>sell cash proceeds=N(1&#8722;f).</p><p>The current simulator applies one fee rate to both buys and sells whenever that rate is configured. However, the bare <code>PortfolioSimulator</code> constructor defaults to a fee rate of <code>0.0</code>. The two-sided fee behavior is therefore a configured convention, not an unconditional simulator default. <code>ExecutionConfig</code> supplies a paper-inspired fee rate when it is passed to the simulator, but the current simulator does not consult the <code>fee_on_buy</code> and <code>fee_on_sell</code> boolean fields. Those flags should not be described as controlling accounting until the implementation is extended to enforce them.</p><p>The core fill calculation is:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;3f7b3581-49a3-4cb5-b1a4-070fd67f647a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">notional = quantity * effective_price
fee = notional * self.fee_rate

if side == "buy":
    cash_change = -(notional + fee)
    self.state.quantity += quantity
    self.state.cash += cash_change
else:
    cash_change = notional - fee
    self.state.quantity -= quantity
    self.state.cash += cash_change

self.state.fees_paid += fee
self.state.traded_notional += notional</code></pre></div><p><code>PortfolioState</code> stores the mutable account state:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;ae8dc651-4111-4a0c-a717-62db99ced9c8&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">@dataclass
class PortfolioState:
    cash: float
    quantity: float = 0.0
    average_price: float = 0.0
    fees_paid: float = 0.0
    traded_notional: float = 0.0
    addition_level: int = 0</code></pre></div><p>The average price is informational. Portfolio value is calculated from cash, quantity, and the current market price. Cumulative fees and traded notional support later turnover and fee-sensitivity reporting.</p><h3>15.3 Slippage</h3><p>The reference price is the observed price supplied to the simulator. Optional proportional slippage adjusts it against the trader:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;3866e612-3219-4657-8b6a-ff51306404e4&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">effective_price = price * (
    1.0 + self.slippage_rate if side == "buy"
    else 1.0 - self.slippage_rate
)</code></pre></div><p>A buy therefore receives a higher effective price and a sell receives a lower one. Fees are calculated from the resulting executed notional.</p><p>This is a simple slippage model. It does not represent bid-ask spread, market impact, liquidity, partial fills, or intrabar price paths. The paper does not provide those details, so proportional slippage is an explicit reconstruction rather than a paper-faithful assumption.</p><p>Because notional-to-unit conversion currently uses the reference price before this adjustment, a request for a fixed notional should be interpreted as a reference-price notional. If exact post-slippage notional sizing is required, the conversion logic in <code>backtest.py</code> must be changed before using that interpretation.</p><h3>15.4 Units, cash limits, and leverage</h3><p>The current simulator always permits fractional quantities. Although <code>ExecutionConfig</code> contains an <code>allow_fractional_units</code> field, <code>PortfolioSimulator</code> does not currently enforce it. Fractional-unit support should therefore be described as the simulator's present behavior, not as a configurable guarantee.</p><p>When leverage is disabled, buys are clipped to available cash:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;802a0ab0-c06f-437f-8cef-f37aeaced5b8&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">if side == "buy" and not self.allow_leverage:
    affordable = self.state.cash / (
        effective_price * (1.0 + self.fee_rate)
    )
    quantity = min(quantity, max(0.0, affordable))</code></pre></div><p>Likewise, a long-only account cannot sell more than it owns:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;6d280058-234b-4c8a-9d88-495752fe9e34&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">if side == "sell" and not self.allow_short:
    quantity = min(quantity, max(0.0, self.state.quantity))</code></pre></div><p>These safeguards prevent negative cash and negative holdings under the simulator's default long-only policy. They are safety choices made by the implementation, not evidence that the paper used the same constraints. The paper does not state whether it allowed leverage, margin, short positions, or unlimited reinvestment.</p><p>For example, an account with <code>$1,000</code> buys <code>$400</code> of an asset at a configured fee rate of <code>0.5%</code>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;9574e3b0-44df-491b-89d7-2cb5573768a4&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">reference notional = $400.00
fee                = $400.00 * 0.005 = $2.00
cash decrease      = $402.00
cash remaining     = $598.00</code></pre></div><p>At a reference price of <code>$100</code>, the requested notional becomes four units before any slippage adjustment.</p><h3>15.5 Mark-to-market valuation</h3><p>At each valuation timestamp, the simulator computes:</p><p>\[ V<em>t=\text{cash}</em>t+\text{quantity}<em>t\times p</em>t. \]</p><p>The corresponding method is straightforward:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;276f0e0d-b351-4f2e-b8f0-1f326e66ad7b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def portfolio_value(self, price: float) -&gt; float:
    price = _number(price)
    return self.cash + self.quantity * price</code></pre></div><p><code>mark_to_market</code> stores the result in a <code>BacktestRecord</code>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;d53121a5-1614-4ec3-bd37-2da6ee7f37fe&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">@dataclass(frozen=True)
class BacktestRecord:
    timestamp: Timestamp
    price: float
    cash: float
    quantity: float
    portfolio_value: float
    fees_paid: float
    traded_notional: float
    fill_count: int = 0</code></pre></div><p>Valuation timestamps must increase strictly. The resulting value must not be materially negative. These rules provide an accounting invariant that can be checked independently for every record.</p><p>Continuing the example, four units held at a market price of <code>$105</code> produce:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;b4c55f71-086f-4113-8f3a-dad42f89898e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">cash             = $598.00
marked holdings  = 4 * $105 = $420.00
portfolio value  = $1,018.00</code></pre></div><p>The entry fee is already reflected in cash. A later sale incurs a further fee when the configured fee rate is nonzero.</p><h3>15.6 Explicit liquidation</h3><p>The final value of an open position can mean either its marked value or the cash remaining after liquidation. These are different when selling incurs fees. The simulator therefore does not silently assume liquidation.</p><p><code>liquidate</code> creates a sell order for the remaining long quantity and places it after the last valuation timestamp:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;d7050202-0d4e-4e71-8f4c-6fab04d387df&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def liquidate(
    self,
    timestamp: Any,
    price: float,
    reason: str = "end_of_backtest",
) -&gt; Fill | None:
    if self.state.quantity &lt;= 0:
        return None
    signal_time = self._last_valuation_timestamp
    return self.execute(
        Order(
            side="sell",
            quantity=self.state.quantity,
            signal_timestamp=signal_time,
            reason=reason,
        ),
        timestamp,
        price,
    )</code></pre></div><p>The liquidation fill remains in the event ledger and any configured sell fee is included in cumulative costs. Whether to liquidate depends on the research question. A cash-outcome report generally requires liquidation, while a marked portfolio report may not. The paper does not specify which convention produced its headline values, so the choice must accompany every result.</p><h3>15.7 Buy-and-hold benchmark</h3><p>A strategy should be compared with passive ownership over the same period. The <code>buy_and_hold</code> function buys at the first available bar, applies the configured purchase assumptions, holds the asset, and optionally liquidates at the end.</p><p>Its entry signal is placed just before the first bar so that it satisfies the same timestamp contract:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;fb29c74e-6cb3-4207-ac29-45589f51600b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">first = _timestamp(frame.index[0])
signal_time = first - pd.Timedelta(nanoseconds=1)

quantity = initial_cash / (
    float(frame.iloc[0, 0])
    * (1.0 + slippage_rate)
    * (1.0 + fee_rate)
)

order = Order(
    side="buy",
    quantity=quantity,
    signal_timestamp=signal_time,
    execution_timestamp=first,
    reason="buy_and_hold_entry",
)</code></pre></div><p>This benchmark uses no forecasts, streak statistics, VaR, or greedy selection. It helps distinguish strategy-specific behavior from the return generated simply by holding the asset. The paper does not report a buy-and-hold comparison, so adding one is an evaluation improvement rather than a claim about the original method.</p><h3>15.8 Accounting tests and verification limits</h3><p><code>tests/test_backtest_accounting.py</code> contains hand-calculable tests for intended contracts such as fee arithmetic, later execution, valuation identity, no-leverage behavior, liquidation, and buy-and-hold accounting. A representative invariant is:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;d22e9829-e66f-4653-adab-92ada50add96&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">cash = _field(state, "cash")
quantity = _field(state, "quantity", "holdings", "asset_quantity")
value = _field(record, "portfolio_value", "value", "equity")
assert value == pytest.approx(cash + quantity * 12.0)</code></pre></div><p>These files are static, intended test artifacts. They should not be presented as executed or passing. The supplied verification record reports that semantic code verification was skipped. It also notes apparent API inconsistencies between some generated tests and the generated sizing, risk, strategy, and configuration modules. Consequently, these tests may require API alignment before they can serve as executable checks.</p><p>The useful claim is narrower: the tests document the accounting invariants that a corrected implementation should satisfy. They do not establish runtime correctness, profitability, or reproduction of the paper.</p><h3>15.9 Why the paper's headline values cannot be validated</h3><p>The paper reports approximately <code>$646</code> from a <code>$500</code> gold allocation and <code>$215,487</code> from a <code>$500</code> Bitcoin allocation. The simulator does not treat these figures as expected outputs. Validation would require, at minimum:</p><ul><li><p>exact assets and historical dates;</p></li><li><p>sampling frequency and price field;</p></li><li><p>forecast target and signal timestamps;</p></li><li><p>buy, sell, hold, and add-position rules;</p></li><li><p>original position-sizing parameters;</p></li><li><p>VaR confidence, horizon, and action rule;</p></li><li><p>greedy objective and constraints;</p></li><li><p>fee, spread, and slippage conventions;</p></li><li><p>fractional-unit, leverage, and reinvestment rules;</p></li><li><p>liquidation or mark-to-market convention.</p></li></ul><p>Without these details, matching a final number would not demonstrate faithful reproduction. It could result from selecting a similar period or from offsetting errors in data preparation and accounting.</p><p>The appropriate interpretation is:</p><p>The backtest module demonstrates timestamped execution and auditable portfolio accounting under explicit assumptions. It does not verify the paper's reported profits.</p><p>This distinction keeps the implementation useful for research while avoiding unsupported claims about profitability or live-trading readiness.</p><h2>16. Orchestrate Forecast Experiments and Fee Sensitivity</h2><p>The experiment layer is intended to make research runs repeatable, but its current generated implementation has a narrower role than the complete architecture described earlier. <code>src/quantitative_trading_model/experiments.py</code> currently provides:</p><ul><li><p>chronological forecast orchestration;</p></li><li><p>expanding walk-forward forecast orchestration;</p></li><li><p>optional benchmark dispatch;</p></li><li><p>fee-sensitivity callback infrastructure; and</p></li><li><p>result metadata and local JSON/CSV persistence.</p></li></ul><p>It does <strong>not yet directly compose</strong> <code>strategy.py</code>, <code>sizing.py</code>, <code>risk.py</code>, and <code>backtest.py</code> into one end-to-end trading run. A complete integration would need an additional orchestration function that converts forecasts into signals, applies sizing and VaR, selects actions, submits next-bar orders, and collects portfolio metrics. The current fee-sensitivity function instead receives that behavior through an injected callback.</p><p>The distinction matters because an experiment runner can preserve assumptions without necessarily implementing every stage itself. The paper's data, execution protocol, position-sizing equations, VaR settings, and greedy policy are also incomplete, so the orchestration layer must report both its own implementation status and the paper-fidelity limitations.</p><h3>16.1 Current position in the research pipeline</h3><p>The intended overall workflow is:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;3a1c951a-2c8c-406e-a43f-8a0a080eb1e4&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">local price data
    -&gt; cleaning and sliding windows
    -&gt; chronological split or walk-forward folds
    -&gt; forecast model and optional benchmarks
    -&gt; signals, sizing, VaR, and greedy policy
    -&gt; event-driven backtest
    -&gt; forecast and portfolio metrics
    -&gt; result metadata and local files</code></pre></div><p>The current <code>experiments.py</code> implementation covers the forecast and result-recording portions directly. The trading stages can be supplied by a caller through a backtest callback, but they are not automatically wired together by <code>run_forecast_experiment</code> or <code>run_fee_sensitivity</code>.</p><p>Its common result container is:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;28138bde-d030-460f-8d4b-883a462651ba&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">@dataclass
class ExperimentResult:
    experiment_type: str
    asset: str | None = None
    model: str | None = None
    status: str = "completed"
    metrics: dict[str, Any] = field(default_factory=dict)
    portfolio: dict[str, Any] = field(default_factory=dict)
    metadata: dict[str, Any] = field(default_factory=dict)
    predictions: list[dict[str, Any]] = field(default_factory=list)
    warnings: list[str] = field(default_factory=list)</code></pre></div><p>The separate <code>metrics</code> and <code>portfolio</code> fields are useful even though the current forecast runner primarily fills <code>metrics</code>. Forecast errors such as RMSE and MAE belong in <code>metrics</code>; final value, drawdown, turnover, and fees belong in <code>portfolio</code> when a backtest callback supplies them. The <code>warnings</code> field should identify unavailable dependencies, unresolved interfaces, and paper claims that were not reproduced.</p><p><code>to_dict()</code>, <code>to_json()</code>, <code>summarize_results()</code>, and <code>save_local_results()</code> are serialization utilities. They preserve local research records; they do not send results to a market service or submit orders.</p><h3>16.2 Paper-style chronological forecast experiments</h3><p>The paper reports a 70:30 train/test split. The current <code>run_forecast_experiment</code> function is the generated implementation's paper-style forecast path:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;c520a358-8770-4922-b2ae-f7affcae5767&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def run_forecast_experiment(
    prices: Any,
    *,
    asset: str = "asset",
    config: ResearchConfig | None = None,
    model_name: str = "att_bilstm",
    model: Any = None,
    include_benchmarks: bool = False,
    seed: int | None = 7,
) -&gt; ExperimentResult:</code></pre></div><p>Its intended sequence is:</p><p>Validate a supplied <code>ResearchConfig</code>.</p><p>Standardize local prices.</p><p>Create windows using the configured window length, stride, and horizon.</p><p>Split samples chronologically.</p><p>Fit the requested model on the training partition.</p><p>Predict the held-out test partition.</p><p>Calculate forecast metrics.</p><p>Store model, split, horizon, seed, and warning metadata.</p><p>This is forecast orchestration, not a complete portfolio experiment. It does not itself call the signal generator, position sizer, VaR filter, greedy policy, or portfolio simulator. A caller that needs trading results must connect those modules separately or provide a higher-level callback.</p><p>The intended metadata shape is:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;1e1b66ef-5d54-48c3-8c69-58ae373f0ebb&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">metadata = {
    "asset": asset,
    "model": model_name,
    "split": "chronological_70_30_or_configured",
    "window_length": 100,
    "horizon": 1,
    "seed": 7,
    "data_points": len(values),
    "synthetic_data_is_not_paper_validation": True,
}</code></pre></div><p>This metadata design is useful, but it should not be treated as proof that every field is currently populated correctly. The generated code contains an unresolved interface mismatch: <code>run_forecast_experiment</code> attempts to pass <code>timestamps</code> to <code>standardize_prices</code>, while the generated <code>standardize_prices</code> signature accepts a DataFrame or <code>PriceData</code> and does not define a <code>timestamps</code> keyword. Timestamp-aware runs therefore require API alignment before they can be considered runtime-ready.</p><p>The paper's exact data source, split date, scaling boundary, and target protocol are also unavailable. A chronological split is therefore a documented comparison protocol, not an exact reproduction of the paper's experiment.</p><h3>16.3 Walk-forward forecast experiments</h3><p>For trading conclusions, expanding walk-forward evaluation is preferable to one fixed split:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;ba9870fb-f295-43fb-84b0-3f6b62c6a197&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">train through t
forecast the next test block
move the boundary forward
expand the training history
forecast again
repeat</code></pre></div><p><code>run_walk_forward_experiment</code> is intended to retrain or invoke a supplied <code>train_predict</code> callback for each fold, then aggregate the fold predictions and targets. This protocol better reflects deployment because each forecast uses only the historical information available at that point.</p><p>Walk-forward evaluation does not remove all sources of optimism. Feature construction, scaling, model selection, and hyperparameter tuning must still be restricted to the information available in each fold. Walk-forward results also should not be compared directly with the paper's 70:30 metrics without explaining the protocol difference.</p><h3>16.4 Fee grids are experiment inputs, not recovered results</h3><p>The paper describes the following transaction-fee scenarios:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!2aYh!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9a32d320-ffc9-4eb8-a545-61e205883377_1114x224.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!2aYh!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9a32d320-ffc9-4eb8-a545-61e205883377_1114x224.png 424w, https://substackcdn.com/image/fetch/$s_!2aYh!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9a32d320-ffc9-4eb8-a545-61e205883377_1114x224.png 848w, https://substackcdn.com/image/fetch/$s_!2aYh!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9a32d320-ffc9-4eb8-a545-61e205883377_1114x224.png 1272w, https://substackcdn.com/image/fetch/$s_!2aYh!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9a32d320-ffc9-4eb8-a545-61e205883377_1114x224.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!2aYh!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9a32d320-ffc9-4eb8-a545-61e205883377_1114x224.png" width="1114" height="224" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9a32d320-ffc9-4eb8-a545-61e205883377_1114x224.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:224,&quot;width&quot;:1114,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:38595,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/206399283?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9a32d320-ffc9-4eb8-a545-61e205883377_1114x224.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!2aYh!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9a32d320-ffc9-4eb8-a545-61e205883377_1114x224.png 424w, https://substackcdn.com/image/fetch/$s_!2aYh!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9a32d320-ffc9-4eb8-a545-61e205883377_1114x224.png 848w, https://substackcdn.com/image/fetch/$s_!2aYh!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9a32d320-ffc9-4eb8-a545-61e205883377_1114x224.png 1272w, https://substackcdn.com/image/fetch/$s_!2aYh!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9a32d320-ffc9-4eb8-a545-61e205883377_1114x224.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p><code>FeeSensitivityConfig</code> defines these fields:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;e55be5c2-df11-410d-809f-4b9d1bdb0d84&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">@dataclass
class FeeSensitivityConfig:
    gold_rates: tuple[float, ...] = (
        0.002, 0.004, 0.006, 0.008, 0.010, 0.012
    )
    bitcoin_rates: tuple[float, ...] = (
        0.015, 0.020, 0.025, 0.030, 0.035
    )
    apply_to_buys: bool = True
    apply_to_sells: bool = True
    hold_signals_fixed: bool = True</code></pre></div><p>A decimal rate of <code>0.002</code> means 0.2%. Passing <code>0.2</code> would mean 20% and would be a different experiment.</p><p>There is an important current implementation limitation: <code>run_fee_sensitivity</code> looks for configuration attributes named <code>gold_fees</code> and <code>bitcoin_fees</code>, but <code>FeeSensitivityConfig</code> defines <code>gold_rates</code> and <code>bitcoin_rates</code>. As generated, custom configured grids are therefore not reliably consumed; the function can fall back to its module-level default grids. This naming mismatch must be repaired before claiming that arbitrary <code>FeeSensitivityConfig</code> values control the sweep.</p><h3>16.5 Fixed signals versus regenerated policy decisions</h3><p>Fee sensitivity can answer two different questions.</p><h4>Fixed-signal sensitivity</h4><p>Generate forecasts and orders once, then replay the same orders under each fee rate:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;f52193d7-4377-4b60-9b51-95d18539236b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">same data
same forecasts
same signals
same sizing schedule
fee rate changes
backtest reruns</code></pre></div><p>This isolates the direct accounting effect of fees.</p><h4>Fee-aware policy sensitivity</h4><p>Regenerate candidate actions for each fee rate when fees affect:</p><ul><li><p>whether an order is affordable;</p></li><li><p>whether its expected return exceeds its cost;</p></li><li><p>which greedy candidate has the highest score; or</p></li><li><p>whether exposure or risk constraints permit it.</p></li></ul><p>The current <code>run_fee_sensitivity</code> function accepts a <code>run_backtest</code> callback and passes <code>fee_rate</code> to that callback:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;ba66d656-3438-4df4-b2e9-79eed57127b7&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def run_fee_sensitivity(
    run_backtest: Callable[..., Any],
    *,
    asset: str,
    config: FeeSensitivityConfig | None = None,
    fees: Sequence[float] | None = None,
    base_kwargs: Mapping[str, Any] | None = None,
) -&gt; list[ExperimentResult]:</code></pre></div><p>This callback-based design is intentional infrastructure, not direct strategy integration. The callback must decide whether to reuse signals or regenerate them. The current function does <strong>not</strong> inspect or enforce <code>FeeSensitivityConfig.hold_signals_fixed</code>, and it does not currently record an explicit <code>signals_fixed</code> or <code>policy_regenerated</code> field. Its hardcoded metadata field <code>non_fee_assumptions_held_fixed=True</code> only states the intended sweep contract; it does not establish how the callback selected actions.</p><p>A repaired orchestration contract should record the policy mode explicitly, for example:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;dd77051f-77dd-48d3-b751-bf73630b8290&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">result.metadata.update(
    {
        "fee_rate": rate,
        "fee_percent": rate * 100.0,
        "signals_fixed": True,
        "policy_regenerated": False,
        "non_fee_assumptions_held_fixed": True,
    }
)</code></pre></div><p>For a fee-aware callback, the last two policy fields should instead indicate that decisions were regenerated. Until that metadata and configuration handling are aligned, fee-sweep results should be treated as callback outputs whose policy behavior must be inspected separately.</p><h3>16.6 Optional benchmark orchestration</h3><p><code>run_forecast_experiment(include_benchmarks=True)</code> is intended to request the optional benchmark suite. However, the generated call currently passes names such as <code>train_data</code> and <code>test_data</code>, while <code>run_benchmark_suite</code> requires <code>train_values</code> and <code>test_values</code>. The compatibility helper filters keyword arguments but cannot invent missing required parameter names. Consequently, the benchmark path may fail and be converted into a warning rather than producing benchmark results.</p><p>The required interface should be aligned explicitly, for example:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;8558fe0b-f85b-47ae-ae74-037cd80542f0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">benchmark = run_benchmark_suite(
    train_values=train_data,
    test_values=test_data,
    metadata={"asset": asset, "horizon": horizon},
)</code></pre></div><p>Until that repair is made, benchmark orchestration should be described as an intended dispatch path, not as a verified working connection. Optional dependency status and model semantics remain important: HMM Viterbi decoding is not automatically a price forecast, and XGBoost is gradient-boosted trees rather than a random forest. Benchmark metrics also require aligned targets, timestamps, preprocessing, and untouched test data.</p><h3>16.7 Recording results and paper claims</h3><p>The result layer can save local artifacts:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;da28ea9c-5bab-4e0f-84fe-d312782f255c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def save_local_results(
    results: ExperimentResult | Iterable[ExperimentResult],
    path: str | Path,
    *,
    format: str | None = None,
) -&gt; Path:</code></pre></div><p>JSON preserves nested predictions, warnings, and metadata. CSV is convenient for scalar comparison tables. Neither format establishes empirical validity.</p><p>The paper's headline outcomes are stored as claims rather than expected outputs:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;707ca046-bb7c-4b90-95d4-bee9d00b0f61&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">PAPER_REPORTED_VALUES = {
    "gold_final_value_usd": {
        "value": 646.0,
        "status": "reported_unverified_claim",
    },
    "bitcoin_final_value_usd": {
        "value": 215487.0,
        "status": "reported_unverified_claim",
    },
    "combined_final_value_usd": {
        "value": 216133.0,
        "status": "reported_unverified_claim",
    },
}</code></pre></div><p>These values must not be used as unit-test expectations. The original data, dates, signals, sizing equations, VaR settings, greedy rules, fee convention, and liquidation protocol are missing. Agreement with one number would not prove that the full method was reproduced; disagreement under a different local protocol would not by itself identify a coding error.</p><h3>16.8 What a trustworthy fee-sensitivity record should contain</h3><p>Each scenario should preserve at least:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!g9K1!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F10015f10-c7d8-4205-95b2-99cfcd494293_1236x666.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!g9K1!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F10015f10-c7d8-4205-95b2-99cfcd494293_1236x666.png 424w, https://substackcdn.com/image/fetch/$s_!g9K1!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F10015f10-c7d8-4205-95b2-99cfcd494293_1236x666.png 848w, https://substackcdn.com/image/fetch/$s_!g9K1!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F10015f10-c7d8-4205-95b2-99cfcd494293_1236x666.png 1272w, https://substackcdn.com/image/fetch/$s_!g9K1!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F10015f10-c7d8-4205-95b2-99cfcd494293_1236x666.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!g9K1!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F10015f10-c7d8-4205-95b2-99cfcd494293_1236x666.png" width="1236" height="666" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/10015f10-c7d8-4205-95b2-99cfcd494293_1236x666.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:666,&quot;width&quot;:1236,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:116265,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/206399283?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F10015f10-c7d8-4205-95b2-99cfcd494293_1236x666.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!g9K1!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F10015f10-c7d8-4205-95b2-99cfcd494293_1236x666.png 424w, https://substackcdn.com/image/fetch/$s_!g9K1!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F10015f10-c7d8-4205-95b2-99cfcd494293_1236x666.png 848w, https://substackcdn.com/image/fetch/$s_!g9K1!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F10015f10-c7d8-4205-95b2-99cfcd494293_1236x666.png 1272w, https://substackcdn.com/image/fetch/$s_!g9K1!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F10015f10-c7d8-4205-95b2-99cfcd494293_1236x666.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The current <code>summarize_results</code> function flattens scalar fields from result metrics, portfolio summaries, and metadata. <code>save_local_results</code> writes those records locally. Before relying on the files, inspect whether the callback supplied portfolio fields and whether policy metadata accurately describes the run.</p><h3>16.9 Practical checklist and current readiness</h3><p>Before preparing a forecast or fee experiment, record:</p><p>Local data path and provenance.</p><p>Asset, instrument, currency, frequency, and date range.</p><p>Window length, stride, target representation, and horizon.</p><p>Chronological or walk-forward boundaries.</p><p>Scaling method and fitting rows.</p><p>Model architecture, seed, and optional dependency versions.</p><p>Signal aggregation and thresholds.</p><p>Sizing schedule and exposure limits.</p><p>VaR confidence, horizon, lookback, and action mode.</p><p>Fee convention, slippage, execution timing, and liquidation policy.</p><p>Whether signals are fixed or regenerated for each fee scenario.</p><p>Forecast metrics and portfolio metrics in separate fields.</p><p>Any warnings caused by unavailable dependencies or API mismatches.</p><p>The generated project should currently be treated as a static, educational artifact rather than an executed experiment system. Known issues include the timestamp argument mismatch in <code>standardize_prices</code>, the benchmark argument-name mismatch, the unused <code>hold_signals_fixed</code> setting, and the fee-grid naming mismatch. Static checks also reported findings in <code>experiments.py</code>, while semantic code verification was skipped. Therefore, this section documents the intended orchestration contracts and the current limitations without claiming that the experiment paths were executed or that their outputs are correct.</p><h3>16.10 Interpreting the missing fee figures</h3><p>The fee grids can be reproduced as an experiment <strong>design</strong>. The paper's numerical curves cannot be recovered from the extracted text. Do not infer them from the headline profits, interpolate them from the listed percentages, or label a newly generated local curve as the paper's result.</p><p>A local result should instead be described as:</p><p>performance under the repository's documented reconstruction and local dataset</p><p>That wording preserves the value of sensitivity analysis while distinguishing it from empirical reproduction. The experiment layer's current contribution is reproducible configuration and result recording; direct end-to-end trading orchestration remains a repair task rather than an implemented fact.</p><h2>17. Run the Offline Workflow from the Command Line</h2><p>The project defines an offline command-line interface (CLI) for inspecting local data, generating synthetic demonstrations, and requesting research experiments. It does not connect to an exchange, download prices, store credentials, or submit orders.</p><p>The CLI should therefore be understood as a documented interface around the research modules, not as a verified end-to-end trading application. Several generated integrations currently have known incompatibilities. The examples below describe the intended command contracts and identify where the generated files still require repair. No command execution is claimed.</p><h3>17.1 Command registration and optional dependencies</h3><p>The shell command is registered in <code>pyproject.toml</code>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;toml&quot;,&quot;nodeId&quot;:&quot;85be3289-1549-443a-898b-2f5fd367f6af&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-toml">[project.scripts]
quant-trading-model = "quantitative_trading_model.cli:main"</code></pre></div><p>After installation, this maps <code>quant-trading-model</code> to <code>main()</code> in <code>src/quantitative_trading_model/cli.py</code>. The parser selects a subcommand and the handler produces local output.</p><p>Modeling dependencies are optional:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;toml&quot;,&quot;nodeId&quot;:&quot;8aedac15-6a0d-4194-9e51-0642e8dc15ae&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-toml">[project.optional-dependencies]
deep-learning = ["torch&gt;=2.0"]
classical = [
    "statsmodels&gt;=0.14",
    "pmdarima&gt;=2.0",
    "hmmlearn&gt;=0.3",
    "xgboost&gt;=2.0"
]</code></pre></div><p>The core data, streak, sizing, risk, and accounting modules use the lighter dependencies declared in the main project configuration. PyTorch and classical forecasting packages are imported only by features that need them.</p><p>The intended behavior is to report a missing optional dependency clearly. However, the generated <code>main()</code> catches <code>CLIError</code>, <code>FileNotFoundError</code>, <code>ValueError</code>, and <code>TypeError</code>, but not <code>ImportError</code>. Consequently, a missing PyTorch installation may still produce an uncaught traceback when a forecast command reaches the model layer. The CLI needs an explicit <code>except ImportError</code> branch before this behavior can be described as reliable shell-level error handling.</p><h3>17.2 Defined subcommands</h3><p><code>build_parser()</code> defines these subcommands:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!FofB!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d470a25-4a0c-4eb9-bda6-7ae64fe3e465_1412x578.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!FofB!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d470a25-4a0c-4eb9-bda6-7ae64fe3e465_1412x578.png 424w, https://substackcdn.com/image/fetch/$s_!FofB!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d470a25-4a0c-4eb9-bda6-7ae64fe3e465_1412x578.png 848w, https://substackcdn.com/image/fetch/$s_!FofB!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d470a25-4a0c-4eb9-bda6-7ae64fe3e465_1412x578.png 1272w, https://substackcdn.com/image/fetch/$s_!FofB!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d470a25-4a0c-4eb9-bda6-7ae64fe3e465_1412x578.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!FofB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d470a25-4a0c-4eb9-bda6-7ae64fe3e465_1412x578.png" width="1412" height="578" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8d470a25-4a0c-4eb9-bda6-7ae64fe3e465_1412x578.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:578,&quot;width&quot;:1412,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:149631,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/206399283?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d470a25-4a0c-4eb9-bda6-7ae64fe3e465_1412x578.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!FofB!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d470a25-4a0c-4eb9-bda6-7ae64fe3e465_1412x578.png 424w, https://substackcdn.com/image/fetch/$s_!FofB!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d470a25-4a0c-4eb9-bda6-7ae64fe3e465_1412x578.png 848w, https://substackcdn.com/image/fetch/$s_!FofB!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d470a25-4a0c-4eb9-bda6-7ae64fe3e465_1412x578.png 1272w, https://substackcdn.com/image/fetch/$s_!FofB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d470a25-4a0c-4eb9-bda6-7ae64fe3e465_1412x578.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Thus, the commands are <strong>statically specified interfaces</strong>, not all working workflows. A complete runtime path requires repairing the interfaces identified below.</p><p>The parser excerpt illustrates the command structure:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;63cb07db-1d50-4604-814e-15e952cb9c84&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def build_parser() -&gt; argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="quant-trading-model",
        description="Offline educational tools for the quantitative trading model paper.",
    )
    subparsers = parser.add_subparsers(dest="command", required=True)

    demo = subparsers.add_parser("demo")
    demo.add_argument("--rows", type=int, default=500)
    demo.add_argument("--seed", type=int, default=7)
    demo.add_argument("--asset", choices=("gold", "bitcoin"), default="gold")
    demo.set_defaults(handler=run_demo)

    # validate-data, forecast, backtest, and fee-sensitivity are
    # registered later in the same function.
    return parser</code></pre></div><p>This is an explanatory excerpt, not a complete replacement for <code>cli.py</code>.</p><h3>17.3 Local-only paths and data provenance</h3><p>The CLI includes path checks intended to reject URLs and network paths:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;35691b31-601c-4d35-8f1c-cbc5d53bf3a0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def _safe_input_path(value: str) -&gt; Path:
    if "://" in value:
        raise CLIError("Only local files are supported; URLs and network paths are not accepted")
    path = Path(value).expanduser()
    if not path.exists():
        raise CLIError(f"Input file does not exist: {path}")
    if not path.is_file():
        raise CLIError(f"Input path is not a regular file: {path}")
    return path.resolve()


def _safe_output_path(value: str) -&gt; Path:
    if "://" in value:
        raise CLIError("Output must be a local path, not a URL")
    path = Path(value).expanduser()
    if path.exists() and path.is_dir():
        raise CLIError(f"Output path is a directory: {path}")
    return path.resolve()</code></pre></div><p>These checks constrain file locations; they do not establish that a dataset matches the paper. Every local result should also record the source file, instrument, currency, frequency, date range, timezone, cleaning rules, and configuration snapshot.</p><p>There is a further generated-file inconsistency in <code>_load_local_data()</code>: it passes <code>asset_name=args.asset</code>, while the generated <code>load_price_csv()</code> function accepts <code>asset</code>, not <code>asset_name</code>. Depending on the compatibility wrapper, this can fail or cause the loader to retain its default asset label. The keyword must be aligned before asset-specific provenance can be trusted.</p><h3>17.4 Synthetic demonstration</h3><p>The intended synthetic command is:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:&quot;9fdaf686-9fc4-4276-808a-53faf0b86831&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">quant-trading-model demo \
  --rows 500 \
  --asset gold \
  --seed 7 \
  --output results/demo.json</code></pre></div><p><code>run_demo()</code> calls <code>generate_synthetic_prices()</code> and records a compact summary. Synthetic data are useful for teaching timestamp handling, window construction, tensor shapes, finite sizing, VaR decisions, and portfolio accounting. They are not historical gold or Bitcoin data and cannot reproduce the paper's reported outcomes.</p><p>The optional <code>--run-experiment</code> flag is currently not a complete path. The generator returns a <code>PriceData</code> object, and <code>run_demo()</code> passes that object to <code>run_forecast_experiment()</code>. In the generated <code>experiments.py</code>, <code>_price_array()</code> handles a pandas <code>Series</code>, a pandas <code>DataFrame</code>, or an array-like object, but does not handle <code>PriceData</code>. The experiment can therefore fail before window construction. The repair must either teach <code>_price_array()</code> to extract <code>PriceData.frame</code> or make the CLI pass a compatible DataFrame or Series.</p><p>This same <code>PriceData</code> mismatch affects the <code>forecast</code> handler, because <code>_load_local_data()</code> also returns <code>PriceData</code> and passes it to <code>run_forecast_experiment()</code>. These commands are therefore documented as intended interfaces until that data contract is repaired.</p><h3>17.5 Validate a local CSV</h3><p>The generated parser uses a positional CSV argument. A statically accurate example is:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:&quot;845cb841-4c7c-4098-bcf3-c98a78b15496&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">quant-trading-model validate-data data/gold.csv \
  --timestamp-column timestamp \
  --price-column close \
  --asset gold \
  --output results/gold_data_summary.json</code></pre></div><p><code>run_validate_data()</code> is intended to:</p><p>check the local path;</p><p>call <code>load_price_csv()</code>;</p><p>summarize the resulting object; and</p><p>print or write local JSON.</p><p>The loader requires parseable timestamps and finite, positive prices, and it sorts and deduplicates observations. The summary should be saved with a provenance record rather than treated as evidence that the data are the same data used by the paper.</p><p>Some older README examples use <code>--input data/gold.csv</code>, but the generated parser defines <code>csv</code> positionally and does not define an <code>--input</code> option. The positional form above matches the generated CLI more closely.</p><h3>17.6 Forecast command: intended interface and current limitation</h3><p>The intended command is:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:&quot;2250484b-b9e0-4476-a7d3-796abf07961b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">quant-trading-model forecast data/gold.csv \
  --timestamp-column timestamp \
  --price-column close \
  --asset gold \
  --output results/gold_forecast.json</code></pre></div><p>Optional benchmarks are requested with:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:&quot;e8507ab8-743d-4af2-94f5-1c8c1e5be647&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">quant-trading-model forecast data/gold.csv \
  --timestamp-column timestamp \
  --price-column close \
  --asset gold \
  --benchmarks \
  --output results/gold_forecast_with_benchmarks.json</code></pre></div><p>The intended data flow is:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;516b58ec-034c-462b-997d-9228fe98245a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">local CSV
  -&gt; local loader
  -&gt; standardized prices
  -&gt; 100-observation windows
  -&gt; chronological split
  -&gt; Att-BiLSTM or selected forecaster
  -&gt; forecast metrics
  -&gt; local result artifact</code></pre></div><p>The current generated path is not yet a verified runtime workflow. The CLI passes <code>PriceData</code> into <code>run_forecast_experiment()</code>, while <code>_price_array()</code> in <code>experiments.py</code> does not extract <code>PriceData</code>. This must be repaired before the command can reliably reach the forecasting model.</p><p>Even after that data mismatch is fixed, a missing PyTorch installation may escape through <code>main()</code> as an uncaught <code>ImportError</code>. The CLI should catch that exception and convert it into an actionable message. Until then, the optional-dependency behavior is a model-layer intention, not a guaranteed CLI behavior.</p><p>A repaired forecast artifact should include the asset, model, window length, horizon, split protocol, seed, preprocessing decisions, forecast metrics, package versions, and warnings. Forecast metrics remain separate from portfolio metrics and do not measure fees, turnover, drawdown, or liquidation.</p><h3>17.7 Backtest command: distinguish forecasting from portfolio simulation</h3><p>The intended command is:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:&quot;c8c0b2d7-0e96-4a60-8610-a249f99419c8&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">quant-trading-model backtest data/bitcoin.csv \
  --timestamp-column timestamp \
  --price-column close \
  --asset bitcoin \
  --output results/bitcoin_backtest.json</code></pre></div><p>In the generated CLI, <code>run_backtest_command()</code> calls <code>run_walk_forward_experiment()</code>. That function is intended to evaluate sequential forecasts; it does not, by itself, compose forecast signals, exponential sizing, VaR, greedy action selection, fills, fees, and portfolio valuation.</p><p>The repository does contain separate strategy and accounting modules. A complete portfolio backtest must explicitly connect them:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;5a3df806-a2a9-4bb0-856b-41f303c6f63b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">walk-forward forecast
  -&gt; signal generation
  -&gt; finite sizing
  -&gt; VaR filter
  -&gt; greedy action selection
  -&gt; next-bar order
  -&gt; portfolio simulator</code></pre></div><p>Therefore, the current <code>backtest</code> command is a statically described walk-forward entry point, not proof that a complete paper trading strategy is running. Its name should not be interpreted as evidence that the paper's missing execution protocol has been recovered.</p><h3>17.8 Fee-sensitivity command: parser exists, callback wiring is incomplete</h3><p>The paper-inspired fee grids are:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!7022!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e5b9cc7-4a51-4275-a7df-0bcfb0f338ef_1096x224.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!7022!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e5b9cc7-4a51-4275-a7df-0bcfb0f338ef_1096x224.png 424w, https://substackcdn.com/image/fetch/$s_!7022!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e5b9cc7-4a51-4275-a7df-0bcfb0f338ef_1096x224.png 848w, https://substackcdn.com/image/fetch/$s_!7022!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e5b9cc7-4a51-4275-a7df-0bcfb0f338ef_1096x224.png 1272w, https://substackcdn.com/image/fetch/$s_!7022!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e5b9cc7-4a51-4275-a7df-0bcfb0f338ef_1096x224.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!7022!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e5b9cc7-4a51-4275-a7df-0bcfb0f338ef_1096x224.png" width="1096" height="224" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4e5b9cc7-4a51-4275-a7df-0bcfb0f338ef_1096x224.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:224,&quot;width&quot;:1096,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:37524,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/206399283?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e5b9cc7-4a51-4275-a7df-0bcfb0f338ef_1096x224.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!7022!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e5b9cc7-4a51-4275-a7df-0bcfb0f338ef_1096x224.png 424w, https://substackcdn.com/image/fetch/$s_!7022!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e5b9cc7-4a51-4275-a7df-0bcfb0f338ef_1096x224.png 848w, https://substackcdn.com/image/fetch/$s_!7022!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e5b9cc7-4a51-4275-a7df-0bcfb0f338ef_1096x224.png 1272w, https://substackcdn.com/image/fetch/$s_!7022!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e5b9cc7-4a51-4275-a7df-0bcfb0f338ef_1096x224.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>The intended command is:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:&quot;b7c2d34c-a93a-4742-a9bf-d6fac483c694&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">quant-trading-model fee-sensitivity data/gold.csv \
  --timestamp-column timestamp \
  --price-column close \
  --asset gold \
  --output results/gold_fee_sensitivity.json</code></pre></div><p>The command is currently only a parser-level interface. <code>run_fee_sensitivity()</code> in <code>experiments.py</code> requires a positional <code>run_backtest</code> callback, followed by the asset and fee configuration. However, <code>run_fee_sensitivity_command()</code> calls it without supplying that required callback. As written, the handler cannot successfully invoke the fee experiment.</p><p>A repair must construct or inject a callback that accepts <code>fee_rate</code>, runs the same strategy and data under that fee, and returns portfolio results. Only then can the CLI perform a sensitivity sweep. The callback should hold all non-fee assumptions fixed: data, dates, forecasts, signal rules, sizing, slippage, initial cash, and liquidation. If fees change feasibility or greedy ranking, decisions must be regenerated and that dependency recorded.</p><p>The missing numeric values in the paper's fee figures must not be inferred from the fee grid itself.</p><h3>17.9 Optional dependency installation</h3><p>The intended installation commands are:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:&quot;399e026e-839d-4252-8720-044aae7306ba&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">python -m pip install -e .
python -m pip install -e '.[deep-learning]'
python -m pip install -e '.[classical]'</code></pre></div><p>The generated <code>pyproject.toml</code> defines the <code>classical</code> extra. Some README text refers to a <code>benchmarks</code> extra, which is inconsistent; <code>classical</code> is the authoritative generated metadata name.</p><p>A missing optional package should ideally produce a message such as:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;ea9f3dc5-5311-4f28-8b0e-79ac7db234eb&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">PyTorch is required for recurrent forecasters. Install the optional deep-learning dependencies for this project.</code></pre></div><p>At present, the model layer can raise that message, but the CLI's <code>main()</code> does not catch <code>ImportError</code>. Repairing the exception handling is necessary for the message to be consistently presented as a normal command-line error.</p><h3>17.10 Archive results with provenance</h3><p>The CLI's output helper is intended to print JSON or write it to a local file:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;15e4c981-a435-4456-856f-c50f81a3bd60&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def _write_output(value: Any, output: str | None) -&gt; None:
    rendered = _dump(value)
    if output is None:
        print(rendered)
        return
    path = _safe_output_path(output)
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(rendered + "\n", encoding="utf-8")
    print(f"Wrote {path}")</code></pre></div><p>A useful research directory might contain:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;3e2d5614-b063-40e6-a7ce-128318e9756b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">results/
&#9500;&#9472;&#9472; gold_data_summary.json
&#9500;&#9472;&#9472; gold_forecast.json
&#9500;&#9472;&#9472; gold_fee_sensitivity.json
&#9500;&#9472;&#9472; config_snapshot.json
&#9500;&#9472;&#9472; package_versions.txt
&#9492;&#9472;&#9472; provenance.md</code></pre></div><p>The configuration snapshot should include the window length, target horizon, scaler policy, model settings, fee rate, slippage, VaR settings, sizing schedule, signal thresholds, execution timing, and liquidation policy. The provenance file should identify the local data file and instrument metadata.</p><p>The paper's approximate <code>$646</code> gold value and <code>$215,487</code> Bitcoin value remain unverified claims. A local JSON file without dates, data provenance, and execution assumptions cannot validate either number.</p><h3>17.11 Offline workflow checklist</h3><p>The intended research workflow is:</p><p>Install only the dependencies required for the selected module.</p><p>Inspect <code>quant-trading-model --help</code> and the relevant subcommand help.</p><p>Use <code>demo</code> to inspect synthetic data flow, without treating it as empirical evidence.</p><p>Use <code>validate-data</code> on each local asset file.</p><p>Save the data summary and provenance metadata.</p><p>Run a chronological forecast experiment after repairing the <code>PriceData</code> handoff.</p><p>Record model status, target horizon, metrics, seed, and configuration.</p><p>Run walk-forward evaluation for trading-oriented conclusions.</p><p>Connect forecasts explicitly to signals, sizing, VaR, greedy selection, and next-bar execution for a portfolio backtest.</p><p>Supply a valid backtest callback before using fee sensitivity.</p><p>Compare portfolio results with buy-and-hold and report drawdown, volatility, turnover, and fees.</p><p>Label every result as implemented, reconstructed, illustrative, unavailable, or reported claim as appropriate.</p><p>The generated CLI and orchestration files also have known static-review findings: placeholder detection was reported for <code>cli.py</code>, and semantic code verification was skipped. Some generated tests and orchestration calls are API-inconsistent with the generated modules. These findings reinforce the correct interpretation of this section: it documents intended offline interfaces and their current repair requirements; it does not claim that any command ran successfully.</p><p>The central lesson is that a CLI can make a research workflow repeatable in form, but it cannot supply missing data or algorithmic details. Reliable interpretation still requires local data provenance, explicit configuration, causal signal timing, repaired module interfaces, and a strict distinction between computed results and the paper's unverified claims.</p><h2>18. Verification: Static, Semantic, and Invariant Checks</h2><p>A paper-to-code project needs more than a collection of source files. It also needs a disciplined way to check whether the implementation is structurally coherent, whether its assumptions are visible, and whether its financial logic avoids obvious look-ahead errors. At the same time, verification must not be overstated. A file that parses successfully is not necessarily semantically correct, and a passing unit test would not prove that the paper's reported profits are reproducible.</p><p>For this project, verification has three layers:</p><p><strong>Static verification</strong> checks source structure without running the research pipeline.</p><p><strong>Semantic review</strong> checks whether the implementation matches the intended mathematics and data flow.</p><p><strong>Empirical validation</strong> would compare executed results with the paper, but it is currently unavailable because the original data and protocol are missing.</p><p>The supplied local verification performed the first layer and recorded a partial result. Semantic code verification was explicitly skipped. Therefore, the discussion below describes the intended checks and the reported findings; it does not claim that the generated code was executed or that the test suite passed.</p><h3>18.1 Static verification: checking structure without execution</h3><p>Static checks inspect files as text or syntax trees. They are useful because they can identify malformed Python, missing symbols, accidental placeholders, unsafe patterns, and documentation inconsistencies before a runtime experiment is attempted.</p><p>The local verification report applied checks such as:</p><ul><li><p>non-empty file validation;</p></li><li><p>detection of Markdown fences in generated code fields;</p></li><li><p>placeholder and TODO detection;</p></li><li><p>checks for live-trading patterns;</p></li><li><p>generic text checks;</p></li><li><p>Python AST parsing for Python files.</p></li></ul><p>AST parsing is stronger than looking for balanced parentheses with a text search. It asks Python's parser whether a file has valid syntax and produces a structured representation of imports, classes, functions, and statements. It does not, however, prove that imported names exist at runtime or that two modules agree on an API.</p><p>The static checks reported successful parsing and structural checks for the core modules, including:</p><ul><li><p><code>data.py</code>, which constructs windows and chronological partitions;</p></li><li><p><code>metrics.py</code>, which separates forecast and portfolio measures;</p></li><li><p><code>models/attention.py</code> and <code>models/forecasters.py</code>, which define the tensor-facing model interfaces;</p></li><li><p><code>streaks.py</code>, <code>sizing.py</code>, and <code>risk.py</code>, which implement the reconstructed analytical components;</p></li><li><p><code>strategy.py</code> and <code>backtest.py</code>, which define candidate actions and portfolio accounting; and</p></li><li><p>the generated test files.</p></li></ul><p>That result gives useful confidence that these files are non-empty and syntactically parseable. It does not establish that the modules can be imported together, that optional dependencies are available, or that the implementation behaves correctly for real data.</p><h3>18.2 Reported static-check findings</h3><p>The overall local static-verification result was <strong>not passing</strong>. Four generated files were reported with issues:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!dQY9!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb85f769-b559-4068-9d43-58974c21fff8_1402x912.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!dQY9!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb85f769-b559-4068-9d43-58974c21fff8_1402x912.png 424w, https://substackcdn.com/image/fetch/$s_!dQY9!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb85f769-b559-4068-9d43-58974c21fff8_1402x912.png 848w, https://substackcdn.com/image/fetch/$s_!dQY9!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb85f769-b559-4068-9d43-58974c21fff8_1402x912.png 1272w, https://substackcdn.com/image/fetch/$s_!dQY9!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb85f769-b559-4068-9d43-58974c21fff8_1402x912.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!dQY9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb85f769-b559-4068-9d43-58974c21fff8_1402x912.png" width="1402" height="912" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/db85f769-b559-4068-9d43-58974c21fff8_1402x912.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:912,&quot;width&quot;:1402,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:207931,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/206399283?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb85f769-b559-4068-9d43-58974c21fff8_1402x912.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!dQY9!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb85f769-b559-4068-9d43-58974c21fff8_1402x912.png 424w, https://substackcdn.com/image/fetch/$s_!dQY9!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb85f769-b559-4068-9d43-58974c21fff8_1402x912.png 848w, https://substackcdn.com/image/fetch/$s_!dQY9!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb85f769-b559-4068-9d43-58974c21fff8_1402x912.png 1272w, https://substackcdn.com/image/fetch/$s_!dQY9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb85f769-b559-4068-9d43-58974c21fff8_1402x912.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The Markdown findings are format warnings in the generated-file representation. A Markdown tutorial normally uses fenced code blocks, so removing every fence would make the document less useful to readers. The important distinction is between a validation rule for a generated <code>code</code> field and the normal syntax of a Markdown document.</p><p>The placeholder findings are more substantive. They should be reviewed manually because a placeholder can mean anything from an intentionally unfinished branch to an incomplete implementation. Until that review and repair occur, the experiment and CLI layers should be described as planned or partially integrated artifacts rather than as verified end-to-end interfaces.</p><h3>18.3 Semantic review: checking meaning, not just syntax</h3><p>Semantic verification asks whether the code's operations match the intended method. For this project, the most important semantic questions are temporal, mathematical, and accounting-related.</p><h4>Temporal and data invariants</h4><p>The windowing and data tests are intended to review the following properties:</p><ul><li><p>timestamps are strictly increasing after cleaning;</p></li><li><p>prices are finite and positive;</p></li><li><p>a target timestamp occurs after the final timestamp in its input window;</p></li><li><p>chronological training and test partitions remain ordered;</p></li><li><p>scaling parameters are fitted only on training observations; and</p></li><li><p>no forecast uses a target or realized return from the future.</p></li></ul><p>These checks matter because a time-series implementation can be syntactically valid while still leaking information. For example, fitting a scaler on the complete series or randomly mixing overlapping windows can make a forecast evaluation look stronger than a deployment-like evaluation.</p><p>The test file <code>tests/test_data_and_streaks.py</code> is designed around small hand-calculated paths. That is a good verification strategy for boundaries: a short sequence makes it possible to calculate the expected target values and return signs manually. It is more informative for temporal invariants than a large opaque fixture.</p><h4>Attention and model-shape invariants</h4><p>The model tests are intended to review tensor contracts rather than predictive quality. The key conditions are:</p><ul><li><p>recurrent inputs have shape <code>(batch, time, features)</code>;</p></li><li><p>attention scores have shape <code>(batch, time)</code>;</p></li><li><p>attention weights are nonnegative;</p></li><li><p>weights sum to one across the time dimension;</p></li><li><p>the context vector has shape <code>(batch, features)</code>;</p></li><li><p>one-step and three-step heads return the configured horizon; and</p></li><li><p>the primary model contains the configured 20% dropout layer.</p></li></ul><p>The attention normalization check is particularly important. Applying softmax across features instead of time would produce a different algorithm from the paper's temporal attention equations. Shape tests can catch that kind of error without claiming that the model has learned a useful forecast.</p><p>The model tests also need PyTorch-aware handling. PyTorch is optional in this project, so a minimal installation should skip tensor-execution tests cleanly rather than fail while importing unrelated data or accounting modules. A skipped optional test is not a passing model validation; it means that the environment did not perform that check.</p><h4>Streak and return invariants</h4><p>The streak tests review the reconstruction of the paper's consecutive-rise and decline analysis:</p><ul><li><p>the default simple return uses the previous price as denominator;</p></li><li><p>zero returns do not silently become rises or declines;</p></li><li><p>maximal runs and overlapping subsequences produce different, intentional counts;</p></li><li><p>streak counts are nonnegative integers; and</p></li><li><p>percentile calculations use only the supplied historical segment.</p></li></ul><p>These checks are important because the paper's use of the term &#8220;Apriori&#8221; is not operationally supported by frequent-itemset support or confidence calculations. The implementation should therefore be reviewed as streak analysis, with its chosen return and zero policies recorded explicitly.</p><h4>Position-sizing invariants</h4><p>The sizing tests should confirm that the normalized exponential reconstruction remains bounded:</p><ul><li><p>every score and allocation is finite and nonnegative;</p></li><li><p>normalized weights sum to one within tolerance;</p></li><li><p>the planned allocations do not exceed the schedule budget;</p></li><li><p>an addition level cannot be consumed twice accidentally;</p></li><li><p>the approved amount cannot exceed available cash; and</p></li><li><p>the exposure cap prevents unlimited averaging down.</p></li></ul><p>These are safety and consistency properties of the reconstruction. They do not prove that the paper's corrupted equations (5)&#8211;(8) were recovered correctly. In fact, exact verification of those equations remains unavailable until the original notation or equation images are supplied.</p><h4>VaR and strategy invariants</h4><p>The risk and strategy layers require a different kind of semantic review. The intended checks include:</p><ul><li><p>VaR uses only returns available before the decision timestamp;</p></li><li><p>insufficient history is represented explicitly;</p></li><li><p>a blocked trade receives zero approved quantity;</p></li><li><p>risk scaling never increases the proposed order;</p></li><li><p>candidate scores use forecasts, current portfolio state, fees, and constraints only;</p></li><li><p>infeasible actions are excluded before ranking; and</p></li><li><p>greedy tie-breaking is deterministic.</p></li></ul><p>These conditions prevent a risk filter from becoming decorative. A VaR calculation that uses future returns, or a strategy score that accidentally includes realized profit, would invalidate the backtest even if every function returns a number.</p><p>The paper does not define its VaR parameters or greedy objective. Consequently, semantic verification can check the internal consistency of the chosen reconstruction, but it cannot check fidelity to an unavailable original algorithm.</p><h4>Portfolio-accounting invariants</h4><p>The backtest tests are designed around hand-computable transactions. The central identity is:</p><p>\[ V<em>t = \operatorname{cash}</em>t + \operatorname{quantity}<em>t p</em>t. \]</p><p>The intended accounting checks include:</p><ul><li><p>every fill occurs after its signal timestamp;</p></li><li><p>buy and sell fees are applied exactly once to executed notional;</p></li><li><p>unaffordable buys do not create negative cash under no-leverage settings;</p></li><li><p>holdings do not become negative when shorting is disabled;</p></li><li><p>marked portfolio value equals cash plus marked holdings;</p></li><li><p>liquidation is explicit; and</p></li><li><p>the buy-and-hold benchmark follows a documented fee and timing convention.</p></li></ul><p>These tests verify the simulator's stated accounting contract. They do not verify the paper's accounting because the paper does not specify its execution dates, fee-side convention, slippage, partial-unit rules, or liquidation policy.</p><h3>18.4 Why the skipped semantic verification matters</h3><p>The semantic verification report states that code semantic verification was skipped using the <code>--skip-code-semantic-verification</code> option. Its overall assessment was therefore &#8220;not semantically reviewed.&#8221; This is an important limitation, not a minor footnote.</p><p>Static AST parsing can show that Python syntax is valid. It cannot reliably detect every issue that a semantic review would investigate, such as:</p><ul><li><p>a caller passing <code>split_config</code> to a function that accepts only <code>config</code>;</p></li><li><p>a test constructing a class with fields that differ from the generated class;</p></li><li><p>a benchmark adapter assuming a metric method that another module does not expose;</p></li><li><p>a forecast output using a different shape from the target array; or</p></li><li><p>an experiment function describing end-to-end backtesting while only orchestrating forecasts.</p></li></ul><p>The supplied project notes also warn that some generated tests and orchestration code appear API-inconsistent with the generated modules. That means the tests should be treated as intended invariant specifications and static artifacts until their interfaces are reconciled. No claim should be made that the test suite passes.</p><h3>18.5 Verification is not execution and execution is not reproduction</h3><p>There are three separate statements that should not be conflated:</p><p><strong>The source parses.</strong> Python's parser accepted the file structure.</p><p><strong>The implementation is semantically coherent.</strong> Functions, types, formulas, and timestamps agree with one another under review.</p><p><strong>The paper's experiment is reproduced.</strong> The implementation matches the original data, protocol, and reported results.</p><p>The available evidence supports only parts of the first statement. The second was not completed by the supplied semantic verifier. The third is impossible at present because the paper's data and several algorithmic details are missing.</p><p>In particular, this tutorial does not claim that:</p><ul><li><p>the generated code was executed;</p></li><li><p>all dependencies were installed;</p></li><li><p>the tests passed;</p></li><li><p>the CLI completed a forecast or backtest;</p></li><li><p>the reported <code>$646</code> gold value or <code>$215,487</code> Bitcoin value was reproduced; or</p></li><li><p>the paper's benchmark metrics or fee-sensitivity curves were confirmed.</p></li></ul><p>The correct interpretation is narrower: the project contains intended implementations, tests, and verification rules, while the local static report identifies both successful structural checks and unresolved findings.</p><h3>18.6 A practical review order</h3><p>When repairing or extending the project, review the layers in this order:</p><p><strong>Resolve format and placeholder findings.</strong> Inspect <code>experiments.py</code> and <code>cli.py</code>; decide whether each detected placeholder is intentional or incomplete. Treat the Markdown-fence findings as representation warnings where fences are normal Markdown syntax.</p><p><strong>Reconcile public APIs.</strong> Compare function signatures and dataclass fields across source modules and tests. In particular, check the experiment-to-data, strategy-to-risk, and test-to-sizer interfaces.</p><p><strong>Run syntax and import checks in the target environment.</strong> Confirm that core imports do not require optional PyTorch or benchmark packages, and that optional failures are explicit.</p><p><strong>Review data boundaries.</strong> Hand-check window endpoints, target timestamps, scaler fitting, and chronological splits.</p><p><strong>Review tensor shapes.</strong> Confirm attention normalization across time and forecast-target horizon agreement.</p><p><strong>Review pure analytical components.</strong> Check return denominators, streak policies, normalized sizing, and VaR sign conventions with small numerical examples.</p><p><strong>Review portfolio accounting.</strong> Reconcile every fill, fee, cash change, holding quantity, and mark-to-market value.</p><p><strong>Only then run local experiments.</strong> Preserve configuration, data provenance, software versions, warnings, and result metadata.</p><p><strong>Compare with the paper cautiously.</strong> Label any mismatch as unresolved unless the original data and protocol are available.</p><p>This order moves from inexpensive structural checks to more demanding semantic and empirical work. It also prevents a striking backtest number from distracting attention from basic timestamp or accounting errors.</p><h3>18.7 Reproduction status remains a separate question</h3><p>The reproduction matrix in <code>docs/reproduction_matrix.md</code> is the appropriate place to track paper fidelity. It distinguishes direct mechanics&#8212;such as temporal attention equations, window construction, and portfolio identities&#8212;from reconstructions such as normalized exponential sizing, historical VaR, and greedy action selection.</p><p>A useful final status summary is:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!BWID!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F669e00cd-16dc-4efb-a7de-5d2d8087fd0a_1412x684.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!BWID!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F669e00cd-16dc-4efb-a7de-5d2d8087fd0a_1412x684.png 424w, https://substackcdn.com/image/fetch/$s_!BWID!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F669e00cd-16dc-4efb-a7de-5d2d8087fd0a_1412x684.png 848w, https://substackcdn.com/image/fetch/$s_!BWID!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F669e00cd-16dc-4efb-a7de-5d2d8087fd0a_1412x684.png 1272w, https://substackcdn.com/image/fetch/$s_!BWID!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F669e00cd-16dc-4efb-a7de-5d2d8087fd0a_1412x684.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!BWID!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F669e00cd-16dc-4efb-a7de-5d2d8087fd0a_1412x684.png" width="1412" height="684" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/669e00cd-16dc-4efb-a7de-5d2d8087fd0a_1412x684.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:684,&quot;width&quot;:1412,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:134565,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/206399283?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F669e00cd-16dc-4efb-a7de-5d2d8087fd0a_1412x684.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!BWID!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F669e00cd-16dc-4efb-a7de-5d2d8087fd0a_1412x684.png 424w, https://substackcdn.com/image/fetch/$s_!BWID!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F669e00cd-16dc-4efb-a7de-5d2d8087fd0a_1412x684.png 848w, https://substackcdn.com/image/fetch/$s_!BWID!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F669e00cd-16dc-4efb-a7de-5d2d8087fd0a_1412x684.png 1272w, https://substackcdn.com/image/fetch/$s_!BWID!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F669e00cd-16dc-4efb-a7de-5d2d8087fd0a_1412x684.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The purpose of this verification process is therefore not to manufacture certainty. It is to make uncertainty inspectable: readers can see which files parse, which invariants are intended, which findings remain unresolved, and which paper claims cannot yet be tested. That is the appropriate standard for an educational implementation of an incompletely specified quantitative-trading paper.</p><h2>19. Worked Synthetic Example from Price Path to Portfolio Record</h2><p>This section connects the data, streak, sizing, risk, strategy, and accounting layers with a short synthetic price path:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;6daf763a-79a6-40a3-9583-6226e0b8ce9b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">prices: 100, 102, 101, 99, 100, 103</code></pre></div><p>The example follows this causal sequence:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;d370ae09-6fda-462f-a940-24f3f0006883&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">prices
  &#8594; standard returns and streaks
  &#8594; manually supplied forecast
  &#8594; finite position-size schedule
  &#8594; VaR decision
  &#8594; greedy candidate selection
  &#8594; next-bar fill
  &#8594; portfolio valuation</code></pre></div><p>This is an <strong>illustrative interface example</strong>, not an empirical backtest. The prices are synthetic and the forecast is supplied manually rather than produced by a trained model. The paper's exact position-sizing equations, VaR procedure, and greedy policy are unavailable, so those components remain documented reconstructions. The code was not executed here; the calculations below describe the intended contracts and hand-checkable accounting.</p><h3>19.1 Prepare the synthetic prices</h3><p>The data layer accepts local historical data. For a small example, create a pandas <code>Series</code> and convert it into the standard timestamp-and-price table:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;7b0df02e-0e8c-4fdb-99dd-06e03f008585&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">import pandas as pd

prices = pd.Series(
    [100.0, 102.0, 101.0, 99.0, 100.0, 103.0],
    index=pd.date_range("2024-01-01", periods=6, freq="D"),
    name="price",
)

from quantitative_trading_model.data import standardize_prices

clean = standardize_prices(
    prices.rename_axis("timestamp").reset_index(),
    timestamp_column="timestamp",
    price_column="price",
    asset="synthetic",
)</code></pre></div><p><code>standardize_prices</code> parses timestamps, sorts observations, removes invalid rows when configured to do so, resolves duplicate timestamps, and returns a <code>PriceData</code> object. Prices must be finite and strictly positive. The function does not identify a market-data source or make network requests.</p><p>Six observations are far shorter than the paper-inspired 100-observation forecasting window. This path is therefore unsuitable for training the Att-BiLSTM. It is only long enough to demonstrate return alignment, decision timing, and portfolio accounting.</p><h3>19.2 Compute standard returns</h3><p>The default reconstruction uses the conventional simple return:</p><p>\[ r<em>t = \frac{p</em>t-p<em>{t-1}}{p</em>{t-1}}. \]</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;337dcd80-1cfd-4dba-b8cd-e6e982d9797c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from quantitative_trading_model.streaks import compute_returns

returns = compute_returns(clean.prices, denominator="previous")</code></pre></div><p>The hand calculations are:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!uC99!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc247d11c-ac66-460d-b842-32ff8bd25123_558x412.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!uC99!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc247d11c-ac66-460d-b842-32ff8bd25123_558x412.png 424w, https://substackcdn.com/image/fetch/$s_!uC99!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc247d11c-ac66-460d-b842-32ff8bd25123_558x412.png 848w, https://substackcdn.com/image/fetch/$s_!uC99!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc247d11c-ac66-460d-b842-32ff8bd25123_558x412.png 1272w, https://substackcdn.com/image/fetch/$s_!uC99!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc247d11c-ac66-460d-b842-32ff8bd25123_558x412.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!uC99!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc247d11c-ac66-460d-b842-32ff8bd25123_558x412.png" width="558" height="412" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c247d11c-ac66-460d-b842-32ff8bd25123_558x412.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:412,&quot;width&quot;:558,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:40950,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/206399283?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc247d11c-ac66-460d-b842-32ff8bd25123_558x412.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!uC99!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc247d11c-ac66-460d-b842-32ff8bd25123_558x412.png 424w, https://substackcdn.com/image/fetch/$s_!uC99!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc247d11c-ac66-460d-b842-32ff8bd25123_558x412.png 848w, https://substackcdn.com/image/fetch/$s_!uC99!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc247d11c-ac66-460d-b842-32ff8bd25123_558x412.png 1272w, https://substackcdn.com/image/fetch/$s_!uC99!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc247d11c-ac66-460d-b842-32ff8bd25123_558x412.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Each return is aligned to the later price. Thus, the return from 101 to 99 becomes known at the 99-price observation. It must not be assigned to the earlier 101 timestamp when constructing an online risk history.</p><p>The extracted paper equation appears to use the current price as the denominator. That alternative is available through <code>denominator="current"</code> or <code>denominator="paper"</code>, but one convention must be used consistently throughout a given experiment.</p><h3>19.3 Classify movements and identify streaks</h3><p>The sign sequence is:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;0248ac5f-6072-457e-8c0c-2154ed0d985a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">+  -  -  +  +</code></pre></div><p>The streak layer separates calculation, classification, and counting:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;4bf63827-3689-4505-9925-d29af15c2b5f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from quantitative_trading_model.streaks import classify_returns, count_streaks

labels = classify_returns(returns, zero_policy="neutral")

rise_counts = count_streaks(
    labels,
    value=1,
    mode="maximal",
    minimum_length=1,
)
decline_counts = count_streaks(
    labels,
    value=-1,
    mode="maximal",
    minimum_length=1,
)</code></pre></div><p>The maximal runs are:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;aa252bbe-72e6-4f95-99c6-4cc1ffa1d443&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">positive run of length 1
negative run of length 2
positive run of length 2</code></pre></div><p>Conceptually, the results are:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;81e52354-a740-410a-aeb6-f694384bbd05&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">rise_streaks:    {1: 1, 2: 1}
decline_streaks: {2: 1}</code></pre></div><p><code>count_streaks</code> also supports <code>mode="overlapping"</code>. A run of four positive returns contains three overlapping two-return subsequences, whereas maximal mode records one complete run of length four. These statistics are different, and the paper does not specify which one it used.</p><p>A complete summary can be requested with:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;67f618d7-a202-4996-82e3-efd2e67b60e3&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from quantitative_trading_model.streaks import compute_streak_statistics

stats = compute_streak_statistics(
    clean.prices,
    denominator="previous",
    zero_policy="neutral",
    streak_mode="maximal",
)

print(stats.gain_percentile_90)
print(stats.decline_percentile_10)
print(stats.rise_streaks)
print(stats.decline_streaks)</code></pre></div><p>The summary exposes the 90th-percentile gain, signed 10th-percentile decline, median gain, and decline magnitudes. With only five returns, these estimates are not statistically meaningful. In a real walk-forward experiment, the statistics must be calculated from observations available before the relevant decision.</p><p>This implementation calls the procedure <strong>streak analysis</strong> or <strong>sequential-pattern analysis</strong>, not standard Apriori. The paper does not provide Apriori itemsets, support, or confidence calculations.</p><h3>19.4 Build and consume a finite sizing level</h3><p>The paper's equations (5)&#8211;(8) are incomplete in the extracted source. The implementation therefore uses a normalized exponential reconstruction. Suppose the schedule has a <code>$300</code> budget and three addition levels:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;ccc6f3d0-da63-4695-969d-21bdd1eccade&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from quantitative_trading_model.sizing import (
    ExponentialPositionSizer,
    build_exponential_schedule,
)

schedule = build_exponential_schedule(
    budget=300.0,
    max_additions=3,
    growth=0.50,
    amplitude=1.0,
)
sizer = ExponentialPositionSizer(schedule=schedule)

print(schedule.weights)
print(schedule.allocations)</code></pre></div><p>The reconstructed scores have the form:</p><p>si=Aexp&#8289;(Bi),</p><p>followed by normalization:</p><p>\[ w<em>i=\frac{s</em>i}{\sum<em>j s</em>j}, \qquad \operatorname{allocation}<em>i=300w</em>i. \]</p><p>For indices <code>i = 0, 1, 2</code>, <code>A = 1</code>, and <code>B = 0.5</code>, the scores are approximately:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;f428c096-70f3-4beb-86aa-6997efe77fce&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">1.0000, 1.6487, 2.7183</code></pre></div><p>The resulting planned allocations are approximately:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;0e17515d-23e7-4cba-b0b2-cdcaaea1841b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">level 1: $55.89
level 2: $92.16
level 3: $151.95</code></pre></div><p>The first schedule level is not merely described as selected: it is connected to the strategy through <code>ExponentialPositionSizer</code>. The strategy uses one-based addition levels in its public action record, while the schedule stores zero-based Python indices internally. After the action is approved, the selected level is consumed exactly once:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;ec987b11-fc28-4ebd-a72a-4a9f9dac779f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">allocation = sizer.allocate_next_addition(
    available_cash=500.0,
    current_exposure=0.0,
)

print(allocation.level)          # first internal level is 0
print(allocation.approved_amount)</code></pre></div><p>The allocation cannot exceed available cash or the configured exposure capacity. This is a finite schedule, not unlimited averaging down. The formula and any calibration from gains, declines, or fees are reconstructions rather than faithful transcriptions of the paper's unreadable recursive break-even equations.</p><h3>19.5 Produce a forecast-derived signal</h3><p>Assume that when the observed price is 99, a forecasting model predicts a one-step price of 101. The forecast is manually supplied for this example:</p><p>rforecast=10199&#8722;1&#8776;0.020202.</p><p>That is an expected increase of about 2.02%.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;369a2173-c4ea-43b9-a1bd-31d80b57327f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from quantitative_trading_model.strategy import SignalGenerator

signal_generator = SignalGenerator(
    buy_threshold=0.01,
    sell_threshold=0.01,
    aggregation="terminal",
)

decision = signal_generator.decide(
    current_price=99.0,
    forecasts=[101.0],
)

print(decision.signal)
print(decision.expected_return)</code></pre></div><p>The expected increase exceeds the 1% buy threshold, so the intended signal is <code>Signal.BUY</code>. <code>SignalGenerator.decide</code> uses only the current price and forecast. It does not receive the later realized prices of 100 or 103.</p><p>For a three-day forecast, the same class can aggregate values such as <code>[100.0, 101.0, 102.0]</code>. The default <code>terminal</code> policy uses the final forecast; <code>mean</code> and <code>max</code> are also available. The paper mentions three-day forecasts but does not define this aggregation rule, so it remains an explicit reconstruction choice.</p><h3>19.6 Apply historical VaR before ranking actions</h3><p>The paper names VaR but does not specify its confidence level, horizon, lookback, or action rule. This example uses a rolling historical estimator with enough pre-decision observations to produce an estimate:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;4d5436f2-074c-4ed0-8911-c040fbd74a67&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from quantitative_trading_model.config import VaRConfig
from quantitative_trading_model.risk import HistoricalVaR, VaRTradeFilter

var_config = VaRConfig(
    enabled=True,
    confidence=0.95,
    horizon=1,
    lookback=100,
    min_observations=3,
    action="scale",
    insufficient_history="allow",
    max_var_fraction=0.05,
)

var_estimator = HistoricalVaR(var_config)
var_filter = VaRTradeFilter(config=var_config)</code></pre></div><p>At the 99-price decision point, the returns known from the path are the first three values:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;ebd470b2-f2a2-4f21-8bdc-1d2f265b28b6&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">pre_decision_returns = returns[:3]
estimate = var_estimator.estimate(pre_decision_returns)
print(estimate.var)
print(estimate.return_quantile)</code></pre></div><p>The estimator now has three observations, matching the configured minimum. This is still only a toy quantile estimate; it is not a meaningful risk model. If the lower-tail return quantile were <code>-1.8%</code>, an exposure of <code>$55.89</code> would imply an estimated loss of roughly:</p><p>55.89&#215;0.018&#8776;$1.01.</p><p>The actual estimate should be read from <code>estimate</code>, not inferred from this hypothetical number.</p><p>The risk decision uses currency notional units:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;01b7783b-c6e3-4c0e-b0e1-c0849e9eba96&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">risk_decision = var_filter.decide(
    proposed_quantity=55.89,
    current_exposure=0.0,
    portfolio_value=500.0,
    returns=pre_decision_returns,
)

print(risk_decision.action)
print(risk_decision.approved_quantity)
print(risk_decision.reason)</code></pre></div><p>Here, <code>proposed_quantity</code> is the VaR module's name for a proposed currency notional. It is not asset quantity. A positive approved amount means the risk filter accepted or scaled the proposed notional; a blocked trade has an approved amount of zero. The filter uses only the supplied pre-decision returns.</p><h3>19.7 Rank the forecast and sizing candidates</h3><p>The sizing schedule and risk decision are now connected explicitly. The risk-approved amount is passed as the maximum buy notional, while the sizer supplies the next addition level:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;5699f371-5329-4d34-937e-ea38813908b0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from quantitative_trading_model.strategy import GreedyPolicy, PortfolioSnapshot

approved_notional = float(risk_decision.approved_quantity)

snapshot = PortfolioSnapshot(
    cash=500.0,
    holdings=0.0,
    price=99.0,
    addition_level=0,
    portfolio_value=500.0,
)

policy = GreedyPolicy(fee_rate=0.002)
selected = policy.choose(
    snapshot,
    forecasts=[101.0],
    signal_generator=signal_generator,
    sizer=sizer,
    max_additions=3,
    buy_notional=approved_notional,
    returns=pre_decision_returns,
)

print(selected.signal)
print(selected.notional)
print(selected.addition_level)</code></pre></div><p>This call does not pass <code>VaRTradeFilter</code> into <code>GreedyPolicy</code>. The VaR decision has already been applied directly with the compatible <code>VaRTradeFilter.decide</code> interface. That explicit separation avoids confusing a currency-notional risk result with an asset-unit order.</p><p>The candidate set conceptually contains:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;66dd9b9f-6d04-4a6c-87fb-ad0b91f829ea&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">hold:  score 0
buy:   risk-approved notional, using current cash
add:   next unused finite sizing level
sell:  unavailable because holdings are zero</code></pre></div><p>The reconstructed greedy score for a buy or add action is:</p><p>score(a)=N(rforecast&#8722;f),</p><p>where <code>N</code> is currency notional, <code>r_forecast</code> is the predicted return, and <code>f</code> is the fee rate. If <code>N = 55.89</code>, <code>r_forecast &#8776; 0.020202</code>, and <code>f = 0.002</code>, then:</p><p>score&#8776;55.89(0.020202&#8722;0.002)&#8776;1.02.</p><p>The hold candidate has score zero, so a positive approved buy or add candidate can outrank it. The actual selected amount must still be read from <code>selected.notional</code>; the paper does not define its own greedy objective or candidate set.</p><p>The <code>GreedyPolicy</code> call uses forecast information, current portfolio state, fees, and the already-approved risk amount. It does not use the later realized 99-to-100 return. That restriction prevents look-ahead bias.</p><h3>19.8 Consume the selected schedule level</h3><p>The strategy's selected addition level is an action description. The sizing schedule should consume its corresponding level only after the action has been accepted for execution:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;50e3ded3-bfb3-4d5e-9ea8-3629f3ef7ce4&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">if selected.signal.value == "add" and selected.addition_level is not None:
    allocation = sizer.allocate_next_addition(
        available_cash=snapshot.cash,
        current_exposure=snapshot.holdings * snapshot.price,
        level=selected.addition_level - 1,
    )
    print(allocation.approved_amount)</code></pre></div><p>The subtraction converts the strategy's one-based public level to the sizer's zero-based schedule index. In a production orchestration layer, the approved amount returned by the sizer and the amount sent to the order should be compared explicitly. If risk scaling or cash caps reduce the amount, the order must use the reduced approved notional rather than the original planned allocation.</p><h3>19.9 Execute on the next bar and calculate portfolio value</h3><p>The signal is formed at the 99-price observation. The next bar has price 100, so the order executes at 100 rather than at the signal price of 99.</p><p>The example uses these units:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!8NNB!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc89285b3-4e72-438a-b08f-105d0bf92120_1154x412.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!8NNB!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc89285b3-4e72-438a-b08f-105d0bf92120_1154x412.png 424w, https://substackcdn.com/image/fetch/$s_!8NNB!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc89285b3-4e72-438a-b08f-105d0bf92120_1154x412.png 848w, https://substackcdn.com/image/fetch/$s_!8NNB!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc89285b3-4e72-438a-b08f-105d0bf92120_1154x412.png 1272w, https://substackcdn.com/image/fetch/$s_!8NNB!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc89285b3-4e72-438a-b08f-105d0bf92120_1154x412.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!8NNB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc89285b3-4e72-438a-b08f-105d0bf92120_1154x412.png" width="1154" height="412" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c89285b3-4e72-438a-b08f-105d0bf92120_1154x412.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:412,&quot;width&quot;:1154,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:87048,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/206399283?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc89285b3-4e72-438a-b08f-105d0bf92120_1154x412.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!8NNB!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc89285b3-4e72-438a-b08f-105d0bf92120_1154x412.png 424w, https://substackcdn.com/image/fetch/$s_!8NNB!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc89285b3-4e72-438a-b08f-105d0bf92120_1154x412.png 848w, https://substackcdn.com/image/fetch/$s_!8NNB!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc89285b3-4e72-438a-b08f-105d0bf92120_1154x412.png 1272w, https://substackcdn.com/image/fetch/$s_!8NNB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc89285b3-4e72-438a-b08f-105d0bf92120_1154x412.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Assume the approved notional is <code>$55.89</code>, execution price is <code>$100</code>, the fee is <code>0.002</code>, and slippage is zero. The hand calculation is:</p><p>q=55.89100=0.5589&nbsp;units,</p><p>fee=55.89&#215;0.002=$0.11178,</p><p>cash after buy=500&#8722;55.89&#8722;0.11178=$443.99822.</p><p>Immediately after the fill, the marked portfolio value is:</p><p>V=443.99822+0.5589&#215;100=$499.88822.</p><p>The difference from <code>$500</code> is the purchase fee.</p><p>The corresponding accounting objects are:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;80d96d2b-6278-459e-84ae-18774593e17c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from quantitative_trading_model.backtest import Order, PortfolioSimulator

simulator = PortfolioSimulator(
    initial_cash=500.0,
    fee_rate=0.002,
    slippage_rate=0.0,
    allow_short=False,
    allow_leverage=False,
)

order = Order(
    side="buy",
    notional=55.89,
    signal_timestamp="2024-01-04",   # the bar priced at 99
    execution_timestamp="2024-01-05",
    addition_level=1,
    reason="forecast recovery signal",
)

fill = simulator.execute(order, "2024-01-05", 100.0)
record = simulator.mark_to_market("2024-01-05", 100.0)

print(fill.notional, fill.fee)
print(record.portfolio_value)</code></pre></div><p><code>Order.notional</code> is deliberately used here because the strategy and VaR layers work in currency-notional terms. The simulator converts that notional into asset units at the execution price. If an order instead supplies <code>Order.quantity</code>, the simulator treats it as asset units directly.</p><p><code>PortfolioSimulator.execute</code> applies configured slippage, calculates executed notional and fees, updates cash and holdings, and records a <code>Fill</code>. <code>mark_to_market</code> creates a <code>BacktestRecord</code> satisfying:</p><p>\[ V<em>t=\text{cash}</em>t+\text{quantity}<em>t p</em>t. \]</p><p>The default simulator is long-only and non-leveraged. It clips an unaffordable buy to the quantity affordable with current cash; the current generated execution path does not implement a separate rejection branch for the <code>reject_insufficient_cash</code> configuration field. That behavior should therefore not be described as configurable rejection without a corresponding backtest-code change.</p><p>If the price later reaches 103, the marked value before selling is approximately:</p><p>443.99822+0.5589&#215;103&#8776;$501.675.</p><p>A later sale would incur a second fee under the default two-sided fee convention. The final realized value would depend on the sale price, slippage, and whether end-of-test liquidation is enabled.</p><h3>19.10 What the example demonstrates</h3><p>The example makes the following contracts visible:</p><ul><li><p>returns are aligned with the later price observation;</p></li><li><p>streaks are calculated from the selected return convention;</p></li><li><p>the sizing schedule is finite, normalized, and connected to the action level;</p></li><li><p>VaR is applied before the action is ranked;</p></li><li><p>risk and strategy values are currency notionals, while backtest quantities are asset units;</p></li><li><p>the greedy score uses forecasts and estimated fees, not realized future returns;</p></li><li><p>the signal at price 99 executes at the later price of 100;</p></li><li><p>the purchase fee is deducted exactly once;</p></li><li><p>portfolio value equals cash plus marked holdings.</p></li></ul><p>It does <strong>not</strong> demonstrate profitability. The forecast is manual, the price path is synthetic, and the paper's exact sizing, VaR, and greedy methods are unavailable. The example is a causal and accounting demonstration, not a reproduction of the paper.</p><h3>19.11 Relation to the paper's reported outcomes</h3><p>The paper reports approximately <code>$646</code> from a <code>$500</code> gold allocation and approximately <code>$215,487</code> from a <code>$500</code> Bitcoin allocation. Neither result can be inferred from this six-price example. Reproducing either claim would require the original datasets, dates, target definition, complete position-sizing equations, VaR settings, greedy rules, fee treatment, execution timing, and liquidation policy.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!2MbP!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F922cdd0b-bf8b-484c-a94f-b4a5c4bf509c_948x470.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!2MbP!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F922cdd0b-bf8b-484c-a94f-b4a5c4bf509c_948x470.png 424w, https://substackcdn.com/image/fetch/$s_!2MbP!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F922cdd0b-bf8b-484c-a94f-b4a5c4bf509c_948x470.png 848w, https://substackcdn.com/image/fetch/$s_!2MbP!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F922cdd0b-bf8b-484c-a94f-b4a5c4bf509c_948x470.png 1272w, https://substackcdn.com/image/fetch/$s_!2MbP!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F922cdd0b-bf8b-484c-a94f-b4a5c4bf509c_948x470.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!2MbP!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F922cdd0b-bf8b-484c-a94f-b4a5c4bf509c_948x470.png" width="948" height="470" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/922cdd0b-bf8b-484c-a94f-b4a5c4bf509c_948x470.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:470,&quot;width&quot;:948,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:75219,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/206399283?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F922cdd0b-bf8b-484c-a94f-b4a5c4bf509c_948x470.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!2MbP!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F922cdd0b-bf8b-484c-a94f-b4a5c4bf509c_948x470.png 424w, https://substackcdn.com/image/fetch/$s_!2MbP!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F922cdd0b-bf8b-484c-a94f-b4a5c4bf509c_948x470.png 848w, https://substackcdn.com/image/fetch/$s_!2MbP!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F922cdd0b-bf8b-484c-a94f-b4a5c4bf509c_948x470.png 1272w, https://substackcdn.com/image/fetch/$s_!2MbP!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F922cdd0b-bf8b-484c-a94f-b4a5c4bf509c_948x470.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The example is useful because it makes causality, units, and accounting explicit. It should not be used as evidence about expected returns, investment quality, or the validity of the paper's headline performance.</p><h2>20. Limitations, Interpretation, and What a True Reproduction Would Need</h2><p>The implementation now expresses the paper&#8217;s main ideas as separate, inspectable components: an attention-enhanced BiLSTM for forecasting, benchmark forecasters, streak analysis, a normalized exponential position-sizing reconstruction, historical VaR, a reconstructed greedy policy, portfolio accounting, and transaction-fee sensitivity experiments. That modular structure is useful, but it should not be mistaken for proof that the paper&#8217;s numerical results or trading claims have been reproduced.</p><p>The central limitation is not Python syntax. It is missing experimental information. A genuine reproduction requires the same observations, preprocessing, model settings, decision rules, execution assumptions, and evaluation dates. Several of those details are absent or corrupted in the source material. The generated project therefore distinguishes what is implemented mechanically from what is reconstructed or merely reported as a claim.</p><h3>20.1 Interpretation labels</h3><p>Use the following vocabulary when describing results from this project:</p><p>| Label | Meaning | | --- | --- | | <strong>Implemented</strong> | The paper describes enough behavior to encode a concrete mechanism, such as a sliding window or dot-product attention operation. | | <strong>Reconstructed</strong> | The paper names a method but omits equations, parameters, or operational rules, so the project supplies a documented interpretation. | | <strong>Illustrative</strong> | The result demonstrates an interface, invariant, or workflow using synthetic or user-provided local data. It is not evidence of paper reproduction. | | <strong>Unavailable</strong> | The paper does not provide enough information or data to validate the method or number. | | <strong>Reported claim</strong> | A value or conclusion stated by the paper and preserved for comparison, but not independently verified. |</p><p>This distinction applies across all major components:</p><ul><li><p><strong>Att-BiLSTM:</strong> the broad sequence-model and attention design is implemented, but hidden dimensions, layer count, query construction, target definition, and training schedule are reconstructed choices.</p></li><li><p><strong>Benchmarks:</strong> recurrent, statistical, HMM, and boosted-tree adapters are available, but comparable numerical results require the original data and exact configurations.</p></li><li><p><strong>Position sizing:</strong> the finite normalized exponential schedule is a reconstruction because equations (5)&#8211;(8) cannot be recovered reliably.</p></li><li><p><strong>VaR:</strong> the rolling historical estimator and trade filter are operational reconstructions because the paper gives no confidence level, horizon, lookback, or action rule.</p></li><li><p><strong>Greedy selection:</strong> the fee-aware candidate-ranking policy is reconstructed because the paper does not define its candidates, objective, constraints, or tie-breaking.</p></li><li><p><strong>Backtesting:</strong> cash, holdings, fees, slippage, next-bar execution, and liquidation are explicit implementation assumptions, not verified details of the original backtest.</p></li><li><p><strong>Fee sensitivity:</strong> the reported fee grids are preserved, but the figures&#8217; numeric outputs are unavailable.</p></li></ul><p>The status table in <code>docs/reproduction_matrix.md</code> is the project&#8217;s main audit record. It maps each paper claim to its implementation location, validation strength, and missing information. It should be updated whenever a source equation, dataset, or protocol detail becomes available.</p><h3>20.2 Why forecast accuracy does not establish trading profitability</h3><p>A forecast metric measures the distance between a prediction and its target. A trading result depends on a much longer chain:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;ce5fa8c3-4f72-4350-962b-45a4a36d48d0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">observed history
    -&gt; forecast
    -&gt; signal threshold
    -&gt; position size
    -&gt; risk filter
    -&gt; order timing
    -&gt; execution price
    -&gt; fees and slippage
    -&gt; holdings and cash
    -&gt; portfolio value</code></pre></div><p>An Att-BiLSTM can have a lower RMSE than a benchmark and still fail to produce a profitable strategy. For example:</p><ul><li><p>the predicted movement may be smaller than the round-trip transaction cost;</p></li><li><p>a small timing error can turn a buy into a loss;</p></li><li><p>a position-sizing rule can amplify an incorrect forecast;</p></li><li><p>the forecast may be evaluated at a different timestamp from the order execution;</p></li><li><p>slippage, spread, liquidity, and partial fills can change the economics;</p></li><li><p>the asset&#8217;s price process may change after the historical training period.</p></li></ul><p>This is why <code>metrics.py</code> keeps forecast metrics separate from portfolio metrics. RMSE, MAE, MAPE, and R-squared should be reported alongside, but not confused with, cumulative return, maximum drawdown, volatility, Sharpe ratio, turnover, and total fees. A buy-and-hold benchmark is also necessary: a strategy&#8217;s final value has little meaning without knowing what passive ownership of the same asset and period would have produced.</p><h3>20.3 Financial and market limitations</h3><p>Historical financial data are not a stationary laboratory process. The relationship between past prices and later prices can change because of market structure, liquidity, regulation, macroeconomic conditions, participant behavior, or technological changes. A model that performs well in one period may not generalize to another.</p><p>The backtest also simplifies real execution. The generated <code>backtest.py</code> simulator supports explicit fees, optional proportional slippage, fractional units, exposure limits, and next-bar execution, but it does not model every market constraint. In particular, it does not provide a full treatment of:</p><ul><li><p>bid-ask spreads and changing spreads;</p></li><li><p>market impact and order-book depth;</p></li><li><p>liquidity limits or volume participation;</p></li><li><p>partial fills and order cancellation;</p></li><li><p>latency and stale signals;</p></li><li><p>exchange outages or data revisions;</p></li><li><p>custody, borrowing, margin, taxes, or funding costs;</p></li><li><p>instrument-specific trading calendars;</p></li><li><p>corporate actions or contract rolls where relevant.</p></li></ul><p>These omissions matter especially for a strategy that repeatedly adds to positions. Transaction costs are not a cosmetic adjustment: they can change whether an action is feasible, whether a greedy candidate has a positive score, and whether the entire position schedule remains solvent.</p><h3>20.4 Why the large Bitcoin result should not be generalized</h3><p>The paper reports an approximate final value of <code>$215,487</code> from a <code>$500</code> Bitcoin allocation, alongside an approximate <code>$646</code> value from a <code>$500</code> gold allocation. Those values are <strong>reported claims</strong>, not reproduced outputs of this project.</p><p>The Bitcoin number may depend on an unusually favorable historical interval, aggressive reinvestment, repeated position additions, leverage-like exposure, unrealistic fills, or an accounting convention that is not described. Without the exact dates, instrument definition, price series, signals, sizing parameters, transaction rules, and liquidation policy, the number cannot be separated into market-period effects and strategy effects.</p><p>It should therefore not be presented as expected future performance, evidence of general profitability, or a target that a synthetic demonstration ought to match. The same caution applies to the reported combined value of approximately <code>$216,133</code> from <code>$1,000</code> and to the paper&#8217;s benchmark forecast metrics.</p><h3>20.5 What a true empirical reproduction would require</h3><p>To replace the current reconstructions with verified implementations, obtain and preserve the following artifacts.</p><h4>Data and timestamps</h4><p>The original gold and Bitcoin files.</p><p>The precise instrument or index represented by each file.</p><p>Currency, timezone, sampling frequency, and date range.</p><p>The meaning of each price column: close, adjusted close, settlement, or another field.</p><p>Rules for missing observations, duplicates, invalid prices, and corporate or market-event adjustments.</p><p>The exact train, validation, and test boundaries.</p><p>Evidence that scaling and feature construction were fitted without using future observations.</p><p>The data layer in <code>data.py</code> can then be adapted to the original schema rather than relying on generic local CSV assumptions.</p><h4>Forecast target and Att-BiLSTM architecture</h4><p>Whether the model predicts raw prices, normalized prices, returns, or percentage changes.</p><p>Whether the forecast horizon is one day, three days, or a sequence of separately generated one-step predictions.</p><p>The number of LSTM layers and hidden units.</p><p>Whether the bidirectional outputs are concatenated, averaged, or projected.</p><p>The exact construction of the attention query <code>q</code>.</p><p>Dense layers, output activation, initialization, and regularization.</p><p>Learning rate, batch size, epoch count, stopping rule, random seed, and optimizer options.</p><p>The exact feature set, including whether volume or technical variables were used.</p><p>The current <code>models/attention.py</code> and <code>models/forecasters.py</code> implement the visible mathematics and a learned-query interpretation, but they cannot infer these missing architectural details.</p><h4>Benchmark configurations</h4><p>For each LSTM, GRU, BiLSTM, Holt-Winters, ARIMA, HMM, and XGBoost model, obtain the input representation, hyperparameters, tuning procedure, forecast horizon, and test-time procedure. This is particularly important for HMM and XGBoost:</p><ul><li><p>Viterbi decoding identifies latent states; it does not by itself define a future price forecast.</p></li><li><p>XGBoost is a gradient-boosted tree method, not a random forest.</p></li><li><p>A fair benchmark comparison requires aligned targets, timestamps, preprocessing, and untouched test data.</p></li></ul><p>Without those details, a locally computed benchmark table can be informative, but it is not a verified reconstruction of the paper&#8217;s table.</p><h4>Position management, VaR, and greedy policy</h4><p>The original source equations or high-resolution figures are required for equations (5)&#8211;(8). In addition, clarify:</p><ul><li><p>whether position variables represent cash, asset units, or notional exposure;</p></li><li><p>how the recovery percentile and decline percentile enter each recursive equation;</p></li><li><p>when an additional position is triggered;</p></li><li><p>the maximum number of additions and any leverage rules;</p></li><li><p>whether fees apply to both sides of each transaction;</p></li><li><p>the VaR confidence level, horizon, lookback, distribution, and aggregation method;</p></li><li><p>whether VaR blocks trades, scales them, or only reports risk;</p></li><li><p>the greedy algorithm&#8217;s candidate actions, objective, constraints, and tie-breaking.</p></li></ul><p>Until those materials are supplied, <code>sizing.py</code>, <code>risk.py</code>, and <code>strategy.py</code> should be described as explicit, auditable reconstructions rather than faithful source translations.</p><h4>Backtest and fee-sensitivity protocol</h4><p>A verified portfolio comparison also needs the backtest start and end dates, initial holdings, cash allocation, signal timestamp, execution timestamp, order type, partial-unit policy, fee convention, spread, slippage, liquidation rule, and reinvestment behavior. For fee sensitivity, the numeric data behind the paper&#8217;s figures are required; the fee grid alone does not determine the curve.</p><p>The experiment layer can hold non-fee assumptions fixed and rerun a supplied backtest callback, but it cannot infer the original callback from the paper. A sensitivity table generated locally should therefore be labeled as a new experiment under documented assumptions.</p><h3>20.6 Educational research code versus deployable trading software</h3><p>This project is intentionally an offline educational framework. It demonstrates how to make data boundaries, model contracts, risk rules, execution timing, and accounting explicit. It is not a deployable trading system.</p><p>A production system would require substantially more work, including independent data validation, secure configuration and credential handling, exchange or broker integration, order-state reconciliation, monitoring, failure recovery, compliance review, capacity analysis, and extensive operational testing. None of those capabilities should be inferred from the presence of a backtest simulator or a command-line interface.</p><p>The safest use of the generated project is as a research scaffold:</p><p>inspect the assumptions;</p><p>replace synthetic data with identified local data;</p><p>preserve chronological boundaries;</p><p>validate each component independently;</p><p>record configuration and provenance;</p><p>compare against passive and simpler baselines;</p><p>treat every result as conditional on its protocol.</p><h3>20.7 Verification confidence and unresolved project findings</h3><p>The available verification evidence is limited. Static checks parsed the generated Python files and inspected basic structure, but the overall static stage did not pass: <code>experiments.py</code> and <code>cli.py</code> triggered placeholder detection, while the generated <code>README.md</code> and <code>docs/tutorial.md</code> fields were flagged because they contain Markdown fences. These findings are recorded issues, not proof that the underlying ideas are correct or incorrect.</p><p>Semantic code verification was explicitly skipped. Consequently, this article must not claim that the generated code was executed, that the test suite passed, or that the model trained successfully. The tests and modules document intended interfaces and invariants, but unresolved API inconsistencies may remain&#8212;for example, between some generated tests, orchestration calls, and implementation signatures.</p><p>This is an important distinction:</p><ul><li><p><strong>Static verification</strong> asks whether source files parse and meet structural checks.</p></li><li><p><strong>Semantic review</strong> asks whether functions, types, formulas, and data flow agree.</p></li><li><p><strong>Runtime testing</strong> would execute the code on controlled fixtures.</p></li><li><p><strong>Empirical reproduction</strong> would compare results with the paper using the original data and protocol.</p></li></ul><p>Only the first category was partially recorded here, and semantic verification was skipped. None of these checks, even if fully successful, would establish that the paper&#8217;s financial claims are valid.</p><h3>20.8 Final interpretation policy</h3><p>When presenting future results, use precise language:</p><ul><li><p>Say <strong>&#8220;the implementation uses&#8221;</strong> for explicit code behavior.</p></li><li><p>Say <strong>&#8220;we reconstruct&#8221;</strong> when interpreting incomplete equations or policies.</p></li><li><p>Say <strong>&#8220;the synthetic example illustrates&#8221;</strong> for generated demonstrations.</p></li><li><p>Say <strong>&#8220;the paper reports&#8221;</strong> for unverified numerical claims.</p></li><li><p>Say <strong>&#8220;locally measured under this protocol&#8221;</strong> for results obtained on identified local data.</p></li><li><p>Do not say <strong>&#8220;reproduced&#8221;</strong> unless the data, dates, preprocessing, model, decision rules, execution protocol, and evaluation outputs have been independently matched.</p></li></ul><p>The project&#8217;s strongest contribution is therefore methodological rather than evidentiary. It shows how to translate an incomplete quantitative-trading description into modular Python components while preserving temporal causality, finite allocation, explicit risk assumptions, transaction-cost accounting, and honest uncertainty. A stronger scientific conclusion must wait for the original data and missing protocol details.</p><h2>Complete Generated Code Files</h2><p>Use the button below to download the source code: </p>
      <p>
          <a href="https://onepagecode.substack.com/p/quantitative-trading-model-from-paper">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Advanced Text Generation Techniques and Tools]]></title><description><![CDATA[Chapter 7 of Hands-On Large Language Models: Advanced text generation pipelines, orchestration with LangChain, memory management, and autonomous agents]]></description><link>https://onepagecode.substack.com/p/advanced-text-generation-techniques</link><guid isPermaLink="false">https://onepagecode.substack.com/p/advanced-text-generation-techniques</guid><dc:creator><![CDATA[Onepagecode]]></dc:creator><pubDate>Thu, 09 Jul 2026 19:51:09 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!qHOK!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c98ed2a-e79d-49df-b1e8-e3a5ee0e102c_2752x1536.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Why a single prompt stops being enough</h2><p>A single prompt is often enough when the task is simple and self-contained. If you need a brief answer, a rewrite, or a one-off classification, prompt design may be all you need. The limit appears when the same task must be repeated, routed, validated, or combined with other steps. At that point, the question is no longer only how to get a better response from the model. It is how to build a reusable process around the model so the result is consistent each time.</p><h2><strong>&#128216; Buy the Entire Book &#8211; Available on Amazon Worldwide (If you are paid subscriber, use the url at the end of this article to download the book for free!)</strong></h2><p>You can now purchase the complete book directly from Amazon in your country:</p><ul><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/us/dualbookshelf.marketplacelink/B0H6WZJ57S">United States (US)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/uk/dualbookshelf.marketplacelink/B0H6WZJ57S">United Kingdom (UK)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/de/dualbookshelf.marketplacelink/B0H6WZJ57S">Germany (DE)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/fr/dualbookshelf.marketplacelink/B0H6WZJ57S">France (FR)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/es/dualbookshelf.marketplacelink/B0H6WZJ57S">Spain (ES)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/it/dualbookshelf.marketplacelink/B0H6WZJ57S">Italy (IT)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/nl/dualbookshelf.marketplacelink/B0H6WZJ57S">Netherlands (NL)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/jp/dualbookshelf.marketplacelink/B0H6WZJ57S">Japan (JP)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/br/dualbookshelf.marketplacelink/B0H6WZJ57S">Brazil (BR):</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/ca/dualbookshelf.marketplacelink/B0H6WZJ57S">Canada (CA)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/mx/dualbookshelf.marketplacelink/B0H6WZJ57S">Mexico (MX)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/au/dualbookshelf.marketplacelink/B0H6WZJ57S">Australia (AU)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/in/dualbookshelf.marketplacelink/B0H6WZJ57S">India (IN)</a></strong></p></li></ul><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!qHOK!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c98ed2a-e79d-49df-b1e8-e3a5ee0e102c_2752x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!qHOK!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c98ed2a-e79d-49df-b1e8-e3a5ee0e102c_2752x1536.png 424w, https://substackcdn.com/image/fetch/$s_!qHOK!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c98ed2a-e79d-49df-b1e8-e3a5ee0e102c_2752x1536.png 848w, https://substackcdn.com/image/fetch/$s_!qHOK!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c98ed2a-e79d-49df-b1e8-e3a5ee0e102c_2752x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!qHOK!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c98ed2a-e79d-49df-b1e8-e3a5ee0e102c_2752x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!qHOK!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c98ed2a-e79d-49df-b1e8-e3a5ee0e102c_2752x1536.png" width="1456" height="813" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2c98ed2a-e79d-49df-b1e8-e3a5ee0e102c_2752x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:813,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:7707739,&quot;alt&quot;:&quot;Figure 7.1: A single prompt is sufficient for simple tasks, but repeated, routed, and validated work requires an orchestrated workflow with explicit steps and checks.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204107502?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c98ed2a-e79d-49df-b1e8-e3a5ee0e102c_2752x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 7.1: A single prompt is sufficient for simple tasks, but repeated, routed, and validated work requires an orchestrated workflow with explicit steps and checks." title="Figure 7.1: A single prompt is sufficient for simple tasks, but repeated, routed, and validated work requires an orchestrated workflow with explicit steps and checks." srcset="https://substackcdn.com/image/fetch/$s_!qHOK!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c98ed2a-e79d-49df-b1e8-e3a5ee0e102c_2752x1536.png 424w, https://substackcdn.com/image/fetch/$s_!qHOK!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c98ed2a-e79d-49df-b1e8-e3a5ee0e102c_2752x1536.png 848w, https://substackcdn.com/image/fetch/$s_!qHOK!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c98ed2a-e79d-49df-b1e8-e3a5ee0e102c_2752x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!qHOK!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c98ed2a-e79d-49df-b1e8-e3a5ee0e102c_2752x1536.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>That shift matters because LLM applications become systems, not just prompts. A support workflow, for example, may need a ticket summary, a tone check, a response draft, and a final formatting pass. One prompt can produce each piece, but it does not by itself ensure the order, the handoffs, or the checks that make the workflow dependable. In that sense, prompt engineering is about behavior you ask for, while orchestration is about behavior you design for.</p><h2>Orchestration as the layer around model calls</h2><p>Orchestration is the control layer that sits around generation. It decides how inputs move through the application, when model calls happen, what happens to intermediate output, and which modules can intervene before the final response is returned. Prompt design shapes the model&#8217;s input. Orchestration shapes the workflow that surrounds the model.</p><p>A useful metaphor is a recipe versus an assembly line. A prompt can tell the model what to make, but orchestration determines how the work is sequenced, checked, and assembled. That is why orchestration improves quality, repeatability, and structure in real applications: it makes the process explicit instead of leaving every response to a single pass.</p><h2>LangChain as the organizing framework</h2><p>LangChain is the framework used in this chapter to keep that system view organized. It is not required for every LLM application, but it gives the chapter a shared language for connecting model I/O, templates, chains, memory, and agents. Thinking in that framework makes it easier to see which part of the application is responsible for input shaping, which part manages workflow, and which part handles longer-lived context or tool use.</p><p>This is also where the distinction between prompt engineering and orchestration becomes useful. Prompt engineering improves the wording of a request. Orchestration arranges the reusable structure around that request. LangChain helps separate those concerns so the application does not collapse into one oversized prompt that is hard to understand or maintain.</p><h2>Chains, memory, and agents in the workflow stack</h2><p>Chains are the most predictable orchestration pattern. They connect model calls and other modules into a known sequence, which makes them well suited to workflows with clear steps. Memory extends the interaction across turns so earlier context can remain available. Agents add flexibility by letting the system choose actions at runtime when the next step is not fully known in advance.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!4ZdU!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7962923-7427-4d13-bb72-f5cce4b2dc97_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!4ZdU!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7962923-7427-4d13-bb72-f5cce4b2dc97_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!4ZdU!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7962923-7427-4d13-bb72-f5cce4b2dc97_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!4ZdU!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7962923-7427-4d13-bb72-f5cce4b2dc97_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!4ZdU!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7962923-7427-4d13-bb72-f5cce4b2dc97_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!4ZdU!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7962923-7427-4d13-bb72-f5cce4b2dc97_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b7962923-7427-4d13-bb72-f5cce4b2dc97_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:6966571,&quot;alt&quot;:&quot;Figure 7.2: Prompt, chain, memory, and agent represent increasing levels of orchestration, from a single call to runtime decision-making.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204107502?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7962923-7427-4d13-bb72-f5cce4b2dc97_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 7.2: Prompt, chain, memory, and agent represent increasing levels of orchestration, from a single call to runtime decision-making." title="Figure 7.2: Prompt, chain, memory, and agent represent increasing levels of orchestration, from a single call to runtime decision-making." srcset="https://substackcdn.com/image/fetch/$s_!4ZdU!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7962923-7427-4d13-bb72-f5cce4b2dc97_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!4ZdU!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7962923-7427-4d13-bb72-f5cce4b2dc97_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!4ZdU!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7962923-7427-4d13-bb72-f5cce4b2dc97_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!4ZdU!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7962923-7427-4d13-bb72-f5cce4b2dc97_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>A simple decision rule follows from that spectrum. Use a prompt when one model call is enough. Use a chain when the steps are predictable. Use an agent when the next action depends on what happens at runtime. Chains give control, agents give flexibility, and memory supports continuity when the conversation itself carries important context.</p><h2>How orchestration improves real systems</h2><p>The practical value of orchestration shows up in maintainability and repeatability. A workflow built from explicit steps is easier to test, easier to revise, and easier to extend than a single prompt that tries to do everything at once. It is also easier to improve output quality because validation, routing, and enrichment can happen before the final generation step instead of being left to chance.</p><p>That is why this chapter moves beyond prompt engineering without abandoning it. Prompt design still matters, but the larger goal is to design the system around the model. LangChain provides one practical way to do that, and the patterns introduced here will prepare you for memory and agents later in the chapter. Retrieval will come later as a complement to orchestration by supplying external context, but the foundation is the same: structure the workflow so the model is part of a reliable application, not an isolated call.</p><h2>What to expect next in the chapter</h2><p>The next sections move from this motivation into the mechanics of model loading, prompt templates, chains, memory, and agents, with retrieval reserved for the following chapter.</p><h2>Loading local and hosted models for chain-based generation</h2><h3>Start with the deployment decision: local vs hosted</h3><p>Before a prompt ever reaches a chain, the model itself has to be available somewhere. In practice, that means choosing between local inference and a hosted API, and the choice affects far more than convenience. A local model gives you control over data, predictable access, and the ability to experiment offline, but it asks for hardware, model files, and a little more patience during setup. A hosted model removes most of that friction, but shifts the trade-off toward network dependence, usage limits, and external cost. For this chapter, the local path is useful because it makes the mechanics visible, yet the hosted fallback keeps the workflow accessible on a laptop or in a constrained environment.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ssdo!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe09aaf54-2092-4018-b386-3d6931f99031_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ssdo!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe09aaf54-2092-4018-b386-3d6931f99031_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!ssdo!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe09aaf54-2092-4018-b386-3d6931f99031_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!ssdo!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe09aaf54-2092-4018-b386-3d6931f99031_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!ssdo!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe09aaf54-2092-4018-b386-3d6931f99031_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ssdo!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe09aaf54-2092-4018-b386-3d6931f99031_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e09aaf54-2092-4018-b386-3d6931f99031_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:8295255,&quot;alt&quot;:&quot;Figure 7.3: Local and hosted model deployment choices differ in hardware, cost, and access constraints, but the surrounding orchestration layer can stay the same.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204107502?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe09aaf54-2092-4018-b386-3d6931f99031_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 7.3: Local and hosted model deployment choices differ in hardware, cost, and access constraints, but the surrounding orchestration layer can stay the same." title="Figure 7.3: Local and hosted model deployment choices differ in hardware, cost, and access constraints, but the surrounding orchestration layer can stay the same." srcset="https://substackcdn.com/image/fetch/$s_!ssdo!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe09aaf54-2092-4018-b386-3d6931f99031_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!ssdo!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe09aaf54-2092-4018-b386-3d6931f99031_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!ssdo!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe09aaf54-2092-4018-b386-3d6931f99031_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!ssdo!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe09aaf54-2092-4018-b386-3d6931f99031_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>A good rule of thumb is simple. If you are iterating on prompt structure, chain design, or message formatting, a hosted model is often the fastest way to get moving. If you want to inspect deployment constraints, work offline, or understand how much model size and context budget shape behavior, a local model is worth the extra setup. The important point is that the orchestration layer should stay conceptually stable even when the backend changes; only the engine underneath needs to swap.</p><h3>What quantization changes in practice</h3><p>The local model in this chapter uses a quantized GGUF file. Quantization compresses the model weights by storing them with fewer bits than a full-precision model would use. You can think of it as packing a suitcase more tightly: the contents are still there, but they have been folded into a smaller space so the model can fit on more modest hardware. That smaller footprint usually means lower VRAM usage and often faster inference, especially on consumer machines. The cost is that the model no longer preserves every detail of the original parameters, so a little fidelity is traded away for practicality.</p><p>That trade-off is exactly why quantized models matter. A large model in full precision may be inconvenient or impossible to run locally, while a compressed variant can become usable without changing the surrounding application logic. The same idea underlies many modern local deployment choices: reduce precision enough to make the model portable, but not so much that the quality drops below what the task needs.</p><h3>Fetch and load the local GGUF model in LangChain</h3><p>The example in this chapter uses a GGUF artifact downloaded from Hugging Face. The specific file matters because the precision variant is part of the contract between the model and your hardware. Choosing a different variant changes memory use, speed, and sometimes output quality, so the file name is not just a label; it is part of the deployment decision.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;5a5eeedb-ab7b-4eec-b706-9fa2de4ca90f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">!wget https://huggingface.co/microsoft/Phi-3-mini-4k-instruct-gguf/resolve/main/Phi-3-mini-4k-instruct-fp16.gguf</code></pre></div><p>Once the file is available locally, LangChain can wrap it through <code>LlamaCpp</code>. This is the local model interface for the chapter, and the parameters you pass here shape the generation behavior before any chain is built on top.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;e7d3c062-70db-42c5-9dce-35518f4f0f51&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from langchain import LlamaCpp

# Make sure the model path is correct for your system!
llm = LlamaCpp(
    model_path="Phi-3-mini-4k-instruct-fp16.gguf",
    n_gpu_layers=-1,
    max_tokens=500,
    n_ctx=2048,
    seed=42,
    verbose=False
)</code></pre></div><p>The practical meaning of those settings is straightforward. <code>model_path</code> points to the GGUF file you downloaded. <code>n_gpu_layers</code> controls how much of the model is offloaded to the GPU when that is available. <code>max_tokens</code> caps the length of the generated answer, which is part of your token budget. <code>n_ctx</code> sets the context window the model can condition on, and that matters because every extra instruction, conversation turn, or retrieved snippet has to fit alongside the answer you hope to generate. <code>seed</code> helps make experiments repeatable, and <code>verbose</code> keeps the output quieter while you are testing.</p><p>That context limit is easy to underestimate. Think of it as a suitcase that must hold both the prompt and the response. If you keep stuffing history into the prompt, you leave less room for the answer, and eventually the model has to truncate, ignore, or compress something. This is why context size becomes a design constraint later in the chapter when chains and memory start accumulating text.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!akEk!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F841ad40b-8274-48f7-a388-4ac925d6e4c8_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!akEk!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F841ad40b-8274-48f7-a388-4ac925d6e4c8_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!akEk!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F841ad40b-8274-48f7-a388-4ac925d6e4c8_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!akEk!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F841ad40b-8274-48f7-a388-4ac925d6e4c8_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!akEk!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F841ad40b-8274-48f7-a388-4ac925d6e4c8_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!akEk!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F841ad40b-8274-48f7-a388-4ac925d6e4c8_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/841ad40b-8274-48f7-a388-4ac925d6e4c8_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:7443994,&quot;alt&quot;:&quot;Figure 7.4: The context window is a shared budget for prompt text, history, retrieval, and generated output, so input growth reduces available response space.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204107502?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F841ad40b-8274-48f7-a388-4ac925d6e4c8_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 7.4: The context window is a shared budget for prompt text, history, retrieval, and generated output, so input growth reduces available response space." title="Figure 7.4: The context window is a shared budget for prompt text, history, retrieval, and generated output, so input growth reduces available response space." srcset="https://substackcdn.com/image/fetch/$s_!akEk!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F841ad40b-8274-48f7-a388-4ac925d6e4c8_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!akEk!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F841ad40b-8274-48f7-a388-4ac925d6e4c8_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!akEk!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F841ad40b-8274-48f7-a388-4ac925d6e4c8_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!akEk!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F841ad40b-8274-48f7-a388-4ac925d6e4c8_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3>Why the raw model call can fail without the right prompt shape</h3><p>Once the model is loaded, it is tempting to call it directly and assume that any text input will work. With some instruct or chat-tuned models, that assumption is wrong. The model may be running correctly, but the request still fails because the input shape does not match the format it expects. That is less like a runtime crash and more like sending a valid HTTP request to the wrong endpoint schema.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;e4fcfef7-90f6-4b17-a21d-34361e4ca161&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">llm.invoke("Hi! My name is Maarten. What is 1 + 1?")

''</code></pre></div><p>The empty result is the important lesson. The model is not broken; the prompt contract is incomplete. Some models need a specific instruction wrapper, a chat template, or other formatting cues before they respond sensibly. In other words, prompt formatting is not an optional refinement layered on top of the model. For certain backends, it is part of the interface definition.</p><p>That distinction matters for the rest of the chapter. If the request format is unstable, every downstream chain inherits that fragility. Once you know the model&#8217;s expected shape, you can stop hand-assembling prompts and move toward reusable templates that enforce the right structure every time.</p><h3>Use a hosted chat model when local inference is not practical</h3><p>The hosted fallback follows the same overall pattern but uses an API-backed chat model instead of a local GGUF file. Operationally, that changes where the computation happens and what constraints matter most. You no longer manage model files or local GPU placement, but you do need credentials, network access, and a model service that fits your use case.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;053c9e3c-f441-4abc-8295-a0b4d45b9609&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from langchain.chat_models import ChatOpenAI

# Create a chat-based LLM
chat_model = ChatOpenAI(openai_api_key="MY_KEY")</code></pre></div><p>This is the same interface idea in a different engine. The point is not that one path is universally better than the other, but that the surrounding generation workflow can remain portable when the model-loading layer is abstracted cleanly. That portability is exactly why the chapter moves next from model invocation to chains: once the backend is in place, the real productivity gain comes from composing prompts, inputs, and outputs into repeatable workflows rather than hand-calling the model one request at a time.</p><h2>Why templates beat one-off prompts</h2><p>A one-off prompt is fine when you are experimenting, but it becomes brittle the moment the same task needs to run again with different inputs. In practice, the useful unit is not a single prompt string; it is a prompt template, which is a stable prompt frame with named slots for the parts that change. That distinction matters because the reusable part is the workflow contract, while the variable part is just the data you feed into it.</p><p>For the ReviewDesk workflow, that difference is easy to see. A support summary might be rewritten into a clean answer draft today, then reused tomorrow for a different ticket, a different tone, or a different product line. If the structure of the request stays mostly the same, a template gives you consistency without forcing you to copy and edit prose by hand. If the task itself needs to be split into distinct decisions, such as naming a title before drafting a response, then the problem has outgrown a single prompt and is ready for chaining.</p><h2>Defining variables and model-specific prompt format</h2><p>A prompt template is useful because it makes the changing parts explicit. Instead of hiding the input inside a long string, you name the fields that the chain expects. That turns prompt writing into a small interface design exercise: the template defines what information must be supplied, and the model receives the formatted text that results from filling those slots.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!8vv9!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F06de3df2-9d87-4e78-a9f4-308a0adcbc94_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!8vv9!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F06de3df2-9d87-4e78-a9f4-308a0adcbc94_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!8vv9!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F06de3df2-9d87-4e78-a9f4-308a0adcbc94_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!8vv9!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F06de3df2-9d87-4e78-a9f4-308a0adcbc94_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!8vv9!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F06de3df2-9d87-4e78-a9f4-308a0adcbc94_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!8vv9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F06de3df2-9d87-4e78-a9f4-308a0adcbc94_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/06de3df2-9d87-4e78-a9f4-308a0adcbc94_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:6614112,&quot;alt&quot;:&quot;Figure 7.5: A prompt template defines a stable text frame with named slots, which are filled with variable inputs before the formatted prompt is sent to the model.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204107502?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F06de3df2-9d87-4e78-a9f4-308a0adcbc94_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 7.5: A prompt template defines a stable text frame with named slots, which are filled with variable inputs before the formatted prompt is sent to the model." title="Figure 7.5: A prompt template defines a stable text frame with named slots, which are filled with variable inputs before the formatted prompt is sent to the model." srcset="https://substackcdn.com/image/fetch/$s_!8vv9!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F06de3df2-9d87-4e78-a9f4-308a0adcbc94_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!8vv9!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F06de3df2-9d87-4e78-a9f4-308a0adcbc94_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!8vv9!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F06de3df2-9d87-4e78-a9f4-308a0adcbc94_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!8vv9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F06de3df2-9d87-4e78-a9f4-308a0adcbc94_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Some models also care about exact formatting. A chat-oriented model may expect role markers, delimiters, or other special tokens around the user and assistant turns. In that case, the template is not only about wording; it is also about preserving the structure the model was trained to recognize. If that structure is wrong, the model can still produce text, but the chain may become less reliable because the model is being asked to read a format it does not expect.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;8c827a97-340f-45ee-a1e1-8315e539e274&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from langchain import PromptTemplate

# Create a prompt template with the "input_prompt" variable
template = """&lt;s&gt;&lt;|user|&gt;
{input_prompt}&lt;|end|&gt;
&lt;|assistant|&gt;"""
prompt = PromptTemplate(
    template=template,
    input_variables=["input_prompt"]
)</code></pre></div><h2>Wiring a prompt directly into an LLM</h2><p>Once the prompt is packaged as a template, the next step is to connect it to the model. LangChain&#8217;s pipe syntax makes that composition explicit: the prompt formats the input, and the model consumes the formatted result. Conceptually, this is the smallest useful chain. It is still just one step, but the step is now reusable and named instead of being a hand-written string glued to a model call.</p><p>The invocation also becomes clearer. You do not send the model a free-form blob of text; you provide a mapping that matches the template&#8217;s variable names. That small discipline pays off quickly, because it makes the call site readable and reduces the chance of silently passing the wrong value into the prompt.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;1c2aa124-9ffb-4eea-b225-14e2c12d1d97&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">basic_chain = prompt | llm

# Use the chain
basic_chain.invoke(
    {
        "input_prompt": "Hi! My name is Maarten. What is 1 + 1?",
    }
)</code></pre></div><h2>Reusing the same pattern for different jobs</h2><p>The real value of templates shows up when the same structure is reused for a different task. A business-name generator, a ticket summarizer, and a response drafter can all follow the same pattern even though they solve different problems. The only thing that changes is the variable name and the wording around it. That is the hallmark of a good template: when the nouns change but the workflow stays stable, you have separated the prompt&#8217;s public interface from its implementation.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;40eb6c2e-8a97-4ec3-a64c-a63c99b60271&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># Create a Chain that creates our business' name
template = "Create a funny name for a business that sells {product}."
name_prompt = PromptTemplate(
    template=template,
    input_variables=["product"]
)</code></pre></div><h2>Breaking a generation task into linked steps</h2><p>A larger generation task often becomes easier to control when it is decomposed into smaller stages. The ReviewDesk analogue is straightforward. Suppose you have a short ticket summary and want first a concise title, then a character sketch for a response style, and finally a full draft. Each step can focus on one subgoal, which makes the output easier to inspect and easier to correct.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!yZzD!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff2a28f65-3fc7-4f49-acbb-0cd1b68d9816_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!yZzD!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff2a28f65-3fc7-4f49-acbb-0cd1b68d9816_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!yZzD!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff2a28f65-3fc7-4f49-acbb-0cd1b68d9816_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!yZzD!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff2a28f65-3fc7-4f49-acbb-0cd1b68d9816_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!yZzD!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff2a28f65-3fc7-4f49-acbb-0cd1b68d9816_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!yZzD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff2a28f65-3fc7-4f49-acbb-0cd1b68d9816_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f2a28f65-3fc7-4f49-acbb-0cd1b68d9816_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:9089237,&quot;alt&quot;:&quot;Figure 7.6: A sequential chain breaks a larger generation task into labeled intermediate steps, where each output becomes input to the next prompt.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204107502?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff2a28f65-3fc7-4f49-acbb-0cd1b68d9816_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 7.6: A sequential chain breaks a larger generation task into labeled intermediate steps, where each output becomes input to the next prompt." title="Figure 7.6: A sequential chain breaks a larger generation task into labeled intermediate steps, where each output becomes input to the next prompt." srcset="https://substackcdn.com/image/fetch/$s_!yZzD!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff2a28f65-3fc7-4f49-acbb-0cd1b68d9816_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!yZzD!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff2a28f65-3fc7-4f49-acbb-0cd1b68d9816_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!yZzD!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff2a28f65-3fc7-4f49-acbb-0cd1b68d9816_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!yZzD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff2a28f65-3fc7-4f49-acbb-0cd1b68d9816_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>This is where sequential chaining becomes more than syntactic convenience. The title is not just another string; it is a labeled artifact that later prompts can use. The character description is likewise not a final answer but a structured intermediate result. By giving each stage a specific role, you make the hidden subgoals visible, much as a kitchen keeps prep, plating, and finishing separate so each station knows exactly what to hand off.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;c3bcf66c-368b-4207-b69f-a0063fdb2c85&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from langchain import LLMChain

# Create a chain for the title of our story
template = """&lt;s&gt;&lt;|user|&gt;
Create a title for a story about {summary}. Only return the title.&lt;|end|&gt;
&lt;|assistant|&gt;"""
title_prompt = PromptTemplate(template=template, input_variables=["summary"])
title = LLMChain(llm=llm, prompt=title_prompt, output_key="title")

title.invoke({"summary": "a girl that lost her mother"})

# Create a chain for the character description using the summary and title
template = """&lt;s&gt;&lt;|user|&gt;
Describe the main character of a story about {summary} with the title {title}. Use only two sentences.&lt;|end|&gt;
&lt;|assistant|&gt;"""
character_prompt = PromptTemplate(
    template=template, input_variables=["summary", "title"]
)
character = LLMChain(llm=llm, prompt=character_prompt, output_key="character")

# Create a chain for the story using the summary, title, and character description
template = """&lt;s&gt;&lt;|user|&gt;
Create a story about {summary} with the title {title}. The main character is: {character}. Only return the story and it cannot be longer than one paragraph. &lt;|end|&gt;
&lt;|assistant|&gt;"""
story_prompt = PromptTemplate(
    template=template, input_variables=["summary", "title", "character"]
)
story = LLMChain(llm=llm, prompt=story_prompt, output_key="story")

# Combine all three components to create the full chain
llm_chain = title | character | story

llm_chain.invoke("a girl that lost her mother")</code></pre></div><h2>Reading, passing, and constraining intermediate outputs</h2><p>Output keys are the shipping labels of a chain. They tell you what each stage produced and what the next stage should expect. In the example above, the first step stores its result as <code>title</code>, the second as <code>character</code>, and the third as <code>story</code>. That naming is not cosmetic; it is how the pipeline keeps track of which artifact should flow into which prompt.</p><p>This is also where many failures start. If a downstream prompt expects <code>{title}</code> but the upstream step saved its result under a different key, the chain breaks or receives the wrong field. The same issue appears when an intermediate output is too verbose. A title prompt that ignores the instruction to return only the title can pollute the next stage with extra text, which is why generation constraints belong inside the prompt rather than as an afterthought.</p><h2>Choosing between a single prompt and a chain</h2><p>Use a single prompt when the task is simple enough that one well-shaped request can handle it. Use a chain when the task has genuinely separable substeps, when you want intermediate artifacts you can inspect, or when the same structure will be reused across many inputs. A prompt template is the reusable interface; a chain is the orchestration around that interface. Keeping that boundary clear will matter even more in the next sections, where stateful memory and tool-using agents build on the same idea of explicit handoffs.</p><h2>Conversation memory and the cost of keeping context</h2><h3>Why a plain chain does not remember yesterday</h3><p>A plain LLM call is stateless. It answers from whatever text you include in the current prompt, and then the interaction ends. That is easy to miss because the first reply can feel conversational: if you tell the model your name in one turn and ask a related question immediately afterward, the answer may seem context-aware. But that impression can be misleading. The model is not carrying a private notebook between calls; unless you pass the earlier exchange back in, the next invocation starts fresh.</p><p>That distinction matters in a support workflow like ReviewDesk. Suppose a user says, &#8220;Hi, I&#8217;m Maarten, and I need help with a billing issue,&#8221; and the assistant responds helpfully. If a second call asks, &#8220;What is my name?&#8221; a basic chain has no reason to know. The missing ingredient is not a better prompt in the abstract, but a mechanism for carrying forward conversation state. In LangChain, that mechanism is memory.</p><h3>Wiring chat history into the prompt</h3><p>Memory does not replace the prompt. It feeds the prompt. The prompt template reserves a slot, often called <code>chat_history</code> or something equivalent, and the memory object fills that slot before the model is called. That separation is important: the chain orchestrates the call, the memory object stores and loads state, and the prompt template defines where that state should appear.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!pwCU!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F203e055c-e4ad-4ad9-a02a-3041a90e63e3_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!pwCU!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F203e055c-e4ad-4ad9-a02a-3041a90e63e3_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!pwCU!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F203e055c-e4ad-4ad9-a02a-3041a90e63e3_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!pwCU!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F203e055c-e4ad-4ad9-a02a-3041a90e63e3_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!pwCU!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F203e055c-e4ad-4ad9-a02a-3041a90e63e3_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!pwCU!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F203e055c-e4ad-4ad9-a02a-3041a90e63e3_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/203e055c-e4ad-4ad9-a02a-3041a90e63e3_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:7909710,&quot;alt&quot;:&quot;Figure 7.7: A memory object loads prior conversation state into a prompt template slot such as `chat_history` before each LLM call.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204107502?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F203e055c-e4ad-4ad9-a02a-3041a90e63e3_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 7.7: A memory object loads prior conversation state into a prompt template slot such as `chat_history` before each LLM call." title="Figure 7.7: A memory object loads prior conversation state into a prompt template slot such as `chat_history` before each LLM call." srcset="https://substackcdn.com/image/fetch/$s_!pwCU!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F203e055c-e4ad-4ad9-a02a-3041a90e63e3_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!pwCU!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F203e055c-e4ad-4ad9-a02a-3041a90e63e3_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!pwCU!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F203e055c-e4ad-4ad9-a02a-3041a90e63e3_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!pwCU!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F203e055c-e4ad-4ad9-a02a-3041a90e63e3_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>A useful mental model is chat-state compression. The chain is the delivery system, the prompt is the envelope, and memory decides how much of the prior conversation to pack into it. If the envelope has no history field, nothing from earlier turns can be injected, no matter how conversational the wording sounds.</p><h3>Full-buffer memory as the simplest persistent transcript</h3><p>The simplest baseline is <code>ConversationBufferMemory</code>. It keeps the full conversation transcript and sends it back into the prompt on each turn. Because nothing is discarded, it is the easiest strategy to reason about. If the user said it earlier, it is still available now. That makes buffer memory a good starting point for short, high-stakes conversations where exact recall matters more than efficiency.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;2e4e832c-46c4-4f09-a19e-6f01e71b5f60&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">template = """&lt;s&gt;&lt;|user|&gt;Current conversation:{chat_history}\n\n{input_prompt}&lt;|end|&gt;\n&lt;|assistant|&gt;"""

prompt = PromptTemplate(
    template=template,
    input_variables=["input_prompt", "chat_history"]
)

memory = ConversationBufferMemory(memory_key="chat_history")

llm_chain = LLMChain(
    prompt=prompt,
    llm=llm,
    memory=memory
)</code></pre></div><p>With that wiring in place, the same two-turn ReviewDesk exchange now behaves like a real conversation. The first invocation stores the user&#8217;s name, and the second can recover it because the entire transcript is still attached to the chain.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;209da89f-6915-4395-8daf-ec20599cfa88&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">llm_chain.invoke({"input_prompt": "Hi! My name is Maarten. What is 1 + 1?"})
llm_chain.invoke({"input_prompt": "What is my name?"})</code></pre></div><p>The advantage is clarity. The downside appears quickly in longer dialogs. Every new turn expands the prompt, which increases token usage, slows generation, and raises cost. Buffer memory is faithful, but it is also greedy. Like carrying every receipt in your wallet, it works until the accumulation becomes the problem.</p><h3>Windowed memory: keep the recent turns, drop the rest</h3><p><code>ConversationBufferWindowMemory</code> trims that growth by keeping only the most recent <code>k</code> turns. The idea is a sliding window over the dialogue. Recent context stays available, older context falls away. This is often a better fit when the current exchange depends on the last few messages but not on the entire history.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;4d23fc0a-15fc-4153-bd64-50a54c4a8e54&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">memory = ConversationBufferWindowMemory(k=2, memory_key="chat_history")

llm_chain = LLMChain(
    prompt=prompt,
    llm=llm,
    memory=memory
)</code></pre></div><p>With a small window, the chain can still answer follow-up questions tied to the recent exchange, but it eventually loses earlier facts. In a ReviewDesk conversation, that can be enough if the assistant only needs to maintain the active thread of one support case. If the user says their name and issue in one or two turns, the model can continue to refer to them correctly. If the discussion drifts long enough, earlier commitments, counts, or names may drop out of scope.</p><p>This is the most common trap with too-small windows: the model sounds coherent until it is asked about a detail that has already slid past the edge of the buffer. Windowed memory is therefore a budgeting tool, not a weaker version of buffer memory. It is an explicit decision to value recency over completeness.</p><h3>Summary memory: compress the conversation instead of replaying it</h3><p><code>ConversationSummaryMemory</code> takes a different path. Instead of preserving the full transcript, it keeps a running summary of the interaction. After each turn, the earlier conversation is compressed into a shorter form, and that condensed state is what gets injected later. In effect, the model is asked to remember the story, not the transcript.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;91274cf5-265b-4da6-96df-219912131b6d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">summary_prompt_template = """&lt;s&gt;&lt;|user|&gt;Summarize the conversations and update with the new lines.\n\nCurrent summary:\n{summary}\n\nnew lines of conversation:\n{new_lines}\n\nNew summary:&lt;|end|&gt;\n&lt;|assistant|&gt;"""
summary_prompt = PromptTemplate(
    input_variables=["new_lines", "summary"],
    template=summary_prompt_template
)

memory = ConversationSummaryMemory(
    llm=llm,
    memory_key="chat_history",
    prompt=summary_prompt
)

llm_chain = LLMChain(
    prompt=prompt,
    llm=llm,
    memory=memory
)</code></pre></div><p>Because the summary is updated incrementally, the chain can stay conversational across longer sessions without replaying everything. That is the main appeal: lower token cost and better scalability. But compression is never free. A summary tends to preserve the gist while softening exact wording, counts, and small commitments. The most subtle failure mode is summary drift, where the condensed history remains plausible but slowly becomes less precise. It is a little like a game of telephone played over many turns: the storyline survives, but the details can blur.</p><p>A direct inspection call makes the behavior less mysterious. You can ask the memory object what it currently holds and see that it returns the compressed state rather than the raw transcript.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;136f588d-6852-43dc-b8e1-b982d5158d3d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">memory.load_memory_variables({})</code></pre></div><p>That inspection step is useful because it reminds you that memory is just another component in the chain. The chain does not magically &#8220;remember&#8221;; it asks memory what should be inserted, then the prompt receives that state like any other input variable.</p><h3>How to choose a memory strategy in practice</h3><p>The choice among buffer, windowed, and summary memory is a trade-off among retention, token usage, latency, and accuracy. If the conversation is short and every detail matters, full buffer memory is the safest choice. If the assistant only needs the latest turns, a small window keeps the system lean. If the discussion is long-lived and the broad arc matters more than exact phrasing, summary memory is usually the most efficient option.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!zc35!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffef380b7-46b2-4569-aab1-e36da8e5b829_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!zc35!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffef380b7-46b2-4569-aab1-e36da8e5b829_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!zc35!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffef380b7-46b2-4569-aab1-e36da8e5b829_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!zc35!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffef380b7-46b2-4569-aab1-e36da8e5b829_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!zc35!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffef380b7-46b2-4569-aab1-e36da8e5b829_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!zc35!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffef380b7-46b2-4569-aab1-e36da8e5b829_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/fef380b7-46b2-4569-aab1-e36da8e5b829_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:8990378,&quot;alt&quot;:&quot;Figure 7.8: Buffer, windowed, and summary memory trade off transcript retention against token cost, latency, and recall fidelity.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204107502?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffef380b7-46b2-4569-aab1-e36da8e5b829_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 7.8: Buffer, windowed, and summary memory trade off transcript retention against token cost, latency, and recall fidelity." title="Figure 7.8: Buffer, windowed, and summary memory trade off transcript retention against token cost, latency, and recall fidelity." srcset="https://substackcdn.com/image/fetch/$s_!zc35!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffef380b7-46b2-4569-aab1-e36da8e5b829_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!zc35!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffef380b7-46b2-4569-aab1-e36da8e5b829_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!zc35!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffef380b7-46b2-4569-aab1-e36da8e5b829_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!zc35!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffef380b7-46b2-4569-aab1-e36da8e5b829_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>One useful rule is to think in terms of continuity versus knowledge. Memory is for continuity of the conversation, not for discovering facts outside the chat. A support assistant may remember that the user already described a billing issue, but it should not be treated as a database of policy documents or past tickets. When the task shifts from remembering the conversation to finding external information, memory is no longer enough on its own. That is the point where retrieval becomes the right tool, not a bigger buffer.</p><h2>Agents, tools, and ReAct-style control flow</h2><p>A fixed chain is like an assembly line. The same stations run in the same order every time, which is ideal when the task is predictable. An agent is different. It behaves more like a dispatcher that decides which specialist to call next, then uses that result to choose the following step. The change is not just that the agent is smarter. The change is that control flow is no longer predetermined. A chain follows a route; an agent selects the route as it goes.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!zilo!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff284d28c-cbac-41d1-bdcd-06900a753d1b_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!zilo!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff284d28c-cbac-41d1-bdcd-06900a753d1b_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!zilo!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff284d28c-cbac-41d1-bdcd-06900a753d1b_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!zilo!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff284d28c-cbac-41d1-bdcd-06900a753d1b_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!zilo!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff284d28c-cbac-41d1-bdcd-06900a753d1b_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!zilo!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff284d28c-cbac-41d1-bdcd-06900a753d1b_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f284d28c-cbac-41d1-bdcd-06900a753d1b_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:8252503,&quot;alt&quot;:&quot;Figure 7.9: Fixed chains execute the same steps in the same order, while agents choose the next action dynamically by routing through tools or specialists at runtime.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204107502?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff284d28c-cbac-41d1-bdcd-06900a753d1b_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 7.9: Fixed chains execute the same steps in the same order, while agents choose the next action dynamically by routing through tools or specialists at runtime." title="Figure 7.9: Fixed chains execute the same steps in the same order, while agents choose the next action dynamically by routing through tools or specialists at runtime." srcset="https://substackcdn.com/image/fetch/$s_!zilo!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff284d28c-cbac-41d1-bdcd-06900a753d1b_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!zilo!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff284d28c-cbac-41d1-bdcd-06900a753d1b_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!zilo!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff284d28c-cbac-41d1-bdcd-06900a753d1b_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!zilo!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff284d28c-cbac-41d1-bdcd-06900a753d1b_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>That matters when the answer depends on capabilities outside the model itself. If the task is stable and self-contained, a chain is usually simpler and safer. If the task needs current facts, a calculation, or a choice among several external actions, an agent becomes useful because it can orchestrate those steps dynamically. The model is still just generating text, but the runtime lets it decide when to search, when to calculate, and when to stop.</p><h2>What counts as a tool, and why tools change the game</h2><p>A tool is an external function that the agent can call through a defined interface. It is not a model weight, and it is not hidden reasoning. It is an explicit capability exposed to the agent so the model can use it as part of its plan. Search is one common tool type because it can fetch current or obscure facts. Math is another because it can support deterministic calculation even when the model should not be trusted to carry the arithmetic in its own text output.</p><p>This separation is the key idea. The model handles selection and coordination, while the tools handle execution. That makes the system compositional: search can be swapped for another retrieval service, and math can be replaced with a different calculator, without changing the basic control pattern. It also gives a practical heuristic for design. Use a chain when the steps are fixed. Use an agent when the next step depends on what just happened. Add tools when the model needs to reach beyond its own text generation.</p><h2>ReAct as the loop that binds thinking to acting</h2><p>ReAct turns that control pattern into a repeating loop of thought, action, and observation. The thought step is the model deciding what to do next. The action step names the tool and the input to send to it. The observation is the tool output that comes back into the system. That observation is then folded into the next decision, which is why the loop can adapt instead of merely replaying a script.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!j7Rm!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4c8a097-7190-44a4-84f3-33fe1d462df5_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!j7Rm!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4c8a097-7190-44a4-84f3-33fe1d462df5_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!j7Rm!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4c8a097-7190-44a4-84f3-33fe1d462df5_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!j7Rm!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4c8a097-7190-44a4-84f3-33fe1d462df5_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!j7Rm!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4c8a097-7190-44a4-84f3-33fe1d462df5_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!j7Rm!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4c8a097-7190-44a4-84f3-33fe1d462df5_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e4c8a097-7190-44a4-84f3-33fe1d462df5_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:9310332,&quot;alt&quot;:&quot;Figure 7.10: ReAct agents iterate through thought, action, and observation, while the scratchpad preserves history so each new decision can use prior tool results.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204107502?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4c8a097-7190-44a4-84f3-33fe1d462df5_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 7.10: ReAct agents iterate through thought, action, and observation, while the scratchpad preserves history so each new decision can use prior tool results." title="Figure 7.10: ReAct agents iterate through thought, action, and observation, while the scratchpad preserves history so each new decision can use prior tool results." srcset="https://substackcdn.com/image/fetch/$s_!j7Rm!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4c8a097-7190-44a4-84f3-33fe1d462df5_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!j7Rm!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4c8a097-7190-44a4-84f3-33fe1d462df5_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!j7Rm!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4c8a097-7190-44a4-84f3-33fe1d462df5_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!j7Rm!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4c8a097-7190-44a4-84f3-33fe1d462df5_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The scratchpad, or intermediate-step state, is what preserves that history. It records prior actions and observations so the agent can see what it has already tried before choosing the next move. Without that state, the model would have to make each decision in isolation. With it, each tool call becomes part of the context for the following turn.</p><p>The prompt has to make the available action space visible as well. Tool names and tool descriptions are injected into the template so the model knows what it is allowed to call and what each tool is for. That is a major difference from a free-form prompt. The agent is not inventing arbitrary actions; it is selecting from the tools that the runtime has exposed.</p><h2>How LangChain wires tools, prompts, and execution</h2><p>The implementation begins with the model. In the example, a hosted chat model is loaded with a zero temperature setting so the agent behaves more consistently when deciding among tools.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;fa85d313-d234-4546-9ff3-f7eefabfa507&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">import os
from langchain_openai import ChatOpenAI

# Load OpenAI's LLMs with LangChain
os.environ["OPENAI_API_KEY"] = "MY_KEY"
openai_llm = ChatOpenAI(model_name="gpt-3.5-turbo", temperature=0)</code></pre></div><p>Next comes the ReAct prompt. Its job is to define the interaction format and reserve placeholders for the tool list, tool names, the user input, and the scratchpad. That is what allows the same template to support many different runs while keeping the intermediate state attached to the current one.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;d2a2ebbf-d624-4afd-a61e-e5b0995bfeb8&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># Create the ReAct template
react_template = """Answer the following questions as best you can. You have access to the following tools:

{tools}

Use the following format:

Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [{tool_names}]
Action Input: the input to the action
Observation: the result of the action
... (this Thought/Action/Action Input/Observation can repeat N times)
Thought: I now know the final answer
Final Answer: the final answer to the original input question

Begin!

Question: {input}
Thought:{agent_scratchpad}"""

prompt = PromptTemplate(
    template=react_template,
    input_variables=["tools", "tool_names", "input", "agent_scratchpad"]
)</code></pre></div><p>The tools are then assembled into a shared list. Here the agent gets both a web search tool and an LLM-backed math tool. That combination is enough to show why agents matter: the model can fetch a live fact, then hand a numeric value to another capability for a deterministic follow-up step.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;dc1c6620-0869-471e-a141-d5e6f4b74743&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from langchain.agents import load_tools, Tool
from langchain.tools import DuckDuckGoSearchResults

# You can create the tool to pass to an agent
search = DuckDuckGoSearchResults()
search_tool = Tool(
    name="duckduck",
    description="A web search engine. Use this to as a search engine for general queries.",
    func=search.run,
)

# Prepare tools
tools = load_tools(["llm-math"], llm=openai_llm)
tools.append(search_tool)</code></pre></div><p>The agent itself is created from the model, the tools, and the prompt. The executor is the runtime layer that makes the whole pattern work. It runs the loop, calls tools, gathers observations, and can print intermediate steps when verbose tracing is enabled. It also has to handle malformed action strings and parsing failures, because model output is not always perfectly formatted.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;8db114d9-6731-485b-95b2-fd5c5edca26d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from langchain.agents import AgentExecutor, create_react_agent

# Construct the ReAct agent
agent = create_react_agent(openai_llm, tools, prompt)
agent_executor = AgentExecutor(
    agent=agent, tools=tools, verbose=True, handle_parsing_errors=True
)</code></pre></div><h2>A worked multi-step example: search plus arithmetic</h2><p>The benefit of agentic control flow becomes clear with a query that mixes current information and computation. Suppose you want the current price of a MacBook Pro in USD and then want to convert that amount to EUR using an exchange rate. A fixed prompt can only guess at the live price. An agent can search for the current price first, then use the math tool on the retrieved value.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;7ac7581c-d72d-4273-8635-d58e05ed27e2&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># What is the price of a MacBook Pro?
agent_executor.invoke(
    {
        "input": "What is the current price of a MacBook Pro in USD? How much would it cost in EUR if the exchange rate is 0.85 EUR for 1 USD."
    }
)</code></pre></div><p>A typical run starts with the agent choosing search because it needs a live fact. The search tool returns an observation, which becomes input to the next decision. If the result is a price in USD, the agent can pass that value into the math tool to compute the EUR equivalent. The executor exposes those intermediate steps, so you can see both the tool call and the observation that followed it. That trace is valuable because it shows not just the final answer, but the path the agent took to get there.</p><p>This example also makes the division of labor obvious. The model is not doing the retrieval or the arithmetic itself. It is coordinating the search tool, the math tool, and the loop that connects them. That is what makes agent systems more flexible than chains, but also more fragile than chains, because each step depends on the quality of the previous one.</p><h2>Where agents fail, and why retrieval comes next</h2><p>Tool use adds power, but it also adds risk. A tool can return irrelevant or stale output. The model can produce malformed action text that fails parsing. A loop can continue taking actions without converging on a final answer. These are the kinds of failures that fixed chains largely avoid because their execution path is predetermined.</p><p>That trade-off is why retrieval-oriented systems come next. Agents are one way to orchestrate access to information, but they are not the only way, and they are not a replacement for retrieval architecture. The next chapter builds on this idea by showing how search and retrieval can be structured more deliberately, so the model can work with information in a more controlled way.</p><p><em>Use the url below to download the enitre book as pdf:</em></p>
      <p>
          <a href="https://onepagecode.substack.com/p/advanced-text-generation-techniques">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Prompt Engineering]]></title><description><![CDATA[Chapter 6 of Hands-On Large Language Models: Iterative prompt design, controlling text generation with temperature and top-p, and structured JSON output parsing]]></description><link>https://onepagecode.substack.com/p/prompt-engineering</link><guid isPermaLink="false">https://onepagecode.substack.com/p/prompt-engineering</guid><dc:creator><![CDATA[Onepagecode]]></dc:creator><pubDate>Wed, 08 Jul 2026 19:48:51 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!Oj1o!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff98c658d-0d01-4743-8613-d3e023fcc448_2816x1536.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>From Representation Models to Generative Prompts</h2><p>Earlier chapters treated text models mainly as representation engines. A review could become an embedding, a support ticket could become a feature vector, and a document could become a label. That perspective is still valuable, but prompt engineering starts when the goal changes. Instead of asking a model to reveal information already encoded in text, we ask it to produce new text that serves a task. The mental model shifts from extracting a signal to directing a generation.</p><p>That shift is important because generation is open ended. A classifier usually lives inside a small answer space, while a generative model can move in many plausible directions. The prompt is the operating surface for that process. It is the control layer through which we frame the task, set the tone, and indicate what kind of output should come next. Prompt engineering is not a writing trick and it is not a substitute for model capability. It is a practical engineering discipline for making the model legible to the task.</p><h2><strong>&#128216; Buy the Entire Book &#8211; Available on Amazon Worldwide (If you are paid subscriber, use the url at the end of this article to download the book for free!)</strong></h2><p>You can now purchase the complete book directly from Amazon in your country:</p><ul><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/us/dualbookshelf.marketplacelink/B0H6WZJ57S">United States (US)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/uk/dualbookshelf.marketplacelink/B0H6WZJ57S">United Kingdom (UK)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/de/dualbookshelf.marketplacelink/B0H6WZJ57S">Germany (DE)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/fr/dualbookshelf.marketplacelink/B0H6WZJ57S">France (FR)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/es/dualbookshelf.marketplacelink/B0H6WZJ57S">Spain (ES)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/it/dualbookshelf.marketplacelink/B0H6WZJ57S">Italy (IT)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/nl/dualbookshelf.marketplacelink/B0H6WZJ57S">Netherlands (NL)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/jp/dualbookshelf.marketplacelink/B0H6WZJ57S">Japan (JP)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/br/dualbookshelf.marketplacelink/B0H6WZJ57S">Brazil (BR):</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/ca/dualbookshelf.marketplacelink/B0H6WZJ57S">Canada (CA)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/mx/dualbookshelf.marketplacelink/B0H6WZJ57S">Mexico (MX)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/au/dualbookshelf.marketplacelink/B0H6WZJ57S">Australia (AU)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/in/dualbookshelf.marketplacelink/B0H6WZJ57S">India (IN)</a></strong></p></li></ul><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Oj1o!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff98c658d-0d01-4743-8613-d3e023fcc448_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Oj1o!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff98c658d-0d01-4743-8613-d3e023fcc448_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!Oj1o!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff98c658d-0d01-4743-8613-d3e023fcc448_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!Oj1o!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff98c658d-0d01-4743-8613-d3e023fcc448_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!Oj1o!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff98c658d-0d01-4743-8613-d3e023fcc448_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Oj1o!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff98c658d-0d01-4743-8613-d3e023fcc448_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f98c658d-0d01-4743-8613-d3e023fcc448_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:6641920,&quot;alt&quot;:&quot;Figure 6.1: Figure: Prompt engineering shifts the model from extracting a representation to directing open-ended generation through a prompt control layer.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204106890?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff98c658d-0d01-4743-8613-d3e023fcc448_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 6.1: Figure: Prompt engineering shifts the model from extracting a representation to directing open-ended generation through a prompt control layer." title="Figure 6.1: Figure: Prompt engineering shifts the model from extracting a representation to directing open-ended generation through a prompt control layer." srcset="https://substackcdn.com/image/fetch/$s_!Oj1o!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff98c658d-0d01-4743-8613-d3e023fcc448_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!Oj1o!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff98c658d-0d01-4743-8613-d3e023fcc448_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!Oj1o!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff98c658d-0d01-4743-8613-d3e023fcc448_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!Oj1o!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff98c658d-0d01-4743-8613-d3e023fcc448_2816x1536.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Seen that way, prompt engineering becomes an iterative workflow. You define the intent, examine what the model returns, notice where it drifts or overreaches, and revise the wording or structure. A weak prompt often behaves like an underspecified test case: it leaves too much room for the model to guess. Better prompts reduce that uncertainty by tightening the task boundary, making the expected behavior clearer, and improving consistency across runs and users. Stronger models help, but they do not remove the need for this control loop. Ambiguity, variability, and task-specific constraints still affect whether the output is useful in practice.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!pgqY!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F52ffcb4b-b19b-4f19-8804-326700f3e908_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!pgqY!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F52ffcb4b-b19b-4f19-8804-326700f3e908_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!pgqY!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F52ffcb4b-b19b-4f19-8804-326700f3e908_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!pgqY!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F52ffcb4b-b19b-4f19-8804-326700f3e908_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!pgqY!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F52ffcb4b-b19b-4f19-8804-326700f3e908_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!pgqY!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F52ffcb4b-b19b-4f19-8804-326700f3e908_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/52ffcb4b-b19b-4f19-8804-326700f3e908_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:7405562,&quot;alt&quot;:&quot;Figure 6.2: Figure: Prompt engineering is an iterative feedback loop that refines intent, reduces ambiguity, and improves output consistency.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204106890?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F52ffcb4b-b19b-4f19-8804-326700f3e908_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 6.2: Figure: Prompt engineering is an iterative feedback loop that refines intent, reduces ambiguity, and improves output consistency." title="Figure 6.2: Figure: Prompt engineering is an iterative feedback loop that refines intent, reduces ambiguity, and improves output consistency." srcset="https://substackcdn.com/image/fetch/$s_!pgqY!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F52ffcb4b-b19b-4f19-8804-326700f3e908_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!pgqY!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F52ffcb4b-b19b-4f19-8804-326700f3e908_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!pgqY!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F52ffcb4b-b19b-4f19-8804-326700f3e908_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!pgqY!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F52ffcb4b-b19b-4f19-8804-326700f3e908_2816x1536.png 1456w" sizes="100vw"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>ReviewDesk makes the change in mindset easy to see. In a representation-first flow, a support item might be assigned a category for analysis. In a generation-first flow, the same item becomes a request for the next text the system should produce, such as a concise reply draft or a short internal summary. The business task is no longer just, what label is this, but what should the model produce next.</p><p>That is why prompt structure continues to matter even as models improve. Good prompts help the system stay consistent, easier to inspect, and easier to use downstream. They also set up the next concerns in this chapter: how to reason with generated outputs, how to verify them, and how to evaluate whether the result is actually fit for the job. Prompt engineering is the first control mechanism, not the last one.</p><h2>Loading a Chat Model and Controlling Sampling</h2><h3>Why Start with a Small Open Chat Model</h3><p>Prompt engineering becomes practical only when you can observe how a model changes as the prompt and decoding settings change. That is why a small open chat model is a sensible starting point instead of a large closed service hidden behind an API. A compact model is easier to run on modest hardware, cheaper to experiment with, and fast enough to support repeated trial and error. In other words, it gives you a low-friction place to learn the workflow before you move on to larger systems.</p><p>A model such as Phi-3-mini is especially useful in this learning phase because it is compact enough to be realistic on limited VRAM while still behaving like a modern instruct-tuned chat model. That makes it a good fit for iterative work: you can revise a prompt, rerun it, and inspect the difference without waiting on heavyweight infrastructure. The goal here is not to claim that this model is universally best. The goal is to choose a practical baseline that keeps experimentation simple.</p><h3>Loading the Model, Tokenizer, and Generation Pipeline</h3><p>The standard Transformers workflow is to load a causal language model, load its tokenizer, and then wrap both in a text-generation pipeline. Each object has a distinct role. The model contains the learned weights. The tokenizer contains the vocabulary and special tokens needed to turn text into token IDs and back again. The pipeline is a convenience layer that packages inference into a single interface.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!yFJY!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1fd7be2-aa12-49f3-9ee2-638ae5b89255_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!yFJY!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1fd7be2-aa12-49f3-9ee2-638ae5b89255_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!yFJY!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1fd7be2-aa12-49f3-9ee2-638ae5b89255_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!yFJY!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1fd7be2-aa12-49f3-9ee2-638ae5b89255_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!yFJY!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1fd7be2-aa12-49f3-9ee2-638ae5b89255_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!yFJY!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1fd7be2-aa12-49f3-9ee2-638ae5b89255_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b1fd7be2-aa12-49f3-9ee2-638ae5b89255_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:5701231,&quot;alt&quot;:&quot;Figure 6.3: The standard generation stack: tokenizer, causal language model, and pipeline work together to turn prompts into decoded text under explicit generation settings.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204106890?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1fd7be2-aa12-49f3-9ee2-638ae5b89255_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 6.3: The standard generation stack: tokenizer, causal language model, and pipeline work together to turn prompts into decoded text under explicit generation settings." title="Figure 6.3: The standard generation stack: tokenizer, causal language model, and pipeline work together to turn prompts into decoded text under explicit generation settings." srcset="https://substackcdn.com/image/fetch/$s_!yFJY!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1fd7be2-aa12-49f3-9ee2-638ae5b89255_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!yFJY!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1fd7be2-aa12-49f3-9ee2-638ae5b89255_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!yFJY!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1fd7be2-aa12-49f3-9ee2-638ae5b89255_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!yFJY!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1fd7be2-aa12-49f3-9ee2-638ae5b89255_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;398a76a8-bf73-4e14-85db-1876c3fbeaf0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline

# Load model and tokenizer
model = AutoModelForCausalLM.from_pretrained(
    "microsoft/Phi-3-mini-4k-instruct",
    device_map="cuda",
    torch_dtype="auto",
    trust_remote_code=True,
)
tokenizer = AutoTokenizer.from_pretrained("microsoft/Phi-3-mini-4k-instruct")

# Create a pipeline
pipe = pipeline(
    "text-generation",
    model=model,
    tokenizer=tokenizer,
    return_full_text=False,
    max_new_tokens=500,
    do_sample=False,
)</code></pre></div><p>This configuration establishes a deterministic baseline. <code>return_full_text=False</code> keeps the output focused on the newly generated continuation instead of echoing the original prompt. <code>max_new_tokens</code> limits how far the response can grow. <code>do_sample=False</code> disables sampling, so generation follows the most likely next token at each step. That makes the result repeatable, which is exactly what you want when you are checking whether a prompt change caused a different answer.</p><p>The pipeline also makes the code easier to read. Instead of wiring generation calls by hand, you pass in a prompt or a message list and let the wrapper handle the inference details. That simplicity matters early in the workflow, because it lets you focus on prompt behavior rather than on low-level generation mechanics.</p><h3>How Chat Messages Become a Serialized Prompt</h3><p>Chat models are usually written against a message list rather than a single string. The list is the form you control as the developer, but it is not yet the exact input the model sees. Before generation, the tokenizer applies a model-specific chat template that serializes the messages into a prompt string and inserts the control tokens the model expects.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;8dc690b5-1ac6-4f0f-9e0e-52f659e5a701&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># Prompt
messages = [
    {"role": "user", "content": "Create a funny joke about chickens."}
]

# Generate the output
output = pipe(messages)
print(output[0]["generated_text"])</code></pre></div><p>The structure above is simple on purpose. The key idea is that the message object is only the starting point. The pipeline turns that structure into the actual text representation the model conditions on.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;d871ad69-43d1-4200-b592-2044ae739b58&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># Apply prompt template
prompt = pipe.tokenizer.apply_chat_template(messages, tokenize=False)
print(prompt)</code></pre></div><p>That printed string is the serialized prompt. It may include role tags, separators, and end markers that do not appear in the original message list. Those details are model-specific, which is why chat templates are not portable in a universal way. A prompt that is formatted correctly for one model can be serialized differently for another, even if the user-visible content looks identical.</p><p>A useful way to think about the distinction is that the message list is your instruction, while the chat template is the translation into the model&#8217;s dialect. When a response looks odd, too verbose, or unexpectedly truncated, the first thing to inspect is often the serialized prompt rather than the message object alone.</p><h3>Turning On Sampling: Temperature, Top-p, and Output Variability</h3><p>Deterministic decoding is the baseline, but it is not the only useful behavior. When sampling is enabled, the model has more freedom in how it selects the next token. Temperature and top-p are the two main controls for shaping that freedom.</p><p>Temperature adjusts how sharply the model favors the most likely tokens. Lower values concentrate probability mass on the top candidates and make outputs more predictable. Higher values spread probability more widely and make less likely tokens more available. In practice, that means a low temperature usually gives steadier wording, while a higher temperature usually produces more variation.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!gAvB!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b19c5f0-d52a-4220-8c32-a23ea835117a_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!gAvB!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b19c5f0-d52a-4220-8c32-a23ea835117a_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!gAvB!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b19c5f0-d52a-4220-8c32-a23ea835117a_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!gAvB!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b19c5f0-d52a-4220-8c32-a23ea835117a_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!gAvB!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b19c5f0-d52a-4220-8c32-a23ea835117a_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!gAvB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b19c5f0-d52a-4220-8c32-a23ea835117a_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0b19c5f0-d52a-4220-8c32-a23ea835117a_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:7045545,&quot;alt&quot;:&quot;Figure 6.4: Temperature reshapes the probability distribution, while top-p selects the smallest token set whose cumulative probability reaches the threshold.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204106890?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b19c5f0-d52a-4220-8c32-a23ea835117a_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 6.4: Temperature reshapes the probability distribution, while top-p selects the smallest token set whose cumulative probability reaches the threshold." title="Figure 6.4: Temperature reshapes the probability distribution, while top-p selects the smallest token set whose cumulative probability reaches the threshold." srcset="https://substackcdn.com/image/fetch/$s_!gAvB!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b19c5f0-d52a-4220-8c32-a23ea835117a_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!gAvB!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b19c5f0-d52a-4220-8c32-a23ea835117a_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!gAvB!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b19c5f0-d52a-4220-8c32-a23ea835117a_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!gAvB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0b19c5f0-d52a-4220-8c32-a23ea835117a_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;fe0a81b4-94ba-453a-a1a5-9c9639000fa4&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># Using a high temperature
output = pipe(messages, do_sample=True, temperature=1)
print(output[0]["generated_text"])</code></pre></div><p>Top-p, also called nucleus sampling, uses cumulative probability mass to limit the candidate set. Instead of considering every token, the model keeps the smallest set of tokens whose combined probability reaches the chosen threshold. This makes the pool adaptive to the prompt and the model state. A related concept is top-k, which keeps a fixed number of highest-probability tokens, but temperature and top-p are the main controls you are likely to use most often.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;66625d0c-0703-46da-830d-e218c0c955a5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># Using a high top_p
output = pipe(messages, do_sample=True, top_p=1)
print(output[0]["generated_text"])</code></pre></div><p>Sampling should be treated as a reliability choice, not just a creativity feature. The more freedom you allow, the more likely the output is to vary from run to run. That variability can be helpful, but it also means you may need more validation if the output feeds a customer-facing or workflow-critical task. Deterministic generation is often the best debugging mode because fixed output makes it easier to tell whether a change came from the prompt or from the decoding settings.</p><h3>Reading the Same Prompt Three Ways: Deterministic, Tempered, and Nucleus-Sampled</h3><p>In ReviewDesk, imagine a support request asking for a polite reply to a customer who has failed to log in several times. With deterministic decoding, the response is usually stable and conservative, which makes it easier to review and compare. With a higher temperature, the wording may become more varied and expressive. With top-p sampling, the answer remains focused on plausible continuations but has more room to choose among them. The prompt stays the same while the decoding policy changes.</p><p>That difference matters because support, triage, and drafting workflows usually need reliability before style. Sampled outputs are useful examples, but they are not guaranteed to repeat exactly on another run, especially when temperature or top-p is enabled. For that reason, deterministic settings are a good place to start whenever you want to debug a prompt or isolate the effect of a formatting change.</p><h2>From Single Prompt to Prompt Assembly</h2><p>A prompt is easier to design when you stop treating it as one uninterrupted string and start treating it as an assembly of reusable parts. That shift matters because prompt engineering is not just about writing well once. It is about building an interface to the model that can be edited, tested, and reused across tasks. In practice, the useful question is not whether a prompt is long or short, but which parts are doing the work and how those parts interact.</p><p>A modular prompt makes that structure visible. If a response is too vague, the likely cause may be a missing instruction, weak context, an unclear format cue, or a mismatch between the audience and the tone. Thinking this way turns prompt writing into a debugging exercise. You can toggle one component at a time, observe the output, and learn which part actually changes the model&#8217;s behavior. That is a more reliable habit than rewriting everything from scratch whenever the result disappoints you.</p><h2>Anatomy of a Modular Prompt</h2><p>A prompt can be built from several reusable design units. Persona sets the role the model should inhabit, such as a support agent, analyst, or editor. Instruction states the job to perform. Context supplies the background that makes the job meaningful. Format tells the model how the answer should be shaped. Audience specifies who the output is for. Tone guides voice and level of formality. Data is the concrete content the task acts on. These pieces overlap in practice, but each one gives a different kind of control.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!QIRn!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F57e65f60-045b-4ab9-89a0-2f846e2872f6_1408x768.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!QIRn!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F57e65f60-045b-4ab9-89a0-2f846e2872f6_1408x768.png 424w, https://substackcdn.com/image/fetch/$s_!QIRn!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F57e65f60-045b-4ab9-89a0-2f846e2872f6_1408x768.png 848w, https://substackcdn.com/image/fetch/$s_!QIRn!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F57e65f60-045b-4ab9-89a0-2f846e2872f6_1408x768.png 1272w, https://substackcdn.com/image/fetch/$s_!QIRn!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F57e65f60-045b-4ab9-89a0-2f846e2872f6_1408x768.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!QIRn!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F57e65f60-045b-4ab9-89a0-2f846e2872f6_1408x768.png" width="1408" height="768" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/57e65f60-045b-4ab9-89a0-2f846e2872f6_1408x768.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:768,&quot;width&quot;:1408,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:3238639,&quot;alt&quot;:&quot;Figure 6.5: A modular prompt is composed of reusable fields&#8212;persona, instruction, context, format, audience, tone, and data&#8212;each contributing a different control signal to the model.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204106890?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F57e65f60-045b-4ab9-89a0-2f846e2872f6_1408x768.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 6.5: A modular prompt is composed of reusable fields&#8212;persona, instruction, context, format, audience, tone, and data&#8212;each contributing a different control signal to the model." title="Figure 6.5: A modular prompt is composed of reusable fields&#8212;persona, instruction, context, format, audience, tone, and data&#8212;each contributing a different control signal to the model." srcset="https://substackcdn.com/image/fetch/$s_!QIRn!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F57e65f60-045b-4ab9-89a0-2f846e2872f6_1408x768.png 424w, https://substackcdn.com/image/fetch/$s_!QIRn!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F57e65f60-045b-4ab9-89a0-2f846e2872f6_1408x768.png 848w, https://substackcdn.com/image/fetch/$s_!QIRn!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F57e65f60-045b-4ab9-89a0-2f846e2872f6_1408x768.png 1272w, https://substackcdn.com/image/fetch/$s_!QIRn!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F57e65f60-045b-4ab9-89a0-2f846e2872f6_1408x768.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>They are not a universal schema. A prompt does not need every field, and the fields do not need to appear in one fixed order. The point is to make the design choices explicit so they can be reused or removed deliberately. That is why prompt composition is best understood as a set of modules rather than a rigid template. If persona says &#8220;brief and direct&#8221; while tone says &#8220;detailed and explanatory,&#8221; the prompt sends mixed signals. If context is added late after the main instruction, it may still help, but the model may attend to the earlier framing more strongly. Ordering is not magic, but it does affect which cue feels dominant.</p><p>Consider a support-style summarization prompt. The persona can establish an expert voice, the instruction can ask for a concise summary, the context can explain that the goal is to extract the most important points, the format can request a labeled output, the audience can point to busy researchers, the tone can keep the result professional, and the data field can hold the text to summarize. Each component narrows the space of acceptable completions in a different way. If one part is removed, the output often changes in a visible and useful way. If the format cue disappears, the answer may become less structured. If the audience cue disappears, the wording may become less tailored. If the tone cue changes, the same content may feel more casual or more formal.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;d664b503-d7a0-463c-8007-9a6023826bbb&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># Prompt components
persona = "You are an expert in Large Language models. You excel at breaking down complex papers into digestible summaries.\n"
instruction = "Summarize the key findings of the paper provided.\n"
context = "Your summary should extract the most crucial points that can help researchers quickly understand the most vital information of the paper.\n"
data_format = "Create a bullet-point summary that outlines the method. Follow this up with a concise paragraph that encapsulates the main results.\n"
audience = "The summary is designed for busy researchers that quickly need to grasp the newest trends in Large Language Models.\n"
tone = "The tone should be professional and clear.\n"
text = "MY TEXT TO SUMMARIZE"
data = f"Text to summarize: {text}"

# The full prompt - remove and add pieces to view its impact on the generated output
query = persona + instruction + context + data_format + audience + tone + data</code></pre></div><p>The engineering value here is not the string concatenation itself. It is the ability to edit one field at a time and compare outputs. That makes prompt work feel closer to A/B testing than to one-shot writing. You can remove the audience clause, shorten the context, or move the format cue earlier and inspect what changes. This is especially useful when a prompt needs to be maintained over time, because each component can be revised without losing the whole structure.</p><h2>Prompt Tuning by Editing the Pieces</h2><p>Prompt refinement is usually iterative. Add a component, remove one, or reorder them, then compare the result. If the answer becomes more precise after adding format, that field is earning its place. If a persona statement causes unnecessary verbosity, it may be too strong for the task. If a context block is helpful only when placed near the instruction, that ordering choice is worth keeping. The point is to isolate cause and effect so the prompt behaves like a controlled design, not a guess.</p><p>A good rule of thumb is to change one thing at a time when possible. That makes the output easier to interpret. It also helps you distinguish a genuinely useful constraint from a cosmetic one. Prompt design becomes more stable when you treat each field as a testable feature rather than as decoration.</p><h2>One-Shot Examples as Behavior Demonstrations</h2><p>Another way to steer a model is to show it a worked example inside the prompt. This is one-shot in-context learning. It is not training, and it does not update the model&#8217;s weights. The example only changes the context the model is currently conditioning on. That distinction matters because a prompt example can demonstrate a pattern, but it does not rewrite the model&#8217;s underlying knowledge.</p><p>One-shot prompting is best understood as behavior demonstration. It can teach style, response shape, label mapping, or task format. It is useful when you want the model to imitate a pattern rather than infer it from a long rule description. It is less reliable as a way to inject brand-new facts the model never learned. A single example can show how a reply should sound, but it cannot guarantee durable factual learning the way training would.</p><p>With chat-based models, the example is usually written as role-structured messages. Those messages are then serialized through the model&#8217;s chat template into the actual input format expected by that model. The distinction is important. The message list is the structured prompt design, while the template serialization is the model-specific conversion step that turns that structure into tokens. In other words, the example is not just text; it is a sequence of roles and contents that the template packages for generation.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;5ffcedc2-9599-4baa-be3a-18ad34cf85ea&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># Use a single example of using the made-up word in a sentence
one_shot_prompt = [
    {
        "role": "user",
        "content": "A 'Gigamuru' is a type of Japanese musical instrument. An example of a sentence that uses the word Gigamuru is:"
    },
    {
        "role": "assistant",
        "content": "I have a Gigamuru that my uncle gave me as a gift. I love to play it at home."
    },
    {
        "role": "user",
        "content": "To 'screeg' something is to swing a sword at it. An example of a sentence that uses the word screeg is:"
    }
]
print(tokenizer.apply_chat_template(one_shot_prompt, tokenize=False))

# Generate the output
outputs = pipe(one_shot_prompt)
print(outputs[0]["generated_text"])</code></pre></div><p>This pattern is useful when the task is mostly about format or style. A single example can show how a ticket summary should be phrased, how a classification label should appear, or how a handoff note should be structured. It is not a shortcut to knowledge acquisition. It is a way of showing the model the pattern you want it to continue.</p><h2>Chaining Prompts into a Workflow</h2><p>Prompting also becomes more powerful when it is split across multiple calls. Chain prompting uses the output of one call as input to the next. Instead of asking the model to do everything in one pass, you let it perform one subtask, capture the intermediate output, and then reuse that result downstream. This is a workflow pattern, not a new model capability.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!NVIf!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F260846da-267b-492c-b1ee-e9e608177373_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!NVIf!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F260846da-267b-492c-b1ee-e9e608177373_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!NVIf!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F260846da-267b-492c-b1ee-e9e608177373_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!NVIf!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F260846da-267b-492c-b1ee-e9e608177373_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!NVIf!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F260846da-267b-492c-b1ee-e9e608177373_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!NVIf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F260846da-267b-492c-b1ee-e9e608177373_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/260846da-267b-492c-b1ee-e9e608177373_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:6270695,&quot;alt&quot;:&quot;Figure 6.6: Chain prompting breaks a task into multiple model calls, where each intermediate output becomes the input to the next stage.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204106890?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F260846da-267b-492c-b1ee-e9e608177373_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 6.6: Chain prompting breaks a task into multiple model calls, where each intermediate output becomes the input to the next stage." title="Figure 6.6: Chain prompting breaks a task into multiple model calls, where each intermediate output becomes the input to the next stage." srcset="https://substackcdn.com/image/fetch/$s_!NVIf!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F260846da-267b-492c-b1ee-e9e608177373_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!NVIf!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F260846da-267b-492c-b1ee-e9e608177373_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!NVIf!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F260846da-267b-492c-b1ee-e9e608177373_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!NVIf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F260846da-267b-492c-b1ee-e9e608177373_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>That separation is valuable when a task naturally breaks into stages. One call can draft a name, summary, or internal note. A later call can transform that draft into a customer-facing message, a shorter pitch, or a cleaner revision. The first output becomes a controlled input for the second prompt. That makes the pipeline easier to reason about because each step has a narrower purpose.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;6d93e4a6-b8d0-4c97-8c32-b20d21822f3e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># Create name and slogan for a product
product_prompt = [
    {"role": "user", "content": "Create a name and slogan for a chatbot that leverages LLMs."}
]
outputs = pipe(product_prompt)
product_description = outputs[0]["generated_text"]
print(product_description)

# Based on a name and slogan for a product, generate a sales pitch
sales_prompt = [
    {"role": "user", "content": f"Generate a very short sales pitch for the following product: '{product_description}'"}
]
outputs = pipe(sales_prompt)
sales_pitch = outputs[0]["generated_text"]
print(sales_pitch)</code></pre></div><p>In this example, the first call produces the product name and slogan, and the second call consumes that output to produce a sales pitch. That handoff is the key idea. Chain prompting is useful when you want decomposition, drafting, or refinement. It is often preferable to a single prompt when the full task is too broad, when the first pass is exploratory, or when the second pass needs a cleaner input than the raw source text.</p><p>It also has failure modes. Errors can accumulate across steps, vague intermediate outputs can become worse in the next call, and brittle handoffs can cause the later prompt to lose the original intent. For that reason, chain prompting works best when each intermediate output is named clearly and is specific enough to support the next stage. Think of it as an assembly line: each station does a smaller job, but each station also depends on the quality of the previous one.</p><h2>What Modular Prompting Can and Cannot Do</h2><p>Modular prompting improves steering, clarity, and reuse, but it does not retrain the model. Prompt examples do not update weights. They do not create permanent knowledge. They only shape the behavior of the current interaction. That is why prompt design and model training are related but fundamentally different. Prompting works by changing inputs; training works by changing parameters.</p><p>The practical takeaway is simple. Use modular prompts when you want to control behavior, demonstrate style, or decompose a task into stages. Use chain prompting when a single pass is too crowded and intermediate results will help. Use examples when you want to demonstrate a pattern. Keep in mind that these methods improve structure, not certainty. The next section extends that idea by moving toward techniques that ask the model to work through tasks more deliberately.</p><h2>What reasoning-oriented prompting is trying to change</h2><p>Ordinary prompt refinement mainly changes what the model says. Reasoning-oriented prompting tries to change the process the model is nudged to produce before it settles on an answer. That difference matters when the task has hidden structure, overlapping labels, or underspecified details. In those situations, a prompt that asks for intermediate work can make the output more stable than a prompt that simply asks for a polished response.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Rzwu!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7b8f156b-7b95-4fa5-ad9d-aff9173eff5b_2752x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Rzwu!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7b8f156b-7b95-4fa5-ad9d-aff9173eff5b_2752x1536.png 424w, https://substackcdn.com/image/fetch/$s_!Rzwu!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7b8f156b-7b95-4fa5-ad9d-aff9173eff5b_2752x1536.png 848w, https://substackcdn.com/image/fetch/$s_!Rzwu!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7b8f156b-7b95-4fa5-ad9d-aff9173eff5b_2752x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!Rzwu!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7b8f156b-7b95-4fa5-ad9d-aff9173eff5b_2752x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Rzwu!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7b8f156b-7b95-4fa5-ad9d-aff9173eff5b_2752x1536.png" width="1456" height="813" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7b8f156b-7b95-4fa5-ad9d-aff9173eff5b_2752x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:813,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:8266307,&quot;alt&quot;:&quot;Figure 6.7: Reasoning-oriented prompting changes the model&#8217;s process, not just the wording of the final answer, which is especially helpful on ambiguous tasks.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204106890?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7b8f156b-7b95-4fa5-ad9d-aff9173eff5b_2752x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 6.7: Reasoning-oriented prompting changes the model&#8217;s process, not just the wording of the final answer, which is especially helpful on ambiguous tasks." title="Figure 6.7: Reasoning-oriented prompting changes the model&#8217;s process, not just the wording of the final answer, which is especially helpful on ambiguous tasks." srcset="https://substackcdn.com/image/fetch/$s_!Rzwu!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7b8f156b-7b95-4fa5-ad9d-aff9173eff5b_2752x1536.png 424w, https://substackcdn.com/image/fetch/$s_!Rzwu!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7b8f156b-7b95-4fa5-ad9d-aff9173eff5b_2752x1536.png 848w, https://substackcdn.com/image/fetch/$s_!Rzwu!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7b8f156b-7b95-4fa5-ad9d-aff9173eff5b_2752x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!Rzwu!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7b8f156b-7b95-4fa5-ad9d-aff9173eff5b_2752x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The important point is not that the model becomes a person-like reasoner. It does not suddenly think the way a human does. The practical effect is narrower and more useful: the prompt can steer generation toward a stepwise style that exposes intermediate choices, which often improves average reliability on messy tasks. For ReviewDesk, that is helpful when a ticket mentions several plausible categories at once and the model needs to compare evidence instead of guessing from the first clue.</p><h2>Chain-of-thought prompting as single-path guided reasoning</h2><p>Chain-of-thought prompting is the simplest version of this idea. You show the model one worked example that includes intermediate reasoning, then ask a new question in the same format. The example acts as a pattern for the next completion, so the model is more likely to produce a visible reasoning trace before giving the final label. This is useful when the answer depends on a hidden condition or on weighing more than one clue.</p><p>Here is a compact ReviewDesk-style example. The first ticket is answered with a brief justification, and the second ticket is left for the model to solve in the same style.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;b5e8fbe2-f6b9-475c-940e-c94e00cab5ce&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">cot_prompt = [
    {"role": "user", "content": "Ticket: 'I was charged twice, but I also can't access my account after the password reset.' Classify the main issue and briefly explain why."},
    {"role": "assistant", "content": "The billing problem is the clearest actionable issue because the duplicate charge is explicit. Access trouble is present, but the payment issue is the strongest label. Final answer: billing."},
    {"role": "user", "content": "Ticket: 'My subscription renewed today, but the app says my plan expired and the invoice looks wrong.' Classify the main issue and briefly explain why."}
]

outputs = pipe(cot_prompt)
print(outputs[0]["generated_text"])</code></pre></div><p>The worked example matters because it demonstrates not just the final format but the expected treatment of evidence. That is why chain-of-thought often helps on ambiguous support tickets, policy triage, or any other task where the label depends on comparing several plausible interpretations. It is still only a prompt-level nudge, though. A reasoning trace can look convincing and still be wrong if the task statement is unclear or if the model latches onto the wrong clue early.</p><h2>Zero-shot chain-of-thought: triggering reasoning without examples</h2><p>Zero-shot chain-of-thought keeps the same goal but removes the example. Instead of demonstrating reasoning, it asks for it directly with a short cue such as &#8220;think step by step.&#8221; This lighter version is useful when you want to save context, avoid crafting a demonstration, or quickly test whether a task benefits from explicit reasoning guidance.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;f50e5146-0e6b-4083-a813-62259cc7a947&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">zeroshot_cot_prompt = [
    {"role": "user", "content": "Ticket: 'My subscription renewed today, but the app says my plan expired and the invoice looks wrong.' Think step by step, then give the main issue."}
]

outputs = pipe(zeroshot_cot_prompt)
print(outputs[0]["generated_text"])</code></pre></div><p>A short cue can still change generation style because it alters what the model is trying to produce next, even without examples. In practice, this is often a good first step. If it works, the prompt stays compact. If it does not, that is a clue that the task may need a better framing, a few-shot example, or a clearer decision rule rather than just a larger model.</p><h2>Self-consistency: sample several reasoning traces and aggregate the answer</h2><p>Self-consistency goes beyond a single reasoning path. Instead of trusting one completion, you sample several completions under stochastic decoding and then aggregate the final answers. The idea is simple: if the prompt is robust, different sampled traces should arrive at the same label. If the prompt is fragile, the variation between samples exposes that instability, and voting over the answers often improves the final decision.</p><p>This method depends on diversity in the samples, so decoding settings matter. If temperature is too low or top_p is too restrictive, the samples may be nearly identical and self-consistency loses much of its value. If sampling is too loose, the outputs can become noisy in a different way. The practical goal is enough diversity to reveal alternative reasoning paths while keeping the completions plausible.</p><p>For ReviewDesk, this is useful when a ticket could reasonably fit more than one category and the first answer is unreliable. A single pass might lock onto the most obvious word in the text. Several sampled passes make it more likely that the dominant pattern emerges, and a simple majority vote over the final labels often gives a better result.</p><p>The tradeoff is cost. Multiple samples mean more tokens, more latency, and more compute expense. That makes self-consistency a good fit for unstable or high-value decisions, but a poor fit for easy tasks where a single answer is already good enough.</p><h2>Tree-of-thought: branching into alternative solution paths</h2><p>Tree-of-thought extends the same general idea, but it changes the unit of exploration. Instead of sampling several final answers, it branches into several intermediate solution paths, compares them, and keeps the promising ones while discarding the weak ones. The key difference is that this is about path exploration, not answer voting.</p><p>That distinction is important. Self-consistency asks, in effect, which complete answer shows up most reliably across samples. Tree-of-thought asks which partial line of reasoning appears most promising before the answer is finalized. It is a drafting-and-narrowing process rather than a voting process.</p><p>A simplified ReviewDesk prompt can imitate this by asking for a few candidate interpretations and then a final choice.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;2561ab67-7836-4bbd-b833-05ee1961ea32&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">tot_prompt = [
    {"role": "user", "content": "Ticket: 'I was charged after canceling, and the dashboard still says my payment failed.' Consider a few possible interpretations, compare them briefly, then choose the best category and explain why."}
]

outputs = pipe(tot_prompt)
print(outputs[0]["generated_text"])</code></pre></div><p>You can think of the method as asking the model to draft several candidate explanations, debate which one fits best, and then narrow to the strongest branch. That is enough to capture the practical idea without turning the prompt into a full search algorithm. Tree-of-thought is most useful when the task is open-ended or when the model needs to choose among several plausible paths rather than retrieve one obvious fact.</p><h2>Choosing a reasoning strategy in practice</h2><p>A simple mental model helps. Use a single-pass prompt when the task is straightforward. Use self-consistency when the task is unstable and the cost of a wrong answer is higher than the cost of extra sampling. Use tree-of-thought when the task requires comparing several competing interpretations or exploring a small decision space before choosing.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!qo_S!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08cecff9-514d-4884-b5c2-02f95223d63d_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!qo_S!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08cecff9-514d-4884-b5c2-02f95223d63d_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!qo_S!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08cecff9-514d-4884-b5c2-02f95223d63d_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!qo_S!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08cecff9-514d-4884-b5c2-02f95223d63d_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!qo_S!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08cecff9-514d-4884-b5c2-02f95223d63d_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!qo_S!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08cecff9-514d-4884-b5c2-02f95223d63d_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/08cecff9-514d-4884-b5c2-02f95223d63d_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:7271415,&quot;alt&quot;:&quot;Figure 6.8: A practical rule of thumb: use single-pass prompts for easy tasks, self-consistency for unstable decisions, and tree-of-thought when the model must explore competing interpretations.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204106890?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08cecff9-514d-4884-b5c2-02f95223d63d_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 6.8: A practical rule of thumb: use single-pass prompts for easy tasks, self-consistency for unstable decisions, and tree-of-thought when the model must explore competing interpretations." title="Figure 6.8: A practical rule of thumb: use single-pass prompts for easy tasks, self-consistency for unstable decisions, and tree-of-thought when the model must explore competing interpretations." srcset="https://substackcdn.com/image/fetch/$s_!qo_S!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08cecff9-514d-4884-b5c2-02f95223d63d_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!qo_S!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08cecff9-514d-4884-b5c2-02f95223d63d_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!qo_S!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08cecff9-514d-4884-b5c2-02f95223d63d_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!qo_S!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08cecff9-514d-4884-b5c2-02f95223d63d_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>All three methods improve prompt behavior and often improve average outcomes, but none of them guarantees correctness. They reduce uncertainty; they do not eliminate it. If the ticket is ambiguous, underspecified, or high stakes, you should still verify the result with downstream checks or human review. The strongest practical benefit of reasoning-oriented prompting is not perfect logic. It is a better chance of getting a careful answer when the problem itself is not already easy.</p><h2>Structured Output, Verification, and Constrained Decoding</h2><p>Structured output becomes a production requirement the moment a model response stops being a reading experience and starts being input for something else. A support triage system cannot &#8220;mostly&#8221; receive JSON. A database loader cannot guess where the category field ended. An API client cannot safely consume prose that looks organized to a human but fails at the border checkpoint of a parser. This is the practical distinction that matters here: a response can look right on screen and still not run right in software.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!4iLt!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1fde96c5-b704-41d3-a3e3-81db026c766a_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!4iLt!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1fde96c5-b704-41d3-a3e3-81db026c766a_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!4iLt!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1fde96c5-b704-41d3-a3e3-81db026c766a_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!4iLt!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1fde96c5-b704-41d3-a3e3-81db026c766a_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!4iLt!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1fde96c5-b704-41d3-a3e3-81db026c766a_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!4iLt!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1fde96c5-b704-41d3-a3e3-81db026c766a_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1fde96c5-b704-41d3-a3e3-81db026c766a_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:7192827,&quot;alt&quot;:&quot;Figure 6.9: A response can appear correct to a person while still failing at the parser boundary; downstream software requires machine-valid structure, not just visual plausibility.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204106890?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1fde96c5-b704-41d3-a3e3-81db026c766a_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 6.9: A response can appear correct to a person while still failing at the parser boundary; downstream software requires machine-valid structure, not just visual plausibility." title="Figure 6.9: A response can appear correct to a person while still failing at the parser boundary; downstream software requires machine-valid structure, not just visual plausibility." srcset="https://substackcdn.com/image/fetch/$s_!4iLt!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1fde96c5-b704-41d3-a3e3-81db026c766a_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!4iLt!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1fde96c5-b704-41d3-a3e3-81db026c766a_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!4iLt!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1fde96c5-b704-41d3-a3e3-81db026c766a_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!4iLt!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1fde96c5-b704-41d3-a3e3-81db026c766a_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The simplest way to improve structure is to ask for it directly. A zero-shot prompt can request a fixed schema, but the model is still free to drift, add commentary, or return partial objects. In ReviewDesk, you might ask for a ticket classification result containing a category, a priority, and a short draft reply. That is enough to guide the model, but not enough to guarantee anything. If the task is forgiving, a prompt alone may be acceptable. If the output is going straight into another system, the same request becomes fragile.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;df31863f-f89d-48a0-8fdb-f659ebfba0d3&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">prompt = [
    {
        "role": "user",
        "content": (
            "Classify this ReviewDesk ticket and return valid JSON with exactly these keys: "
            "category, priority, draft_response. "
            "Ticket: 'My login works on mobile but not on desktop after the last update.'"
        ),
    }
]

output = llm.create_chat_completion(messages=prompt, temperature=0)["choices"][0]["message"]["content"]
print(output)</code></pre></div><p>A one-shot or few-shot template usually improves adherence because the model sees the target shape rather than merely being told about it. The example acts like a small map of the destination. You are not training the model, and you are not changing its weights; you are narrowing the space of likely completions by showing a format that should be imitated. That distinction is easy to miss. In-context examples are temporary steering signals, not new knowledge stored by the model.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;e4d8d577-ee7f-4ff3-8709-f7c73a2a5744&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">prompt = [
    {
        "role": "user",
        "content": (
            "Return ReviewDesk tickets in this JSON format only:\n"
            '{"category":"...","priority":"...","draft_response":"..."}\n\n'
            "Example:\n"
            '{"category":"authentication","priority":"high","draft_response":"Please try resetting your password..."}\n\n'
            "Ticket: 'The billing page shows an error when I open it from Safari.'"
        ),
    }
]

output = llm.create_chat_completion(messages=prompt, temperature=0)["choices"][0]["message"]["content"]
print(output)</code></pre></div><p>This is where the contrast between guidance and enforcement becomes important. Prompt shaping is persuasive; constrained decoding is restrictive. Prompting tells the model what kind of answer is wanted. Grammar-constrained decoding or JSON-enforced generation limits the next token choices so the model can only produce output that fits the allowed format. In other words, prompting steers the car, while constrained decoding puts rails on the road. The prompt still matters, but the runtime is now doing part of the reliability work.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Q4oG!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6c7b9ff-1549-4d1c-a619-fb2b9feb23f5_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Q4oG!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6c7b9ff-1549-4d1c-a619-fb2b9feb23f5_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!Q4oG!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6c7b9ff-1549-4d1c-a619-fb2b9feb23f5_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!Q4oG!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6c7b9ff-1549-4d1c-a619-fb2b9feb23f5_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!Q4oG!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6c7b9ff-1549-4d1c-a619-fb2b9feb23f5_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Q4oG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6c7b9ff-1549-4d1c-a619-fb2b9feb23f5_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e6c7b9ff-1549-4d1c-a619-fb2b9feb23f5_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:7486089,&quot;alt&quot;:&quot;Figure 6.10: Prompting nudges the model toward the desired structure, while constrained decoding limits token choices so only schema-valid output can be produced.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204106890?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6c7b9ff-1549-4d1c-a619-fb2b9feb23f5_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 6.10: Prompting nudges the model toward the desired structure, while constrained decoding limits token choices so only schema-valid output can be produced." title="Figure 6.10: Prompting nudges the model toward the desired structure, while constrained decoding limits token choices so only schema-valid output can be produced." srcset="https://substackcdn.com/image/fetch/$s_!Q4oG!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6c7b9ff-1549-4d1c-a619-fb2b9feb23f5_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!Q4oG!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6c7b9ff-1549-4d1c-a619-fb2b9feb23f5_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!Q4oG!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6c7b9ff-1549-4d1c-a619-fb2b9feb23f5_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!Q4oG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6c7b9ff-1549-4d1c-a619-fb2b9feb23f5_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>With runtimes that support structured generation, the same ReviewDesk task can be asked for as JSON-only output. A model such as a quantized GGUF chat model loaded through <code>llama-cpp-python</code> can be configured so the response must satisfy a JSON grammar or a JSON response format. That does not make the answer correct in the semantic sense, but it does strongly improve the guarantee that the string is machine-readable.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;6fee3632-9f7b-40d7-aedd-7e71bd0bc732&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from llama_cpp.llama import Llama

llm = Llama.from_pretrained(
    repo_id="microsoft/Phi-3-mini-4k-instruct-gguf",
    filename="*fp16.gguf",
    n_gpu_layers=-1,
    n_ctx=2048,
    verbose=False,
)

output = llm.create_chat_completion(
    messages=[
        {
            "role": "user",
            "content": (
                "For this ReviewDesk ticket, return JSON with category, priority, and draft_response. "
                "Ticket: 'My invoice downloaded as a blank PDF.'"
            ),
        }
    ],
    response_format={"type": "json_object"},
    temperature=0,
)["choices"][0]["message"]["content"]

print(output)</code></pre></div><p>The runtime can enforce shape, but it cannot enforce truth. That is why verification remains necessary. A visually plausible blob of text is not the same thing as valid JSON, and valid JSON is not the same thing as a correct or useful answer. Parsing is the first real test. If <code>json.loads</code> fails, the output is not ready. If parsing succeeds, the next step is to check that the expected fields are present and that their types make sense for the downstream consumer.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;45ddb9b1-2786-4191-9d00-5a0c3daac3a4&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">import json

parsed = json.loads(output)
print(json.dumps(parsed, indent=2))

required_keys = {"category", "priority", "draft_response"}
if not required_keys.issubset(parsed):
    raise ValueError(f"Missing keys: {required_keys - set(parsed)}")

if not isinstance(parsed["category"], str):
    raise TypeError("category must be a string")
if not isinstance(parsed["priority"], str):
    raise TypeError("priority must be a string")
if not isinstance(parsed["draft_response"], str):
    raise TypeError("draft_response must be a string")</code></pre></div><p>That small parse-and-validate step is more than a convenience. It is the border checkpoint between generated text and usable data. If the model returns a malformed object, a truncated string, or a response with commentary wrapped around the JSON, the failure mode should be classified rather than ignored. A malformed object usually calls for regeneration. A truncated object often points to token limits or overly long outputs. Commentary-laden output may be fixed by stronger instructions or by constrained decoding. In a brittle pipeline, the right response is often to retry with lower temperature, a tighter schema, or a stricter decoder. In a less brittle workflow, it may be acceptable to reject the result and fall back to a human review path.</p><p>The operational rule is simple. Shape with examples when failure is tolerable, enforce with decoding when the downstream consumer is brittle, and validate whenever the output is reused programmatically. That sequence keeps prompt engineering from becoming guesswork. It treats malformed output as a workflow incident rather than as a cosmetic annoyance, which is exactly the mindset needed when a model stops being a chatbot and starts being a component.</p><p><em>Use the url below to download the enitre book as pdf:</em></p>
      <p>
          <a href="https://onepagecode.substack.com/p/prompt-engineering">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Text Clustering and Topic Modeling]]></title><description><![CDATA[Chapter 5 of Hands-On Large Language Models: Leveraging dense embeddings, dimensionality reduction (UMAP), density-based clustering (HDBSCAN), and BERTopic workflows for topic discovery]]></description><link>https://onepagecode.substack.com/p/text-clustering-and-topic-modeling</link><guid isPermaLink="false">https://onepagecode.substack.com/p/text-clustering-and-topic-modeling</guid><dc:creator><![CDATA[Onepagecode]]></dc:creator><pubDate>Tue, 07 Jul 2026 19:44:03 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!2V5g!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4d208012-f50a-455a-9f73-9532b2a62632_2816x1536.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>Why Cluster Documents Before You Label Topics?</h1><p>When a text collection gets large enough, the first problem is usually not prediction but orientation. A support team may have thousands of ReviewDesk tickets, and reading them one by one is like opening a crowded inbox after a long weekend: you can see that there is structure, but you cannot yet see the structure clearly enough to act on it. Unsupervised text clustering gives you a way to sort that inbox by similarity before you decide what the folders should be called. The goal is not to assign final categories on day one. The goal is to reveal recurring shapes in the data so that a human can inspect them with much less effort.</p><p>That distinction matters. A cluster is a discovered grouping of documents that appear to belong together; a topic label is the human-readable name you attach after you inspect that group. Those are related tasks, but they are not the same task. If you collapse them too early, you end up treating an interpretive label as if it were an objective property of the corpus. In practice, clustering is the intermediate representation that helps you get from a messy archive to a manageable set of candidate themes.</p><p>The reason embedding-based methods work so well here is that they group by meaning rather than by surface overlap. Two ReviewDesk tickets can talk about the same billing issue using very different words, and a keyword-matching approach may miss that relationship entirely. A semantic model can still place them near each other because it has learned that the underlying intent is similar, even when the vocabulary is not. That is the crucial advantage of dense representations: they make latent structure visible when the obvious words are misleading.</p><h2><strong>&#128216; Buy the Entire Book &#8211; Available on Amazon Worldwide (If you are paid subscriber, use the url at the end of this article to download the book for free!)</strong></h2><p>You can now purchase the complete book directly from Amazon in your country:</p><ul><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/us/dualbookshelf.marketplacelink/B0H6WZJ57S">United States (US)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/uk/dualbookshelf.marketplacelink/B0H6WZJ57S">United Kingdom (UK)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/de/dualbookshelf.marketplacelink/B0H6WZJ57S">Germany (DE)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/fr/dualbookshelf.marketplacelink/B0H6WZJ57S">France (FR)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/es/dualbookshelf.marketplacelink/B0H6WZJ57S">Spain (ES)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/it/dualbookshelf.marketplacelink/B0H6WZJ57S">Italy (IT)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/nl/dualbookshelf.marketplacelink/B0H6WZJ57S">Netherlands (NL)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/jp/dualbookshelf.marketplacelink/B0H6WZJ57S">Japan (JP)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/br/dualbookshelf.marketplacelink/B0H6WZJ57S">Brazil (BR):</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/ca/dualbookshelf.marketplacelink/B0H6WZJ57S">Canada (CA)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/mx/dualbookshelf.marketplacelink/B0H6WZJ57S">Mexico (MX)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/au/dualbookshelf.marketplacelink/B0H6WZJ57S">Australia (AU)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/in/dualbookshelf.marketplacelink/B0H6WZJ57S">India (IN)</a></strong></p></li></ul><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!2V5g!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4d208012-f50a-455a-9f73-9532b2a62632_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!2V5g!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4d208012-f50a-455a-9f73-9532b2a62632_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!2V5g!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4d208012-f50a-455a-9f73-9532b2a62632_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!2V5g!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4d208012-f50a-455a-9f73-9532b2a62632_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!2V5g!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4d208012-f50a-455a-9f73-9532b2a62632_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!2V5g!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4d208012-f50a-455a-9f73-9532b2a62632_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4d208012-f50a-455a-9f73-9532b2a62632_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:5434419,&quot;alt&quot;:&quot;Figure 5.1: Dense embeddings can place documents near each other by meaning, even when the wording is different, making latent structure visible to clustering algorithms.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204106410?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4d208012-f50a-455a-9f73-9532b2a62632_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 5.1: Dense embeddings can place documents near each other by meaning, even when the wording is different, making latent structure visible to clustering algorithms." title="Figure 5.1: Dense embeddings can place documents near each other by meaning, even when the wording is different, making latent structure visible to clustering algorithms." srcset="https://substackcdn.com/image/fetch/$s_!2V5g!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4d208012-f50a-455a-9f73-9532b2a62632_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!2V5g!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4d208012-f50a-455a-9f73-9532b2a62632_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!2V5g!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4d208012-f50a-455a-9f73-9532b2a62632_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!2V5g!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4d208012-f50a-455a-9f73-9532b2a62632_2816x1536.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>This is especially useful in unlabeled corpora, where classification is not available or not worth the cost. In many teams, labels are incomplete, stale, or too coarse to explain the patterns hidden in the archive. Clustering gives you a fast way to explore the shape of the dataset before you commit to a taxonomy. In a support setting, that might surface separate piles for product bugs, billing questions, feature requests, and operational issues. In a media workflow, the same idea could expose groups of similar assets, recurring content briefs, or unusual one-off submissions that deserve special handling.</p><p>Outliers are part of the value, not a nuisance to be ignored. A density-based method may leave some documents unassigned because they do not fit any sufficiently coherent group. That is often exactly what you want. The odd ticket that references two unrelated problems, the duplicate message copied from another channel, or the rare edge case from a premium customer can be more informative than a clean cluster. These documents tell you where the corpus is noisy, where workflows are messy, and where your categories may need revision.</p><p>Once you have clusters, topic modeling becomes a second layer of interpretation rather than a replacement for discovery. The cluster tells you that a set of documents belong together; the topic label tells you what to call that set in a way a person can read quickly. In that sense, topic modeling is like writing captions for a photo album after the photos have already been grouped. The grouping comes first because it reduces the search space. The naming comes later because it is easier to label a coherent pile than to assign meaning to thousands of isolated documents.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!bzJk!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5fc44823-aabd-46b8-ab41-7348b08d48cc_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!bzJk!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5fc44823-aabd-46b8-ab41-7348b08d48cc_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!bzJk!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5fc44823-aabd-46b8-ab41-7348b08d48cc_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!bzJk!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5fc44823-aabd-46b8-ab41-7348b08d48cc_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!bzJk!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5fc44823-aabd-46b8-ab41-7348b08d48cc_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!bzJk!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5fc44823-aabd-46b8-ab41-7348b08d48cc_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5fc44823-aabd-46b8-ab41-7348b08d48cc_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:5610316,&quot;alt&quot;:&quot;Figure 5.2: Topic labeling is a later interpretive step: clustering first discovers coherent document groups, then humans name those groups after inspection.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204106410?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5fc44823-aabd-46b8-ab41-7348b08d48cc_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 5.2: Topic labeling is a later interpretive step: clustering first discovers coherent document groups, then humans name those groups after inspection." title="Figure 5.2: Topic labeling is a later interpretive step: clustering first discovers coherent document groups, then humans name those groups after inspection." srcset="https://substackcdn.com/image/fetch/$s_!bzJk!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5fc44823-aabd-46b8-ab41-7348b08d48cc_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!bzJk!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5fc44823-aabd-46b8-ab41-7348b08d48cc_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!bzJk!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5fc44823-aabd-46b8-ab41-7348b08d48cc_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!bzJk!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5fc44823-aabd-46b8-ab41-7348b08d48cc_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>That is the workflow this chapter develops. We will start with embeddings, use them to group documents semantically, and then refine those groups into readable topics. The later BERTopic-style steps will add keyword summaries, representation methods, and labeling tools, but their usefulness depends on this first move: discovering clusters before pretending to know their names.</p><h2>Anchor the Running Corpus</h2><p>The chapter&#8217;s shared corpus is the ArXiv cs.CL collection, a convenient stand-in for a real document stream that already contains a broad spread of computation and language research abstracts. Treat it as the chapter&#8217;s working file cabinet: the documents are already collected, but they still need to be arranged into a clean structure before any semantic grouping can happen. That structure matters because clustering does not begin with labels; it begins with a corpus that can be read consistently, inspected later, and passed unchanged through the next stages of the pipeline.</p><h2>Load the Dataset from Hugging Face</h2><p>The starting point is intentionally simple. The dataset is fetched from Hugging Face, and the chapter uses the training split as the source corpus. That gives us a reproducible pool of documents without needing to assemble the collection by hand. In practice, this kind of loading step is the bridge between a hosted dataset and an analysis-ready document table.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Tbeu!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F90a1e6c2-8381-422f-8c84-ad29c8a5afb7_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Tbeu!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F90a1e6c2-8381-422f-8c84-ad29c8a5afb7_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!Tbeu!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F90a1e6c2-8381-422f-8c84-ad29c8a5afb7_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!Tbeu!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F90a1e6c2-8381-422f-8c84-ad29c8a5afb7_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!Tbeu!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F90a1e6c2-8381-422f-8c84-ad29c8a5afb7_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Tbeu!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F90a1e6c2-8381-422f-8c84-ad29c8a5afb7_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/90a1e6c2-8381-422f-8c84-ad29c8a5afb7_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:6745411,&quot;alt&quot;:&quot;Figure 5.3: Loading a hosted dataset and separating abstracts from titles produces an analysis-ready corpus table for later embedding and clustering.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204106410?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F90a1e6c2-8381-422f-8c84-ad29c8a5afb7_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 5.3: Loading a hosted dataset and separating abstracts from titles produces an analysis-ready corpus table for later embedding and clustering." title="Figure 5.3: Loading a hosted dataset and separating abstracts from titles produces an analysis-ready corpus table for later embedding and clustering." srcset="https://substackcdn.com/image/fetch/$s_!Tbeu!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F90a1e6c2-8381-422f-8c84-ad29c8a5afb7_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!Tbeu!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F90a1e6c2-8381-422f-8c84-ad29c8a5afb7_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!Tbeu!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F90a1e6c2-8381-422f-8c84-ad29c8a5afb7_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!Tbeu!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F90a1e6c2-8381-422f-8c84-ad29c8a5afb7_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;b48f2647-328f-4964-b9be-c2af8d65606e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from datasets import load_dataset

dataset = load_dataset("maartengr/arxiv_nlp")["train"]

abstracts = dataset["Abstracts"]
titles = dataset["Titles"]</code></pre></div><p>The code does not do any modeling yet. Its job is to make the corpus explicit. If the public dataset is unavailable in another setting, the same pattern still applies to a domain corpus such as ReviewDesk support tickets: load the source table, choose the slice you want to analyze, and separate the text you want to embed from the metadata you want to keep for interpretation.</p><h2>Separate Text from Metadata</h2><p>Here the abstracts are the primary documents. They are the text that will later be embedded, compared, and grouped. Titles play a different role. They are lightweight metadata that help you inspect a cluster, understand a document at a glance, and later assign or evaluate a topic label. Keeping those two fields separate is a small design choice with large downstream consequences. If you merge the title into the abstract too early, you blur the boundary between content and annotation, and later inspection becomes noisier than it needs to be.</p><p>A useful way to think about it is that the abstract is the folder contents, while the title is the tab on the folder. Clustering works on the contents; the tab helps you understand what ended up together after the fact. That separation also makes error analysis easier, because you can ask whether a document landed in the right group without having to untangle extra metadata from the text itself.</p><h2>Sanity-Check the Corpus Shape</h2><p>Before moving on, it is worth confirming that the corpus is usable as a one-document-per-row collection. The abstract and title arrays should stay aligned, and missing entries should be understood before they propagate into later steps. Even when the dataset is already curated, this is the point where a quick structural check pays off: if the document table is misaligned now, every cluster inspection afterward will inherit the mistake.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!jg7-!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd2fa064a-b9dc-4f4d-be2a-248fba4f580a_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!jg7-!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd2fa064a-b9dc-4f4d-be2a-248fba4f580a_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!jg7-!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd2fa064a-b9dc-4f4d-be2a-248fba4f580a_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!jg7-!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd2fa064a-b9dc-4f4d-be2a-248fba4f580a_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!jg7-!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd2fa064a-b9dc-4f4d-be2a-248fba4f580a_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!jg7-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd2fa064a-b9dc-4f4d-be2a-248fba4f580a_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d2fa064a-b9dc-4f4d-be2a-248fba4f580a_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:6864659,&quot;alt&quot;:&quot;Figure 5.4: A quick alignment and missing-value check prevents corpus-table errors from propagating into downstream clustering and inspection.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204106410?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd2fa064a-b9dc-4f4d-be2a-248fba4f580a_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 5.4: A quick alignment and missing-value check prevents corpus-table errors from propagating into downstream clustering and inspection." title="Figure 5.4: A quick alignment and missing-value check prevents corpus-table errors from propagating into downstream clustering and inspection." srcset="https://substackcdn.com/image/fetch/$s_!jg7-!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd2fa064a-b9dc-4f4d-be2a-248fba4f580a_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!jg7-!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd2fa064a-b9dc-4f4d-be2a-248fba4f580a_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!jg7-!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd2fa064a-b9dc-4f4d-be2a-248fba4f580a_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!jg7-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd2fa064a-b9dc-4f4d-be2a-248fba4f580a_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2>Why This Structure Supports Later Clustering</h2><p>This preparation step does not yet cluster anything, but it sets up the exact input shape that clustering expects: a clean collection of documents with stable metadata attached beside them. That makes the later embedding stage straightforward, and it gives you a reliable way to read the results once groups begin to emerge. In unsupervised text analysis, the quality of the final clusters is only part of the story; the quality of the document table determines whether those clusters can be interpreted with confidence.</p><h2>From Embeddings to Clusters and Visual Maps</h2><h3>Why embeddings are the right starting point for text groups</h3><p>If you want to sort a messy inbox, you do not begin by counting shared words and hoping the piles organize themselves. That kind of sparse lexical view is useful for some problems, but it is a weak way to group whole documents by meaning. Two support tickets can describe the same issue with different product names and different wording, yet still belong together. Semantic embeddings handle that better because they turn each document into a dense vector where related texts end up near one another.</p><p>That is why this workflow starts with embeddings rather than bag-of-words or TF-IDF. The goal is not to preserve exact vocabulary. The goal is to preserve enough semantic neighborhood structure that similar documents can be grouped even when surface wording differs. In practice, this makes a mixed corpus easier to sort before you assign human-readable topic names.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!4htA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F57573f8a-dfa3-4452-b869-34e1b17020b0_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!4htA!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F57573f8a-dfa3-4452-b869-34e1b17020b0_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!4htA!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F57573f8a-dfa3-4452-b869-34e1b17020b0_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!4htA!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F57573f8a-dfa3-4452-b869-34e1b17020b0_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!4htA!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F57573f8a-dfa3-4452-b869-34e1b17020b0_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!4htA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F57573f8a-dfa3-4452-b869-34e1b17020b0_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/57573f8a-dfa3-4452-b869-34e1b17020b0_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:5741465,&quot;alt&quot;:&quot;Figure 5.5: Dense semantic embeddings preserve neighborhood structure, making it possible to group documents by meaning even when they share few surface words.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204106410?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F57573f8a-dfa3-4452-b869-34e1b17020b0_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 5.5: Dense semantic embeddings preserve neighborhood structure, making it possible to group documents by meaning even when they share few surface words." title="Figure 5.5: Dense semantic embeddings preserve neighborhood structure, making it possible to group documents by meaning even when they share few surface words." srcset="https://substackcdn.com/image/fetch/$s_!4htA!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F57573f8a-dfa3-4452-b869-34e1b17020b0_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!4htA!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F57573f8a-dfa3-4452-b869-34e1b17020b0_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!4htA!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F57573f8a-dfa3-4452-b869-34e1b17020b0_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!4htA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F57573f8a-dfa3-4452-b869-34e1b17020b0_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Think of embeddings as a smarter sorting surface. The model has not named the folders yet, but it has already arranged the documents so likely neighbors sit close together. That is the foundation for the rest of the pipeline.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;a027ed44-03bc-45c7-9133-c7a2096ec9dc&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from sentence_transformers import SentenceTransformer

embedding_model = SentenceTransformer("thenlper/gte-small")
embeddings = embedding_model.encode(titles_and_texts, show_progress_bar=True)</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;fbfe2116-cb39-4fbb-95cf-f3786c25b848&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">embeddings.shape</code></pre></div><h3>From high-dimensional vectors to a clusterable representation</h3><p>The embedding matrix is still high-dimensional, which is good for representation but awkward for clustering. Many clustering methods become less effective when the space is wide and sparse, especially when the useful structure lives in local neighborhoods rather than across every dimension. Reducing the vectors first makes the geometry more tractable.</p><p>This reduction has two distinct jobs in the workflow. One reduced space is used as the working representation for clustering. A separate two-dimensional projection is used later for plotting. The first helps the algorithm organize the documents. The second helps you inspect the result. Those are related, but they are not the same step.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!fvVH!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf1b3c2a-0f21-459d-938b-cd2bbfce9fc0_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!fvVH!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf1b3c2a-0f21-459d-938b-cd2bbfce9fc0_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!fvVH!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf1b3c2a-0f21-459d-938b-cd2bbfce9fc0_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!fvVH!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf1b3c2a-0f21-459d-938b-cd2bbfce9fc0_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!fvVH!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf1b3c2a-0f21-459d-938b-cd2bbfce9fc0_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!fvVH!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf1b3c2a-0f21-459d-938b-cd2bbfce9fc0_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bf1b3c2a-0f21-459d-938b-cd2bbfce9fc0_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:7166513,&quot;alt&quot;:&quot;Figure 5.6: Dimensionality reduction serves two different roles here: a reduced working space for clustering and a separate 2D projection for visualization.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204106410?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf1b3c2a-0f21-459d-938b-cd2bbfce9fc0_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 5.6: Dimensionality reduction serves two different roles here: a reduced working space for clustering and a separate 2D projection for visualization." title="Figure 5.6: Dimensionality reduction serves two different roles here: a reduced working space for clustering and a separate 2D projection for visualization." srcset="https://substackcdn.com/image/fetch/$s_!fvVH!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf1b3c2a-0f21-459d-938b-cd2bbfce9fc0_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!fvVH!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf1b3c2a-0f21-459d-938b-cd2bbfce9fc0_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!fvVH!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf1b3c2a-0f21-459d-938b-cd2bbfce9fc0_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!fvVH!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf1b3c2a-0f21-459d-938b-cd2bbfce9fc0_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The distance measure matters too. Cosine distance fits embedding-based text similarity well because semantic embeddings are usually interpreted by direction rather than raw magnitude. If two documents point in a similar semantic direction, they should be treated as close even if their lengths differ or their vector norms do not match exactly.</p><p>A useful way to picture this is a working geometry, not a true map. The reducer is trying to preserve the neighborhoods that matter for grouping, not to reveal some perfect universal coordinate system for language.</p><h3>Using UMAP to preserve neighborhood structure</h3><p>UMAP is a practical reducer for this task because it aims to preserve local neighborhoods while compressing the data into fewer dimensions. Documents that are close in the original embedding space should remain close after reduction, at least well enough for clustering to use the result. That makes UMAP useful both as a preparatory step for clustering and later as a projection for display.</p><p>The main parameters here are easy to read in conceptual terms. <code>n_components</code> sets the size of the reduced working space. A small value such as five often gives a clustering algorithm enough room to separate groups without keeping the full embedding width. <code>metric</code> is set to <code>cosine</code> so the reduction respects embedding geometry. <code>min_dist</code> controls how tightly neighboring points may pack together, and a small value encourages compact local structure. <code>random_state</code> fixes the stochastic parts of the process so repeated runs are easier to compare.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;b2dffb8e-7f9b-423f-b684-0d76555fd028&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from umap import UMAP

umap_model = UMAP(
    n_components=5,
    min_dist=0.0,
    metric="cosine",
    random_state=42,
)
reduced_embeddings = umap_model.fit_transform(embeddings)</code></pre></div><p>That reproducibility matters because cluster results are not absolute facts. They depend on the model, the reducer, and the seed. If the seed changes, the exact arrangement can change too. Holding it fixed makes interpretation much easier when you are trying to understand whether a group is stable or just an artifact of a particular run.</p><h3>Letting HDBSCAN discover groups and reject noise</h3><p>Once the embeddings have been reduced, HDBSCAN can discover clusters without requiring you to choose <code>k</code> in advance. That is one reason it works well for open-ended text corpora. You do not always know how many themes are present, and forcing a fixed cluster count can be awkward when the corpus is messy or uneven.</p><p>HDBSCAN looks for dense regions in the reduced space. Where points form a compact neighborhood, it assigns a cluster. Where points are sparse, ambiguous, or transitional, it can leave them unassigned. That unassigned behavior is not a bug. It is often the most useful part of the method, because real corpora usually contain items that do not belong cleanly anywhere.</p><p>The label <code>-1</code> marks those noise points. In practice, that means borderline documents, vague requests, or one-off items can be separated from the main clusters instead of being forced into a misleading group. If you expect messy real-world text, this is a valuable feature, not a failure.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;e342df11-3e6f-4abe-8e32-968a95be3f79&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from hdbscan import HDBSCAN

hdbscan_model = HDBSCAN(
    min_cluster_size=50,
    metric="euclidean",
    cluster_selection_method="eom",
).fit(reduced_embeddings)
clusters = hdbscan_model.labels_

len(set(clusters))</code></pre></div><p>The exact cluster labels are parameter-dependent. Change the embedding model, the UMAP settings, or the HDBSCAN thresholds, and the cluster assignment can shift. That is normal. The point is not to treat one run as permanent truth. The point is to get a usable grouping that fits the corpus and the task.</p><h3>Checking cluster meaning by reading sample documents</h3><p>A cluster becomes meaningful only after you read some of the documents inside it. The simplest validation habit is to sample a few members from one cluster and ask whether they share a recognizable theme. In a support-ticket corpus, that might reveal a cluster about login failures, another about billing corrections, or another about export requests.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;55f28549-6e91-448f-8582-1675f4389d0e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">import numpy as np

cluster = 0
for index in np.where(clusters == cluster)[0][:3]:
    print(titles_and_texts[index][:300] + "...\n")</code></pre></div><p>This step catches an important failure mode. A cluster can look neat numerically and still mix several subthemes that happen to share vocabulary. Human reading is what tells you whether the cluster is genuinely coherent or merely compact in reduced space. Numerical grouping gives you structure, but human inspection gives you confidence.</p><h3>Projecting into two dimensions for diagnosis, not proof</h3><p>After clustering, you can make a two-dimensional projection for plotting. This is a diagnostic view, not the clustering step itself. It helps you spot broad structure, noise points, and overlap, but it is not a ground-truth map of meaning. A nice scatterplot can be misleading if you treat it as proof that the clusters are perfectly pure.</p><p>A practical plotting convention is to keep outliers separate from clustered points. Noise points are often drawn in gray so they recede visually, while clustered points are colored by cluster ID. The axes are usually hidden because the exact coordinates matter less than the relative arrangement.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;5b997b2f-ff66-4712-8d11-6da8251659f8&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">import pandas as pd
from umap import UMAP

plot_embeddings = UMAP(
    n_components=2,
    min_dist=0.0,
    metric="cosine",
    random_state=42,
).fit_transform(embeddings)

df = pd.DataFrame(plot_embeddings, columns=["x", "y"])
df["title"] = titles
df["cluster"] = [str(c) for c in clusters]

to_plot = df.loc[df.cluster != "-1", :]
outliers = df.loc[df.cluster == "-1", :]</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;6e179bb7-1063-4044-93bf-de5ebcf43cd3&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">import matplotlib.pyplot as plt

plt.scatter(outliers.x, outliers.y, alpha=0.05, s=2, c="grey")
plt.scatter(
    to_plot.x,
    to_plot.y,
    c=to_plot.cluster.astype(int),
    alpha=0.6,
    s=2,
    cmap="tab20b",
)
plt.axis("off")</code></pre></div><p>Read that plot as an X-ray, not as a map. It can show where clusters sit, where outliers collect, and where the method may be uncertain. It cannot prove semantic purity. Visual separation and semantic purity are related, but they are not identical. Two clusters can look far apart and still share content, and two clusters can sit near each other while still representing distinct themes.</p><p>This is also where the workflow connects to topic modeling. Clustering has organized the corpus into semantically related groups, but those groups still need readable topic descriptions and representative terms. The next section builds on that structure rather than starting over from scratch.</p><h2>From Cluster IDs to Topic Representations</h2><p>Once you have clustered document embeddings, you have already found something useful, but the result is still unnamed. Clustering tells you which documents belong together; topic modeling asks what those groups are about. BERTopic treats those as separate layers. It can reuse the embeddings, UMAP reduction, and HDBSCAN clusters from the previous step, then build a topic representation on top of them. The cluster assignment is the backbone, and the topic name is the label that comes later.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!3vBC!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F108e2575-e3ab-4198-962a-0bc197728f10_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!3vBC!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F108e2575-e3ab-4198-962a-0bc197728f10_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!3vBC!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F108e2575-e3ab-4198-962a-0bc197728f10_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!3vBC!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F108e2575-e3ab-4198-962a-0bc197728f10_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!3vBC!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F108e2575-e3ab-4198-962a-0bc197728f10_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!3vBC!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F108e2575-e3ab-4198-962a-0bc197728f10_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/108e2575-e3ab-4198-962a-0bc197728f10_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:6374898,&quot;alt&quot;:&quot;Figure 5.7: BERTopic keeps clustering and topic naming separate: embeddings and clusters are reused, while topic labels are built and refined on top of the fixed cluster assignments.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204106410?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F108e2575-e3ab-4198-962a-0bc197728f10_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 5.7: BERTopic keeps clustering and topic naming separate: embeddings and clusters are reused, while topic labels are built and refined on top of the fixed cluster assignments." title="Figure 5.7: BERTopic keeps clustering and topic naming separate: embeddings and clusters are reused, while topic labels are built and refined on top of the fixed cluster assignments." srcset="https://substackcdn.com/image/fetch/$s_!3vBC!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F108e2575-e3ab-4198-962a-0bc197728f10_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!3vBC!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F108e2575-e3ab-4198-962a-0bc197728f10_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!3vBC!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F108e2575-e3ab-4198-962a-0bc197728f10_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!3vBC!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F108e2575-e3ab-4198-962a-0bc197728f10_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>That separation is what makes BERTopic modular. You can keep the same document groups while changing only how they are described. The inbox analogy is a good one: clustering sorts the messages into folders, and representation models rename the folders after you look inside. In ReviewDesk, one folder might collect billing failures, another mobile crashes, and another feature requests. The groups already exist before they are named, and BERTopic gives you a structured way to turn those groups into searchable topics.</p><p>Outliers carry over from the clustering step as well. If HDBSCAN leaves some documents unassigned, BERTopic preserves that behavior with topic <code>-1</code> instead of forcing every document into a topic that may not fit. That is useful when the corpus contains short, ambiguous, or genuinely unusual documents.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;febbd642-4dcf-4083-ab83-90c6aa6cdaba&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from bertopic import BERTopic

# Train our model with our previously defined models
topic_model = BERTopic(
    embedding_model=embedding_model,
    umap_model=umap_model,
    hdbscan_model=hdbscan_model,
    verbose=True
).fit(abstracts, embeddings)</code></pre></div><h2>How c-TF-IDF Builds the First Topic Keywords</h2><p>BERTopic turns each cluster into an initial keyword description with c-TF-IDF, or class-based TF-IDF. The key idea is to pool all documents in a cluster and treat that pool as one class-level text. The model then scores words by asking a simple question: what appears often in this folder, and how distinctive is it compared with the other folders?</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!7AkB!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75f6ff4b-985b-41b4-99fb-8a6929d40b61_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!7AkB!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75f6ff4b-985b-41b4-99fb-8a6929d40b61_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!7AkB!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75f6ff4b-985b-41b4-99fb-8a6929d40b61_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!7AkB!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75f6ff4b-985b-41b4-99fb-8a6929d40b61_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!7AkB!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75f6ff4b-985b-41b4-99fb-8a6929d40b61_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!7AkB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75f6ff4b-985b-41b4-99fb-8a6929d40b61_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/75f6ff4b-985b-41b4-99fb-8a6929d40b61_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:7068293,&quot;alt&quot;:&quot;Figure 5.8: c-TF-IDF pools documents within each cluster and ranks words by how common they are inside that cluster and how rare they are across the others.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204106410?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75f6ff4b-985b-41b4-99fb-8a6929d40b61_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 5.8: c-TF-IDF pools documents within each cluster and ranks words by how common they are inside that cluster and how rare they are across the others." title="Figure 5.8: c-TF-IDF pools documents within each cluster and ranks words by how common they are inside that cluster and how rare they are across the others." srcset="https://substackcdn.com/image/fetch/$s_!7AkB!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75f6ff4b-985b-41b4-99fb-8a6929d40b61_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!7AkB!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75f6ff4b-985b-41b4-99fb-8a6929d40b61_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!7AkB!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75f6ff4b-985b-41b4-99fb-8a6929d40b61_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!7AkB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75f6ff4b-985b-41b4-99fb-8a6929d40b61_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>That is the important difference from ordinary document-level TF-IDF. Standard TF-IDF works on individual documents. c-TF-IDF shifts the unit of analysis to the cluster, so the words are chosen to represent the group as a whole rather than any single document. In practice, this is what makes the topic keywords feel topic-like instead of document-like.</p><p>A compact version of the scoring idea is</p><p>c-TF-IDF(w,c)=tf(w,c)&#8901;log&#8289;(Ndf(w)),</p><p>where w is a word, c is a cluster, tf(w,c) measures how often the word appears in the pooled cluster text, and df(w) counts how many clusters contain that word. The formula is only a shorthand for the intuition: topic words should be common inside one cluster and relatively uncommon across the others.</p><p>This class-level weighting also explains why topic <code>-1</code> can appear in the output. If some documents remain outliers, their pooled text becomes its own class-level representation. The resulting words are a reminder that density-based clustering can preserve uncertainty instead of inventing a confident label where none is justified.</p><h2>Working with the Topic API in Practice</h2><p>After fitting the model, the fastest way to understand what it found is to inspect the topic summary table. This shows topic IDs, document counts, and the first pass at representative keywords. It is a quick audit of whether the clusters look meaningful and whether the outlier topic is large enough to deserve attention.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;1882c03d-3953-46bb-b9b9-3565cb4e6228&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">topic_model.get_topic_info()</code></pre></div><p>To inspect one topic in more detail, you can ask for its ranked keywords directly. That is often the most practical way to verify whether a cluster named by c-TF-IDF really corresponds to the subject you expect.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;f3504386-430b-41a8-91ea-372b9218c516&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">topic_model.get_topic(0)</code></pre></div><p>You can also look up the topic assigned to a specific document. In a support workflow, that is how you check whether a ticket about payment failures, app crashes, or account access landed in the right group.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;f0e606fd-2940-4fcc-99c0-ae776edbaffd&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">topic_model.topics_[titles.index("BERTopic: Neural topic modeling with a class-based TF-IDF procedure")]</code></pre></div><p>Query-based search works a little differently. It asks which discovered topic is most similar to a text prompt, such as <code>"topic modeling"</code>. This is topic retrieval, not corpus search. You are not finding documents that contain the phrase; you are asking which folder in the learned topic space best matches the phrase.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;6dc1e9bb-f76e-4db7-882f-fa69c03fa5c4&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">topic_model.find_topics("topic modeling")</code></pre></div><h2>Improving Topic Word Lists Without Reclustering</h2><p>BERTopic keeps cluster membership separate from topic wording, which means you can refine the labels without changing the groups themselves. That is a major advantage of the framework. The same cluster can be named in several different ways, and those names can be updated without rerunning the whole embedding and clustering pipeline.</p><p>The baseline topic words come from c-TF-IDF, but representation models can rerank or diversify them afterward. This is the post-processing layer. It is cheaper than retraining and often produces clearer labels, although not always more faithful ones. Cleaner wording is useful, but it can also flatten nuance if a reranker removes a distinctive term.</p><p>A small helper makes the before-and-after comparison easier to inspect.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;53760b56-42fe-491d-847f-ec5079d351ee&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from copy import deepcopy
original_topics = deepcopy(topic_model.topic_representations_)

def topic_differences(model, original_topics, nr_topics=5):
    df = pd.DataFrame(columns=["Topic", "Original", "Updated"])
    for topic in range(nr_topics):
        og_words = " | ".join(list(zip(*original_topics[topic]))[0][:5])
        new_words = " | ".join(list(zip(*model.get_topic(topic)))[0][:5])
        df.loc[len(df)] = [topic, og_words, new_words]
    return df</code></pre></div><p><code>KeyBERTInspired</code> is a similarity-focused reranker. It prefers words that sit closer to the semantic center of the topic, so the resulting keyword list often reads more coherently than raw c-TF-IDF output.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;f460b2b7-133c-441a-aabb-f58d2ec6b3f4&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from bertopic.representation import KeyBERTInspired

representation_model = KeyBERTInspired()
topic_model.update_topics(abstracts, representation_model=representation_model)
topic_differences(topic_model, original_topics)</code></pre></div><p><code>MaximalMarginalRelevance</code>, or MMR, pushes in a different direction. It still wants relevant words, but it also tries to avoid redundancy. If KeyBERTInspired looks for the best-fitting tracks on a playlist, MMR tries to keep the playlist from sounding repetitive. The benefit is a more varied label; the limitation is that too much diversification can hide useful detail.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;d8581e76-408c-4f9b-94af-9d8fd6259119&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from bertopic.representation import MaximalMarginalRelevance

representation_model = MaximalMarginalRelevance(diversity=0.2)
topic_model.update_topics(abstracts, representation_model=representation_model)
topic_differences(topic_model, original_topics)</code></pre></div><p>In practice, reranking is best seen as label editing rather than topic discovery. It can make a topic easier to read, but it does not change which documents belong to that topic. That is why the cluster boundaries stay fixed while the sign on the folder changes.</p><h2>Generating Human-Readable Labels with LLMs</h2><p>Representation models can also use text generation. In that setup, BERTopic gives the model representative documents and topic keywords, then asks it to produce a short label. This feels closer to summarizing a cluster than to training a new model. The prompt acts like a captioning template for the topic.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;b20397d6-00d6-4ecd-9d37-d08136738d55&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from transformers import pipeline
from bertopic.representation import TextGeneration

prompt = """I have a topic that contains the following documents: 
[DOCUMENTS]

The topic is described by the following keywords: '[KEYWORDS]'.

Based on the documents and keywords, what is this topic about?"""

generator = pipeline("text2text-generation", model="google/flan-t5-small")
representation_model = TextGeneration(
    generator, prompt=prompt, doc_length=50, tokenizer="whitespace"
)
topic_model.update_topics(abstracts, representation_model=representation_model)
topic_differences(topic_model, original_topics)</code></pre></div><p>A chat model can do the same job with a slightly different prompt format.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;41813690-0906-4f80-9d07-bae0d7059fe5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">import openai
from bertopic.representation import OpenAI

prompt = """
I have a topic that contains the following documents:
[DOCUMENTS]

The topic is described by the following keywords: [KEYWORDS]

Based on the information above, extract a short topic label in the following format:
topic: &lt;short topic label&gt;
"""

client = openai.OpenAI(api_key="YOUR_KEY_HERE")
representation_model = OpenAI(
    client, model="gpt-3.5-turbo", exponential_backoff=True, chat=True, prompt=prompt
)
topic_model.update_topics(abstracts, representation_model=representation_model)
topic_differences(topic_model, original_topics)</code></pre></div><p>Generated labels are flexible, but that flexibility comes with caveats. They can be broad, corpus-dependent, and slightly different across runs or models. A label like <code>Billing Issues</code> may be fluent, yet a more specific label such as <code>Refund Delays and Invoice Problems</code> may be more useful for support triage. The generated name should therefore be treated as a suggestion that still needs human review.</p><h2>Visualizing Topics as an Inspection Tool</h2><p>Visualization helps you audit the learned topic structure, but it does not prove that a topic is correct. A document map can show whether documents within a topic are close together, whether two topics overlap, or whether outliers sit far from the main groups. A keyword bar chart can show which words dominate a topic, and a heatmap or hierarchy can reveal similarity between topics. These plots are diagnostic tools, not final evidence.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;f2d98348-d8c1-45b2-a546-6cd159477247&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">fig = topic_model.visualize_documents(
    titles, 
    reduced_embeddings=reduced_embeddings, 
    width=1200, 
    hide_annotations=True
)

fig.update_layout(font=dict(size=16))

topic_model.visualize_barchart()
topic_model.visualize_heatmap(n_clusters=30)
topic_model.visualize_hierarchy()</code></pre></div><p>A datamap-style view is another way to inspect the same structure at a glance.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;f1975735-77c4-4c38-907e-7a734370591f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">fig = topic_model.visualize_document_datamap(
    titles,
    topics=list(range(20)),
    reduced_embeddings=reduced_embeddings,
    width=1200,
    label_font_size=11,
    label_wrap_width=20,
    use_medoids=True,
)</code></pre></div><p>The practical value of these visualizations is that they help you ask better questions. Do similar topics sit near each other? Are the largest topics internally consistent? Do outliers suggest noise or a missing theme? In that sense, the plots support interpretation, but the topic meaning still comes from the cluster structure and the words chosen to describe it.</p><h2>Recap: Choosing and Interpreting Topic Workflows</h2><p>The chapter&#8217;s workflow is best remembered as a translation pipeline: embeddings capture semantic proximity, dimensionality reduction makes that structure easier to work with, clustering discovers document groups, and topic representation turns those groups into language a person can inspect. That sequence matters because each stage solves a different problem. The geometry in embedding space tells you which documents belong together; it does not yet tell you what to call the cluster or whether the grouping deserves to be treated as a coherent theme.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!lZ4r!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F000e8dbd-9845-419f-a208-743d432b8224_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!lZ4r!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F000e8dbd-9845-419f-a208-743d432b8224_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!lZ4r!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F000e8dbd-9845-419f-a208-743d432b8224_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!lZ4r!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F000e8dbd-9845-419f-a208-743d432b8224_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!lZ4r!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F000e8dbd-9845-419f-a208-743d432b8224_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!lZ4r!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F000e8dbd-9845-419f-a208-743d432b8224_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/000e8dbd-9845-419f-a208-743d432b8224_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:5892497,&quot;alt&quot;:&quot;Figure 5.9: Topic workflows are a translation pipeline: embeddings and clustering discover structure, while topic representation converts that structure into human-readable labels.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204106410?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F000e8dbd-9845-419f-a208-743d432b8224_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 5.9: Topic workflows are a translation pipeline: embeddings and clustering discover structure, while topic representation converts that structure into human-readable labels." title="Figure 5.9: Topic workflows are a translation pipeline: embeddings and clustering discover structure, while topic representation converts that structure into human-readable labels." srcset="https://substackcdn.com/image/fetch/$s_!lZ4r!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F000e8dbd-9845-419f-a208-743d432b8224_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!lZ4r!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F000e8dbd-9845-419f-a208-743d432b8224_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!lZ4r!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F000e8dbd-9845-419f-a208-743d432b8224_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!lZ4r!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F000e8dbd-9845-419f-a208-743d432b8224_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>That separation between discovery and interpretation is the central idea to carry forward. Clustering is like sorting a crowded inbox into piles by similarity. Topic work is what writes the folder tabs. The piles can be useful even before they are named, but the names are what make the output communicate well enough for analysis, reporting, or downstream workflows. In practice, a dense cluster may correspond to a real theme, a loose mixture of related complaints, or simply a repeated phrasing pattern. Outlier handling is part of that reality, not a sign that the method failed.</p><p>BERTopic sits on top of this discovery process rather than replacing it. It reuses the clustered documents and then applies c-TF-IDF to surface the terms that best characterize each group. That gives the first readable version of a topic. Representation models then refine that view by changing how the cluster is summarized, which can improve keyword quality, reduce repetition, or emphasize more informative phrases. Methods such as maximal marginal relevance and KeyBERTInspired are useful here because they help topic descriptions become less redundant and more legible without reclustering the corpus.</p><p>Generative topic labels add another layer of usefulness, especially when the keyword view is too sparse or too technical. But these labels should be treated as assistive summaries, not authoritative truth. A model can produce a crisp label that sounds plausible while still flattening an important distinction in the documents underneath. For that reason, manual inspection remains the final check before a topic is used operationally. A good practical rule is simple: if you only need exploration, raw clusters may be enough; if you need communication, topic labels help; if you need accountability, human review is still required.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!_j7P!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4478358-a9d4-41ec-82f7-8094a6d9b76e_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!_j7P!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4478358-a9d4-41ec-82f7-8094a6d9b76e_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!_j7P!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4478358-a9d4-41ec-82f7-8094a6d9b76e_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!_j7P!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4478358-a9d4-41ec-82f7-8094a6d9b76e_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!_j7P!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4478358-a9d4-41ec-82f7-8094a6d9b76e_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!_j7P!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4478358-a9d4-41ec-82f7-8094a6d9b76e_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b4478358-a9d4-41ec-82f7-8094a6d9b76e_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:7265244,&quot;alt&quot;:&quot;Figure 5.10: The right interpretation layer depends on the use case: exploration can stop at clusters, communication benefits from labels, and operational decisions require human validation.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204106410?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4478358-a9d4-41ec-82f7-8094a6d9b76e_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 5.10: The right interpretation layer depends on the use case: exploration can stop at clusters, communication benefits from labels, and operational decisions require human validation." title="Figure 5.10: The right interpretation layer depends on the use case: exploration can stop at clusters, communication benefits from labels, and operational decisions require human validation." srcset="https://substackcdn.com/image/fetch/$s_!_j7P!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4478358-a9d4-41ec-82f7-8094a6d9b76e_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!_j7P!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4478358-a9d4-41ec-82f7-8094a6d9b76e_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!_j7P!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4478358-a9d4-41ec-82f7-8094a6d9b76e_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!_j7P!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4478358-a9d4-41ec-82f7-8094a6d9b76e_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>This chapter closes by moving from unsupervised discovery to controlled generation. In the next chapter, prompts become the main way to steer what a model says, which is the natural complement to the unsupervised methods here.</p><p><em>Use the url below to download the enitre book as pdf:</em></p>
      <p>
          <a href="https://onepagecode.substack.com/p/text-clustering-and-topic-modeling">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Text Classification Pipelines: Direct, Embedding-Based, and Prompted Generative Workflows]]></title><description><![CDATA[Chapter 4 of Hands-On Large Language Models: A comparative analysis of sentiment classification using BERT, Logistic Regression, and zero-shot generative prompting]]></description><link>https://onepagecode.substack.com/p/text-classification-pipelines-direct</link><guid isPermaLink="false">https://onepagecode.substack.com/p/text-classification-pipelines-direct</guid><dc:creator><![CDATA[Onepagecode]]></dc:creator><pubDate>Mon, 06 Jul 2026 19:43:08 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!JyFe!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F00f11106-91a7-4f01-a5a8-4ba2dba9566d_2816x1536.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>What text classification means in practice</h2><p>Text classification is the task of assigning a discrete label to a text input. In the running example for this chapter, a review is mapped to sentiment labels such as positive or negative. The same basic idea also appears in intent detection, topic assignment, language identification, and entity-related labeling. The input is text, and the output is one choice from a fixed set of classes.</p><p>That simple task definition matters because it lets us compare methods on the same problem. If every approach sees the same labeled data and the same target labels, then the comparison is about workflow choice rather than about changing the task itself.</p><h2>Why pretrained models change the workflow</h2><p>For modern text classification, pretrained language models are the practical starting point. They already encode useful syntactic and semantic information, so downstream work begins from a capable base rather than from random initialization. That makes them much more useful than training a model from scratch when the goal is to solve a real classification task quickly and reliably.</p><p>A pretrained model can still play different roles. It may behave as a direct classifier, or it may serve as a source of representations that another model consumes. It can also be used as a generative system that is prompted to emit a label. The common thread is reuse, but what gets reused is different in each case: the decision head, the vector space, or the language-generation ability.</p><h2><strong>&#128216; Buy the Entire Book &#8211; Available on Amazon Worldwide (If you are paid subscriber, use the url at the end of this article to download the book for free!)</strong></h2><p>You can now purchase the complete book directly from Amazon in your country:</p><ul><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/us/dualbookshelf.marketplacelink/B0H6WZJ57S">United States (US)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/uk/dualbookshelf.marketplacelink/B0H6WZJ57S">United Kingdom (UK)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/de/dualbookshelf.marketplacelink/B0H6WZJ57S">Germany (DE)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/fr/dualbookshelf.marketplacelink/B0H6WZJ57S">France (FR)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/es/dualbookshelf.marketplacelink/B0H6WZJ57S">Spain (ES)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/it/dualbookshelf.marketplacelink/B0H6WZJ57S">Italy (IT)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/nl/dualbookshelf.marketplacelink/B0H6WZJ57S">Netherlands (NL)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/jp/dualbookshelf.marketplacelink/B0H6WZJ57S">Japan (JP)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/br/dualbookshelf.marketplacelink/B0H6WZJ57S">Brazil (BR):</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/ca/dualbookshelf.marketplacelink/B0H6WZJ57S">Canada (CA)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/mx/dualbookshelf.marketplacelink/B0H6WZJ57S">Mexico (MX)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/au/dualbookshelf.marketplacelink/B0H6WZJ57S">Australia (AU)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/in/dualbookshelf.marketplacelink/B0H6WZJ57S">India (IN)</a></strong></p></li></ul><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!JyFe!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F00f11106-91a7-4f01-a5a8-4ba2dba9566d_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!JyFe!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F00f11106-91a7-4f01-a5a8-4ba2dba9566d_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!JyFe!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F00f11106-91a7-4f01-a5a8-4ba2dba9566d_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!JyFe!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F00f11106-91a7-4f01-a5a8-4ba2dba9566d_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!JyFe!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F00f11106-91a7-4f01-a5a8-4ba2dba9566d_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!JyFe!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F00f11106-91a7-4f01-a5a8-4ba2dba9566d_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/00f11106-91a7-4f01-a5a8-4ba2dba9566d_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:5083275,&quot;alt&quot;:&quot;Figure 4.1: A pretrained language model can support text classification in three ways: direct classification, embedding-based classification, or prompted generation of labels.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204106027?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F00f11106-91a7-4f01-a5a8-4ba2dba9566d_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 4.1: A pretrained language model can support text classification in three ways: direct classification, embedding-based classification, or prompted generation of labels." title="Figure 4.1: A pretrained language model can support text classification in three ways: direct classification, embedding-based classification, or prompted generation of labels." srcset="https://substackcdn.com/image/fetch/$s_!JyFe!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F00f11106-91a7-4f01-a5a8-4ba2dba9566d_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!JyFe!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F00f11106-91a7-4f01-a5a8-4ba2dba9566d_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!JyFe!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F00f11106-91a7-4f01-a5a8-4ba2dba9566d_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!JyFe!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F00f11106-91a7-4f01-a5a8-4ba2dba9566d_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2>The first family: task-specific classifiers built on representations</h2><p>The first workflow family is the direct classifier. Here the pretrained model has been shaped for a downstream prediction task, so its output is already aligned with the label space rather than being a generic description of the text. In sentiment analysis, that means the model is set up to decide whether a review is positive or negative.</p><p>This is the most direct use of pretrained capability. The model receives text and produces a class or class score without requiring another algorithm to interpret intermediate vectors. A good way to think about it is as a specialist: it is built to answer the classification question directly, which often makes it a strong choice when speed and consistent behavior matter.</p><h2>The second family: reusable embeddings feeding a separate classifier</h2><p>The second workflow family uses a pretrained model as an embedding model. In this setup, the model does not make the final label decision itself. Instead, it turns each text into a reusable vector, and a separate classifier learns from those vectors. The classifier can be comparatively simple, such as logistic regression, because the representation model has already done the heavy lifting of turning text into features.</p><p>This split between feature extraction and classification is what makes the pipeline modular. The same embeddings can support sentiment classification in one setting and a different downstream use in another setting. Here the pretrained model acts like an adapter or feature extractor: it supplies a vector space, and the classifier on top learns the decision boundary.</p><h2>The third family: prompting a generative model to classify</h2><p>The third workflow family uses a generative model as a classifier. A model whose native job is text generation can still be asked to output a class label, such as positive or negative, when the prompt is framed carefully. In other words, generation and classification are not mutually exclusive.</p><p>This approach differs from the representation-based workflows because no separate classifier consumes embeddings and no dedicated prediction head is required for the task. The model is steered to produce a label directly from its generation ability. That gives it flexibility, especially when the desired behavior may change across tasks, but it also gives the user less deterministic control than a fixed classifier.</p><h2>How the chapter will compare these workflows</h2><p>The rest of the chapter compares these three families on the same labeled dataset and the same classification task. That makes the evaluation fairer and easier to interpret, because the methods are being judged on identical labels and inputs. The question is not which problem is easier, but which workflow is the better fit for the same problem.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!v8Up!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e838dfc-56ce-4616-929f-fb4d4f3e1a14_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!v8Up!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e838dfc-56ce-4616-929f-fb4d4f3e1a14_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!v8Up!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e838dfc-56ce-4616-929f-fb4d4f3e1a14_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!v8Up!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e838dfc-56ce-4616-929f-fb4d4f3e1a14_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!v8Up!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e838dfc-56ce-4616-929f-fb4d4f3e1a14_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!v8Up!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e838dfc-56ce-4616-929f-fb4d4f3e1a14_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4e838dfc-56ce-4616-929f-fb4d4f3e1a14_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:5774061,&quot;alt&quot;:&quot;Figure 4.2: The chapter compares workflows fairly by holding the dataset, labels, and task constant, so differences reflect method choice rather than problem definition.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204106027?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e838dfc-56ce-4616-929f-fb4d4f3e1a14_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 4.2: The chapter compares workflows fairly by holding the dataset, labels, and task constant, so differences reflect method choice rather than problem definition." title="Figure 4.2: The chapter compares workflows fairly by holding the dataset, labels, and task constant, so differences reflect method choice rather than problem definition." srcset="https://substackcdn.com/image/fetch/$s_!v8Up!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e838dfc-56ce-4616-929f-fb4d4f3e1a14_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!v8Up!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e838dfc-56ce-4616-929f-fb4d4f3e1a14_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!v8Up!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e838dfc-56ce-4616-929f-fb4d4f3e1a14_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!v8Up!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e838dfc-56ce-4616-929f-fb4d4f3e1a14_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>A classic TF-IDF plus logistic regression baseline remains a useful reference point, but the main focus here is on pretrained approaches. The comparison will therefore ask a practical question: when is the direct classifier the best option, when does an embedding pipeline make more sense, and when is prompt-based generation flexible enough to justify its extra variability?</p><h2>Introduce the Rotten Tomatoes corpus as the chapter&#8217;s shared dataset</h2><p>The chapter uses the Rotten Tomatoes movie-review corpus as its running example for binary sentiment classification. Each record pairs a short review with a label that says whether the sentiment is positive or negative. That makes the task a clean form of review triage: the model only has to choose between two classes, not generate a summary or score a five-star scale. The same workflow also transfers to other two-way text decisions, such as routing support tickets into positive or negative tone categories, so the dataset is a concrete instance of a broader supervised classification pattern.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!zqJ0!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3909bf2b-d030-4071-9e0e-3c4abc627788_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!zqJ0!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3909bf2b-d030-4071-9e0e-3c4abc627788_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!zqJ0!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3909bf2b-d030-4071-9e0e-3c4abc627788_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!zqJ0!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3909bf2b-d030-4071-9e0e-3c4abc627788_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!zqJ0!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3909bf2b-d030-4071-9e0e-3c4abc627788_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!zqJ0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3909bf2b-d030-4071-9e0e-3c4abc627788_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3909bf2b-d030-4071-9e0e-3c4abc627788_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:5250247,&quot;alt&quot;:&quot;Figure 4.3: Rotten Tomatoes reviews are organized as text-and-label pairs for binary sentiment classification, where each example is assigned to either the positive or negative class.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204106027?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3909bf2b-d030-4071-9e0e-3c4abc627788_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 4.3: Rotten Tomatoes reviews are organized as text-and-label pairs for binary sentiment classification, where each example is assigned to either the positive or negative class." title="Figure 4.3: Rotten Tomatoes reviews are organized as text-and-label pairs for binary sentiment classification, where each example is assigned to either the positive or negative class." srcset="https://substackcdn.com/image/fetch/$s_!zqJ0!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3909bf2b-d030-4071-9e0e-3c4abc627788_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!zqJ0!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3909bf2b-d030-4071-9e0e-3c4abc627788_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!zqJ0!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3909bf2b-d030-4071-9e0e-3c4abc627788_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!zqJ0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3909bf2b-d030-4071-9e0e-3c4abc627788_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2>Load and inspect the dataset container</h2><p>The first setup step is to load the corpus with <code>datasets</code> and inspect the object it returns. The important detail is not just that the data exists, but that it arrives as a structured container with the partitions already prepared.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;1b14bc7d-8803-4528-ab00-e2ecf448d334&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from datasets import load_dataset

# Load our data
data = load_dataset("rotten_tomatoes")
data</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;054cee1f-f2c3-4891-a5ca-3915937c18b1&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">DatasetDict({
    train: Dataset({
        features: ['text', 'label'],
        num_rows: 8530
    })
    validation: Dataset({
        features: ['text', 'label'],
        num_rows: 1066
    })
    test: Dataset({
        features: ['text', 'label'],
        num_rows: 1066
    })
})</code></pre></div><p>This <code>DatasetDict</code> shows that the corpus already comes split for experimentation. The training partition is the one used to fit a model, the validation partition supports iteration and selection, and the test partition stays reserved for the final comparison. Keeping those roles separate is what makes later results trustworthy across direct classifiers, embedding-based methods, and prompt-based approaches.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!O_5f!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0fcbffb0-45ab-45a2-a268-ab0201971784_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!O_5f!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0fcbffb0-45ab-45a2-a268-ab0201971784_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!O_5f!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0fcbffb0-45ab-45a2-a268-ab0201971784_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!O_5f!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0fcbffb0-45ab-45a2-a268-ab0201971784_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!O_5f!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0fcbffb0-45ab-45a2-a268-ab0201971784_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!O_5f!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0fcbffb0-45ab-45a2-a268-ab0201971784_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0fcbffb0-45ab-45a2-a268-ab0201971784_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:3593143,&quot;alt&quot;:&quot;Figure 4.4: The Rotten Tomatoes corpus is already partitioned into train, validation, and test splits, each with the same text-and-label schema but different evaluation roles.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204106027?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0fcbffb0-45ab-45a2-a268-ab0201971784_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 4.4: The Rotten Tomatoes corpus is already partitioned into train, validation, and test splits, each with the same text-and-label schema but different evaluation roles." title="Figure 4.4: The Rotten Tomatoes corpus is already partitioned into train, validation, and test splits, each with the same text-and-label schema but different evaluation roles." srcset="https://substackcdn.com/image/fetch/$s_!O_5f!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0fcbffb0-45ab-45a2-a268-ab0201971784_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!O_5f!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0fcbffb0-45ab-45a2-a268-ab0201971784_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!O_5f!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0fcbffb0-45ab-45a2-a268-ab0201971784_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!O_5f!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0fcbffb0-45ab-45a2-a268-ab0201971784_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2>Read the schema from actual examples</h2><p>Each example contains two fields: review text and a binary label. That is the complete supervised learning schema for this chapter. The input is raw language, and the target is a class ID that names one of two sentiment categories. At this point there is no preprocessing story to solve and no feature engineering decision to make; the task definition is simply text in, sentiment class out.</p><h2>Decode the label values with concrete samples</h2><p>A quick look at raw examples makes the encoding unambiguous.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;d89147ed-4e9f-4401-930a-b12968ec1145&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">data["train"][0, -1]

{'text': ['the rock is destined to be the 21st century\'s new " conan " and that he\'s going to make a splash even greater than arnold schwarzenegger , jean-claud van damme or steven segal .',
  'things really get weird , though not particularly scary : the movie is all portent and no content .'],
 'label': [1, 0]}</code></pre></div><p>Here, <code>1</code> stands for positive and <code>0</code> stands for negative. Those values are class identifiers, not a sentiment scale, so they should not be read as degrees of positivity or negativity. The first review reads like praise, while the second clearly sounds unfavorable, which confirms the mapping in a way that is easier to trust than the numeric labels alone.</p><h2>Set evaluation boundaries before modeling begins</h2><p>This inspection step is small, but it protects every method that follows. The data is already split, and those same partitions must be preserved for all model families in the chapter if the comparison is to remain fair. That means no tuning on test data, no peeking at labels beyond basic inspection, and no changing the split logic from one approach to another. A dataset can look simple and still hide skew, mislabeled cases, or leakage risks, so checking the raw structure now is the safest way to avoid invalid conclusions later. With the corpus, schema, and label encoding clear, the next sections can focus on modeling rather than on repairing the data pipeline.</p><h2>Model selection and practical checkpoint choices</h2><p>Checkpoint choice is not a cosmetic detail; it is part of the experiment design. Before you evaluate anything, you decide what kind of tool you want in your hands. In this chapter, the same ReviewDesk-style sentiment triage task could be solved by a model that returns a label directly, by a model that turns text into reusable vectors, or by a generative model that is asked to behave like a classifier. Those are not interchangeable routes. A direct classifier is a courier that delivers a verdict. An embedding model is more like a map: it does not answer the question for you, but it gives you a stable coordinate system that many downstream methods can use.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!o5Od!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc1fd205e-feb3-47e3-8abb-086ed6eaeaf9_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!o5Od!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc1fd205e-feb3-47e3-8abb-086ed6eaeaf9_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!o5Od!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc1fd205e-feb3-47e3-8abb-086ed6eaeaf9_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!o5Od!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc1fd205e-feb3-47e3-8abb-086ed6eaeaf9_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!o5Od!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc1fd205e-feb3-47e3-8abb-086ed6eaeaf9_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!o5Od!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc1fd205e-feb3-47e3-8abb-086ed6eaeaf9_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c1fd205e-feb3-47e3-8abb-086ed6eaeaf9_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:6241183,&quot;alt&quot;:&quot;Figure 4.5: The same sentiment task can be solved by three different checkpoint roles: direct classification, reusable embeddings, or a generative model instructed to behave like a classifier.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204106027?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc1fd205e-feb3-47e3-8abb-086ed6eaeaf9_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 4.5: The same sentiment task can be solved by three different checkpoint roles: direct classification, reusable embeddings, or a generative model instructed to behave like a classifier." title="Figure 4.5: The same sentiment task can be solved by three different checkpoint roles: direct classification, reusable embeddings, or a generative model instructed to behave like a classifier." srcset="https://substackcdn.com/image/fetch/$s_!o5Od!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc1fd205e-feb3-47e3-8abb-086ed6eaeaf9_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!o5Od!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc1fd205e-feb3-47e3-8abb-086ed6eaeaf9_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!o5Od!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc1fd205e-feb3-47e3-8abb-086ed6eaeaf9_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!o5Od!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc1fd205e-feb3-47e3-8abb-086ed6eaeaf9_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>A useful checkpoint selection process starts with the task itself. If the goal is plain sentiment classification, a model already adapted for that kind of decision is usually the most direct baseline. If the goal is broader reuse across similarity search, clustering, or lightweight classification, an embedding model is often the better foundation. After that comes language and domain fit. A model can be strong in general and still be awkward for a particular source of text if the tone, vocabulary, or style is mismatched. Architecture matters too. Encoder-only models are often the most practical choice here because they are compact, fast, and naturally suited to producing either labels or vectors. Size then becomes a deployment question: smaller checkpoints are easier to run, easier to compare, and easier to keep responsive, while larger models may offer more raw capacity but add latency and complexity that this chapter does not need.</p><p>That is why the chapter emphasizes frozen pretrained checkpoints rather than training or fine-tuning them here. Frozen usage keeps the focus on what the pretrained model already knows and how its outputs behave in a downstream workflow. It also keeps the comparison honest. If you change weights, you have changed the model family and the experiment itself; if you keep the checkpoint fixed, you can compare workflows rather than training recipes. In other words, this section is about choosing a starting point, not claiming a final winner.</p><p>For the direct sentiment classifier, the chapter uses a sentiment-ready RoBERTa checkpoint, specifically <code>cardiffnlp/twitter-roberta-base-sentiment-latest</code>. It is a practical baseline because it is already specialized for sentiment-like decisions and is therefore a natural fit for a classification path that should work out of the box. It also reflects a broader pattern in applied NLP: a smaller task-specific model often gives you a better first answer than a larger generative model, especially when you care about predictability, throughput, and simple label outputs. For the embedding path, the chapter uses <code>sentence-transformers/all-mpnet-base-v2</code>. That checkpoint is a strong default because it balances quality with efficiency and produces vectors that are useful beyond a single task. In a workflow like ReviewDesk, those vectors can support supervised classification today and similarity-based retrieval tomorrow.</p><p>The practical lesson is simple. Choose the smallest checkpoint that still matches the task well, unless your real objective is reusable representations across several workflows. Prefer encoder-only models when you want direct, stable, low-friction baselines. Reach for a generative model later when the problem calls for flexible language output rather than compact prediction. For now, these selections define the chapter&#8217;s starting line, not its finish line.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!BHvd!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8105cbad-3a85-4b16-a6c2-e354fc246456_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!BHvd!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8105cbad-3a85-4b16-a6c2-e354fc246456_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!BHvd!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8105cbad-3a85-4b16-a6c2-e354fc246456_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!BHvd!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8105cbad-3a85-4b16-a6c2-e354fc246456_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!BHvd!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8105cbad-3a85-4b16-a6c2-e354fc246456_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!BHvd!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8105cbad-3a85-4b16-a6c2-e354fc246456_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8105cbad-3a85-4b16-a6c2-e354fc246456_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:5106898,&quot;alt&quot;:&quot;Figure 4.6: A practical checkpoint-selection flowchart: start with the task, then choose the smallest model that fits, unless reusable representations or flexible generation are the real goal.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204106027?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8105cbad-3a85-4b16-a6c2-e354fc246456_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 4.6: A practical checkpoint-selection flowchart: start with the task, then choose the smallest model that fits, unless reusable representations or flexible generation are the real goal." title="Figure 4.6: A practical checkpoint-selection flowchart: start with the task, then choose the smallest model that fits, unless reusable representations or flexible generation are the real goal." srcset="https://substackcdn.com/image/fetch/$s_!BHvd!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8105cbad-3a85-4b16-a6c2-e354fc246456_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!BHvd!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8105cbad-3a85-4b16-a6c2-e354fc246456_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!BHvd!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8105cbad-3a85-4b16-a6c2-e354fc246456_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!BHvd!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8105cbad-3a85-4b16-a6c2-e354fc246456_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2>Direct inference with a task-specific sentiment model</h2><h3>Why this baseline works without training</h3><p>A task-specific sentiment checkpoint is already tuned to make sentiment decisions, so you do not need to train it again before using it. The workflow is therefore direct inference: load the model, point it at unseen text, and evaluate the predictions. That makes it a strong baseline when your new data is close to the domain the checkpoint was trained on, because specialized models often encode useful decision boundaries from the start.</p><p>A useful way to think about this is as a prebuilt triage model for messages or reviews. The system is not discovering the meaning of positive and negative labels from scratch. It is applying knowledge that is already inside the checkpoint, which is why this section can move quickly from setup to evaluation and still produce a meaningful benchmark for later comparisons.</p><h3>Load the sentiment pipeline and inspect its output shape</h3><p>Hugging Face wraps tokenization, model execution, and label scoring into a single pipeline interface. The model and tokenizer must come from the same checkpoint path so that the text preprocessing matches the classifier weights. Here the pipeline is asked to return all label scores, not just the best label, because the next step will make the class decision explicitly.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!eian!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8cf1dbef-3577-4539-9627-48abdb2f177f_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!eian!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8cf1dbef-3577-4539-9627-48abdb2f177f_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!eian!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8cf1dbef-3577-4539-9627-48abdb2f177f_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!eian!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8cf1dbef-3577-4539-9627-48abdb2f177f_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!eian!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8cf1dbef-3577-4539-9627-48abdb2f177f_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!eian!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8cf1dbef-3577-4539-9627-48abdb2f177f_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8cf1dbef-3577-4539-9627-48abdb2f177f_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:5427692,&quot;alt&quot;:&quot;Figure 4.7: Direct inference flow for a task-specific sentiment checkpoint: text is tokenized, scored by the pretrained model, and converted to a class label via argmax over the output scores.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204106027?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8cf1dbef-3577-4539-9627-48abdb2f177f_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 4.7: Direct inference flow for a task-specific sentiment checkpoint: text is tokenized, scored by the pretrained model, and converted to a class label via argmax over the output scores." title="Figure 4.7: Direct inference flow for a task-specific sentiment checkpoint: text is tokenized, scored by the pretrained model, and converted to a class label via argmax over the output scores." srcset="https://substackcdn.com/image/fetch/$s_!eian!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8cf1dbef-3577-4539-9627-48abdb2f177f_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!eian!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8cf1dbef-3577-4539-9627-48abdb2f177f_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!eian!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8cf1dbef-3577-4539-9627-48abdb2f177f_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!eian!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8cf1dbef-3577-4539-9627-48abdb2f177f_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;023111ec-4f34-432e-bee5-71ad5311296b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from transformers import pipeline

# Path to our HF model
model_path = "cardiffnlp/twitter-roberta-base-sentiment-latest"

# Load model into pipeline
pipe = pipeline(
    model=model_path,
    tokenizer=model_path,
    return_all_scores=True,
    device="cuda:0"
)</code></pre></div><p>Returning all scores matters because it exposes the full sentiment distribution the model assigns to an input. Those values are what the code uses to decide between the binary classes. In this checkpoint, the score vector includes a negative class and a positive class, and preserving that structure makes the later argmax step easy to interpret and harder to misuse.</p><h3>Score the held-out test split and turn scores into labels</h3><p>The evaluation must happen on the test split, not on training data. That keeps the result honest, because the model is being judged on examples it did not use to form its sentiment boundary. The loop uses <code>KeyDataset</code> to stream only the text column into the pipeline, which is a compact way to connect a dataset object to inference without manually unpacking rows. <code>tqdm</code> simply adds progress reporting while the held-out set is processed.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;aa9de264-d9ad-4cea-8e2d-e912f0a32b18&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">import numpy as np
from tqdm import tqdm
from transformers.pipelines.pt_utils import KeyDataset

# Run inference
y_pred = []
for output in tqdm(pipe(KeyDataset(data["test"], "text")), total=len(data["test"])):
    negative_score = output[0]["score"]
    positive_score = output[2]["score"]
    assignment = np.argmax([negative_score, positive_score])
    y_pred.append(assignment)</code></pre></div><p>The score vector is reduced to one class with <code>argmax</code>. That decision rule is deliberate: the model gives two relevant class scores, and the larger one becomes the prediction. The label order matters here, because reading the wrong score index would quietly flip the meaning of the output. In this example, the code treats the first extracted score as negative and the second as positive, then uses the larger value to choose between them.</p><p>Seen as a workflow, this is a three-step pipeline. First the classifier is loaded. Then the test split is scored. Finally the scores are translated into labels that can be compared with the true targets. That simple sequence is enough to turn a pretrained checkpoint into a complete baseline classifier.</p><h3>Read the classification report as a performance summary</h3><p>The classification report turns the predictions into a compact summary of quality. Precision tells you how reliable the predicted positives are. Recall tells you how many of the true positives the model manages to recover. Accuracy gives the overall share of correct predictions. F1 combines precision and recall into one number, which is useful when you want a balanced view of both kinds of error.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;fa984655-9847-47e7-8cb8-45fe69d54f90&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from sklearn.metrics import classification_report

def evaluate_performance(y_true, y_pred):
    """Create and print the classification report"""
    performance = classification_report(
        y_true, y_pred,
        target_names=["Negative Review", "Positive Review"]
    )
    print(performance)</code></pre></div><p>The weighted average is especially useful when you want a single summary score that reflects the class distribution. Even so, it can hide imbalance. A model may look strong on weighted F1 while still doing noticeably better on one sentiment than the other, so the per-class rows remain important. In practice, the report gives you a quick sense of whether the classifier is broadly reliable or only strong on one side of the label space.</p><h3>Use the confusion matrix to diagnose asymmetric mistakes</h3><p>The confusion matrix adds a different perspective because it shows how the errors are distributed across the two classes. It makes the false positives and false negatives visible instead of folding them into one aggregate score. That matters whenever one kind of mistake is more costly than the other, or whenever the model seems to favor one label over the other.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Rarf!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1f94ef28-ec21-4f6c-9415-320f1476d5d4_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Rarf!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1f94ef28-ec21-4f6c-9415-320f1476d5d4_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!Rarf!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1f94ef28-ec21-4f6c-9415-320f1476d5d4_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!Rarf!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1f94ef28-ec21-4f6c-9415-320f1476d5d4_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!Rarf!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1f94ef28-ec21-4f6c-9415-320f1476d5d4_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Rarf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1f94ef28-ec21-4f6c-9415-320f1476d5d4_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1f94ef28-ec21-4f6c-9415-320f1476d5d4_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:5341393,&quot;alt&quot;:&quot;Figure 4.8: Confusion matrix for a binary sentiment classifier, showing correct predictions on the diagonal and false positives/false negatives on the off-diagonal.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204106027?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1f94ef28-ec21-4f6c-9415-320f1476d5d4_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 4.8: Confusion matrix for a binary sentiment classifier, showing correct predictions on the diagonal and false positives/false negatives on the off-diagonal." title="Figure 4.8: Confusion matrix for a binary sentiment classifier, showing correct predictions on the diagonal and false positives/false negatives on the off-diagonal." srcset="https://substackcdn.com/image/fetch/$s_!Rarf!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1f94ef28-ec21-4f6c-9415-320f1476d5d4_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!Rarf!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1f94ef28-ec21-4f6c-9415-320f1476d5d4_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!Rarf!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1f94ef28-ec21-4f6c-9415-320f1476d5d4_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!Rarf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1f94ef28-ec21-4f6c-9415-320f1476d5d4_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>For a binary sentiment task, the matrix can show whether the model is especially good at catching positive reviews, especially good at rejecting them, or simply uneven in both directions. If the off-diagonal cells are imbalanced, the model has an asymmetry that the classification report alone may not make obvious. That is why the matrix is best treated as a debugging tool for prediction behavior rather than as a decorative summary.</p><p>A sentiment-specific checkpoint can be strong precisely because it already matches the task. When the training domain and the target domain are close, direct inference often gives a surprisingly solid baseline. The result is not the final answer, but a reference point for the next comparisons. As the chapter moves on, that baseline will help you judge whether embeddings or prompting are actually improving performance, or only changing the style of the decision process.</p><h2>From pretrained embeddings to reusable features</h2><p>A pretrained embedding model can act like a universal text compressor: it turns a review, a ticket, or a document into a fixed-length vector that another model can consume. That is a different role from a generative large language model, which predicts the next token and is optimized to produce free-form text. In this section, the embedding model is frozen. Its weights do not change, and its job is not to learn the sentiment task directly. Instead, it produces a stable representation that makes the downstream classification problem easier.</p><p>That separation matters because it gives you a very practical tradeoff. If you have labeled data and want a compact, fast classifier, you can keep the encoder fixed and train a lightweight head on top. If you have little or no labeled data for the target task, you can sometimes skip training a classifier entirely and compare the document embedding to embeddings of label descriptions. Both workflows reuse the same encoder, but they solve the problem in different ways. The first learns a decision boundary from examples; the second treats label text as a prototype and asks which label phrase is closest in semantic space.</p><h2>Building a supervised classifier on frozen vectors</h2><p>The supervised version starts with an encode-once-then-train pattern. First, the text is converted into embeddings. Then those vectors are handed to a conventional classifier. The key point is that the classifier never sees raw text; it sees a matrix of numbers where each row corresponds to one example and each column corresponds to one dimension of the embedding space.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Pjc8!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1a05eb19-8be9-48ee-a4cb-fde02bd8f460_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Pjc8!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1a05eb19-8be9-48ee-a4cb-fde02bd8f460_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!Pjc8!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1a05eb19-8be9-48ee-a4cb-fde02bd8f460_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!Pjc8!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1a05eb19-8be9-48ee-a4cb-fde02bd8f460_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!Pjc8!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1a05eb19-8be9-48ee-a4cb-fde02bd8f460_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Pjc8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1a05eb19-8be9-48ee-a4cb-fde02bd8f460_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1a05eb19-8be9-48ee-a4cb-fde02bd8f460_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:5622118,&quot;alt&quot;:&quot;Figure 4.9: Frozen embeddings transform raw text into a fixed-size feature matrix that a lightweight classifier can use without ever seeing the original tokens.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204106027?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1a05eb19-8be9-48ee-a4cb-fde02bd8f460_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 4.9: Frozen embeddings transform raw text into a fixed-size feature matrix that a lightweight classifier can use without ever seeing the original tokens." title="Figure 4.9: Frozen embeddings transform raw text into a fixed-size feature matrix that a lightweight classifier can use without ever seeing the original tokens." srcset="https://substackcdn.com/image/fetch/$s_!Pjc8!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1a05eb19-8be9-48ee-a4cb-fde02bd8f460_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!Pjc8!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1a05eb19-8be9-48ee-a4cb-fde02bd8f460_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!Pjc8!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1a05eb19-8be9-48ee-a4cb-fde02bd8f460_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!Pjc8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1a05eb19-8be9-48ee-a4cb-fde02bd8f460_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;9241606a-7c78-46fa-b850-14c4cb8f73b1&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from sentence_transformers import SentenceTransformer

# Load model
model = SentenceTransformer("sentence-transformers/all-mpnet-base-v2")

# Convert text to embeddings
train_embeddings = model.encode(data["train"]["text"], show_progress_bar=True)
test_embeddings = model.encode(data["test"]["text"], show_progress_bar=True)</code></pre></div><p>The shape of that feature matrix is part of the story.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;fd872ee3-d643-4740-8e6d-a8a84357907b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">train_embeddings.shape

(8530, 768)</code></pre></div><p>Each review becomes a 768-dimensional vector. That fixed dimensionality is what makes the next step possible: the classifier receives a standard tabular input rather than an irregular sequence of tokens. In that sense, the embedding model has already done the hard language work, and the classifier is mostly learning how to separate the classes in a space that is hopefully organized by meaning.</p><p>A simple logistic regression is often enough to make this idea concrete. It is not trying to relearn language from scratch. It is drawing a boundary through the embedding space using the labeled training examples.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;25e0b6df-0b22-4824-bb7d-e8dd677358c2&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from sklearn.linear_model import LogisticRegression

# Train a logistic regression on our train embeddings
clf = LogisticRegression(random_state=42)
clf.fit(train_embeddings, data["train"]["label"])

# Predict previously unseen instances
y_pred = clf.predict(test_embeddings)
evaluate_performance(data["test"]["label"], y_pred)</code></pre></div><p>This workflow is useful because it keeps the model head small while still allowing the encoder to carry rich semantic information. It also exposes a common reality of modern NLP systems: once the downstream model is simple, embedding quality can dominate the result. A stronger embedding model often helps more than a more elaborate classifier head. At the same time, frozen embeddings are not magic. If the encoder is generic and the task is specialized, a task-tuned model can still outperform this lightweight baseline.</p><h2>What the embedding matrix is actually feeding the classifier</h2><p>The phrase &#8220;embedding-based classifier&#8221; can sound more mysterious than it is. The downstream model is not operating on language in the abstract. It is operating on a table of vectors. Each row is one document, and each row has the same width because the encoder always emits the same dimensionality for a given model checkpoint. That is why a simple linear model can work at all: the input format is regular, even though the text was not.</p><p>A useful mental model is to think of the embedding encoder as a reusable feature compressor. You can swap in different heads on top of the same compressor without retraining the compressor itself. For ReviewDesk ticket triage, for example, the same encoder could feed a classifier for urgent versus non-urgent messages, or a different classifier for billing versus technical issues, as long as you have labels for the downstream task. The frozen encoder stays fixed; only the decision layer changes.</p><h2>Turning labels into text prototypes</h2><p>Zero-shot classification with embeddings uses the same encoder, but now the labels themselves are rewritten as short text descriptions. Instead of assigning an opaque class ID, you represent each class with a phrase such as &#8220;A negative review&#8221; or &#8220;A positive review.&#8221; Those phrases are embedded the same way as the documents, so the label text becomes a semantic anchor in the same vector space.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!VOaO!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe59d470f-76b2-4935-b83e-f8ce3ebb9c1b_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!VOaO!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe59d470f-76b2-4935-b83e-f8ce3ebb9c1b_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!VOaO!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe59d470f-76b2-4935-b83e-f8ce3ebb9c1b_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!VOaO!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe59d470f-76b2-4935-b83e-f8ce3ebb9c1b_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!VOaO!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe59d470f-76b2-4935-b83e-f8ce3ebb9c1b_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!VOaO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe59d470f-76b2-4935-b83e-f8ce3ebb9c1b_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e59d470f-76b2-4935-b83e-f8ce3ebb9c1b_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:5931805,&quot;alt&quot;:&quot;Figure 4.10: In zero-shot label matching, document embeddings are compared directly with embeddings of label descriptions, and the nearest label text becomes the prediction.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204106027?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe59d470f-76b2-4935-b83e-f8ce3ebb9c1b_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 4.10: In zero-shot label matching, document embeddings are compared directly with embeddings of label descriptions, and the nearest label text becomes the prediction." title="Figure 4.10: In zero-shot label matching, document embeddings are compared directly with embeddings of label descriptions, and the nearest label text becomes the prediction." srcset="https://substackcdn.com/image/fetch/$s_!VOaO!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe59d470f-76b2-4935-b83e-f8ce3ebb9c1b_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!VOaO!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe59d470f-76b2-4935-b83e-f8ce3ebb9c1b_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!VOaO!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe59d470f-76b2-4935-b83e-f8ce3ebb9c1b_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!VOaO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe59d470f-76b2-4935-b83e-f8ce3ebb9c1b_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;65d3b941-b743-4f77-b5c2-34029198e1bc&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># Create embeddings for our labels
label_embeddings = model.encode(["A negative review",  "A positive review"])

from sklearn.metrics.pairwise import cosine_similarity

# Find the best matching label for each document
sim_matrix = cosine_similarity(test_embeddings, label_embeddings)
y_pred = np.argmax(sim_matrix, axis=1)</code></pre></div><p>This is not the same as supervised classification. No classifier is trained on labeled examples. The labels function more like prototype postcards: short textual summaries of each class that compete for similarity with the document. In the ReviewDesk setting, you could imagine label descriptions such as &#8220;A critical bug report&#8221; and &#8220;A routine account question&#8221; playing the same role. The method works only if the label wording is semantically close to how the documents express the classes, which is why label phrasing matters much more here than in ordinary supervised learning.</p><p>A subtle but important advantage of this approach is that the same encoder is used for both sides of the comparison. That symmetry is what makes zero-shot matching possible. The document and the label description live in the same coordinate system, so closeness has a meaningful interpretation. You are not matching IDs; you are matching meanings.</p><h2>Selecting the nearest label with cosine similarity</h2><p>Cosine similarity is the scoring rule that turns the embedding comparison into a prediction. For each document, the similarity scores against all label descriptions are computed, and the label with the highest score is selected. In practice, this is just an argmax over the similarity matrix, but conceptually it is doing something very different from a learned decision boundary. The model is not adapting to the target task through training; it is searching for the label phrase that the document most naturally aligns with in embedding space.</p><p>That makes zero-shot matching appealing when you want a fast baseline or when labeled data are scarce. It also makes the method fragile in a way supervised classification is not. If one label description is vague, overly long, or semantically unbalanced compared with the others, its embedding may be disadvantaged even when the class itself is valid. Small wording changes can shift results because you are asking the encoder to judge the relationship between two pieces of text, not between a document and a learned class boundary.</p><h2>When each workflow is the better fit</h2><p>Supervised classification on frozen embeddings is the more dependable option when you have enough labeled examples to train a lightweight head and you want a classifier that is still cheap to run. Zero-shot label matching is attractive when labels are scarce, when you need a quick prototype, or when you want to test whether the label semantics are already well captured by the encoder. Both are lightweight compared with end-to-end fine-tuning, but they fail for different reasons. The supervised version depends on the quality of the frozen representation and the amount of labeled data available to the downstream classifier. The zero-shot version depends even more heavily on how well the label text describes the task.</p><p>For a sentiment task like the movie-review example, both methods can be evaluated with the same held-out test set and the same classification metrics, which makes them easy to compare fairly. That comparison is the real lesson: embeddings are not just a prettier way to store text. They are reusable features, and sometimes they are also a semantic space where labels themselves can act as predictors.</p><h3>From labels to text: the generative-classification idea</h3><p>A generative model can be used as a classifier when it is asked to produce a short text sequence that stands in for a class label. That is the central shift in this section. Instead of using a dedicated classification head that directly returns label probabilities, we give the model an instruction and let it generate a token sequence such as <code>positive</code>, <code>negative</code>, <code>1</code>, or <code>0</code>. The model is still making a class decision, but it is doing so through generation rather than through direct label prediction.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!dt1W!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe7d0c705-dfef-4328-bb7d-fc0db8bc7bac_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!dt1W!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe7d0c705-dfef-4328-bb7d-fc0db8bc7bac_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!dt1W!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe7d0c705-dfef-4328-bb7d-fc0db8bc7bac_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!dt1W!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe7d0c705-dfef-4328-bb7d-fc0db8bc7bac_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!dt1W!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe7d0c705-dfef-4328-bb7d-fc0db8bc7bac_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!dt1W!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe7d0c705-dfef-4328-bb7d-fc0db8bc7bac_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e7d0c705-dfef-4328-bb7d-fc0db8bc7bac_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:6229612,&quot;alt&quot;:&quot;Figure 4.11: Prompt-based classification replaces a dedicated classification head with a text-generation step, then maps the generated string back to a class label.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204106027?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe7d0c705-dfef-4328-bb7d-fc0db8bc7bac_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 4.11: Prompt-based classification replaces a dedicated classification head with a text-generation step, then maps the generated string back to a class label." title="Figure 4.11: Prompt-based classification replaces a dedicated classification head with a text-generation step, then maps the generated string back to a class label." srcset="https://substackcdn.com/image/fetch/$s_!dt1W!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe7d0c705-dfef-4328-bb7d-fc0db8bc7bac_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!dt1W!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe7d0c705-dfef-4328-bb7d-fc0db8bc7bac_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!dt1W!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe7d0c705-dfef-4328-bb7d-fc0db8bc7bac_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!dt1W!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe7d0c705-dfef-4328-bb7d-fc0db8bc7bac_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>That difference matters because the prompt becomes part of the model interface. In a direct classifier, the architecture defines the output space. In a text-to-text or chat workflow, the wording of the instruction helps define the task itself. This is why prompt-based classification is attractive for rapid prototyping, limited labeled data, or access to a strong hosted model. It is also why the method can fail in confusing ways: the model may know the task, but the prompt may not constrain the answer format well enough.</p><p>The same pattern transfers cleanly to ReviewDesk-style triage. A support message can be wrapped in a prompt that asks for a single label such as urgent or non-urgent. The model is not becoming a different kind of classifier; it is being guided to emit the answer in text form.</p><h3>Flan-T5 as a local text-to-text classifier</h3><p>Flan-T5 is a good first example because it is an encoder-decoder model used locally through a text2text-generation pipeline. That makes it natural to treat the model as a text generator that can be instructed to output a sentiment label.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;4c517487-cdc2-49ed-8f28-e2b7771c84d4&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">pipe = pipeline(
    "text2text-generation", 
    model="google/flan-t5-small", 
    device="cuda:0"
)</code></pre></div><p>The prompt is attached to every review before inference. In practice, that means the dataset is mapped so each example contains the same instruction plus its own text. The prompt is short on purpose, because the point is to show the mechanism rather than to overfit the wording.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;09157b3e-36ff-4b25-8bd6-745b56e8d056&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">prompt = "Is the following sentence positive or negative? "
data = data.map(lambda example: {"t5": prompt + example['text']})
data</code></pre></div><p>This mapping step makes the prompt part of the data pipeline. Every review now enters the model with the same instruction, so the whole test set is prepared consistently. That consistency matters because the model is not selecting from a label head; it is completing a tiny generation task whose output must later be interpreted as a class.</p><p>The batch loop runs over the test split, extracts each generated string, and converts it into a numeric label. In the example below, <code>negative</code> maps to <code>0</code>, while anything else is treated as <code>1</code>.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;c54f9252-fc56-4d6b-8b6d-bb00eeeaaa00&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">y_pred = []
for output in tqdm(pipe(KeyDataset(data["test"], "t5")), total=len(data["test"])):
    text = output[0]["generated_text"]
    y_pred.append(0 if text == "negative" else 1)</code></pre></div><p>That conversion step is where output variability can quietly affect evaluation. A model might return <code>negative</code>, <code>Negative</code>, or <code>negative review</code>, and each of those can require a different parsing rule. Exact string matching is simple, but it can also be brittle. If the model gives a semantically correct answer in a slightly different format, the score can drop even though the underlying prediction was reasonable. Label normalization is therefore part of the evaluation logic, not just a cleanup step.</p><p>With the generated labels converted back to integers, the same helper used earlier in the chapter can score the results.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;5f2f0af8-90a1-4bca-8b7c-d43da3d23826&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">evaluate_performance(data["test"]["label"], y_pred)</code></pre></div><p>Using the same precision, recall, accuracy, and F1 score keeps the comparison aligned with the earlier classifier and embedding workflows. A confusion-oriented view is especially helpful here because it reveals whether the model is leaning toward one class or missing one side of the decision boundary.</p><h3>Running the full test set and scoring Flan-T5 outputs</h3><p>The full test-set run shows the practical workflow end to end. The prompt is prepared once, attached to every example, generated outputs are collected in batch, and the returned text is converted into a class label before evaluation. That is the core pattern for prompt-based classification.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!vZ_H!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffbc9a2e7-8532-49ab-9c0d-dbe72abc5392_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!vZ_H!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffbc9a2e7-8532-49ab-9c0d-dbe72abc5392_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!vZ_H!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffbc9a2e7-8532-49ab-9c0d-dbe72abc5392_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!vZ_H!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffbc9a2e7-8532-49ab-9c0d-dbe72abc5392_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!vZ_H!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffbc9a2e7-8532-49ab-9c0d-dbe72abc5392_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!vZ_H!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffbc9a2e7-8532-49ab-9c0d-dbe72abc5392_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/fbc9a2e7-8532-49ab-9c0d-dbe72abc5392_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:6833046,&quot;alt&quot;:&quot;Figure 4.12: End-to-end prompt-based classification workflow: template the input, generate text labels, normalize outputs, and evaluate against ground-truth classes.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204106027?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffbc9a2e7-8532-49ab-9c0d-dbe72abc5392_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 4.12: End-to-end prompt-based classification workflow: template the input, generate text labels, normalize outputs, and evaluate against ground-truth classes." title="Figure 4.12: End-to-end prompt-based classification workflow: template the input, generate text labels, normalize outputs, and evaluate against ground-truth classes." srcset="https://substackcdn.com/image/fetch/$s_!vZ_H!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffbc9a2e7-8532-49ab-9c0d-dbe72abc5392_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!vZ_H!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffbc9a2e7-8532-49ab-9c0d-dbe72abc5392_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!vZ_H!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffbc9a2e7-8532-49ab-9c0d-dbe72abc5392_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!vZ_H!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffbc9a2e7-8532-49ab-9c0d-dbe72abc5392_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>A useful way to debug this kind of system is to separate prompt quality, parsing rules, and actual model fit. If the prompt is vague, the model may answer unpredictably. If the parser is too strict, the model may be correct but still fail the metric pipeline. If both of those are fine and the score is still poor, then the model may simply not fit the task well enough. This triad is often the fastest way to isolate the real problem.</p><p>The same template also works for ReviewDesk triage. A prompt such as &#8220;Classify this ticket as urgent or non-urgent&#8221; can be prepended to each message, and the generated text can be normalized into the corresponding binary label. The specific domain changes, but the method stays the same: the model is asked to complete a label-shaped text fragment.</p><h3>Using ChatGPT for the same sentiment task</h3><p>The API-based version uses the same idea, but it wraps the prompt inside a chat request. That makes the workflow more operationally explicit: a reusable function inserts the document into a template, sends the messages to the API, and returns the model response.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;59d540fd-1a98-46a4-9fcc-10b9be89b4e4&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">import openai

# Create client
client = openai.OpenAI(api_key="YOUR_KEY_HERE")</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;8f8dedc9-7488-434e-b2bb-c30e5a575f49&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def chatgpt_generation(prompt, document, model="gpt-3.5-turbo-0125"):
    """Generate an output based on a prompt and an input document."""
    messages=[
        {
            "role": "system",
            "content": "You are a helpful assistant."
            },
        {
            "role": "user",
            "content":   prompt.replace("[DOCUMENT]", document)
            }
    ]
    chat_completion = client.chat.completions.create(
      messages=messages,
      model=model,
      temperature=0
    )
    return chat_completion.choices[0].message.content</code></pre></div><p>Temperature is set to zero to reduce run-to-run variation. That does not make every service path perfectly deterministic, but it does make evaluation more stable than a higher-temperature setting would. For classification, stability matters more than diversity, because the goal is to measure the prompt and model against fixed labels.</p><p>The prompt template is designed to force a label-only response. The <code>[DOCUMENT]</code> placeholder makes the function reusable, and the explicit instruction narrows the output space so the model behaves more like a classifier.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;a193b65c-fcf4-48a4-ae67-f77126d28d43&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">prompt = """Predict whether the following document is a positive or negative movie review:

[DOCUMENT]

If it is positive return 1 and if it is negative return 0. Do not give any other answers.
"""

document = "unpretentious , charming , quirky , original"
chatgpt_generation(prompt, document)</code></pre></div><p>That same pattern transfers directly to a different binary task. For ReviewDesk-style triage, the template can be adjusted to ask for urgent or non-urgent while keeping the same label-only structure. The important part is not the domain wording itself, but the fact that the output format is tightly constrained.</p><p>To score the whole test set, the wrapper is applied example by example. This is where API-based classification becomes operationally different from local inference. Rate limits can slow the run, retries may be needed if requests fail, and every prediction has a cost. Those realities affect how practical the evaluation is, especially when the test set is large.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;a03a3cc4-8b5e-4293-a898-141167c68886&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">predictions = [
    chatgpt_generation(prompt, doc) for doc in tqdm(data["test"]["text"])
]</code></pre></div><p>The returned strings are then converted into integers before the same evaluation helper is called.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;bcb65c22-de42-46b1-a740-3057d86abf7e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">y_pred = [int(pred) for pred in predictions]

evaluate_performance(data["test"]["label"], y_pred)</code></pre></div><p>Parsing matters here as well. If the model returns exactly <code>1</code> or <code>0</code>, conversion is easy. If it returns <code>positive</code>, <code>negative</code>, or a longer sentence, then the parser needs to normalize those variants before evaluation. Otherwise the metric may reflect formatting noise rather than model behavior. In practice, a strict output rule plus a careful parser often improves score stability more than changing the model does.</p><h3>Practical limits and interpretation</h3><p>Prompt-based classification is useful when you want a fast way to reuse a strong generative model for a label task, but the method is only as good as the prompt, the parser, and the model itself. A weak score may come from vague instructions, brittle normalization, or genuine model disagreement with the task. Debugging is easier when those causes are considered separately.</p><p>Closed-source models add another constraint: their training data, update cadence, and internal behavior are not visible in the way a local model is. That makes results harder to interpret, even when the metrics look strong. The method is still valuable, but it should be treated as a prompt-driven labeling protocol rather than a transparent classifier head.</p><h3>Reconstructing the chapter as one decision tree</h3><p>Text classification in this chapter can be read as one task with three control knobs. The goal stayed the same: assign labels to text under a shared evaluation setup. What changed was the adaptation path. You could use a task-specific pretrained classifier, reuse embeddings and train a downstream classifier, or ask a generative model for the label through a prompt. Seen this way, the methods are not separate problems but different answers to the same label-prediction question.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!CZ1V!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe7ddb777-8a58-4bd4-bf12-504ad5b7a190_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!CZ1V!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe7ddb777-8a58-4bd4-bf12-504ad5b7a190_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!CZ1V!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe7ddb777-8a58-4bd4-bf12-504ad5b7a190_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!CZ1V!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe7ddb777-8a58-4bd4-bf12-504ad5b7a190_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!CZ1V!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe7ddb777-8a58-4bd4-bf12-504ad5b7a190_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!CZ1V!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe7ddb777-8a58-4bd4-bf12-504ad5b7a190_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e7ddb777-8a58-4bd4-bf12-504ad5b7a190_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:5214309,&quot;alt&quot;:&quot;Figure 4.13: Three ways to solve the same text classification task: direct classification, embedding-based classification, and prompt-based label generation under a shared evaluation setup.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204106027?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe7ddb777-8a58-4bd4-bf12-504ad5b7a190_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 4.13: Three ways to solve the same text classification task: direct classification, embedding-based classification, and prompt-based label generation under a shared evaluation setup." title="Figure 4.13: Three ways to solve the same text classification task: direct classification, embedding-based classification, and prompt-based label generation under a shared evaluation setup." srcset="https://substackcdn.com/image/fetch/$s_!CZ1V!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe7ddb777-8a58-4bd4-bf12-504ad5b7a190_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!CZ1V!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe7ddb777-8a58-4bd4-bf12-504ad5b7a190_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!CZ1V!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe7ddb777-8a58-4bd4-bf12-504ad5b7a190_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!CZ1V!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe7ddb777-8a58-4bd4-bf12-504ad5b7a190_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3>Compare the three workflows by where the signal comes from</h3><p>The direct classifier puts the model choice front and center. The embedding-based pipeline shifts the work to the representation and the lightweight classifier on top. The prompt-based route changes the wording of the request instead of adding a new training stage. That is why the chapter&#8217;s methods can be remembered as label-first, vector-first, and prompt-first. Flan-T5 and GPT-style models fit the last category by turning generation into classification without task-specific retraining.</p><h3>Why the same labeled dataset supports multiple evaluation paths</h3><p>The same labeled dataset can support all three workflows, which makes comparison straightforward and fair. Each method sees the same examples and the same ground truth labels, so differences in results reflect the method choice rather than a different benchmark. This gives a practical decision lens based on simplicity, flexibility, and dependence on labels or prompts.</p><h3>The practical takeaway and the next problem setting</h3><p>The chapter&#8217;s core lesson is that you choose among different levers, not different tasks. If labels are available, the question is how best to predict them. If labels are absent, the problem changes shape entirely. That is where the next chapter begins, with clustering and topic modeling as ways to discover structure instead of assigning known categories.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!P-2H!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6e1c6d42-9bca-45e1-b72d-ee158366bc62_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!P-2H!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6e1c6d42-9bca-45e1-b72d-ee158366bc62_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!P-2H!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6e1c6d42-9bca-45e1-b72d-ee158366bc62_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!P-2H!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6e1c6d42-9bca-45e1-b72d-ee158366bc62_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!P-2H!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6e1c6d42-9bca-45e1-b72d-ee158366bc62_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!P-2H!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6e1c6d42-9bca-45e1-b72d-ee158366bc62_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6e1c6d42-9bca-45e1-b72d-ee158366bc62_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:6561106,&quot;alt&quot;:&quot;Figure 4.14: A chapter transition map: when labels are available, the task is classification; when labels are absent, the problem shifts to unsupervised structure discovery such as clustering and topic modeling.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204106027?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6e1c6d42-9bca-45e1-b72d-ee158366bc62_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 4.14: A chapter transition map: when labels are available, the task is classification; when labels are absent, the problem shifts to unsupervised structure discovery such as clustering and topic modeling." title="Figure 4.14: A chapter transition map: when labels are available, the task is classification; when labels are absent, the problem shifts to unsupervised structure discovery such as clustering and topic modeling." srcset="https://substackcdn.com/image/fetch/$s_!P-2H!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6e1c6d42-9bca-45e1-b72d-ee158366bc62_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!P-2H!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6e1c6d42-9bca-45e1-b72d-ee158366bc62_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!P-2H!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6e1c6d42-9bca-45e1-b72d-ee158366bc62_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!P-2H!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6e1c6d42-9bca-45e1-b72d-ee158366bc62_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><em>Use the url below to download the enitre book as pdf:</em></p>
      <p>
          <a href="https://onepagecode.substack.com/p/text-classification-pipelines-direct">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Tokens and Embeddings]]></title><description><![CDATA[Chapter 2 of Hands-On Large Language Models: Deconstructing the representation pipeline from raw text to token IDs and vector embeddings]]></description><link>https://onepagecode.substack.com/p/tokens-and-embeddings</link><guid isPermaLink="false">https://onepagecode.substack.com/p/tokens-and-embeddings</guid><dc:creator><![CDATA[Onepagecode]]></dc:creator><pubDate>Sun, 05 Jul 2026 19:41:26 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!RHre!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4d115f9-dbda-4baf-b566-0895332b9389_2816x1536.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Why Tokens and Embeddings Matter</h2><p>Raw text is convenient for people and awkward for models. A support ticket in ReviewDesk might arrive as a polished paragraph, but the model cannot use that paragraph directly. It first needs the text broken into tokens, then converted into token IDs, and then mapped into embeddings. That progression is the basic representation pipeline of modern language systems, and it explains why a small change in preprocessing can affect generation quality, search quality, and even cost.</p><div><hr></div><h2><strong>&#128216; Buy the Entire Book &#8211; Available on Amazon Worldwide (If you are paid subscriber, use the url at the end of this article to download the book for free!)</strong></h2><p>You can now purchase the complete book directly from Amazon in your country:</p><ul><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/us/dualbookshelf.marketplacelink/B0H6WZJ57S">United States (US)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/uk/dualbookshelf.marketplacelink/B0H6WZJ57S">United Kingdom (UK)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/de/dualbookshelf.marketplacelink/B0H6WZJ57S">Germany (DE)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/fr/dualbookshelf.marketplacelink/B0H6WZJ57S">France (FR)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/es/dualbookshelf.marketplacelink/B0H6WZJ57S">Spain (ES)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/it/dualbookshelf.marketplacelink/B0H6WZJ57S">Italy (IT)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/nl/dualbookshelf.marketplacelink/B0H6WZJ57S">Netherlands (NL)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/jp/dualbookshelf.marketplacelink/B0H6WZJ57S">Japan (JP)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/br/dualbookshelf.marketplacelink/B0H6WZJ57S">Brazil (BR):</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/ca/dualbookshelf.marketplacelink/B0H6WZJ57S">Canada (CA)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/mx/dualbookshelf.marketplacelink/B0H6WZJ57S">Mexico (MX)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/au/dualbookshelf.marketplacelink/B0H6WZJ57S">Australia (AU)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/in/dualbookshelf.marketplacelink/B0H6WZJ57S">India (IN)</a></strong></p><div><hr></div></li></ul><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!RHre!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4d115f9-dbda-4baf-b566-0895332b9389_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!RHre!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4d115f9-dbda-4baf-b566-0895332b9389_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!RHre!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4d115f9-dbda-4baf-b566-0895332b9389_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!RHre!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4d115f9-dbda-4baf-b566-0895332b9389_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!RHre!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4d115f9-dbda-4baf-b566-0895332b9389_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!RHre!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4d115f9-dbda-4baf-b566-0895332b9389_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a4d115f9-dbda-4baf-b566-0895332b9389_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:5660623,&quot;alt&quot;:&quot;Figure 2.1: A language model does not consume raw text directly; it transforms text into tokens, token IDs, and embeddings through a representation pipeline.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204104981?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4d115f9-dbda-4baf-b566-0895332b9389_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 2.1: A language model does not consume raw text directly; it transforms text into tokens, token IDs, and embeddings through a representation pipeline." title="Figure 2.1: A language model does not consume raw text directly; it transforms text into tokens, token IDs, and embeddings through a representation pipeline." srcset="https://substackcdn.com/image/fetch/$s_!RHre!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4d115f9-dbda-4baf-b566-0895332b9389_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!RHre!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4d115f9-dbda-4baf-b566-0895332b9389_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!RHre!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4d115f9-dbda-4baf-b566-0895332b9389_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!RHre!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4d115f9-dbda-4baf-b566-0895332b9389_2816x1536.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>A useful mental model is simple: tokens are the pieces, token IDs are the labels, and embeddings are the coordinates. A token is a text fragment produced by tokenization, such as a word, part of a word, punctuation mark, or special symbol. A token ID is just the integer label assigned to that fragment by a tokenizer vocabulary. An embedding is a learned numeric vector that places that token, or a larger unit of text, in a space where related items sit near one another. The ID identifies a symbol in a vocabulary; it does not carry meaning by itself. The embedding is where meaning becomes computable.</p><p>This distinction matters because models do not reason over raw strings. They reason over structured inputs. In generation, the model consumes token IDs, looks up token embeddings, and transforms them into internal states that drive the next prediction. In retrieval-style systems, the same general idea appears in a different form: text is turned into a vector representation so the system can compare meanings rather than match exact words. A ticket about a &#8220;login loop&#8221; and another about &#8220;cannot sign in&#8221; may share little surface overlap, yet their embeddings can still land nearby if they describe the same underlying problem.</p><p>That is why representation choices shape behavior so strongly. If the model sees the wrong token boundaries, it may spend extra capacity recovering a phrase that should have been simple. If the embedding space is poorly organized, semantically similar items will not cluster together, and search or recommendation will feel brittle. In other words, the model&#8217;s input representation is not a neutral prelude to intelligence; it is part of the intelligence.</p><p>It is also important to separate token embeddings from text embeddings. Token embeddings live inside a model and correspond to individual tokens. They are the learned vectors the model uses as it processes a sequence. Text embeddings are usually fixed-size vectors for an entire sentence, ticket, paragraph, or document, designed for similarity search, clustering, classification, or recommendation. A token embedding is like a local coordinate for one piece of text inside a model&#8217;s internal machinery. A text embedding is more like a compact map of the whole item, suitable for comparing one item to another.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!n0pd!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3cf12595-a562-4024-bb07-7dc3526a8e18_1408x768.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!n0pd!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3cf12595-a562-4024-bb07-7dc3526a8e18_1408x768.png 424w, https://substackcdn.com/image/fetch/$s_!n0pd!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3cf12595-a562-4024-bb07-7dc3526a8e18_1408x768.png 848w, https://substackcdn.com/image/fetch/$s_!n0pd!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3cf12595-a562-4024-bb07-7dc3526a8e18_1408x768.png 1272w, https://substackcdn.com/image/fetch/$s_!n0pd!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3cf12595-a562-4024-bb07-7dc3526a8e18_1408x768.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!n0pd!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3cf12595-a562-4024-bb07-7dc3526a8e18_1408x768.png" width="1408" height="768" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3cf12595-a562-4024-bb07-7dc3526a8e18_1408x768.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:768,&quot;width&quot;:1408,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:258244,&quot;alt&quot;:&quot;Figure 2.2: Token embeddings are internal per-token vectors used within a model, while text embeddings are fixed-size whole-text vectors used for similarity and downstream tasks.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204104981?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3cf12595-a562-4024-bb07-7dc3526a8e18_1408x768.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 2.2: Token embeddings are internal per-token vectors used within a model, while text embeddings are fixed-size whole-text vectors used for similarity and downstream tasks." title="Figure 2.2: Token embeddings are internal per-token vectors used within a model, while text embeddings are fixed-size whole-text vectors used for similarity and downstream tasks." srcset="https://substackcdn.com/image/fetch/$s_!n0pd!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3cf12595-a562-4024-bb07-7dc3526a8e18_1408x768.png 424w, https://substackcdn.com/image/fetch/$s_!n0pd!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3cf12595-a562-4024-bb07-7dc3526a8e18_1408x768.png 848w, https://substackcdn.com/image/fetch/$s_!n0pd!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3cf12595-a562-4024-bb07-7dc3526a8e18_1408x768.png 1272w, https://substackcdn.com/image/fetch/$s_!n0pd!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3cf12595-a562-4024-bb07-7dc3526a8e18_1408x768.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>This chapter will show how that representation idea extends far beyond language generation. Word2vec made the embedding concept famous by learning useful vector neighborhoods from co-occurrence patterns, long before today&#8217;s large language models dominated the field. The same broad idea now powers semantic search over help-center articles, clustering of similar support requests, and recommendation systems that suggest related items based on vector proximity. The surface domain changes, but the workflow is the same: turn objects into coordinates, then use geometry to answer practical questions.</p><p>For ReviewDesk, that means a ticket can move from raw text to tokens, from tokens to IDs, and from IDs to vectors that support routing, search, and later analysis. The common mistake is to treat every embedding as interchangeable. A token embedding, a contextual hidden state, and a sentence embedding all live in vector space, but they are not the same object and they do not serve the same role. Keeping that distinction clear will make the next sections much easier to follow.</p><h2>From Text to Token IDs</h2><h3>The model never sees raw text</h3><p>A language model does not read your prompt as plain text. It receives token IDs, which are numbers assigned by a tokenizer. That is the first important boundary in the workflow: the visible sentence you type is not the model input. The tokenizer turns that sentence into a sequence of numeric IDs, and the model generates new IDs in response. Decoding turns those IDs back into text after generation.</p><p>A good way to think about this is as a translation layer. Human text goes in, machine-readable inventory codes come out, and those codes are what the model actually processes. In a support-ticket workflow, for example, the message a user types is only the starting point. The model sees the tokenized version of that message, not the original characters.</p><p>It also helps to keep four terms separate. Text is the string you type. Tokens are the pieces the tokenizer creates. Token IDs are the numbers attached to those pieces. A decoded string is what you get when token IDs are converted back into readable text. Those are related forms of the same content, but they are not interchangeable.</p><h3>Tracing the encode step end to end</h3><p>The practical starting point is to load a pretrained model with its matching tokenizer. That pairing matters because tokenization is checkpoint-specific rather than universal.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;a67284a0-0e30-46d5-8fca-d787c6039e97&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from transformers import AutoModelForCausalLM, AutoTokenizer

# Load model and tokenizer
model = AutoModelForCausalLM.from_pretrained(
    "microsoft/Phi-3-mini-4k-instruct",
    device_map="cuda",
    torch_dtype="auto",
    trust_remote_code=True,
)
tokenizer = AutoTokenizer.from_pretrained("microsoft/Phi-3-mini-4k-instruct")</code></pre></div><p>Once the pair is loaded, the encode step converts a prompt into <code>input_ids</code>. That tensor is the model input. The model does not consume the visible prompt string directly; it consumes the numeric IDs produced by the tokenizer.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Bth8!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5f8da1b-6383-4bd8-a560-7d6fdaafd50a_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Bth8!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5f8da1b-6383-4bd8-a560-7d6fdaafd50a_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!Bth8!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5f8da1b-6383-4bd8-a560-7d6fdaafd50a_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!Bth8!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5f8da1b-6383-4bd8-a560-7d6fdaafd50a_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!Bth8!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5f8da1b-6383-4bd8-a560-7d6fdaafd50a_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Bth8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5f8da1b-6383-4bd8-a560-7d6fdaafd50a_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d5f8da1b-6383-4bd8-a560-7d6fdaafd50a_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:5892546,&quot;alt&quot;:&quot;Figure 2.3: Text is transformed into token IDs by a tokenizer, processed by the model in numeric form, and then decoded back into readable text after generation.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204104981?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5f8da1b-6383-4bd8-a560-7d6fdaafd50a_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 2.3: Text is transformed into token IDs by a tokenizer, processed by the model in numeric form, and then decoded back into readable text after generation." title="Figure 2.3: Text is transformed into token IDs by a tokenizer, processed by the model in numeric form, and then decoded back into readable text after generation." srcset="https://substackcdn.com/image/fetch/$s_!Bth8!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5f8da1b-6383-4bd8-a560-7d6fdaafd50a_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!Bth8!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5f8da1b-6383-4bd8-a560-7d6fdaafd50a_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!Bth8!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5f8da1b-6383-4bd8-a560-7d6fdaafd50a_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!Bth8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5f8da1b-6383-4bd8-a560-7d6fdaafd50a_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;abc2a531-6408-4b25-aa27-d600a8abe55c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">prompt = "Write an email apologizing to Sarah for the tragic gardening mishap. Explain how it happened.&lt;|assistant|&gt;"

# Tokenize the input prompt
input_ids = tokenizer(prompt, return_tensors="pt").input_ids.to("cuda")</code></pre></div><p>If you inspect <code>input_ids</code>, you can see how much context the prompt occupies and where the tokenizer placed its boundaries. This is useful in practice because two prompts that look similar to a person can take very different numbers of tokens. That difference affects how much room remains in the context window for the actual task.</p><p>The encode step is also where subword tokenization shows up. A tokenizer may keep a common word intact, split a rarer word into pieces, or separate punctuation and whitespace in ways that are not obvious from the rendered sentence. The result depends on the tokenizer&#8217;s vocabulary, special-token rules, and training domain.</p><h3>Tracing the decode step and the generation loop</h3><p>Generation happens in token space. The model predicts the next token ID, appends it to the sequence, predicts another ID, and repeats until it reaches a stopping condition such as a token limit or an end marker. That is autoregressive generation: the model extends the output one token at a time rather than producing a finished sentence all at once.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;c78275fd-48a5-429d-9845-ebd4f5e7d2ab&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># Generate the text
generation_output = model.generate(
  input_ids=input_ids,
  max_new_tokens=20
)

# Print the output
print(tokenizer.decode(generation_output[0]))</code></pre></div><p>The decoded output is easier for a person to read, but it is not always a perfect visual mirror of what the model produced. The returned sequence may include the original prompt tokens, special markers, or formatting artifacts that were important to the model during inference. In a chat-style checkpoint, a marker such as <code>&lt;|assistant|&gt;</code> is structural input, not ordinary prose. It tells the model which role is expected to speak next.</p><p>That is why prompt templates matter. Copying a chat prompt from one checkpoint to another without checking its expected special tokens can change behavior even when the visible text looks correct. The prompt may still read naturally to you while being malformed for the model.</p><h3>Reading token boundaries one piece at a time</h3><p>A per-token decode loop is a simple way to inspect segmentation. It reveals where one token ends and the next begins, which is especially helpful when a visible word becomes several pieces.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;8b2b95d9-b28c-4494-a4ba-144f160bbdb8&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">for id in input_ids[0]:
   print(tokenizer.decode(id))</code></pre></div><p>This kind of inspection makes subword tokenization concrete. A common word may appear as a single token, while a rarer word, a hyphenated phrase, a digit sequence, or punctuation may split across multiple IDs. Whitespace can also be handled differently across tokenizers, so the same text can look clean on the page while being packed in an unexpected way underneath.</p><p>Special tokens are part of that structure too. They are reserved symbols for roles, separators, padding, or sequence boundaries. They are not ordinary words, and they should be treated as model-facing formatting rather than prose. If they are omitted or misplaced, the visible prompt may still look reasonable while the model behavior changes.</p><h3>Same sentence, different tokenizer, different result</h3><p>The same sentence can tokenize differently across pretrained checkpoints. That is the main practical lesson: tokenization is model-specific, not universal. Different tokenizers reflect different vocabularies, special-token conventions, capitalization choices, and training domains.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!RKf3!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce60a644-70ae-4955-b9cf-e139ad35cce1_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!RKf3!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce60a644-70ae-4955-b9cf-e139ad35cce1_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!RKf3!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce60a644-70ae-4955-b9cf-e139ad35cce1_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!RKf3!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce60a644-70ae-4955-b9cf-e139ad35cce1_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!RKf3!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce60a644-70ae-4955-b9cf-e139ad35cce1_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!RKf3!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce60a644-70ae-4955-b9cf-e139ad35cce1_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ce60a644-70ae-4955-b9cf-e139ad35cce1_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:5435421,&quot;alt&quot;:&quot;Figure 2.4: The same sentence can split into different token sequences depending on the tokenizer, changing token count, context usage, and model behavior.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204104981?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce60a644-70ae-4955-b9cf-e139ad35cce1_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 2.4: The same sentence can split into different token sequences depending on the tokenizer, changing token count, context usage, and model behavior." title="Figure 2.4: The same sentence can split into different token sequences depending on the tokenizer, changing token count, context usage, and model behavior." srcset="https://substackcdn.com/image/fetch/$s_!RKf3!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce60a644-70ae-4955-b9cf-e139ad35cce1_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!RKf3!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce60a644-70ae-4955-b9cf-e139ad35cce1_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!RKf3!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce60a644-70ae-4955-b9cf-e139ad35cce1_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!RKf3!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce60a644-70ae-4955-b9cf-e139ad35cce1_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;93ddea8c-c7a7-4229-b7e7-43df033c4b0e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">colors_list = [
    '102;194;165', '252;141;98', '141;160;203', 
    '231;138;195', '166;216;84', '255;217;47'
]

def show_tokens(sentence, tokenizer_name):
    tokenizer = AutoTokenizer.from_pretrained(tokenizer_name)
    token_ids = tokenizer(sentence).input_ids
    for idx, t in enumerate(token_ids):
        print(
            f'\x1b[0;30;48;2;{colors_list[idx % len(colors_list)]}m' + 
            tokenizer.decode(t) + 
            '\x1b[0m', 
            end=' '
        )</code></pre></div><p>This comparison becomes most revealing when the sentence contains whitespace, punctuation, digits, code indentation, emojis, or multilingual text. A model trained for general conversation may split those patterns differently from a code-oriented or domain-specific checkpoint. Code-focused tokenizers often preserve indentation and spacing more carefully because those details matter in programming text. Prose-oriented tokenizers may fragment the same structure more aggressively.</p><p>Digits can also surprise you. Some tokenizers keep number strings compact, while others break them into smaller pieces. Emojis and non-English text vary as well, depending on how broad the tokenizer&#8217;s Unicode coverage is and how much of its training data came from those forms of text. The point is not that one behavior is universally better. The point is that the same visible sentence can be packaged differently depending on the checkpoint.</p><p>In practical terms, those differences affect context usage, cost, and output quality. If you pay by token in an API setting, a more fragmented prompt costs more. If you are working with a fixed context window, a longer token sequence leaves less room for instructions, examples, or conversation history. Tokenization also affects reproducibility: a prompt template that works with one model may not behave the same way with another because the special tokens and boundary rules are different.</p><h3>Why tokenization choices matter in practice</h3><p>Tokenization is therefore part of the model interface, not just a preprocessing step. Choosing a model also means choosing its tokenizer contract, and that contract shapes how your text is packaged for inference. The practical lesson is simple: the model operates on token IDs, not on raw characters or words, and the path from text to IDs is specific to the checkpoint you use.</p><h2>Token Embeddings and Contextual Vectors</h2><h3>From token IDs to learned embedding lookups</h3><p>Once text has been tokenized, the model no longer sees ordinary words. It receives token IDs, and those IDs are just integer addresses into the vocabulary. An ID is not a meaning in itself. A good mental model is a filing cabinet: the label on the drawer is not the document inside it. The label tells the model where to look, and the contents of that location are the learned token embeddings.</p><p>Those embeddings live in the model as an embedding matrix, a parameter tensor with one row for each vocabulary item. During training, the model learns those rows so they become useful starting representations for the tokens it can read. This is why token IDs matter so much: they do not carry semantics directly, but they determine which row of the embedding table is selected.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!-2j5!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9aa08cd0-f753-4a48-a145-aee72b3612b6_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!-2j5!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9aa08cd0-f753-4a48-a145-aee72b3612b6_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!-2j5!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9aa08cd0-f753-4a48-a145-aee72b3612b6_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!-2j5!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9aa08cd0-f753-4a48-a145-aee72b3612b6_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!-2j5!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9aa08cd0-f753-4a48-a145-aee72b3612b6_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!-2j5!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9aa08cd0-f753-4a48-a145-aee72b3612b6_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9aa08cd0-f753-4a48-a145-aee72b3612b6_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:5820487,&quot;alt&quot;:&quot;Figure 2.5: Token IDs act as addresses into a learned embedding matrix, producing static token embeddings that are later transformed into context-sensitive vectors.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204104981?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9aa08cd0-f753-4a48-a145-aee72b3612b6_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 2.5: Token IDs act as addresses into a learned embedding matrix, producing static token embeddings that are later transformed into context-sensitive vectors." title="Figure 2.5: Token IDs act as addresses into a learned embedding matrix, producing static token embeddings that are later transformed into context-sensitive vectors." srcset="https://substackcdn.com/image/fetch/$s_!-2j5!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9aa08cd0-f753-4a48-a145-aee72b3612b6_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!-2j5!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9aa08cd0-f753-4a48-a145-aee72b3612b6_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!-2j5!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9aa08cd0-f753-4a48-a145-aee72b3612b6_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!-2j5!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9aa08cd0-f753-4a48-a145-aee72b3612b6_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>That distinction is important when you move from text on the page to model internals. The tokenizer turns the input into discrete symbols, and the model turns those symbols into dense vectors that can be processed by later layers. At this stage, the model is not yet classifying, summarizing, or reasoning about the text. It is simply mapping token IDs into learned vector space.</p><h3>What is stored in the embedding matrix</h3><p>The embedding matrix is shared across every occurrence of a token. If the same token appears in two different texts, the same row is used as its initial representation each time. That row is learned, not hand-designed, and it belongs to the model checkpoint rather than to the raw text.</p><p>This shared lookup is powerful, but it is also limited. A static token embedding does not change when the surrounding words change. The token for a word or subword piece always starts from the same vector. What changes later is the model&#8217;s contextual output for that token, not the stored row itself.</p><p>You can think of the embedding matrix as a vocabulary-sized table of reusable starting points. The model assigns each token an address in that table, and the rest of the network can reshape those starting points once the full sequence has been read. That is why the embedding matrix is a learned parameter tensor, while the output of a forward pass is a context-sensitive representation.</p><h3>Why the tokenizer and model must match</h3><p>A pretrained checkpoint depends on the tokenizer it was trained with. The vocabulary, subword segmentation, and special tokens are part of the model&#8217;s expected interface. If the tokenizer changes, the token IDs change too, and the lookup no longer lands on the rows the model learned to use.</p><p>This coupling is not a minor implementation detail. It is the bridge that makes the tokenizer-model workflow work at all. A pretrained model expects its original tokenization scheme, and that is why tokenizer and model are best treated as a matched pair.</p><h3>Context turns static token vectors into contextual outputs</h3><p>The same token can point to different meanings depending on context. Consider a word like &#8220;charge.&#8221; In &#8220;an extra charge appeared on the bill,&#8221; the surrounding words suggest a billing sense. In &#8220;the battery charge dropped overnight,&#8221; the same token appears in a different setting. The embedding row used to start both cases is the same, but the vector that comes out of the model can differ because the context differs.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!3LMJ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1855f2b1-14ab-4f99-833a-9a94bc2388d4_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!3LMJ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1855f2b1-14ab-4f99-833a-9a94bc2388d4_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!3LMJ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1855f2b1-14ab-4f99-833a-9a94bc2388d4_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!3LMJ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1855f2b1-14ab-4f99-833a-9a94bc2388d4_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!3LMJ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1855f2b1-14ab-4f99-833a-9a94bc2388d4_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!3LMJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1855f2b1-14ab-4f99-833a-9a94bc2388d4_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1855f2b1-14ab-4f99-833a-9a94bc2388d4_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:5427893,&quot;alt&quot;:&quot;Figure 2.6: The same token embedding can produce different contextual vectors when the surrounding words change the meaning of the token.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204104981?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1855f2b1-14ab-4f99-833a-9a94bc2388d4_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 2.6: The same token embedding can produce different contextual vectors when the surrounding words change the meaning of the token." title="Figure 2.6: The same token embedding can produce different contextual vectors when the surrounding words change the meaning of the token." srcset="https://substackcdn.com/image/fetch/$s_!3LMJ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1855f2b1-14ab-4f99-833a-9a94bc2388d4_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!3LMJ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1855f2b1-14ab-4f99-833a-9a94bc2388d4_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!3LMJ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1855f2b1-14ab-4f99-833a-9a94bc2388d4_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!3LMJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1855f2b1-14ab-4f99-833a-9a94bc2388d4_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>This is the key distinction to keep straight. A token embedding is the learned lookup vector associated with a token ID. A contextual token vector is the model&#8217;s output for that token after the surrounding sequence has influenced it. The first is static and shared. The second is dynamic and sequence-dependent.</p><p>That is also why these vectors are useful later in the chapter. For tasks such as tagging or summarization, the model needs representations that reflect how each token is used in context, not just which token was present. The model output preserves token-level structure while carrying information from the whole sequence.</p><p>It is tempting to inspect a vector and search for a neat human meaning in each dimension. That usually does not work. Individual coordinates are not reliable feature labels. These vectors are better understood as points in a learned space, where meaning comes from relationships and transformations rather than from single values.</p><h3>Inspecting the forward pass: shapes, special tokens, and decoded tokens</h3><p>The small example below shows the basic workflow: load a tokenizer and model, tokenize a short input, run a forward pass, and inspect the outputs. The point is not the checkpoint names themselves. The point is to see how token IDs become contextual token vectors and how you can verify that process with simple shape inspection.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;bae4c2be-31d4-47f3-9616-67392927f05c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from transformers import AutoModel, AutoTokenizer

# Load a tokenizer
tokenizer = AutoTokenizer.from_pretrained("microsoft/deberta-base")

# Load a language model
model = AutoModel.from_pretrained("microsoft/deberta-v3-xsmall")

# Tokenize the sentence
tokens = tokenizer('Hello world', return_tensors='pt')

# Process the tokens
output = model(**tokens)[0]</code></pre></div><p>If you check <code>output.shape</code>, the dimensions tell you how the forward pass is organized. A shape such as <code>(1, 4, 768)</code> reads as batch size, sequence length, and embedding width. The first number says how many examples are in the batch. The second says how many token positions the model is returning. The third says how wide each contextual token vector is. Shape inspection is a useful debugging habit because it confirms wiring, even though it does not by itself explain semantics.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;a10c465e-64bd-4d97-be75-d9da1098f30e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">output.shape</code></pre></div><p>You can also decode the token IDs to see the actual sequence the model received. That list may include special tokens, which are bookkeeping markers rather than ordinary words, and it may include subword pieces instead of full words. Seeing those tokens directly helps explain why a decoded list sometimes looks different from the original sentence. The model sees the tokenized sequence, not the human-friendly string alone.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;552f6bab-09e8-457b-84a3-8a3ab9dc926d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">for token in tokens['input_ids'][0]:
    print(tokenizer.decode(token))</code></pre></div><h3>Why these vectors matter later</h3><p>These outputs are not final predictions, and they are not sentence embeddings. They are contextual token vectors that keep token-level detail while carrying information from the entire input. That makes them the right internal representation for many downstream sequence tasks, and it also sets up the next chapter&#8217;s discussion of Transformer internals, where these context-sensitive vectors are produced and transformed.</p><h2>Fixed-Size Text Embeddings</h2><h3>From Token Vectors to One Text Vector</h3><p>Token embeddings are useful when the model needs to reason about individual words or subword pieces, but many practical tasks ask a different question: what does this whole sentence, ticket, paragraph, or document mean? A text embedding answers that question with a single fixed-size vector. Instead of keeping one vector per token, the representation compresses the entire text into one reusable object whose position in vector space reflects semantic meaning. If two support tickets describe the same outage in different words, their text embeddings should land near each other even when the surface wording differs. That is the key shift from token-level representation to text-level representation.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!9ux1!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff8b4aaad-f79d-4540-94d2-e8d1d102a8e8_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!9ux1!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff8b4aaad-f79d-4540-94d2-e8d1d102a8e8_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!9ux1!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff8b4aaad-f79d-4540-94d2-e8d1d102a8e8_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!9ux1!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff8b4aaad-f79d-4540-94d2-e8d1d102a8e8_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!9ux1!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff8b4aaad-f79d-4540-94d2-e8d1d102a8e8_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!9ux1!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff8b4aaad-f79d-4540-94d2-e8d1d102a8e8_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f8b4aaad-f79d-4540-94d2-e8d1d102a8e8_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:4818949,&quot;alt&quot;:&quot;Figure 2.7: Token embeddings represent individual pieces of text, while a text embedding compresses the entire sentence or document into one fixed-size vector whose position reflects semantic meaning.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204104981?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff8b4aaad-f79d-4540-94d2-e8d1d102a8e8_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 2.7: Token embeddings represent individual pieces of text, while a text embedding compresses the entire sentence or document into one fixed-size vector whose position reflects semantic meaning." title="Figure 2.7: Token embeddings represent individual pieces of text, while a text embedding compresses the entire sentence or document into one fixed-size vector whose position reflects semantic meaning." srcset="https://substackcdn.com/image/fetch/$s_!9ux1!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff8b4aaad-f79d-4540-94d2-e8d1d102a8e8_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!9ux1!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff8b4aaad-f79d-4540-94d2-e8d1d102a8e8_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!9ux1!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff8b4aaad-f79d-4540-94d2-e8d1d102a8e8_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!9ux1!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff8b4aaad-f79d-4540-94d2-e8d1d102a8e8_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>A helpful way to think about this is as a semantic photocopier. The model does not preserve every token in visible form; it preserves meaning in a compact vector form that is easy to compare later. For a ReviewDesk-style workflow, that means an incoming message about a refund delay can be turned into one vector and compared against vectors for resolved cases, knowledge-base articles, or ticket categories. The value is not just that the text becomes numeric. The value is that similar texts become nearby vectors, which makes downstream matching possible.</p><h3>How a Model Produces Text Embeddings</h3><p>There are two common ways to get a text embedding. The practical default is to use a model designed specifically for representation learning, such as a sentence-transformer. These models are built to map text directly into useful semantic vectors. A simpler mental model is to imagine aggregating token embeddings, such as averaging them across the sequence. That intuition is helpful, and it explains why a sentence can be collapsed into one vector, but in real applications specialized embedding models usually work better because they are trained for that exact purpose.</p><p>This distinction matters because producing an embedding is not the same thing as deciding what to do with it. Generating the vector is only the representation step. Using that vector for semantic search, categorization, clustering, or retrieval is the pipeline step that follows. In other words, the model produces the map, and the application decides how to navigate with it. A good embedding by itself is not the solution; it becomes useful when paired with a task that can exploit similarity.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!3rGj!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2bff0991-3dbf-45fd-abb1-f6f3f84ef122_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!3rGj!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2bff0991-3dbf-45fd-abb1-f6f3f84ef122_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!3rGj!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2bff0991-3dbf-45fd-abb1-f6f3f84ef122_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!3rGj!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2bff0991-3dbf-45fd-abb1-f6f3f84ef122_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!3rGj!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2bff0991-3dbf-45fd-abb1-f6f3f84ef122_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!3rGj!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2bff0991-3dbf-45fd-abb1-f6f3f84ef122_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2bff0991-3dbf-45fd-abb1-f6f3f84ef122_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:5044470,&quot;alt&quot;:&quot;Figure 2.8: An embedding model produces a representation; downstream applications then use that vector for search, classification, clustering, or retrieval.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204104981?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2bff0991-3dbf-45fd-abb1-f6f3f84ef122_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 2.8: An embedding model produces a representation; downstream applications then use that vector for search, classification, clustering, or retrieval." title="Figure 2.8: An embedding model produces a representation; downstream applications then use that vector for search, classification, clustering, or retrieval." srcset="https://substackcdn.com/image/fetch/$s_!3rGj!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2bff0991-3dbf-45fd-abb1-f6f3f84ef122_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!3rGj!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2bff0991-3dbf-45fd-abb1-f6f3f84ef122_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!3rGj!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2bff0991-3dbf-45fd-abb1-f6f3f84ef122_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!3rGj!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2bff0991-3dbf-45fd-abb1-f6f3f84ef122_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3>Loading a Pretrained Sentence-Transformer</h3><p>The easiest way to see the idea in action is to load a pretrained sentence-transformer and encode a sentence. Notice that this is not training from scratch. The model already knows how to place related texts near each other in embedding space, and your job is simply to reuse that capability.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;8b02ae53-bed5-48bd-affc-e47a23b3a479&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from sentence_transformers import SentenceTransformer

# Load model
model = SentenceTransformer("sentence-transformers/all-mpnet-base-v2")

# Convert text to text embeddings
vector = model.encode("Best movie ever!")</code></pre></div><p>That single call turns a sentence into a vector that can later be compared with other vectors. In a support workflow, you might encode the message and then search for the nearest resolved ticket or the most relevant help article. In a recommendation workflow, you might compare a query vector with item vectors and surface the closest matches. The model does not know whether you will use the vector for search, ranking, or classification; it only produces the representation.</p><h3>Checking the Shape of the Output</h3><p>The most immediate sanity check is the vector shape.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;e076dec2-3e21-4e03-a9a2-8e3ea38153c2&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">vector.shape</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;9dd17fdd-31f4-4e39-91db-66b4953e655f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">(768,)</code></pre></div><p>This is what fixed-size means in practice. The text may be a short phrase or a long paragraph, but the output is one vector with a constant dimensionality. The exact size is model-dependent, not universal. Here it happens to be 768, but another embedding model may produce 384, 1024, or a different size altogether. Treat the dimensionality as a property of the model, the same way you would treat an API response schema as part of the contract.</p><h3>Where Text Embeddings Fit in Real Systems</h3><p>Fixed-size embeddings are especially attractive when you need comparison, storage, or indexing. They are compact enough to cache, easy to pass into a classifier, and convenient for nearest-neighbor search. That makes them a natural bridge into semantic search and retrieval-augmented generation, where an input query is embedded, similar documents are retrieved, and those documents are then supplied to a generative model. The embedding is not the retrieval system, and it is not the RAG system, but it is often the representation layer that makes both possible.</p><p>The same idea also appears in categorization. A support message embedding can feed a label classifier, or it can be compared against labeled exemplars to predict a category. In both cases, the text is first transformed into a vector that captures meaning at the document level rather than the token level. That broader representation is what allows systems to work on intent, theme, and similarity instead of exact word overlap.</p><h2>Word Embeddings and Word2vec Intuition</h2><h3>From Learned Vectors to Pretrained Word Embeddings</h3><p>Embeddings turn meaning into geometry. Words that behave similarly in text can end up near one another in a vector space, and that neighborhood structure becomes useful for search, clustering, retrieval, and language understanding. Here the focus is on pretrained word embeddings: standalone vectors learned ahead of time on large corpora and then reused in other systems.</p><p>That is different from the token embeddings inside a Transformer, which are part of a larger model and are learned as one component of that model. Pretrained word embeddings are reusable representations that can be inspected directly. They are not the final prediction for a task; they are coordinates in a learned space that can support similarity checks before any larger model enters the picture.</p><h3>Loading and Probing a Real Embedding Model</h3><p>A pretrained embedding model becomes concrete when you can load it and inspect it immediately. The following example uses Gensim&#8217;s model zoo to load a compact GloVe embedding set. This is not training; it is retrieval of a pretrained resource that already contains a vocabulary and a vector table.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;1397931c-3787-4bb0-9d86-de4b8ce40613&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">import gensim.downloader as api

# Download embeddings (66MB, glove, trained on wikipedia, vector size: 50)
# Other options include "word2vec-google-news-300"
# More options at https://github.com/RaRe-Technologies/gensim-data
model = api.load("glove-wiki-gigaword-50")</code></pre></div><p>Nearest-neighbor inspection gives a quick diagnostic view of the space. The query below asks which words are closest to <code>king</code> in the learned geometry.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;542ecbff-7e3f-4a01-8913-033b2186ea95&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">model.most_similar([model['king']], topn=11)</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;8ec0d0d4-74b2-4b89-b9d1-184d3ec34c9d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">[('king', 1.0000001192092896),
('prince', 0.8236179351806641),
('queen', 0.7839043140411377),
('ii', 0.7746230363845825),
('emperor', 0.7736247777938843),
('son', 0.766719400882721),
('uncle', 0.7627150416374207),
('kingdom', 0.7542161345481873),
('throne', 0.7539914846420288),
('brother', 0.7492411136627197),
('ruler', 0.7434253692626953)]</code></pre></div><p>The ranking is the useful part. It suggests that royal and family-related terms sit in the same neighborhood, which is evidence that the embedding space has learned structure. This is only an illustrative check, not a guarantee of perfect semantic quality, but it is a practical way to see whether the space behaves like a map rather than a random scatter of points.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!_YF1!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc8c7c23-ef0f-4fda-9268-bdeac1974e1f_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!_YF1!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc8c7c23-ef0f-4fda-9268-bdeac1974e1f_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!_YF1!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc8c7c23-ef0f-4fda-9268-bdeac1974e1f_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!_YF1!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc8c7c23-ef0f-4fda-9268-bdeac1974e1f_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!_YF1!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc8c7c23-ef0f-4fda-9268-bdeac1974e1f_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!_YF1!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc8c7c23-ef0f-4fda-9268-bdeac1974e1f_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/dc8c7c23-ef0f-4fda-9268-bdeac1974e1f_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:5027753,&quot;alt&quot;:&quot;Figure 2.9: Nearest-neighbor inspection of a pretrained embedding space: words related to royalty and family cluster near the query word 'king'.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204104981?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc8c7c23-ef0f-4fda-9268-bdeac1974e1f_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 2.9: Nearest-neighbor inspection of a pretrained embedding space: words related to royalty and family cluster near the query word 'king'." title="Figure 2.9: Nearest-neighbor inspection of a pretrained embedding space: words related to royalty and family cluster near the query word 'king'." srcset="https://substackcdn.com/image/fetch/$s_!_YF1!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc8c7c23-ef0f-4fda-9268-bdeac1974e1f_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!_YF1!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc8c7c23-ef0f-4fda-9268-bdeac1974e1f_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!_YF1!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc8c7c23-ef0f-4fda-9268-bdeac1974e1f_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!_YF1!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc8c7c23-ef0f-4fda-9268-bdeac1974e1f_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3>How Word2vec Turns Text into Training Pairs</h3><p>Word2vec is the classic training recipe that makes these neighborhoods learnable from raw text. Instead of starting with fixed vectors, it learns them from co-occurrence. A sliding window moves across a corpus and builds center-context pairs. If the sentence is &#8220;the refund was processed quickly,&#8221; then a window around <code>refund</code> might produce positive pairs such as <code>refund</code> with <code>processed</code> and <code>refund</code> with <code>quickly</code>, depending on the chosen window size.</p><p>This converts unlabeled text into supervision. Every local neighborhood in the corpus becomes a small training example that says which words appeared together. The model is not told that two words are synonyms. It only sees repeated patterns of nearby usage and learns from those patterns.</p><p>Word2vec is still static, though, and that matters. A single word can appear in many different contexts, but the method gives that word only one vector. As a result, polysemous words can blur together. <code>Charge</code> in a payments context and <code>charge</code> in a legal or battery context may be pulled into one representation, which is one reason contextual embeddings later became so important.</p><h3>Skip-Gram and Negative Sampling as a Learning Game</h3><p>The simplest word2vec story is skip-gram. In skip-gram, the center word predicts surrounding words. If <code>refund</code> is the center word, the model should score nearby words such as <code>payment</code>, <code>charge</code>, or <code>processed</code> highly when they truly appear in the context window. The direction matters: center word to context word, one pair at a time.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Ilqt!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F792b2d83-0b0d-4496-adee-a3ce40f122b3_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Ilqt!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F792b2d83-0b0d-4496-adee-a3ce40f122b3_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!Ilqt!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F792b2d83-0b0d-4496-adee-a3ce40f122b3_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!Ilqt!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F792b2d83-0b0d-4496-adee-a3ce40f122b3_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!Ilqt!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F792b2d83-0b0d-4496-adee-a3ce40f122b3_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Ilqt!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F792b2d83-0b0d-4496-adee-a3ce40f122b3_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/792b2d83-0b0d-4496-adee-a3ce40f122b3_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:5233571,&quot;alt&quot;:&quot;Figure 2.10: Skip-gram turns a sentence into center-context training pairs, while negative sampling adds impostor pairs to make the learned space discriminative.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204104981?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F792b2d83-0b0d-4496-adee-a3ce40f122b3_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 2.10: Skip-gram turns a sentence into center-context training pairs, while negative sampling adds impostor pairs to make the learned space discriminative." title="Figure 2.10: Skip-gram turns a sentence into center-context training pairs, while negative sampling adds impostor pairs to make the learned space discriminative." srcset="https://substackcdn.com/image/fetch/$s_!Ilqt!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F792b2d83-0b0d-4496-adee-a3ce40f122b3_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!Ilqt!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F792b2d83-0b0d-4496-adee-a3ce40f122b3_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!Ilqt!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F792b2d83-0b0d-4496-adee-a3ce40f122b3_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!Ilqt!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F792b2d83-0b0d-4496-adee-a3ce40f122b3_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Negative sampling gives the model its contrast. It is easiest to think of this as a spot-the-impostor game. For each real center-context pair, the model also sees random mismatches, such as <code>refund</code> with <code>saxophone</code> or <code>delay</code> with <code>galaxy</code>. The training goal is to score the true pair higher than the fake ones. That extra contrast is important because rewarding only positive pairs would not teach the model what to separate, and the space could become uninformative.</p><p>So negative samples do more than add noise. They force the embedding space to learn a boundary between related and unrelated words. This positive-versus-negative pattern is the same broad recipe that later appears in contrastive learning.</p><h3>Why Embedding Neighborhoods Are Useful but Imperfect</h3><p>Nearest neighbors are a useful diagnostic, but they are not a full evaluation. When the neighbors look sensible, the space is capturing some structure of usage. When they look odd, the model may be reflecting corpus bias, frequency effects, or surface-form artifacts. A nearest-neighbor list tells you that the space has organization, not that every top match is a perfect semantic partner.</p><p>Static word embeddings also compress multiple senses into one vector, so a neighborhood can mix related meanings instead of separating them cleanly. That limitation is one reason later contextual models matter. The practical lesson is simple: good neighbors are helpful, but usefulness is judged by downstream fit, not by proximity alone.</p><h3>The Broader Pattern: Pairwise Similarity as a Reusable Idea</h3><p>The deeper lesson of word2vec is not limited to words. It is a pairwise learning pattern: bring positives together and push negatives apart. That recipe reappears in retrieval systems, recommendation pipelines, and multimodal matching, where the items may be products, songs, images, or captions instead of words.</p><p>That is why this section matters as a bridge. Word2vec shows how co-occurrence can be turned into geometry, how negative sampling makes the geometry discriminative, and how the same training intuition later becomes a general contrastive strategy across modern machine learning.</p><h2>Embedding-Based Recommendation with Playlists</h2><h3>Why Playlists Behave Like Sentences</h3><p>Embeddings are not limited to language. Any domain that produces repeated local co-occurrence can support the same learning pattern. Playlists are a clean example because songs that appear near each other often share context such as mood, genre, artist, or listening habit. If you treat each playlist as a sentence and each song as a token, then the training signal becomes familiar: items that repeatedly occur in similar neighborhoods should end up close in vector space.</p><p>That analogy matters at the level of training, not just intuition. The model is not learning music theory or human intent. It is learning from ordered sequences, so the song IDs in a playlist function as the internal tokens that carry the co-occurrence signal. Titles and artists are not part of training; they are only the labels we later use to present the result. This is why the same embedding idea can also work for a ReviewDesk-style workflow where similar tickets appear together in a support stream.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!gfdA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4fa55212-d512-41c4-a3cd-e2185365db00_2752x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!gfdA!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4fa55212-d512-41c4-a3cd-e2185365db00_2752x1536.png 424w, https://substackcdn.com/image/fetch/$s_!gfdA!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4fa55212-d512-41c4-a3cd-e2185365db00_2752x1536.png 848w, https://substackcdn.com/image/fetch/$s_!gfdA!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4fa55212-d512-41c4-a3cd-e2185365db00_2752x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!gfdA!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4fa55212-d512-41c4-a3cd-e2185365db00_2752x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!gfdA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4fa55212-d512-41c4-a3cd-e2185365db00_2752x1536.png" width="1456" height="813" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4fa55212-d512-41c4-a3cd-e2185365db00_2752x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:813,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:5206101,&quot;alt&quot;:&quot;Figure 2.11: Playlists are treated as ordered token sequences, allowing Word2Vec to learn song embeddings from local co-occurrence rather than from titles or artist names.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204104981?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4fa55212-d512-41c4-a3cd-e2185365db00_2752x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 2.11: Playlists are treated as ordered token sequences, allowing Word2Vec to learn song embeddings from local co-occurrence rather than from titles or artist names." title="Figure 2.11: Playlists are treated as ordered token sequences, allowing Word2Vec to learn song embeddings from local co-occurrence rather than from titles or artist names." srcset="https://substackcdn.com/image/fetch/$s_!gfdA!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4fa55212-d512-41c4-a3cd-e2185365db00_2752x1536.png 424w, https://substackcdn.com/image/fetch/$s_!gfdA!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4fa55212-d512-41c4-a3cd-e2185365db00_2752x1536.png 848w, https://substackcdn.com/image/fetch/$s_!gfdA!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4fa55212-d512-41c4-a3cd-e2185365db00_2752x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!gfdA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4fa55212-d512-41c4-a3cd-e2185365db00_2752x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3>Preparing Playlist and Song Metadata Data</h3><p>The first step is to load the playlist corpus and the song metadata table. The playlists are parsed into ordered lists of song IDs, because Word2Vec needs sequence data rather than isolated rows. Playlists with a single song are removed because they do not provide any surrounding context, so they contribute no useful co-occurrence information. That filtering keeps the input format aligned with the sentence analogy: a one-word sentence does not teach the model anything about neighborhood relationships.</p><p>The metadata table plays a separate but essential role. It maps each internal song ID to a human-readable title and artist. Without that lookup layer, the embedding model would only return anonymous IDs, which are useful to the algorithm but not to a person reading the output. In practice, the vector model and the metadata table work together: one learns similarity, and the other makes the similarity interpretable.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;ad0d8e87-9fcb-4b27-b645-d6bc8872a385&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">import pandas as pd
from urllib import request

# Get the playlist dataset file
data = request.urlopen('https://storage.googleapis.com/maps-premium/dataset/yes_complete/train.txt')

# Parse the playlist dataset file. Skip the first two lines as
# they only contain metadata
lines = data.read().decode("utf-8").split('\n')[2:]

# Remove playlists with only one song
playlists = [s.rstrip().split() for s in lines if len(s.split()) &gt; 1]

# Load song metadata
songs_file = request.urlopen('https://storage.googleapis.com/maps-premium/dataset/yes_complete/song_hash.txt')
songs_file = songs_file.read().decode("utf-8").split('\n')
songs = [s.rstrip().split('\t') for s in songs_file]
songs_df = pd.DataFrame(data=songs, columns = ['id', 'title', 'artist'])
songs_df = songs_df.set_index('id')</code></pre></div><h3>Inspecting Sequences Before Training</h3><p>A quick preview helps confirm that the preprocessing worked as intended. Each playlist should look like an ordered sequence of song IDs, not a flattened list of metadata fields. This small check is useful because the model will happily train on whatever structure it is given, even if the structure is wrong.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;57a5d7f8-ed50-484c-becd-412266057200&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">print( 'Playlist #1:\n ', playlists[0], '\n')
print( 'Playlist #2:\n ', playlists[1])</code></pre></div><h3>Learning Song Vectors with Word2Vec</h3><p>Word2Vec fits this problem because it learns vectors from co-occurrence, not from explicit labels. In this setting, the model scans each playlist and learns which song IDs tend to appear in similar local contexts. The window size controls how far the model looks around each song in the sequence, so it directly expresses the idea of local playlist context. A larger window allows broader neighborhood signals, while a smaller window focuses on tighter transitions between adjacent songs.</p><p>The other settings are practical choices. The vector size sets how large each song representation will be. Negative sampling makes training efficient by contrasting true neighbors with distractors. The minimum count keeps rare items from dominating the embedding space, and the worker count uses parallelism to speed up training. Together, these settings define a reusable item embedding space that captures proximity from co-occurrence rather than from manual labels.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!eV_Q!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04575cef-05e4-4228-9555-5f1d23af3a3f_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!eV_Q!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04575cef-05e4-4228-9555-5f1d23af3a3f_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!eV_Q!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04575cef-05e4-4228-9555-5f1d23af3a3f_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!eV_Q!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04575cef-05e4-4228-9555-5f1d23af3a3f_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!eV_Q!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04575cef-05e4-4228-9555-5f1d23af3a3f_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!eV_Q!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04575cef-05e4-4228-9555-5f1d23af3a3f_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/04575cef-05e4-4228-9555-5f1d23af3a3f_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:5989243,&quot;alt&quot;:&quot;Figure 2.12: Word2Vec hyperparameters control the size, context range, sampling strategy, filtering, and training parallelism used to learn song embeddings from playlists.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204104981?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04575cef-05e4-4228-9555-5f1d23af3a3f_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 2.12: Word2Vec hyperparameters control the size, context range, sampling strategy, filtering, and training parallelism used to learn song embeddings from playlists." title="Figure 2.12: Word2Vec hyperparameters control the size, context range, sampling strategy, filtering, and training parallelism used to learn song embeddings from playlists." srcset="https://substackcdn.com/image/fetch/$s_!eV_Q!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04575cef-05e4-4228-9555-5f1d23af3a3f_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!eV_Q!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04575cef-05e4-4228-9555-5f1d23af3a3f_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!eV_Q!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04575cef-05e4-4228-9555-5f1d23af3a3f_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!eV_Q!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04575cef-05e4-4228-9555-5f1d23af3a3f_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;1727e669-8b1f-4375-858c-46055f90e804&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from gensim.models import Word2Vec

# Train our Word2Vec model
model = Word2Vec(
    playlists, vector_size=32, window=20, negative=50, min_count=1, workers=4
)</code></pre></div><h3>From Similar Songs to Usable Recommendations</h3><p>After training, the workflow becomes retrieval over the embedding space. Given a song ID, the model searches for nearby song vectors and returns the closest neighbors. That is the key distinction to keep in mind: this is an embedding retrieval workflow, not generation. The system is not inventing new content token by token; it is selecting items that are already present in the learned space.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;d5bd29fa-e5b5-4684-a7e5-66b98b37bfd8&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">song_id = 2172

# Ask the model for songs similar to song #2172
model.wv.most_similar(positive=str(song_id))</code></pre></div><p>The raw neighbor list still needs interpretation, so the metadata table is used to map the song ID back to a title and artist. That lookup turns a vector-space result into something a user can read. The model stores only internal identifiers, but the display layer translates those identifiers into a product-facing recommendation.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;468abaf2-596a-4996-90ee-9e18cb6b7483&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">print(songs_df.iloc[2172])</code></pre></div><p>A small helper function makes that pattern reusable. It asks for the top neighbors of a song ID, extracts their IDs, and returns the matching metadata rows as a table of recommendations. This is the simplest form of an application-ready interface built on embeddings: retrieve nearest neighbors first, then resolve them through metadata.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;c16dd9c8-7ff2-4da7-b547-5509e6862209&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">import numpy as np

def print_recommendations(song_id):
    similar_songs = np.array(
        model.wv.most_similar(positive=str(song_id),topn=5)
    )[:,0]
    return  songs_df.iloc[similar_songs]

# Extract recommendations
print_recommendations(2172)</code></pre></div><p>The output is only as trustworthy as the data and training setup behind it. Neighbors can look visually plausible because they share genre or playlist context, but that does not by itself prove they are validated recommendations. Results can also change with different random initialization, different corpus slices, or different preprocessing decisions, so the sample output should be read as an illustration of the retrieval pattern rather than a fixed guarantee.</p><h3>What This Example Does and Does Not Prove</h3><p>This example shows how an embedding model can turn co-occurrence sequences into a similarity structure that supports recommendation retrieval. The same pattern can be applied to songs, products, tickets, or other items that appear in ordered contexts. What it does not show is text generation or a full recommender system. It shows embedding-based retrieval: learn vectors from local context, then use nearest neighbors and metadata lookup to make the results usable.</p><h2>Chapter Recap and Bridge to Transformers</h2><p>This chapter has built a layered map from raw text to useful representations. The sequence begins with text, which is broken into tokens by a tokenizer. Those tokens are converted into token IDs, which are the numeric codes a model can actually process. Inside the model, each token ID is mapped to a token embedding, a learned vector from the model&#8217;s internal lookup table. After that, the surrounding words reshape those vectors into contextual token vectors, so the representation of a token changes with the sentence around it. At the other end of the spectrum are text embeddings, which compress an entire sentence, paragraph, or document into a single fixed-size vector that can be compared with other texts.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Xev2!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4fa89151-197d-4cf0-b7eb-15677e11da9e_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Xev2!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4fa89151-197d-4cf0-b7eb-15677e11da9e_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!Xev2!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4fa89151-197d-4cf0-b7eb-15677e11da9e_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!Xev2!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4fa89151-197d-4cf0-b7eb-15677e11da9e_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!Xev2!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4fa89151-197d-4cf0-b7eb-15677e11da9e_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Xev2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4fa89151-197d-4cf0-b7eb-15677e11da9e_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4fa89151-197d-4cf0-b7eb-15677e11da9e_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:4672523,&quot;alt&quot;:&quot;Figure 2.13: The chapter&#8217;s representation stack: raw text becomes tokens, token IDs, token embeddings, contextual token vectors, and, separately, a single text embedding for whole-input similarity tasks.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204104981?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4fa89151-197d-4cf0-b7eb-15677e11da9e_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 2.13: The chapter&#8217;s representation stack: raw text becomes tokens, token IDs, token embeddings, contextual token vectors, and, separately, a single text embedding for whole-input similarity tasks." title="Figure 2.13: The chapter&#8217;s representation stack: raw text becomes tokens, token IDs, token embeddings, contextual token vectors, and, separately, a single text embedding for whole-input similarity tasks." srcset="https://substackcdn.com/image/fetch/$s_!Xev2!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4fa89151-197d-4cf0-b7eb-15677e11da9e_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!Xev2!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4fa89151-197d-4cf0-b7eb-15677e11da9e_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!Xev2!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4fa89151-197d-4cf0-b7eb-15677e11da9e_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!Xev2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4fa89151-197d-4cf0-b7eb-15677e11da9e_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>That hierarchy matters because each layer serves a different purpose. Token embeddings are part of the model&#8217;s machinery; they help the model turn symbolic input into something it can compute with. Text embeddings are standalone representations; they are designed for similarity, search, clustering, and classification pipelines that need a reusable vector for the whole input. It is easy to blur those ideas, especially because both are called embeddings, but they live at different levels of the stack. A token embedding is like a row in the model&#8217;s internal codebook, while a text embedding is more like a compact map of the meaning of the entire passage.</p><p>The choice of tokenizer shapes that whole pipeline. Word, subword, character, and byte strategies each make different tradeoffs in vocabulary size, robustness to unseen text, and how often a word gets split into pieces. Special tokens, capitalization rules, and the data used to train the tokenizer also affect what the model &#8220;sees&#8221; before learning begins. In practice, a tokenizer is part of the model&#8217;s input dialect: change the dialect, and the same sentence may become longer, shorter, cheaper, or harder for the model to interpret. That is why tokenizer behavior is tied so tightly to particular pretrained checkpoints.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!7eoz!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66d5460f-309b-4620-8ae6-d6b4ecccca47_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!7eoz!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66d5460f-309b-4620-8ae6-d6b4ecccca47_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!7eoz!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66d5460f-309b-4620-8ae6-d6b4ecccca47_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!7eoz!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66d5460f-309b-4620-8ae6-d6b4ecccca47_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!7eoz!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66d5460f-309b-4620-8ae6-d6b4ecccca47_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!7eoz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66d5460f-309b-4620-8ae6-d6b4ecccca47_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/66d5460f-309b-4620-8ae6-d6b4ecccca47_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:6264238,&quot;alt&quot;:&quot;Figure 2.14: Different tokenization strategies trade off vocabulary size, sequence length, robustness, and checkpoint compatibility; the tokenizer effectively defines the model&#8217;s input dialect.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204104981?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66d5460f-309b-4620-8ae6-d6b4ecccca47_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 2.14: Different tokenization strategies trade off vocabulary size, sequence length, robustness, and checkpoint compatibility; the tokenizer effectively defines the model&#8217;s input dialect." title="Figure 2.14: Different tokenization strategies trade off vocabulary size, sequence length, robustness, and checkpoint compatibility; the tokenizer effectively defines the model&#8217;s input dialect." srcset="https://substackcdn.com/image/fetch/$s_!7eoz!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66d5460f-309b-4620-8ae6-d6b4ecccca47_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!7eoz!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66d5460f-309b-4620-8ae6-d6b4ecccca47_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!7eoz!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66d5460f-309b-4620-8ae6-d6b4ecccca47_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!7eoz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66d5460f-309b-4620-8ae6-d6b4ecccca47_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The chapter also placed modern embeddings in historical context. Word2vec introduced the idea that distributional context can produce useful vector spaces, and its skip-gram and negative sampling setup helped make that idea practical at scale. Even though contextual models have largely replaced static word vectors for many tasks, the core intuition remains valuable: items that appear in similar neighborhoods can be placed near one another in vector space. That is the same basic geometry behind recommendation systems, including the playlist-style example from this chapter. Once item embeddings are learned, similar items can be surfaced by nearest-neighbor search, turning abstract vectors into practical suggestions.</p><p>The main lesson is that embeddings are not just a language-model detail. They are a general-purpose representation tool that supports retrieval, recommendation, classification, and other workflows where similarity matters. The next chapter explains the engine behind the most important distinction in this chapter: why token vectors become contextual in the first place. Transformers show how sequence models turn token streams into context-aware representations and then use those representations to generate text autoregressively, which is the missing mechanism behind everything you have just learned.</p><p><em>Use the url below to download the enitre book as pdf:</em></p>
      <p>
          <a href="https://onepagecode.substack.com/p/tokens-and-embeddings">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[An Introduction to Large Language Models]]></title><description><![CDATA[Chapter 1 of Hands-On Large Language Models: What Language AI and LLMs cover, the transition from bag-of-words to dense embeddings, and the Transformer architecture]]></description><link>https://onepagecode.substack.com/p/an-introduction-to-large-language</link><guid isPermaLink="false">https://onepagecode.substack.com/p/an-introduction-to-large-language</guid><dc:creator><![CDATA[Onepagecode]]></dc:creator><pubDate>Sat, 04 Jul 2026 19:40:09 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!8pgf!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d5bf83f-c510-43cb-9513-b2295f368cd6_2816x1536.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>What Language AI and LLMs Cover</h2><p>Language AI is the part of AI concerned with understanding, processing, and generating human language. That sounds broad because it is broad: it includes systems that classify messages, extract facts, summarize documents, translate text, search across content, and generate new responses. A large language model, or LLM, is one especially capable model class inside that space. In practical terms, it is a pretrained language model that has learned patterns from large collections of text and can then be used to predict and generate language in ways that are useful for real applications.</p><p>It helps to separate the model from the product built around it. The model is the language engine; the product is the vehicle. A chatbot is only one possible vehicle, and often not the most important one. The same underlying model might sit behind a support triage tool, a document summarizer, a drafting assistant, or a search experience that helps people find the right answer faster. In other words, LLM value comes from how well the model fits a task inside a workflow, not from the fact that it can hold a conversation.</p><h2><strong>&#128216; Buy the Entire Book &#8211; Available on Amazon Worldwide (If you are paid subscriber, use the url at the end of this article to download the book for free!)</strong></h2><p>You can now purchase the complete book directly from Amazon in your country:</p><ul><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/us/dualbookshelf.marketplacelink/B0H6WZJ57S">United States (US)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/uk/dualbookshelf.marketplacelink/B0H6WZJ57S">United Kingdom (UK)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/de/dualbookshelf.marketplacelink/B0H6WZJ57S">Germany (DE)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/fr/dualbookshelf.marketplacelink/B0H6WZJ57S">France (FR)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/es/dualbookshelf.marketplacelink/B0H6WZJ57S">Spain (ES)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/it/dualbookshelf.marketplacelink/B0H6WZJ57S">Italy (IT)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/nl/dualbookshelf.marketplacelink/B0H6WZJ57S">Netherlands (NL)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/jp/dualbookshelf.marketplacelink/B0H6WZJ57S">Japan (JP)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/br/dualbookshelf.marketplacelink/B0H6WZJ57S">Brazil (BR):</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/ca/dualbookshelf.marketplacelink/B0H6WZJ57S">Canada (CA)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/mx/dualbookshelf.marketplacelink/B0H6WZJ57S">Mexico (MX)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/au/dualbookshelf.marketplacelink/B0H6WZJ57S">Australia (AU)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/in/dualbookshelf.marketplacelink/B0H6WZJ57S">India (IN)</a></strong></p></li></ul><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!8pgf!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d5bf83f-c510-43cb-9513-b2295f368cd6_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!8pgf!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d5bf83f-c510-43cb-9513-b2295f368cd6_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!8pgf!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d5bf83f-c510-43cb-9513-b2295f368cd6_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!8pgf!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d5bf83f-c510-43cb-9513-b2295f368cd6_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!8pgf!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d5bf83f-c510-43cb-9513-b2295f368cd6_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!8pgf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d5bf83f-c510-43cb-9513-b2295f368cd6_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9d5bf83f-c510-43cb-9513-b2295f368cd6_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:5585423,&quot;alt&quot;:&quot;Figure 1.1: A language model is the engine; products such as chatbots, triage tools, and summarizers are different vehicles built around it.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204104616?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d5bf83f-c510-43cb-9513-b2295f368cd6_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 1.1: A language model is the engine; products such as chatbots, triage tools, and summarizers are different vehicles built around it." title="Figure 1.1: A language model is the engine; products such as chatbots, triage tools, and summarizers are different vehicles built around it." srcset="https://substackcdn.com/image/fetch/$s_!8pgf!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d5bf83f-c510-43cb-9513-b2295f368cd6_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!8pgf!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d5bf83f-c510-43cb-9513-b2295f368cd6_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!8pgf!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d5bf83f-c510-43cb-9513-b2295f368cd6_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!8pgf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d5bf83f-c510-43cb-9513-b2295f368cd6_2816x1536.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>That distinction matters because Language AI is larger than generation alone. A system may use language understanding to route a ticket, retrieval to find supporting context, and generation to draft a reply. Each component contributes a different kind of value. In ReviewDesk, for example, an incoming support message might first be classified as billing, bug report, or account access. A retrieval step could pull similar past cases or policy notes. Only then would an LLM draft a response for an agent to review. The workflow is the product; the LLM is one powerful step inside it.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!mFcS!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F94a57b43-c7e3-468e-9cb8-7aaac0345ef0_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!mFcS!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F94a57b43-c7e3-468e-9cb8-7aaac0345ef0_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!mFcS!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F94a57b43-c7e3-468e-9cb8-7aaac0345ef0_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!mFcS!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F94a57b43-c7e3-468e-9cb8-7aaac0345ef0_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!mFcS!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F94a57b43-c7e3-468e-9cb8-7aaac0345ef0_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!mFcS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F94a57b43-c7e3-468e-9cb8-7aaac0345ef0_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/94a57b43-c7e3-468e-9cb8-7aaac0345ef0_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:5781631,&quot;alt&quot;:&quot;Figure 1.2: In a real application, language understanding, retrieval, and generation combine in a workflow, with the LLM occupying just one step.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204104616?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F94a57b43-c7e3-468e-9cb8-7aaac0345ef0_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 1.2: In a real application, language understanding, retrieval, and generation combine in a workflow, with the LLM occupying just one step." title="Figure 1.2: In a real application, language understanding, retrieval, and generation combine in a workflow, with the LLM occupying just one step." srcset="https://substackcdn.com/image/fetch/$s_!mFcS!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F94a57b43-c7e3-468e-9cb8-7aaac0345ef0_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!mFcS!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F94a57b43-c7e3-468e-9cb8-7aaac0345ef0_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!mFcS!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F94a57b43-c7e3-468e-9cb8-7aaac0345ef0_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!mFcS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F94a57b43-c7e3-468e-9cb8-7aaac0345ef0_2816x1536.png 1456w" sizes="100vw"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>This broader view prevents a common mistake: assuming that a strong chat experience means the model is universally best at every language task. A polished answer in a conversation interface does not automatically make the same model ideal for extraction, routing, summarization, or search support. Language AI is a toolbox, not a single tool, and later chapters will show how embeddings, retrieval, prompting, and tuning combine with LLMs to build systems that are more reliable and more useful than chat alone.</p><h2>Why text needed a numeric representation</h2><p>Before a model can do anything useful with language, the language has to become numbers. That sounds mechanical, but it is one of the most important design choices in Language AI, because the representation determines what the model can easily notice and what it is likely to miss. A system that sees text as a simple inventory of words will behave very differently from one that sees text as a dense semantic map or a context-sensitive sequence.</p><p>Think about the ReviewDesk ticket, &#8220;The app crashes after the latest update.&#8221; If a support team wants to route that message, compare it with past incidents, or draft a response, the system must first decide what kind of information to preserve. Early methods preserved frequency. Later methods preserved meaning. Modern methods preserve meaning in context. The history of Language AI is, in large part, the history of making that representation more informative without making it unusably complex.</p><h2>Bag-of-words and sparse counts</h2><p>Bag-of-words was the workhorse starting point for many early text systems. The idea is straightforward: build a vocabulary from the corpus, assign each vocabulary item a position, and represent a document by counting how often each token appears. If a sentence contains the tokens &#8220;crash,&#8221; &#8220;update,&#8221; and &#8220;app,&#8221; those positions get incremented in the vector. Everything else remains zero. The resulting representation is sparse because most vocabulary entries do not appear in any given document.</p><p>That sparsity was both the strength and the weakness of the approach. It made text usable in ordinary machine-learning pipelines, especially for classification and search, because a sentence could be turned into a fixed-length numeric vector. For tasks such as spam filtering or early ticket triage, raw counts were often enough to give a useful signal. In ReviewDesk, a bag-of-words vector can tell you that a ticket mentions &#8220;refund,&#8221; &#8220;broken,&#8221; or &#8220;urgent,&#8221; which is already enough to separate some categories.</p><p>The limitation is just as important as the mechanism. Bag-of-words ignores word order, phrase structure, and context. &#8220;The app crashed after the update&#8221; and &#8220;The update crashed the app&#8221; look very similar, even though the emphasis is different. &#8220;Not happy with the refund&#8221; and &#8220;happy with the refund&#8221; can end up unnervingly close if the representation is too literal. Bag-of-words is a useful inventory, but it is not a model of meaning. It records what appeared, not how the pieces of language fit together.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!jaxj!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e9a1f28-7944-4d93-bebb-e76e38bf8b26_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!jaxj!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e9a1f28-7944-4d93-bebb-e76e38bf8b26_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!jaxj!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e9a1f28-7944-4d93-bebb-e76e38bf8b26_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!jaxj!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e9a1f28-7944-4d93-bebb-e76e38bf8b26_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!jaxj!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e9a1f28-7944-4d93-bebb-e76e38bf8b26_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!jaxj!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e9a1f28-7944-4d93-bebb-e76e38bf8b26_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2e9a1f28-7944-4d93-bebb-e76e38bf8b26_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:6007689,&quot;alt&quot;:&quot;Figure 1.3: Bag-of-words turns text into a sparse count vector, which preserves token presence but discards order, structure, and most contextual meaning.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204104616?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e9a1f28-7944-4d93-bebb-e76e38bf8b26_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 1.3: Bag-of-words turns text into a sparse count vector, which preserves token presence but discards order, structure, and most contextual meaning." title="Figure 1.3: Bag-of-words turns text into a sparse count vector, which preserves token presence but discards order, structure, and most contextual meaning." srcset="https://substackcdn.com/image/fetch/$s_!jaxj!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e9a1f28-7944-4d93-bebb-e76e38bf8b26_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!jaxj!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e9a1f28-7944-4d93-bebb-e76e38bf8b26_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!jaxj!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e9a1f28-7944-4d93-bebb-e76e38bf8b26_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!jaxj!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e9a1f28-7944-4d93-bebb-e76e38bf8b26_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2>Dense embeddings and the promise of semantic proximity</h2><p>The next major step was to replace huge sparse count vectors with compact dense vectors. An embedding is a learned vector representation in which related words or texts end up near one another in vector space. Instead of storing a long list of mostly zeros, the model stores a shorter vector whose coordinates are not human-interpretable counts but latent features learned from data.</p><p>Word2vec made this idea especially influential. At a high level, it learned word representations from surrounding words, so the model could infer that words appearing in similar contexts should be placed near each other. This was a conceptual breakthrough because language itself became part of the supervision signal. You did not need labeled sentiment or topic data to start learning useful structure; the distribution of words in text already carried information.</p><p>That is why vector closeness became so valuable. If two words or phrases land near each other in embedding space, the model can treat them as semantically related even when they do not share the same surface form. &#8220;Crash,&#8221; &#8220;failure,&#8221; and &#8220;error&#8221; can occupy neighboring regions, which helps with search, clustering, similarity, and downstream classification. In ReviewDesk, embeddings let the system connect &#8220;can&#8217;t log in after update&#8221; with &#8220;sign-in stopped working,&#8221; even though the wording differs.</p><p>A caution is worth stating plainly: semantic proximity is not the same as reasoning, truth, or understanding. Embeddings are excellent at organizing similarity, but they do not decide whether a complaint is valid, whether a claim is false, or whether a response is appropriate. They build a useful map, not a judge.</p><h2>Why static word vectors hit a ceiling</h2><p>Static embeddings solved a real problem, but they introduced another. A word-vector model assigns one representation per word, regardless of where that word appears. That works well until context changes the meaning. The word &#8220;bank&#8221; can refer to a financial institution or the side of a river. A single static vector has to compromise between those meanings, which means it can never be fully right in either case.</p><p>The same problem appears in everyday product language. In a support message, &#8220;crash&#8221; may describe software failure, while in another context it may describe a car accident or a temporary collapse in service. The surrounding words carry the disambiguating signal, but a static embedding does not adapt itself to the sentence in front of it. It knows the word, not the situation.</p><p>This ceiling is the reason context-aware representations became the next frontier. The key idea was no longer just to learn a better word vector, but to let a model build a different representation depending on neighboring words, sentence structure, and position in the sequence. That shift changed text modeling from lookup to interpretation.</p><h2>Attention changes the unit of modeling</h2><p>Attention made that shift possible. Conceptually, attention lets a model decide which parts of a sequence matter most for the current token or prediction. Rather than compressing a sentence into a single fixed summary too early, the model can look back across the sequence and selectively emphasize the informative pieces. A selective spotlight is a better metaphor than a black box: for each word or prediction, the model can illuminate some context and dim the rest.</p><p>This was especially powerful because it reduced the burden on older sequence architectures that had to carry information step by step. Recurrent models processed text sequentially, which made long-range dependencies harder to preserve and slowed training because the sequence could not be fully parallelized. Attention changed the unit of modeling from &#8220;what did I remember from the previous step?&#8221; to &#8220;what in the whole sequence is relevant right now?&#8221;</p><p>The Transformer combined attention with a different processing strategy and removed recurrence as the central mechanism. That mattered for two practical reasons. First, training became much more parallelizable, which made large-scale learning far more efficient. Second, the model became better at handling long-range relationships in text, because it could connect distant parts of a sentence without forcing information through a long chain of intermediate states. In the language of this book, the Transformer is the architectural turning point that made modern LLMs feasible.</p><h2>Encoder-only and decoder-only Transformers as two paths</h2><p>Once the Transformer arrived, the field split into two especially important families. BERT is the classic encoder-only path. It reads the whole input and builds contextual representations for the sequence, which makes it especially strong as a representation model. Its training objective, masked language modeling, hides selected tokens and asks the model to predict them from surrounding context. That objective trains the model to understand language structure and context rather than to produce open-ended continuation.</p><p>GPT-1 represents the decoder-only path. Instead of learning to fill in missing words in both directions, it learns to predict the next token from what has already appeared. That autoregressive objective makes generation natural: the model becomes good at continuing text one token at a time. In practice, that means GPT-style models are built for completion, drafting, and free-form generation, while BERT-style models are built for contextual understanding and downstream tasks such as classification.</p><p>The contrast is easier to remember if you tie it to usage. BERT often produces a sequence-level representation that can be read from a special pooled output such as the [CLS] token, making it convenient for labeling a whole text. GPT-1, by design, is less about producing a single summary vector and more about generating the next segment of text. One model is optimized to interpret a sentence in context; the other is optimized to continue it.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!fwwg!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5660e16b-f7cb-4d7b-9116-377f6d26f4c5_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!fwwg!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5660e16b-f7cb-4d7b-9116-377f6d26f4c5_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!fwwg!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5660e16b-f7cb-4d7b-9116-377f6d26f4c5_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!fwwg!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5660e16b-f7cb-4d7b-9116-377f6d26f4c5_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!fwwg!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5660e16b-f7cb-4d7b-9116-377f6d26f4c5_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!fwwg!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5660e16b-f7cb-4d7b-9116-377f6d26f4c5_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5660e16b-f7cb-4d7b-9116-377f6d26f4c5_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:6250796,&quot;alt&quot;:&quot;Figure 1.4: BERT-style encoder Transformers build contextual representations for understanding tasks, while GPT-style decoder Transformers predict the next token for text generation.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204104616?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5660e16b-f7cb-4d7b-9116-377f6d26f4c5_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 1.4: BERT-style encoder Transformers build contextual representations for understanding tasks, while GPT-style decoder Transformers predict the next token for text generation." title="Figure 1.4: BERT-style encoder Transformers build contextual representations for understanding tasks, while GPT-style decoder Transformers predict the next token for text generation." srcset="https://substackcdn.com/image/fetch/$s_!fwwg!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5660e16b-f7cb-4d7b-9116-377f6d26f4c5_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!fwwg!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5660e16b-f7cb-4d7b-9116-377f6d26f4c5_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!fwwg!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5660e16b-f7cb-4d7b-9116-377f6d26f4c5_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!fwwg!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5660e16b-f7cb-4d7b-9116-377f6d26f4c5_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>That distinction is important because it prevents a common simplification: not every Transformer is trying to do the same thing. The architecture is shared, but the training objective and the downstream role differ. A representation model and a generator can both be &#8220;LLM-adjacent&#8221; in the way they support language work, but they are not interchangeable.</p><h2>The book&#8217;s broader meaning of LLM</h2><p>In this book, large language model is used as a pragmatic umbrella term, not a narrow label reserved only for giant chat-style generators. The scope includes models that generate text, models that produce representations, and models that support language applications even when they are not themselves chatbots. That choice is deliberate. It keeps the discussion aligned with how real systems are built, where retrieval, classification, embeddings, and generation are often assembled together.</p><p>This broader definition also avoids a naming trap. If LLM meant only &#8220;very large decoder-only chatbot model,&#8221; then a powerful text encoder or embedding model would fall outside the story even though it might be the most important part of a search system or a triage workflow. ReviewDesk does not need every component to be a chat model. It needs the right component for each job, whether that is sparse counts, dense embeddings, contextual encodings, or text generation.</p><p>That is why the earlier stages of this history still matter. Bag-of-words is not dead, embeddings are not obsolete, and representation models remain essential in modern Language AI. The rest of the book builds on that stack rather than replacing it with a single model family.</p><h2>Why LLM Training Is Not Just &#8220;Train a Model Once&#8221;</h2><p>Classic supervised machine learning often follows a one-step pattern: gather labeled examples for a task, train a model for that task, and then use it the same way in production. A sentiment classifier is trained to predict sentiment. A spam filter is trained to predict spam. The training procedure is tightly coupled to the final job, so the model is usually created with one narrow use case in mind.</p><p>Large language models work differently. Their lifecycle is staged. First comes pretraining, where a model learns from very large text corpora and absorbs broad patterns of language, style, facts, and continuation behavior. Only after that does the model get adapted toward a narrower outcome. That later adaptation can happen through fine-tuning, but it can also happen through prompts, retrieval, or a combination of the two. In other words, the system is often assembled after the base model already exists.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Mq9W!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa852d93e-e024-4859-876d-29b7911b9131_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Mq9W!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa852d93e-e024-4859-876d-29b7911b9131_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!Mq9W!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa852d93e-e024-4859-876d-29b7911b9131_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!Mq9W!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa852d93e-e024-4859-876d-29b7911b9131_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!Mq9W!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa852d93e-e024-4859-876d-29b7911b9131_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Mq9W!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa852d93e-e024-4859-876d-29b7911b9131_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a852d93e-e024-4859-876d-29b7911b9131_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:5123710,&quot;alt&quot;:&quot;Figure 1.5: Figure: Unlike classic one-step supervised training, LLMs are typically built in stages: broad pretraining creates a reusable base model, which is then adapted through fine-tuning, prompting, retrieval, or combinations of these methods.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204104616?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa852d93e-e024-4859-876d-29b7911b9131_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 1.5: Figure: Unlike classic one-step supervised training, LLMs are typically built in stages: broad pretraining creates a reusable base model, which is then adapted through fine-tuning, prompting, retrieval, or combinations of these methods." title="Figure 1.5: Figure: Unlike classic one-step supervised training, LLMs are typically built in stages: broad pretraining creates a reusable base model, which is then adapted through fine-tuning, prompting, retrieval, or combinations of these methods." srcset="https://substackcdn.com/image/fetch/$s_!Mq9W!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa852d93e-e024-4859-876d-29b7911b9131_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!Mq9W!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa852d93e-e024-4859-876d-29b7911b9131_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!Mq9W!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa852d93e-e024-4859-876d-29b7911b9131_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!Mq9W!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa852d93e-e024-4859-876d-29b7911b9131_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>That shift matters because it changes the engineering question. Instead of asking only how to train a model, you also ask how to reuse a pretrained model effectively. For a support workflow like ReviewDesk, that means one team does not need to rebuild everything every time a policy changes. Sometimes the right move is to change the instructions. Sometimes it is to add a knowledge source. Sometimes it is to update the model itself.</p><h2>Pretraining as the Broad Capability Layer</h2><p>Pretraining is the expensive stage that gives the model its general language capability. The model is trained on a massive corpus so it can learn reusable patterns rather than one task-specific mapping. The result is intentionally broad. A pretrained model is useful precisely because it is not locked to one workflow. It can later serve as the starting point for many different downstream systems.</p><p>The cost is why this layer is treated as a shared asset. Building a pretrained model from scratch requires substantial data and compute, so most organizations cannot justify doing it repeatedly for every new application. That is why reuse is central to modern LLM practice. Once a general model exists, its cost can be amortized across many products and teams.</p><p>The terminology follows that logic. A pretrained model is a model that has completed the broad initial training stage. A base model usually refers to that same model state before a later specialization is added. A foundation model is a broader label for a large pretrained model intended to support many downstream uses. The terms overlap, but the important idea is stable: the model is general before it becomes specialized.</p><p>This is also why a pretrained model should not be assumed to be instruction-ready. Broad language competence is not the same thing as reliably following user intent or business policy. Pretraining creates capability, not final product behavior.</p><h2>From Base Model to Specialized Behavior</h2><p>Fine-tuning, often called post-training, is the step that adapts a pretrained model toward a narrower goal. The key distinction is simple: fine-tuning changes model weights, while prompting changes inputs and retrieval changes the external information available to the model. Those are different kinds of change, even though they can be combined in one application.</p><p>ReviewDesk makes the tradeoff concrete. If the support team wants replies to consistently use a company-approved tone, fine-tuning may be a strong choice because the desired behavior should hold across many conversations. If the team only needs to change the framing of responses for a temporary policy update, a prompt change may be enough. If the team needs the assistant to answer questions about a newly published policy or a fresh FAQ, retrieval is often the better option because the knowledge can live outside the model and be updated without retraining.</p><p>This is the central caution: better adaptation is not always more training. Sometimes the problem is missing context, not missing skill. In that case, adding retrieval or improving the prompt is cheaper, faster, and safer than changing weights. Fine-tuning is powerful, but it is not the default answer to every product problem.</p><h2>Composing the Stack: Prompts, Embeddings, Retrieval, and Models</h2><p>Real LLM applications are usually composed, not monolithic. The model generates language, prompts define the task and constrain behavior, embeddings turn text into searchable representations, and retrieval brings in relevant facts or examples from outside the model. Each part contributes something different, and each part changes the system in a different way.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!c9Vz!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf8b99d5-6ba1-48de-af04-f971d80c91f6_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!c9Vz!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf8b99d5-6ba1-48de-af04-f971d80c91f6_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!c9Vz!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf8b99d5-6ba1-48de-af04-f971d80c91f6_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!c9Vz!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf8b99d5-6ba1-48de-af04-f971d80c91f6_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!c9Vz!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf8b99d5-6ba1-48de-af04-f971d80c91f6_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!c9Vz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf8b99d5-6ba1-48de-af04-f971d80c91f6_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/af8b99d5-6ba1-48de-af04-f971d80c91f6_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:5359032,&quot;alt&quot;:&quot;Figure 1.6: Figure: Modern LLM applications are composed from multiple layers&#8212;prompting, embeddings, retrieval, and a pretrained model&#8212;rather than relying on a single monolithic model alone.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204104616?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf8b99d5-6ba1-48de-af04-f971d80c91f6_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 1.6: Figure: Modern LLM applications are composed from multiple layers&#8212;prompting, embeddings, retrieval, and a pretrained model&#8212;rather than relying on a single monolithic model alone." title="Figure 1.6: Figure: Modern LLM applications are composed from multiple layers&#8212;prompting, embeddings, retrieval, and a pretrained model&#8212;rather than relying on a single monolithic model alone." srcset="https://substackcdn.com/image/fetch/$s_!c9Vz!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf8b99d5-6ba1-48de-af04-f971d80c91f6_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!c9Vz!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf8b99d5-6ba1-48de-af04-f971d80c91f6_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!c9Vz!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf8b99d5-6ba1-48de-af04-f971d80c91f6_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!c9Vz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf8b99d5-6ba1-48de-af04-f971d80c91f6_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>A useful analogy is to think in terms of the machine, the briefing, and the toolbelt. Fine-tuning changes the machine itself. Prompting changes the briefing given to the machine. Retrieval adds a toolbelt or library of reference material the system can consult when it needs evidence or context. Embeddings help organize that library by similarity so the right material can be found. These are complementary layers, not competing versions of the same idea.</p><p>The ReviewDesk workflow shows how the layers fit together. An incoming ticket is interpreted by the model, a prompt sets the tone and response format, embeddings help locate similar prior cases, retrieval supplies the most relevant resolutions or policy excerpts, and the pretrained model drafts the final answer using that context. If the team later wants more stable policy compliance, fine-tuning may be worth the investment. If the issue is stale information, retrieval is the cleaner fix. If the issue is phrasing or structure, prompt changes may be enough.</p><p>This layered view helps avoid a common engineering mistake: assuming that the right solution is always to train more. Often the best improvement is in the surrounding system rather than in the weights. A better prompt, a better retrieval index, or a better embedding model can deliver most of the benefit with less cost and less maintenance burden.</p><h2>How to Choose the Lightest Sufficient Adaptation</h2><p>A practical decision rule is to start with the lightest change that can solve the problem reliably. If the task is mainly about wording, policy framing, or response structure, prompting is often the first lever. If the system needs current facts, private documents, or case-specific context, retrieval is usually the better lever. If the desired behavior must be stable across many requests and reflect a durable operating style, fine-tuning becomes more attractive.</p><p>The tradeoffs are straightforward but important. Prompting is fast to try and easy to revise, but it can be brittle when the task gets complex. Retrieval improves grounding and maintainability, but it adds indexing, search, and context-management overhead. Fine-tuning can produce more consistent behavior, but it brings training cost, data preparation, evaluation effort, and ongoing maintenance. The choice is not just about model quality; it is about where the operational complexity should live.</p><p>For ReviewDesk, this leads to a practical sequence. The first prototype may use a hosted model with careful prompts because that is the fastest way to validate the workflow. If the assistant needs access to internal policy documents, retrieval can be added without retraining. If the team later wants the assistant to behave consistently enough that the same style and policy discipline are needed in every response, then fine-tuning becomes a stronger candidate. The right order is usually to improve the surrounding stack before changing the weights.</p><p>The broader lesson is that LLM systems are built as compositions of capability, instruction, and external knowledge. Pretraining gives the general base. Fine-tuning changes model behavior. Prompting shapes what the model does in the moment. Retrieval extends what it can know without retraining. That compositional view is the foundation for the later chapters on embeddings, prompting, retrieval, and tuning.</p><h2>Safety, Governance, and Practical Constraints</h2><h3>Why Capability Creates Governance Obligations</h3><p>When an LLM becomes useful, it also becomes consequential. A system that can draft messages, summarize documents, or suggest next steps is no longer just a language toy; it is part of a workflow that may influence people, records, and decisions. That is why capability does not remove responsibility. It increases it. An output that looks polished can still be wrong, misleading, or inappropriate for the setting in which it appears. In a ReviewDesk support flow, for example, a model might help draft replies or sort incoming tickets, but the team still needs to decide which cases are safe to automate and which require review before anything reaches a customer.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!I6cv!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa351ca3-c325-4d0a-b9e0-e361dc6700b1_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!I6cv!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa351ca3-c325-4d0a-b9e0-e361dc6700b1_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!I6cv!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa351ca3-c325-4d0a-b9e0-e361dc6700b1_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!I6cv!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa351ca3-c325-4d0a-b9e0-e361dc6700b1_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!I6cv!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa351ca3-c325-4d0a-b9e0-e361dc6700b1_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!I6cv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa351ca3-c325-4d0a-b9e0-e361dc6700b1_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/fa351ca3-c325-4d0a-b9e0-e361dc6700b1_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:5088475,&quot;alt&quot;:&quot;Figure 1.7: Capability increases governance obligations: as LLM outputs move from drafting to decisions that affect people or records, stronger human review and approval controls are required.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204104616?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa351ca3-c325-4d0a-b9e0-e361dc6700b1_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 1.7: Capability increases governance obligations: as LLM outputs move from drafting to decisions that affect people or records, stronger human review and approval controls are required." title="Figure 1.7: Capability increases governance obligations: as LLM outputs move from drafting to decisions that affect people or records, stronger human review and approval controls are required." srcset="https://substackcdn.com/image/fetch/$s_!I6cv!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa351ca3-c325-4d0a-b9e0-e361dc6700b1_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!I6cv!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa351ca3-c325-4d0a-b9e0-e361dc6700b1_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!I6cv!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa351ca3-c325-4d0a-b9e0-e361dc6700b1_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!I6cv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa351ca3-c325-4d0a-b9e0-e361dc6700b1_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The practical lesson is simple. LLM use should be planned as a governance decision as much as a technical one. Before a team adopts a model, it should ask what kinds of errors matter, who is expected to catch them, and how much human judgment must remain in the loop when the stakes are high.</p><h3>Bias, Fairness, and the Limits of Apparent Neutrality</h3><p>Bias in LLM output often comes from training data and context rather than from any explicit intent to be unfair. If the source material is skewed, incomplete, or unevenly represented, the model can reproduce those patterns while still sounding fluent and reasonable. That is what makes fairness problems easy to miss. A response can read smoothly and still reflect stereotypes, treat similar cases inconsistently, or favor one framing over another.</p><p>This matters because polished language can hide structural problems. A model may appear balanced while still producing outcomes that are unreliable for certain groups or sensitive domains. Misinformation, misleading confidence, and inappropriate content are all part of that risk surface. The output may sound certain even when it is uncertain, or sound helpful even when it is simply wrong. For that reason, bias and harmful generation are not edge cases. They are part of the normal operating risk of LLMs, especially when the system is used to support real judgment.</p><h3>Human Oversight, Transparency, and Accountability in Practice</h3><p>Human oversight matters most when an output can affect a person, a business process, or a regulated decision. In low-stakes drafting, a human can quickly edit what the model produces. In more sensitive settings, the human role must be stronger than copyediting. It has to include checking facts, rejecting unsuitable advice, and deciding whether the model should be trusted at all. The more sensitive the decision, the less acceptable it is to let a model act alone.</p><p>Transparency is part of that responsibility. People should know whether they are interacting with a model, a person, or a combination of both. If a customer receives a support message or recommendation, the system should not obscure where the model is involved. Otherwise, users may assume human judgment where there is none, or treat model output as more authoritative than it deserves. The organization deploying the system remains accountable even when the text reads as if it came from an expert.</p><h3>Legal and Policy Uncertainty Around LLM Use</h3><p>LLM adoption also sits inside legal and policy uncertainty. Intellectual property questions remain unsettled in many practical situations, especially around training data, copyrighted material, and whether generated text can resemble protected content too closely. This is not a place for a full legal analysis, and the doctrine is still developing. The useful point for a technical reader is that IP uncertainty can affect procurement, publishing, and internal policy choices even before any dispute is tested in court.</p><p>Regulation is emerging as another constraint on adoption. Governments are beginning to treat foundation-model deployment as something that may require oversight, particularly in sensitive use cases. The details vary and continue to evolve, which is exactly why governance belongs in the early planning stage rather than at the end of implementation.</p><h3>Compute Reality: GPUs, VRAM, and the Cost of Scale</h3><p>There is also a hard feasibility boundary. LLMs depend on GPUs for efficient training and inference, and they depend on VRAM because the model, its context, and the runtime all need memory at the same time. That means the question is not just whether a model exists, but whether it fits the available hardware. Model size, context length, and backend implementation all affect memory needs, so a single fixed VRAM number does not tell the whole story.</p><p>Cost changes just as dramatically with scale. Frontier-model training can require enormous GPU budgets, far beyond what most individuals or small teams can reproduce. Modest experimentation is much more realistic, especially when the goal is to run inference or adapt a smaller model. The practical takeaway is that compute is a design constraint, not an afterthought. If the hardware is too limited, the project may need a smaller model, a different deployment path, or a narrower scope.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!lU5r!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45d5adda-8348-44a9-a8ef-659231ef0fd5_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!lU5r!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45d5adda-8348-44a9-a8ef-659231ef0fd5_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!lU5r!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45d5adda-8348-44a9-a8ef-659231ef0fd5_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!lU5r!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45d5adda-8348-44a9-a8ef-659231ef0fd5_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!lU5r!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45d5adda-8348-44a9-a8ef-659231ef0fd5_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!lU5r!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45d5adda-8348-44a9-a8ef-659231ef0fd5_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/45d5adda-8348-44a9-a8ef-659231ef0fd5_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:6364974,&quot;alt&quot;:&quot;Figure 1.8: Hosted APIs and local open models differ mainly in privacy, control, customization, and hardware burden; the right choice depends on which constraints matter most.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204104616?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45d5adda-8348-44a9-a8ef-659231ef0fd5_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 1.8: Hosted APIs and local open models differ mainly in privacy, control, customization, and hardware burden; the right choice depends on which constraints matter most." title="Figure 1.8: Hosted APIs and local open models differ mainly in privacy, control, customization, and hardware burden; the right choice depends on which constraints matter most." srcset="https://substackcdn.com/image/fetch/$s_!lU5r!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45d5adda-8348-44a9-a8ef-659231ef0fd5_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!lU5r!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45d5adda-8348-44a9-a8ef-659231ef0fd5_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!lU5r!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45d5adda-8348-44a9-a8ef-659231ef0fd5_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!lU5r!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45d5adda-8348-44a9-a8ef-659231ef0fd5_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3>Choosing Between Hosted APIs and Local Open Models</h3><p>A second major decision is how the model is accessed. Hosted API use is attractive because it removes most of the infrastructure burden. The provider manages serving, scaling, and much of the operational complexity, so a team can start quickly. The tradeoff is that data may have to leave the organization, control over the system is limited, and direct fine-tuning is often unavailable or restricted. For sensitive material, that privacy boundary can be decisive.</p><p>Local open-model deployment shifts those tradeoffs. It usually asks for more hardware and more setup, but it gives the team greater control over data handling, model behavior, and adaptation. Fine-tuning is typically more accessible when the model weights are available and the runtime is under local control. When comparing the two paths, the useful lens is privacy, cost, control, and adaptability taken together rather than any single feature in isolation. Hosted APIs often win on convenience and initial speed. Local open models often win when the organization needs more autonomy and a clearer path to customization.</p><p>A compact way to think about the choice is to ask whether the workflow values simplicity or control. If the answer is convenience, a hosted service may be enough for early experimentation. If the answer is privacy or adaptation, a local open model becomes more attractive, provided the hardware budget is realistic.</p><h3>ReviewDesk as a Compact Deployment Decision</h3><p>ReviewDesk makes the comparison concrete. Suppose the team wants an initial system that classifies tickets and drafts first-pass replies. A hosted API version is the quickest way to test the idea, especially if the ticket text is low sensitivity and the team wants to minimize setup work. If the same team later decides that ticket data should stay on internal machines, or that the model should be tuned to its own support style, a local open-model deployment becomes more compelling. The first option favors speed and simplicity. The second favors privacy, control, and fine-tuning access. In practice, that is the kind of decision teams make repeatedly: not just which model is best, but which deployment path best fits the risk, the budget, and the way the work actually has to run.</p><h2>First Generation Workflow with Hugging Face</h2><p>A useful way to make the chapter concrete is to run a model once and see the entire path from text to text. At this point, the goal is not to become fluent in every generation parameter or decode strategy. The goal is simpler: load a pretrained model, pair it with the right tokenizer, send in a prompt, and recover the generated text. That first loop is the smallest working unit of modern LLM usage, and it is also the easiest place to see how the moving parts fit together.</p><p>For a ReviewDesk-style support workflow, the same pattern could take a customer message such as a complaint about a delayed refund and draft a polite reply. That is already a real application, even though the model itself is just producing text. The application value comes from the surrounding workflow, not from the raw model alone. In practice, the desk, the kitchen, and the delivery route all matter: the tokenizer prepares the order, the model cooks it, and the pipeline hands the meal back in a form the application can use.</p><h3>Loading the model-tokenizer pair from the Hub</h3><p>The first thing to notice is that the model and tokenizer are not independent utilities. They are a matched inference pair. The tokenizer defines how text is turned into tokens, and the model expects exactly that tokenization scheme. Hugging Face makes this relationship easy to work with because both assets are usually published under the same model identifier on the Hub. That is why the same identifier appears in both loading calls below.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!_jwe!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F787493fe-31fe-4334-9778-6286006e5781_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!_jwe!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F787493fe-31fe-4334-9778-6286006e5781_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!_jwe!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F787493fe-31fe-4334-9778-6286006e5781_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!_jwe!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F787493fe-31fe-4334-9778-6286006e5781_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!_jwe!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F787493fe-31fe-4334-9778-6286006e5781_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!_jwe!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F787493fe-31fe-4334-9778-6286006e5781_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/787493fe-31fe-4334-9778-6286006e5781_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:4634421,&quot;alt&quot;:&quot;Figure 1.9: The model and tokenizer are a matched pair loaded from the same Hugging Face identifier; correct generation depends on using compatible text-to-token and token-to-text components.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204104616?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F787493fe-31fe-4334-9778-6286006e5781_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 1.9: The model and tokenizer are a matched pair loaded from the same Hugging Face identifier; correct generation depends on using compatible text-to-token and token-to-text components." title="Figure 1.9: The model and tokenizer are a matched pair loaded from the same Hugging Face identifier; correct generation depends on using compatible text-to-token and token-to-text components." srcset="https://substackcdn.com/image/fetch/$s_!_jwe!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F787493fe-31fe-4334-9778-6286006e5781_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!_jwe!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F787493fe-31fe-4334-9778-6286006e5781_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!_jwe!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F787493fe-31fe-4334-9778-6286006e5781_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!_jwe!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F787493fe-31fe-4334-9778-6286006e5781_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;1a953dec-88c1-4c68-a710-f5171c12f79a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from transformers import AutoModelForCausalLM, AutoTokenizer

# Load model and tokenizer
model = AutoModelForCausalLM.from_pretrained(
    "microsoft/Phi-3-mini-4k-instruct",
    device_map="cuda",
    torch_dtype="auto",
    trust_remote_code=True,
)
tokenizer = AutoTokenizer.from_pretrained("microsoft/Phi-3-mini-4k-instruct")</code></pre></div><p>This example uses a compact causal language model, which is the kind of model that predicts the next token repeatedly to produce free-form text. The <code>device_map="cuda"</code> setting tells Transformers to place the model on a GPU when one is available, and <code>torch_dtype="auto"</code> lets the library choose a suitable numerical precision for the hardware. Both choices are practical rather than decorative: a model that fits comfortably in memory is much easier to experiment with than one that forces constant out-of-memory errors. The <code>trust_remote_code=True</code> flag is a compatibility hook for model-specific loading behavior. It is not something every model needs, but it is sometimes required when a repository includes custom architecture code that the standard loader does not know about in advance.</p><p>The VRAM story matters early because model size changes what is feasible. A model that is pleasant to query on a workstation may be awkward on a laptop, and a model that seems small in parameter count can still be heavy once you account for precision and runtime buffers. Quantization can reduce that footprint, which is one reason smaller or compressed models remain valuable even as larger systems attract more attention. If you are choosing a model for hands-on work, memory budget is part of the design decision, not an afterthought.</p><h3>Wrapping the pair in a text-generation pipeline</h3><p>Once the model and tokenizer are loaded, the Hugging Face pipeline turns them into a simpler interface. Instead of manually tokenizing inputs, calling the model, and decoding outputs yourself, you ask a higher-level object to do the orchestration. That does not remove the underlying steps; it just hides them behind a clean entry point.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!waQJ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1efb46b-66ab-40c9-add1-23117d61247f_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!waQJ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1efb46b-66ab-40c9-add1-23117d61247f_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!waQJ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1efb46b-66ab-40c9-add1-23117d61247f_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!waQJ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1efb46b-66ab-40c9-add1-23117d61247f_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!waQJ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1efb46b-66ab-40c9-add1-23117d61247f_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!waQJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1efb46b-66ab-40c9-add1-23117d61247f_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b1efb46b-66ab-40c9-add1-23117d61247f_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:5576045,&quot;alt&quot;:&quot;Figure 1.10: A Hugging Face text-generation pipeline wraps tokenization, model inference, decoding, and formatting into one higher-level call.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204104616?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1efb46b-66ab-40c9-add1-23117d61247f_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 1.10: A Hugging Face text-generation pipeline wraps tokenization, model inference, decoding, and formatting into one higher-level call." title="Figure 1.10: A Hugging Face text-generation pipeline wraps tokenization, model inference, decoding, and formatting into one higher-level call." srcset="https://substackcdn.com/image/fetch/$s_!waQJ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1efb46b-66ab-40c9-add1-23117d61247f_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!waQJ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1efb46b-66ab-40c9-add1-23117d61247f_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!waQJ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1efb46b-66ab-40c9-add1-23117d61247f_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!waQJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1efb46b-66ab-40c9-add1-23117d61247f_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;48f00683-f786-4710-a83c-f025a76828a9&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from transformers import pipeline

# Create a pipeline
generator = pipeline(
    "text-generation",
    model=model,
    tokenizer=tokenizer,
    return_full_text=False,
    max_new_tokens=500,
    do_sample=False
)</code></pre></div><p>The main value of the pipeline is that it reduces setup friction. The model supplies the learned behavior, the tokenizer supplies the text interface, and the pipeline supplies the glue. For a first experiment, that is exactly the right abstraction level. You can focus on whether the model produces something sensible before worrying about lower-level mechanics.</p><p>The generation settings here already influence product behavior in a visible way. <code>max_new_tokens=500</code> places a ceiling on how long the answer can become, which is important because generation does not stop by itself unless it reaches a limit or an end condition. <code>do_sample=False</code> chooses a deterministic decoding path, so repeated runs with the same model and prompt are much more likely to give the same output. <code>return_full_text=False</code> means the pipeline returns only the completion, not the original prompt plus the completion. That small flag matters a lot when you are building an application, because you usually want to display or store only the model&#8217;s answer, not the entire input echoed back again.</p><h3>Sending a chat-style prompt through the generator</h3><p>The prompt in this style of workflow is a structured list of messages rather than a single plain string. Each message has a role and content field, which matches the conversational format used by many modern instruction-tuned models. Even though the section is still introductory, this is a useful preview of the distinction between raw text and model-ready input formatting.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;d4ce8ffc-77b5-4484-8bc8-f389cc4a7f25&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># The prompt (user input / query)
messages = [
    {"role": "user", "content": "Create a funny joke about chickens."}
]

# Generate output
output = generator(messages)
print(output[0]["generated_text"])</code></pre></div><p>The output is returned as a structured object, and the generated text is read from the first item in that object. Beginners often stop at the call to the generator and forget that the result still has a shape. In other words, the model does not just hand back a string in the simplest possible form; it returns an inference result that the application then extracts and formats. If you were building the ReviewDesk support assistant, the same pattern would accept a customer message, send it through the generator, and then display the completion as a proposed reply.</p><p>This is also the place where it becomes clear that similar code does not guarantee identical behavior across models. A different causal language model may respond with a different tone, different verbosity, or a different sense of structure even when the pipeline call looks nearly identical. The model identifier selects not only weights but also a family of conventions and capabilities. The code is stable in shape, but the behavior is model-specific.</p><h3>Interpreting the first result and its limits</h3><p>At this point, you have a working generation stack, but the visible controls are only a small slice of what actually shapes the answer. Length is governed by <code>max_new_tokens</code>, repeatability is affected by the decoding choice, and the model&#8217;s overall style comes from the pretrained weights themselves. If the result is too terse, too repetitive, or too casual, the remedy is not always a code change in this section. Often the deeper issue lies in the model choice, the prompt format, or the limits of the underlying architecture.</p><p>The hardware side matters just as much. If the model is too large for available VRAM, the workflow breaks before it begins, which is why compact checkpoints and quantized variants are so useful for development. That practical constraint is one reason the Hugging Face ecosystem is valuable: it gives you a common loading pattern across many models, while still letting you choose one that fits your machine.</p><h3>What this workflow prepares you to learn next</h3><p>This first success is intentionally incomplete. It gets you from prompt to output, but it does not yet explain how raw text becomes tokens, how those tokens become internal vectors, or how the model decides which next token to emit. Those hidden transformations are the subject of the next chapters. Tokenization will show how text is broken into model-ready units, embeddings will show how those units become numeric representations, and the generation chapters will open the black box behind the text that the pipeline so conveniently returns.</p><p><em>Use the url below to download the enitre book as pdf:</em></p>
      <p>
          <a href="https://onepagecode.substack.com/p/an-introduction-to-large-language">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Building the GPT architecture core]]></title><description><![CDATA[Chapter 7: Implementing GPT configuration, tensor flow, and the modular decoder block from scratch in PyTorch]]></description><link>https://onepagecode.substack.com/p/building-the-gpt-architecture-core</link><guid isPermaLink="false">https://onepagecode.substack.com/p/building-the-gpt-architecture-core</guid><dc:creator><![CDATA[Onepagecode]]></dc:creator><pubDate>Fri, 03 Jul 2026 19:38:00 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!kokH!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feec192e2-bb0b-4a7d-9006-05245a3d299d_1615x974.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>GPT configuration as the model contract</h2><p>A GPT model begins with a configuration object that fixes the architectural shape before any weights are learned. That distinction matters. The configuration specifies the size of the vocabulary, the maximum context length, the width of the internal representations, the number of attention heads, the number of stacked blocks, the dropout rate, and whether attention projections include bias terms. These values do not change during training; they define the container in which trainable weights will live.</p><div><hr></div><h2><strong>&#128216; Buy the Entire Book &#8211; Available on Amazon Worldwide (If you are paid subscriber, use the url at the end of this article to download the book for free!)</strong></h2><p>You can now purchase the complete book directly from Amazon in your country:</p><ul><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/us/dualbookshelf.marketplacelink/B0H6WZJ57S">United States (US)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/uk/dualbookshelf.marketplacelink/B0H6WZJ57S">United Kingdom (UK)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/de/dualbookshelf.marketplacelink/B0H6WZJ57S">Germany (DE)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/fr/dualbookshelf.marketplacelink/B0H6WZJ57S">France (FR)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/es/dualbookshelf.marketplacelink/B0H6WZJ57S">Spain (ES)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/it/dualbookshelf.marketplacelink/B0H6WZJ57S">Italy (IT)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/nl/dualbookshelf.marketplacelink/B0H6WZJ57S">Netherlands (NL)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/jp/dualbookshelf.marketplacelink/B0H6WZJ57S">Japan (JP)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/br/dualbookshelf.marketplacelink/B0H6WZJ57S">Brazil (BR):</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/ca/dualbookshelf.marketplacelink/B0H6WZJ57S">Canada (CA)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/mx/dualbookshelf.marketplacelink/B0H6WZJ57S">Mexico (MX)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/au/dualbookshelf.marketplacelink/B0H6WZJ57S">Australia (AU)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/in/dualbookshelf.marketplacelink/B0H6WZJ57S">India (IN)</a></strong></p><div><hr></div></li></ul><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;166c4f0d-7148-40ed-9570-229dd8d8cd16&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">GPT_CONFIG_124M = {
    "vocab_size": 50257,
    "context_length": 1024,
    "emb_dim": 768,
    "n_heads": 12,
    "n_layers": 12,
    "drop_rate": 0.1,
    "qkv_bias": False
}</code></pre></div><p>The vocabulary size determines the dimensionality of the final prediction space. The context length sets the longest token sequence the model can process at once. The embedding dimension controls the width of token and position vectors as they move through the network. The head count and layer count determine how much attention computation is distributed across each block and how many times the block is repeated. Dropout introduces stochastic regularization, and the attention bias flag governs whether the query, key, and value projections use bias parameters. Together, these fields describe the model contract that every submodule must obey.</p><p>Parameters are the trainable weights inside that contract. Embedding tables, projection matrices, normalization scales, and feed-forward layers all contribute parameters. By contrast, the configuration values are fixed design choices. Keeping that separation clear makes the later implementation easier to read, because each module can be built from the same shared specification.</p><h2>A dummy GPT stack to preview tensor flow</h2><p>Before implementing the real transformer internals, it is useful to sketch the full route that data will take through the model. The dummy GPT class below has the same outer structure as the finished network. It starts with token embeddings and positional embeddings, applies dropout, passes the sequence through a stack of placeholder transformer blocks, normalizes the result, and finally maps each hidden vector to vocabulary-sized logits.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;1115eec3-720f-4140-a8b9-a64c9a82fce4&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">class DummyGPTModel(nn.Module):
    def __init__(self, cfg):
        super().__init__()
        self.tok_emb = nn.Embedding(cfg["vocab_size"], cfg["emb_dim"])
        self.pos_emb = nn.Embedding(cfg["context_length"], cfg["emb_dim"])
        self.drop_emb = nn.Dropout(cfg["drop_rate"])
        self.trf_blocks = nn.Sequential(
            *[DummyTransformerBlock(cfg)
              for _ in range(cfg["n_layers"])]
        )
        self.final_norm = DummyLayerNorm(cfg["emb_dim"])
        self.out_head = nn.Linear(
            cfg["emb_dim"], cfg["vocab_size"], bias=False
        )

    def forward(self, in_idx):
        batch_size, seq_len = in_idx.shape
        tok_embeds = self.tok_emb(in_idx)
        pos_embeds = self.pos_emb(
            torch.arange(seq_len, device=in_idx.device)
        )
        x = tok_embeds + pos_embeds
        x = self.drop_emb(x)
        x = self.trf_blocks(x)
        x = self.final_norm(x)
        logits = self.out_head(x)
        return logits

class DummyTransformerBlock(nn.Module):
    def __init__(self, cfg):
        super().__init__()

    def forward(self, x):
        return x

class DummyLayerNorm(nn.Module):
    def __init__(self, normalized_shape, eps=1e-5):
        super().__init__()

    def forward(self, x):
        return x</code></pre></div><p>This skeleton already reveals the full tensor path. The input is a batch of token IDs with shape <code>[batch_size, seq_len]</code>. Token embedding converts each ID into a dense vector, producing <code>[batch_size, seq_len, emb_dim]</code>. Positional embedding generates one learned position vector per time step, and the two are added elementwise so that each token representation carries both identity and order information. After that, dropout preserves the same shape while randomly masking activations during training. The placeholder transformer stack accepts and returns the same three-dimensional tensor, which is exactly what the real blocks will do once attention and feed-forward logic are inserted. The final normalization prepares the hidden states for the output head, and the linear projection produces logits with shape <code>[batch_size, seq_len, vocab_size]</code>.</p><p>Although the dummy block and dummy normalization do nothing numerically, they are still valuable. They preserve the interface of the eventual GPT implementation, which means the rest of the model can be developed without changing the outer structure. That makes the section a structural preview rather than an approximation of language modeling behavior.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!kokH!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feec192e2-bb0b-4a7d-9006-05245a3d299d_1615x974.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!kokH!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feec192e2-bb0b-4a7d-9006-05245a3d299d_1615x974.png 424w, https://substackcdn.com/image/fetch/$s_!kokH!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feec192e2-bb0b-4a7d-9006-05245a3d299d_1615x974.png 848w, https://substackcdn.com/image/fetch/$s_!kokH!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feec192e2-bb0b-4a7d-9006-05245a3d299d_1615x974.png 1272w, https://substackcdn.com/image/fetch/$s_!kokH!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feec192e2-bb0b-4a7d-9006-05245a3d299d_1615x974.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!kokH!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feec192e2-bb0b-4a7d-9006-05245a3d299d_1615x974.png" width="1456" height="878" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/eec192e2-bb0b-4a7d-9006-05245a3d299d_1615x974.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:878,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1316767,&quot;alt&quot;:&quot;Full GPT architecture tensor flow from token IDs through embeddings, transformer blocks, final normalization, and vocabulary logits.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/203536313?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feec192e2-bb0b-4a7d-9006-05245a3d299d_1615x974.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Full GPT architecture tensor flow from token IDs through embeddings, transformer blocks, final normalization, and vocabulary logits." title="Full GPT architecture tensor flow from token IDs through embeddings, transformer blocks, final normalization, and vocabulary logits." srcset="https://substackcdn.com/image/fetch/$s_!kokH!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feec192e2-bb0b-4a7d-9006-05245a3d299d_1615x974.png 424w, https://substackcdn.com/image/fetch/$s_!kokH!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feec192e2-bb0b-4a7d-9006-05245a3d299d_1615x974.png 848w, https://substackcdn.com/image/fetch/$s_!kokH!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feec192e2-bb0b-4a7d-9006-05245a3d299d_1615x974.png 1272w, https://substackcdn.com/image/fetch/$s_!kokH!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feec192e2-bb0b-4a7d-9006-05245a3d299d_1615x974.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><em>Figure 7.1. The GPT model keeps a stable tensor path: token IDs become token-plus-position embeddings, pass through repeated transformer blocks, and become vocabulary-sized logits.</em></p><h2>Tokenizing a toy batch and running the preview model</h2><p>To make the shape flow concrete, start with two short strings and convert them into a batched tensor of token IDs using the GPT-2 tokenizer. The resulting batch is small enough to inspect directly, yet it already matches the input form expected by the model.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;c3a7c87b-3070-4779-8839-7c3e6743177d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">import tiktoken

tokenizer = tiktoken.get_encoding("gpt2")
batch = []
txt1 = "Every effort moves you"
txt2 = "Every day holds a"

batch.append(torch.tensor(tokenizer.encode(txt1)))
batch.append(torch.tensor(tokenizer.encode(txt2)))
batch = torch.stack(batch, dim=0)
print(batch)</code></pre></div><p>The stacked tensor contains token IDs for two sequences. Because the sequences are short, the printed tensor is easy to inspect, but the same batching pattern scales to long sequences as long as they fit inside the configured context length. When that batch is passed into the dummy model, the model returns one vocabulary-sized score vector per token position.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;5021fb83-5d51-48d1-96ce-63c51528dd0d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">torch.manual_seed(123)
model = DummyGPTModel(GPT_CONFIG_124M)
logits = model(batch)
print("Output shape:", logits.shape)
print(logits)</code></pre></div><p>The output shape confirms the model interface: batch dimension first, sequence length second, vocabulary dimension last. The actual values in the tensor are not meant to be interpreted as meaningful predictions yet, because the central transformer machinery is still a stub. What matters here is that the complete GPT pipeline is already visible. The configuration sets the dimensions, the embeddings convert token IDs into vectors, the placeholder stack preserves the sequence shape, and the output head maps each position back to the vocabulary. In the next sections, the dummy pieces will be replaced by real normalization, activation, attention, and residual logic.</p><h2>Why normalize token embeddings at all</h2><p>Deep networks can become difficult to optimize when internal activations drift too far from a well-behaved range. In a GPT-style model, that issue is especially visible once token representations pass through many stacked sublayers. Layer normalization addresses this by keeping each token&#8217;s hidden state on a controlled scale before the next computation sees it. The key detail is that GPT normalizes each token independently across its embedding features, not across the batch. That choice makes the method insensitive to batch size and well suited to autoregressive models that process variable-sized batches during training and generation.</p><p>This normalization step does not remove information the model needs to learn. Instead, it provides a stable operating point. Once the activations are standardized, later layers can focus on transforming meaning rather than constantly compensating for wildly changing magnitudes. That stability becomes more important as the network grows deeper, where poorly scaled activations can amplify optimization problems and make gradients harder to use effectively.</p><h2>A worked normalization example on a small activation vector</h2><p>Suppose a token is represented by a short activation vector. For one example, we can compute the mean and variance across the last dimension and use those statistics to center and scale the vector. The manual normalization rule is</p><p>out\_norm=out&#8722;&#956;&#963;2</p><p>where &#956; is the mean of the features in the token vector and &#963;2 is the corresponding variance. After subtracting the mean and dividing by the standard deviation, the result has zero mean and unit variance along that feature axis.</p><p>The final LayerNorm module adds a small stability term to the denominator:</p><p>x^=x&#8722;&#956;&#963;2+&#1013;</p><p>That &#1013; prevents division by zero and softens numerical issues when the variance is extremely small. In practice, this matters even when the tensor values are ordinary, because deep models run through many repeated floating-point operations.</p><p>A quick toy example makes the mechanics concrete. Imagine a tensor with shape [batch,features], where each row is one token representation. We compute the statistics row by row, normalize each row independently, and then check that the transformed rows are close to zero mean and unit variance. The important point is that the computation runs across the last dimension, so each token is normalized from its own features rather than from other examples in the batch.</p><h2>Implementing LayerNorm in PyTorch</h2><p>The reusable module is compact, but each line is doing something essential.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;af78d0d1-7c72-4255-882d-2c605ebd8fc6&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">class LayerNorm(nn.Module):
    def __init__(self, emb_dim):
        super().__init__()
        self.eps = 1e-5
        self.scale = nn.Parameter(torch.ones(emb_dim))
        self.shift = nn.Parameter(torch.zeros(emb_dim))

    def forward(self, x):
        mean = x.mean(dim=-1, keepdim=True)
        var = x.var(dim=-1, keepdim=True, unbiased=False)
        norm_x = (x - mean) / torch.sqrt(var + self.eps)
        return self.scale * norm_x + self.shift</code></pre></div><p>The reduction over <code>dim=-1</code> tells PyTorch to compute statistics along the embedding axis. With GPT tensors, that is the last dimension of a shape such as <code>[batch, num_tokens, emb_dim]</code>. The <code>keepdim=True</code> argument preserves the rank of the tensor, which makes broadcasting work cleanly when subtracting the mean and dividing by the standard deviation.</p><p>The call to <code>var(..., unbiased=False)</code> is deliberate. It uses the population-style variance estimate rather than Bessel-corrected sample variance. That choice matches the implementation used in GPT-style models and keeps the behavior aligned with the weights you will later load or reproduce. The epsilon term is added inside the square root, exactly as in the mathematical definition above, so the code and the derivation stay synchronized.</p><p>The learnable <code>scale</code> and <code>shift</code> parameters restore flexibility after normalization. Standardization alone would force every token feature toward the same distribution, but the model still needs the ability to reweight or translate those normalized values when that helps the task. LayerNorm therefore standardizes first and then applies a trainable affine transform.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!QVpE!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1d122f3-f900-40e0-9182-3461cf3a8da8_1536x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!QVpE!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1d122f3-f900-40e0-9182-3461cf3a8da8_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!QVpE!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1d122f3-f900-40e0-9182-3461cf3a8da8_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!QVpE!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1d122f3-f900-40e0-9182-3461cf3a8da8_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!QVpE!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1d122f3-f900-40e0-9182-3461cf3a8da8_1536x1024.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!QVpE!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1d122f3-f900-40e0-9182-3461cf3a8da8_1536x1024.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a1d122f3-f900-40e0-9182-3461cf3a8da8_1536x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1463584,&quot;alt&quot;:&quot;LayerNorm normalizes each token independently across embedding features and then applies learned scale and shift parameters.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/203536313?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1d122f3-f900-40e0-9182-3461cf3a8da8_1536x1024.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="LayerNorm normalizes each token independently across embedding features and then applies learned scale and shift parameters." title="LayerNorm normalizes each token independently across embedding features and then applies learned scale and shift parameters." srcset="https://substackcdn.com/image/fetch/$s_!QVpE!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1d122f3-f900-40e0-9182-3461cf3a8da8_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!QVpE!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1d122f3-f900-40e0-9182-3461cf3a8da8_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!QVpE!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1d122f3-f900-40e0-9182-3461cf3a8da8_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!QVpE!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1d122f3-f900-40e0-9182-3461cf3a8da8_1536x1024.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><em>Figure 7.2. LayerNorm normalizes each token independently across its embedding features, then restores flexibility with learned scale and shift parameters.</em></p><h2>Checking the output statistics and contrasting with batch normalization</h2><p>A verification pass should show that the module is doing what the derivation promises. Using a small activation tensor, you can feed the same values through the custom module and then inspect the statistics along the last dimension. The means should be very close to zero, and the variances should be very close to one, up to floating-point error and the small epsilon term.</p><p>That behavior differs from batch normalization in an important way. Batch normalization computes statistics across the batch dimension, so its output depends on how many examples happen to be in the mini-batch. Layer normalization does not have that dependency. It normalizes each token independently, which is a better fit for transformer hidden states and for the variable batch sizes commonly used in language modeling. With this module in place, the next step is to place it where the model can use it repeatedly inside each transformer sublayer.</p><h2>GELU as the transformer&#8217;s smooth nonlinearity</h2><p>A transformer block needs more than attention. After attention mixes information across tokens, each token still needs a private transformation that can reshape its feature vector before the next block sees it. In GPT-style models, that transformation is usually a two-layer feed-forward network with a GELU nonlinearity in the middle.</p><p>GELU stands for Gaussian error linear unit. Its exact definition is</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\mathrm{GELU}(x)=x\\cdot\\Phi(x)&quot;,&quot;id&quot;:&quot;34E176FD02&quot;}" data-component-name="LatexBlockToDOM"></div><p>where $\Phi(x)$ is the cumulative distribution function of the standard normal distribution. This makes GELU smooth, which is one reason it works well inside deep transformer stacks. Instead of cutting off negative values abruptly, it lets them pass through in a graded way. That softness often leads to smoother optimization than a hard-threshold activation.</p><p>A familiar baseline is ReLU, which returns zero for negative inputs and the input itself for positive inputs. GELU behaves differently on the negative side: small negative values are suppressed, but not all at once. That matters in a feed-forward block because the network is not merely filtering features; it is learning a nuanced token-wise transformation. For GPT-style models, the usual implementation uses a fast approximation:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\mathrm{GELU}(x)\\approx 0.5x\\left(1+\\tanh\\left(\\sqrt{2/\\pi}\\left(x+0.044715x^3\\right)\\right)\\right)&quot;,&quot;id&quot;:&quot;A5506BC25B&quot;}" data-component-name="LatexBlockToDOM"></div><p>This approximation is accurate enough for practical use and cheaper to evaluate than the exact CDF form.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;e2072e47-bba4-483c-94f6-fb70581429e4&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">class GELU(nn.Module):
    def __init__(self):
        super().__init__()

    def forward(self, x):
        return 0.5 * x * (1 + torch.tanh(
            torch.sqrt(torch.tensor(2.0 / torch.pi)) * 
            (x + 0.044715 * torch.pow(x, 3))
        ))</code></pre></div><h2>Visual comparison with ReLU</h2><p>The easiest way to see the difference is to compare the two activations on the same input range. ReLU produces a sharp corner at zero, while GELU bends smoothly through that region and leaves a small, nonzero response for some negative values. That shape is often a better fit for transformer MLPs, where the model benefits from gradual feature suppression rather than a hard cutoff.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;ac130a71-daf2-44da-a3cc-e7ae201ce007&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">import matplotlib.pyplot as plt

gelu, relu = GELU(), nn.ReLU()

x = torch.linspace(-3, 3, 100)     #1
y_gelu, y_relu = gelu(x), relu(x)
plt.figure(figsize=(8, 3))
for i, (y, label) in enumerate(zip([y_gelu, y_relu], ["GELU", "ReLU"]), 1):
    plt.subplot(1, 2, i)
    plt.plot(x, y)
    plt.title(f"{label} activation function")
    plt.xlabel("x")
    plt.ylabel(f"{label}(x)")
    plt.grid(True)
plt.tight_layout()
plt.show()</code></pre></div><p>The point of the plot is not that one curve is universally superior. It is that transformer feed-forward layers benefit from a smoother response profile, especially when the network is deep and the activation must cooperate with normalization and residual paths.</p><h2>Building the position-wise feed-forward module</h2><p>The feed-forward sublayer is position-wise, which means it processes each token representation independently of the others. Attention has already handled interaction across positions. This module now acts on the feature dimension of each token vector, expanding it into a larger hidden space, applying GELU, and then projecting it back to the model width.</p><p>The hidden expansion is a standard design choice in GPT-style architectures. If the embedding dimension is $d<em>{\text{model}}$, the inner layer is typically set to $4d</em>{\text{model}}$. That fourfold expansion gives the MLP room to build richer intermediate features before compressing them back to the original embedding size. The output width must match the input width so the sublayer can fit into the surrounding transformer block without changing tensor layout.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;0ae3c5b7-9328-45f7-b857-292820a9da18&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">class FeedForward(nn.Module):
    def __init__(self, cfg):
        super().__init__()
        self.layers = nn.Sequential(
            nn.Linear(cfg["emb_dim"], 4 * cfg["emb_dim"]),
            GELU(),
            nn.Linear(4 * cfg["emb_dim"], cfg["emb_dim"]),
        )

    def forward(self, x):
        return self.layers(x)</code></pre></div><p>This module is intentionally simple. The first linear layer widens the representation, the activation introduces nonlinearity, and the second linear layer returns the vector to the original embedding size. Because the operation is applied independently to each token position, the sequence length is unchanged. Only the feature representation within each token is transformed.</p><p>In practice, this means a tensor shaped like <code>(batch_size, num_tokens, emb_dim)</code> enters the feed-forward network and exits with the same outer dimensions. That interface is exactly what later transformer blocks need. The module can be stacked repeatedly because every block receives and returns the same embedding width.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!J7WP!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d029416-3758-4649-90b9-22afbe6c9e64_1536x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!J7WP!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d029416-3758-4649-90b9-22afbe6c9e64_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!J7WP!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d029416-3758-4649-90b9-22afbe6c9e64_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!J7WP!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d029416-3758-4649-90b9-22afbe6c9e64_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!J7WP!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d029416-3758-4649-90b9-22afbe6c9e64_1536x1024.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!J7WP!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d029416-3758-4649-90b9-22afbe6c9e64_1536x1024.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6d029416-3758-4649-90b9-22afbe6c9e64_1536x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1420620,&quot;alt&quot;:&quot;The position-wise feed-forward network expands each token representation, applies GELU, and projects it back to the original embedding width.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/203536313?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d029416-3758-4649-90b9-22afbe6c9e64_1536x1024.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="The position-wise feed-forward network expands each token representation, applies GELU, and projects it back to the original embedding width." title="The position-wise feed-forward network expands each token representation, applies GELU, and projects it back to the original embedding width." srcset="https://substackcdn.com/image/fetch/$s_!J7WP!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d029416-3758-4649-90b9-22afbe6c9e64_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!J7WP!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d029416-3758-4649-90b9-22afbe6c9e64_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!J7WP!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d029416-3758-4649-90b9-22afbe6c9e64_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!J7WP!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d029416-3758-4649-90b9-22afbe6c9e64_1536x1024.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><em>Figure 7.3. The position-wise feed-forward network expands each token from `emb_dim` to `4 &#215; emb_dim`, applies GELU, and projects it back without changing the sequence length.</em></p><h2>Checking the tensor interface</h2><p>A quick shape test is enough to confirm that the submodule behaves as intended. The test below uses the chapter&#8217;s configuration object, which already defines the embedding dimension for the model.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;f439af71-d5de-4211-85f9-b38d0f90841f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">ffn = FeedForward(GPT_CONFIG_124M)
x = torch.rand(2, 3, 768)          #1
out = ffn(x)
print(out.shape)</code></pre></div><p>If the implementation is correct, the output shape matches the input shape. That result is not cosmetic; it is the structural requirement that makes the feed-forward network usable inside a transformer block. The block can then combine attention output, normalization, and this MLP sublayer while preserving the same token and embedding interfaces throughout the stack.</p><h2>Residual connections as an extra path</h2><p>A residual connection gives a layer two ways to carry information forward. The layer can still learn a transformation of its input, but it can also preserve the original signal by adding that input back to the transformed output. In practice, this means the network does not have to decide between &#8220;replace the representation&#8221; and &#8220;keep the representation.&#8221; It can do both, with the shortcut carrying the earlier activation around the main transformation.</p><p>That simple elementwise add becomes especially useful in deep networks. When several layers are stacked, each layer only needs to learn the part that changes the representation, rather than rebuilding the entire signal from scratch. For transformer blocks, this idea fits naturally: the block computes an update, then adds that update to the input of the block. The block output remains close to the original representation when needed, while still allowing meaningful transformation when the data demands it.</p><h2>Why deep stacks become hard to optimize</h2><p>Without shortcut paths, gradients must pass through every layer in sequence during backpropagation. As depth increases, those gradients can shrink or become poorly scaled before they reach the earliest layers. The result is the familiar vanishing-gradient problem: later layers may still learn, but the layers near the input receive such weak learning signals that training becomes slow or unstable.</p><p>Residual paths help because they create a more direct route for both activations and gradients. During the forward pass, the model can preserve useful information instead of repeatedly compressing it through transformations. During the backward pass, the shortcut gives the optimization process a path that is less dependent on the exact behavior of every intermediate transformation. This does not eliminate all training difficulty, but it makes very deep stacks much more practical.</p><h2>A minimal PyTorch experiment</h2><p>The following toy network is intentionally small and shape-matched so that the residual add is always valid when the shortcut is enabled. It is not the final GPT block; it is only a clean way to observe what changes when the skip path is present.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;03ac371c-f713-4af2-85c1-3ca68c9266d5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">class ExampleDeepNeuralNetwork(nn.Module):
    def __init__(self, layer_sizes, use_shortcut):
        super().__init__()
        self.use_shortcut = use_shortcut
        self.layers = nn.ModuleList([
            nn.Sequential(nn.Linear(layer_sizes[0], layer_sizes[1]), 
                          GELU()),
            nn.Sequential(nn.Linear(layer_sizes[1], layer_sizes[2]), 
                          GELU()),
            nn.Sequential(nn.Linear(layer_sizes[2], layer_sizes[3]), 
                          GELU()),
            nn.Sequential(nn.Linear(layer_sizes[3], layer_sizes[4]), 
                          GELU()),
            nn.Sequential(nn.Linear(layer_sizes[4], layer_sizes[5]), 
                          GELU())
        ])

    def forward(self, x):
        for layer in self.layers:
            layer_output = layer(x)
            if self.use_shortcut and x.shape == layer_output.shape:
                x = x + layer_output
            else:
                x = layer_output
        return x</code></pre></div><p>The key detail is the shape check. Elementwise addition only makes sense when the two tensors have identical shapes, so the shortcut is applied only when the input and the layer output can be added safely. That condition is not a theoretical detail; it is the implementation rule that makes residual connections well-defined.</p><p>To compare gradient flow, we can run the same input through a plain stack and through the same stack with shortcuts, then print the mean absolute gradient for every weight tensor after backpropagation.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;40cab2da-a824-46e1-8da3-e3fc7f9666ad&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def print_gradients(model, x):
    output = model(x)
    target = torch.tensor([[0.]])

    loss = nn.MSELoss()
    loss = loss(output, target)

    loss.backward()

    for name, param in model.named_parameters():
        if 'weight' in name:
            print(f"{name} has gradient mean of {param.grad.abs().mean().item()}")</code></pre></div><p>This experiment uses a fixed target and a simple mean-squared error loss so that the only thing we are comparing is how gradients move through depth. The printed values are not a benchmark and they are not a proof. They are a diagnostic view of gradient flow in one concrete example.</p><h2>Reading the gradient results</h2><p>In the plain network, gradient magnitudes often become smaller as they reach earlier layers. That is the practical sign of a deep stack that is harder to optimize: the layers farthest from the loss receive weaker updates. With residual paths enabled, the gradient means tend to stay more usable across the stack, which tells you that the learning signal is propagating more effectively.</p><p>The important lesson is qualitative rather than numerical. You should read the output as evidence that shortcut connections improve trainability by making gradients less fragile over depth. This is why residuals are not just an architectural convenience. They are one of the mechanisms that allow deep models to be trained at all.</p><h2>Why GPT blocks rely on this idea</h2><p>GPT-style transformer blocks repeat the same pattern many times, and each repetition depends on residual paths to keep optimization stable. The attention sublayer and the feed-forward sublayer will each be wrapped in this kind of skip structure so that every block can refine the representation without destroying it. Residual connections are therefore part of the basic grammar of the GPT architecture, not an optional embellishment.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!hFyM!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6651c776-fadd-4c71-83a9-fb135f5fa28a_1536x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!hFyM!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6651c776-fadd-4c71-83a9-fb135f5fa28a_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!hFyM!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6651c776-fadd-4c71-83a9-fb135f5fa28a_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!hFyM!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6651c776-fadd-4c71-83a9-fb135f5fa28a_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!hFyM!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6651c776-fadd-4c71-83a9-fb135f5fa28a_1536x1024.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!hFyM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6651c776-fadd-4c71-83a9-fb135f5fa28a_1536x1024.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6651c776-fadd-4c71-83a9-fb135f5fa28a_1536x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1391552,&quot;alt&quot;:&quot;Residual connections provide shortcut paths for activations and gradients through a deep GPT-style block stack.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/203536313?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6651c776-fadd-4c71-83a9-fb135f5fa28a_1536x1024.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Residual connections provide shortcut paths for activations and gradients through a deep GPT-style block stack." title="Residual connections provide shortcut paths for activations and gradients through a deep GPT-style block stack." srcset="https://substackcdn.com/image/fetch/$s_!hFyM!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6651c776-fadd-4c71-83a9-fb135f5fa28a_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!hFyM!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6651c776-fadd-4c71-83a9-fb135f5fa28a_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!hFyM!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6651c776-fadd-4c71-83a9-fb135f5fa28a_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!hFyM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6651c776-fadd-4c71-83a9-fb135f5fa28a_1536x1024.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><em>Figure 7.4. Residual connections let each GPT block add a learned update while preserving a direct path for activations and gradients.</em></p><p><em>Use the url below to download the enitre book as pdf:</em></p>
      <p>
          <a href="https://onepagecode.substack.com/p/building-the-gpt-architecture-core">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Looking Inside Large Language Models]]></title><description><![CDATA[Chapter 3 of Hands-On Large Language Models: Looking inside transformer architecture, forward passes, hidden states, and the LM head]]></description><link>https://onepagecode.substack.com/p/looking-inside-large-language-models</link><guid isPermaLink="false">https://onepagecode.substack.com/p/looking-inside-large-language-models</guid><dc:creator><![CDATA[Onepagecode]]></dc:creator><pubDate>Fri, 03 Jul 2026 07:32:38 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!qfxJ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc84965b6-94e5-48f8-84d9-140b33d9b4b5_2816x1536.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>From pretrained checkpoint to generation pipeline</h2><p>This chapter is about opening the hood on a generative Transformer, but it helps to begin with the outside view. Before we trace attention scores or cache mechanics, we need a clean mental picture of what happens when a pretrained checkpoint is turned into a working text generator. In practice, that means separating three ideas that are easy to blur together: the tokenizer that converts text into tokens, the model checkpoint that contains learned weights, and the generation step that uses both to produce new text.</p><div><hr></div><h2>&#128216; Buy the Entire Book &#8211; Available on Amazon Worldwide</h2><p>You can now purchase the complete book directly from Amazon in your country:</p><ul><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/us/dualbookshelf.marketplacelink/B0H6WZJ57S">United States (US)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/uk/dualbookshelf.marketplacelink/B0H6WZJ57S">United Kingdom (UK)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/de/dualbookshelf.marketplacelink/B0H6WZJ57S">Germany (DE)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/fr/dualbookshelf.marketplacelink/B0H6WZJ57S">France (FR)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/es/dualbookshelf.marketplacelink/B0H6WZJ57S">Spain (ES)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/it/dualbookshelf.marketplacelink/B0H6WZJ57S">Italy (IT)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/nl/dualbookshelf.marketplacelink/B0H6WZJ57S">Netherlands (NL)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/jp/dualbookshelf.marketplacelink/B0H6WZJ57S">Japan (JP)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/br/dualbookshelf.marketplacelink/B0H6WZJ57S">Brazil (BR):</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/ca/dualbookshelf.marketplacelink/B0H6WZJ57S">Canada (CA)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/mx/dualbookshelf.marketplacelink/B0H6WZJ57S">Mexico (MX)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/au/dualbookshelf.marketplacelink/B0H6WZJ57S">Australia (AU)</a></strong></p></li><li><p><strong><a href="https://kdp.amazon.com/amazon-dp-action/in/dualbookshelf.marketplacelink/B0H6WZJ57S">India (IN)</a></strong></p></li></ul><div><hr></div><p></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!qfxJ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc84965b6-94e5-48f8-84d9-140b33d9b4b5_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!qfxJ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc84965b6-94e5-48f8-84d9-140b33d9b4b5_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!qfxJ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc84965b6-94e5-48f8-84d9-140b33d9b4b5_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!qfxJ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc84965b6-94e5-48f8-84d9-140b33d9b4b5_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!qfxJ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc84965b6-94e5-48f8-84d9-140b33d9b4b5_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!qfxJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc84965b6-94e5-48f8-84d9-140b33d9b4b5_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c84965b6-94e5-48f8-84d9-140b33d9b4b5_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:5809893,&quot;alt&quot;:&quot;Figure 3.1: The generation workflow separates tokenization, the pretrained causal language model checkpoint, and the generation step that assembles the final text output.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204105532?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc84965b6-94e5-48f8-84d9-140b33d9b4b5_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 3.1: The generation workflow separates tokenization, the pretrained causal language model checkpoint, and the generation step that assembles the final text output." title="Figure 3.1: The generation workflow separates tokenization, the pretrained causal language model checkpoint, and the generation step that assembles the final text output." srcset="https://substackcdn.com/image/fetch/$s_!qfxJ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc84965b6-94e5-48f8-84d9-140b33d9b4b5_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!qfxJ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc84965b6-94e5-48f8-84d9-140b33d9b4b5_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!qfxJ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc84965b6-94e5-48f8-84d9-140b33d9b4b5_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!qfxJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc84965b6-94e5-48f8-84d9-140b33d9b4b5_2816x1536.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>A useful way to think about the setup is as a translator, a scorer, and a desk clerk. The tokenizer translates your prompt into the model&#8217;s token vocabulary. The causal language model scores what token should come next, one step at a time. The text-generation pipeline acts like the desk clerk that gathers the pieces, hands the request to the right machinery, and returns the finished response in a convenient format. That convenience is real, but it can also hide important details when you later care about latency, memory use, or why the output changed after a library upgrade.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;fac99d73-629f-4943-b102-73b7d57e49dc&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline

# Load model and tokenizer
tokenizer = AutoTokenizer.from_pretrained("microsoft/Phi-3-mini-4k-instruct")

model = AutoModelForCausalLM.from_pretrained(
    "microsoft/Phi-3-mini-4k-instruct",
    device_map="cuda",
    torch_dtype="auto",
    trust_remote_code=True,
)

# Create a pipeline
generator = pipeline(
    "text-generation",
    model=model,
    tokenizer=tokenizer,
    return_full_text=False,
    max_new_tokens=50,
    do_sample=False,
)</code></pre></div><p>The tokenizer is loaded first because text must be converted into tokens before the model can do anything useful. The model is loaded separately because <code>AutoModelForCausalLM</code> selects a causal language model class, which is the autoregressive family designed for next-token prediction and completion-style generation. That choice matters: this is not a classifier or an embedding model, but a generator trained to predict what comes next given the tokens it has already seen.</p><p>The pipeline sits on top of those components and bundles them into a single generation interface. It does not generate text by itself, and it does not replace the checkpoint; it simply coordinates the tokenizer, the model, and the generation settings. The visible options already reveal a few important behaviors. <code>return_full_text=False</code> means the output is just the newly generated continuation rather than the original prompt plus the continuation. <code>max_new_tokens=50</code> limits the length of the completion, and <code>do_sample=False</code> asks for deterministic decoding so the same prompt tends to produce the same answer. The model is also placed on CUDA with automatic precision selection, which is a reminder that inference is not just about language behavior but also about where the computation runs.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!sE92!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdfb7fc4a-b017-400e-9f65-acdd39bb5d40_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!sE92!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdfb7fc4a-b017-400e-9f65-acdd39bb5d40_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!sE92!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdfb7fc4a-b017-400e-9f65-acdd39bb5d40_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!sE92!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdfb7fc4a-b017-400e-9f65-acdd39bb5d40_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!sE92!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdfb7fc4a-b017-400e-9f65-acdd39bb5d40_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!sE92!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdfb7fc4a-b017-400e-9f65-acdd39bb5d40_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/dfb7fc4a-b017-400e-9f65-acdd39bb5d40_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:4839028,&quot;alt&quot;:&quot;Figure 3.2: Generation settings control whether the prompt is included in the result, how long the completion can be, and whether decoding is deterministic or sampled.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204105532?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdfb7fc4a-b017-400e-9f65-acdd39bb5d40_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 3.2: Generation settings control whether the prompt is included in the result, how long the completion can be, and whether decoding is deterministic or sampled." title="Figure 3.2: Generation settings control whether the prompt is included in the result, how long the completion can be, and whether decoding is deterministic or sampled." srcset="https://substackcdn.com/image/fetch/$s_!sE92!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdfb7fc4a-b017-400e-9f65-acdd39bb5d40_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!sE92!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdfb7fc4a-b017-400e-9f65-acdd39bb5d40_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!sE92!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdfb7fc4a-b017-400e-9f65-acdd39bb5d40_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!sE92!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdfb7fc4a-b017-400e-9f65-acdd39bb5d40_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>For a ReviewDesk-style prompt, the flow looks like a request for a short draft response to a customer issue, sent through the generator and returned as text. The specific wording of the prompt can change the completion, but the structure stays the same: prompt in, tokens through the model, generated tokens out. That is the core workflow this section is orienting you to. In the next section, we will stop treating generation as a black box and follow that prompt token by token through the model&#8217;s internal computation.</p><h3>From prompt text to token IDs and back to text</h3><p>A generative Transformer does not start with words as humans read them. It starts with token IDs, the compact integers produced by the tokenizer. The full generation path therefore begins by turning a prompt string into model input, passing those IDs through the network, and then turning the selected token back into readable text. That is the basic loop behind every causal language model, even when a higher-level pipeline hides the details.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;913150c0-4fd5-44be-8321-781e7e029cd9&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">prompt = "Write an email apologizing to Sarah for the tragic gardening mishap. Explain how it happened."

output = generator(prompt)

print(output[0]['generated_text'])</code></pre></div><p>That one call hides three separate jobs. The tokenizer converts text into IDs. The model turns those IDs into scores for the next token. The decoding step converts the chosen token ID back into text. Keeping those roles distinct matters when you want to inspect a failure, compare performance, or understand what part of the system changed the output.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!k-LX!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0e88134e-9412-4d08-81c1-d8dd4e749afb_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!k-LX!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0e88134e-9412-4d08-81c1-d8dd4e749afb_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!k-LX!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0e88134e-9412-4d08-81c1-d8dd4e749afb_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!k-LX!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0e88134e-9412-4d08-81c1-d8dd4e749afb_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!k-LX!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0e88134e-9412-4d08-81c1-d8dd4e749afb_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!k-LX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0e88134e-9412-4d08-81c1-d8dd4e749afb_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0e88134e-9412-4d08-81c1-d8dd4e749afb_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:5117576,&quot;alt&quot;:&quot;Figure 3.3: A causal language model converts prompt text to token IDs, transforms them into logits, selects one next token, and decodes that token back to text before repeating the loop.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204105532?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0e88134e-9412-4d08-81c1-d8dd4e749afb_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 3.3: A causal language model converts prompt text to token IDs, transforms them into logits, selects one next token, and decodes that token back to text before repeating the loop." title="Figure 3.3: A causal language model converts prompt text to token IDs, transforms them into logits, selects one next token, and decodes that token back to text before repeating the loop." srcset="https://substackcdn.com/image/fetch/$s_!k-LX!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0e88134e-9412-4d08-81c1-d8dd4e749afb_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!k-LX!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0e88134e-9412-4d08-81c1-d8dd4e749afb_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!k-LX!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0e88134e-9412-4d08-81c1-d8dd4e749afb_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!k-LX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0e88134e-9412-4d08-81c1-d8dd4e749afb_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3>Inside the forward pass: embeddings, decoder blocks, and hidden states</h3><p>After tokenization, the prompt is processed in parallel. This is one of the easiest places to make a wrong mental model. The prompt is not read one word at a time in the way generation is emitted. Instead, the entire token sequence enters the Transformer stack at once, and each position is transformed in parallel during the prompt pass.</p><p>The printed module tree shows that the model is modular rather than monolithic. It contains an embedding layer, a repeated stack of decoder layers, a final normalization, and an LM head.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;df45447b-55a1-4c0e-baca-5ae2cd39b2af&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">Phi3ForCausalLM(
  (model): Phi3Model(
    (embed_tokens): Embedding(32064, 3072, padding_idx=32000)
    (embed_dropout): Dropout(p=0.0, inplace=False)
    (layers): ModuleList(
      (0-31): 32 x Phi3DecoderLayer(
        (self_attn): Phi3Attention(
          (o_proj): Linear(in_features=3072, out_features=3072, bias=False)
          (qkv_proj): Linear(in_features=3072, out_features=9216, bias=False)
          (rotary_emb): Phi3RotaryEmbedding()
        )
        (mlp): Phi3MLP(
          (gate_up_proj): Linear(in_features=3072, out_features=16384, bias=False)
          (down_proj): Linear(in_features=8192, out_features=3072, bias=False)
          (activation_fn): SiLU()
        )
        (input_layernorm): Phi3RMSNorm()
        (resid_attn_dropout): Dropout(p=0.0, inplace=False)
        (resid_mlp_dropout): Dropout(p=0.0, inplace=False)
        (post_attention_layernorm): Phi3RMSNorm()
      )
    )
    (norm): Phi3RMSNorm()
  )
  (lm_head): Linear(in_features=3072, out_features=32064, bias=False)
)</code></pre></div><p>The embedding layer maps each token ID to a learned vector. Each decoder block then updates those vectors using self-attention and a feedforward network. Self-attention lets a token representation gather information from earlier positions through queries, keys, and values. The feedforward layer then transforms each position locally. The result of the stack is a set of hidden states, not yet a prediction.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!B4zJ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91160cec-5d79-4bf5-aa69-a1c14c0976cc_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!B4zJ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91160cec-5d79-4bf5-aa69-a1c14c0976cc_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!B4zJ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91160cec-5d79-4bf5-aa69-a1c14c0976cc_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!B4zJ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91160cec-5d79-4bf5-aa69-a1c14c0976cc_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!B4zJ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91160cec-5d79-4bf5-aa69-a1c14c0976cc_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!B4zJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91160cec-5d79-4bf5-aa69-a1c14c0976cc_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/91160cec-5d79-4bf5-aa69-a1c14c0976cc_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:5508069,&quot;alt&quot;:&quot;Figure 3.4: During the prompt pass, token IDs are embedded, transformed by stacked decoder blocks in parallel, and converted into hidden states before the LM head produces vocabulary logits.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/204105532?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91160cec-5d79-4bf5-aa69-a1c14c0976cc_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Figure 3.4: During the prompt pass, token IDs are embedded, transformed by stacked decoder blocks in parallel, and converted into hidden states before the LM head produces vocabulary logits." title="Figure 3.4: During the prompt pass, token IDs are embedded, transformed by stacked decoder blocks in parallel, and converted into hidden states before the LM head produces vocabulary logits." srcset="https://substackcdn.com/image/fetch/$s_!B4zJ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91160cec-5d79-4bf5-aa69-a1c14c0976cc_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!B4zJ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91160cec-5d79-4bf5-aa69-a1c14c0976cc_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!B4zJ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91160cec-5d79-4bf5-aa69-a1c14c0976cc_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!B4zJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91160cec-5d79-4bf5-aa69-a1c14c0976cc_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>That distinction is worth keeping sharp. Hidden states are internal representations. Attention weights are the pattern of looking from one position to another. Logits are vocabulary scores. They are related, but they are not interchangeable.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;729be591-e83f-4501-96fc-0938242e5192&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">prompt = "The capital of France is"

# Tokenize the input prompt
input_ids = tokenizer(prompt, return_tensors="pt").input_ids

# Tokenize the input prompt
input_ids = input_ids.to("cuda")

# Get the output of the model before the lm_head
model_output = model.model(input_ids)

# Get the output of the lm_head
lm_head_output = model.lm_head(model_output[0])</code></pre></div><p>This makes the handoff concrete. <code>input_ids</code> are the tokenized prompt. <code>model_output[0]</code> is the hidden-state tensor coming out of the Transformer stack. <code>lm_head_output</code> is the next-token score tensor over the vocabulary.</p><h3>From hidden states to next-token scores at the LM head</h3>
      <p>
          <a href="https://onepagecode.substack.com/p/looking-inside-large-language-models">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Trainable causal attention and multi-head attention]]></title><description><![CDATA[Chapter 6: Implementing learnable query-key-value projections, causal masking, and multi-head attention in PyTorch]]></description><link>https://onepagecode.substack.com/p/trainable-causal-attention-and-multi</link><guid isPermaLink="false">https://onepagecode.substack.com/p/trainable-causal-attention-and-multi</guid><dc:creator><![CDATA[Onepagecode]]></dc:creator><pubDate>Mon, 29 Jun 2026 20:22:07 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!8inB!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19d38a58-3ff5-4d67-bab5-f1771a00a129_1024x559.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3>Download the entire book as EPUB or PDF using the URL at the end of this article!</h3><h2>Learning queries, keys, and values</h2><p>In the earlier form of self-attention, the similarity score between tokens was computed directly from the input representations. That is useful for understanding the mechanism, but it is still a fixed rule. GPT-style attention becomes much more expressive when the model learns how to turn each token embedding into three different views of the same token: a query, a key, and a value. The query asks what this token is looking for, the key advertises what the token offers, and the value carries the information that can be passed along if the token is selected.</p><p>The important change is that these views are not hand-designed features. They are produced by trainable projection matrices. If the input embedding has dimension <code>d_in</code> and we want each projected vector to have dimension <code>d_out</code>, then each token embedding is multiplied by a learned weight matrix of shape <code>(d_in, d_out)</code>. In other words, the model learns how to map the same token into three different spaces, each one specialized for a different role in attention.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!8inB!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19d38a58-3ff5-4d67-bab5-f1771a00a129_1024x559.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!8inB!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19d38a58-3ff5-4d67-bab5-f1771a00a129_1024x559.jpeg 424w, https://substackcdn.com/image/fetch/$s_!8inB!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19d38a58-3ff5-4d67-bab5-f1771a00a129_1024x559.jpeg 848w, https://substackcdn.com/image/fetch/$s_!8inB!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19d38a58-3ff5-4d67-bab5-f1771a00a129_1024x559.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!8inB!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19d38a58-3ff5-4d67-bab5-f1771a00a129_1024x559.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!8inB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19d38a58-3ff5-4d67-bab5-f1771a00a129_1024x559.jpeg" width="1024" height="559" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/19d38a58-3ff5-4d67-bab5-f1771a00a129_1024x559.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:559,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:84621,&quot;alt&quot;:&quot;The same token embedding is projected into separate query, key, and value vectors by learned matrices.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/203534783?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19d38a58-3ff5-4d67-bab5-f1771a00a129_1024x559.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="The same token embedding is projected into separate query, key, and value vectors by learned matrices." title="The same token embedding is projected into separate query, key, and value vectors by learned matrices." srcset="https://substackcdn.com/image/fetch/$s_!8inB!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19d38a58-3ff5-4d67-bab5-f1771a00a129_1024x559.jpeg 424w, https://substackcdn.com/image/fetch/$s_!8inB!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19d38a58-3ff5-4d67-bab5-f1771a00a129_1024x559.jpeg 848w, https://substackcdn.com/image/fetch/$s_!8inB!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19d38a58-3ff5-4d67-bab5-f1771a00a129_1024x559.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!8inB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19d38a58-3ff5-4d67-bab5-f1771a00a129_1024x559.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><em>Figure 6.1. Trainable projection matrices map the same token embedding into three different views: query, key, and value.</em></p><p>A small worked example makes the shape flow concrete. Suppose <code>x_2</code> is the embedding of one token, and the full input sequence is stored in <code>inputs</code>. The token embedding has shape <code>(d_in,)</code>. After projection, the query, key, and value each have shape <code>(d_out,)</code>. The code below uses a tiny output dimension so that the intermediate vectors are easy to inspect.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;289dece5-681c-40c5-803f-d638b2a70ef5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">x_2 = inputs[1]
d_in = inputs.shape[1]
d_out = 2

torch.manual_seed(123)
W_query = torch.nn.Parameter(torch.rand(d_in, d_out), requires_grad=False)
W_key   = torch.nn.Parameter(torch.rand(d_in, d_out), requires_grad=False)
W_value = torch.nn.Parameter(torch.rand(d_in, d_out), requires_grad=False)

query_2 = x_2 @ W_query
key_2 = x_2 @ W_key
value_2 = x_2 @ W_value
print(query_2)</code></pre></div><p>The notation here is easy to read if you keep the shapes in mind. <code>x_2</code> is one row vector, and each projection matrix maps it into a new vector of length <code>d_out</code>. The three matrices are distinct because the model does not need the same transformation for asking, matching, and transmitting information. That separation is what gives attention its flexibility.</p><p>The same projection is then applied to every token in the sequence. Instead of projecting one token at a time, the full sequence tensor can be multiplied by the learned matrices in one step. This produces a key matrix and a value matrix whose rows correspond to tokens in the sequence.</p><h2>Computing scaled dot-product attention</h2><p>Once the query, key, and value vectors exist, the attention computation follows a simple path. The query for one token is compared with every key by a dot product. The result is a score that measures how strongly the query matches each token in the sequence. For a single query token, the score against one particular key is a scalar; for the whole sequence, the scores form a vector.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;3d60a605-e3ec-4524-a620-71857784e964&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">keys = inputs @ W_key
values = inputs @ W_value
print("keys.shape:", keys.shape)
print("values.shape:", values.shape)

keys_2 = keys[1]
attn_score_22 = query_2.dot(keys_2)
print(attn_score_22)

attn_scores_2 = query_2 @ keys.T
print(attn_scores_2)</code></pre></div><p>These raw scores are not yet suitable as mixing coefficients. Their scale depends on the dimensionality of the key vectors, and larger dimensions tend to produce larger dot products. Before normalization, the scores are divided by the square root of the key dimension. This keeps the values in a range where softmax can distribute probability mass more usefully instead of collapsing too quickly toward a single position.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;6885ab94-7212-49c4-9783-cdede8d25085&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">d_k = keys.shape[-1]
attn_weights_2 = torch.softmax(attn_scores_2 / d_k**0.5, dim=-1)
print(attn_weights_2)</code></pre></div><p>At this point it helps to separate the vocabulary of the mechanism from the vocabulary of learning. <code>W_query</code>, <code>W_key</code>, and <code>W_value</code> are learned model parameters. They are part of the network and are updated during training. The attention weights computed by softmax are different: they are temporary, input-dependent coefficients produced during the forward pass. They change from one token to the next because they depend on the current query and the current sequence content.</p><p>The final context vector is a weighted sum of the value vectors. Tokens that receive a high attention weight contribute more strongly to the output representation. This is where the model gathers information from the sequence into a single output vector for the query token.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;cd7921f7-1e52-41ed-8acc-c91b148ff0dd&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">context_vec_2 = attn_weights_2 @ values
print(context_vec_2)</code></pre></div><p>That weighted sum is the essence of self-attention. The query determines what to look for, the keys determine where to look, and the values determine what information gets copied forward. Because the projections are learned, the model can specialize these roles for the task at hand instead of relying on a fixed similarity measure.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!_Vzo!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8b20e094-41b2-49b1-8908-6b1c53cb6eb1_1024x559.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!_Vzo!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8b20e094-41b2-49b1-8908-6b1c53cb6eb1_1024x559.jpeg 424w, https://substackcdn.com/image/fetch/$s_!_Vzo!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8b20e094-41b2-49b1-8908-6b1c53cb6eb1_1024x559.jpeg 848w, https://substackcdn.com/image/fetch/$s_!_Vzo!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8b20e094-41b2-49b1-8908-6b1c53cb6eb1_1024x559.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!_Vzo!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8b20e094-41b2-49b1-8908-6b1c53cb6eb1_1024x559.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!_Vzo!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8b20e094-41b2-49b1-8908-6b1c53cb6eb1_1024x559.jpeg" width="1024" height="559" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8b20e094-41b2-49b1-8908-6b1c53cb6eb1_1024x559.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:559,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:77911,&quot;alt&quot;:&quot;A query is compared with keys, scaled, normalized with softmax, and used to mix value vectors.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/203534783?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8b20e094-41b2-49b1-8908-6b1c53cb6eb1_1024x559.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="A query is compared with keys, scaled, normalized with softmax, and used to mix value vectors." title="A query is compared with keys, scaled, normalized with softmax, and used to mix value vectors." srcset="https://substackcdn.com/image/fetch/$s_!_Vzo!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8b20e094-41b2-49b1-8908-6b1c53cb6eb1_1024x559.jpeg 424w, https://substackcdn.com/image/fetch/$s_!_Vzo!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8b20e094-41b2-49b1-8908-6b1c53cb6eb1_1024x559.jpeg 848w, https://substackcdn.com/image/fetch/$s_!_Vzo!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8b20e094-41b2-49b1-8908-6b1c53cb6eb1_1024x559.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!_Vzo!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8b20e094-41b2-49b1-8908-6b1c53cb6eb1_1024x559.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><em>Figure 6.2. Scaled dot-product attention compares a query with keys, normalizes the scores, and uses the resulting weights to mix the values.</em></p><h2>Refactoring the computation into a PyTorch module</h2><p>The manual sequence of matrix multiplications is correct, but it should not remain as loose notebook code. The same logic belongs in a reusable module so it can be inserted into a larger transformer block. The first version below packages the projection matrices as <code>nn.Parameter</code> objects and performs the full attention computation in <code>forward</code>. This is a direct translation of the manual steps into a custom layer.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;c9c49870-9d3d-463e-aad4-e2a5fa3dcc19&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">import torch.nn as nn

class SelfAttention_v1(nn.Module):
    def __init__(self, d_in, d_out):
        super().__init__()
        self.W_query = nn.Parameter(torch.rand(d_in, d_out))
        self.W_key = nn.Parameter(torch.rand(d_in, d_out))
        self.W_value = nn.Parameter(torch.rand(d_in, d_out))

    def forward(self, x):
        keys = x @ self.W_key
        queries = x @ self.W_query
        values = x @ self.W_value
        attn_scores = queries @ keys.T
        attn_weights = torch.softmax(attn_scores / keys.shape[-1]**0.5, dim=-1)
        context_vec = attn_weights @ values
        return context_vec

torch.manual_seed(123)
sa_v1 = SelfAttention_v1(d_in, d_out)
print(sa_v1(inputs))</code></pre></div><p>This version is useful because it makes the mechanics explicit. It also highlights the separation between the trainable weights stored in the module and the attention weights created during execution. The module does not memorize a single fixed context vector. Instead, it computes a new one for each input sequence.</p><p>In practice, the same layer is usually written with <code>nn.Linear</code>. That is the more idiomatic PyTorch form because <code>Linear</code> already manages the weight matrix, optional bias, and the usual parameter registration behavior. The computation is unchanged; only the packaging improves.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;d2e36550-b3a4-4dc2-93d2-441b6156e2fd&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">class SelfAttention_v2(nn.Module):
    def __init__(self, d_in, d_out, qkv_bias=False):
        super().__init__()
        self.W_query = nn.Linear(d_in, d_out, bias=qkv_bias)
        self.W_key = nn.Linear(d_in, d_out, bias=qkv_bias)
        self.W_value = nn.Linear(d_in, d_out, bias=qkv_bias)

    def forward(self, x):
        keys = self.W_key(x)
        queries = self.W_query(x)
        values = self.W_value(x)
        attn_scores = queries @ keys.T
        attn_weights = torch.softmax(attn_scores / keys.shape[-1]**0.5, dim=-1)
        context_vec = attn_weights @ values
        return context_vec

torch.manual_seed(789)
sa_v2 = SelfAttention_v2(d_in, d_out)
print(sa_v2(inputs))</code></pre></div><p>This is the form you want to carry forward. It is concise, trainable, and compatible with the rest of a transformer implementation. The same projection-and-mixing pattern will later be extended with masking and multiple heads, but the core idea remains the same: learned queries, keys, and values produce attention weights, and those weights assemble a context vector from the value representations.</p><h2>Packaging the derivation into a reusable module</h2><p>The attention computation we derived earlier is ready for a small but important shift in perspective. Until now, the operations were written as a sequence of tensor manipulations: project the input into queries, keys, and values, score their similarity, normalize the scores, and combine the values. That is a useful way to learn the mechanism, but it is not yet the form you want inside a model.</p><p>In PyTorch, the natural next step is to wrap the attention path in an <code>nn.Module</code>. That gives the computation a stable interface, makes the learned projection matrices part of the model state, and lets the same code run on any input sequence with the same feature dimension. The forward pass then becomes a direct translation of the attention pipeline: inputs are projected, similarities are computed, softmax turns those similarities into weights, and the weighted sum produces the context vectors.</p><h2>A Parameter-based first implementation</h2><p>The first version keeps the mechanics explicit by storing the projection matrices as <code>torch.nn.Parameter</code> objects. This is a good teaching step because it exposes the boundary between learned weights and runtime-computed attention weights. The projection matrices are parameters of the model. The attention weights are not parameters at all; they are temporary values produced anew for each input sequence during the forward pass.</p><p>For a toy sequence, the flow is easy to trace. If <code>x_2</code> is one token embedding, and <code>d_in</code> is the embedding size while <code>d_out</code> is the projection size, then the token is mapped into three different spaces: one for queries, one for keys, and one for values. The same idea extends to the full sequence by applying the same learned matrices to every token embedding.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;74be2f69-c430-439f-9460-abed36165fe9&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">x_2 = inputs[1]
d_in = inputs.shape[1]
d_out = 2

torch.manual_seed(123)
W_query = torch.nn.Parameter(torch.rand(d_in, d_out), requires_grad=False)
W_key   = torch.nn.Parameter(torch.rand(d_in, d_out), requires_grad=False)
W_value = torch.nn.Parameter(torch.rand(d_in, d_out), requires_grad=False)

query_2 = x_2 @ W_query
key_2 = x_2 @ W_key
value_2 = x_2 @ W_value
print(query_2)

keys = inputs @ W_key
values = inputs @ W_value
print("keys.shape:", keys.shape)
print("values.shape:", values.shape)

keys_2 = keys[1]
attn_score_22 = query_2.dot(keys_2)
print(attn_score_22)

attn_scores_2 = query_2 @ keys.T
print(attn_scores_2)

d_k = keys.shape[-1]
attn_weights_2 = torch.softmax(attn_scores_2 / d_k**0.5, dim=-1)
print(attn_weights_2)

context_vec_2 = attn_weights_2 @ values
print(context_vec_2)</code></pre></div><p>The important pattern here is that the scores and weights are not stored anywhere permanent. They are computed from the current input, used to build the context vector, and then discarded. That is exactly what you want from attention: the model keeps the learned projections, but the routing between tokens changes with the sequence.</p><p>The scaling by the square root of the key dimension is also essential. Without it, the raw dot products can become too large as the dimensionality grows, which makes the softmax overly sharp and the gradients harder to optimize. The scaled version keeps the attention distribution in a healthier range while preserving the same underlying similarity computation.</p><h2>Refactoring to <code>nn.Linear</code></h2><p>Once the mechanism is clear, the more idiomatic implementation replaces raw parameters with <code>nn.Linear</code> layers. This does not change the computation; it changes how the learnable projections are represented. A linear layer still performs an affine projection from <code>d_in</code> to <code>d_out</code>, but it gives you a cleaner module interface and fits naturally with the rest of PyTorch.</p><p>The optional bias flag is included because some transformer implementations use bias terms in the projection layers and others do not. The key point is that the forward pass remains the same: the input sequence is projected into queries, keys, and values, the attention scores are formed from query-key dot products, the scores are scaled and normalized, and the resulting weights mix the values into context vectors.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;6316c022-670c-4a8b-9162-f2a354608c3a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">import torch.nn as nn

class SelfAttention_v1(nn.Module):
    def __init__(self, d_in, d_out):
        super().__init__()
        self.W_query = nn.Parameter(torch.rand(d_in, d_out))
        self.W_key = nn.Parameter(torch.rand(d_in, d_out))
        self.W_value = nn.Parameter(torch.rand(d_in, d_out))

    def forward(self, x):
        keys = x @ self.W_key
        queries = x @ self.W_query
        values = x @ self.W_value
        attn_scores = queries @ keys.T
        attn_weights = torch.softmax(attn_scores / keys.shape[-1]**0.5, dim=-1)
        context_vec = attn_weights @ values
        return context_vec

torch.manual_seed(123)
sa_v1 = SelfAttention_v1(d_in, d_out)
print(sa_v1(inputs))

class SelfAttention_v2(nn.Module):
    def __init__(self, d_in, d_out, qkv_bias=False):
        super().__init__()
        self.W_query = nn.Linear(d_in, d_out, bias=qkv_bias)
        self.W_key = nn.Linear(d_in, d_out, bias=qkv_bias)
        self.W_value = nn.Linear(d_in, d_out, bias=qkv_bias)

    def forward(self, x):
        keys = self.W_key(x)
        queries = self.W_query(x)
        values = self.W_value(x)
        attn_scores = queries @ keys.T
        attn_weights = torch.softmax(attn_scores / keys.shape[-1]**0.5, dim=-1)
        context_vec = attn_weights @ values
        return context_vec

torch.manual_seed(789)
sa_v2 = SelfAttention_v2(d_in, d_out)
print(sa_v2(inputs))</code></pre></div><p>This refactor is mostly about ergonomics and readability, but that matters in a larger model. Once attention is one block among many, you want each block to look and feel like a standard layer. <code>nn.Linear</code> also makes parameter management cleaner, especially when you later save checkpoints, load weights, or assemble the attention module into a deeper transformer stack.</p><h2>Sanity-checking the module on toy input</h2><p>A quick forward pass on the toy sequence is enough to verify that the module behaves as expected. The output should have one context vector per input token, and each context vector should have the projection size <code>d_out</code>. If the shapes line up and the values print without error, the reusable attention block is doing its job.</p><p>What you have at this point is still plain self-attention, not yet language-model attention. Every token can still attend to every other token, which is fine for understanding the mechanism but not for autoregressive generation. The next step is to make the attention causal so each position can only use earlier tokens, and then extend the same core idea to multiple heads.</p><h2>Why causal masking is needed</h2><p>A GPT-style model predicts the next token from the tokens that came before it. That requirement changes the attention mechanism in a fundamental way. In ordinary self-attention, every token can look at every other token in the sequence. For language modeling, that would let a token borrow information from the future, which is exactly what the model is not allowed to see during training or generation.</p><p>Causal attention removes this shortcut. At position <em>t</em>, the token may attend only to positions up to <em>t</em> itself. The attention pattern becomes left-to-right: earlier tokens may influence later ones, but not the reverse. Once that rule is enforced, the attention layer matches the autoregressive setting used by GPT-style models.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!c4R5!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac0111e2-b21e-4566-83e7-303c40ac9b3f_1024x559.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!c4R5!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac0111e2-b21e-4566-83e7-303c40ac9b3f_1024x559.jpeg 424w, https://substackcdn.com/image/fetch/$s_!c4R5!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac0111e2-b21e-4566-83e7-303c40ac9b3f_1024x559.jpeg 848w, https://substackcdn.com/image/fetch/$s_!c4R5!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac0111e2-b21e-4566-83e7-303c40ac9b3f_1024x559.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!c4R5!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac0111e2-b21e-4566-83e7-303c40ac9b3f_1024x559.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!c4R5!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac0111e2-b21e-4566-83e7-303c40ac9b3f_1024x559.jpeg" width="1024" height="559" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ac0111e2-b21e-4566-83e7-303c40ac9b3f_1024x559.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:559,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:95823,&quot;alt&quot;:&quot;A current token can attend to previous tokens but cannot attend to future tokens.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/203534783?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac0111e2-b21e-4566-83e7-303c40ac9b3f_1024x559.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="A current token can attend to previous tokens but cannot attend to future tokens." title="A current token can attend to previous tokens but cannot attend to future tokens." srcset="https://substackcdn.com/image/fetch/$s_!c4R5!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac0111e2-b21e-4566-83e7-303c40ac9b3f_1024x559.jpeg 424w, https://substackcdn.com/image/fetch/$s_!c4R5!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac0111e2-b21e-4566-83e7-303c40ac9b3f_1024x559.jpeg 848w, https://substackcdn.com/image/fetch/$s_!c4R5!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac0111e2-b21e-4566-83e7-303c40ac9b3f_1024x559.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!c4R5!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac0111e2-b21e-4566-83e7-303c40ac9b3f_1024x559.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><em>Figure 6.3. Causal attention prevents each position from attending to future tokens, preserving the left-to-right language-modeling rule.</em></p><h2>Comparing unmasked and masked attention weights</h2><p>The easiest way to see the issue is to start from the unmasked attention scores we already know how to compute. Using the query and key projections from the previous section, we can form the raw score matrix and normalize it with softmax.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;ab94e4fa-49ad-4640-88ac-a7ba0cc78569&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">queries = sa_v2.W_query(inputs)
keys = sa_v2.W_key(inputs)
attn_scores = queries @ keys.T
attn_weights = torch.softmax(attn_scores / keys.shape[-1]**0.5, dim=-1)
print(attn_weights)</code></pre></div><p>This distribution is perfectly valid as self-attention, but it still allows a token to assign probability mass to later tokens in the sequence. For language modeling, that is a problem. If the model can see the future, the training objective no longer matches the generation process.</p><p>A simple way to build intuition is to mask the already-normalized weights. A lower-triangular mask keeps the diagonal and everything below it, while zeroing out positions above the diagonal.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;53f359c9-17e7-42f3-94e6-e2901a13db9f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">context_length = attn_scores.shape[0]
mask_simple = torch.tril(torch.ones(context_length, context_length))
print(mask_simple)

masked_simple = attn_weights*mask_simple
print(masked_simple)

row_sums = masked_simple.sum(dim=-1, keepdim=True)
masked_simple_norm = masked_simple / row_sums
print(masked_simple_norm)</code></pre></div><p>This version makes the causal rule obvious. Future positions are removed, and the remaining probabilities are renormalized so each row again sums to one. The important point is that this does not leak information. Renormalizing after zeroing future positions is equivalent to computing a softmax over only the allowed tokens. The masked tokens contribute nothing to the output.</p><h2>Masking before softmax</h2><p>The renormalization trick is useful for understanding the mechanism, but it is not the cleanest way to implement causal attention. In practice, the common approach is to hide future positions before softmax by replacing their scores with negative infinity.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;c7479b1e-71d2-4680-b9c8-78363bf91ef4&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">mask = torch.triu(torch.ones(context_length, context_length), diagonal=1)
masked = attn_scores.masked_fill(mask.bool(), -torch.inf)
print(masked)

attn_weights = torch.softmax(masked / keys.shape[-1]**0.5, dim=1)
print(attn_weights)</code></pre></div><p>This works because softmax turns negative infinity into zero probability. The masked entries disappear automatically, so there is no need to compute weights for illegal positions and then renormalize them afterward. The implementation is also cleaner: the score matrix is altered once, and the softmax handles the rest.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!0ND7!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9c0045e-a12c-4a7f-81cc-987a943c25e4_1024x559.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!0ND7!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9c0045e-a12c-4a7f-81cc-987a943c25e4_1024x559.jpeg 424w, https://substackcdn.com/image/fetch/$s_!0ND7!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9c0045e-a12c-4a7f-81cc-987a943c25e4_1024x559.jpeg 848w, https://substackcdn.com/image/fetch/$s_!0ND7!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9c0045e-a12c-4a7f-81cc-987a943c25e4_1024x559.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!0ND7!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9c0045e-a12c-4a7f-81cc-987a943c25e4_1024x559.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!0ND7!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9c0045e-a12c-4a7f-81cc-987a943c25e4_1024x559.jpeg" width="1024" height="559" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e9c0045e-a12c-4a7f-81cc-987a943c25e4_1024x559.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:559,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:137219,&quot;alt&quot;:&quot;A causal mask replaces future-token scores with negative infinity before softmax, producing zero probability for masked positions.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/203534783?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9c0045e-a12c-4a7f-81cc-987a943c25e4_1024x559.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="A causal mask replaces future-token scores with negative infinity before softmax, producing zero probability for masked positions." title="A causal mask replaces future-token scores with negative infinity before softmax, producing zero probability for masked positions." srcset="https://substackcdn.com/image/fetch/$s_!0ND7!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9c0045e-a12c-4a7f-81cc-987a943c25e4_1024x559.jpeg 424w, https://substackcdn.com/image/fetch/$s_!0ND7!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9c0045e-a12c-4a7f-81cc-987a943c25e4_1024x559.jpeg 848w, https://substackcdn.com/image/fetch/$s_!0ND7!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9c0045e-a12c-4a7f-81cc-987a943c25e4_1024x559.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!0ND7!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9c0045e-a12c-4a7f-81cc-987a943c25e4_1024x559.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><em>Figure 6.4. Future-token scores are replaced with `-inf` before softmax, so forbidden positions become zero probability.</em></p><p>That efficiency matters inside a training loop. Causal masking is not a special case layered on top of attention after the fact. It is part of the attention computation itself, and the pre-softmax formulation expresses that directly.</p><h2>Dropout on attention weights</h2><p>Once the attention distribution is masked correctly, it is common to regularize it with dropout. The dropout is applied to the attention weights, not to the input embeddings. During training, some attention links are randomly removed, and the surviving weights are rescaled. This encourages the model not to depend too heavily on a small set of positions.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;eaeedbbc-6bd8-4c71-9824-92f93cd190f1&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">torch.manual_seed(123)
dropout = torch.nn.Dropout(0.5)
example = torch.ones(6, 6)
print(dropout(example))

torch.manual_seed(123)
print(dropout(attn_weights))</code></pre></div><p>On a tensor of ones, dropout shows its inverted-scaling behavior clearly. Some entries become zero, while the remaining entries increase so the expected value stays consistent. Applied to attention weights, the same mechanism randomly suppresses a subset of connections during training. At inference time, dropout is disabled, so the full attention pattern is used.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!3bbf!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf6236df-fc05-4330-9889-312f7e194159_1024x559.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!3bbf!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf6236df-fc05-4330-9889-312f7e194159_1024x559.jpeg 424w, https://substackcdn.com/image/fetch/$s_!3bbf!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf6236df-fc05-4330-9889-312f7e194159_1024x559.jpeg 848w, https://substackcdn.com/image/fetch/$s_!3bbf!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf6236df-fc05-4330-9889-312f7e194159_1024x559.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!3bbf!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf6236df-fc05-4330-9889-312f7e194159_1024x559.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!3bbf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf6236df-fc05-4330-9889-312f7e194159_1024x559.jpeg" width="1024" height="559" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/af6236df-fc05-4330-9889-312f7e194159_1024x559.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:559,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:102940,&quot;alt&quot;:&quot;Dropout randomly removes some attention links during training while the remaining links are rescaled.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/203534783?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf6236df-fc05-4330-9889-312f7e194159_1024x559.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Dropout randomly removes some attention links during training while the remaining links are rescaled." title="Dropout randomly removes some attention links during training while the remaining links are rescaled." srcset="https://substackcdn.com/image/fetch/$s_!3bbf!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf6236df-fc05-4330-9889-312f7e194159_1024x559.jpeg 424w, https://substackcdn.com/image/fetch/$s_!3bbf!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf6236df-fc05-4330-9889-312f7e194159_1024x559.jpeg 848w, https://substackcdn.com/image/fetch/$s_!3bbf!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf6236df-fc05-4330-9889-312f7e194159_1024x559.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!3bbf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf6236df-fc05-4330-9889-312f7e194159_1024x559.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><em>Figure 6.5. Dropout randomly removes some attention links during training, encouraging the model not to rely too heavily on a few positions.</em></p><h2>A batch-first CausalAttention module</h2><p>The next step is to package the whole computation into a reusable PyTorch module. For GPT-style training, the module should accept batch-first inputs shaped as <code>batch_size &#215; num_tokens &#215; d_in</code>. That means the attention code must work on a stack of sequences, not only on a single toy sentence.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;970023d9-54ca-48e9-88df-9edd817db77c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">batch = torch.stack((inputs, inputs), dim=0)
print(batch.shape)</code></pre></div><p>The batch dimension changes the tensor operations slightly, but the logic stays the same. Queries, keys, and values are still learned projections. The causal mask still prevents access to future positions. The main difference is that the score matrix now has a batch dimension in front of it, so all computations must preserve that leading axis.</p><p>A compact implementation captures the whole pattern.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;476384d6-a6fc-4cf2-a022-0a9c8e98ee79&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">class CausalAttention(nn.Module):
    def __init__(self, d_in, d_out, context_length,
                dropout, qkv_bias=False):
        super().__init__()
        self.d_out = d_out
        self.W_query = nn.Linear(d_in, d_out, bias=qkv_bias)
        self.W_key   = nn.Linear(d_in, d_out, bias=qkv_bias)
        self.W_value = nn.Linear(d_in, d_out, bias=qkv_bias)
        self.dropout = nn.Dropout(dropout)
        self.register_buffer(
           'mask',
           torch.triu(torch.ones(context_length, context_length),
           diagonal=1)
        )

    def forward(self, x):
        b, num_tokens, d_in = x.shape
        keys = self.W_key(x)
        queries = self.W_query(x)
        values = self.W_value(x)

        attn_scores = queries @ keys.transpose(1, 2)
        attn_scores.masked_fill_(
            self.mask.bool()[:num_tokens, :num_tokens], -torch.inf)
        attn_weights = torch.softmax(
            attn_scores / keys.shape[-1]**0.5, dim=-1
        )
        attn_weights = self.dropout(attn_weights)

        context_vec = attn_weights @ values
        return context_vec</code></pre></div><p>Several details are doing real work here. The three linear layers create trainable query, key, and value projections. The mask is stored with <code>register_buffer</code>, which keeps it attached to the module without making it a learnable parameter. That is useful because the mask should move with the model to the correct device, but it should not be updated by gradient descent.</p><p>The forward pass is shaped around batch-first tensors. If <code>x</code> has shape <code>b &#215; num_tokens &#215; d_in</code>, then the projected keys, queries, and values have shape <code>b &#215; num_tokens &#215; d_out</code>. The matrix multiply <code>queries @ keys.transpose(1, 2)</code> produces a batch of score matrices with shape <code>b &#215; num_tokens &#215; num_tokens</code>. The in-place <code>masked_fill_</code> call applies the causal restriction only to the visible portion of the mask, trimmed to the current sequence length. After that, softmax produces causal attention weights, dropout regularizes those weights, and the final matrix multiply returns context vectors with shape <code>b &#215; num_tokens &#215; d_out</code>.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!G_Aw!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd94fe9df-e9d7-45a2-90a1-29d948e948c1_1024x559.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!G_Aw!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd94fe9df-e9d7-45a2-90a1-29d948e948c1_1024x559.jpeg 424w, https://substackcdn.com/image/fetch/$s_!G_Aw!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd94fe9df-e9d7-45a2-90a1-29d948e948c1_1024x559.jpeg 848w, https://substackcdn.com/image/fetch/$s_!G_Aw!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd94fe9df-e9d7-45a2-90a1-29d948e948c1_1024x559.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!G_Aw!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd94fe9df-e9d7-45a2-90a1-29d948e948c1_1024x559.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!G_Aw!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd94fe9df-e9d7-45a2-90a1-29d948e948c1_1024x559.jpeg" width="1024" height="559" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d94fe9df-e9d7-45a2-90a1-29d948e948c1_1024x559.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:559,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:94730,&quot;alt&quot;:&quot;The batch-first causal attention module projects inputs, computes masked attention scores, and returns context vectors.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/203534783?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd94fe9df-e9d7-45a2-90a1-29d948e948c1_1024x559.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="The batch-first causal attention module projects inputs, computes masked attention scores, and returns context vectors." title="The batch-first causal attention module projects inputs, computes masked attention scores, and returns context vectors." srcset="https://substackcdn.com/image/fetch/$s_!G_Aw!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd94fe9df-e9d7-45a2-90a1-29d948e948c1_1024x559.jpeg 424w, https://substackcdn.com/image/fetch/$s_!G_Aw!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd94fe9df-e9d7-45a2-90a1-29d948e948c1_1024x559.jpeg 848w, https://substackcdn.com/image/fetch/$s_!G_Aw!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd94fe9df-e9d7-45a2-90a1-29d948e948c1_1024x559.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!G_Aw!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd94fe9df-e9d7-45a2-90a1-29d948e948c1_1024x559.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><em>Figure 6.6. The batch-first causal attention module preserves the batch and token dimensions while projecting the final feature width to `d_out`.</em></p><p>That shape flow is the key sanity check. Each token still receives a context vector, but now the whole operation works on a batch of sequences in one pass.</p><h2>Sanity-checking the batched module</h2><p>A small test with zero dropout confirms that the module preserves the expected batch and sequence dimensions.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;bcb51a3e-a8b7-4de6-a9f9-e7e0c9bb3d49&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">torch.manual_seed(123)
context_length = batch.shape[1]
ca = CausalAttention(d_in, d_out, context_length, 0.0)
context_vecs = ca(batch)
print("context_vecs.shape:", context_vecs.shape)</code></pre></div><p>The result should match the batch size and token count of the input, with the projected output width on the last dimension. That is the exact interface needed for the next step. Multi-head attention will reuse the same causal machinery, but it will distribute the work across several attention heads instead of one.</p><h2>Why causal attention must accept batches</h2><p>Causal attention becomes practical only when it can operate on minibatches, because training code rarely feeds one sequence at a time. A data loader usually assembles tensors with a leading batch dimension, so the attention module has to accept inputs shaped as batch &#215; tokens &#215; embedding. That layout keeps each token sequence intact while letting the optimizer process several examples in parallel.</p><p>The autoregressive rule does not change just because a batch dimension appears. Each sequence still needs the same causal constraint, and the masking logic must apply independently to every item in the batch. The main task here is therefore dimensional, not conceptual: the single-sequence attention block must be rewritten so that its tensor operations remain valid for stacked inputs.</p><h2>Tracing shapes through a batched forward pass</h2><p>Start with an input tensor x shaped as batch &#215; tokens &#215; d<em>in, where batch is the number of examples, tokens is the sequence length, and d</em>in is the input embedding size. The learned query, key, and value projections act on that 3D tensor and return three new tensors with shape batch &#215; tokens &#215; d_out. The projections still happen token by token, but the linear layers apply them across all batch elements at once.</p><p>The attention scores are computed by multiplying the query tensor with the transposed key tensor over the last two dimensions. With batched input, that produces scores shaped as batch &#215; tokens &#215; tokens. Each score matrix describes one example in the batch, and each row tells how strongly a token attends to earlier and later positions in that same sequence. After masking, scaling, softmax, and dropout, the attention weights are applied to the value tensor, yielding context vectors with shape batch &#215; tokens &#215; d_out.</p><h2>A batch-ready CausalAttention module</h2><p>The reusable module keeps the same basic ingredients as the single-sequence version: three linear projections, a causal mask, and dropout on the attention weights. The forward method now expects a batch-first tensor and uses a batched matrix multiply for the score computation. The key point is that nothing about the autoregressive rule changes when the input gains a batch axis.</p><p>The causal mask should live inside the module as registered state. Using register_buffer stores the mask without turning it into a learnable parameter, and it ensures the mask moves with the module when the model is transferred or saved. Because a minibatch may contain sequences shorter than the configured context length, the mask is sliced to the current token count before it is applied. That keeps the code safe for shorter inputs and prevents the module from touching positions that are not part of the active sequence.</p><p>The masking itself remains efficient. Future positions are filled with negative infinity before the softmax, so the forbidden entries become zero probability automatically. This is done in place with masked<em>fill</em>, which updates the score tensor directly. The softmax then normalizes only the allowed positions, and dropout randomly removes some of the resulting attention weights during training. That dropout acts on the batched attention matrix, not on the tokens themselves, so it regularizes which token-to-token links survive. The final context vectors come from the weighted sum of the values.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;8851ac47-2121-47ad-8645-9e65a39ade33&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">class CausalAttention(nn.Module):
    def __init__(self, d_in, d_out, context_length,
                dropout, qkv_bias=False):
        super().__init__()
        self.d_out = d_out
        self.W_query = nn.Linear(d_in, d_out, bias=qkv_bias)
        self.W_key   = nn.Linear(d_in, d_out, bias=qkv_bias)
        self.W_value = nn.Linear(d_in, d_out, bias=qkv_bias)
        self.dropout = nn.Dropout(dropout)
        self.register_buffer(
           'mask',
           torch.triu(torch.ones(context_length, context_length),
           diagonal=1)
        )

    def forward(self, x):
        b, num_tokens, d_in = x.shape
        keys = self.W_key(x)
        queries = self.W_query(x)
        values = self.W_value(x)

        attn_scores = queries @ keys.transpose(1, 2)
        attn_scores.masked_fill_(
            self.mask.bool()[:num_tokens, :num_tokens], -torch.inf)
        attn_weights = torch.softmax(
            attn_scores / keys.shape[-1]**0.5, dim=-1
        )
        attn_weights = self.dropout(attn_weights)

        context_vec = attn_weights @ values
        return context_vec</code></pre></div><h2>Sanity-checking the batched output</h2><p>A tiny stacked batch is enough to verify that the layer is batch-ready. If two identical sequences are stacked along the leading axis, the module should return one context tensor per example and preserve the batch-first layout from input to output.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;e04ae5d2-d831-4d32-b30d-428caaf7f6e1&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">torch.manual_seed(123)
batch = torch.stack((inputs, inputs), dim=0)
print(batch.shape)

context_length = batch.shape[1]
ca = CausalAttention(d_in, d_out, context_length, 0.0)
context_vecs = ca(batch)
print("context_vecs.shape:", context_vecs.shape)</code></pre></div><p>If the input batch has shape 2 &#215; tokens &#215; d<em>in, the output should have shape 2 &#215; tokens &#215; d</em>out. That is the contract the next GPT components depend on, and it confirms that the causal mask still works correctly after the module is extended from a single sequence to minibatched training.</p><h2>Why multiple heads help</h2><p>A single attention head can only build one set of token interactions at a time in its own learned subspace. Multi-head attention increases what the model can represent by running several attention heads in parallel, each with its own projection of the same sequence. One head may emphasize local dependencies while another captures a broader relationship, but the useful idea is more general than any particular interpretation: the model can organize information in several complementary ways without changing the causal next-token setup used by GPT-style decoders.</p><p>The key point is that this extra capacity comes from structure, not from changing the workflow. The model still reads tokens left to right, still applies a causal mask, and still produces one context vector per position. Multi-head attention simply repeats the attention computation across multiple learned subspaces and then combines the results into one output representation.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!snkv!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fec1e0ef0-ad00-491f-b368-1ca743d587c2_1024x559.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!snkv!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fec1e0ef0-ad00-491f-b368-1ca743d587c2_1024x559.jpeg 424w, https://substackcdn.com/image/fetch/$s_!snkv!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fec1e0ef0-ad00-491f-b368-1ca743d587c2_1024x559.jpeg 848w, https://substackcdn.com/image/fetch/$s_!snkv!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fec1e0ef0-ad00-491f-b368-1ca743d587c2_1024x559.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!snkv!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fec1e0ef0-ad00-491f-b368-1ca743d587c2_1024x559.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!snkv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fec1e0ef0-ad00-491f-b368-1ca743d587c2_1024x559.jpeg" width="1024" height="559" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ec1e0ef0-ad00-491f-b368-1ca743d587c2_1024x559.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:559,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:95378,&quot;alt&quot;:&quot;Several attention heads process the same sequence in parallel and then merge their outputs.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/203534783?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fec1e0ef0-ad00-491f-b368-1ca743d587c2_1024x559.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Several attention heads process the same sequence in parallel and then merge their outputs." title="Several attention heads process the same sequence in parallel and then merge their outputs." srcset="https://substackcdn.com/image/fetch/$s_!snkv!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fec1e0ef0-ad00-491f-b368-1ca743d587c2_1024x559.jpeg 424w, https://substackcdn.com/image/fetch/$s_!snkv!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fec1e0ef0-ad00-491f-b368-1ca743d587c2_1024x559.jpeg 848w, https://substackcdn.com/image/fetch/$s_!snkv!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fec1e0ef0-ad00-491f-b368-1ca743d587c2_1024x559.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!snkv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fec1e0ef0-ad00-491f-b368-1ca743d587c2_1024x559.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><em>Figure 6.7. Multi-head attention runs several attention heads in parallel so the model can represent complementary token relationships.</em></p><h2>A simple wrapper over causal attention heads</h2><p>The most direct way to build multi-head attention is to reuse the single-head causal attention module several times and concatenate the results. This wrapper is not the most efficient implementation, but it is a useful stepping stone because it makes the idea of independent heads completely explicit.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;a08f1d19-6eac-4adf-a8f5-3d29960ff5ed&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">class MultiHeadAttentionWrapper(nn.Module):
    def __init__(self, d_in, d_out, context_length,
                 dropout, num_heads, qkv_bias=False):
        super().__init__()
        self.heads = nn.ModuleList(
            [CausalAttention(
                 d_in, d_out, context_length, dropout, qkv_bias
             ) 
             for _ in range(num_heads)]
        )

    def forward(self, x):
        return torch.cat([head(x) for head in self.heads], dim=-1)</code></pre></div><p>If <code>x</code> has shape <code>[batch, tokens, d_in]</code> and each causal head returns <code>[batch, tokens, d_out]</code>, then concatenating <code>num_heads</code> outputs produces <code>[batch, tokens, num_heads * d_out]</code>. For example, two heads that each produce a width of 2 give a final width of 4. That makes the wrapper easy to understand, but it also shows why it is only a teaching version: every head repeats the full attention calculation separately.</p><h2>An efficient fused MultiHeadAttention module</h2><p>The implementation used in GPT-style models keeps the same idea of multiple heads, but it performs the projection work more efficiently. Instead of creating separate attention modules, it computes one set of queries, keys, and values, then splits those projected features into heads internally. This is the version worth implementing in practice.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;541ba549-1b23-43fe-9b0c-ed1d9817ddf6&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">class MultiHeadAttention(nn.Module):
    def __init__(self, d_in, d_out, 
                 context_length, dropout, num_heads, qkv_bias=False):
        super().__init__()
        assert (d_out % num_heads == 0), \
            "d_out must be divisible by num_heads"

        self.d_out = d_out
        self.num_heads = num_heads
        self.head_dim = d_out // num_heads
        self.W_query = nn.Linear(d_in, d_out, bias=qkv_bias)
        self.W_key = nn.Linear(d_in, d_out, bias=qkv_bias)
        self.W_value = nn.Linear(d_in, d_out, bias=qkv_bias)
        self.out_proj = nn.Linear(d_out, d_out)
        self.dropout = nn.Dropout(dropout)
        self.register_buffer(
            "mask",
            torch.triu(torch.ones(context_length, context_length),
                       diagonal=1)
        )

    def forward(self, x):
        b, num_tokens, d_in = x.shape
        keys = self.W_key(x)
        queries = self.W_query(x)
        values = self.W_value(x)

        keys = keys.view(b, num_tokens, self.num_heads, self.head_dim)
        values = values.view(b, num_tokens, self.num_heads, self.head_dim)
        queries = queries.view(b, num_tokens, self.num_heads, self.head_dim)

        keys = keys.transpose(1, 2)
        queries = queries.transpose(1, 2)
        values = values.transpose(1, 2)

        attn_scores = queries @ keys.transpose(2, 3)
        mask_bool = self.mask.bool()[:num_tokens, :num_tokens]
        attn_scores.masked_fill_(mask_bool, -torch.inf)

        attn_weights = torch.softmax(attn_scores / keys.shape[-1]**0.5, dim=-1)
        attn_weights = self.dropout(attn_weights)

        context_vec = (attn_weights @ values).transpose(1, 2)
        context_vec = context_vec.contiguous().view(b, num_tokens, self.d_out)
        context_vec = self.out_proj(context_vec)
        return context_vec</code></pre></div><p>The divisibility check matters because the output width is partitioned across heads. If <code>d_out = 12</code> and <code>num_heads = 3</code>, then each head gets <code>head_dim = 4</code>. That means the model represents each token in three parallel four-dimensional subspaces and then recombines them into one 12-dimensional output.</p><p>The shape mechanics are the main idea to keep in mind. After the three linear projections, <code>queries</code>, <code>keys</code>, and <code>values</code> each have shape <code>[batch, tokens, d_out]</code>. The <code>view</code> call reshapes them to <code>[batch, tokens, num_heads, head_dim]</code>, and <code>transpose(1, 2)</code> moves the head axis forward so the tensors become <code>[batch, num_heads, tokens, head_dim]</code>. With that arrangement, a single batched matrix multiplication computes attention scores for all heads at once, giving <code>[batch, num_heads, tokens, tokens]</code>.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!BfGG!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F27bda695-dcd3-4b5a-9310-cd8d64fee9db_1024x559.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!BfGG!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F27bda695-dcd3-4b5a-9310-cd8d64fee9db_1024x559.jpeg 424w, https://substackcdn.com/image/fetch/$s_!BfGG!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F27bda695-dcd3-4b5a-9310-cd8d64fee9db_1024x559.jpeg 848w, https://substackcdn.com/image/fetch/$s_!BfGG!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F27bda695-dcd3-4b5a-9310-cd8d64fee9db_1024x559.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!BfGG!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F27bda695-dcd3-4b5a-9310-cd8d64fee9db_1024x559.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!BfGG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F27bda695-dcd3-4b5a-9310-cd8d64fee9db_1024x559.jpeg" width="1024" height="559" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/27bda695-dcd3-4b5a-9310-cd8d64fee9db_1024x559.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:559,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:134110,&quot;alt&quot;:&quot;A fused multi-head attention module projects once, splits into heads, computes attention in parallel, and merges the heads.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/203534783?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F27bda695-dcd3-4b5a-9310-cd8d64fee9db_1024x559.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="A fused multi-head attention module projects once, splits into heads, computes attention in parallel, and merges the heads." title="A fused multi-head attention module projects once, splits into heads, computes attention in parallel, and merges the heads." srcset="https://substackcdn.com/image/fetch/$s_!BfGG!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F27bda695-dcd3-4b5a-9310-cd8d64fee9db_1024x559.jpeg 424w, https://substackcdn.com/image/fetch/$s_!BfGG!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F27bda695-dcd3-4b5a-9310-cd8d64fee9db_1024x559.jpeg 848w, https://substackcdn.com/image/fetch/$s_!BfGG!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F27bda695-dcd3-4b5a-9310-cd8d64fee9db_1024x559.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!BfGG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F27bda695-dcd3-4b5a-9310-cd8d64fee9db_1024x559.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><em>Figure 6.8. The fused multi-head module projects once, splits the projected features into heads, computes attention in parallel, and merges the heads back together.</em></p><p>Causal masking is applied before softmax so future tokens cannot receive attention. The upper-triangular mask is stored once and sliced to the current sequence length, which makes the module work for shorter sequences as well. After masking, the scores are scaled by the square root of the head dimension, softmax converts them into attention weights, and dropout regularizes the weights before they are used to mix the value vectors.</p><p>The remaining steps restore the token-major layout. Multiplying the attention weights by <code>values</code> yields per-head context vectors with shape <code>[batch, num_heads, tokens, head_dim]</code>. Transposing back and calling <code>contiguous().view(b, num_tokens, self.d_out)</code> merges the heads into one representation per token, and the output projection mixes information across heads before the result leaves the attention block.</p><h2>Shape trace and GPT-scale perspective</h2><p>A compact shape trace helps connect the code to the underlying mechanism. Start with <code>x</code> shaped <code>[b, t, d_in]</code>. After the Q, K, and V projections, each tensor is <code>[b, t, d_out]</code>. After reshaping and transposing, the tensors become <code>[b, h, t, head_dim]</code>, where <code>h = num_heads</code> and <code>head_dim = d_out / h</code>. The attention scores are <code>[b, h, t, t]</code>, the per-head context vectors are <code>[b, h, t, head_dim]</code>, and the final merge returns <code>[b, t, d_out]</code>.</p><p>This also clarifies the difference between the wrapper and the fused module. The wrapper says &#8220;run several heads, then concatenate their outputs.&#8221; The fused module says &#8220;project once, split into heads, compute all heads in parallel, and project back.&#8221; That second design is the one used in GPT-style implementations.</p><p>A GPT-2-scale example makes the partitioning concrete. With 12 heads and a 768-dimensional embedding space, each head operates on 64 features because <code>768 / 12 = 64</code>. The module still returns a 768-dimensional vector at every token position, but it reaches that width by combining many smaller attention computations rather than by making one large head do everything.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!BXzy!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13f0bd5e-6dcc-417a-a0b7-04788daf7174_1024x559.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!BXzy!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13f0bd5e-6dcc-417a-a0b7-04788daf7174_1024x559.jpeg 424w, https://substackcdn.com/image/fetch/$s_!BXzy!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13f0bd5e-6dcc-417a-a0b7-04788daf7174_1024x559.jpeg 848w, https://substackcdn.com/image/fetch/$s_!BXzy!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13f0bd5e-6dcc-417a-a0b7-04788daf7174_1024x559.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!BXzy!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13f0bd5e-6dcc-417a-a0b7-04788daf7174_1024x559.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!BXzy!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13f0bd5e-6dcc-417a-a0b7-04788daf7174_1024x559.jpeg" width="1024" height="559" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/13f0bd5e-6dcc-417a-a0b7-04788daf7174_1024x559.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:559,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:112344,&quot;alt&quot;:&quot;A 768-dimensional representation is divided across 12 attention heads, each with 64 features, then merged back together.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/203534783?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13f0bd5e-6dcc-417a-a0b7-04788daf7174_1024x559.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="A 768-dimensional representation is divided across 12 attention heads, each with 64 features, then merged back together." title="A 768-dimensional representation is divided across 12 attention heads, each with 64 features, then merged back together." srcset="https://substackcdn.com/image/fetch/$s_!BXzy!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13f0bd5e-6dcc-417a-a0b7-04788daf7174_1024x559.jpeg 424w, https://substackcdn.com/image/fetch/$s_!BXzy!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13f0bd5e-6dcc-417a-a0b7-04788daf7174_1024x559.jpeg 848w, https://substackcdn.com/image/fetch/$s_!BXzy!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13f0bd5e-6dcc-417a-a0b7-04788daf7174_1024x559.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!BXzy!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13f0bd5e-6dcc-417a-a0b7-04788daf7174_1024x559.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><em>Figure 6.9. A GPT-style attention layer keeps the full embedding width but divides it across multiple smaller heads.</em></p><p><em>Use the url below to download it:</em></p>
      <p>
          <a href="https://onepagecode.substack.com/p/trainable-causal-attention-and-multi">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Attention mechanisms: from intuition to vectorized self-attention]]></title><description><![CDATA[Chapter 5: A deep dive into attention mechanisms, dot-product calculations, and PyTorch vectorization for transformer architectures]]></description><link>https://onepagecode.substack.com/p/attention-mechanisms-from-intuition</link><guid isPermaLink="false">https://onepagecode.substack.com/p/attention-mechanisms-from-intuition</guid><dc:creator><![CDATA[Onepagecode]]></dc:creator><pubDate>Mon, 29 Jun 2026 20:16:18 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!T1jd!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F55ed1f2c-5ab4-4d86-9621-4b4319359974_2816x1536.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Use the URL at the end of this article to download entire book in EPUB or PDF</h2><h3>The bottleneck in fixed recurrent summaries</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!T1jd!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F55ed1f2c-5ab4-4d86-9621-4b4319359974_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!T1jd!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F55ed1f2c-5ab4-4d86-9621-4b4319359974_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!T1jd!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F55ed1f2c-5ab4-4d86-9621-4b4319359974_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!T1jd!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F55ed1f2c-5ab4-4d86-9621-4b4319359974_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!T1jd!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F55ed1f2c-5ab4-4d86-9621-4b4319359974_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!T1jd!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F55ed1f2c-5ab4-4d86-9621-4b4319359974_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/55ed1f2c-5ab4-4d86-9621-4b4319359974_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:4364730,&quot;alt&quot;:&quot;A recurrent encoder compressing many input tokens into one fixed-size hidden state vector.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/203532650?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F55ed1f2c-5ab4-4d86-9621-4b4319359974_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="A recurrent encoder compressing many input tokens into one fixed-size hidden state vector." title="A recurrent encoder compressing many input tokens into one fixed-size hidden state vector." srcset="https://substackcdn.com/image/fetch/$s_!T1jd!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F55ed1f2c-5ab4-4d86-9621-4b4319359974_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!T1jd!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F55ed1f2c-5ab4-4d86-9621-4b4319359974_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!T1jd!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F55ed1f2c-5ab4-4d86-9621-4b4319359974_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!T1jd!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F55ed1f2c-5ab4-4d86-9621-4b4319359974_2816x1536.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><em>Figure 5.1. A fixed recurrent encoder compresses the whole source sequence into one hidden state, creating an information bottleneck.</em></p><p>A classic sequence-to-sequence model reads an input sentence one token at a time and folds everything it has seen into a single hidden state. That design is elegant, but it creates an immediate bottleneck: the entire source sequence must be squeezed through one fixed-size vector before the decoder can begin producing output. For short inputs, that summary may be good enough. For longer inputs, it becomes a weak approximation of the full sequence.</p><p>The problem is not merely that the vector is compact. It is that compression is irreversible. As the encoder moves forward, some details are emphasized and others are blurred away. A subject mentioned near the beginning, a negation appearing in the middle, or a phrase that determines agreement at the end can all become harder to recover once they are folded into the same state. In practice, the further apart two pieces of information are, the less reliable that single summary becomes as a carrier of their relationship.</p><h3>Selective access instead of one compressed memory</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!2qVf!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e6058bd-4ef1-4029-ac7b-1c3d3ffd5012_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!2qVf!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e6058bd-4ef1-4029-ac7b-1c3d3ffd5012_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!2qVf!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e6058bd-4ef1-4029-ac7b-1c3d3ffd5012_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!2qVf!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e6058bd-4ef1-4029-ac7b-1c3d3ffd5012_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!2qVf!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e6058bd-4ef1-4029-ac7b-1c3d3ffd5012_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!2qVf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e6058bd-4ef1-4029-ac7b-1c3d3ffd5012_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9e6058bd-4ef1-4029-ac7b-1c3d3ffd5012_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:4250122,&quot;alt&quot;:&quot;A decoder attending to different source tokens at different output steps.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/203532650?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e6058bd-4ef1-4029-ac7b-1c3d3ffd5012_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="A decoder attending to different source tokens at different output steps." title="A decoder attending to different source tokens at different output steps." srcset="https://substackcdn.com/image/fetch/$s_!2qVf!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e6058bd-4ef1-4029-ac7b-1c3d3ffd5012_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!2qVf!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e6058bd-4ef1-4029-ac7b-1c3d3ffd5012_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!2qVf!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e6058bd-4ef1-4029-ac7b-1c3d3ffd5012_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!2qVf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e6058bd-4ef1-4029-ac7b-1c3d3ffd5012_2816x1536.png 1456w" sizes="100vw"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><em>Figure 5.2. Attention lets the decoder retrieve different parts of the source sequence depending on the current output step.</em></p><p>Attention was introduced to break that restriction. Rather than forcing the model to rely on one memory vector, attention lets the decoder consult the source sequence directly. At each output step, it can decide which parts of the input matter most and retrieve a weighted combination of them. The key shift is from one global summary to selective access.</p><p>That change matters because different decoding decisions often depend on different source tokens. When translating a sentence, one output word may depend on an early noun, while the next word may depend on a later verb or modifier. With attention, the model does not have to guess that all relevant information has already been packed into one hidden state. It can look back, choose what is useful, and form a context-sensitive representation on demand.</p><h3>From attention to self-attention</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!U6iZ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee439461-28f2-435b-832c-9981f4bc58c0_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!U6iZ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee439461-28f2-435b-832c-9981f4bc58c0_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!U6iZ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee439461-28f2-435b-832c-9981f4bc58c0_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!U6iZ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee439461-28f2-435b-832c-9981f4bc58c0_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!U6iZ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee439461-28f2-435b-832c-9981f4bc58c0_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!U6iZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee439461-28f2-435b-832c-9981f4bc58c0_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ee439461-28f2-435b-832c-9981f4bc58c0_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:4563698,&quot;alt&quot;:&quot;Tokens in the same sequence connected to one another through self-attention.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/203532650?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee439461-28f2-435b-832c-9981f4bc58c0_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Tokens in the same sequence connected to one another through self-attention." title="Tokens in the same sequence connected to one another through self-attention." srcset="https://substackcdn.com/image/fetch/$s_!U6iZ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee439461-28f2-435b-832c-9981f4bc58c0_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!U6iZ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee439461-28f2-435b-832c-9981f4bc58c0_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!U6iZ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee439461-28f2-435b-832c-9981f4bc58c0_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!U6iZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee439461-28f2-435b-832c-9981f4bc58c0_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><em>Figure 5.3. In self-attention, each token can communicate directly with other tokens in the same sequence.</em></p><p>Self-attention takes this idea one step further. Instead of having a decoder attend to an external encoder state, each token in a sequence attends to other tokens in the same sequence. The sequence therefore becomes its own memory. Every position can gather information from earlier positions, later positions, or both, depending on the architecture being used.</p><p>This is the viewpoint that underlies transformer models and, by extension, GPT-style language models. The model no longer depends on recurrence to carry information forward token by token. It computes relationships directly among tokens, then uses those relationships to build representations that reflect the full surrounding context.</p><h3>Toy comparison without recurrence</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!et0N!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb39ed75d-dd01-4d4a-95d3-b76e7c3d10d3_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!et0N!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb39ed75d-dd01-4d4a-95d3-b76e7c3d10d3_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!et0N!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb39ed75d-dd01-4d4a-95d3-b76e7c3d10d3_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!et0N!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb39ed75d-dd01-4d4a-95d3-b76e7c3d10d3_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!et0N!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb39ed75d-dd01-4d4a-95d3-b76e7c3d10d3_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!et0N!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb39ed75d-dd01-4d4a-95d3-b76e7c3d10d3_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b39ed75d-dd01-4d4a-95d3-b76e7c3d10d3_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:4410085,&quot;alt&quot;:&quot;The word moved attends back to robot rather than the closer word engineer.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/203532650?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb39ed75d-dd01-4d4a-95d3-b76e7c3d10d3_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="The word moved attends back to robot rather than the closer word engineer." title="The word moved attends back to robot rather than the closer word engineer." srcset="https://substackcdn.com/image/fetch/$s_!et0N!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb39ed75d-dd01-4d4a-95d3-b76e7c3d10d3_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!et0N!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb39ed75d-dd01-4d4a-95d3-b76e7c3d10d3_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!et0N!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb39ed75d-dd01-4d4a-95d3-b76e7c3d10d3_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!et0N!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb39ed75d-dd01-4d4a-95d3-b76e7c3d10d3_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><em>Figure 5.4. Self-attention can directly recover the long-range relationship between the verb `moved` and the true subject `robot`.</em></p><p>Consider a short sentence such as &#8220;the robot that the engineer repaired moved.&#8221; If the model predicts the verb form for moved, it benefits from knowing that the true subject is robot, not engineer. A recurrent model has to preserve that dependency through the hidden state as it processes each intervening word. A self-attention model can instead let the final token look back directly to robot and assign it the greatest relevance, even if several words separate the two.</p><p>That direct access is the essential appeal of attention. It replaces fragile compression with token-to-token communication, which is exactly why the rest of this chapter focuses on building self-attention in a vectorized way before moving on to the full GPT architecture.</p><h2>Single-token self-attention by hand</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!1ARn!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42c8653d-fafb-45df-a2be-b8487858c0a6_2816x1536.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!1ARn!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42c8653d-fafb-45df-a2be-b8487858c0a6_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!1ARn!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42c8653d-fafb-45df-a2be-b8487858c0a6_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!1ARn!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42c8653d-fafb-45df-a2be-b8487858c0a6_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!1ARn!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42c8653d-fafb-45df-a2be-b8487858c0a6_2816x1536.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!1ARn!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42c8653d-fafb-45df-a2be-b8487858c0a6_2816x1536.png" width="1456" height="794" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/42c8653d-fafb-45df-a2be-b8487858c0a6_2816x1536.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:794,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:4957649,&quot;alt&quot;:&quot;A query token flows through dot products, softmax, weighted sum, and becomes a context vector.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/203532650?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42c8653d-fafb-45df-a2be-b8487858c0a6_2816x1536.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="A query token flows through dot products, softmax, weighted sum, and becomes a context vector." title="A query token flows through dot products, softmax, weighted sum, and becomes a context vector." srcset="https://substackcdn.com/image/fetch/$s_!1ARn!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42c8653d-fafb-45df-a2be-b8487858c0a6_2816x1536.png 424w, https://substackcdn.com/image/fetch/$s_!1ARn!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42c8653d-fafb-45df-a2be-b8487858c0a6_2816x1536.png 848w, https://substackcdn.com/image/fetch/$s_!1ARn!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42c8653d-fafb-45df-a2be-b8487858c0a6_2816x1536.png 1272w, https://substackcdn.com/image/fetch/$s_!1ARn!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42c8653d-fafb-45df-a2be-b8487858c0a6_2816x1536.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><em>Figure 5.5. Single-token self-attention computes similarity scores, normalizes them into attention weights, and uses those weights to build a context vector.</em></p><h3>A token can look back without recurrence</h3><p>Self-attention lets one token gather information from the rest of the sequence in a single computation. That is different from a recurrent approach, where information must survive a chain of updates before a later token can use it. Here, the token does not wait for a hidden state to carry context forward. It compares itself directly with the other tokens, decides which ones matter most, and then builds a new representation from that decision.</p><p>In this worked example, the second token is the query token. That means we will ask how strongly that token relates to every token in the input sequence. The provided PyTorch snippets are the calculation scaffold for the whole walkthrough: one snippet defines the toy input tensor, another computes the raw scores, others show normalization, and the final one builds the context vector. Reading the code alongside the prose makes each arithmetic step easy to trace.</p><h3>Scores from dot-product similarity</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!A4-_!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb5ee089f-73db-454c-a310-b46e5a96dcab_1024x559.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!A4-_!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb5ee089f-73db-454c-a310-b46e5a96dcab_1024x559.jpeg 424w, https://substackcdn.com/image/fetch/$s_!A4-_!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb5ee089f-73db-454c-a310-b46e5a96dcab_1024x559.jpeg 848w, https://substackcdn.com/image/fetch/$s_!A4-_!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb5ee089f-73db-454c-a310-b46e5a96dcab_1024x559.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!A4-_!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb5ee089f-73db-454c-a310-b46e5a96dcab_1024x559.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!A4-_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb5ee089f-73db-454c-a310-b46e5a96dcab_1024x559.jpeg" width="1024" height="559" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b5ee089f-73db-454c-a310-b46e5a96dcab_1024x559.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:559,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:110453,&quot;alt&quot;:&quot;Two token embedding vectors are multiplied element by element and summed into one attention score.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/203532650?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb5ee089f-73db-454c-a310-b46e5a96dcab_1024x559.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Two token embedding vectors are multiplied element by element and summed into one attention score." title="Two token embedding vectors are multiplied element by element and summed into one attention score." srcset="https://substackcdn.com/image/fetch/$s_!A4-_!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb5ee089f-73db-454c-a310-b46e5a96dcab_1024x559.jpeg 424w, https://substackcdn.com/image/fetch/$s_!A4-_!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb5ee089f-73db-454c-a310-b46e5a96dcab_1024x559.jpeg 848w, https://substackcdn.com/image/fetch/$s_!A4-_!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb5ee089f-73db-454c-a310-b46e5a96dcab_1024x559.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!A4-_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb5ee089f-73db-454c-a310-b46e5a96dcab_1024x559.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><em>Figure 5.7. A dot product compares two token vectors and produces one scalar similarity score.</em></p><p>The same small input tensor is used throughout the example so that every number can be checked by hand.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;8b147c87-5d82-4350-b74f-aa35af5ac388&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">import torch
inputs = torch.tensor(
  [[0.43, 0.15, 0.89], # Your     (x^1)
   [0.55, 0.87, 0.66], # journey  (x^2)
   [0.57, 0.85, 0.64], # starts   (x^3)
   [0.22, 0.58, 0.33], # with     (x^4)
   [0.77, 0.25, 0.10], # one      (x^5)
   [0.05, 0.80, 0.55]] # step     (x^6)
)</code></pre></div><p>The query token is <code>inputs[1]</code>, the vector for &#8220;journey.&#8221; In single-token self-attention, that token is the reference point for the entire calculation. Every other input vector is compared with it, including the query token itself.</p><p>The comparison uses dot products. A dot product produces one scalar score for each pair of vectors, and that score acts as a measure of alignment. If two vectors point in similar directions, the score is larger; if they point in less similar directions, the score is smaller.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;3bc0b62d-69f9-4732-b847-3796f4069dd6&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">query = inputs[1]                            #1
attn_scores_2 = torch.empty(inputs.shape[0])
for i, x_i in enumerate(inputs):
    attn_scores_2[i] = torch.dot(x_i, query)
print(attn_scores_2)</code></pre></div><p>For the first and second tokens, the dot product can also be unpacked into elementwise multiplication followed by summation. That tiny check is useful because it shows exactly what <code>torch.dot</code> is doing behind the scenes.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;5e4e26ca-ca9c-42dd-97ba-8515afd2dab5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">res = 0.
for idx, element in enumerate(inputs[0]):
    res += inputs[0][idx] * query[idx]
print(res)
print(torch.dot(inputs[0], query))</code></pre></div><p>This gives the raw attention scores. They are informative, but they are not yet usable as weights. A score can be positive, negative, small, or large, and nothing in the raw values guarantees that they describe a sensible share of importance across the sequence. Before the scores can be used to combine token vectors, they need to be normalized.</p><h3>From raw scores to attention weights</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!s3P8!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F81ccbffe-83bf-44c5-89cc-68bd39bc7bae_1024x559.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!s3P8!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F81ccbffe-83bf-44c5-89cc-68bd39bc7bae_1024x559.jpeg 424w, https://substackcdn.com/image/fetch/$s_!s3P8!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F81ccbffe-83bf-44c5-89cc-68bd39bc7bae_1024x559.jpeg 848w, https://substackcdn.com/image/fetch/$s_!s3P8!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F81ccbffe-83bf-44c5-89cc-68bd39bc7bae_1024x559.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!s3P8!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F81ccbffe-83bf-44c5-89cc-68bd39bc7bae_1024x559.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!s3P8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F81ccbffe-83bf-44c5-89cc-68bd39bc7bae_1024x559.jpeg" width="1024" height="559" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/81ccbffe-83bf-44c5-89cc-68bd39bc7bae_1024x559.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:559,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:85001,&quot;alt&quot;:&quot;Raw attention scores pass through softmax and become positive attention weights shown as bars.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/203532650?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F81ccbffe-83bf-44c5-89cc-68bd39bc7bae_1024x559.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Raw attention scores pass through softmax and become positive attention weights shown as bars." title="Raw attention scores pass through softmax and become positive attention weights shown as bars." srcset="https://substackcdn.com/image/fetch/$s_!s3P8!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F81ccbffe-83bf-44c5-89cc-68bd39bc7bae_1024x559.jpeg 424w, https://substackcdn.com/image/fetch/$s_!s3P8!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F81ccbffe-83bf-44c5-89cc-68bd39bc7bae_1024x559.jpeg 848w, https://substackcdn.com/image/fetch/$s_!s3P8!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F81ccbffe-83bf-44c5-89cc-68bd39bc7bae_1024x559.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!s3P8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F81ccbffe-83bf-44c5-89cc-68bd39bc7bae_1024x559.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><em>Figure 5.8. Softmax converts raw similarity scores into positive attention weights that sum to one.</em></p><p>The conceptual goal of normalization is simple: turn the scores into values that add up to one, so they can behave like proportions of attention. A first, informal way to see that idea is to divide each score by the sum of all scores.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;b4cdd5f9-fb36-4d52-8057-7ffabbe86582&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">attn_weights_2_tmp = attn_scores_2 / attn_scores_2.sum()
print("Attention weights:", attn_weights_2_tmp)
print("Sum:", attn_weights_2_tmp.sum())</code></pre></div><p>This shows the bookkeeping idea clearly, but plain division is not the standard choice for attention. It does not guarantee positive weights, and it is awkward when the raw scores have an inconvenient scale. The practical solution is softmax, which converts the scores into positive numbers that still sum to one.</p><p>A naive softmax makes the transformation explicit by exponentiating each score and dividing by the sum of all exponentials.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;fcbb0c03-0836-4e6c-a580-822fae69257d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def softmax_naive(x):
    return torch.exp(x) / torch.exp(x).sum(dim=0)

attn_weights_2_naive = softmax_naive(attn_scores_2)
print("Attention weights:", attn_weights_2_naive)
print("Sum:", attn_weights_2_naive.sum())</code></pre></div><p>PyTorch provides the preferred form directly.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;b422f8ce-dd63-4606-ae7c-66ac7f6c40f0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">attn_weights_2 = torch.softmax(attn_scores_2, dim=0)
print("Attention weights:", attn_weights_2)
print("Sum:", attn_weights_2.sum())</code></pre></div><p>The two softmax versions express the same idea, but the PyTorch version is the one to rely on in practice because it is numerically more stable. After normalization, each attention weight can be interpreted as a share of the total attention budget assigned to a token.</p><h3>Building the context vector</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!803X!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F026be93c-c53d-4560-b1d5-acf6c2c0d8b1_1024x559.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!803X!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F026be93c-c53d-4560-b1d5-acf6c2c0d8b1_1024x559.jpeg 424w, https://substackcdn.com/image/fetch/$s_!803X!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F026be93c-c53d-4560-b1d5-acf6c2c0d8b1_1024x559.jpeg 848w, https://substackcdn.com/image/fetch/$s_!803X!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F026be93c-c53d-4560-b1d5-acf6c2c0d8b1_1024x559.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!803X!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F026be93c-c53d-4560-b1d5-acf6c2c0d8b1_1024x559.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!803X!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F026be93c-c53d-4560-b1d5-acf6c2c0d8b1_1024x559.jpeg" width="1024" height="559" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/026be93c-c53d-4560-b1d5-acf6c2c0d8b1_1024x559.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:559,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:96122,&quot;alt&quot;:&quot;Attention weights control how strongly each input token vector contributes to the final context vector.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/203532650?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F026be93c-c53d-4560-b1d5-acf6c2c0d8b1_1024x559.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Attention weights control how strongly each input token vector contributes to the final context vector." title="Attention weights control how strongly each input token vector contributes to the final context vector." srcset="https://substackcdn.com/image/fetch/$s_!803X!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F026be93c-c53d-4560-b1d5-acf6c2c0d8b1_1024x559.jpeg 424w, https://substackcdn.com/image/fetch/$s_!803X!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F026be93c-c53d-4560-b1d5-acf6c2c0d8b1_1024x559.jpeg 848w, https://substackcdn.com/image/fetch/$s_!803X!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F026be93c-c53d-4560-b1d5-acf6c2c0d8b1_1024x559.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!803X!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F026be93c-c53d-4560-b1d5-acf6c2c0d8b1_1024x559.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><em>Figure 5.9. The context vector is a weighted sum of the input vectors, where larger attention weights contribute more strongly.</em></p><p>Once the weights are known, the output of self-attention is a weighted sum of the input vectors. That output is the context vector for the query token. It is a new representation that keeps the query token in view while blending in information from the rest of the sequence.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;2961cbd7-eed2-460f-aa3a-361776143fac&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">query = inputs[1]         #1
context_vec_2 = torch.zeros(query.shape)
for i,x_i in enumerate(inputs):
    context_vec_2 += attn_weights_2[i]*x_i
print(context_vec_2)</code></pre></div><p>This step is the main payoff of the whole calculation. Tokens with larger attention weights contribute more strongly to the result, and tokens with smaller weights contribute less. The query token is therefore not represented by its original embedding alone. It is represented by a context-sensitive mixture of the full sequence, with the mixture proportions set by the attention weights computed from the dot-product scores.</p><p>The code snippets above form a complete hand calculation. The tensor definition supplies the inputs, the score computation snippet produces the raw similarities, the normalization snippets convert those scores into weights, and the final snippet aggregates the vectors into the context vector. That step-by-step structure is what makes the example traceable.</p><h3>A small perturbation, a different context</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!S-qo!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb384e59-5553-4d9d-a91c-b07128334c2f_1024x559.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!S-qo!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb384e59-5553-4d9d-a91c-b07128334c2f_1024x559.jpeg 424w, https://substackcdn.com/image/fetch/$s_!S-qo!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb384e59-5553-4d9d-a91c-b07128334c2f_1024x559.jpeg 848w, https://substackcdn.com/image/fetch/$s_!S-qo!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb384e59-5553-4d9d-a91c-b07128334c2f_1024x559.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!S-qo!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb384e59-5553-4d9d-a91c-b07128334c2f_1024x559.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!S-qo!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb384e59-5553-4d9d-a91c-b07128334c2f_1024x559.jpeg" width="1024" height="559" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/db384e59-5553-4d9d-a91c-b07128334c2f_1024x559.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:559,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:93665,&quot;alt&quot;:&quot;Before and after attention weight charts showing one token receiving more attention after its score increases.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/203532650?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb384e59-5553-4d9d-a91c-b07128334c2f_1024x559.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Before and after attention weight charts showing one token receiving more attention after its score increases." title="Before and after attention weight charts showing one token receiving more attention after its score increases." srcset="https://substackcdn.com/image/fetch/$s_!S-qo!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb384e59-5553-4d9d-a91c-b07128334c2f_1024x559.jpeg 424w, https://substackcdn.com/image/fetch/$s_!S-qo!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb384e59-5553-4d9d-a91c-b07128334c2f_1024x559.jpeg 848w, https://substackcdn.com/image/fetch/$s_!S-qo!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb384e59-5553-4d9d-a91c-b07128334c2f_1024x559.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!S-qo!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb384e59-5553-4d9d-a91c-b07128334c2f_1024x559.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><em>Figure 5.10. A small change in one attention score changes the softmax distribution and shifts the resulting context vector.</em></p><p>The sensitivity of attention becomes clear if one score changes slightly. Suppose the score for the third token rises by 0.10 before softmax, while the other scores stay the same. If the original score was 1.33, the adjusted score becomes 1.43. After softmax, that token receives a little more weight, while the other weights must shrink slightly so the total still sums to one. The context vector then shifts toward the third token&#8217;s embedding, because the weighted sum now gives that vector more influence.</p><p>The same logic works for any score change in this single-query example. If the query token&#8217;s own score increases, the context becomes more self-focused. If an earlier token&#8217;s score increases, the context draws more strongly from that earlier position. The important point is that the change is direct and measurable: a different similarity score produces different normalized weights, and different weights produce a different context vector.</p><p>That chain from similarity to normalization to aggregation is the core of single-token self-attention. Once it is clear for one query, the same idea can be applied to every token in the sequence.</p><h2>From one query to every token</h2><p>The single-token example in the previous section shows the essence of self-attention: one token acts as a query, compares itself with every token in the sequence, turns those comparisons into weights, and then forms a context vector from the whole sequence. The only thing that changes when we move from one query to the full model view is that every token gets to play that same role.</p><p>That shift matters because a sequence model should not force one token at a time through a recurrent chain. Instead, each token can directly inspect the entire set of token embeddings and decide which parts are relevant. In a sentence, the final word can look back to an earlier subject, a modifier can look forward to the word it describes, and an ambiguous token can draw context from multiple places at once. Self-attention gives every token a path to every other token without stepping through the sequence one position at a time.</p><h2>Loop-based pairwise scores</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!WLZa!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb489b921-db3b-4d58-850a-884b70af3bb9_1024x559.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!WLZa!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb489b921-db3b-4d58-850a-884b70af3bb9_1024x559.jpeg 424w, https://substackcdn.com/image/fetch/$s_!WLZa!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb489b921-db3b-4d58-850a-884b70af3bb9_1024x559.jpeg 848w, https://substackcdn.com/image/fetch/$s_!WLZa!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb489b921-db3b-4d58-850a-884b70af3bb9_1024x559.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!WLZa!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb489b921-db3b-4d58-850a-884b70af3bb9_1024x559.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!WLZa!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb489b921-db3b-4d58-850a-884b70af3bb9_1024x559.jpeg" width="1024" height="559" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b489b921-db3b-4d58-850a-884b70af3bb9_1024x559.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:559,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:99318,&quot;alt&quot;:&quot;A six by six attention score matrix showing every token compared with every other token.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/203532650?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb489b921-db3b-4d58-850a-884b70af3bb9_1024x559.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="A six by six attention score matrix showing every token compared with every other token." title="A six by six attention score matrix showing every token compared with every other token." srcset="https://substackcdn.com/image/fetch/$s_!WLZa!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb489b921-db3b-4d58-850a-884b70af3bb9_1024x559.jpeg 424w, https://substackcdn.com/image/fetch/$s_!WLZa!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb489b921-db3b-4d58-850a-884b70af3bb9_1024x559.jpeg 848w, https://substackcdn.com/image/fetch/$s_!WLZa!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb489b921-db3b-4d58-850a-884b70af3bb9_1024x559.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!WLZa!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb489b921-db3b-4d58-850a-884b70af3bb9_1024x559.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><em>Figure 5.11. The full attention score matrix contains one row per query token and one column per token being attended to.</em></p><p>To make the mechanics concrete, start with the toy input tensor from the previous example, which has shape <code>6 &#215; 3</code>: six token embeddings, each with three features. If we compute attention for all tokens, the result should be a <code>6 &#215; 6</code> score matrix, where each row corresponds to one query token and each column corresponds to one token being considered as a source of context.</p><p>The most literal way to build that matrix is with nested loops. For each token vector <code>x_i</code>, we compare it with every token vector <code>x_j</code> using a dot product. The result is the raw attention score between token <code>i</code> and token <code>j</code>.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;936d4333-b11c-4fd7-b2e9-20d174857052&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">attn_scores = torch.empty(6, 6)
for i, x_i in enumerate(inputs):
    for j, x_j in enumerate(inputs):
        attn_scores[i, j] = torch.dot(x_i, x_j)
print(attn_scores)</code></pre></div><p>This code is not efficient, but it is valuable because it makes the structure of self-attention explicit. Every row is one token&#8217;s set of affinities to the full sequence. Every column shows how often a particular token is selected as relevant by different queries. The score matrix is the central object of the computation.</p><h2>Matrix multiplication as vectorization</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!sEHe!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0969c0ce-10c7-491c-8416-8afcd0187e1e_1024x559.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!sEHe!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0969c0ce-10c7-491c-8416-8afcd0187e1e_1024x559.jpeg 424w, https://substackcdn.com/image/fetch/$s_!sEHe!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0969c0ce-10c7-491c-8416-8afcd0187e1e_1024x559.jpeg 848w, https://substackcdn.com/image/fetch/$s_!sEHe!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0969c0ce-10c7-491c-8416-8afcd0187e1e_1024x559.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!sEHe!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0969c0ce-10c7-491c-8416-8afcd0187e1e_1024x559.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!sEHe!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0969c0ce-10c7-491c-8416-8afcd0187e1e_1024x559.jpeg" width="1024" height="559" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0969c0ce-10c7-491c-8416-8afcd0187e1e_1024x559.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:559,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:109834,&quot;alt&quot;:&quot;Matrix multiplication shape diagram showing a 6 by 3 input matrix multiplied by a 3 by 6 transposed matrix to produce a 6 by 6 score matrix.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/203532650?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0969c0ce-10c7-491c-8416-8afcd0187e1e_1024x559.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Matrix multiplication shape diagram showing a 6 by 3 input matrix multiplied by a 3 by 6 transposed matrix to produce a 6 by 6 score matrix." title="Matrix multiplication shape diagram showing a 6 by 3 input matrix multiplied by a 3 by 6 transposed matrix to produce a 6 by 6 score matrix." srcset="https://substackcdn.com/image/fetch/$s_!sEHe!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0969c0ce-10c7-491c-8416-8afcd0187e1e_1024x559.jpeg 424w, https://substackcdn.com/image/fetch/$s_!sEHe!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0969c0ce-10c7-491c-8416-8afcd0187e1e_1024x559.jpeg 848w, https://substackcdn.com/image/fetch/$s_!sEHe!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0969c0ce-10c7-491c-8416-8afcd0187e1e_1024x559.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!sEHe!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0969c0ce-10c7-491c-8416-8afcd0187e1e_1024x559.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><em>Figure 5.12. The pairwise attention scores can be computed at once with matrix multiplication: `6 &#215; 3 @ 3 &#215; 6 = 6 &#215; 6`.</em></p><p>The nested loops are exactly what matrix multiplication already does for us. If <code>inputs</code> has shape <code>6 &#215; 3</code>, then <code>inputs @ inputs.T</code> multiplies the matrix by its transpose and produces the same <code>6 &#215; 6</code> matrix of pairwise dot products.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;b2e67318-99e4-4e3e-941c-989006278fb9&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">attn_scores = inputs @ inputs.T
print(attn_scores)</code></pre></div><p>This is the same computation in a more compact and much faster form. The first <code>inputs</code> supplies the query vectors row by row, and the transposed matrix supplies the keys that each query is compared against. Because each output entry is a dot product between one row from the left matrix and one row from the right matrix, matrix multiplication gives us all pairwise attention scores in a single operation.</p><p>The shape tracing is the important part. The input matrix is <code>6 &#215; 3</code>. The transposed matrix is <code>3 &#215; 6</code>. Their product is <code>6 &#215; 6</code>. That means we have one score row per token, and each row contains one score for every token in the sequence.</p><h2>Row-wise softmax and sanity checks</h2><p>Raw dot products are only similarities. They are not yet usable as weights because they can be negative, and they do not automatically add up to one. To turn each row into a normalized distribution over the sequence, we apply softmax along the last dimension.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;1000841f-eeeb-4ef5-91d7-b54023ac3050&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">attn_weights = torch.softmax(attn_scores, dim=-1)
print(attn_weights)</code></pre></div><p>The <code>dim=-1</code> argument tells PyTorch to normalize each row independently. That is exactly what self-attention needs. The first row becomes the weight distribution for the first token, the second row becomes the weight distribution for the second token, and so on. Each token receives its own normalized view of the sequence.</p><p>A quick sanity check helps confirm that the normalization worked as intended.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;af50c672-40f5-4e89-bd19-2c8c1541a05d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">row_2_sum = sum([0.1385, 0.2379, 0.2333, 0.1240, 0.1082, 0.1581])
print("Row 2 sum:", row_2_sum)
print("All row sums:", attn_weights.sum(dim=-1))</code></pre></div><p>The exact numbers in the hand-checked row are less important than the property they illustrate. After softmax, each row sums to one, so the weights behave like a distribution over the available tokens. This makes the next step interpretable: the context vector will be a weighted average of the input embeddings, not an unbounded sum.</p><h2>All context vectors at once</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!XZF8!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3691dadf-b638-427c-a6b4-b08db6877a16_1024x559.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!XZF8!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3691dadf-b638-427c-a6b4-b08db6877a16_1024x559.jpeg 424w, https://substackcdn.com/image/fetch/$s_!XZF8!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3691dadf-b638-427c-a6b4-b08db6877a16_1024x559.jpeg 848w, https://substackcdn.com/image/fetch/$s_!XZF8!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3691dadf-b638-427c-a6b4-b08db6877a16_1024x559.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!XZF8!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3691dadf-b638-427c-a6b4-b08db6877a16_1024x559.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!XZF8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3691dadf-b638-427c-a6b4-b08db6877a16_1024x559.jpeg" width="1024" height="559" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3691dadf-b638-427c-a6b4-b08db6877a16_1024x559.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:559,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:126448,&quot;alt&quot;:&quot;Matrix multiplication shape diagram showing a 6 by 6 attention weight matrix multiplied by a 6 by 3 input matrix to produce a 6 by 3 context matrix.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/203532650?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3691dadf-b638-427c-a6b4-b08db6877a16_1024x559.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Matrix multiplication shape diagram showing a 6 by 6 attention weight matrix multiplied by a 6 by 3 input matrix to produce a 6 by 3 context matrix." title="Matrix multiplication shape diagram showing a 6 by 6 attention weight matrix multiplied by a 6 by 3 input matrix to produce a 6 by 3 context matrix." srcset="https://substackcdn.com/image/fetch/$s_!XZF8!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3691dadf-b638-427c-a6b4-b08db6877a16_1024x559.jpeg 424w, https://substackcdn.com/image/fetch/$s_!XZF8!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3691dadf-b638-427c-a6b4-b08db6877a16_1024x559.jpeg 848w, https://substackcdn.com/image/fetch/$s_!XZF8!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3691dadf-b638-427c-a6b4-b08db6877a16_1024x559.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!XZF8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3691dadf-b638-427c-a6b4-b08db6877a16_1024x559.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><em>Figure 5.13. Multiplying the attention weights by the input matrix computes all context vectors at once: `6 &#215; 6 @ 6 &#215; 3 = 6 &#215; 3`.</em></p><p>Once the attention weights are normalized, the final context vectors come from multiplying the attention matrix by the input matrix. This performs the weighted aggregation for every token in one batched operation.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;8db5c4fa-aeb5-4c6e-8358-4dfc8843606f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">all_context_vecs = attn_weights @ inputs
print(all_context_vecs)</code></pre></div><p>The shape again tells the story. The attention matrix is <code>6 &#215; 6</code>, and the input matrix is <code>6 &#215; 3</code>, so the product is <code>6 &#215; 3</code>. That means the output contains one context vector per token, and each context vector has the same dimensionality as the original embeddings. The <code>i</code>th row is token <code>i</code>&#8217;s summary of the full sequence.</p><p>This batched result should agree with the earlier single-query example. The second token&#8217;s context vector was computed manually before, and the second row of the full matrix should match it exactly.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;a9dd8c06-0e74-45ec-8f1f-86b83d8c32ca&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">print("Previous 2nd context vector:", context_vec_2)</code></pre></div><p>That equivalence is the key bridge between the hand-worked example and the vectorized implementation. The algorithm has not changed. The computation has only been reorganized so that one matrix multiplication replaces many Python loops. In practice, that reorganization is what makes self-attention fast enough to scale.</p><h2>Chapter recap: what self-attention computes</h2><p>Self-attention is the chapter&#8217;s central idea because it lets every token build a new representation by gathering information from the same sequence. Instead of carrying a single hidden state forward the way a recurrent model does, the token looks across the available context and forms a weighted mixture of what it finds relevant. The result is a context vector: a representation that reflects both the token itself and the surrounding tokens that matter for the current position.</p><p>The important separation is between score computation and context aggregation. First, each token pair receives a similarity score that measures how strongly one position should attend to another. Those scores do not yet represent the final output; they are only the raw material. Softmax turns them into normalized weights, which makes them comparable and ensures that each row of attention weights behaves like a distribution over the sequence. Only then are the values combined into the context vector through a weighted sum.</p><p>That pipeline is what made the earlier toy examples useful. Dot products and matrix multiplication provide the mechanical basis for computing all pairwise scores efficiently, and batched matrix multiplication lets the same idea scale from a hand-worked sequence to a full attention block. In the GPT setting, causal masking keeps the mechanism autoregressive by preventing a position from using future tokens. Dropout is then layered on as a regularization detail, not as a change to the core attention logic.</p><p>The toy version of self-attention is also the foundation of the trainable version used in real transformer blocks. Query, key, and value projections simply make the score-and-aggregation pipeline learnable. Multi-head attention extends the same idea by running several attention heads in parallel, each learning a different view of the sequence. With those pieces in place, the chapter&#8217;s implementation path leads naturally from a simple similarity-based mechanism to the attention modules used in GPT-style models.</p><p>Use the URL below to download the folder:</p>
      <p>
          <a href="https://onepagecode.substack.com/p/attention-mechanisms-from-intuition">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Preparing GPT-style training inputs]]></title><description><![CDATA[Chapter 4: Formulating sliding-window datasets, shifted next-token targets, and data loading pipelines for generative pretraining]]></description><link>https://onepagecode.substack.com/p/preparing-gpt-style-training-inputs</link><guid isPermaLink="false">https://onepagecode.substack.com/p/preparing-gpt-style-training-inputs</guid><dc:creator><![CDATA[Onepagecode]]></dc:creator><pubDate>Sun, 28 Jun 2026 20:15:09 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!_W6n!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3078fdad-8e80-40a9-be17-640eb18fa447_1254x1254.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3>Download the entire book as pdf or EPUB using the URL at the end of this article </h3><h2>Sliding-window next-token examples</h2><p>A tokenized corpus is just a long stream of token IDs until we decide how to carve it into training examples. For next-token prediction, the essential move is to take a fixed-length context and pair it with the very next token in the stream. That creates a simple supervised signal: the model reads a prefix and learns to continue it one token at a time.</p><p>To keep the pattern easy to inspect, it helps to work with a short slice of token IDs rather than an entire book or article. The exact text behind the numbers is not important yet. What matters is that the slice is long enough to show how one window leads into the next.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;6c116b37-b751-4ae3-9fae-651af4449eab&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">with open("the-verdict.txt", "r", encoding="utf-8") as f:
    raw_text = f.read()

enc_text = tokenizer.encode(raw_text)
enc_sample = enc_text[50:]
print(len(enc_text))</code></pre></div><h3>A tiny token stream as the running example</h3><p>Suppose the sample begins with a compact sequence such as <code>[10, 11, 12, 13, 14, 15, 16]</code>. If the context size is 4, then the first input window is the first four token IDs, and the target window is the next four token IDs shifted forward by one position. In other words, <code>x</code> contains <code>[10, 11, 12, 13]</code> and <code>y</code> contains <code>[11, 12, 13, 14]</code>. The target is not a new sequence invented by the model; it is the same stream, moved over by one token.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;914fb14b-9a90-4114-873b-8c5b01616d17&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">context_size = 4
x = enc_sample[:context_size]
y = enc_sample[1:context_size+1]
print(f"x: {x}")
print(f"y: {y}")</code></pre></div><p>This is the core training pair. The model is asked to predict the next token for each position in the context window, so every token in <code>x</code> points to the token immediately to its right in <code>y</code>. The supervision comes entirely from the shift.</p><h3>From one sequence to one training pair</h3><p>The same idea becomes easier to see when the prefixes are shown one by one. A prefix of length one maps to the next token, a prefix of length two maps to the following token, and so on until the full context size is reached. This progression makes the causal structure explicit: each prediction depends only on what has already appeared.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;56080b89-b4c5-4364-abad-8f4ff012f1ed&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">for i in range(1, context_size + 1):
    context = enc_sample[:i]
    desired = enc_sample[i]
    print(context, "----&gt;", desired)</code></pre></div><p>The same inspection can be written in decoded form so the token IDs are easier to read as text. That does not change the sampling rule; it only makes the prefix-to-next-token relationship more transparent.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;bc2ffe0f-a7af-4c08-a6fb-5e9d99739833&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">for i in range(1, context_size + 1):
    context = enc_sample[:i]
    desired = enc_sample[i]
    print(tokenizer.decode(context), "----&gt;", tokenizer.decode([desired]))</code></pre></div><h3>Overlapping prefixes and next-token labels</h3><p>A single token stream can produce many training examples because neighboring windows overlap. With a stride of 1, the second window starts one token later than the first, so almost all of the tokens are shared. If the first window is <code>[10, 11, 12, 13]</code>, the next window is <code>[11, 12, 13, 14]</code>, and the next one is <code>[12, 13, 14, 15]</code>. Each step drops the oldest token and introduces one new token at the end.</p><p>That overlap is the reason sliding-window sampling is so productive. One long corpus can be reused many times over, not by duplicating text, but by shifting the window across it. The samples remain closely related, yet each one supplies a slightly different next-token label.</p><h3>Why context size and stride matter</h3><p>Context size is the fixed window length. It determines how many token IDs appear in each input example and therefore how much history the model can condition on at once. A larger context size gives each sample more prior tokens, but it also makes every window longer and reduces how many windows fit into a short sequence.</p><p>Stride is the step between consecutive windows. It tells us how far to move the window after each sample. A stride of 1 produces maximal overlap, while a stride equal to the context size produces windows that do not overlap at all. Those two knobs play different roles: context size sets the width of each example, and stride sets the spacing between examples.</p><p>At this stage, everything is still raw token IDs. Nothing has been embedded yet, and no model has consumed the data. The only task here is to turn one long token stream into many shifted input-target pairs that are ready for the next step.</p><h2>Why a Dataset abstraction helps</h2><p>A tokenized corpus is just a long stream of integers. That is enough to reason about next-token prediction on paper, but it is not yet a convenient training interface. During optimization, the model needs many short, fixed-length samples, and each sample must preserve the exact alignment between an input sequence and the tokens that follow it. A custom <code>Dataset</code> gives that structure a permanent home. Instead of manually slicing token IDs every time you want a batch, you define the sampling rule once and let PyTorch index it repeatedly.</p><p>This separation matters because sample creation and batch formation are different jobs. The <code>Dataset</code> decides which token windows exist. The <code>DataLoader</code> decides how those windows are grouped, whether they are shuffled, and whether incomplete batches are kept. Once those responsibilities are separated, the training loop can treat the data pipeline like any other reusable component.</p><h2>Building sliding-window samples</h2><p>The core idea is to move a fixed-length window across the token stream and, for every window, create a target sequence that is shifted by one token. If the input contains tokens from position <code>i</code> through <code>i + max_length - 1</code>, then the target contains tokens from position <code>i + 1</code> through <code>i + max_length</code>. That one-token shift is what turns raw text into supervised next-token examples.</p><p>The amount of overlap between consecutive samples is controlled by the stride. A small stride reuses many of the same tokens across neighboring examples, which produces a dense set of training rows. A stride equal to the context length produces non-overlapping chunks, which reduces reuse and makes the sample boundaries easier to see. In practice, the stride is a sampling choice rather than a model property. It changes the dataset geometry, not the learning objective.</p><p>The implementation below tokenizes the text once, then stores each input chunk and its corresponding target chunk as tensors. The <code>__len__</code> method reports how many samples the dataset contains, and <code>__getitem__</code> returns one aligned pair at a time.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;dcb5d4a7-a635-4666-b8c8-74492eea9e0c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">import torch
from torch.utils.data import Dataset, DataLoader

class GPTDatasetV1(Dataset):
    def __init__(self, txt, tokenizer, max_length, stride):
        self.input_ids = []
        self.target_ids = []

        token_ids = tokenizer.encode(txt)

        for i in range(0, len(token_ids) - max_length, stride):
            input_chunk = token_ids[i:i + max_length]
            target_chunk = token_ids[i + 1:i + max_length + 1]
            self.input_ids.append(torch.tensor(input_chunk))
            self.target_ids.append(torch.tensor(target_chunk))

    def __len__(self):
        return len(self.input_ids)

    def __getitem__(self, idx):
        return self.input_ids[idx], self.target_ids[idx]</code></pre></div><p>A small example makes the shift explicit. Suppose a compact token stream begins with eight tokens and you choose a context length of four. The first sample uses tokens <code>0</code> through <code>3</code> as input and tokens <code>1</code> through <code>4</code> as target. The next sample starts one token later if the stride is one, so its input is <code>1</code> through <code>4</code> and its target is <code>2</code> through <code>5</code>. Each example teaches the same prediction rule from a slightly different offset.</p><p>That overlap is the whole point of the sliding window. Rather than consuming the corpus once, the dataset reuses the same text many times in shifted form. The model sees a large number of local contexts, and the training objective remains consistent across every sample.</p><h2>Turning samples into a DataLoader</h2><p>A <code>Dataset</code> alone only defines indexed access. For training, we also want batches, optional shuffling, and iteration support that fits naturally into a loop. The <code>DataLoader</code> supplies that layer. It reads samples from the dataset and assembles them into tensors of shape <code>[batch_size, max_length]</code> for inputs and targets.</p><p>The helper below hides the dataset construction and exposes the main knobs you will actually tune. <code>batch_size</code> determines how many sequences are processed together. <code>shuffle</code> changes the order in which samples are drawn, but it never changes the token order inside a sample. <code>drop_last</code> discards a final incomplete batch so the batch shape stays consistent. <code>num_workers</code> controls how many subprocesses help load data, which can matter later when datasets grow larger.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;21afd63a-7cd3-4364-acca-1cdd6e167140&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def create_dataloader_v1(txt, batch_size=4, max_length=256,
                         stride=128, shuffle=True, drop_last=True,
                         num_workers=0):
    tokenizer = tiktoken.get_encoding("gpt2")
    dataset = GPTDatasetV1(txt, tokenizer, max_length, stride)
    dataloader = DataLoader(
        dataset,
        batch_size=batch_size,
        shuffle=shuffle,
        drop_last=drop_last,
        num_workers=num_workers
    )
    return dataloader</code></pre></div><p>At this stage, the important thing is not performance tuning but predictability. You want to know exactly how many examples exist, how much overlap they share, and whether your batches preserve the next-token shift you intended.</p><h2>Inspecting batches for correctness</h2><p>The quickest way to verify the pipeline is to print the first few batches and inspect how the windows advance. A DataLoader is an iterable, so you can wrap it with <code>iter()</code> and call <code>next()</code> to pull out successive batches. When <code>shuffle</code> is disabled, neighboring batches should reflect the stride directly.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;d4c315bd-4fe5-4ff3-8a01-d40a6e39699c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">with open("the-verdict.txt", "r", encoding="utf-8") as f:
    raw_text = f.read()

dataloader = create_dataloader_v1(
    raw_text, batch_size=1, max_length=4, stride=1, shuffle=False)
data_iter = iter(dataloader)
first_batch = next(data_iter)
print(first_batch)

second_batch = next(data_iter)
print(second_batch)</code></pre></div><p>With a batch size of one, the output is easy to reason about because each batch contains a single input-target pair. The second batch begins one token later than the first when the stride is one, so the overlap is immediately visible. That makes it a useful debugging setup before you move to larger batches.</p><h2>Non-overlapping batches as a comparison case</h2><p>If you set the stride equal to the context length, the window advances by exactly one full sample each time. The resulting chunks no longer overlap, so every token appears in only one position within each sample. This does not change the prediction rule, but it does change how much of the corpus is reused across nearby examples.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;72e9ebe8-44eb-4bb5-9169-b197b1c17331&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">dataloader = create_dataloader_v1(
    raw_text, batch_size=8, max_length=4, stride=4,
    shuffle=False
)

data_iter = iter(dataloader)
inputs, targets = next(data_iter)
print("Inputs:\n", inputs)
print("\nTargets:\n", targets)</code></pre></div><p>This contrast is useful because it isolates the effect of stride. Overlapping windows increase sample density, while non-overlapping windows make the boundaries between examples easier to see. In both cases, the target remains the input shifted by one token, which is the invariant that the model will learn from.</p><h2>Token embeddings as learned lookups</h2><p>A token ID is just an integer label. By itself, it is not useful as an input to a neural network, because the model needs a dense vector with trainable parameters, not a category number. The standard solution is a token embedding layer, which behaves like a learned lookup table. Each token ID selects one row from a weight matrix, and that row becomes the vector representation for the token.</p><p>The toy example below keeps the vocabulary deliberately tiny so the lookup behavior is easy to inspect. The embedding matrix has six rows, one per token in the vocabulary, and each row contains a three-dimensional vector. When the embedding layer is initialized, those vectors start as small random values. During training, backpropagation adjusts them so that tokens useful in similar contexts end up with similar representations.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;376b776e-c4f1-461a-aaa4-784ef9cfbd0a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">import torch

torch.manual_seed(123)

input_ids = torch.tensor([2, 3, 5, 1])
vocab_size = 6
output_dim = 3
embedding_layer = torch.nn.Embedding(vocab_size, output_dim)

print(embedding_layer.weight)
print(embedding_layer(torch.tensor([3])))
print(embedding_layer(input_ids))</code></pre></div><p>The printed weight tensor is the embedding table itself. If token ID <code>3</code> appears, the model does not compute a formula from the integer; it retrieves row <code>3</code> from that table. The same idea applies to the entire sequence <code>[2, 3, 5, 1]</code>, except that the lookup happens for each position in the sequence and the result is stacked into a tensor of vectors.</p><p>This is the implementation view that matters for GPT-style models. Token embeddings are not handcrafted features. They are learned parameters, just like the weights in a linear layer, and they are updated as part of the model&#8217;s training process.</p><h2>From token IDs to batched embedding tensors</h2><p>A single token sequence becomes more useful when it is batched with other sequences. In practice, the embedding layer receives a two-dimensional tensor of token IDs with shape <code>[batch_size, sequence_length]</code>. Applying the embedding lookup to that batch adds one more dimension, because every token ID is replaced by a vector of length <code>output_dim</code>.</p><p>The result is a three-dimensional tensor with shape <code>[batch_size, sequence_length, output_dim]</code>. That shape is the key to understanding how text preprocessing connects to the model. The first dimension indexes examples in the batch, the second dimension indexes token positions within each example, and the third dimension stores the learned representation for each token.</p><p>The code below shows the shape progression explicitly. The input IDs are printed first, then the batch shape, and finally the shape after the embedding lookup.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;c3e0abc5-648e-446d-a55c-08724e39b071&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">vocab_size = 50257
output_dim = 256
token_embedding_layer = torch.nn.Embedding(vocab_size, output_dim)

max_length = 4
inputs = torch.tensor([
    [2, 3, 5, 1],
    [4, 7, 2, 0],
    [6, 1, 3, 9],
])

print("Token IDs:\n", inputs)
print("\nInputs shape:\n", inputs.shape)

token_embeddings = token_embedding_layer(inputs)
print(token_embeddings.shape)</code></pre></div><p>The embedding layer preserves the batch and sequence dimensions and expands only the last dimension. That is exactly what you want: every token in every sequence is replaced by a dense vector, but the ordering and grouping structure of the input batch stays intact.</p><p>At this stage, the model knows what each token is, but not where it appears.</p><h2>Why position information is necessary</h2><p>Token embeddings alone are position-agnostic. The same token ID always maps to the same vector, whether it appears at the beginning of a sentence, in the middle of a paragraph, or at the end of a prompt. That behavior is useful for representing token identity, but it does not tell the model anything about sequence order.</p><p>For language modeling, order matters. The token sequence <code>"dog bites man"</code> is not equivalent to <code>"man bites dog"</code>, even though both contain the same words. A model that only sees token embeddings would receive the same set of vectors for both sequences, just arranged in different positions. It therefore needs an additional signal that identifies the position of each token in the context window.</p><p>GPT-style models use absolute positional embeddings for this purpose. The implementation is again a learned embedding table, but this time the lookup key is the position index rather than the token ID.</p><h2>Absolute positional embeddings and final input embeddings</h2><p>Absolute positional embeddings are built to match the token embedding size exactly. If token embeddings are 256-dimensional, positional embeddings must also be 256-dimensional, because the two tensors are combined by elementwise addition. A position embedding for index <code>0</code> can therefore be added directly to the token embedding at position <code>0</code>, and the same applies to every other position in the context length.</p><p>The position IDs are usually created as a simple range from <code>0</code> to <code>context_length - 1</code>. Those indices are passed through a positional embedding layer to produce a matrix of positional vectors, one row per sequence position. Because the shape is <code>[sequence_length, output_dim]</code>, it can be added to the token embedding tensor of shape <code>[batch_size, sequence_length, output_dim]</code>. PyTorch broadcasts the positional embeddings across the batch dimension, so every example in the batch receives the same set of position vectors.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;c91d0808-8dc1-4dd7-8373-d2ed0c699526&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">context_length = max_length
pos_embedding_layer = torch.nn.Embedding(context_length, output_dim)
pos_embeddings = pos_embedding_layer(torch.arange(context_length))
print(pos_embeddings.shape)

input_embeddings = token_embeddings + pos_embeddings
print(input_embeddings.shape)</code></pre></div><p>This final tensor is the input representation consumed by the later transformer layers. It contains two pieces of information at once. The token embedding contributes content, telling the model which token is present. The positional embedding contributes order, telling the model where that token sits in the context window.</p><p>The shape progression is worth keeping in mind because it is the bridge between preprocessing and modeling. Token IDs start as a batch of integers with shape <code>[batch_size, sequence_length]</code>. Token embeddings turn that into <code>[batch_size, sequence_length, output_dim]</code>. Positional embeddings produce <code>[sequence_length, output_dim]</code>, and after addition the final input embedding tensor still has shape <code>[batch_size, sequence_length, output_dim]</code>. That is the tensor format the GPT-style model expects as it enters the first transformer block.</p><p>The important point is that no new semantic information is hidden in the addition itself. The model is not compressing two separate signals into one mysterious vector. It is combining a content vector and a position vector into a single representation that remains fully differentiable and trainable. Relative positional methods handle order differently, but in this chapter the learned absolute embedding table is the mechanism that turns a token sequence into a model-ready input.</p><p>Use the url below to download the book</p>
      <p>
          <a href="https://onepagecode.substack.com/p/preparing-gpt-style-training-inputs">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Text Representations and Tokenization]]></title><description><![CDATA[Chapter 3: How large language models translate raw text into numerical token IDs and dense vector embeddings]]></description><link>https://onepagecode.substack.com/p/text-representations-and-tokenization</link><guid isPermaLink="false">https://onepagecode.substack.com/p/text-representations-and-tokenization</guid><dc:creator><![CDATA[Onepagecode]]></dc:creator><pubDate>Sat, 27 Jun 2026 20:13:24 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!_W6n!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3078fdad-8e80-40a9-be17-640eb18fa447_1254x1254.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3>Download the entire book using the URL at the end of this article</h3><h3>From symbols to numbers</h3><p>Raw text is easy for people to read, but it is not something a neural network can consume directly. A model works with numbers, not with letters, punctuation, or words as symbolic labels. Before a GPT-style system can learn from language, the text has to be converted into a numerical form that can flow through matrix operations, optimization, and the rest of the training pipeline.</p><p>That need creates the first important idea in this chapter. The problem is not yet how to split text into pieces or how to build a vocabulary. The more basic problem is that symbolic language must be turned into vectors before learning can begin.</p><h3>What an embedding represents</h3><p>An embedding is a mapping from a discrete text unit to a continuous vector. Instead of treating a word or token as a bare identifier, the model associates it with a dense list of numbers that serves as its machine-readable representation. Dense means the vector carries information across many dimensions rather than relying on a sparse code with mostly empty positions.</p><p>This representation is useful because it can place related items near one another in vector space. If two words tend to appear in similar contexts, their embeddings can end up close together. That proximity gives the model a way to reuse statistical patterns across related inputs instead of learning every symbol in isolation.</p><p>For GPT-style next-token prediction, the most important unit is the token, or a word-like piece of text. Broader embedding use cases exist, and people sometimes represent sentences or documents as vectors, but that is not the focus here. In this chapter, the central concern is the representation that sits at the model input boundary for next-token modeling.</p><p>A familiar early example is Word2Vec, which showed that useful word vectors can be learned from context. The training details are not important yet. What matters is the idea it helped popularize: meaning can be reflected in the geometry of a vector space. Modern large language models keep that idea, but they usually learn their embedding tables as part of training rather than relying only on fixed pretrained vectors.</p><h3>Why dense vectors are useful</h3><p>Dense vectors fit neural networks better than sparse symbolic forms. A symbolic token identity only tells the model which item is present; it does not provide a smooth numeric structure for generalization. A dense embedding gives the network a compact representation that can be transformed, compared, and updated during learning.</p><p>This also matters for efficiency. Dense inputs are easier to feed into attention and feed-forward layers than a huge sparse one-hot representation would be. The result is a practical input format that supports learning while keeping the model&#8217;s computations tractable.</p><h3>A first look at design tradeoffs</h3><p>Embedding dimensionality is a design choice, and it is one of the first tradeoffs a practitioner faces. A smaller vector may not hold enough information to distinguish important patterns, while a larger vector can increase memory use and computation. The right size depends on the model and the task, so dimensionality is a balance between expressiveness and cost.</p><p>In modern LLMs, these vectors are usually learned during training. The embedding table is therefore part of the model, not just a fixed lookup resource. As training proceeds, the vectors adapt to the data and to the prediction objective, which lets the model shape its own representation space.</p><h3>Bridge to tokenization</h3><p>Embeddings answer the question of how text becomes a vector. Tokenization answers the earlier question of what units should receive those vectors in the first place. With the need for numerical representation in place, the next step is to see how raw text is divided into tokens and prepared for embedding.</p><h2>From text symbols to numerical IDs</h2><p>Raw text is not yet a model input. A GPT-style model ultimately consumes integers, not strings, so the preprocessing pipeline has to turn text into a sequence of token IDs. That pipeline has three distinct stages. First, the text is split into tokens. Next, those tokens are assigned stable integer IDs through a vocabulary. Finally, the model can work with the ID sequence as a numerical representation of the original text.</p><p>It is easy to blur the difference between a token and a token ID, but they play different roles. A token is the textual unit itself, such as a word or punctuation mark. A token ID is the integer label used to represent that unit in a fixed vocabulary. The model never sees the text token directly; it sees the ID, and later an embedding layer turns that ID into a dense vector.</p><h2>From naive splitting to punctuation-aware tokenization</h2><p>The simplest tokenizer idea is to split on whitespace. For many English sentences, that is enough to reveal the basic structure of a text. But whitespace splitting leaves punctuation attached to neighboring words, which means that <code>hello</code> and <code>hello,</code> become different units even when the model should treat the comma separately.</p><p>A tiny example makes the issue clear. Consider the sentence <code>Hello, world. This, is a test.</code> If we split only on whitespace, commas and periods remain glued to the words they follow. A punctuation-aware tokenizer gives a cleaner result because it separates those marks into their own tokens.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;301be782-5ed2-47ca-bcde-85fe15327118&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">import re

text = "Hello, world. This, is a test."

result = re.split(r'(\s)', text)
print(result)

result = re.split(r'([,.]|\s)', text)
print(result)

result = [item for item in result if item.strip()]
print(result)</code></pre></div><p>The first split exposes the weakness of whitespace tokenization. The second split treats commas and periods as token boundaries, and the final cleanup removes empty and whitespace-only fragments. That cleanup step matters because regular-expression splitting often produces separator entries that are not useful as tokens.</p><p>For slightly more realistic text, the pattern can be extended to cover a broader set of punctuation marks and a few multi-character symbols such as double dashes. The goal is still modest: separate meaningful text units without trying to solve every possible tokenization edge case.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;9cd739ff-f39b-44eb-844d-95862888bc4c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">text = "Hello, world. Is this-- a test?"
result = re.split(r'([,.:;?_!"()\']|--|\s)', text)
result = [item.strip() for item in result if item.strip()]
print(result)</code></pre></div><p>At this stage, tokenization is still a rule-based text split, not a learned representation. That is enough to prepare a corpus for vocabulary construction.</p><h2>Building a deterministic vocabulary</h2><p>Once the text has been tokenized, the next step is to build a vocabulary. A vocabulary is the fixed set of unique tokens that the tokenizer knows how to map to integers. If the same corpus is processed twice, the vocabulary should assign the same token IDs both times. Determinism matters because the model parameters, saved checkpoints, and decoded outputs all depend on a stable mapping.</p><p>A common way to make the ordering reproducible is to collect the unique tokens and sort them before enumeration. Sorting is not required in principle, but it prevents the token IDs from changing with incidental details such as set iteration order. That makes experiments easier to reproduce and code easier to reason about.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;fc75d5aa-6f2c-4b04-aee9-6b5dce068521&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">all_words = sorted(set(result))
vocab_size = len(all_words)
print(vocab_size)

vocab = {token: integer for integer, token in enumerate(all_words)}
for i, item in enumerate(vocab.items()):
    print(item)
    if i &gt;= 10:
        break</code></pre></div><p>Here, <code>all_words</code> is the vocabulary as an ordered list of unique tokens, and <code>vocab</code> is the forward mapping from token to ID. The vocabulary size tells you how many distinct symbols the tokenizer can represent. The enumeration step then fixes a one-to-one correspondence between the text tokens and their integer labels.</p><p>The inverse mapping is just as important as the forward mapping. Encoding text requires token-to-ID lookup, but decoding requires the reverse. Without the reverse mapping, integers would be easy to produce and hard to interpret.</p><h2>A minimal tokenizer with encode and decode</h2><p>A small tokenizer class turns the ideas above into a reusable interface. It stores the forward and reverse dictionaries, converts text to IDs in <code>encode</code>, and reconstructs text in <code>decode</code>. The implementation is intentionally simple because its purpose is to expose the mechanics of the text-to-ID pipeline rather than to be a production tokenizer.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;1ca807ed-3d83-49c6-a707-47d3586d9c37&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">class SimpleTokenizerV1:
    def __init__(self, vocab):
        self.str_to_int = vocab
        self.int_to_str = {i: s for s, i in vocab.items()}

    def encode(self, text):
        preprocessed = re.split(r'([,.?_!"()\']|--|\s)', text)
        preprocessed = [item.strip() for item in preprocessed if item.strip()]
        ids = [self.str_to_int[s] for s in preprocessed]
        return ids

    def decode(self, ids):
        text = " ".join([self.int_to_str[i] for i in ids])
        text = re.sub(r'\s+([,.?!"()\'])', r'\1', text)
        return text</code></pre></div><p>The <code>encode</code> method applies the same tokenization rule set that was used to build the vocabulary, then maps each token to its integer ID. The <code>decode</code> method walks in the opposite direction. It joins the decoded tokens with spaces and then removes spaces that should not appear before punctuation. That final cleanup restores a more natural text form after a round trip through IDs.</p><p>A quick sanity check should succeed on text drawn from the same domain as the vocabulary. When the tokenizer sees familiar tokens, the encode-decode cycle should preserve the sentence closely.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;6d5b4b8d-1daf-4fac-8648-4739bf223e9a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">tokenizer = SimpleTokenizerV1(vocab)
text = """"It's the last he painted, you know," 
       Mrs. Gisburn said with pardonable pride."""
ids = tokenizer.encode(text)
print(ids)
print(tokenizer.decode(ids))</code></pre></div><p>This round trip verifies that the vocabulary and the tokenizer agree on the tokenization scheme. It also shows why the vocabulary must be built from the same token boundaries that <code>encode</code> will later use.</p><p>The limitation appears immediately when the tokenizer encounters a token that was never part of the vocabulary. In that case, there is no valid integer ID to return, so encoding fails.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;1fd6e6fa-011f-4c6f-85f3-0a8f0b68cf0b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">text = "Hello, do you like tea?"
print(tokenizer.encode(text))</code></pre></div><p>That failure is not a bug in the implementation. It is a direct consequence of a closed vocabulary. A tokenizer that only knows the tokens it saw during vocabulary construction cannot represent new words, misspellings, or other unseen text. The next step is to make that interface more robust without losing the simplicity of the token-ID mapping.</p><h2>Unknown words and boundary markers</h2><p>A tokenizer that only knows ordinary words works well until it meets a word outside its vocabulary. At that point, the mapping from text to IDs breaks unless the tokenizer has a fallback. The simplest fallback is a reserved symbol such as <code>&lt;|unk|&gt;</code>, which stands in for any out-of-vocabulary item. This keeps the pipeline moving, because every input string still becomes a sequence of integers, even when the vocabulary is incomplete.</p><p>Boundary markers solve a different problem. Language model training data is usually assembled from many unrelated fragments, and those fragments should not blur together as if they were a single continuous document. A token such as <code>&lt;|endoftext|&gt;</code> gives the tokenizer a way to mark that boundary explicitly. In practice, this means one sentence can end and another can begin without pretending that they belong to the same source. The model then sees the transition as part of the token stream rather than as an invisible formatting detail.</p><p>The distinction matters because special tokens are not decorative labels. They are reserved vocabulary entries with a defined role in the tokenizer and, later, in the model&#8217;s behavior. <code>&lt;|unk|&gt;</code> is a fallback for missing lexical items, while <code>&lt;|endoftext|&gt;</code> identifies a boundary between segments. Together they make a simple tokenizer much more resilient, especially when the input is messy, heterogeneous, or stitched together from multiple documents.</p><h2>Extending the vocabulary and testing round-trip behavior</h2><p>The implementation change is small but important. Special symbols are added to the token inventory before the final string-to-ID mapping is built, which means they become first-class vocabulary items rather than ad hoc exceptions. In the example below, the ordinary tokens are collected first, and then the reserved symbols are appended before the dictionary is created.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;b38bf260-5a53-490b-bd99-2a5b62ae1b09&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">all_tokens = sorted(list(set(preprocessed)))
all_tokens.extend(["&lt;|endoftext|&gt;", "&lt;|unk|&gt;"])
vocab = {token:integer for integer,token in enumerate(all_tokens)}

print(len(vocab.items()))</code></pre></div><p>A quick inspection of the end of the vocabulary confirms that the special entries were inserted as expected.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;e19cbb5e-2ffc-4ffd-9176-3272e4e0b557&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">for i, item in enumerate(list(vocab.items())[-5:]):
    print(item)</code></pre></div><p>Once the vocabulary contains reserved symbols, the tokenizer can be updated so that unknown words no longer cause a failure. Instead, they are translated to <code>&lt;|unk|&gt;</code> during encoding. Decoding then reconstructs a readable string and cleans up spacing before punctuation so that the round trip remains usable.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;403a05a8-5d78-4699-8523-388fe576a4f4&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">class SimpleTokenizerV2:
    def __init__(self, vocab):
        self.str_to_int = vocab
        self.int_to_str = { i:s for s,i in vocab.items()}

    def encode(self, text):
        preprocessed = re.split(r'([,.:;?_!"()\']|--|\s)', text)
        preprocessed = [
            item.strip() for item in preprocessed if item.strip()
        ]
        preprocessed = [item if item in self.str_to_int
                        else "&lt;|unk|&gt;" for item in preprocessed]

        ids = [self.str_to_int[s] for s in preprocessed]
        return ids

    def decode(self, ids):
        text = " ".join([self.int_to_str[i] for i in ids])

        text = re.sub(r'\s+([,.:;?!"()\'])', r'\1', text)
        return text</code></pre></div><p>That tokenizer can now handle both unknown words and boundary tokens in a single pass. If two unrelated text fragments are joined with <code>&lt;|endoftext|&gt;</code>, the separator survives encoding and decoding, and any unfamiliar word is mapped to <code>&lt;|unk|&gt;</code> rather than breaking the workflow.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;51e1da74-fbf6-4f17-b412-42c294f675c9&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">text1 = "Hello, do you like tea?"
text2 = "In the sunlit terraces of the palace."
text = " &lt;|endoftext|&gt; ".join((text1, text2))
print(text)


tokenizer = SimpleTokenizerV2(vocab)
print(tokenizer.encode(text))

print(tokenizer.decode(tokenizer.encode(text)))</code></pre></div><p>This round-trip check is more than a convenience test. It verifies that the tokenizer still behaves predictably after special symbols are added, which is exactly what you want before building training data from many concatenated documents.</p><h2>Why GPT-style tokenizers move to BPE</h2><p>The <code>&lt;|unk|&gt;</code> strategy is useful as an intermediate step, but it is also a sign of a deeper limitation. A word-level tokenizer has a hard boundary between known and unknown vocabulary items. If a word is missing, the tokenizer can only collapse it into a generic placeholder. That keeps the system functioning, but it discards information that the model could otherwise use.</p><p>Byte pair encoding changes that design. Instead of treating whole words as the basic unit, BPE builds a vocabulary of subword pieces. Unseen words can then be represented by smaller fragments that are already known to the tokenizer. The effect is that the tokenizer no longer needs a permanent unknown-token fallback for ordinary text. Rather than saying, &#8220;this word is missing, so I will replace it with a placeholder,&#8221; it says, &#8220;this word can be decomposed into known parts.&#8221; That is a much better fit for open-ended language.</p><p>This is why GPT-style tokenizers rely on subword units instead of a rigid word list. Rare names, inflections, technical terms, and typos can all be expressed as combinations of smaller pieces. The vocabulary stays manageable, yet the tokenizer remains expressive. In practice, <code>&lt;|endoftext|&gt;</code> is the special token that remains central, while the rest of the token stream is handled by subword decomposition.</p><p>Common special-token conventions such as beginning-of-sequence, end-of-sequence, and padding are familiar from other model families, but GPT-style tokenizers take a slightly different route. They typically use <code>&lt;|endoftext|&gt;</code> as the key explicit boundary symbol, and later stages of the pipeline handle any padding or masking behavior that the model needs. The important point at this stage is not the exact convention used by every model family, but the design principle: a robust tokenizer should preserve structure where needed and avoid collapsing unfamiliar text into a single dead-end symbol.</p><h2>A practical GPT tokenizer example with tiktoken</h2><p>A GPT-compatible tokenizer makes the difference concrete. The <code>tiktoken</code> library exposes a GPT-2 style BPE tokenizer that can encode text containing both a special boundary token and an unfamiliar word. The example below shows the same conceptual workflow as the simple tokenizer, but with subword tokenization underneath.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:&quot;f5c4d7fd-c37c-4b6e-ac0f-b49df2dbe8ee&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">pip install tiktoken</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;da5732a6-d26a-4b28-89fe-afa21896023c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from importlib.metadata import version
import tiktoken
print("tiktoken version:", version("tiktoken"))

tokenizer = tiktoken.get_encoding("gpt2")</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;f21d48a4-0fe6-4f59-821d-2f1029c1b94d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">text = (
    "Hello, do you like tea? &lt;|endoftext|&gt; In the sunlit terraces"
     "of someunknownPlace."
)
integers = tokenizer.encode(text, allowed_special={"&lt;|endoftext|&gt;"})
print(integers)</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;32029f17-2f88-4713-9aff-67d6d6639395&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">strings = tokenizer.decode(integers)
print(strings)</code></pre></div><p>The key idea is visible in the output behavior rather than in the package details. The special token is preserved because it has been explicitly allowed, and the unfamiliar word is not flattened into <code>&lt;|unk|&gt;</code>. Instead, it is decomposed into smaller units that the tokenizer can represent. That is the practical advantage of BPE for GPT-style systems: robust handling of open vocabulary text without forcing the model to rely on a single generic unknown symbol.</p><p>By this point, the preprocessing pipeline is ready for model training. The tokenizer can separate unrelated text, survive novel words, and produce a stable sequence of IDs that is suitable for the next stage of the GPT workflow.</p><p>Use the URL below to download book as pdf or epub. </p>
      <p>
          <a href="https://onepagecode.substack.com/p/text-representations-and-tokenization">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Large Language Models: Architectures, Pretraining, and Roadmaps]]></title><description><![CDATA[Chapters 1 & 2: A foundational guide defining the GPT path, transformer decoders, and the three-stage implementation blueprint]]></description><link>https://onepagecode.substack.com/p/large-language-models-architectures</link><guid isPermaLink="false">https://onepagecode.substack.com/p/large-language-models-architectures</guid><dc:creator><![CDATA[Onepagecode]]></dc:creator><pubDate>Fri, 26 Jun 2026 20:11:05 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!_W6n!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3078fdad-8e80-40a9-be17-640eb18fa447_1254x1254.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h4>Download the entire book using the URL at the end of this article!</h4><p>A large language model is a deep neural network trained on a very large text corpus so it can learn patterns in language and use those patterns to generate or respond to text. The phrase sounds broad because the capability is broad: the same model can complete a sentence, answer a question, rewrite a paragraph, or continue a conversation. What makes it a language model is not that it contains hard-coded grammar rules, but that it learns statistical structure from text itself. What makes it large is not just that the network has many parameters, but also that it has been exposed to a large volume of training data.</p><p>That distinction matters. A model with millions or billions of parameters can still be trained on a narrow dataset, and a small model can be trained on a huge corpus. In practice, modern LLMs are defined by both scale dimensions together. Parameter count gives the model capacity to store and transform patterns, while training data scale gives it the breadth of language experience needed to learn those patterns in the first place. The result is a model that does not merely memorize phrases; it learns reusable regularities across syntax, semantics, style, and task structure.</p><p>This is also where LLMs differ from older text systems built around manual rules or feature engineering. Traditional machine learning often depends on a human deciding which signals matter, such as specific keywords, counts, or other hand-crafted indicators. Deep learning replaces much of that manual design with learned representations. Instead of telling the model which textual cues are important, you provide data and let the network discover useful internal features during training. For language tasks, that shift is especially powerful because text is messy, contextual, and full of exceptions that are difficult to capture with fixed rules.</p><h3>Where LLMs sit in the AI hierarchy</h3><p>It is useful to place LLMs inside the broader field hierarchy. Artificial intelligence is the largest umbrella term here. It includes any approach aimed at building systems that perform tasks associated with intelligent behavior. Machine learning is a subset of AI focused on methods that learn from data rather than following only explicitly programmed rules. Deep learning is a subset of machine learning based on multilayer neural networks that learn representations through many stages of transformation. LLMs live at the intersection of deep learning and natural language processing, and they are one of the most visible examples of what deep learning can do at scale.</p><p>This hierarchy is more than taxonomy. It explains why LLMs emerged when they did. Earlier AI systems often relied on symbolic reasoning, expert rules, or carefully engineered statistical features. Those methods were useful, but they struggled to generalize across the enormous variability of human language. Deep learning offered a different route: learn the representation directly from data, then use that representation to solve downstream tasks. LLMs take that idea further than earlier neural language models by combining scale, architecture, and data to produce models that are useful across many language tasks without task-specific redesign.</p><h3>Why next-word prediction matters</h3><p>The basic training signal behind a language model is next-word prediction, often framed more precisely as predicting the next token in a sequence. During training, the model sees a prefix of text and learns to estimate what should come next. That simple objective turns ordinary text into supervision. No person needs to label every example with a class or answer key, because the structure is already present in the text itself. This is why language modeling is described as self-supervised learning: the input text provides its own training target.</p><p>The power of next-word prediction is easy to underestimate. To make good predictions, the model has to internalize a wide range of patterns, from short-range syntax to longer-range meaning and discourse flow. It must learn which words tend to follow which contexts, how tone changes across settings, how formal and informal phrasing differ, and how facts are usually expressed. The model is not trained to memorize isolated continuations; it is trained to build a general representation of language that supports many different continuations.</p><p>That same mechanism also explains why language modeling is such a strong foundation for later adaptation. A model trained to continue text has already absorbed a broad model of language, so it can be repurposed for tasks that require understanding, generation, or both. The next section of the chapter will build on this idea by separating the broad training phase from later task-specific adaptation.</p><h3>Why these models are useful</h3><p>The practical value of LLMs comes from flexibility. Once trained on sufficiently broad text, the model can often handle new prompts without having been explicitly trained on that exact task. That is the basis of zero-shot behavior, where a model is asked to perform a task with no examples beyond the prompt itself. Few-shot behavior goes a step further by providing a small number of examples in context so the model can infer the pattern to follow. These capabilities are not magic; they are consequences of large-scale learning from diverse text and the model&#8217;s ability to generalize from patterns it has already internalized.</p><p>This is why LLMs matter in practice. A single model can support many text-centric workflows, especially when the task can be expressed in natural language. The same underlying engine that predicts the next word can also generate drafts, rewrite content, summarize passages, or assist with conversational querying. The book&#8217;s later chapters will focus on how to build that kind of system, but the key point here is conceptual: an LLM is valuable because it learns a transferable language representation rather than a single narrow rule set.</p><h3>A simple everyday example</h3><p>Consider writing a short reply to a colleague who asks whether a meeting should move to Friday. A traditional system would need explicit logic for every phrasing variation and every possible context. An LLM, by contrast, can read the prompt, infer the intent, and continue with a response that fits the tone and the situation. It does this because it has learned from large amounts of text how requests, confirmations, and scheduling language are typically expressed. That ability to generalize from language patterns is the core reason LLMs have become so useful, and it is the foundation for everything that follows in the book.</p><h2>Pretraining as the foundation</h2><p>Pretraining is the stage where a language model first learns the broad patterns of text before it is asked to solve a narrower task. The core objective is next-word prediction. Given a sequence of tokens, the model learns to predict what token is likely to come next. That training signal is simple, but it is surprisingly rich because it can be extracted directly from raw text. No separate annotation step is required, and no task-specific label set has to exist in advance. The text supplies its own supervision, which is why this approach is called self-supervised learning.</p><p>This matters because pretraining is not trying to teach the model one skill at a time. It is building a general language capability from many examples of ordinary text. Over time, the model picks up syntax, phrasing, word relationships, and longer-range regularities that make language coherent. The result of this stage is a pretrained base model, sometimes called a foundation model, that can continue text in a broadly useful way even before it has been adapted to any specific use case.</p><p>The idea of a foundation model is central to the rest of the training lifecycle. Once a model has learned general language behavior from raw text, later training can focus on shaping that ability toward a narrower goal instead of recreating it from zero. That separation between general language learning and task-specific adaptation is what makes the pretraining-first workflow so effective.</p><h2>Why scale and diversity matter</h2><p>Pretraining only becomes powerful when it is done on a large and varied corpus. A small dataset can teach local patterns, but it will not expose the model to enough different writing styles, topics, and contexts to support broad reuse. Large corpora help the model see the same language mechanisms in many settings, which improves its ability to generalize. Diversity matters for the same reason. When the training text spans many domains and forms of expression, the model is more likely to learn reusable structure rather than memorize a narrow slice of language.</p><p>This is part of why pretrained models are expensive. Modern language models contain many parameters, and fitting those parameters to a large corpus takes substantial compute, time, and engineering effort. That cost is not a side issue; it is one of the reasons pretraining is so valuable. If a model already required a major investment to learn general language behavior, then reusing those learned weights is far more practical than repeating the same expensive process for every new application.</p><p>The payoff is that a pretrained model becomes a reusable starting point. It can support a wide range of downstream uses because it already encodes broad linguistic competence. The model is not specialized yet, but it has enough general knowledge to make later adaptation efficient. This is the main reason large-scale pretraining sits at the base of the workflow: it creates a model worth reusing.</p><h2>Fine-tuning as task adaptation</h2><p>Fine-tuning is the second stage of the lifecycle. Instead of learning language from scratch, the model begins with the pretrained base model and then continues training on data that reflects a narrower objective. That objective might be a domain-specific corpus, a set of labeled examples, or paired inputs and outputs that express the behavior you want. The point is not to relearn language, but to steer an already capable model toward a particular task or domain.</p><p>This distinction is easy to miss, but it is crucial. A model trained only on a small task dataset usually has to spend its capacity on both general language understanding and task adaptation at the same time. That is inefficient and often limiting. A pretrained model has already done the hard work of absorbing broad language structure, so fine-tuning can focus on specialization. In effect, the model shifts from broad language learning to narrow task learning.</p><p>That is why the two stages belong in this order. Pretraining comes first because it builds the reusable foundation. Fine-tuning comes later because it adapts that foundation to a concrete use case. Training from scratch on every downstream problem would be wasteful, especially when the model already spent substantial compute to learn general patterns. Pretraining makes the later step cheaper, faster, and more reliable.</p><p>This broad-first, specialized-second pattern is the organizing idea for the rest of the book. Pretraining gives the model its general language ability. Fine-tuning turns that ability into something practical for a specific application. Once you keep that distinction in mind, the rest of the pipeline becomes easier to understand and easier to reuse in later chapters.</p><h2>Transformer architecture as the common starting point</h2><p>Transformers are the architectural family that underlies most modern large language models. That statement matters because it separates the broad idea of an LLM from the specific design choices that make one model better suited for classification, another for translation, and another for open-ended text generation. In this book, the word transformer is the umbrella term, while GPT refers to one particular transformer design that we will build, train, and use throughout the later chapters.</p><p>At a high level, the transformer replaced earlier sequence-processing approaches by letting tokens interact directly with one another through self-attention. Instead of reading text strictly from left to right and compressing everything into a single hidden state, the model can decide which earlier tokens are most relevant for the current token representation. That one change is what makes transformers so effective at capturing context, long-distance dependencies, and subtle relationships inside text.</p><h2>Encoder, decoder, and the role of self-attention</h2><p>The original transformer organizes computation into two parts: an encoder that reads the input sequence and a decoder that produces the output sequence. The encoder builds contextual representations of the source text, and the decoder uses those representations to generate target text one step at a time. Self-attention sits inside both sides of that design and is the mechanism that lets each token incorporate information from other tokens in the sequence.</p><p>A useful way to think about self-attention is that it gives the model a learned routing system for context. When the model processes a word, it does not have to rely only on nearby words or a fixed memory window. It can place more weight on whichever earlier tokens are most useful for the current prediction. That flexibility is the reason transformers are so effective on text tasks where meaning depends on relationships across an entire passage rather than on local patterns alone.</p><h2>Where BERT-like and GPT-like models diverge</h2><p>Once the transformer family was established, two major directions emerged. One direction keeps the encoder and turns it into a strong representation learner. BERT-style models follow this path. They are trained with a masked-word prediction objective, which teaches the model to infer hidden tokens from their surrounding context. Because the encoder sees the whole input sequence at once, these models are especially effective when the goal is to understand text rather than to generate it. Classification, document tagging, and similar discriminative tasks fit this pattern well.</p><p>The other direction keeps the decoder and removes the encoder entirely. GPT-style models follow this path. Instead of reconstructing masked tokens from both sides of context, they are trained autoregressively to predict the next token from the tokens that came before it. This makes the model naturally suited to completion and generation, because the training objective matches the way the model will be used at inference time.</p><p>The difference is not only architectural, but behavioral. An encoder-focused model is built to produce a rich internal representation of an input. A decoder-only model is built to continue a sequence. That distinction becomes important as soon as you ask what the model should do with new text. If the task is sentiment classification or topic labeling, a BERT-like encoder is a natural fit. If the task is writing the next sentence, answering a prompt, or extending code, a GPT-like decoder is the better match.</p><p>A simple comparison makes the split concrete. Suppose the input is a product review. An encoder model reads the full review and produces a label such as positive or negative. A decoder-only model receives a prompt such as "Write a short summary of this review:" and then generates the summary token by token. The same source text can support both workflows, but the architecture shapes what the model is trained to do best.</p><h2>Why GPT is the book&#8217;s implementation target</h2><p>GPT is the model family this book will implement because it gives the cleanest path from transformer theory to a usable large language model. Its decoder-only structure is simpler than the original encoder-decoder layout, yet it is powerful enough to support the behaviors people now associate with LLMs: completion, rewriting, translation, summarization, and many forms of reasoning-by-prompting. That simplicity is practical as well as conceptual. Fewer moving parts make it easier to understand the model, train it from scratch, and inspect how each component contributes to the final result.</p><p>GPT is trained with self-supervised next-token prediction, which means the learning signal comes from the text itself rather than from manually labeled examples. This matters because it lets the model learn from enormous collections of ordinary text. Once trained on enough data and enough parameters, the model often acquires capabilities that were never explicitly programmed into it. It may respond sensibly to prompts it has not seen before, perform zero-shot tasks without examples, or improve further when given a few demonstrations in the prompt. Those behaviors are not magic; they are the result of scale, diverse pretraining data, and an objective that rewards learning general language structure rather than memorizing one narrow task.</p><p>That same scaling also gives rise to emergent behavior, meaning skills that appear only after the model reaches sufficient size and training breadth. In practice, this is one of the main reasons GPT-style models changed the field. A model trained only to predict the next token can end up handling far more than next-token completion when it is large enough and trained well enough. The exact boundary of what emerges is not fixed, but the pattern is clear enough to shape the modern LLM workflow.</p><p>For the rest of the book, GPT is the target model because it provides a direct path from first principles to a working language model. We will start with text data, build the token and embedding machinery it needs, implement attention and the transformer block, assemble the decoder-only network, and then train and adapt it for downstream tasks. Understanding why this architecture is the center of the book makes the later implementation chapters much easier to place: every component we build serves the same autoregressive generative model.</p><div><hr></div><h1>From roadmap to implementation milestones</h1><h2>The book&#8217;s three-stage implementation roadmap</h2><p>A large language model is easiest to understand when it is built in the same order that the book will use to construct it. Rather than treating tokenization, attention, pretraining, and fine-tuning as disconnected subjects, the implementation is organized as a sequence of milestones. Each milestone prepares the ground for the next one, and the full system only makes sense once all three stages are in place.</p><p>The central blueprint throughout the book is a GPT-style decoder-only model. That choice matters because it gives the implementation a clear direction. We are not building a general survey of neural architectures or a family of unrelated language systems. We are building one specific kind of model, then following the path from raw text to a pretrained base model and finally to task-specific adaptation. The roadmap is deliberately narrow so that every chapter contributes to the same working system.</p><h2>Stage 1: prepare the data and implement attention</h2><p>The first stage establishes the infrastructure that makes the model trainable at all. Before a GPT-like model can predict the next token, text has to be turned into a form that a neural network can process. That means preparing text data, organizing it into token sequences, and setting up the inputs the model will eventually consume during training. This early work is not a side topic. It is the point where language becomes model input.</p><p>At the same time, the book introduces the attention mechanism, which is the core computational idea behind the transformer family. The reason attention appears so early is simple: without it, the rest of the model would have no meaningful way to relate one token to another across a sequence. Preprocessing and attention form the foundation of the entire implementation. Once they are understood and working, the later chapters can focus on assembling larger systems rather than inventing the basic machinery from scratch.</p><h2>Stage 2: pretrain a GPT-like model and learn basic evaluation</h2><p>The second stage turns the components into a trainable language model. Here the book shifts from preparation to learning, using the GPT-style decoder-only architecture as the training target. Pretraining is the phase that gives the model its general language competence. It is also the phase that turns the architecture into a foundation model, meaning a base model that can later be reused for other tasks instead of being built anew each time.</p><p>This stage also introduces evaluation in its simplest practical form. Once training begins, the question is no longer only whether the code runs, but whether the model is improving in a measurable way. The book therefore treats evaluation as part of the pretraining workflow rather than as an afterthought. That gives the reader an early habit that matters throughout the rest of the book: train, inspect, compare, and only then decide whether the model is moving in the right direction.</p><p>Pretraining is expensive in the real world, especially when the goal is a large model trained on massive data. For that reason, the book uses small-scale examples when it demonstrates training from scratch. Those examples are pedagogical, not industrial. They show the mechanics of the process and the role of each training step without pretending to reproduce the compute budgets of a full-scale production system. Where it helps the workflow, the book also shows how to load openly available pretrained weights, which makes it possible to study and adapt a capable base model without paying the full cost of pretraining every time.</p><h2>Stage 3: adapt the pretrained model for downstream use</h2><p>The third stage begins after a pretrained base model exists. At that point the model has learned broad language patterns, but it still needs adaptation before it becomes useful for a specific application. Fine-tuning provides that adaptation. In this book, fine-tuning is used to turn the pretrained GPT-style model into a system that can follow instructions and into a model that can perform text classification.</p><p>This stage is where the idea of reuse becomes concrete. A foundation model is valuable because it is not the final product. It is the starting point for downstream work. Fine-tuning lets the same pretrained backbone serve different goals without rebuilding the language model from the ground up. The model becomes narrower in purpose but more useful in practice, because its general linguistic knowledge is redirected toward a task the user actually cares about.</p><p>The downstream examples matter because they show two different kinds of adaptation. Instruction following emphasizes interactive behavior and response generation. Classification emphasizes structured prediction over a fixed label space. Together they demonstrate that the same pretrained core can support more than one kind of application, as long as the adaptation stage is designed with the task in mind.</p><h2>Why the stages are ordered this way</h2><p>The ordering is not arbitrary. Data preparation must come first because the model cannot learn from text it cannot consume. Attention must come early because it is the mechanism that gives the decoder-only transformer its power. Pretraining comes next because it is the step that produces a general-purpose base model. Fine-tuning comes last because it only becomes meaningful once that base model exists.</p><p>This dependency chain is the organizing principle of the book. Each stage solves a different problem, but the later stages depend on the earlier ones being in place and understood. By the time you reach the adaptation chapters, you will already have the vocabulary, the architecture, and the training picture needed to understand what is changing and why. That is the reason for the roadmap: it turns a large technical subject into a sequence of concrete milestones that build toward a working GPT-style system.</p><h1>Chapter takeaways at a glance</h1><p>This chapter established the vocabulary the rest of the book will use. An LLM is a transformer-based language model trained to predict text autoregressively, one token at a time. That simple formulation is the center of gravity for everything that follows, because it explains both why these models can generate coherent sequences and why they need large amounts of text to learn effectively.</p><h2>What an LLM is and how transformer models fit</h2><p>The most important structural idea is that LLMs are not a separate species of model; they are language models built on the transformer architecture. The transformer supplies the mechanism that makes large-scale sequence modeling practical, especially through self-attention. Self-attention lets the model use the surrounding context in a sequence while it is producing the next token, so generation is informed by the full available history rather than by a narrow local window.</p><p>That relationship matters because it connects the high-level promise of LLMs to the mechanics we will implement later. The model is not simply memorizing text. It is learning how to use context, representation, and probability to continue a sequence in a way that generalizes across many forms of language use.</p><h2>GPT-style decoder-only models versus the broader transformer family</h2><p>The transformer family is broader than the GPT-style models this book builds. The original transformer design included encoder and decoder components, which are useful in settings where one sequence must be read and another sequence produced. GPT-style models, by contrast, use only the decoder side of the architecture and are trained for left-to-right generation.</p><p>That distinction is more than terminology. A decoder-only model is aligned with the autoregressive objective that defines modern text generation systems. It predicts the next token using the tokens already seen, which makes it the natural architecture for a book focused on building a GPT-style model from scratch. Later chapters will work within this narrower design rather than treating the transformer as a generic all-purpose template.</p><h2>From pretraining to foundation-model reuse</h2><p>The chapter also introduced the two-phase lifecycle that gives an LLM its practical value. First comes pretraining on a large corpus of unlabeled text. This stage teaches the model general statistical structure, broad language patterns, and many regularities that are not tied to a single supervised task. After that, the same base model is adapted to a downstream task through fine-tuning or another form of task-specific specialization.</p><p>This is where the idea of a foundation model becomes useful. A pretrained model is not the end of the workflow; it is a reusable starting point. Once the base model has absorbed enough general structure, it can be specialized for narrower goals such as classification or instruction following. Large-scale pretraining can also produce capabilities that were never directly supervised, because breadth and scale let the model internalize patterns that transfer into many downstream uses. The important point is that later adaptation depends on the earlier foundation, since the base model&#8217;s broad pretraining is what makes it generally useful before task-specific supervision is applied.</p><h2>Bridge to the next chapters</h2><p>With these distinctions in place, the next chapters can move from concepts to construction. The book will start with data preparation, then build attention step by step, and from there assemble and train the GPT-style model itself.</p><p>Use the url below to download the entire book</p>
      <p>
          <a href="https://onepagecode.substack.com/p/large-language-models-architectures">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Recurrent Neural Networks for Quant Trading: Time-Series Forecasting]]></title><description><![CDATA[Building LSTM, GRU, and simple RNN models to predict asset returns and identify sequential market patterns]]></description><link>https://onepagecode.substack.com/p/recurrent-neural-networks-for-quant</link><guid isPermaLink="false">https://onepagecode.substack.com/p/recurrent-neural-networks-for-quant</guid><dc:creator><![CDATA[Onepagecode]]></dc:creator><pubDate>Thu, 25 Jun 2026 20:00:48 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!jTIX!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbbdbbff9-ea99-4059-8924-1f676617d59f_1000x496.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h4>Use the URL at the end of this article to download the source code!</h4><h2>Imports and configuration</h2><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;ff526b01-eea2-42b0-8bbc-c76d39b5a52d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">import warnings
warnings.filterwarnings('ignore')</code></pre></div><p>The purpose here is simply to quiet down warning messages before the rest of the notebook runs. The warnings module is imported first so Python has a standard way to control how warnings are handled, and then the warning filter is set to ignore. Behind the scenes, that tells the interpreter not to print warning notices to the screen even if pandas or another library encounters something slightly unusual, such as a deprecated behavior or a formatting issue. Nothing is calculated or displayed here, so there is no saved output. The effect is just to keep later notebook output cleaner and easier to read while the data-processing steps continue.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;f4d1cde1-8cd8-487e-8648-be7700dcd42f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from pathlib import Path

import numpy as np
import pandas as pd</code></pre></div><p>This cell sets up the basic tools that the rest of the notebook will rely on. It brings in Path from the standard library so file locations can be handled cleanly, especially when building paths to data files. It also imports NumPy and pandas, which are the main libraries used for numerical work and tabular data manipulation throughout the notebook. There is no visible output here because the cell is only preparing the environment; it defines the names that later cells will use, but it does not yet load data, compute anything, or display results.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;e4aeaee5-713d-464d-9979-b52a095076a5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">np.random.seed(42)</code></pre></div><p>This line sets the random seed for NumPy&#8217;s random number generator to 42, which makes any future random operations behave the same way each time the notebook runs. That matters when later steps involve sampling, shuffling, or any other stochastic process, because it keeps results reproducible and easier to debug or compare. Nothing is displayed because the command only changes the internal random state; it prepares the environment for consistent behavior rather than producing visible output.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;e15787b9-2420-41f6-b911-a9acdc8e27cf&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">idx = pd.IndexSlice</code></pre></div><p>This line creates a handy shortcut for slicing pandas objects with MultiIndexes. A MultiIndex is pandas&#8217; way of giving a table more than one level of indexing, such as date and ticker together, and selecting pieces from it can get a little awkward to write repeatedly. By assigning pandas&#8217; special IndexSlice helper to the name idx, the notebook sets up a cleaner way to express those multi-level selections later on. Nothing is displayed when the cell runs because it is only defining a reusable object for future use, not producing any data or results yet.</p><h2>Create the daily dataset</h2><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;9f7593bf-9353-4381-bace-6395ff15e668&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">DATA_DIR = Path('..', 'data')</code></pre></div><p>This line sets up a path object that points to the data directory one level above the current working location. It gives the notebook a reusable reference for where the input and output files live, so later cells can load the stock price data and save the prepared datasets without hardcoding long file paths each time. Nothing is displayed when it runs because it is just defining a location for future use, not performing a calculation or producing a result.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;1b679b7e-a439-49b3-8f3b-dfa8330e08f0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">prices = (pd.read_hdf(DATA_DIR / 'assets.h5', 'quandl/wiki/prices')
          .loc[idx['2010':'2017', :], ['adj_close', 'adj_volume']])
prices.info()</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;2fa5417b-8ee2-41bb-8abe-d818321c1b09&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">&lt;class 'pandas.core.frame.DataFrame'&gt;
MultiIndex: 5698754 entries, (Timestamp('2010-01-04 00:00:00'), 'A') to (Timestamp('2017-12-29 00:00:00'), 'ZUMZ')
Data columns (total 2 columns):
 #   Column      Dtype  
---  ------      -----  
 0   adj_close   float64
 1   adj_volume  float64
dtypes: float64(2)
memory usage: 109.5+ MB</code></pre></div><p>The cell loads the stock price data that will be used for the daily return dataset and immediately trims it down to the part needed for the next steps. It reads the HDF5 file from the data directory, pulls out the Quandl Wiki prices table, and then filters the rows to dates from 2010 through 2017 while keeping only the adjusted close and adjusted volume fields. That means the result is already narrowed to the time span and columns relevant for later return calculations, instead of carrying around the full raw dataset.</p><p>The saved output is the summary of that filtered table, which is what pandas shows when you ask for the data frame information. It confirms that the object is a DataFrame with a MultiIndex, meaning each row is identified by both a date and a ticker symbol. The index contains 5,698,754 rows, stretching from the first trading day in 2010 for ticker A through the last trading day in 2017 for ticker ZUMZ. The two retained columns are both floating-point values, one for adjusted closing price and one for adjusted trading volume, and the memory usage is a little over 100 MB. That output is useful because it verifies that the load worked, the filtering by date succeeded, and the dataset has the structure expected for the return calculations that come next.</p><h3>Filter for the stocks with the highest trading activity</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;cabca65a-1015-4573-878e-207ce31d73c1&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">n_dates = len(prices.index.unique('date'))
dollar_vol = (prices.adj_close.mul(prices.adj_volume)
              .unstack('ticker')
              .dropna(thresh=int(.95 * n_dates), axis=1)
              .rank(ascending=False, axis=1)
              .stack('ticker'))</code></pre></div><p>The purpose here is to measure how heavily each stock is traded on each date and then use that information to filter the universe of stocks. First, the number of unique trading dates is counted so the code knows how much history is available. That count is then used as a benchmark for deciding whether a ticker has enough observations to be kept.</p><p>Next, adjusted close prices are multiplied by adjusted volume. That combination gives dollar volume, which is a better measure of trading activity than volume alone because it reflects both how many shares traded and what those shares were worth. The result is then reshaped so that tickers become columns and dates remain the rows, making it easier to compare all stocks side by side on each day.</p><p>After that, columns with too many missing values are removed. The threshold requires a ticker to have data on at least 95% of the available dates, so stocks with sparse histories are filtered out before any ranking happens. Once the data is cleaned this way, each date&#8217;s stocks are ranked by dollar volume, with the most actively traded stocks receiving the highest priority in that daily cross-section. Finally, the table is stacked back into a long format indexed by ticker again.</p><p>The result, dollar_vol, is a series-like object that contains per-date dollar-volume rankings for the stocks that passed the availability filter. There is no displayed output from the cell itself, but the variable it creates is an important intermediate step for selecting the most liquid tickers in the next stage.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;51d799b2-bed8-4c29-a89d-76d3f79d4bcf&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">most_traded = dollar_vol.groupby(level='ticker').mean().nsmallest(500).index</code></pre></div><p>This line picks out the tickers that will make up the stock universe for the daily dataset. It starts from the dollar-volume series, groups the data by ticker, and averages that value across time for each stock. The result is a single summary measure per ticker showing its typical trading activity over the period. From there, the 500 tickers with the smallest values are selected by asking for the smallest entries in that grouped list, and only their index labels are kept. The saved result is just that ticker list, which becomes the filter used in the next step to limit the dataset to a fixed set of stocks.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;d978a3b2-1692-43a0-9117-53164b13a894&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">returns = (prices.loc[idx[:, most_traded], 'adj_close']
           .unstack('ticker')
           .pct_change()
           .sort_index(ascending=False))
returns.info()</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;0733eb5e-dc41-49b2-b764-117c0fc26303&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">&lt;class 'pandas.core.frame.DataFrame'&gt;
DatetimeIndex: 2013 entries, 2017-12-29 to 2010-01-04
Columns: 500 entries, AAPL to CNC
dtypes: float64(500)
memory usage: 7.7 MB</code></pre></div><p>The goal here is to turn the filtered price data into a clean matrix of daily returns that can be used for sequence modeling. The first part selects only the adjusted close prices for the stocks that were identified as the most traded, using the date and ticker structure of the original data to pull out exactly that slice. After that, the ticker labels are spread out into separate columns so each stock has its own return series side by side, which makes the data much easier to work with for machine learning. Once the prices are in this wide format, percentage change is computed from one day to the next, converting raw prices into daily returns. The final step reorders the dates from newest to oldest, which matches the way later parts of the notebook build rolling time windows by stepping backward through time.</p><p>The printed output is a quick check on the shape and type of the resulting table. It shows a DataFrame with 2013 trading dates running from late 2017 back to early 2010, and 500 columns, one for each selected ticker from AAPL through CNC. Every column is stored as a float, which makes sense because returns are continuous numeric values. The reported memory usage also gives a sense of scale: although the dataset is fairly wide, it is still compact enough to handle comfortably in memory.</p><h3>Assemble 21-day return sequences</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;bdb9bcc2-2043-40dd-b36e-681826ec15b2&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">n = len(returns)
T = 21 # days
tcols = list(range(T))
tickers = returns.columns</code></pre></div><p>The purpose here is to set up a few basic pieces of information that the later sequence-building step will need. First, the length of the returns data is stored in n, which gives the total number of time steps available. Then T is set to 21, defining the size of the historical window in days that each sample will use. That choice matters because it determines how much past information will be included whenever the data is turned into training examples. Next, tcols is created as a simple list of column positions from 0 up to 20, matching the 21 days in the window. Finally, the tickers variable captures the column names from the returns data, so the code has the set of stock symbols available when it reshapes or labels the sequence data later on. Nothing is displayed or saved yet, because this cell is just preparing the key dimensions and labels for the next transformations.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;ecc7ce2c-1e05-4115-92c8-b5cb78a56fa5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">data = pd.DataFrame()
for i in range(n-T-1):
    df = returns.iloc[i:i+T+1]
    date = df.index.max()
    data = pd.concat([data, 
                      df.reset_index(drop=True).T
                      .assign(date=date, ticker=tickers)
                      .set_index(['ticker', 'date'])])
data = data.rename(columns={0: 'label'}).sort_index().dropna()
data.loc[:, tcols[1:]] = (data.loc[:, tcols[1:]].apply(lambda x: x.clip(lower=x.quantile(.01),
                                                  upper=x.quantile(.99))))
data.info()</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;558b0720-fd36-4d64-aa5d-4e01f686be1c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">&lt;class 'pandas.core.frame.DataFrame'&gt;
MultiIndex: 995499 entries, ('A', Timestamp('2010-02-04 00:00:00')) to ('ZION', Timestamp('2017-12-29 00:00:00'))
Data columns (total 22 columns):
 #   Column  Non-Null Count   Dtype  
---  ------  --------------   -----  
 0   label   995499 non-null  float64
 1   1       995499 non-null  float64
 2   2       995499 non-null  float64
 3   3       995499 non-null  float64
 4   4       995499 non-null  float64
 5   5       995499 non-null  float64
 6   6       995499 non-null  float64
 7   7       995499 non-null  float64
 8   8       995499 non-null  float64
 9   9       995499 non-null  float64
 10  10      995499 non-null  float64
 11  11      995499 non-null  float64
 12  12      995499 non-null  float64
 13  13      995499 non-null  float64
 14  14      995499 non-null  float64
 15  15      995499 non-null  float64
 16  16      995499 non-null  float64
 17  17      995499 non-null  float64
 18  18      995499 non-null  float64
 19  19      995499 non-null  float64
 20  20      995499 non-null  float64
 21  21      995499 non-null  float64
dtypes: float64(22)
memory usage: 171.0+ MB</code></pre></div><p>The goal here is to turn the raw return series into a supervised-learning table where each row represents one stock at one reference date, with a fixed-length history of past returns and a target value for the next move. The empty DataFrame is created first as a container, and then the loop walks forward through the return history one step at a time. For each position, it grabs a window of T plus one rows from the returns data: the extra row is important because it gives the forward value that will become the prediction target, while the first T rows serve as the input history.</p><p>Inside the loop, that window is reshaped so that time runs across columns and each ticker becomes its own row. The date attached to the sample is the last date in that window, and the ticker names are paired with it so the result can be indexed by both stock and date. Concatenating each window into the growing table builds a stacked panel of samples across the full time span, rather than leaving the data in its original time-series form.</p><p>After all windows are collected, the first column is renamed to label, which matches the convention that the forward return will be the value to predict. Sorting the index makes the rows orderly by ticker and date, and dropping missing values removes any incomplete windows that could have been created at the edges of the series or by gaps in the underlying data. The next step trims the return-history columns at the 1st and 99th percentiles, which keeps unusually large spikes from dominating model training while leaving the overall pattern of the data intact.</p><p>The saved output from data.info() shows the finished structure of the table. It is a large DataFrame with 995,499 rows, indexed by a two-level MultiIndex of ticker and date, which confirms that each observation is one stock on one day. There are 22 floating-point columns in total: one label column plus 21 history columns, matching the expected window length. Every column is fully populated after the cleaning step, and the memory usage reflects how large this stacked training set becomes once all of the rolling windows have been assembled.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;747cee2e-7814-40d3-b9a8-31af44a193fb&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">data.shape</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;50cea232-b3c1-4dcb-91c4-b976213adff7&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">(995499, 22)</code></pre></div><p>This cell is a quick sanity check on the prepared dataset. By asking for the shape of the data table, it reveals how many rows and columns are currently in memory after all of the earlier filtering, reshaping, and labeling steps. The result, 995499 by 22, means the dataset contains 995,499 individual examples and 22 fields for each one. Those fields include the target value and the fixed-length history features that were built from stock returns, so the column count reflects the final supervised-learning structure of the table. Seeing a large row count here is expected, because each ticker-date pair becomes its own sample once the time series has been stacked into a panel-like format.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;421e8623-5a1f-43a7-9d37-58f570cdc892&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">data.to_hdf('data.h5', 'returns_daily')</code></pre></div><p>The purpose here is to save the prepared daily return dataset so it can be reused later without rebuilding it from scratch. The object being written, data, already contains the cleaned and stacked daily samples created in the earlier steps, with one row per ticker-date pair and columns for the forward return label and the historical return features. Writing it to an HDF5 file stores that table in a compact, structured format that works well for larger datasets like this one.</p><p>The file name, data.h5, identifies the storage location, and the key returns_daily tells HDF5 which dataset inside the file this table belongs to. If that key already exists, it will be replaced with the new version of the data. Since saving to disk does not produce a visible result by itself, there is no output shown after the cell runs. The important effect is that the daily dataset is now preserved on disk and can be loaded again later for modeling or analysis.</p><h2>Prepare the weekly dataset</h2><p>We begin by reading in the Quandl dataset of adjusted stock prices.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;3c21ef10-9c80-4f85-ba94-f1deb27d1f85&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">prices = (pd.read_hdf(DATA_DIR / 'assets.h5', 'quandl/wiki/prices')
          .adj_close
          .unstack().loc['2007':])
prices.info()</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;c2dd4658-02f5-4400-a097-76558c7f25b9&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">&lt;class 'pandas.core.frame.DataFrame'&gt;
DatetimeIndex: 2896 entries, 2007-01-01 to 2018-03-27
Columns: 3199 entries, A to ZUMZ
dtypes: float64(3199)
memory usage: 70.7 MB</code></pre></div><p>The purpose here is to load the adjusted closing prices for all available stocks and reshape them into a regular date-by-ticker table that is easy to work with for later calculations. The data is read from the HDF5 file, the adjusted close field is selected, and then the index is unstacked so that dates become the rows and tickers become the columns. That turns the original stacked market data into a wide price matrix where each cell is the price of one stock on one day. The slice starting at 2007 trims the history to begin in that year, which keeps the dataset aligned with the later weekly analysis.</p><p>The final line asks pandas to summarize the resulting object, and the saved output is exactly that summary. It shows a DataFrame with 2,896 daily rows spanning from 2007-01-01 to 2018-03-27, and 3,199 ticker columns ranging from A to ZUMZ. Every column is stored as float64, which makes sense because prices are numeric and may include missing values represented internally as floating-point data. The memory usage gives a sense of the table&#8217;s size, showing that the reshaped price history is fairly large but still manageable in memory for the later processing steps.</p><h3>Convert the data to weekly intervals</h3><p>We begin by computing weekly returns for nearly 2,500 stocks that have complete data over the period from 2008 to 2017, using the procedure below:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;7a27dc71-5b19-4d90-a0ac-265ba52cc187&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">returns = (prices
           .resample('W')
           .last()
           .pct_change()
           .loc['2008': '2017']
           .dropna(axis=1)
           .sort_index(ascending=False))
returns.info()</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;4071e054-012e-4b89-b65c-21673dceb929&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">&lt;class 'pandas.core.frame.DataFrame'&gt;
DatetimeIndex: 522 entries, 2017-12-31 to 2008-01-06
Freq: -1W-SUN
Columns: 2489 entries, A to ZUMZ
dtypes: float64(2489)
memory usage: 9.9 MB</code></pre></div><p>The goal here is to turn the raw price history into a cleaner weekly return table that is ready for the next modeling step. The prices are first grouped into weekly periods, and for each week the last available trading day is kept. That gives one representative price per stock per week, which is a common way to smooth daily noise and align the data to a consistent weekly cadence. From there, percentage change is computed, so each value now represents the stock&#8217;s return from one week to the next rather than the price level itself.</p><p>After the weekly returns are created, the date range is narrowed to 2008 through 2017 so the dataset covers the period of interest. Any stock columns with missing values are removed, which leaves only tickers with a complete enough weekly history to work with. The rows are then sorted in descending date order, so the most recent week appears first and earlier weeks follow below it. That reversed order is useful later when the notebook builds rolling windows over time.</p><p>The saved output from the information summary confirms exactly what remains after those transformations. The result is a DataFrame with 522 weekly observations, spanning from the end of 2017 back to the first week of 2008. It contains 2,489 stock columns, and every one of them is stored as a float64 return series. The memory usage is modest, which makes sense because returns are compact numeric values rather than full price histories.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;ce43f21c-b10a-4f91-b62c-1a8ee888a74a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">returns.head().append(returns.tail())</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;c9db4b94-dcfa-458e-8a0b-c98e0d7cde02&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">ticker             A       AAL       AAN      AAON       AAP      AAPL  \
date                                                                     
2017-12-31 -0.005642 -0.010648 -0.010184 -0.001361 -0.008553 -0.033027   
2017-12-24 -0.003846  0.029965  0.090171  0.044034 -0.001490  0.006557   
2017-12-17  0.003413  0.000784 -0.052591 -0.014006  0.003888  0.026569   
2017-12-10 -0.019071  0.041012 -0.005359 -0.017882  0.010375 -0.009822   
2017-12-03 -0.009660  0.009267  0.105501  0.013947  0.112630 -0.022404   
2008-02-03  0.038265  0.252238  0.002941  0.095182  0.097833  0.028767   
2008-01-27 -0.013963 -0.048762  0.191310  0.071788  0.043997 -0.194286   
2008-01-20 -0.065000  0.086627 -0.080541 -0.054762 -0.007176 -0.065609   
2008-01-13  0.035375 -0.041902 -0.037818 -0.046538 -0.101486 -0.040878   
2008-01-06 -0.072553 -0.156356 -0.068707 -0.133301 -0.065496 -0.098984   

ticker          AAWW      ABAX       ABC      ABCB  ...      ZEUS      ZIGO  \
date                                                ...                       
2017-12-31 -0.024938 -0.001814 -0.006922 -0.019329  ... -0.029797  0.000000   
2017-12-24  0.046087  0.032681 -0.007620  0.017598  ...  0.032153  0.000000   
2017-12-17  0.004367  0.008396  0.074625  0.026567  ...  0.036715  0.000000   
2017-12-10 -0.028014 -0.010386  0.020600 -0.054271  ... -0.002410  0.000000   
2017-12-03  0.073838 -0.028456  0.045796  0.024717  ...  0.065742  0.000000   
2008-02-03  0.006245 -0.078058  0.036913  0.083217  ...  0.137066  0.127561   
2008-01-27 -0.008984 -0.090807 -0.034771  0.054572  ...  0.018349 -0.026292   
2008-01-20  0.015818 -0.019721 -0.015219 -0.044397  ...  0.040573  0.010999   
2008-01-13 -0.052095  0.097385  0.080137 -0.017313  ... -0.054176 -0.047993   
2008-01-06 -0.029478 -0.098374 -0.037363 -0.132733  ... -0.027290 -0.075806   

ticker          ZINC      ZION      ZIOP      ZIXI       ZLC       ZMH  \
date                                                                     
2017-12-31  0.000000 -0.009741  0.022222 -0.015730  0.000000  0.000000   
2017-12-24  0.000000  0.026395 -0.068966 -0.024123  0.000000  0.000000   
2017-12-17  0.000000 -0.018064 -0.018059  0.075472  0.000000  0.000000   
2017-12-10  0.000000  0.016973 -0.015556 -0.055679  0.000000  0.000000   
2017-12-03  0.000000  0.080475  0.014656 -0.006637  0.000000  0.000000   
2008-02-03  0.286550  0.167722 -0.087879  0.069364  0.171949  0.193189   
2008-01-27 -0.046975  0.136418 -0.003021  0.145695  0.042164 -0.014553   
2008-01-20 -0.167109 -0.051614 -0.054286 -0.124638  0.037172 -0.037312   
2008-01-13 -0.102381  0.037264 -0.022346 -0.172662  0.011799  0.051880   
2008-01-06 -0.004739 -0.081058  0.101538 -0.143737 -0.134100  0.000752   

ticker           ZQK      ZUMZ  
date                            
2017-12-31  0.000000 -0.029138  
2017-12-24  0.000000  0.067164  
2017-12-17  0.000000 -0.051887  
2017-12-10  0.000000  0.062657  
2017-12-03  0.000000  0.047244  
2008-02-03  0.127811  0.149083  
2008-01-27  0.141892  0.118666  
2008-01-20 -0.030144 -0.076969  
2008-01-13  0.018692 -0.094249  
2008-01-06 -0.133102 -0.269012  

[10 rows x 2489 columns]</code></pre></div><p>This cell is just a quick peek at the return matrix so you can check that the data has been arranged the way you expect before moving on. It takes the first few rows and the last few rows of the returns table and joins them together into one display, which is a handy way to inspect both ends of a long time series without printing the entire dataset.</p><p>The output shows weekly return values laid out with dates down the left and tickers across the top. Because the table has already been sorted in reverse chronological order, the newest dates appear at the top and the oldest dates appear at the bottom. That is why the first visible rows are from late 2017 while the last ones are from early 2008. Each cell is a percent return for one stock in one week, so positive values indicate gains and negative values indicate losses. A lot of the later tickers have zeros in the 2017 rows because some securities no longer have active data by the end of the sample, and those missing or inactive entries have effectively been filled in a way that makes the table rectangular.</p><p>The shape in the output also tells you something important about the dataset. There are 2,489 ticker columns, which means this return panel is very wide and contains a large universe of stocks. By combining the head and tail, the cell gives a compact sanity check that the date ordering, the ticker layout, and the numerical values all look sensible before the data is used for the next preparation step.</p><h3>Build and stack 52-week sequences</h3><p>We will build the 52-week sequences in a stacked layout:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;eaea6c94-e578-4c6d-b752-e59ced4fe98c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">n = len(returns)
T = 52 # weeks
tcols = list(range(T))
tickers = returns.columns</code></pre></div><p>The cell is setting up a few simple pieces of bookkeeping before the weekly sequence-building step. It first measures how many time points are available in the returns data, which gives a useful limit for later looping through the series. It then defines the sequence length as 52 weeks, so each example will represent one full year of weekly history. After that, it creates a list of column positions from 0 up to 51, which will be used to label the different weeks inside each sequence. Finally, it stores the ticker symbols from the returns table so the code can keep track of which stock each row belongs to when the data is reshaped. There is no saved output here because nothing is being displayed yet; this cell simply prepares values that the following cells will rely on when constructing the weekly dataset.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;561aa4a9-dffc-4091-bc66-02cca7a339b1&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">data = pd.DataFrame()
for i in range(n-T-1):
    df = returns.iloc[i:i+T+1]
    date = df.index.max()    
    data = pd.concat([data, (df.reset_index(drop=True).T
                             .assign(date=date, ticker=tickers)
                             .set_index(['ticker', 'date']))])
data.info()</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;eec72d60-c72b-4a71-aac4-76d8608aa9c5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">&lt;class 'pandas.core.frame.DataFrame'&gt;
MultiIndex: 1167341 entries, ('A', Timestamp('2017-12-31 00:00:00')) to ('ZUMZ', Timestamp('2009-01-11 00:00:00'))
Data columns (total 53 columns):
 #   Column  Non-Null Count    Dtype  
---  ------  --------------    -----  
 0   0       1167341 non-null  float64
 1   1       1167341 non-null  float64
 2   2       1167341 non-null  float64
 3   3       1167341 non-null  float64
 4   4       1167341 non-null  float64
 5   5       1167341 non-null  float64
 6   6       1167341 non-null  float64
 7   7       1167341 non-null  float64
 8   8       1167341 non-null  float64
 9   9       1167341 non-null  float64
 10  10      1167341 non-null  float64
 11  11      1167341 non-null  float64
 12  12      1167341 non-null  float64
 13  13      1167341 non-null  float64
 14  14      1167341 non-null  float64
 15  15      1167341 non-null  float64
 16  16      1167341 non-null  float64
 17  17      1167341 non-null  float64
 18  18      1167341 non-null  float64
 19  19      1167341 non-null  float64
 20  20      1167341 non-null  float64
 21  21      1167341 non-null  float64
 22  22      1167341 non-null  float64
 23  23      1167341 non-null  float64
 24  24      1167341 non-null  float64
 25  25      1167341 non-null  float64
 26  26      1167341 non-null  float64
 27  27      1167341 non-null  float64
 28  28      1167341 non-null  float64
 29  29      1167341 non-null  float64
 30  30      1167341 non-null  float64
 31  31      1167341 non-null  float64
 32  32      1167341 non-null  float64
 33  33      1167341 non-null  float64
 34  34      1167341 non-null  float64
 35  35      1167341 non-null  float64
 36  36      1167341 non-null  float64
 37  37      1167341 non-null  float64
 38  38      1167341 non-null  float64
 39  39      1167341 non-null  float64
 40  40      1167341 non-null  float64
 41  41      1167341 non-null  float64
 42  42      1167341 non-null  float64
 43  43      1167341 non-null  float64
 44  44      1167341 non-null  float64
 45  45      1167341 non-null  float64
 46  46      1167341 non-null  float64
 47  47      1167341 non-null  float64
 48  48      1167341 non-null  float64
 49  49      1167341 non-null  float64
 50  50      1167341 non-null  float64
 51  51      1167341 non-null  float64
 52  52      1167341 non-null  float64
dtypes: float64(53)
memory usage: 476.6+ MB</code></pre></div><p>The purpose of this step is to turn a long table of return data into many supervised-learning samples, where each sample contains a fixed history window plus the next value to predict. The loop walks forward through the return series one starting point at a time, taking a slice of length T plus one extra row. That extra row is important because it gives the forward value that will sit alongside the history. For each slice, the code finds the most recent date in that window and uses it as the reference date for the sample.</p><p>Inside the loop, the selected block is reshaped so that time becomes the vertical direction and tickers become rows after the transpose. The date and ticker labels are then attached, and those two fields are moved into the index. This creates a panel-like layout with one row per ticker for a given reference date, which is exactly the kind of structure that is convenient for sequence models later on. Each pass through the loop produces another batch of these stacked samples, and all of them are concatenated into the growing DataFrame named data.</p><p>The output from data.info() confirms that the transformation worked and shows the shape of the resulting dataset. The table now has a MultiIndex made from ticker and date, with over 1.1 million rows, which means the rolling-window construction produced a very large number of ticker-date examples. It also has 53 numeric columns, all stored as float64. Those columns represent the values from the time window, with the final column coming from the forward day that was included in each slice. The fact that every entry is non-null indicates that the earlier cleaning steps succeeded in removing incomplete windows, and the memory usage reflects the size of the assembled training set.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;041c54a9-0872-4968-9ef5-a546d622e419&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">data[tcols] = (data[tcols].apply(lambda x: x.clip(lower=x.quantile(.01),
                                                  upper=x.quantile(.99))))</code></pre></div><p>The goal here is to trim extreme return values so the model sees a cleaner, more stable version of the data. The selected return-history columns are taken from the larger dataset and each column is processed separately. For every column, the code finds the 1st percentile and the 99th percentile of that column&#8217;s values, then limits anything below the lower cutoff up to that cutoff and anything above the upper cutoff down to that cutoff. Behind the scenes, this leaves the middle 98% of values unchanged while shrinking only the most unusual lows and highs, which helps reduce the influence of outliers without removing rows entirely.</p><p>The result is written back into the same set of columns in the dataset, so the underlying table keeps its shape but now contains clipped values instead of extreme spikes. There is no printed output because the operation is an in-place-style data cleanup step rather than something meant to display a result. The effect only becomes visible later when the prepared data is used for modeling or saved to disk.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;ad018b35-d442-4c08-b5cc-5702c059af39&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">data = data.rename(columns={0: 'fwd_returns'})</code></pre></div><p>The purpose here is to give the first column in the dataset a more meaningful name. Up to this point, that column is still carrying the generic name 0 because it came from the way the stacked return window was assembled. Renaming it to fwd_returns makes the table easier to understand and signals that this column holds the forward return value, which is the quantity being used as the target for the weekly dataset. Nothing is displayed when the cell runs because the operation simply updates the column label in the existing dataframe; the underlying values stay the same, only the name changes.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;906f42e9-94da-4be6-854d-149c361d5637&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">data['label'] = (data['fwd_returns'] &gt; 0).astype(int)</code></pre></div><p>The purpose here is to turn the forward return into a simple direction signal that a model can learn more easily. The existing forward-return values are checked one by one to see whether they are positive, and that comparison produces a True-or-False result for each row. Those logical values are then converted into integers, so True becomes 1 and False becomes 0. The new values are written into the label column, replacing whatever was there before, so the dataset now carries a binary target instead of a continuous return. Since this step only changes the contents of the dataframe and does not print anything or save a file by itself, there is no visible output from the cell.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;c0034d73-92f4-4cbd-a947-e300ab8bfbf7&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">data.shape</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;a0d578f4-0f2c-4255-a843-68fd7300d137&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">(1167341, 54)</code></pre></div><p>The cell checks the shape of the prepared data table so you can see how large the dataset is after all the earlier reshaping and cleaning steps. The result shows 1,167,341 rows and 54 columns. That size makes sense for a stacked time-series dataset: each row represents one ticker on one date, and the columns include the target value along with the fixed-length history features used for modeling. The row count tells you there are over a million ticker-date samples available, while the 54 columns reflect the label plus the return-window features and any remaining identifying information needed for the stored table.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;38342d6e-9658-4925-aa8c-d889aea8985c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">data.sort_index().to_hdf('data.h5', 'returns_weekly')</code></pre></div><p>The final step is to write the weekly returns dataset into an HDF5 file so it can be reused later without rebuilding it from scratch. Before saving, the data is sorted by its index, which makes the rows appear in a consistent order, usually grouped cleanly by ticker and date. That matters because the dataset was assembled from many stacked windows of weekly returns, so sorting helps keep the stored table organized and easier to retrieve efficiently. The result is saved into the file named data.h5 under the key returns_weekly, which is the version of the weekly feature-and-label table that downstream model-building steps can load directly. Since this operation only writes data to disk, it produces no visible output when the cell runs.</p><div><hr></div><h2>Notebook 2 of 8: <code>01_univariate_time_series_regression</code></h2><p><em>Source file: `01_univariate_time_series_regression_processed.ipynb`</em></p><div><hr></div><h1>Recurrent Neural Networks</h1><h2>Regression on a Single Time Series</h2><p>This notebook shows how to predict the S and P 500 index with a recurrent neural network.</p><h2>Imports and configuration</h2><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;5e1900a6-d278-4a46-9796-e48767fb0d79&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">import warnings
warnings.filterwarnings('ignore')</code></pre></div><p>The purpose of this cell is to quiet down non-critical warning messages so the notebook output stays easier to read. It first imports the warnings module, which is Python&#8217;s built-in way of controlling how warning messages are handled. Then it tells Python to ignore warnings from that point onward. Behind the scenes, this changes the warning filter for the current session, so issues that would normally appear as yellow warning messages will be suppressed unless something more serious goes wrong. Since the cell only adjusts this display behavior and does not compute anything or display a result, there is no saved output.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;eea47c4f-f685-43b7-b8e3-012e542cac3c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">%matplotlib inline

from pathlib import Path

import numpy as np
import pandas as pd
import pandas_datareader.data as web
from scipy.stats import spearmanr

from sklearn.metrics import mean_squared_error
from sklearn.preprocessing import MinMaxScaler

import tensorflow as tf
from tensorflow.keras.callbacks import ModelCheckpoint, EarlyStopping
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, LSTM
from tensorflow import keras

import matplotlib.pyplot as plt
import seaborn as sns</code></pre></div><p>The purpose of this cell is to set up everything the notebook will need for time-series forecasting, data preparation, model training, evaluation, and plotting. It begins by enabling inline plotting so that any figures created later will appear directly inside the notebook rather than in a separate window. After that, it brings in a collection of libraries for handling files and paths, working with numerical arrays and tabular data, downloading market data, measuring correlation, scaling values, building the neural network, and visualizing results.</p><p>Several of these imports are especially important for the workflow that follows. The path utility will later help manage output files and saved models in a cleaner, platform-independent way. NumPy and pandas provide the basic data structures for the time series. pandas_datareader is used to fetch the S&amp;P 500 data from an external source. Spearman correlation and mean squared error are imported so the model&#8217;s predictions can be judged both by ranking agreement and by average error. MinMaxScaler prepares the data for neural network training by rescaling it into a bounded range, which is often helpful for LSTM models. TensorFlow and Keras supply the recurrent network layers, the Sequential model container, and the callbacks that will later save the best version of the model and stop training early if validation performance stops improving. Finally, Matplotlib and Seaborn are loaded for the plots that will be used to inspect the data, monitor training, and compare predictions with actual values.</p><p>There is no saved output from this cell because it only loads tools and definitions into memory. Nothing is computed yet, so the notebook stays quiet here; the real work begins in later cells that use these imports to fetch data, build the dataset, and train the network.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;e2884231-13be-4320-a0d2-672bb9fa73ed&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">gpu_devices = tf.config.experimental.list_physical_devices('GPU')
if gpu_devices:
    print('Using GPU')
    tf.config.experimental.set_memory_growth(gpu_devices[0], True)
else:
    print('Using CPU')</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;d547e363-ea42-44b5-9e0d-f0d916017054&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">Using CPU</code></pre></div><p>The purpose here is to check whether TensorFlow can see a graphics processing unit and, if it can, to configure it so memory is allocated more carefully. The first line asks TensorFlow for a list of physical devices that belong to the GPU category. That returns either an empty list if no GPU is available or one or more GPU devices if the machine has them. The next part branches based on that result. When a GPU is found, the notebook prints a message saying it will use the GPU and then enables memory growth on the first GPU device, which tells TensorFlow not to grab all of the GPU memory at once at startup. That can help avoid unnecessary memory reservation and makes the setup friendlier when other processes may also need the GPU.</p><p>The saved output shows &#8220;Using CPU,&#8221; which means the device search came back empty and no GPU was detected by TensorFlow in the current environment. As a result, the memory-growth setting is skipped, because there is no GPU to configure. This kind of check is useful at the beginning of a notebook because it confirms what hardware the model will run on and makes the rest of the training behavior easier to interpret.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;831f628d-1e54-4a12-82dc-efd1cf58d64c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">sns.set_style('whitegrid')
np.random.seed(42)</code></pre></div><p>The purpose of this cell is to set up a consistent look for the plots and make the random behavior reproducible. The plotting style is changed to a white-grid theme so that later figures will have a cleaner background with light grid lines, which makes trends and comparisons easier to read. Setting the NumPy random seed to 42 fixes the starting point for any random number generation used later in the workflow, so operations that depend on randomness will produce the same results each time the notebook is run. There is no visible output because neither of these actions produces a printed result; they simply configure the environment for the cells that follow.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;e732e3ee-232f-4460-9fcf-0c7b1fc41d71&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">results_path = Path('results', 'univariate_time_series')
if not results_path.exists():
    results_path.mkdir(parents=True)</code></pre></div><p>The purpose here is to make sure there is a folder available for saving later results from the analysis. It first builds a path pointing to a directory named results/univariate<em>time</em>series, which is a convenient place to store files such as model checkpoints, plots, or any other outputs created during the workflow. After that, it checks whether that folder already exists on disk. If the directory is missing, it creates it, and the parents option allows any needed parent folders to be created as well. Since there is no saved output, nothing is displayed when the cell runs; instead, it quietly prepares the file system so later steps can write their results without running into a missing-folder error.</p><h2>Load the Data</h2><p>We pull the 2010 to 2018 series from the Federal Reserve Bank of St. Louis Data Service, also known as FRED, by using the <code>pandas_datareader</code> library that was introduced in Chapter 2 on Market and Fundamental Data.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;c2d62b1b-7f78-418c-a934-76e5db2e9bc2&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">sp500 = web.DataReader('SP500', 'fred', start='2010', end='2020').dropna()
ax = sp500.plot(title='S&amp;P 500',
           legend=False,
           figsize=(14, 4),
           rot=0)
ax.set_xlabel('')
sns.despine()</code></pre></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!mcBx!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5e50cabc-a273-47dd-9c03-ac35db21acf7_828x243.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!mcBx!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5e50cabc-a273-47dd-9c03-ac35db21acf7_828x243.png 424w, https://substackcdn.com/image/fetch/$s_!mcBx!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5e50cabc-a273-47dd-9c03-ac35db21acf7_828x243.png 848w, https://substackcdn.com/image/fetch/$s_!mcBx!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5e50cabc-a273-47dd-9c03-ac35db21acf7_828x243.png 1272w, https://substackcdn.com/image/fetch/$s_!mcBx!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5e50cabc-a273-47dd-9c03-ac35db21acf7_828x243.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!mcBx!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5e50cabc-a273-47dd-9c03-ac35db21acf7_828x243.png" width="828" height="243" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5e50cabc-a273-47dd-9c03-ac35db21acf7_828x243.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:243,&quot;width&quot;:828,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:24890,&quot;alt&quot;:&quot;Output image&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/202848543?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5e50cabc-a273-47dd-9c03-ac35db21acf7_828x243.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Output image" title="Output image" srcset="https://substackcdn.com/image/fetch/$s_!mcBx!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5e50cabc-a273-47dd-9c03-ac35db21acf7_828x243.png 424w, https://substackcdn.com/image/fetch/$s_!mcBx!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5e50cabc-a273-47dd-9c03-ac35db21acf7_828x243.png 848w, https://substackcdn.com/image/fetch/$s_!mcBx!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5e50cabc-a273-47dd-9c03-ac35db21acf7_828x243.png 1272w, https://substackcdn.com/image/fetch/$s_!mcBx!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5e50cabc-a273-47dd-9c03-ac35db21acf7_828x243.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The cell begins by downloading daily S&amp;P 500 data from the FRED database for the years 2010 through 2020 and immediately drops any missing values so the series is clean and continuous. Once the data are loaded, it is plotted as a single time series with the title &#8220;S&amp;P 500.&#8221; The legend is turned off because there is only one line, and the figure is made wide enough to show the full decade-long trend clearly. The x-axis labels are kept horizontal for easier reading, and the x-axis label itself is removed so the chart looks cleaner and less cluttered.</p><p>The saved output is the resulting line chart. It shows the S&amp;P 500 moving upward over time with noticeable dips and recoveries, which is exactly what you would expect from a historical market index. The line starts around the low 1000s near 2011, climbs steadily through the middle of the decade, shows some volatility around 2015 to 2016 and again in late 2018 to early 2019, and then rises sharply into 2020. The light grid and the removed top and right borders give the plot a simple, polished appearance, making the overall trend stand out more clearly.</p><h2>Data preparation</h2><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;ce7ed942-fc77-45dd-96d9-b5f4cc57f49f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">scaler = MinMaxScaler()</code></pre></div><p>This cell creates a new MinMaxScaler object, which is the tool later used to rescale the S&amp;P 500 values into a common 0-to-1 range. At this point nothing is transformed yet; the scaler is just being initialized so it can learn the minimum and maximum values from the data in a later step. That preparation matters because neural networks usually train more smoothly when inputs are on a consistent scale, especially for a time series like stock index values that can vary widely over time. Since no output is shown, the cell is simply setting up an object for the preprocessing stage that follows.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;a155ad3f-8c8e-4dba-b8f1-3f1a9b016cff&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">sp500_scaled = pd.Series(scaler.fit_transform(sp500).squeeze(), 
                         index=sp500.index)
sp500_scaled.describe()</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;18e3b1bb-7bf0-4bbf-9e45-1fde8f857faf&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">count    2229.000000
mean        0.451605
std         0.254561
min         0.000000
25%         0.238076
50%         0.447456
75%         0.659023
max         1.000000
dtype: float64</code></pre></div><p>The series of S&amp;P 500 values is being rescaled here so it can be used more comfortably by the neural network. The scaler learns the minimum and maximum values in the original data and then transforms every observation into a number between 0 and 1. The result is wrapped back into a pandas Series and keeps the same date index as the original data, which matters because the time order needs to stay intact for later forecasting steps.</p><p>After the scaled series is created, its summary statistics are displayed to give a quick check of the transformation. The output shows 2,229 observations, which matches the amount of usable data after missing values are removed. The minimum is 0 and the maximum is 1 because MinMax scaling forces the series into that range. The mean and quartiles fall between those extremes, showing how the historical S&amp;P 500 values are distributed after scaling. This kind of summary is useful as a sanity check: it confirms that the scaling worked and gives a sense of where most of the values now sit within the normalized range.</p><h2>Building recurrent training examples from the time series</h2><p>The data we are working with is a time-ordered sequence of values, one observation after another:</p><p>The series starts at the first recorded value, continues through each subsequent time step, and ends at the final observation. Here, the notation x at time t refers to the numeric value observed in period t, and T denotes the full length of the sequence.</p><p>To use an RNN for regression rather than classification, we reshape the series with a sliding window. This creates a moving set of input and target pairs, where each window of past values is paired with the next value the model should learn to predict, as shown in the animation below.</p><p>&lt;img src="../assets/timeseries_windowing.gif" width=600 height=600/&gt;</p><p>We will build sliding sequences that each cover 63 trading days, which is about three months, and then train a single LSTM layer with 20 hidden units to forecast the index one step into the future.</p><p>An LSTM layer expects its input to be organized in three dimensions:</p><ul><li><p><strong>Samples</strong>: each complete sequence is treated as one sample, and a batch is made up of one or more samples.</p></li><li><p><strong>Time Steps</strong>: each sample is made of a series of ordered observation points.</p></li><li><p><strong>Features</strong>: at each time step, the model receives one or more measured values.</p></li></ul><p>In this S&amp;P 500 example, there are 2,264 observations in total, so the data contains 2,264 time steps. Using a window length of 63 observations, we form overlapping sequences that move through the series one step at a time.</p><p>To make the idea easier to see, consider a shorter window length of T equal to 5. In that case, the input and output examples would look like the following:</p><p>The pattern is simple: each input consists of a fixed-length window of earlier observations, and the target is the very next value in the series.</p><p>For example, the first sample uses the five values from x one through x five to predict x six. The next sample shifts the window forward by one step, using x two through x six to predict x seven. This continues in the same way until the final window, which uses x T minus five through x T minus one to predict x T.</p><p>In general, when the window size is S, the model is written as a function that maps the previous S observations to the current value for each time step from S through T.</p><p>Each of the T minus S lagged input sequences is a vector of length S, and each one is paired with a single scalar target output.</p><p>We can apply the <code>create_univariate_rnn_data()</code> function to build input sequences by taking values from a moving window over the series:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;81266a9a-8835-4adf-aefa-484e3f93706d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def create_univariate_rnn_data(data, window_size):
    n = len(data)
    y = data[window_size:]
    data = data.values.reshape(-1, 1) # make 2D
    X = np.hstack(tuple([data[i: n-j, :] for i, j in enumerate(range(window_size, 0, -1))]))
    return pd.DataFrame(X, index=y.index), y</code></pre></div><p>This cell defines a small helper function that turns a single time series into the kind of input an RNN can learn from. The basic idea is to take a rolling lookback window of past values and use that window to predict the next value in the sequence. That is a standard way to turn forecasting into a supervised learning problem.</p><p>The function first measures how long the series is and then separates out the target values by skipping the first few observations equal to the window size. Those skipped observations do not have enough history behind them to form a full input window, so they cannot be used as prediction targets yet. Next, the series is reshaped into a two-dimensional form because the stacking step that follows expects column-like data rather than a flat vector. Behind the scenes, this reshaping does not change the values themselves; it just changes the way they are organized in memory so they can be sliced consistently.</p><p>The input windows are built by taking several shifted slices of the series and placing them side by side. Each slice represents one lag, and the slices are ordered so that the oldest observation appears first and the most recent observation appears last. When these slices are combined, each row of the resulting matrix becomes one training example containing a full history window. The function then returns those input windows as a DataFrame, using the same date index as the target series so the inputs and outputs stay aligned in time. The target series is returned alongside it, ready to be paired with the corresponding windows during model training. Since the cell only defines the function, there is no visible output yet; its effect is to prepare a reusable data-construction step for later cells.</p><p>We use this function on the scaled stock index with a window size of 63 to build a two dimensional dataset whose shape is number of samples by number of timesteps:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;3891f7bc-2c6e-4ef8-8bd9-5e1c16d27a2f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">window_size = 63</code></pre></div><p>This step sets the lookback period for the forecasting model. By assigning the value 63 to the window size, the notebook is telling later cells to use the previous 63 trading days as input when predicting the next S&amp;P 500 value. Since 63 trading days is roughly three months, this choice gives the model a medium-length recent history to learn from, rather than just a few days or an entire year. Nothing is displayed here because the cell simply stores a number for later use, but that number becomes important when the time series is reshaped into sliding windows for the LSTM model.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;699771b5-1b27-4913-84da-2c49f2ea24eb&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">X, y = create_univariate_rnn_data(sp500_scaled, window_size=window_size)</code></pre></div><p>This step turns the scaled S&amp;P 500 series into a supervised learning dataset that an LSTM can work with. The helper function takes the one-dimensional time series and slices it into overlapping windows of 63 trading days, where each window becomes one input example and the value immediately after that window becomes the target to predict. Behind the scenes, this means the model is being taught a next-day forecasting task: it looks at a stretch of past index values and learns to estimate the following value.</p><p>The result is stored as two aligned arrays, one for the inputs and one for the outputs. Each row in the input data represents a sequence of past observations, and the matching target is the next point in the series. Because the series was already scaled before this step, the values are all in a normalized range, which makes them easier for the neural network to train on. There is no visible output from the cell because it is preparing data rather than displaying anything, but it creates the training examples that the later model-fitting steps depend on.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;5c1162eb-c8b5-44f4-bd26-6ba0b82cdc73&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">X.head()</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;2a6e57e1-dd78-44e1-8840-edec9671547e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">                  0         1         2         3         4         5   \
DATE                                                                     
2011-05-24  0.097240  0.096633  0.103069  0.106498  0.096740  0.097726   
2011-05-25  0.096633  0.103069  0.106498  0.096740  0.097726  0.108250   
2011-05-26  0.103069  0.106498  0.096740  0.097726  0.108250  0.103663   
2011-05-27  0.106498  0.096740  0.097726  0.108250  0.103663  0.098515   
2011-05-31  0.096740  0.097726  0.108250  0.103663  0.098515  0.103976   

                  6         7         8         9   ...        53        54  \
DATE                                                ...                       
2011-05-24  0.108250  0.103663  0.098515  0.103976  ...  0.120484  0.113439   
2011-05-25  0.103663  0.098515  0.103976  0.103135  ...  0.113439  0.116508   
2011-05-26  0.098515  0.103976  0.103135  0.091499  ...  0.116508  0.111426   
2011-05-27  0.103976  0.103135  0.091499  0.095782  ...  0.111426  0.107549   
2011-05-31  0.103135  0.091499  0.095782  0.092097  ...  0.107549  0.107320   

                  55        56        57        58        59        60  \
DATE                                                                     
2011-05-24  0.116508  0.111426  0.107549  0.107320  0.112785  0.114149   
2011-05-25  0.111426  0.107549  0.107320  0.112785  0.114149  0.109324   
2011-05-26  0.107549  0.107320  0.112785  0.114149  0.109324  0.101897   
2011-05-27  0.107320  0.112785  0.114149  0.109324  0.101897  0.101388   
2011-05-31  0.112785  0.114149  0.109324  0.101897  0.101388  0.103345   

                  61        62  
DATE                            
2011-05-24  0.109324  0.101897  
2011-05-25  0.101897  0.101388  
2011-05-26  0.101388  0.103345  
2011-05-27  0.103345  0.105783  
2011-05-31  0.105783  0.108310  

[5 rows x 63 columns]</code></pre></div><p>The goal here is to take a quick look at the input matrix that was created from the stock series after it was turned into sliding windows. Each row now represents one training example, and each column represents one step back in time within the lookback window. Seeing the first few rows helps confirm that the data transformation worked as intended before the model uses it.</p><p>The output shows that there are 63 columns, which matches the chosen window length. The values are all small decimals because the original S&amp;P 500 levels were scaled into a 0-to-1 range earlier, so these rows are no longer raw index values but normalized prices. The dates on the left are the timestamps attached to the end of each input window, meaning each row corresponds to a specific day and contains the 63 prior scaled observations leading up to that day. The fact that the numbers shift gradually from row to row also makes sense, because each new window reuses most of the previous observations and moves forward by one trading day at a time.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;66f00411-9bb3-4a21-a0f5-0d4cfc37f932&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">y.head()</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;80ba0770-1eb8-49e5-abf9-0505197f6d3a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">DATE
2011-05-24    0.101388
2011-05-25    0.103345
2011-05-26    0.105783
2011-05-27    0.108310
2011-05-31    0.114897
dtype: float64</code></pre></div><p>The cell asks for the first few entries of the target series, so it is used as a quick check on the data after the preprocessing steps that created the supervised-learning labels. The output shows a pandas Series indexed by date, which confirms that the target values are still aligned to the original trading calendar rather than being reduced to a plain array. The numbers themselves are the scaled next-day S&amp;P 500 values, so they appear as small decimals between 0 and 1 instead of familiar index levels. The dates also make sense: the first target starts after the initial lookback window has been used to build the input sequences, which is why the series begins in late May 2011 rather than at the very start of the dataset. Seeing these first five rows is a simple but important sanity check that the forecasting target was constructed correctly and that the time index is preserved.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;7bf9d4e7-888e-4e8e-9e2e-436a72500c37&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">X.shape</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;7d8e90b9-7060-4709-a1f7-7789bb8090f2&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">(2166, 63)</code></pre></div><p>This cell checks the shape of the input matrix that was created from the time series windows. The result shows 2,166 samples, and each sample contains 63 values, which matches the chosen lookback window size. In other words, the original single price series has already been transformed into a supervised learning dataset where each row represents one 63-day history segment that can be used to predict the next day&#8217;s S&amp;P 500 value. The output confirms that the sliding-window preparation worked as expected and that there are 2,166 training examples in this tabularized form.</p><h2>Train-test split</h2><p>Because this is a time series, the split has to follow the order of time rather than being random. For that reason, we reserve the observations from the end of the sample as the hold-out portion, which serves as the test set. In this case, the period used for testing is 2018.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;662f564b-aafe-4628-8c0a-204b2a7ecdc1&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">ax = sp500_scaled.plot(lw=2, figsize=(14, 4), rot=0)
ax.set_xlabel('')
sns.despine()</code></pre></div><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!hkAt!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87cd7adc-32ca-41ba-b2fa-c3ac6e36b9ba_818x227.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!hkAt!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87cd7adc-32ca-41ba-b2fa-c3ac6e36b9ba_818x227.png 424w, https://substackcdn.com/image/fetch/$s_!hkAt!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87cd7adc-32ca-41ba-b2fa-c3ac6e36b9ba_818x227.png 848w, https://substackcdn.com/image/fetch/$s_!hkAt!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87cd7adc-32ca-41ba-b2fa-c3ac6e36b9ba_818x227.png 1272w, https://substackcdn.com/image/fetch/$s_!hkAt!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87cd7adc-32ca-41ba-b2fa-c3ac6e36b9ba_818x227.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!hkAt!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87cd7adc-32ca-41ba-b2fa-c3ac6e36b9ba_818x227.png" width="818" height="227" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/87cd7adc-32ca-41ba-b2fa-c3ac6e36b9ba_818x227.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:227,&quot;width&quot;:818,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:22603,&quot;alt&quot;:&quot;Output image&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/202848543?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87cd7adc-32ca-41ba-b2fa-c3ac6e36b9ba_818x227.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Output image" title="Output image" srcset="https://substackcdn.com/image/fetch/$s_!hkAt!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87cd7adc-32ca-41ba-b2fa-c3ac6e36b9ba_818x227.png 424w, https://substackcdn.com/image/fetch/$s_!hkAt!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87cd7adc-32ca-41ba-b2fa-c3ac6e36b9ba_818x227.png 848w, https://substackcdn.com/image/fetch/$s_!hkAt!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87cd7adc-32ca-41ba-b2fa-c3ac6e36b9ba_818x227.png 1272w, https://substackcdn.com/image/fetch/$s_!hkAt!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87cd7adc-32ca-41ba-b2fa-c3ac6e36b9ba_818x227.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>A quick line plot is being created here to show the scaled S&amp;P 500 series over time. The data have already been transformed into the 0 to 1 range, so the y-axis in the output runs from near 0 up to 1 rather than showing the index&#8217;s original price levels. The line width is increased so the trend is easier to see, the figure is made wide enough to show the full time span clearly, and the date labels are kept horizontal for readability.</p><p>After plotting, the x-axis label is removed because the dates already speak for themselves and the default label would just add clutter. The final styling touch removes the top and right borders of the plot, which gives the figure a cleaner presentation and makes the trend line stand out more. The saved output reflects all of that: a simple, polished time series chart with a strong upward long-term pattern, a few sharp dips along the way, and no extra framing elements beyond the main axes and grid.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;c8d8e9aa-0bb0-47e0-86bb-1a44379782f2&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">X_train = X[:'2018'].values.reshape(-1, window_size, 1)
y_train = y[:'2018']

# keep the last year for testing
X_test = X['2019'].values.reshape(-1, window_size, 1)
y_test = y['2019']</code></pre></div><p>The purpose here is to split the sliding-window dataset into a training set and a test set while preserving the time order of the series. The data are not being shuffled, because with time series that would mix future information into the past and give an unrealistically optimistic view of performance. Instead, everything up through the end of 2018 is treated as the training period, and 2019 is held back as the testing period.</p><p>The first pair of lines takes the earlier portion of the input windows and target values and prepares them for the neural network. The feature array is reshaped into a three-dimensional structure, which is what an LSTM expects: one dimension for the number of samples, one for the length of each lookback window, and one for the single variable being observed, the S&amp;P 500 value. The target values are kept in their matching one-dimensional form, since each window is meant to predict one next-step value.</p><p>The next pair of lines sets aside the final year as unseen data. The same windowed features are reshaped into the LSTM-friendly format, and the corresponding targets are separated out as the test labels. Because the split is based on dates rather than a random fraction of the rows, the model will later be evaluated on a genuinely future period, which makes the results more meaningful for forecasting. There is no printed output from the cell because its job is simply to organize the data in memory for the modeling step that follows.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;f194a679-90f0-4764-bc6e-3c81a43e5867&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">n_obs, window_size, n_features = X_train.shape</code></pre></div><p>This step is pulling apart the shape of the training input array so the model setup can reuse those dimensions later. The training data for this LSTM has already been turned into three-dimensional sequences, and the shape of that array tells you how many training examples there are, how long each lookback window is, and how many features are in each time step. Because the series is univariate, the feature count is just one, but keeping it as a separate value makes the later model definition more flexible and less dependent on hard-coded numbers.</p><p>The first number captures the total count of training samples created from the sliding windows. The second number is the window length, which represents how many past days the model sees before making a prediction. The third number confirms that each time step contains only a single value, the scaled S&amp;P 500 level. Nothing is displayed here because the line only assigns those dimensions to variables; its purpose is to prepare the information needed for the next modeling steps rather than produce visible output.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;3c103e2c-31c2-4daf-966b-82f4315c2658&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">y_train.shape</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;500513e8-c9d5-4d1f-bbb5-4bf8fc9ad9ad&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">(1914,)</code></pre></div><p>This check is confirming the shape of the training target array, which is the list of values the model is meant to learn to predict. The result shows a one-dimensional array with 1,914 entries, so there are 1,914 training examples in total. Each entry corresponds to one next-day target value paired with a 63-day input window created earlier, and the one-dimensional shape makes sense because the model is doing regression on a single number at a time rather than predicting multiple outputs.</p><h2>Keras LSTM Layer</h2><p>Keras includes a number of recurrent neural network layers, each with its own set of configuration choices. The full details are available in the <a href="https://keras.io/layers/recurrent/">documentation</a>.</p><p><code>LSTM</code> is configured with the following arguments:</p><ul><li><p><code>units</code>: the number of hidden units in the layer</p></li><li><p><code>activation='tanh'</code></p></li><li><p><code>recurrent_activation='hard_sigmoid'</code></p></li><li><p><code>use_bias=True</code></p></li><li><p><code>kernel_initializer='glorot_uniform'</code></p></li><li><p><code>recurrent_initializer='orthogonal'</code></p></li><li><p><code>bias_initializer='zeros'</code></p></li><li><p><code>unit_forget_bias=True</code></p></li><li><p><code>kernel_regularizer=None</code></p></li><li><p><code>recurrent_regularizer=None</code></p></li><li><p><code>bias_regularizer=None</code></p></li><li><p><code>activity_regularizer=None</code></p></li><li><p><code>kernel_constraint=None</code></p></li><li><p><code>recurrent_constraint=None</code></p></li><li><p><code>bias_constraint=None</code></p></li><li><p><code>dropout=0.0</code></p></li><li><p><code>recurrent_dropout=0.0</code></p></li><li><p><code>implementation=1</code></p></li><li><p><code>return_sequences=False</code></p></li><li><p><code>return_state=False</code></p></li><li><p><code>go_backwards=False</code></p></li><li><p><code>stateful=False</code></p></li><li><p><code>unroll=False</code></p></li></ul><h2>Define the Model Architecture</h2><p>After converting the time series into input and target pairs and splitting the data into training and testing portions, we are ready to build the recurrent neural network. Keras makes it straightforward to assemble the model with the following design:</p><ul><li><p>the first layer is an LSTM layer with 20 hidden units, and its input shape is set to window_size by 1</p></li><li><p>the second layer is a dense layer with a single output unit</p></li><li><p>the loss function should be mean<em>squared</em>error because this is a regression problem</p></li></ul><p>Only a small amount of code is needed to put this together. For examples of how to create models in Keras, see the general Keras sequential model guide and the LSTM layer documentation. When setting up the optimizer, follow the approach recommended by Keras for recurrent neural networks.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;101b29f4-e1a3-4ab7-ac78-c4c66b652c43&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">rnn = Sequential([
    LSTM(units=10, 
         input_shape=(window_size, n_features), name='LSTM'),
    Dense(1, name='Output')
])</code></pre></div><p>A small recurrent neural network is being defined here for the forecasting task. The model is built as a simple Keras Sequential stack, which means the layers are arranged one after another so the output of one becomes the input to the next. The first layer is an LSTM layer with 10 hidden units. Its job is to read each sliding window of past S&amp;P 500 values and learn patterns in their order over time, which is exactly what makes recurrent networks useful for sequence data. The input shape tells the model what each training example looks like: a sequence of length window<em>size, with n</em>features values at each time step. Since this is a univariate problem, there is only one feature per time step.</p><p>After the LSTM has processed the whole sequence, its learned summary is passed to a Dense layer with a single output unit. That final neuron produces one numeric prediction, which matches the goal of predicting the next S&amp;P 500 value as a regression problem. There is no saved output for this cell because it only constructs the model object; the architecture is defined in memory, but nothing is trained or displayed yet.</p><p>The model summary indicates that it contains 1,781 trainable parameters:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;e5628ed2-e71c-46e4-a488-1e521a634f46&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">rnn.summary()</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;1e9a563d-f46c-4d4d-8a7a-ac33e26b6ec8&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
LSTM (LSTM)                  (None, 10)                480       
_________________________________________________________________
Output (Dense)               (None, 1)                 11        
=================================================================
Total params: 491
Trainable params: 491
Non-trainable params: 0
_________________________________________________________________</code></pre></div><p>The purpose here is to display a compact summary of the trained neural network so you can inspect its structure before moving on to evaluation or interpretation. When this command runs, Keras prints a table describing each layer in the model, the shape of the data that comes out of that layer, and how many parameters that layer contains.</p><p>The output shows that the model is a simple sequential network with two layers. The first is an LSTM layer with 10 units, which takes the rolling time windows and learns a hidden representation of the recent S&amp;P 500 history. Its output shape is listed as (None, 10), where None stands for the batch size, which can vary from one prediction call to another. The parameter count of 480 reflects the weights and biases the LSTM needs to connect the 63-timestep input sequence to its 10 internal memory units.</p><p>The second layer is a dense output layer with a single neuron. Its output shape is (None, 1), which makes sense because the model is predicting one value at a time: the next day&#8217;s scaled S&amp;P 500 level. The 11 parameters here come from the 10 incoming values plus one bias term. At the bottom, Keras totals everything up and reports 491 trainable parameters in all, with none frozen. That small total confirms this is a lightweight forecasting model rather than a large deep network, which is appropriate for a one-dimensional time series task.</p><h2>Fit the Model</h2><p>We fit the network with the RMSProp optimizer, which is commonly used for recurrent neural networks, keep its default configuration, and compile the model with mean squared error as the loss function for this regression task:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;f66cb80c-24a3-41d8-b34f-bc06962965fd&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">optimizer = keras.optimizers.RMSprop(lr=0.001,
                                     rho=0.9,
                                     epsilon=1e-08,
                                     decay=0.0)</code></pre></div><p>This cell sets up the optimizer that will be used to train the neural network. RMSprop is a common choice for recurrent models because it adapts the learning rate based on recent gradient behavior, which helps stabilize training when the model is working through sequential data. The learning rate is set to 0.001, which controls how large each parameter update can be, while rho at 0.9 determines how much of the recent gradient history is kept in the running average. The epsilon value is a small constant added for numerical stability so that division does not become problematic when values get very small. Decay is left at 0.0, so the learning rate is not reduced over time through this setting.</p><p>Nothing is printed when the cell runs because it only creates the optimizer object and stores it for later use. The actual effect of this step appears later when the model is compiled and trained with this optimizer.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;4326e260-7017-421b-935f-27b20042b5b5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">rnn.compile(loss='mean_squared_error', 
            optimizer=optimizer)</code></pre></div><p>The purpose here is to prepare the recurrent neural network for training by telling it how to measure mistakes and how to update its weights. The model is set up for a regression task, so the loss function is mean squared error, which compares each predicted value with the true target and penalizes larger errors more heavily. That choice fits a forecasting problem like this because the goal is to predict a numerical value as accurately as possible, not to assign classes.</p><p>The optimizer is passed in from earlier setup, so the cell uses the training strategy that was already configured rather than creating a new one here. Behind the scenes, compiling a Keras model links together the network architecture, the loss function, and the optimization algorithm into a complete training object. After this step, the model is ready to learn from data during the next fitting stage, but nothing is trained yet and no output is shown because compiling mainly sets up the internal machinery rather than producing a visible result.</p><p>We set up an EarlyStopping callback and let the model train for as many as 100 epochs.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;79d9c990-2ab5-4171-8018-07f258cac109&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">rnn_path = (results_path / 'rnn.h5').as_posix()
checkpointer = ModelCheckpoint(filepath=rnn_path,
                               verbose=1,
                               monitor='val_loss',
                               save_best_only=True)</code></pre></div><p>This cell prepares a checkpoint for the neural network so the best version of the model can be saved during training. First it builds the path where the trained weights will be stored, converting the results location into a standard file path that Keras can use. Then it creates a ModelCheckpoint callback that watches the validation loss while the model trains. Because save<em>best</em>only is turned on, the file is updated only when the model improves on the validation set, rather than after every epoch. The verbose setting means training will print a message whenever a new best model is written. Nothing is displayed yet because the cell is only setting up this saving mechanism, but the saved file will later hold the most successful version of the model according to validation performance.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;23468e94-f20b-4127-b5c7-0ef8ba2b8e6b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">early_stopping = EarlyStopping(monitor='val_loss', 
                              patience=20,
                              restore_best_weights=True)</code></pre></div><p>This cell sets up an early stopping rule for model training, which is a way to prevent the network from continuing to learn after it stops improving on the validation data. It watches the validation loss and keeps track of whether that value is getting better from epoch to epoch. If the validation loss does not improve for 20 consecutive epochs, training will be stopped automatically. The patience setting gives the model some room to fluctuate, since validation performance can move around a little during training without that necessarily meaning the model has finished learning.</p><p>The final part is especially important because it tells the training process to restore the best weights seen during the run, rather than keeping the weights from the last epoch before stopping. That means even if the model starts to overfit after its best validation performance, the saved version will roll back to the point where it generalized best on the validation set. Since this cell only defines the callback and does not execute training on its own, there is no visible output yet; its effect appears later when the model training loop uses this callback to decide when to stop and which weights to keep.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;59f4a8d7-a923-40da-b465-6d0e7f9c765a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">lstm_training = rnn.fit(X_train,
                        y_train,
                        epochs=150,
                        batch_size=20,
                        shuffle=True,
                        validation_data=(X_test, y_test),
                        callbacks=[early_stopping, checkpointer],
                        verbose=1)</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;80a79514-74e8-43cd-b56b-24347a4d89c8&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">Epoch 1/150
95/96 [============================&gt;.] - ETA: 0s - loss: 0.0162
Epoch 00001: val_loss improved from inf to 0.00766, saving model to results/univariate_time_series/rnn.h5
96/96 [==============================] - 1s 12ms/step - loss: 0.0161 - val_loss: 0.0077
Epoch 2/150
95/96 [============================&gt;.] - ETA: 0s - loss: 5.0726e-04
Epoch 00002: val_loss improved from 0.00766 to 0.00135, saving model to results/univariate_time_series/rnn.h5
96/96 [==============================] - 1s 9ms/step - loss: 5.0613e-04 - val_loss: 0.0014
Epoch 3/150
94/96 [============================&gt;.] - ETA: 0s - loss: 4.2700e-04
Epoch 00003: val_loss did not improve from 0.00135
96/96 [==============================] - 1s 9ms/step - loss: 4.2515e-04 - val_loss: 0.0033
Epoch 4/150
94/96 [============================&gt;.] - ETA: 0s - loss: 4.0140e-04
Epoch 00004: val_loss did not improve from 0.00135
96/96 [==============================] - 1s 10ms/step - loss: 3.9946e-04 - val_loss: 0.0022
Epoch 5/150
91/96 [===========================&gt;..] - ETA: 0s - loss: 3.7595e-04
Epoch 00005: val_loss did not improve from 0.00135
96/96 [==============================] - 1s 9ms/step - loss: 3.6440e-04 - val_loss: 0.0033
Epoch 6/150
96/96 [==============================] - ETA: 0s - loss: 3.4672e-04
Epoch 00006: val_loss improved from 0.00135 to 0.00068, saving model to results/univariate_time_series/rnn.h5
96/96 [==============================] - 1s 9ms/step - loss: 3.4672e-04 - val_loss: 6.7836e-04
Epoch 7/150
95/96 [============================&gt;.] - ETA: 0s - loss: 3.1172e-04
Epoch 00007: val_loss did not improve from 0.00068
96/96 [==============================] - 1s 9ms/step - loss: 3.1417e-04 - val_loss: 0.0047
Epoch 8/150
93/96 [============================&gt;.] - ETA: 0s - loss: 3.2092e-04
Epoch 00008: val_loss did not improve from 0.00068
96/96 [==============================] - 1s 9ms/step - loss: 3.1923e-04 - val_loss: 0.0014
Epoch 9/150
95/96 [============================&gt;.] - ETA: 0s - loss: 2.9657e-04
Epoch 00009: val_loss improved from 0.00068 to 0.00043, saving model to results/univariate_time_series/rnn.h5
96/96 [==============================] - 1s 9ms/step - loss: 2.9568e-04 - val_loss: 4.3396e-04
Epoch 10/150
91/96 [===========================&gt;..] - ETA: 0s - loss: 2.8456e-04
Epoch 00010: val_loss did not improve from 0.00043
96/96 [==============================] - 1s 9ms/step - loss: 2.8585e-04 - val_loss: 0.0016
Epoch 11/150
92/96 [===========================&gt;..] - ETA: 0s - loss: 2.5980e-04
Epoch 00011: val_loss improved from 0.00043 to 0.00032, saving model to results/univariate_time_series/rnn.h5
96/96 [==============================] - 1s 9ms/step - loss: 2.6074e-04 - val_loss: 3.1798e-04
Epoch 12/150
91/96 [===========================&gt;..] - ETA: 0s - loss: 2.6164e-04
Epoch 00012: val_loss did not improve from 0.00032
96/96 [==============================] - 1s 9ms/step - loss: 2.5868e-04 - val_loss: 4.8836e-04
Epoch 13/150
96/96 [==============================] - ETA: 0s - loss: 2.5184e-04
Epoch 00013: val_loss did not improve from 0.00032
96/96 [==============================] - 1s 9ms/step - loss: 2.5184e-04 - val_loss: 4.2231e-04
Epoch 14/150
95/96 [============================&gt;.] - ETA: 0s - loss: 2.4671e-04
Epoch 00014: val_loss did not improve from 0.00032
96/96 [==============================] - 1s 10ms/step - loss: 2.4586e-04 - val_loss: 4.4436e-04
Epoch 15/150
91/96 [===========================&gt;..] - ETA: 0s - loss: 2.3177e-04
Epoch 00015: val_loss did not improve from 0.00032
96/96 [==============================] - 1s 9ms/step - loss: 2.3762e-04 - val_loss: 4.7206e-04
Epoch 16/150
92/96 [===========================&gt;..] - ETA: 0s - loss: 2.2798e-04
Epoch 00016: val_loss did not improve from 0.00032
96/96 [==============================] - 1s 9ms/step - loss: 2.2959e-04 - val_loss: 3.2628e-04
Epoch 17/150
92/96 [===========================&gt;..] - ETA: 0s - loss: 2.2682e-04
Epoch 00017: val_loss did not improve from 0.00032
96/96 [==============================] - 1s 10ms/step - loss: 2.2815e-04 - val_loss: 0.0013
Epoch 18/150
96/96 [==============================] - ETA: 0s - loss: 2.1929e-04
Epoch 00018: val_loss did not improve from 0.00032
96/96 [==============================] - 1s 9ms/step - loss: 2.1929e-04 - val_loss: 0.0022
Epoch 19/150
91/96 [===========================&gt;..] - ETA: 0s - loss: 2.1801e-04
Epoch 00019: val_loss improved from 0.00032 to 0.00024, saving model to results/univariate_time_series/rnn.h5
96/96 [==============================] - 1s 9ms/step - loss: 2.1470e-04 - val_loss: 2.4200e-04
Epoch 20/150
96/96 [==============================] - ETA: 0s - loss: 2.1644e-04
Epoch 00020: val_loss improved from 0.00024 to 0.00023, saving model to results/univariate_time_series/rnn.h5
96/96 [==============================] - 1s 10ms/step - loss: 2.1644e-04 - val_loss: 2.3101e-04
Epoch 21/150
96/96 [==============================] - ETA: 0s - loss: 2.0451e-04
Epoch 00021: val_loss improved from 0.00023 to 0.00021, saving model to results/univariate_time_series/rnn.h5
96/96 [==============================] - 1s 9ms/step - loss: 2.0451e-04 - val_loss: 2.1255e-04
Epoch 22/150
91/96 [===========================&gt;..] - ETA: 0s - loss: 2.0134e-04
Epoch 00022: val_loss did not improve from 0.00021
96/96 [==============================] - 1s 9ms/step - loss: 2.0179e-04 - val_loss: 2.2027e-04
Epoch 23/150
96/96 [==============================] - ETA: 0s - loss: 1.9941e-04
Epoch 00023: val_loss did not improve from 0.00021
96/96 [==============================] - 1s 9ms/step - loss: 1.9941e-04 - val_loss: 4.4025e-04
Epoch 24/150
96/96 [==============================] - ETA: 0s - loss: 1.9096e-04
Epoch 00024: val_loss did not improve from 0.00021
96/96 [==============================] - 1s 9ms/step - loss: 1.9096e-04 - val_loss: 5.4885e-04
Epoch 25/150
96/96 [==============================] - ETA: 0s - loss: 1.8358e-04
Epoch 00025: val_loss did not improve from 0.00021
96/96 [==============================] - 1s 9ms/step - loss: 1.8358e-04 - val_loss: 2.4444e-04
Epoch 26/150
95/96 [============================&gt;.] - ETA: 0s - loss: 1.8497e-04
Epoch 00026: val_loss did not improve from 0.00021
96/96 [==============================] - 1s 10ms/step - loss: 1.8471e-04 - val_loss: 4.2620e-04
Epoch 27/150
96/96 [==============================] - ETA: 0s - loss: 1.7350e-04
Epoch 00027: val_loss did not improve from 0.00021
96/96 [==============================] - 1s 9ms/step - loss: 1.7350e-04 - val_loss: 4.0677e-04
Epoch 28/150
91/96 [===========================&gt;..] - ETA: 0s - loss: 1.6671e-04
Epoch 00028: val_loss did not improve from 0.00021
96/96 [==============================] - 1s 9ms/step - loss: 1.6921e-04 - val_loss: 3.8056e-04
Epoch 29/150
94/96 [============================&gt;.] - ETA: 0s - loss: 1.6757e-04
Epoch 00029: val_loss did not improve from 0.00021
96/96 [==============================] - 1s 10ms/step - loss: 1.7095e-04 - val_loss: 3.5113e-04
Epoch 30/150
93/96 [============================&gt;.] - ETA: 0s - loss: 1.5892e-04
Epoch 00030: val_loss improved from 0.00021 to 0.00018, saving model to results/univariate_time_series/rnn.h5
96/96 [==============================] - 1s 10ms/step - loss: 1.5886e-04 - val_loss: 1.7758e-04
Epoch 31/150
92/96 [===========================&gt;..] - ETA: 0s - loss: 1.5301e-04
Epoch 00031: val_loss improved from 0.00018 to 0.00016, saving model to results/univariate_time_series/rnn.h5
96/96 [==============================] - 1s 10ms/step - loss: 1.5573e-04 - val_loss: 1.5858e-04
Epoch 32/150
95/96 [============================&gt;.] - ETA: 0s - loss: 1.4994e-04
Epoch 00032: val_loss improved from 0.00016 to 0.00016, saving model to results/univariate_time_series/rnn.h5
96/96 [==============================] - 1s 10ms/step - loss: 1.5008e-04 - val_loss: 1.5702e-04
Epoch 33/150
94/96 [============================&gt;.] - ETA: 0s - loss: 1.5448e-04
Epoch 00033: val_loss did not improve from 0.00016
96/96 [==============================] - 1s 10ms/step - loss: 1.5528e-04 - val_loss: 3.3243e-04
Epoch 34/150
94/96 [============================&gt;.] - ETA: 0s - loss: 1.5249e-04
Epoch 00034: val_loss improved from 0.00016 to 0.00014, saving model to results/univariate_time_series/rnn.h5
96/96 [==============================] - 1s 10ms/step - loss: 1.5086e-04 - val_loss: 1.4220e-04</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;8674ef5a-c919-4eb8-9c44-0479dc315ca5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">Epoch 35/150
96/96 [==============================] - ETA: 0s - loss: 1.4350e-04
Epoch 00035: val_loss did not improve from 0.00014
96/96 [==============================] - 1s 9ms/step - loss: 1.4350e-04 - val_loss: 8.7128e-04
Epoch 36/150
91/96 [===========================&gt;..] - ETA: 0s - loss: 1.4087e-04
Epoch 00036: val_loss did not improve from 0.00014
96/96 [==============================] - 1s 9ms/step - loss: 1.4304e-04 - val_loss: 2.2118e-04
Epoch 37/150
93/96 [============================&gt;.] - ETA: 0s - loss: 1.4318e-04
Epoch 00037: val_loss did not improve from 0.00014
96/96 [==============================] - 1s 9ms/step - loss: 1.4475e-04 - val_loss: 5.7758e-04
Epoch 38/150
91/96 [===========================&gt;..] - ETA: 0s - loss: 1.3891e-04
Epoch 00038: val_loss did not improve from 0.00014
96/96 [==============================] - 1s 9ms/step - loss: 1.4370e-04 - val_loss: 5.5123e-04
Epoch 39/150
96/96 [==============================] - ETA: 0s - loss: 1.3512e-04
Epoch 00039: val_loss did not improve from 0.00014
96/96 [==============================] - 1s 9ms/step - loss: 1.3512e-04 - val_loss: 2.0821e-04
Epoch 40/150
92/96 [===========================&gt;..] - ETA: 0s - loss: 1.3034e-04
Epoch 00040: val_loss did not improve from 0.00014
96/96 [==============================] - 1s 10ms/step - loss: 1.3073e-04 - val_loss: 6.1821e-04
Epoch 41/150
95/96 [============================&gt;.] - ETA: 0s - loss: 1.3208e-04
Epoch 00041: val_loss did not improve from 0.00014
96/96 [==============================] - 1s 10ms/step - loss: 1.3247e-04 - val_loss: 5.5452e-04
Epoch 42/150
95/96 [============================&gt;.] - ETA: 0s - loss: 1.2555e-04
Epoch 00042: val_loss did not improve from 0.00014
96/96 [==============================] - 1s 10ms/step - loss: 1.2543e-04 - val_loss: 3.1468e-04
Epoch 43/150
92/96 [===========================&gt;..] - ETA: 0s - loss: 1.2727e-04
Epoch 00043: val_loss did not improve from 0.00014
96/96 [==============================] - 1s 12ms/step - loss: 1.2745e-04 - val_loss: 2.5483e-04
Epoch 44/150
96/96 [==============================] - ETA: 0s - loss: 1.3116e-04
Epoch 00044: val_loss did not improve from 0.00014
96/96 [==============================] - 1s 11ms/step - loss: 1.3116e-04 - val_loss: 2.0916e-04
Epoch 45/150
93/96 [============================&gt;.] - ETA: 0s - loss: 1.2340e-04
Epoch 00045: val_loss did not improve from 0.00014
96/96 [==============================] - 1s 12ms/step - loss: 1.2624e-04 - val_loss: 1.5523e-04
Epoch 46/150
95/96 [============================&gt;.] - ETA: 0s - loss: 1.2741e-04
Epoch 00046: val_loss did not improve from 0.00014
96/96 [==============================] - 1s 16ms/step - loss: 1.2674e-04 - val_loss: 1.4887e-04
Epoch 47/150
92/96 [===========================&gt;..] - ETA: 0s - loss: 1.2104e-04
Epoch 00047: val_loss did not improve from 0.00014
96/96 [==============================] - 2s 16ms/step - loss: 1.2028e-04 - val_loss: 3.1503e-04
Epoch 48/150
93/96 [============================&gt;.] - ETA: 0s - loss: 1.2040e-04
Epoch 00048: val_loss did not improve from 0.00014
96/96 [==============================] - 1s 10ms/step - loss: 1.2169e-04 - val_loss: 3.1043e-04
Epoch 49/150
96/96 [==============================] - ETA: 0s - loss: 1.2513e-04
Epoch 00049: val_loss did not improve from 0.00014
96/96 [==============================] - 1s 10ms/step - loss: 1.2513e-04 - val_loss: 3.6431e-04
Epoch 50/150
93/96 [============================&gt;.] - ETA: 0s - loss: 1.1566e-04
Epoch 00050: val_loss did not improve from 0.00014
96/96 [==============================] - 1s 10ms/step - loss: 1.1806e-04 - val_loss: 2.0745e-04
Epoch 51/150
96/96 [==============================] - ETA: 0s - loss: 1.1787e-04
Epoch 00051: val_loss did not improve from 0.00014
96/96 [==============================] - 1s 8ms/step - loss: 1.1787e-04 - val_loss: 3.8254e-04
Epoch 52/150
94/96 [============================&gt;.] - ETA: 0s - loss: 1.1391e-04
Epoch 00052: val_loss improved from 0.00014 to 0.00013, saving model to results/univariate_time_series/rnn.h5
96/96 [==============================] - 1s 9ms/step - loss: 1.1292e-04 - val_loss: 1.3098e-04
Epoch 53/150
95/96 [============================&gt;.] - ETA: 0s - loss: 1.1152e-04
Epoch 00053: val_loss did not improve from 0.00013
96/96 [==============================] - 1s 8ms/step - loss: 1.1118e-04 - val_loss: 3.5197e-04
Epoch 54/150
90/96 [===========================&gt;..] - ETA: 0s - loss: 1.1793e-04
Epoch 00054: val_loss did not improve from 0.00013
96/96 [==============================] - 1s 9ms/step - loss: 1.1724e-04 - val_loss: 1.9148e-04
Epoch 55/150
94/96 [============================&gt;.] - ETA: 0s - loss: 1.1052e-04
Epoch 00055: val_loss did not improve from 0.00013
96/96 [==============================] - 1s 9ms/step - loss: 1.1202e-04 - val_loss: 2.7836e-04
Epoch 56/150
92/96 [===========================&gt;..] - ETA: 0s - loss: 1.0805e-04
Epoch 00056: val_loss did not improve from 0.00013
96/96 [==============================] - 1s 9ms/step - loss: 1.0802e-04 - val_loss: 1.3425e-04
Epoch 57/150
94/96 [============================&gt;.] - ETA: 0s - loss: 1.1062e-04
Epoch 00057: val_loss improved from 0.00013 to 0.00012, saving model to results/univariate_time_series/rnn.h5
96/96 [==============================] - 1s 9ms/step - loss: 1.0968e-04 - val_loss: 1.1734e-04
Epoch 58/150
90/96 [===========================&gt;..] - ETA: 0s - loss: 1.0471e-04
Epoch 00058: val_loss did not improve from 0.00012
96/96 [==============================] - 1s 9ms/step - loss: 1.0535e-04 - val_loss: 2.0888e-04
Epoch 59/150
93/96 [============================&gt;.] - ETA: 0s - loss: 1.0651e-04
Epoch 00059: val_loss did not improve from 0.00012
96/96 [==============================] - 1s 9ms/step - loss: 1.0444e-04 - val_loss: 1.2834e-04
Epoch 60/150
96/96 [==============================] - ETA: 0s - loss: 1.0868e-04
Epoch 00060: val_loss did not improve from 0.00012
96/96 [==============================] - 1s 8ms/step - loss: 1.0868e-04 - val_loss: 1.7766e-04
Epoch 61/150
95/96 [============================&gt;.] - ETA: 0s - loss: 1.0337e-04
Epoch 00061: val_loss did not improve from 0.00012
96/96 [==============================] - 1s 9ms/step - loss: 1.0307e-04 - val_loss: 2.2622e-04
Epoch 62/150
91/96 [===========================&gt;..] - ETA: 0s - loss: 1.0358e-04
Epoch 00062: val_loss did not improve from 0.00012
96/96 [==============================] - 1s 9ms/step - loss: 1.0332e-04 - val_loss: 1.1764e-04
Epoch 63/150
95/96 [============================&gt;.] - ETA: 0s - loss: 1.0141e-04
Epoch 00063: val_loss did not improve from 0.00012
96/96 [==============================] - 1s 9ms/step - loss: 1.0113e-04 - val_loss: 1.7721e-04
Epoch 64/150
95/96 [============================&gt;.] - ETA: 0s - loss: 1.0574e-04
Epoch 00064: val_loss did not improve from 0.00012
96/96 [==============================] - 1s 9ms/step - loss: 1.0589e-04 - val_loss: 2.7786e-04
Epoch 65/150
91/96 [===========================&gt;..] - ETA: 0s - loss: 9.9946e-05
Epoch 00065: val_loss did not improve from 0.00012
96/96 [==============================] - 1s 9ms/step - loss: 9.8424e-05 - val_loss: 2.5257e-04
Epoch 66/150
93/96 [============================&gt;.] - ETA: 0s - loss: 1.0225e-04
Epoch 00066: val_loss did not improve from 0.00012
96/96 [==============================] - 1s 9ms/step - loss: 1.0111e-04 - val_loss: 1.2785e-04
Epoch 67/150
95/96 [============================&gt;.] - ETA: 0s - loss: 1.0120e-04
Epoch 00067: val_loss did not improve from 0.00012
96/96 [==============================] - 1s 9ms/step - loss: 1.0110e-04 - val_loss: 1.5218e-04
Epoch 68/150
96/96 [==============================] - ETA: 0s - loss: 9.5239e-05
Epoch 00068: val_loss improved from 0.00012 to 0.00011, saving model to results/univariate_time_series/rnn.h5
96/96 [==============================] - 1s 9ms/step - loss: 9.5239e-05 - val_loss: 1.0982e-04
Epoch 69/150
95/96 [============================&gt;.] - ETA: 0s - loss: 9.7534e-05
Epoch 00069: val_loss did not improve from 0.00011
96/96 [==============================] - 1s 9ms/step - loss: 9.7244e-05 - val_loss: 1.3820e-04
Epoch 70/150
94/96 [============================&gt;.] - ETA: 0s - loss: 1.0033e-04
Epoch 00070: val_loss did not improve from 0.00011
96/96 [==============================] - 1s 9ms/step - loss: 1.0096e-04 - val_loss: 2.6083e-04
Epoch 71/150
94/96 [============================&gt;.] - ETA: 0s - loss: 9.8744e-05
Epoch 00071: val_loss did not improve from 0.00011
96/96 [==============================] - 1s 9ms/step - loss: 9.8619e-05 - val_loss: 1.4675e-04
Epoch 72/150
93/96 [============================&gt;.] - ETA: 0s - loss: 9.8295e-05
Epoch 00072: val_loss did not improve from 0.00011
96/96 [==============================] - 1s 9ms/step - loss: 9.7614e-05 - val_loss: 1.7677e-04
Epoch 73/150
95/96 [============================&gt;.] - ETA: 0s - loss: 9.5597e-05
Epoch 00073: val_loss did not improve from 0.00011
96/96 [==============================] - 1s 9ms/step - loss: 9.5436e-05 - val_loss: 2.8398e-04
Epoch 74/150
91/96 [===========================&gt;..] - ETA: 0s - loss: 9.7810e-05
Epoch 00074: val_loss did not improve from 0.00011
96/96 [==============================] - 1s 9ms/step - loss: 9.7079e-05 - val_loss: 1.4352e-04
Epoch 75/150
92/96 [===========================&gt;..] - ETA: 0s - loss: 9.5967e-05
Epoch 00075: val_loss did not improve from 0.00011
96/96 [==============================] - 1s 9ms/step - loss: 9.6704e-05 - val_loss: 1.2011e-04
Epoch 76/150
92/96 [===========================&gt;..] - ETA: 0s - loss: 9.8459e-05
Epoch 00076: val_loss did not improve from 0.00011
96/96 [==============================] - 1s 9ms/step - loss: 9.8009e-05 - val_loss: 1.7817e-04
Epoch 77/150
96/96 [==============================] - ETA: 0s - loss: 9.1118e-05
Epoch 00077: val_loss did not improve from 0.00011
96/96 [==============================] - 1s 9ms/step - loss: 9.1118e-05 - val_loss: 1.1157e-04
Epoch 78/150
95/96 [============================&gt;.] - ETA: 0s - loss: 9.5011e-05
Epoch 00078: val_loss did not improve from 0.00011
96/96 [==============================] - 1s 9ms/step - loss: 9.5155e-05 - val_loss: 1.5538e-04
Epoch 79/150
96/96 [==============================] - ETA: 0s - loss: 9.4418e-05
Epoch 00079: val_loss did not improve from 0.00011
96/96 [==============================] - 1s 9ms/step - loss: 9.4418e-05 - val_loss: 1.4241e-04
Epoch 80/150
94/96 [============================&gt;.] - ETA: 0s - loss: 9.4359e-05
Epoch 00080: val_loss improved from 0.00011 to 0.00011, saving model to results/univariate_time_series/rnn.h5
96/96 [==============================] - 1s 9ms/step - loss: 9.4327e-05 - val_loss: 1.0896e-04
Epoch 81/150
95/96 [============================&gt;.] - ETA: 0s - loss: 9.4806e-05
Epoch 00081: val_loss did not improve from 0.00011
96/96 [==============================] - 1s 9ms/step - loss: 9.4769e-05 - val_loss: 1.3463e-04
Epoch 82/150
95/96 [============================&gt;.] - ETA: 0s - loss: 9.3654e-05
Epoch 00082: val_loss did not improve from 0.00011
96/96 [==============================] - 1s 9ms/step - loss: 9.4321e-05 - val_loss: 1.4250e-04
Epoch 83/150
93/96 [============================&gt;.] - ETA: 0s - loss: 9.5975e-05
Epoch 00083: val_loss did not improve from 0.00011
96/96 [==============================] - 1s 9ms/step - loss: 9.6019e-05 - val_loss: 1.1075e-04
Epoch 84/150
91/96 [===========================&gt;..] - ETA: 0s - loss: 9.5794e-05
Epoch 00084: val_loss did not improve from 0.00011
96/96 [==============================] - 1s 9ms/step - loss: 9.6212e-05 - val_loss: 2.0834e-04
Epoch 85/150
91/96 [===========================&gt;..] - ETA: 0s - loss: 8.9862e-05
Epoch 00085: val_loss did not improve from 0.00011
96/96 [==============================] - 1s 9ms/step - loss: 8.7901e-05 - val_loss: 1.2320e-04
Epoch 86/150
95/96 [============================&gt;.] - ETA: 0s - loss: 9.0540e-05
Epoch 00086: val_loss did not improve from 0.00011
96/96 [==============================] - 1s 8ms/step - loss: 9.0288e-05 - val_loss: 1.1778e-04
Epoch 87/150
91/96 [===========================&gt;..] - ETA: 0s - loss: 9.1800e-05
Epoch 00087: val_loss did not improve from 0.00011
96/96 [==============================] - 1s 9ms/step - loss: 9.0853e-05 - val_loss: 1.8410e-04
Epoch 88/150
94/96 [============================&gt;.] - ETA: 0s - loss: 9.0630e-05
Epoch 00088: val_loss did not improve from 0.00011
96/96 [==============================] - 1s 9ms/step - loss: 9.1169e-05 - val_loss: 1.5173e-04
Epoch 89/150
95/96 [============================&gt;.] - ETA: 0s - loss: 9.0615e-05
Epoch 00089: val_loss did not improve from 0.00011
96/96 [==============================] - 1s 9ms/step - loss: 9.0969e-05 - val_loss: 2.6647e-04
Epoch 90/150
91/96 [===========================&gt;..] - ETA: 0s - loss: 8.4236e-05
Epoch 00090: val_loss did not improve from 0.00011
96/96 [==============================] - 1s 10ms/step - loss: 8.6761e-05 - val_loss: 2.2579e-04
Epoch 91/150
95/96 [============================&gt;.] - ETA: 0s - loss: 9.0738e-05
Epoch 00091: val_loss did not improve from 0.00011
96/96 [==============================] - 1s 9ms/step - loss: 9.0392e-05 - val_loss: 1.4102e-04
Epoch 92/150
91/96 [===========================&gt;..] - ETA: 0s - loss: 8.7762e-05
Epoch 00092: val_loss did not improve from 0.00011
96/96 [==============================] - 1s 9ms/step - loss: 9.1175e-05 - val_loss: 1.5866e-04
Epoch 93/150
92/96 [===========================&gt;..] - ETA: 0s - loss: 8.8039e-05
Epoch 00093: val_loss did not improve from 0.00011
96/96 [==============================] - 1s 9ms/step - loss: 8.9345e-05 - val_loss: 2.2237e-04
Epoch 94/150
92/96 [===========================&gt;..] - ETA: 0s - loss: 8.7299e-05
Epoch 00094: val_loss did not improve from 0.00011
96/96 [==============================] - 1s 9ms/step - loss: 8.8101e-05 - val_loss: 2.0188e-04
Epoch 95/150
91/96 [===========================&gt;..] - ETA: 0s - loss: 8.7247e-05
Epoch 00095: val_loss did not improve from 0.00011
96/96 [==============================] - 1s 9ms/step - loss: 8.7141e-05 - val_loss: 1.1806e-04
Epoch 96/150
95/96 [============================&gt;.] - ETA: 0s - loss: 9.1653e-05
Epoch 00096: val_loss did not improve from 0.00011
96/96 [==============================] - 1s 9ms/step - loss: 9.1519e-05 - val_loss: 1.1541e-04
Epoch 97/150
91/96 [===========================&gt;..] - ETA: 0s - loss: 8.8514e-05
Epoch 00097: val_loss improved from 0.00011 to 0.00011, saving model to results/univariate_time_series/rnn.h5
96/96 [==============================] - 1s 9ms/step - loss: 8.6380e-05 - val_loss: 1.0814e-04
Epoch 98/150
96/96 [==============================] - ETA: 0s - loss: 8.6094e-05
Epoch 00098: val_loss did not improve from 0.00011
96/96 [==============================] - 1s 9ms/step - loss: 8.6094e-05 - val_loss: 1.1347e-04
Epoch 99/150
90/96 [===========================&gt;..] - ETA: 0s - loss: 8.7294e-05
Epoch 00099: val_loss did not improve from 0.00011
96/96 [==============================] - 1s 9ms/step - loss: 8.9347e-05 - val_loss: 1.3592e-04
Epoch 100/150
96/96 [==============================] - ETA: 0s - loss: 8.6722e-05
Epoch 00100: val_loss did not improve from 0.00011
96/96 [==============================] - 1s 9ms/step - loss: 8.6722e-05 - val_loss: 7.0361e-04
Epoch 101/150
92/96 [===========================&gt;..] - ETA: 0s - loss: 9.0485e-05
Epoch 00101: val_loss did not improve from 0.00011
96/96 [==============================] - 1s 9ms/step - loss: 8.9286e-05 - val_loss: 1.1654e-04
Epoch 102/150
94/96 [============================&gt;.] - ETA: 0s - loss: 8.7473e-05
Epoch 00102: val_loss did not improve from 0.00011
96/96 [==============================] - 1s 9ms/step - loss: 8.6557e-05 - val_loss: 1.1325e-04
Epoch 103/150
94/96 [============================&gt;.] - ETA: 0s - loss: 8.9702e-05
Epoch 00103: val_loss did not improve from 0.00011
96/96 [==============================] - 1s 9ms/step - loss: 8.9611e-05 - val_loss: 2.9964e-04
Epoch 104/150
91/96 [===========================&gt;..] - ETA: 0s - loss: 8.8178e-05
Epoch 00104: val_loss improved from 0.00011 to 0.00011, saving model to results/univariate_time_series/rnn.h5
96/96 [==============================] - 1s 9ms/step - loss: 8.8027e-05 - val_loss: 1.0701e-04
Epoch 105/150</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;e5d7e89e-419c-4dee-8e5e-866bc5d311ed&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">90/96 [===========================&gt;..] - ETA: 0s - loss: 8.4287e-05
Epoch 00105: val_loss did not improve from 0.00011
96/96 [==============================] - 1s 9ms/step - loss: 8.5441e-05 - val_loss: 2.6547e-04
Epoch 106/150
93/96 [============================&gt;.] - ETA: 0s - loss: 8.5374e-05
Epoch 00106: val_loss did not improve from 0.00011
96/96 [==============================] - 1s 9ms/step - loss: 8.6718e-05 - val_loss: 1.4452e-04
Epoch 107/150
92/96 [===========================&gt;..] - ETA: 0s - loss: 8.6347e-05
Epoch 00107: val_loss did not improve from 0.00011
96/96 [==============================] - 1s 9ms/step - loss: 8.8267e-05 - val_loss: 1.1976e-04
Epoch 108/150
95/96 [============================&gt;.] - ETA: 0s - loss: 8.7606e-05
Epoch 00108: val_loss did not improve from 0.00011
96/96 [==============================] - 1s 9ms/step - loss: 8.7117e-05 - val_loss: 1.0868e-04
Epoch 109/150
96/96 [==============================] - ETA: 0s - loss: 9.0933e-05
Epoch 00109: val_loss did not improve from 0.00011
96/96 [==============================] - 1s 9ms/step - loss: 9.0933e-05 - val_loss: 1.1853e-04
Epoch 110/150
90/96 [===========================&gt;..] - ETA: 0s - loss: 8.8712e-05
Epoch 00110: val_loss did not improve from 0.00011
96/96 [==============================] - 1s 9ms/step - loss: 8.6446e-05 - val_loss: 1.0818e-04
Epoch 111/150
95/96 [============================&gt;.] - ETA: 0s - loss: 8.7685e-05
Epoch 00111: val_loss did not improve from 0.00011
96/96 [==============================] - 1s 9ms/step - loss: 8.8124e-05 - val_loss: 2.3456e-04
Epoch 112/150
91/96 [===========================&gt;..] - ETA: 0s - loss: 8.6183e-05
Epoch 00112: val_loss did not improve from 0.00011
96/96 [==============================] - 1s 9ms/step - loss: 8.6186e-05 - val_loss: 1.1133e-04
Epoch 113/150
96/96 [==============================] - ETA: 0s - loss: 9.0423e-05
Epoch 00113: val_loss did not improve from 0.00011
96/96 [==============================] - 1s 9ms/step - loss: 9.0423e-05 - val_loss: 1.3993e-04
Epoch 114/150
94/96 [============================&gt;.] - ETA: 0s - loss: 8.8848e-05
Epoch 00114: val_loss did not improve from 0.00011
96/96 [==============================] - 1s 9ms/step - loss: 8.9644e-05 - val_loss: 1.1688e-04
Epoch 115/150
95/96 [============================&gt;.] - ETA: 0s - loss: 9.0703e-05
Epoch 00115: val_loss did not improve from 0.00011
96/96 [==============================] - 1s 9ms/step - loss: 9.0592e-05 - val_loss: 1.3260e-04
Epoch 116/150
92/96 [===========================&gt;..] - ETA: 0s - loss: 8.5621e-05
Epoch 00116: val_loss did not improve from 0.00011
96/96 [==============================] - 1s 9ms/step - loss: 8.4973e-05 - val_loss: 1.0909e-04
Epoch 117/150
91/96 [===========================&gt;..] - ETA: 0s - loss: 8.9659e-05
Epoch 00117: val_loss did not improve from 0.00011
96/96 [==============================] - 1s 9ms/step - loss: 8.9528e-05 - val_loss: 1.5286e-04
Epoch 118/150
96/96 [==============================] - ETA: 0s - loss: 8.5693e-05
Epoch 00118: val_loss did not improve from 0.00011
96/96 [==============================] - 1s 9ms/step - loss: 8.5693e-05 - val_loss: 1.6449e-04
Epoch 119/150
93/96 [============================&gt;.] - ETA: 0s - loss: 8.7340e-05
Epoch 00119: val_loss did not improve from 0.00011
96/96 [==============================] - 1s 9ms/step - loss: 8.6209e-05 - val_loss: 3.1345e-04
Epoch 120/150
92/96 [===========================&gt;..] - ETA: 0s - loss: 8.4168e-05
Epoch 00120: val_loss did not improve from 0.00011
96/96 [==============================] - 1s 9ms/step - loss: 8.5270e-05 - val_loss: 1.1120e-04
Epoch 121/150
93/96 [============================&gt;.] - ETA: 0s - loss: 8.6140e-05
Epoch 00121: val_loss did not improve from 0.00011
96/96 [==============================] - 1s 9ms/step - loss: 8.6331e-05 - val_loss: 1.0745e-04
Epoch 122/150
91/96 [===========================&gt;..] - ETA: 0s - loss: 8.7073e-05
Epoch 00122: val_loss did not improve from 0.00011
96/96 [==============================] - 1s 9ms/step - loss: 8.5809e-05 - val_loss: 1.0791e-04
Epoch 123/150
92/96 [===========================&gt;..] - ETA: 0s - loss: 8.7627e-05
Epoch 00123: val_loss did not improve from 0.00011
96/96 [==============================] - 1s 10ms/step - loss: 8.7544e-05 - val_loss: 1.1710e-04
Epoch 124/150
92/96 [===========================&gt;..] - ETA: 0s - loss: 8.9236e-05
Epoch 00124: val_loss did not improve from 0.00011
96/96 [==============================] - 1s 9ms/step - loss: 8.7432e-05 - val_loss: 1.0717e-04</code></pre></div><p>The model is now being trained on the prepared input windows and targets, and the training process is set up to run for as many as 150 epochs while watching how well the network performs on the held-out test period. Each epoch means the network makes one full pass through the training data, updates its weights, and then immediately checks the validation loss on the test set so you can see whether it is learning patterns that generalize beyond the data it was fit on. The batch size of 20 means the weights are updated in small groups of 20 training examples at a time rather than all at once, and shuffling is turned on so the training windows are presented in a different order each epoch.</p><p>The saved output shows that happening step by step. At the start, the loss is relatively high, but it drops very quickly over the first few epochs, which is a good sign that the network is learning the basic structure in the time series. After each epoch, the checkpoint callback compares the current validation loss to the best one seen so far. When the validation score improves, the model is saved to the results folder, which is why the output repeatedly says that the model is being saved. Once the validation loss stops improving, the output switches to messages saying it did not improve from the previous best. That does not mean training has failed; it simply means the model is still fitting the training data, but its performance on unseen data is no longer getting better at that moment.</p><p>Because early stopping is active, training does not need to continue all the way to 150 epochs if the validation loss stays flat or gets worse for long enough. The long stretch of epochs with very small training loss and only tiny changes in validation loss suggests the network is converging and then hovering around its best generalization point. The fluctuations in validation loss from epoch to epoch are normal for neural network training, especially on financial time series, where the signal is noisy and small changes in weights can noticeably affect performance.</p><p>Training ends at epoch 51, and the <code>early_stopping</code> callback reloads the parameters from the best-performing model, which was reached at epoch 41.</p><h2>Assess the model&#8217;s performance</h2><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;3b3c9d37-f049-42a6-b2f3-a333a193827d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">fig, ax = plt.subplots(figsize=(12, 4))

loss_history = pd.DataFrame(lstm_training.history).pow(.5)
loss_history.index += 1
best_rmse = loss_history.val_loss.min()

best_epoch = loss_history.val_loss.idxmin()

title = f'5-Epoch Rolling RMSE (Best Validation RMSE: {best_rmse:.4%})'
loss_history.columns=['Training RMSE', 'Validation RMSE']
loss_history.rolling(5).mean().plot(logy=True, lw=2, title=title, ax=ax)

ax.axvline(best_epoch, ls='--', lw=1, c='k')

sns.despine()
fig.tight_layout()
fig.savefig(results_path / 'rnn_sp500_error', dpi=300);</code></pre></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!UdmZ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08125d3e-2c37-4c40-aad9-f2a26bc8cdf4_856x280.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!UdmZ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08125d3e-2c37-4c40-aad9-f2a26bc8cdf4_856x280.png 424w, https://substackcdn.com/image/fetch/$s_!UdmZ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08125d3e-2c37-4c40-aad9-f2a26bc8cdf4_856x280.png 848w, https://substackcdn.com/image/fetch/$s_!UdmZ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08125d3e-2c37-4c40-aad9-f2a26bc8cdf4_856x280.png 1272w, https://substackcdn.com/image/fetch/$s_!UdmZ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08125d3e-2c37-4c40-aad9-f2a26bc8cdf4_856x280.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!UdmZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08125d3e-2c37-4c40-aad9-f2a26bc8cdf4_856x280.png" width="856" height="280" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/08125d3e-2c37-4c40-aad9-f2a26bc8cdf4_856x280.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:280,&quot;width&quot;:856,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:35931,&quot;alt&quot;:&quot;Output image&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/202848543?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08125d3e-2c37-4c40-aad9-f2a26bc8cdf4_856x280.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Output image" title="Output image" srcset="https://substackcdn.com/image/fetch/$s_!UdmZ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08125d3e-2c37-4c40-aad9-f2a26bc8cdf4_856x280.png 424w, https://substackcdn.com/image/fetch/$s_!UdmZ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08125d3e-2c37-4c40-aad9-f2a26bc8cdf4_856x280.png 848w, https://substackcdn.com/image/fetch/$s_!UdmZ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08125d3e-2c37-4c40-aad9-f2a26bc8cdf4_856x280.png 1272w, https://substackcdn.com/image/fetch/$s_!UdmZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08125d3e-2c37-4c40-aad9-f2a26bc8cdf4_856x280.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The purpose of this cell is to turn the training history into a clearer view of how the model&#8217;s error changed over time, and to save that diagnostic plot for later inspection. It starts by creating a figure and axes so the chart has a defined size and place to draw. Then it takes the recorded training history from the LSTM fit and converts the loss values into root mean squared error by taking the square root of each value. That matters because the original training loss was mean squared error, and RMSE is easier to interpret in the same style as the model&#8217;s later evaluation metrics.</p><p>The index is shifted so the epochs are numbered starting at 1 instead of 0, which makes the plot more natural to read. From that transformed history, the code finds the smallest validation RMSE and the epoch where that minimum occurred. Those two values are used for the plot title and for the vertical marker that will show the best validation point. The columns are then renamed so the two lines will appear as training RMSE and validation RMSE, which makes the legend easier to understand.</p><p>Before plotting, the code smooths both curves with a 5-epoch rolling mean. That reduces short-term wiggles and makes the overall training trend easier to see. The plot is drawn on a logarithmic y-axis, which helps compress the scale and makes it easier to compare improvement early in training with the finer changes later on. The saved output reflects all of that: the blue training curve drops steadily as the model learns, while the orange validation curve is noisier but also trends downward. The title at the top reports the best validation RMSE, and the dashed vertical line marks the epoch where that best score was reached. That line appears slightly after the point where the validation curve bottoms out because the code is marking the exact epoch of the minimum after smoothing and tracking the recorded history.</p><p>Finally, the cell removes unnecessary plot borders, tightens the layout so labels fit cleanly, and saves the figure to the results folder. The displayed image is the figure object itself, showing the completed error-tracking plot exactly as it was generated.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;45732ab9-3510-4ade-abfb-d9302c336870&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">train_rmse_scaled = np.sqrt(rnn.evaluate(X_train, y_train, verbose=0))
test_rmse_scaled = np.sqrt(rnn.evaluate(X_test, y_test, verbose=0))
print(f'Train RMSE: {train_rmse_scaled:.4f} | Test RMSE: {test_rmse_scaled:.4f}')</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;8d716687-8176-4e2d-8ec8-bb2a63853f58&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">Train RMSE: 0.0085 | Test RMSE: 0.0103</code></pre></div><p>The purpose of this step is to measure how well the trained recurrent model is fitting the training period and how accurately it generalizes to the unseen test period. It asks the model to predict the next-day values for both the training windows and the test windows, then compares those predictions to the true targets using the same loss function that was used during training. Because the model was compiled with mean squared error, the evaluation returns MSE values, and taking the square root turns them into RMSE, which is easier to interpret because it is on the same scale as the target variable after scaling.</p><p>Behind the scenes, the evaluation routine runs the trained network forward on each input sequence without updating any weights. It computes the average squared difference between predicted and actual values for the training set and then does the same for the test set. Taking the square root converts those averages into a more familiar error measure. The printed output shows very small errors in scaled space: 0.0085 for training and 0.0103 for testing. The test error is a bit higher, which is expected because the model is being judged on data it did not train on, but the two numbers are still fairly close, suggesting the model is not overfitting badly and is learning a pattern that carries over to the later period.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;9542a840-d070-42e4-b6e9-f5143c25d22e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">train_predict_scaled = rnn.predict(X_train)
test_predict_scaled = rnn.predict(X_test)</code></pre></div><p>This step uses the trained recurrent neural network to generate forecasts for both the training period and the test period. The model takes the prepared sliding windows of past S&amp;P 500 values and produces one predicted next-day value for each window. First it runs through the training inputs, which gives a prediction for every training example, and then it does the same for the held-out test inputs. The results are kept in scaled form because the model was trained on normalized values rather than raw index levels. Since there is no saved output shown here, the main effect of the cell is the creation of these two arrays of predictions, which will be used in later steps for evaluation, inverse scaling, and plotting against the actual series.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;28215b58-8ad0-4406-81f4-3962f493127d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">train_ic = spearmanr(y_train, train_predict_scaled)[0]
test_ic = spearmanr(y_test, test_predict_scaled)[0]
print(f'Train IC: {train_ic:.4f} | Test IC: {test_ic:.4f}')</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;58962c21-f1a1-4b59-bdc2-0810635a4cc2&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">Train IC: 0.9986 | Test IC: 0.9817</code></pre></div><p>The purpose here is to measure how well the model preserves the ordering of the true values, not just how close the numbers are in a squared-error sense. Spearman correlation is used for that because it checks whether larger actual values tend to line up with larger predicted values, which is a good way to judge whether the forecast captures the overall rank relationship in the series. The first calculation compares the training targets with the model&#8217;s training predictions, and the second does the same for the test set. Each call returns several values, but only the correlation coefficient itself is kept, since that is the summary number being reported here.</p><p>After those two values are computed, they are printed in a compact format with four decimal places. The saved output shows very high correlations on both splits: 0.9986 for training and 0.9817 for testing. That means the model&#8217;s predictions track the ups and downs of the S&amp;P 500 very closely, especially on the data it was trained on, and still remain strongly aligned on the unseen test period. The slightly lower test value is expected because the model is being evaluated on new data, but the fact that it stays so close to 1 suggests it is capturing the general movement of the series quite well.</p><h3>Convert predictions back to the original scale</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;1ec94c85-aa61-47c1-b3df-af597698ab93&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">train_predict = pd.Series(scaler.inverse_transform(train_predict_scaled).squeeze(), index=y_train.index)
test_predict = (pd.Series(scaler.inverse_transform(test_predict_scaled)
                          .squeeze(), 
                          index=y_test.index))</code></pre></div><p>The goal here is to take the model&#8217;s predictions, which are still in the normalized 0-to-1 scale, and convert them back into actual S&amp;P 500 values. The first line applies the inverse of the earlier scaling step to the training predictions, then squeezes the result down from a two-dimensional array into a simple one-dimensional series of values. Those restored values are wrapped in a pandas Series and given the same date index as the training targets, so each prediction lines up with the correct point in time.</p><p>The second line does the same thing for the test predictions. After reversing the scaling and flattening the result, it stores the values as a pandas Series indexed by the dates in the test target set. That indexing matters because it lets the predictions be compared, plotted, and evaluated directly against the real observed values for each period. There is no saved output because nothing is being displayed yet; the cell is quietly preparing the prediction series in their original units for the next evaluation and visualization steps.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;87d12d1e-1397-4609-84a1-4df7639d66e5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">y_train_rescaled = scaler.inverse_transform(y_train.to_frame()).squeeze()
y_test_rescaled = scaler.inverse_transform(y_test.to_frame()).squeeze()</code></pre></div><p>The purpose here is to convert the target values for both the training period and the test period back into their original S&amp;P 500 scale. Earlier, the series was normalized to a 0-to-1 range so the neural network could train more easily, but those scaled numbers are not very meaningful on their own. To make the results interpretable again, each target series is first turned into a one-column table, because the scaler expects the same kind of 2D input shape it saw during fitting. The inverse transformation then undoes the earlier min-max scaling and restores the values to their original price levels. After that, the extra column structure is removed so the results become plain one-dimensional arrays again.</p><p>Nothing is displayed in the output area because the cell only prepares data for later steps. What it produces is two rescaled versions of the true target values, one for training and one for testing, which can then be compared directly with the model&#8217;s predictions and used in plots or error calculations in the original units of the index.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;54f4cbbb-6412-4f49-9f64-cd2433909415&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">train_rmse = np.sqrt(mean_squared_error(train_predict, y_train_rescaled))
test_rmse = np.sqrt(mean_squared_error(test_predict, y_test_rescaled))
f'Train RMSE: {train_rmse:.2f} | Test RMSE: {test_rmse:.2f}'</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;0ea51577-36dc-4280-b5a9-69ef9462f448&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">'Train RMSE: 18.18 | Test RMSE: 22.15'</code></pre></div><p>The cell measures how far the model&#8217;s predictions are from the actual S&amp;P 500 values after both have been returned to their original scale. It first computes the mean squared error separately for the training predictions and the test predictions, then takes the square root of each value so the result is expressed in the same units as the index itself. That makes the numbers easier to interpret, because an RMSE tells you the typical size of the prediction error in points rather than in squared points. After those two error values are calculated, they are inserted into a formatted string that rounds them to two decimal places for display.</p><p>The saved output shows those final error summaries: the training RMSE is 18.18 and the test RMSE is 22.15. The test error is higher, which is what you would usually expect when a model performs a little better on data it has already seen than on newer, unseen data. The fact that both values are fairly close suggests the model is capturing some of the pattern in the series, while still making noticeable forecasting errors, especially when the market moves more sharply.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;51c01da9-1532-4c8c-85a6-029c4fb201bb&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">sp500['Train Predictions'] = train_predict
sp500['Test Predictions'] = test_predict
sp500 = sp500.join(train_predict.to_frame('predictions').assign(data='Train')
                        .append(test_predict.to_frame('predictions').assign(data='Test')))</code></pre></div><p>The goal here is to add the model&#8217;s forecasts back into the original S&amp;P 500 data so they can be compared directly with the true index values. First, the training predictions are placed into a new column in the main data frame, and the test predictions are placed into another new column. That gives the table a simple side-by-side record of what the model predicted on each part of the timeline.</p><p>Next, the predictions are reorganized into a separate table that labels each row as either coming from the training period or the test period. The two pieces are stacked together and then joined to the original S&amp;P 500 data. Behind the scenes, the join aligns everything by date index, so each prediction ends up attached to the correct trading day. The added data label is useful later because it makes it easy to distinguish training-period forecasts from test-period forecasts when plotting or analyzing results.</p><p>There is no saved output from this cell because its job is mainly to reshape and enrich the data rather than display something immediately. Its effect is stored in the updated data frame, preparing the forecast results for the final visual comparisons and error analysis.</p><h3>Visualize the results</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;05a38023-02f2-446d-958d-137033f0ee06&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">fig=plt.figure(figsize=(14,7))
ax1 = plt.subplot(221)

sp500.loc['2015':, 'SP500'].plot(lw=4, ax=ax1, c='k')
sp500.loc['2015':, ['Test Predictions', 'Train Predictions']].plot(lw=1, ax=ax1, ls='--')
ax1.set_title('In- and Out-of-sample Predictions')


with sns.axes_style("white"):
    ax3 = plt.subplot(223)
    sns.scatterplot(x='SP500', y='predictions', data=sp500, hue='data', ax=ax3)
    ax3.text(x=.02, y=.95, s=f'Test IC ={test_ic:.2%}', transform=ax3.transAxes)
    ax3.text(x=.02, y=.87, s=f'Train IC={train_ic:.2%}', transform=ax3.transAxes)
    ax3.set_title('Correlation')
    ax3.legend(loc='lower right')
    
    ax2 = plt.subplot(222)
    ax4 = plt.subplot(224, sharex = ax2, sharey=ax2)
    sns.distplot(train_predict.squeeze()- y_train_rescaled, ax=ax2)
    ax2.set_title('Train Error')
    ax2.text(x=.03, y=.92, s=f'Train RMSE ={train_rmse:.4f}', transform=ax2.transAxes)
    sns.distplot(test_predict.squeeze()-y_test_rescaled, ax=ax4)
    ax4.set_title('Test Error')
    ax4.text(x=.03, y=.92, s=f'Test RMSE ={test_rmse:.4f}', transform=ax4.transAxes)

sns.despine()
fig.tight_layout()
fig.savefig(results_path / 'rnn_sp500_regression', dpi=300);</code></pre></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!jTIX!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbbdbbff9-ea99-4059-8924-1f676617d59f_1000x496.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!jTIX!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbbdbbff9-ea99-4059-8924-1f676617d59f_1000x496.png 424w, https://substackcdn.com/image/fetch/$s_!jTIX!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbbdbbff9-ea99-4059-8924-1f676617d59f_1000x496.png 848w, https://substackcdn.com/image/fetch/$s_!jTIX!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbbdbbff9-ea99-4059-8924-1f676617d59f_1000x496.png 1272w, https://substackcdn.com/image/fetch/$s_!jTIX!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbbdbbff9-ea99-4059-8924-1f676617d59f_1000x496.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!jTIX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbbdbbff9-ea99-4059-8924-1f676617d59f_1000x496.png" width="1000" height="496" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bbdbbff9-ea99-4059-8924-1f676617d59f_1000x496.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:496,&quot;width&quot;:1000,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:90759,&quot;alt&quot;:&quot;Output image&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/202848543?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbbdbbff9-ea99-4059-8924-1f676617d59f_1000x496.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Output image" title="Output image" srcset="https://substackcdn.com/image/fetch/$s_!jTIX!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbbdbbff9-ea99-4059-8924-1f676617d59f_1000x496.png 424w, https://substackcdn.com/image/fetch/$s_!jTIX!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbbdbbff9-ea99-4059-8924-1f676617d59f_1000x496.png 848w, https://substackcdn.com/image/fetch/$s_!jTIX!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbbdbbff9-ea99-4059-8924-1f676617d59f_1000x496.png 1272w, https://substackcdn.com/image/fetch/$s_!jTIX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbbdbbff9-ea99-4059-8924-1f676617d59f_1000x496.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The cell brings together the model&#8217;s results into one summary figure so the forecast can be judged visually from several angles at once. It starts by creating a large canvas and placing a line plot in the upper-left panel. There, the actual S&amp;P 500 values from 2015 onward are drawn as a thick black line, and the model&#8217;s train and test predictions are layered on top with thinner dashed lines. That choice makes it easy to see whether the predictions track the broad market movement and whether they stay close to the real series once the model moves from the training period into the held-out test period. The saved figure shows that the predicted lines sit very close to the black price path, with the test section continuing the same pattern into 2019 and 2020.</p><p>The next part switches to a cleaner white plotting style and builds a scatter plot in the lower-left panel. Here, actual S&amp;P 500 values are placed on the horizontal axis and predicted values on the vertical axis, with train and test points colored separately. If the model were perfect, all the points would fall exactly on a diagonal line, because each prediction would equal the true value. In the saved output, the points cluster tightly around that diagonal, which is why the plot looks almost like a narrow rising band. The text annotations inside the panel report the rank correlations for both splits, and those values are very high, which matches the strong visual alignment between actual and predicted values. This panel is useful because it shows not just whether the line forecasts are close in time, but whether the model preserves the ordering of low and high market levels.</p><p>The remaining two panels look at errors rather than predictions. The upper-right panel shows the training residuals, and the lower-right panel shows the test residuals. Each distribution is built from the difference between the predicted values and the rescaled true values, so the histograms tell you how far off the model tends to be and whether those errors are centered around zero. A bell-shaped curve is overlaid to give a smoother sense of the spread. In the saved figure, both error distributions are concentrated near zero, which suggests the model is usually close, although the test errors are a bit wider than the training ones. The RMSE values printed inside each panel summarize that spread in a single number, and the test RMSE is larger than the train RMSE, which is what you would expect once the model is evaluated on unseen data.</p><p>After the plots are assembled, the figure is cleaned up with lighter borders and tighter spacing so the four panels fit together neatly. Finally, the figure is saved to the results folder as an image file, which is why the saved output shows the complete 2-by-2 dashboard exactly as constructed in the cell.</p><div><hr></div><h2>Notebook 3 of 8: <code>02_stacked_lstm_with_feature_embeddings</code></h2><p><em>Source file: `02_stacked_lstm_with_feature_embeddings_processed.ipynb`</em></p><div><hr></div><h1>Stacked LSTMs for Time Series Classification in TensorFlow</h1><p>We will now construct a somewhat deeper network by placing two LSTM layers on top of each other and training it on the Quandl stock price data. In addition to the sequential price history, we will also feed in non-sequential features, specifically indicator variables that identify the equity and the month.</p><h2>Imports</h2><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;a561e5ba-bb3e-4111-a280-591cc8c92936&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">import warnings
warnings.filterwarnings('ignore')</code></pre></div><p>The purpose of this cell is to quiet down warning messages so they do not clutter the notebook output. It first imports Python&#8217;s warnings module, which is the standard way to control how warnings are handled during execution. Then it tells the system to ignore warnings entirely, so any non-fatal notices that would normally appear during later steps are suppressed. Since this cell only changes how future messages are displayed and does not perform any calculation or produce a visible result, there is no saved output.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;69b412e0-0d18-4fa6-ba7a-473ee1f50c9b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">%matplotlib inline

from pathlib import Path
import numpy as np
import pandas as pd
from scipy.stats import spearmanr
from sklearn.metrics import roc_auc_score

import tensorflow as tf
from tensorflow.keras.callbacks import ModelCheckpoint, EarlyStopping
from tensorflow.keras.models import Model
from tensorflow.keras.layers import Dense, LSTM, Input, concatenate, Embedding, Reshape, BatchNormalization
import tensorflow.keras.backend as K

import matplotlib.pyplot as plt
import seaborn as sns</code></pre></div><p>The cell sets up the tools needed for the rest of the notebook by bringing together plotting, data handling, statistics, machine learning metrics, and deep learning components. The first line tells Jupyter to render plots directly inside the notebook, so any figures created later will appear inline instead of opening in a separate window.</p><p>After that, it imports a few general-purpose libraries for working with files and numerical arrays, along with pandas for tabular data. It also brings in Spearman correlation and ROC AUC from scientific and machine learning libraries, since those are useful for evaluating how well the model ranks and classifies outcomes. TensorFlow and several Keras building blocks are imported next, including the model class itself, dense and recurrent layers, an embedding layer, reshaping, batch normalization, and callbacks for saving the best model and stopping training early if performance stops improving. The backend import gives access to lower-level TensorFlow/Keras utilities if they are needed later.</p><p>Finally, matplotlib and Seaborn are imported to support plotting and styling the training history and evaluation figures. There is no saved output because this cell only prepares the environment; nothing is calculated or displayed yet, but everything imported here will be used in later cells to build, train, evaluate, and visualize the neural network.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;6e8dbe1a-dec7-4d65-81cd-d94830ffcace&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">gpu_devices = tf.config.experimental.list_physical_devices('GPU')
if gpu_devices:
    print('Using GPU')
    tf.config.experimental.set_memory_growth(gpu_devices[0], True)
else:
    print('Using CPU')</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;f6ee88ac-2a73-4c34-b7e0-0263d1f99302&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">Using CPU</code></pre></div><p>The purpose here is to check what kind of hardware TensorFlow can use before the model starts training. It first asks TensorFlow to list any available GPU devices. If at least one GPU is found, the notebook announces that it is using a GPU and turns on memory growth for the first one, which tells TensorFlow to reserve GPU memory gradually instead of grabbing all of it at once. That helps avoid unnecessary memory allocation issues when multiple processes share the same machine. If no GPU is available, the notebook falls back to the CPU path and prints that instead. The saved output shows exactly that fallback happening, so TensorFlow did not detect a usable GPU on this run and the work will continue on the CPU.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;75a0cf0f-3a60-428b-a408-6377feb0d484&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">idx = pd.IndexSlice
sns.set_style('whitegrid')
np.random.seed(42)</code></pre></div><p>This cell is setting up a few small but important defaults that will be used later in the notebook. The first line creates a shortcut for Pandas indexing syntax, so that later selections from MultiIndex data can be written more cleanly and readably. That is especially useful when working with time-series data organized by more than one index level, because it makes it easier to refer to slices without repeating the full indexing machinery each time.</p><p>The next line changes the plotting style in Seaborn to a white grid theme. That affects how any future charts will look, giving them a cleaner background with light grid lines, which tends to make trends and comparisons easier to read. Since no figure is drawn here, there is no saved output yet; the change simply updates the default appearance for plots that will be created later.</p><p>The final line sets the NumPy random seed to 42. Behind the scenes, this tells NumPy to produce the same sequence of random numbers each time the notebook runs, which helps make results reproducible. That matters for tasks like model initialization, shuffling, and any other randomized operation, because it makes it easier to compare runs and debug behavior consistently.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;1ba5ebd0-86a5-4cae-bcd5-8fb5ea6dc1e2&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">results_path = Path('results', 'lstm_embeddings')
if not results_path.exists():
    results_path.mkdir(parents=True)</code></pre></div><p>This step sets up the folder where the notebook will store its results. It first builds a path pointing to a subdirectory named results/lstm_embeddings, then checks whether that folder already exists. If the folder is missing, it creates it, including any parent folders that may also be needed along the way. Nothing is printed because the cell is only preparing the file system behind the scenes, making sure later steps have a safe place to save things like training plots, checkpoints, or other outputs.</p><h2>Data</h2><p>Data generated by the notebook build_dataset.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;a5e9ca1a-d137-414a-a8e1-e60cd4c47ea6&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">data = pd.read_hdf('data.h5', 'returns_weekly')</code></pre></div><p>The purpose here is to load the prepared weekly returns dataset from disk so it can be used in the modeling steps that follow. The data is read from an HDF file, which is a convenient format for storing larger tabular datasets efficiently. The specific table being pulled in is the one keyed by returns_weekly, so the result is a DataFrame containing the weekly return features and related fields that were previously saved.</p><p>Nothing is displayed as output because the operation only assigns the loaded data to a variable. Behind the scenes, pandas opens the file, locates the named dataset inside it, and reconstructs it into memory as a DataFrame. That loaded object becomes the starting point for the rest of the preprocessing and model-building workflow.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;df2e6a48-c473-4a9a-942d-4f18d7cc6acf&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">data['ticker'] = pd.factorize(data.index.get_level_values('ticker'))[0]</code></pre></div><p>The goal here is to turn the ticker names in the dataset into a numeric form that a neural network can work with. The ticker symbols are stored as part of the index, and the expression pulls out that ticker level from the index and then factorizes it, which means each unique ticker gets assigned its own integer code. Those codes are then saved into a new column called ticker. This is a common preprocessing step for categorical data because machine learning models cannot use text labels directly, but they can learn from consistent integer identifiers. Behind the scenes, the factorization also preserves the one-to-one mapping between each ticker and its assigned number, so every row for the same company receives the same code. There is no visible output because the operation simply updates the data in place, preparing it for the embedding layer that will use these integer ticker IDs later on.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;36255445-f948-4c61-9f5f-2c22f7177dcf&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">data['month'] = data.index.get_level_values('date').month
data = pd.get_dummies(data, columns=['month'], prefix='month')</code></pre></div><p>The first step pulls the month number out of each row&#8217;s date, using the date level of the index so every observation is tagged with the month it belongs to. That gives the model a simple seasonal signal, which can be useful in financial data because behavior may differ across the calendar year. The next step turns that month column into a set of separate binary indicator columns, one for each month, instead of keeping it as a single numeric value. Behind the scenes, this one-hot encoding creates a cleaner categorical representation: a month is no longer treated as if December is &#8220;larger&#8221; than January, but simply as one of twelve distinct possibilities. There is no visible output because these operations only change the data frame in memory, preparing it for the later model inputs.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;9175b165-42b7-499c-aa2d-225badee4e19&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">data.info()</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;3c18ec7c-86d8-4f8e-8c1b-7f530d4e462f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">&lt;class 'pandas.core.frame.DataFrame'&gt;
MultiIndex: 1167341 entries, ('A', Timestamp('2009-01-11 00:00:00')) to ('ZUMZ', Timestamp('2017-12-31 00:00:00'))
Data columns (total 67 columns):
 #   Column       Non-Null Count    Dtype  
---  ------       --------------    -----  
 0   fwd_returns  1167341 non-null  float64
 1   1            1167341 non-null  float64
 2   2            1167341 non-null  float64
 3   3            1167341 non-null  float64
 4   4            1167341 non-null  float64
 5   5            1167341 non-null  float64
 6   6            1167341 non-null  float64
 7   7            1167341 non-null  float64
 8   8            1167341 non-null  float64
 9   9            1167341 non-null  float64
 10  10           1167341 non-null  float64
 11  11           1167341 non-null  float64
 12  12           1167341 non-null  float64
 13  13           1167341 non-null  float64
 14  14           1167341 non-null  float64
 15  15           1167341 non-null  float64
 16  16           1167341 non-null  float64
 17  17           1167341 non-null  float64
 18  18           1167341 non-null  float64
 19  19           1167341 non-null  float64
 20  20           1167341 non-null  float64
 21  21           1167341 non-null  float64
 22  22           1167341 non-null  float64
 23  23           1167341 non-null  float64
 24  24           1167341 non-null  float64
 25  25           1167341 non-null  float64
 26  26           1167341 non-null  float64
 27  27           1167341 non-null  float64
 28  28           1167341 non-null  float64
 29  29           1167341 non-null  float64
 30  30           1167341 non-null  float64
 31  31           1167341 non-null  float64
 32  32           1167341 non-null  float64
 33  33           1167341 non-null  float64
 34  34           1167341 non-null  float64
 35  35           1167341 non-null  float64
 36  36           1167341 non-null  float64
 37  37           1167341 non-null  float64
 38  38           1167341 non-null  float64
 39  39           1167341 non-null  float64
 40  40           1167341 non-null  float64
 41  41           1167341 non-null  float64
 42  42           1167341 non-null  float64
 43  43           1167341 non-null  float64
 44  44           1167341 non-null  float64
 45  45           1167341 non-null  float64
 46  46           1167341 non-null  float64
 47  47           1167341 non-null  float64
 48  48           1167341 non-null  float64
 49  49           1167341 non-null  float64
 50  50           1167341 non-null  float64
 51  51           1167341 non-null  float64
 52  52           1167341 non-null  float64
 53  label        1167341 non-null  int64  
 54  ticker       1167341 non-null  int64  
 55  month_1      1167341 non-null  uint8  
 56  month_2      1167341 non-null  uint8  
 57  month_3      1167341 non-null  uint8  
 58  month_4      1167341 non-null  uint8  
 59  month_5      1167341 non-null  uint8  
 60  month_6      1167341 non-null  uint8  
 61  month_7      1167341 non-null  uint8  
 62  month_8      1167341 non-null  uint8  
 63  month_9      1167341 non-null  uint8  
 64  month_10     1167341 non-null  uint8  
 65  month_11     1167341 non-null  uint8  
 66  month_12     1167341 non-null  uint8  
dtypes: float64(53), int64(2), uint8(12)
memory usage: 507.7+ MB</code></pre></div><p>The purpose here is to quickly inspect the prepared dataset before it is fed into the model. Calling the information summary on the DataFrame prints a compact structural report so you can verify that the data loaded correctly, see how many observations are present, check the index format, and confirm that the expected columns and data types are in place.</p><p>The output shows that the dataset is indexed by a MultiIndex with ticker symbols and dates, which matches the time-series setup used later in the notebook. It also reveals that there are 1,167,341 rows, stretching from early 2009 through the end of 2017, so the data covers the full training and test period. That matters because the later split depends on date order, and this output confirms that the dates are available in the index exactly where they need to be.</p><p>The column listing is just as important. The summary shows 67 total columns: one forward return column, 52 lagged return columns numbered 1 through 52, a label column, a ticker identifier, and 12 month indicator columns. That lines up with the model design, where the return history becomes the LSTM input, ticker becomes a learned categorical feature, and month becomes a one-hot seasonal feature. The fact that all 52 lag columns are non-null and stored as floating-point values tells you the sequential input is complete and numerically ready for neural network training.</p><p>The label and ticker columns are stored as integers, which is appropriate because the label is the target for classification and the ticker code will later be treated as an embedding input. The month columns are stored as uint8 values, which is typical for one-hot dummy variables because they only need to represent zeros and ones. The memory usage at the bottom, a little over 500 MB, gives a sense of how large the assembled dataset is and why it is helpful to check its structure before training begins.</p><h2>Train-test split</h2><p>Because this is time series data, the split has to preserve chronological order. For that reason, we reserve the final portion of the sample as the hold-out test set, using the observations from 2017.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;161694fd-91f9-47a3-9a74-f3255d9490f4&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">window_size=52
sequence = list(range(1, window_size+1))
ticker = 1
months = 12
n_tickers = data.ticker.nunique()</code></pre></div><p>This step sets up a few basic constants that will be used to describe the model&#8217;s inputs. The lookback window is fixed at 52, which means each training example will use 52 past weekly returns as its sequence history. A matching sequence of positions from 1 through 52 is created so the weeks can be referred to in order. The ticker value is set to 1 as a simple placeholder example, and months is set to 12 because the model will later use a one-hot representation with one feature for each month of the year. The number of tickers is then calculated from the data by counting how many unique ticker identifiers are present, which is important for sizing the embedding layer correctly so every ticker can be assigned its own learned representation. Since nothing is printed or displayed here, there is no saved output; the cell simply prepares these values for the model-building steps that follow.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;9c6d2558-8888-43b6-82d1-11e7c16070c3&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">train_data = data.drop('fwd_returns', axis=1).loc[idx[:, :'2016'], :]
test_data = data.drop('fwd_returns', axis=1).loc[idx[:, '2017'],:]</code></pre></div><p>The purpose here is to split the prepared dataset into two separate groups for modeling: one for training and one for final evaluation. The first line removes the forward returns column, which is not needed as an input feature for the model because it would represent information from the future. After that, the data is filtered by date using the index, so everything up through the end of 2016 becomes the training set. The second line takes only the rows from 2017 and sets them aside as the test set.</p><p>Because the split is based on time rather than random sampling, it preserves the natural order of the financial data and avoids leaking future information into training. The result is that train<em>data contains the historical observations the model will learn from, while test</em>data holds a completely later period that can be used to check how well the model generalizes to unseen market data. There is no saved output because nothing is displayed here; the cell simply prepares two new data tables for the next steps.</p><p>For both the training and test sets, we build a three-part input list made up of the return sequence, the stock ticker encoded as an integer, and the month represented as an integer, as illustrated below:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;b685514b-2d12-4911-b738-6695555bd51f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">X_train = [
    train_data.loc[:, sequence].values.reshape(-1, window_size , 1),
    train_data.ticker,
    train_data.filter(like='month')
]
y_train = train_data.label
[x.shape for x in X_train], y_train.shape</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;0d90ef37-bee2-4295-bd3c-5888880d1842&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">([(1035424, 52, 1), (1035424,), (1035424, 12)], (1035424,))</code></pre></div><p>The purpose here is to assemble the training inputs in the exact format the model expects. The first item in the list is the 52-week return history for each training example. The selected lag columns are pulled out of the training table, converted to raw values, and reshaped so that each sample becomes a sequence with 52 time steps and 1 feature per step. That extra last dimension matters because the LSTM layers are built to read sequences shaped like a series of vectors, even when each vector contains only one number.</p><p>The second item is the ticker identifier for each example. Rather than turning it into a sequence, it stays as a simple one-dimensional array because it will later be passed through an embedding layer, which learns a dense representation for each ticker. The third item is the set of month features. Those columns are selected by name pattern, so the result is a 12-column one-hot style matrix that captures which month the sample belongs to. Together, these three pieces form the multi-input training set: sequential market history, categorical ticker identity, and seasonal month information.</p><p>The target values are collected separately into y_train, which contains the binary label the model is trying to predict. The final line checks the shapes of everything so it is easy to verify that the data has been prepared correctly before training begins. The saved output confirms that the sequence input has 1,035,424 samples, each shaped as 52 by 1, that the ticker input has the same number of entries as a flat vector, and that the month input has 12 features per sample. The label array also has 1,035,424 values, which matches the number of training examples and shows that the inputs and targets are aligned.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;1432b261-247c-408d-b521-d52b9af4a4d4&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># keep the last year for testing
X_test = [
    test_data.loc[:, list(range(1, window_size+1))].values.reshape(-1, window_size , 1),
    test_data.ticker,
    test_data.filter(like='month')
]
y_test = test_data.label
[x.shape for x in X_test], y_test.shape</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;2e27a4de-88ad-40b1-b718-2c46e55ac782&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">([(131917, 52, 1), (131917,), (131917, 12)], (131917,))</code></pre></div><p>The purpose of this cell is to assemble the test set in exactly the same three-part form the model expects at prediction time. It takes the held-out data from the final year and separates it into a sequence input, a ticker input, and a month input. The first part pulls the weekly return columns for each sample, covering the full lookback window, and reshapes them into a three-dimensional array so each example looks like a 52-step sequence with one feature at each step. The second part keeps the ticker identifier as a single categorical value for each row, because that will later be passed through the embedding layer. The third part collects all columns whose names contain &#8220;month,&#8221; which gives the one-hot seasonal indicators the model can use alongside the sequence.</p><p>The target labels are pulled out at the end and stored separately as the test outcomes the model is supposed to predict. The final line prints the shapes of each input and the label vector so it is easy to confirm everything lines up correctly. The saved output shows that there are 131,917 test samples, each return history has shape 52 by 1, the ticker input is one value per sample, and the month features expand to 12 columns. The label array also has 131,917 entries, which matches the number of samples and confirms that the inputs and targets are aligned properly for evaluation.</p><h2>Define the model architecture</h2><p>Keras&#8217;s functional API is well suited to models that need several inputs or outputs. In this case, the network takes three separate sources of information:</p><ul><li><p>two LSTM layers arranged one after the other, with 25 units in the first layer and 10 units in the second</p></li><li><p>an embedding layer that learns a real-valued 10-dimensional representation for the equities</p></li><li><p>a one-hot encoded month feature vector</p></li></ul><p>All of this can be built in only a few lines. For background, see the <a href="https://keras.io/getting-started/sequential-model-guide/">general Keras documentation</a> and the <a href="https://keras.io/layers/recurrent/">LSTM documentation</a>.</p><p>When setting up the optimizer, follow the <a href="https://keras.io/optimizers/">Keras guidance for recurrent neural networks</a>.</p><p>We start by declaring the three inputs and their corresponding shapes, as outlined below:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;08319484-e00e-4997-89e1-08c1cbe66e7f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">K.clear_session()</code></pre></div><p>This step resets Keras&#8217; internal state so the notebook can start cleanly before building or training another model. Clearing the session removes the layers, graphs, and variables that Keras has kept in memory from earlier runs, which helps avoid confusion if the model is rebuilt more than once in the same notebook. It also frees up resources, especially useful when working with neural networks that can leave behind a lot of leftover state after experimentation. Since nothing is printed or displayed, there is no visible output; the effect is entirely behind the scenes.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;7fe29cf3-9b97-4998-ab37-dc20d1d059f7&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">n_features = 1</code></pre></div><p>This step sets up a small piece of configuration for the model by recording that each time step in the return sequence has one numeric feature. In practical terms, that means the LSTM will receive a single value at each week in the 52-week history, rather than a vector of multiple measurements per week. That choice matches the shape of the prepared return data and helps keep the later model definitions consistent, since the input layer needs to know how many features appear at each point in the sequence. Because the cell only assigns a value and does not produce any printed result or display anything, there is no saved output.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;93c24aae-c4fa-4cc5-9b0f-dc66b53d14f5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">returns = Input(shape=(window_size, n_features),
                name='Returns')

tickers = Input(shape=(1,),
                name='Tickers')

months = Input(shape=(12,),
               name='Months')</code></pre></div><p>The purpose here is to define the three separate inputs that the model will accept later on. One input is reserved for the sequence of past returns, one for the ticker identity, and one for the month-of-year features. These inputs are created before any layers are connected so that the model can be built as a multi-input network rather than a single straight stack of layers.</p><p>The first input is shaped to hold a rolling window of return history, with the number of time steps given by the window size and the number of values per step given by the feature count. That means each sample will arrive as a small sequence, not just a flat vector, which is exactly what the recurrent layers need. The next input is a single integer for the ticker, because the ticker will later be treated as a categorical label and turned into a learned embedding. The last input is a 12-element vector for the months, which matches the one-hot encoding of January through December.</p><p>Nothing is displayed when this cell runs because it is only setting up placeholders for the model graph. The result is a set of named input tensors that define the shape and meaning of each branch of the network, ready to be connected to the layers that follow.</p><h3>LSTM Layers</h3><p>To build stacked LSTM layers, we set the <code>return_sequences</code> argument to <code>True</code>. That makes the first layer output a sequence with the three-dimensional shape the next layer expects. We also include dropout for regularization and use the functional API so the tensor produced by one layer can be fed directly into the next one:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;daf3f06d-bdcd-4d89-8c8e-d3395b1dedda&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">lstm1_units = 25
lstm2_units = 10</code></pre></div><p>The purpose here is to set the size of the two LSTM layers before the model is built. One variable is assigned to the first recurrent layer and the other to the second, so the network architecture can refer to these values later without hard-coding them directly into the model definition. That makes the design easier to read and adjust, since changing the number of hidden units only requires updating these two lines.</p><p>Behind the scenes, these names simply store integer values that control how much learning capacity each LSTM layer will have. The first layer is given a larger size, which is common because it processes the full return sequence and needs enough room to capture patterns in the weekly history. The second layer is smaller, acting more like a compact summary stage that further distills the information coming from the first layer. Since the cell only defines these settings and does not run any computation by itself, there is no displayed output.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;7a59d850-75b3-4f8b-967a-4b60c648a1e3&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">lstm1 = LSTM(units=lstm1_units, 
             input_shape=(window_size, 
                          n_features), 
             name='LSTM1', 
             dropout=.2,
             return_sequences=True)(returns)

lstm_model = LSTM(units=lstm2_units, 
             dropout=.2,
             name='LSTM2')(lstm1)</code></pre></div><p>The purpose here is to build the recurrent part of the model that learns from the weekly return history. The first LSTM layer takes the sequence of past returns as input, with the shape telling it how many time steps to expect in each sample and how many values are stored at each step. Its job is to read through the 52-week window and produce a learned representation of the whole sequence rather than treating each week independently. The dropout setting randomly drops some connections during training, which helps reduce overfitting and encourages the layer to learn more robust patterns.</p><p>The important detail in the first layer is that it keeps the full sequence output instead of collapsing everything into a single vector right away. That makes it possible to stack another LSTM on top of it, because the second recurrent layer needs a sequence to process. The second LSTM then takes the transformed sequence from the first layer and compresses it into one final hidden representation for the entire return window. This second layer acts like a higher-level summary of the temporal pattern, turning the earlier sequence features into a compact signal that can later be combined with the ticker and month inputs in the rest of the model.</p><p>There is no saved output because nothing is printed or displayed here. The cell simply defines the stacked LSTM pathway that will be reused when the full neural network is assembled.</p><h3>Embedding layer</h3><p>The <code>Embedding</code> layer must be created with an <code>input_dim</code> argument, which sets the number of distinct items it can represent, an <code>output_dim</code> argument, which controls the length of each learned vector, and an <code>input_length</code> argument, which specifies how many values are fed into the layer at once. In this case, each sample contains just a single ticker.</p><p>Before this embedding output can be merged with the LSTM branch and the month features, it has to be reshaped into a flat vector, as shown below:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;2e9d6529-8fce-41a9-ab25-fef44d9dd851&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">ticker_embedding = Embedding(input_dim=n_tickers, 
                             output_dim=5, 
                             input_length=1)(tickers)
ticker_embedding = Reshape(target_shape=(5,))(ticker_embedding)</code></pre></div><p>The purpose here is to turn the ticker input into a learned numeric representation that the neural network can use alongside the return history. Instead of treating each ticker ID as a meaningful number on its own, the embedding layer learns a small dense vector for every ticker, so the model can discover relationships between stocks during training.</p><p>First, the ticker IDs are passed through an embedding layer with one row for each ticker in the universe and five values in each embedding vector. Behind the scenes, the layer looks up the vector associated with each ticker ID and updates those vectors during training so they become useful features for the prediction task. The input length is set to one because each sample contains a single ticker identifier rather than a sequence of tickers.</p><p>The result of that lookup is still wrapped in an extra dimension, so it is reshaped into a flat five-element vector. That makes it compatible with the other branches of the model, especially the LSTM output and the month features, which will later be joined together into one combined feature representation. Since this cell only builds part of the model graph, it does not produce any visible output when run; its effect is to define the ticker-embedding pathway that will be used later in the full network.</p><h3>Combine the model branches</h3><p>Now the three tensors can be merged into a single representation, after which dense layers can learn the relationship between the time-series signal, the ticker embedding, and the month features and the final target, which is whether the return in the next week is positive or negative, as illustrated below:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;bfe0ea8b-7455-46e7-b34d-b448bf5a3c2e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">merged = concatenate([lstm_model, 
                      ticker_embedding, 
                      months], name='Merged')

bn = BatchNormalization()(merged)
hidden_dense = Dense(10, name='FC1')(bn)

output = Dense(1, name='Output', activation='sigmoid')(hidden_dense)

rnn = Model(inputs=[returns, tickers, months], outputs=output)</code></pre></div><p>The purpose here is to combine the three separate streams of information the model has been building into one shared representation, and then turn that combined representation into a final binary prediction. The stacked LSTM branch has already distilled the 52-week return history into a compact sequence-based feature vector, while the ticker embedding captures which stock is being modeled and the month features add a seasonal signal. Those three pieces are concatenated into a single merged layer so the network can consider them together rather than separately.</p><p>After the merge, batch normalization is applied to stabilize the scale of the combined features. Behind the scenes, this helps keep the values flowing into the next dense layer more consistent, which can make training smoother and less sensitive to initialization. The result is then passed through a fully connected layer with 10 units, which gives the model a small learned space to mix and refine the information from all inputs.</p><p>Finally, a single-output dense layer with a sigmoid activation produces the prediction. Because sigmoid squashes the value into the range from 0 to 1, the output can be interpreted as the model&#8217;s estimated probability of the positive class. The last line wraps everything into a Keras Model object with the three inputs and that final prediction as the output, creating the complete neural network that will later be compiled and trained.</p><p>The summary describes this somewhat more advanced architecture, which contains 29,371 parameters, in the following way:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;ddbeb504-00de-4c23-8ed7-aa1ba017d122&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">rnn.summary()</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;b46161d5-66b9-40d5-87d4-6ad5d4eabdab&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">Model: "model"
__________________________________________________________________________________________________
Layer (type)                    Output Shape         Param #     Connected to                     
==================================================================================================
Returns (InputLayer)            [(None, 52, 1)]      0                                            
__________________________________________________________________________________________________
Tickers (InputLayer)            [(None, 1)]          0                                            
__________________________________________________________________________________________________
LSTM1 (LSTM)                    (None, 52, 25)       2700        Returns[0][0]                    
__________________________________________________________________________________________________
embedding (Embedding)           (None, 1, 5)         12445       Tickers[0][0]                    
__________________________________________________________________________________________________
LSTM2 (LSTM)                    (None, 10)           1440        LSTM1[0][0]                      
__________________________________________________________________________________________________
reshape (Reshape)               (None, 5)            0           embedding[0][0]                  
__________________________________________________________________________________________________
Months (InputLayer)             [(None, 12)]         0                                            
__________________________________________________________________________________________________
Merged (Concatenate)            (None, 27)           0           LSTM2[0][0]                      
                                                                 reshape[0][0]                    
                                                                 Months[0][0]                     
__________________________________________________________________________________________________
batch_normalization (BatchNorma (None, 27)           108         Merged[0][0]                     
__________________________________________________________________________________________________
FC1 (Dense)                     (None, 10)           280         batch_normalization[0][0]        
__________________________________________________________________________________________________
Output (Dense)                  (None, 1)            11          FC1[0][0]                        
==================================================================================================
Total params: 16,984
Trainable params: 16,930
Non-trainable params: 54
__________________________________________________________________________________________________</code></pre></div><p>Printing the model summary gives a compact blueprint of the neural network that has been assembled, so you can verify the architecture before training or evaluation. The output starts by naming the model and then lists each layer in order, along with the shape of the data flowing through it, how many parameters each layer contains, and which earlier layer feeds into it. The first input is the return history, shaped as a 52-step sequence with a single value at each step, and that is what enters the first LSTM layer. Because this first LSTM is configured to pass along the full sequence, its output still has a time dimension, which allows the second LSTM to process it and compress the sequence down to a smaller 10-unit representation.</p><p>At the same time, the ticker input is sent through an embedding layer, which turns each ticker identifier into a learned 5-number vector. That is why the embedding output initially has shape with a length of 1 and an embedding size of 5, and then a reshape layer removes the extra sequence dimension so it can be combined with the other features. The month input arrives separately as a 12-dimensional vector, already one-hot encoded, so it can be merged directly. Those three branches meet in the concatenation layer, producing a 27-dimensional combined feature vector made up of the LSTM output, the ticker embedding, and the month indicators.</p><p>After the merge, batch normalization is applied to stabilize the feature values before they go into a small dense layer with 10 units, and finally into the single-node output layer. That last layer uses one output because the task is binary classification, so it produces a score that can be interpreted as the probability of the positive class. The parameter counts shown beside each layer reflect how many weights the model has learned at each stage, and the totals at the bottom summarize the whole network: almost all of the parameters are trainable, with a small non-trainable portion coming from batch normalization&#8217;s internal statistics.</p><h2>Fit the Model</h2><p>We compile the model so it can track a custom AUC metric in the following way:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;290ee982-3eed-416d-9074-84b685318ea4&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">optimizer = tf.keras.optimizers.RMSprop(lr=0.001,
                                        rho=0.9,
                                        epsilon=1e-08,
                                        decay=0.0)</code></pre></div><p>This line creates the optimizer that will be used when the neural network is trained. RMSprop is a gradient-based method that adjusts the learning rate for each parameter using a moving average of recent squared gradients, which often works well for recurrent networks like LSTMs because it helps keep training stable when the signals through time can be noisy. The learning rate is set to 0.001, which controls how large each update step can be, while rho at 0.9 determines how much recent gradient history is remembered. The epsilon value is a very small number added for numerical stability so the updates do not run into divide-by-zero problems. The decay setting is left at 0.0, so the learning rate is not reduced over time by this optimizer itself. There is no saved output because creating the optimizer only defines the training rule; nothing is printed yet and no computation is run on the data at this stage.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;a919672d-d319-471e-8afa-27aa13425523&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">rnn.compile(loss='binary_crossentropy',
            optimizer=optimizer,
            metrics=['accuracy', 
                     tf.keras.metrics.AUC(name='AUC')])</code></pre></div><p>The model is being prepared for training by telling Keras exactly how it should judge its predictions. Because the task is binary classification, the loss function is set to binary cross-entropy, which measures how far the predicted probabilities are from the true 0-or-1 labels. The optimizer is supplied from earlier setup, so the network will use that chosen learning rule to adjust its weights during training. Alongside the loss, two evaluation metrics are attached: accuracy, which checks how often the model&#8217;s rounded predictions match the labels, and AUC, which looks at how well the model separates the positive class from the negative class across all possible thresholds. Nothing is printed here because compiling a model only configures it internally; it does not train anything yet or produce a visible result.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;10897d14-5181-450a-94f4-8e31468c3391&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">lstm_path = (results_path / 'lstm.classification.h5').as_posix()

checkpointer = ModelCheckpoint(filepath=lstm_path,
                               verbose=1,
                               monitor='val_AUC',
                               mode='max',
                               save_best_only=True)</code></pre></div><p>The purpose here is to set up a checkpoint so the model can be saved automatically during training whenever validation performance improves. First, the path for the saved model file is built by combining the results folder with the filename for the LSTM classifier, then converting that path into a plain string format that Keras can use. After that, a ModelCheckpoint callback is created. Behind the scenes, this callback watches the validation AUC score during training, and because the goal is to maximize AUC, it is told to treat higher values as better. With save<em>best</em>only enabled, the checkpoint will overwrite the file only when the current epoch produces a new best validation AUC. The verbose setting means training will print a message whenever a new best model is saved, which is why there is no output at this moment: the cell only prepares the saving mechanism and does not run training itself.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;d5e2c854-b2ce-4391-be7d-fd5c437c0d8d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">early_stopping = EarlyStopping(monitor='val_AUC', 
                              patience=5,
                              restore_best_weights=True,
                              mode='max')</code></pre></div><p>This sets up an early stopping rule for model training so the network does not keep running once validation performance stops improving. The training process will watch the validation AUC, which is a metric that measures how well the model ranks positive cases above negative ones. Because the goal is to maximize that value, the monitoring mode is set to max. The patience value of 5 means training is allowed to continue for up to five more epochs after the best validation AUC is reached, giving the model a little time to recover if the metric briefly dips before improving again. The option to restore the best weights ensures that, when training finishes, the model keeps the parameter values from the epoch with the strongest validation AUC rather than the final epoch, which helps preserve the most useful version of the model.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;72af6c15-991a-480a-b886-25b2a7f00021&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">training = rnn.fit(X_train,
                   y_train,
                   epochs=50,
                   batch_size=32,
                   validation_data=(X_test, y_test),
                   callbacks=[early_stopping, checkpointer],
                   verbose=1)</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;92aa9c90-a80d-40c5-b6ff-1410b4fc5fb0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">Epoch 1/50
32356/32357 [============================&gt;.] - ETA: 0s - loss: 0.6892 - accuracy: 0.5375 - AUC: 0.5504
Epoch 00001: val_AUC improved from -inf to 0.61860, saving model to results/lstm_embeddings/lstm.classification.h5
32357/32357 [==============================] - 335s 10ms/step - loss: 0.6892 - accuracy: 0.5375 - AUC: 0.5504 - val_loss: 0.6701 - val_accuracy: 0.5826 - val_AUC: 0.6186
Epoch 2/50
32354/32357 [============================&gt;.] - ETA: 0s - loss: 0.6855 - accuracy: 0.5493 - AUC: 0.5680
Epoch 00002: val_AUC improved from 0.61860 to 0.63500, saving model to results/lstm_embeddings/lstm.classification.h5
32357/32357 [==============================] - 338s 10ms/step - loss: 0.6855 - accuracy: 0.5493 - AUC: 0.5680 - val_loss: 0.6668 - val_accuracy: 0.5902 - val_AUC: 0.6350
Epoch 3/50
32352/32357 [============================&gt;.] - ETA: 0s - loss: 0.6857 - accuracy: 0.5488 - AUC: 0.5671
Epoch 00003: val_AUC improved from 0.63500 to 0.63709, saving model to results/lstm_embeddings/lstm.classification.h5
32357/32357 [==============================] - 308s 10ms/step - loss: 0.6857 - accuracy: 0.5488 - AUC: 0.5671 - val_loss: 0.6732 - val_accuracy: 0.5825 - val_AUC: 0.6371
Epoch 4/50
32352/32357 [============================&gt;.] - ETA: 0s - loss: 0.6831 - accuracy: 0.5471 - AUC: 0.5660
Epoch 00004: val_AUC did not improve from 0.63709
32357/32357 [==============================] - 254s 8ms/step - loss: 0.6831 - accuracy: 0.5471 - AUC: 0.5660 - val_loss: 0.6747 - val_accuracy: 0.5803 - val_AUC: 0.6361
Epoch 5/50
32353/32357 [============================&gt;.] - ETA: 0s - loss: 0.6807 - accuracy: 0.5487 - AUC: 0.5676
Epoch 00005: val_AUC improved from 0.63709 to 0.67301, saving model to results/lstm_embeddings/lstm.classification.h5
32357/32357 [==============================] - 253s 8ms/step - loss: 0.6807 - accuracy: 0.5486 - AUC: 0.5676 - val_loss: 0.5795 - val_accuracy: 0.6061 - val_AUC: 0.6730
Epoch 6/50
32357/32357 [==============================] - ETA: 0s - loss: 0.6798 - accuracy: 0.5489 - AUC: 0.5687
Epoch 00006: val_AUC improved from 0.67301 to 0.68151, saving model to results/lstm_embeddings/lstm.classification.h5
32357/32357 [==============================] - 251s 8ms/step - loss: 0.6798 - accuracy: 0.5489 - AUC: 0.5687 - val_loss: 0.5815 - val_accuracy: 0.6175 - val_AUC: 0.6815
Epoch 7/50
32355/32357 [============================&gt;.] - ETA: 0s - loss: 0.6780 - accuracy: 0.5508 - AUC: 0.5718
Epoch 00007: val_AUC did not improve from 0.68151
32357/32357 [==============================] - 254s 8ms/step - loss: 0.6780 - accuracy: 0.5508 - AUC: 0.5718 - val_loss: 0.6432 - val_accuracy: 0.6144 - val_AUC: 0.6721
Epoch 8/50
32355/32357 [============================&gt;.] - ETA: 0s - loss: 0.6814 - accuracy: 0.5497 - AUC: 0.5694
Epoch 00008: val_AUC did not improve from 0.68151
32357/32357 [==============================] - 255s 8ms/step - loss: 0.6814 - accuracy: 0.5497 - AUC: 0.5694 - val_loss: 0.5745 - val_accuracy: 0.6122 - val_AUC: 0.6757
Epoch 9/50
32353/32357 [============================&gt;.] - ETA: 0s - loss: 0.6759 - accuracy: 0.5525 - AUC: 0.5750
Epoch 00009: val_AUC did not improve from 0.68151
32357/32357 [==============================] - 254s 8ms/step - loss: 0.6759 - accuracy: 0.5525 - AUC: 0.5750 - val_loss: 0.5710 - val_accuracy: 0.6176 - val_AUC: 0.6813
Epoch 10/50
32352/32357 [============================&gt;.] - ETA: 0s - loss: 0.6760 - accuracy: 0.5529 - AUC: 0.5758
Epoch 00010: val_AUC did not improve from 0.68151
32357/32357 [==============================] - 246s 8ms/step - loss: 0.6760 - accuracy: 0.5529 - AUC: 0.5758 - val_loss: 0.5733 - val_accuracy: 0.6151 - val_AUC: 0.6765
Epoch 11/50
32346/32357 [============================&gt;.] - ETA: 0s - loss: 0.6743 - accuracy: 0.5537 - AUC: 0.5768
Epoch 00011: val_AUC did not improve from 0.68151
32357/32357 [==============================] - 141s 4ms/step - loss: 0.6743 - accuracy: 0.5537 - AUC: 0.5768 - val_loss: 0.5703 - val_accuracy: 0.6161 - val_AUC: 0.6797</code></pre></div><p>The purpose here is to start training the neural network and let Keras monitor how well it learns from the training set while checking performance on the held-out test set after each epoch. The fit call takes the prepared training inputs and labels, runs for up to 50 passes through the data, and uses a batch size of 32, which means the model updates its weights after seeing small chunks of 32 samples at a time rather than all at once. By supplying the test data as validation_data, the model can report validation loss, validation accuracy, and validation AUC at the end of every epoch, giving a direct picture of how well it generalizes beyond the training examples. The callbacks are what make the training more practical: early stopping watches the validation metric so training can halt once progress stops improving, and the checkpoint saves the best-performing version of the model to disk whenever validation AUC gets better.</p><p>The printed output is the training log that Keras produces while this learning process is underway. Each epoch line shows the main quantities being tracked: loss measures how far the model&#8217;s predictions are from the true labels, accuracy shows the fraction of correct classifications at the current threshold, and AUC measures how well the model ranks positive cases above negative ones across all thresholds. At the beginning, the model is only slightly better than guessing, with accuracy around the mid-50% range and AUC a little above 0.55 on the training set, while validation AUC starts near 0.62. Whenever validation AUC improves, the checkpoint message appears and the model is saved to the file in the results folder; that is why the output repeatedly says the score improved and the model was saved. Later epochs show that validation AUC rises into the high 0.6 range, peaking at 0.68151 in epoch 6, after which several epochs fail to beat that score. That pattern is exactly what the callbacks are meant to reveal: training continues while there is still improvement, but the best version of the model is preserved automatically. The decreasing time per epoch also reflects the fact that the model is running through a large dataset efficiently once the training loop is underway, with the progress bar showing nearly all batches completed each time.</p><p>Training ends after 18 epochs. The strongest checkpoint reaches a test area under the curve of 0.63, and the best result comes from the model after 13 training rounds. Each epoch takes roughly three minutes on a single GPU.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;85b0fa32-45aa-417e-b403-c799c823308f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">loss_history = pd.DataFrame(training.history)</code></pre></div><p>The purpose here is to take the training log produced by the model and turn it into a tidy table that is easier to inspect and plot. The history object returned by model training stores the values for each metric at every epoch in a Python dictionary-like form, with separate entries for things like loss, accuracy, AUC, and their validation counterparts. Wrapping that history in a DataFrame arranges those lists into columns, so each row corresponds to one epoch and each column corresponds to a metric tracked during training.</p><p>Behind the scenes, this makes the recorded training results much more convenient to work with. Instead of dealing with a nested history structure, the metrics are now organized in a tabular format that can be summarized, sliced, exported, or plotted directly. There is no visible output from the cell itself because it is just creating and storing this DataFrame for later use, not displaying it yet.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;420740fb-5721-4ae9-986e-5e5bcfd035a6&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def which_metric(m):
    return m.split('_')[-1]</code></pre></div><p>The cell defines a small helper function that pulls the metric name out of a longer string. When it receives a string, it splits that string wherever there is an underscore and then returns the last piece. The purpose is to make labels easier to group later on, especially when metric names arrive with prefixes such as training or validation tags. For example, a name like this would be reduced to just the metric itself, which makes it simpler to organize results and plot them cleanly. Nothing is displayed when the cell runs because the function only gets created and stored for later use; the actual effect appears later when other code needs a way to interpret metric names.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;7979c383-eb1a-487f-ae86-ea3beddbfaf6&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">fig, axes = plt.subplots(ncols=3, figsize=(18,4))
for i, (metric, hist) in enumerate(loss_history.groupby(which_metric, axis=1)):
    hist.plot(ax=axes[i], title=metric)
    axes[i].legend(['Training', 'Validation'])

sns.despine()
fig.tight_layout()
fig.savefig(results_path / 'lstm_stacked_classification', dpi=300);</code></pre></div><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!2aI3!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91398cf0-8a91-40c7-9357-de3a625bc5f9_1288x280.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!2aI3!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91398cf0-8a91-40c7-9357-de3a625bc5f9_1288x280.png 424w, https://substackcdn.com/image/fetch/$s_!2aI3!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91398cf0-8a91-40c7-9357-de3a625bc5f9_1288x280.png 848w, https://substackcdn.com/image/fetch/$s_!2aI3!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91398cf0-8a91-40c7-9357-de3a625bc5f9_1288x280.png 1272w, https://substackcdn.com/image/fetch/$s_!2aI3!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91398cf0-8a91-40c7-9357-de3a625bc5f9_1288x280.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!2aI3!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91398cf0-8a91-40c7-9357-de3a625bc5f9_1288x280.png" width="1288" height="280" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/91398cf0-8a91-40c7-9357-de3a625bc5f9_1288x280.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:280,&quot;width&quot;:1288,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:43026,&quot;alt&quot;:&quot;Output image&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/202848543?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91398cf0-8a91-40c7-9357-de3a625bc5f9_1288x280.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Output image" title="Output image" srcset="https://substackcdn.com/image/fetch/$s_!2aI3!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91398cf0-8a91-40c7-9357-de3a625bc5f9_1288x280.png 424w, https://substackcdn.com/image/fetch/$s_!2aI3!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91398cf0-8a91-40c7-9357-de3a625bc5f9_1288x280.png 848w, https://substackcdn.com/image/fetch/$s_!2aI3!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91398cf0-8a91-40c7-9357-de3a625bc5f9_1288x280.png 1272w, https://substackcdn.com/image/fetch/$s_!2aI3!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91398cf0-8a91-40c7-9357-de3a625bc5f9_1288x280.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>The purpose here is to turn the training history into a compact visual summary and save it for later inspection. First, a figure with three side-by-side panels is created, which gives enough room to compare separate metrics without crowding the lines. Then the recorded history is grouped by metric name, so each subplot receives the pair of training and validation curves for one measure at a time. As each group is plotted, the panel title is set to the metric name, and the legend is simplified so the two lines are clearly labeled as training and validation.</p><p>The saved image shows exactly that setup: one panel for AUC, one for accuracy, and one for loss. The curves reflect how those quantities changed over the course of training, so the line shapes come directly from the values collected during each epoch. The AUC and accuracy panels show the validation scores rising and then flattening, while the loss panel shows the training loss drifting gently downward and the validation loss fluctuating more sharply. That visual gap between the training and validation behavior is useful because it helps reveal whether the model is improving steadily or starting to overfit. After the plots are drawn, the notebook removes the top and right borders for a cleaner look, tightens the layout so the subplots fit neatly, and saves the finished figure into the results folder at high resolution so it can be reused outside the notebook.</p><h2>Assess model performance</h2><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;da12d60b-0ba5-4bd0-891e-4d5f725d4791&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">test_predict = pd.Series(rnn.predict(X_test).squeeze(), index=y_test.index)</code></pre></div><p>The purpose of this line is to run the trained recurrent neural network on the test set and store the model&#8217;s predicted probabilities in a convenient pandas Series. The model receives the test inputs, produces one score for each sample, and those scores are squeezed down from a two-dimensional array into a flat one-dimensional shape so they are easier to work with. By attaching the predictions to the same index as the test labels, the results stay aligned with the original observations, which makes later comparisons, plotting, and metric calculations much simpler.</p><p>Behind the scenes, the model is applying everything it learned during training to unseen 2017 data and outputting a probability-like value for each example, reflecting how likely it thinks the label is positive. Wrapping that output in a Series gives the predictions a meaningful pandas structure rather than leaving them as a raw NumPy array. Since there is no saved output from the cell itself, nothing is displayed here; the main effect is preparing a labeled set of test predictions for the evaluation steps that follow.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;e66a86e1-b5e8-485f-99cc-f002f1bee51e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">roc_auc_score(y_score=test_predict, y_true=y_test)</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;6fa6832b-493e-4191-8d3b-abc064eaa5a3&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">0.6815303447045473</code></pre></div><p>The purpose here is to measure how well the trained model separates the two classes on the held-out test set. The model has already produced a score for each test example, and those scores are passed together with the true test labels into the ROC AUC calculation. Behind the scenes, the metric is checking how often a randomly chosen positive example receives a higher predicted score than a randomly chosen negative example, so it focuses on ranking quality rather than just a fixed cutoff.</p><p>The result shown, 0.6815303447045473, is the model&#8217;s area under the ROC curve on the test data. A value of 0.5 would mean the scores are no better than random guessing, while a value of 1.0 would mean perfect separation. So this output indicates that the model has learned some useful predictive signal and ranks positive cases ahead of negative ones more often than chance, though it is still far from perfect.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;414b7ebf-9a1d-483e-bf9f-d8670801938c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">((test_predict&gt;.5) == y_test).astype(int).mean()</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;1792e741-d7d4-4bb2-be93-063581abd022&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">0.6174943335582223</code></pre></div><p>This line turns the model&#8217;s predicted probabilities into a simple yes-or-no forecast by checking whether each prediction is above 0.5. Predictions above that threshold are treated as class 1, and predictions at or below it are treated as class 0. That binary forecast is then compared element by element with the true test labels, so each example is marked as correct or incorrect. Converting those True/False results into 1s and 0s and taking the mean gives the overall classification accuracy.</p><p>The saved output, 0.6174943335582223, means the model got about 61.7% of the test cases right when using 0.5 as the cutoff. The decimal form appears because accuracy is calculated as an average across many individual predictions rather than being rounded to a percentage.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;91783f5f-2606-49cc-b1d1-11fc490525ea&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">spearmanr(test_predict, y_test)[0]</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;3a1892e3-d290-4e94-96c2-4ccf5965a26d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">0.3105869204256358</code></pre></div><p>This line measures how well the model&#8217;s predicted scores line up with the true test labels using Spearman rank correlation. Rather than checking whether the predictions are exactly right, it asks whether higher predicted probabilities tend to correspond to the positive class and lower probabilities tend to correspond to the negative class, even if the relationship is not perfectly linear. The two inputs being compared are the model&#8217;s test predictions and the actual labels for the test set, and the function returns a correlation value along with a p-value. By selecting the first element, the cell keeps only the correlation coefficient itself. The saved output, 0.3105869204256358, shows a moderate positive relationship, which means the model&#8217;s ranking of examples is somewhat aligned with the true outcomes, though far from perfect.</p><div><hr></div><h2>Notebook 4 of 8: <code>03_stacked_lstm_with_feature_embeddings_regression</code></h2><p><em>Source file: `03_stacked_lstm_with_feature_embeddings_regression_processed.ipynb`</em></p><div><hr></div><h1>Stacked LSTMs for Time Series Regression</h1><p>We will now construct a somewhat deeper network by placing two LSTM layers on top of each other and training it on the Quandl stock price data. For the full implementation details, refer to the stacked<em>lstm</em>with<em>feature</em>embeddings notebook. In addition to the time series input, this model also incorporates non-sequential information: one set of indicator variables to identify the equity and another to represent the month.</p><h2>Imports</h2><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;c8da9189-d2f9-4daa-9b21-a9f591adc71b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">import warnings
warnings.filterwarnings('ignore')</code></pre></div><p>The purpose here is to quiet down warning messages so they do not clutter the notebook as it runs. First, the warnings module is imported, which gives access to Python&#8217;s built-in warning system. Then the warning filter is set to ignore warnings, so messages that would normally appear during execution are suppressed. Since this cell only changes the notebook&#8217;s message behavior and does not produce any calculations, plots, or printed results, there is no saved output.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;fb503005-6f6d-4c4b-830b-94cebdccdf1d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">%matplotlib inline

from pathlib import Path
import numpy as np
import pandas as pd

from scipy.stats import spearmanr

import tensorflow as tf
from tensorflow.keras.callbacks import ModelCheckpoint, EarlyStopping
from tensorflow.keras.models import Sequential, Model
from tensorflow.keras.layers import Dense, LSTM, Input, concatenate, Embedding, Reshape, BatchNormalization
import tensorflow.keras.backend as K

import matplotlib.pyplot as plt
from matplotlib.ticker import FuncFormatter
import seaborn as sns</code></pre></div><p>The purpose of this cell is to set up the main tools the notebook will use for modeling, analysis, and plotting. It begins by enabling inline matplotlib output, which tells Jupyter to display any later charts directly in the notebook rather than in a separate window. After that, it brings in the standard data and numerical libraries so the later cells can work with tables and arrays efficiently. Path handling is added so file locations can be managed cleanly, while NumPy and pandas provide the numerical and tabular foundation for the rest of the workflow.</p><p>Next, the cell imports a statistical function for Spearman rank correlation, which is especially useful when the goal is to judge whether predictions preserve the right ordering rather than match exact values. The TensorFlow and Keras imports prepare the deep learning pieces: model containers, recurrent layers, dense layers, embeddings, reshaping, normalization, and the training callbacks that will later save the best model and stop training when improvement stalls. The backend import is there for lower-level Keras control if needed in later cells.</p><p>The final imports bring in plotting utilities from matplotlib and seaborn, including a formatter for customizing axis labels. Since no output is produced here, the cell&#8217;s role is purely preparatory: it loads the libraries and functions that later cells depend on, so the notebook can build the model, train it, evaluate rank-based performance, and visualize the results without needing to repeat these setup steps.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;d7782449-820f-43fd-8dcc-f5c2c2de9a82&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">gpu_devices = tf.config.experimental.list_physical_devices('GPU')
if gpu_devices:
    print('Using GPU')
    tf.config.experimental.set_memory_growth(gpu_devices[0], True)
else:
    print('Using CPU')</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;3f3b0a5e-1292-4661-b098-5adb3d62b762&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">Using CPU</code></pre></div><p>The cell checks whether TensorFlow can see a GPU on the machine and then adjusts how it will use the available hardware. It first asks TensorFlow for a list of physical GPU devices. If that list is not empty, it would print a message saying a GPU is being used and then turn on memory growth for the first GPU, which tells TensorFlow to allocate GPU memory gradually instead of reserving everything up front. That helps avoid grabbing more GPU memory than needed and can make it easier to share the device with other processes. If no GPU is found, the cell falls back to the CPU path and prints that it will use the CPU instead.</p><p>The saved output shows &#8220;Using CPU,&#8221; which means TensorFlow did not detect a GPU in the runtime environment. As a result, none of the GPU-specific configuration is applied, and the rest of the notebook will run on the processor rather than on a graphics device.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;28886164-f76c-4644-8466-74dff49b85be&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">idx = pd.IndexSlice
sns.set_style('whitegrid')
np.random.seed(42)</code></pre></div><p>The cell sets up a few small but important defaults that will be used later in the notebook. First, it creates a shorthand reference for pandas indexing so that more complex row and column selections can be written more cleanly in later cells. That kind of helper is especially useful when working with multi-level indexes, where selecting a slice of data can otherwise become fairly verbose.</p><p>Next, it applies a seaborn plotting style with a white grid background. This affects the appearance of any charts produced afterward, making them easier to read and giving them a consistent visual theme. Because plotting settings change the look of future figures rather than producing an immediate result, there is no visible output from this line on its own.</p><p>The final line fixes NumPy&#8217;s random seed at 42. That step makes random number generation reproducible, which is important for machine learning experiments and any other operations that involve randomness. With the seed set, later steps that depend on random initialization or random sampling will behave the same way each time the notebook is run, as long as the surrounding setup stays the same. Since these are configuration changes rather than calculations or displays, the cell produces no saved output.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;8eb8cf8c-c137-4b2d-aa32-74f25221b400&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">results_path = Path('results', 'lstm_embeddings')
if not results_path.exists():
    results_path.mkdir(parents=True)</code></pre></div><p>This step sets up the folder where the model&#8217;s results will be stored. It first points to a location named results/lstm_embeddings, which is a path object representing a directory on disk. Then it checks whether that directory already exists. If it does not, the code creates it, including any missing parent folders along the way. That means later cells can safely save files such as model checkpoints, prediction tables, or plots into this location without worrying about the folder being missing. Since this cell only prepares the filesystem and does not print anything or display anything, there is no saved output.</p><h2>Data</h2><p>Data generated by the notebook build_dataset.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;907c6b67-cda2-493e-973a-87a9710b89fe&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">data = pd.read_hdf('data.h5', 'returns_weekly').drop('label', axis=1)</code></pre></div><p>The goal here is to load the prepared weekly returns dataset from the HDF5 file and immediately remove the extra label column that is not needed for the modeling workflow. The data is read from the stored table named returns_weekly, which brings it back into memory as a pandas DataFrame with the original index and feature columns intact. Right after loading, the label column is dropped along the column axis, leaving only the fields that will actually be used later for building model inputs and evaluating predictions. Since this step only prepares the DataFrame and does not print, plot, or save anything, there is no visible output when the cell runs.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;a7c82337-a281-4968-a389-dd086d82c57f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">data['ticker'] = pd.factorize(data.index.get_level_values('ticker'))[0]</code></pre></div><p>The purpose here is to turn each stock&#8217;s ticker symbol into a numeric category ID that the model can work with. The data is indexed by ticker and date, so the ticker names are stored as labels rather than numbers. Since neural networks cannot directly use text labels, the ticker level is pulled out of the index, factorized, and converted into integers. Each unique ticker gets its own consistent ID, and those IDs are then stored in a new ticker column in the data table. Behind the scenes, this creates a simple mapping from ticker names to zero-based numbers, which is exactly what the later embedding layer needs so it can learn a compact representation for each stock. There is no saved output because the operation only modifies the dataframe in memory; it quietly prepares the data for the next modeling steps without displaying anything.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;e7e8d76a-2cfe-443e-9fe8-99c7a8ddb529&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">data['month'] = data.index.get_level_values('date').month
data = pd.get_dummies(data, columns=['month'], prefix='month')</code></pre></div><p>The first step here is to pull the month out of each row&#8217;s date and store it as a new column called month. Since the data is indexed by date, the month can be read directly from the date values without needing any extra lookup. After that, the month column is converted into one-hot encoded indicator variables, which means each month is turned into its own separate yes-or-no column. Instead of keeping month as a single number from 1 to 12, the data now has a set of binary columns that tell the model exactly which month each observation belongs to.</p><p>That transformation is useful because month is a categorical feature, not a continuous one. If it were left as a plain number, the model might incorrectly treat December as being &#8220;larger&#8221; or &#8220;farther away&#8221; than January in a numeric sense. One-hot encoding avoids that problem by giving each month its own independent signal. There is no saved output because the cell only reshapes the DataFrame in memory; it prepares the data for later modeling, but it does not print anything or display a table.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;68bdf917-ca1e-4de5-a4ca-66bcdccd3d47&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">data.info()</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;a1e5e9df-5295-40f3-ac48-8e5aebcfc1ff&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">&lt;class 'pandas.core.frame.DataFrame'&gt;
MultiIndex: 1167341 entries, ('A', Timestamp('2009-01-11 00:00:00')) to ('ZUMZ', Timestamp('2017-12-31 00:00:00'))
Data columns (total 66 columns):
 #   Column       Non-Null Count    Dtype  
---  ------       --------------    -----  
 0   fwd_returns  1167341 non-null  float64
 1   1            1167341 non-null  float64
 2   2            1167341 non-null  float64
 3   3            1167341 non-null  float64
 4   4            1167341 non-null  float64
 5   5            1167341 non-null  float64
 6   6            1167341 non-null  float64
 7   7            1167341 non-null  float64
 8   8            1167341 non-null  float64
 9   9            1167341 non-null  float64
 10  10           1167341 non-null  float64
 11  11           1167341 non-null  float64
 12  12           1167341 non-null  float64
 13  13           1167341 non-null  float64
 14  14           1167341 non-null  float64
 15  15           1167341 non-null  float64
 16  16           1167341 non-null  float64
 17  17           1167341 non-null  float64
 18  18           1167341 non-null  float64
 19  19           1167341 non-null  float64
 20  20           1167341 non-null  float64
 21  21           1167341 non-null  float64
 22  22           1167341 non-null  float64
 23  23           1167341 non-null  float64
 24  24           1167341 non-null  float64
 25  25           1167341 non-null  float64
 26  26           1167341 non-null  float64
 27  27           1167341 non-null  float64
 28  28           1167341 non-null  float64
 29  29           1167341 non-null  float64
 30  30           1167341 non-null  float64
 31  31           1167341 non-null  float64
 32  32           1167341 non-null  float64
 33  33           1167341 non-null  float64
 34  34           1167341 non-null  float64
 35  35           1167341 non-null  float64
 36  36           1167341 non-null  float64
 37  37           1167341 non-null  float64
 38  38           1167341 non-null  float64
 39  39           1167341 non-null  float64
 40  40           1167341 non-null  float64
 41  41           1167341 non-null  float64
 42  42           1167341 non-null  float64
 43  43           1167341 non-null  float64
 44  44           1167341 non-null  float64
 45  45           1167341 non-null  float64
 46  46           1167341 non-null  float64
 47  47           1167341 non-null  float64
 48  48           1167341 non-null  float64
 49  49           1167341 non-null  float64
 50  50           1167341 non-null  float64
 51  51           1167341 non-null  float64
 52  52           1167341 non-null  float64
 53  ticker       1167341 non-null  int64  
 54  month_1      1167341 non-null  uint8  
 55  month_2      1167341 non-null  uint8  
 56  month_3      1167341 non-null  uint8  
 57  month_4      1167341 non-null  uint8  
 58  month_5      1167341 non-null  uint8  
 59  month_6      1167341 non-null  uint8  
 60  month_7      1167341 non-null  uint8  
 61  month_8      1167341 non-null  uint8  
 62  month_9      1167341 non-null  uint8  
 63  month_10     1167341 non-null  uint8  
 64  month_11     1167341 non-null  uint8  
 65  month_12     1167341 non-null  uint8  
dtypes: float64(53), int64(1), uint8(12)
memory usage: 498.8+ MB</code></pre></div><p>The purpose here is to inspect the structure of the dataset after it has been loaded and transformed, so you can confirm that it has the expected shape before moving on to modeling. Calling the DataFrame&#8217;s information summary prints a compact inventory of what is inside: how many rows there are, how the index is organized, what columns exist, how many non-missing values each one has, and what data types they use.</p><p>The output shows that the data is organized with a MultiIndex made up of ticker and date, which means each row represents one stock at one weekly time point. There are 1,167,341 rows in total, spanning from an early observation for ticker A in 2009 to a late observation for ticker ZUMZ in 2017. That immediately tells you the dataset is large and time-ordered across many securities, which is exactly what a weekly return prediction model needs.</p><p>Below the index summary, the output lists 66 columns. The first column is the forward return target, and the next 52 columns are the lagged weekly returns numbered 1 through 52. Those are stored as floating-point numbers, which makes sense because returns are continuous values. After that comes the ticker identifier, stored as an integer, which will later be used as the categorical input for the embedding layer. The final 12 columns are the month dummy variables, one for each month of the year, stored as small unsigned integers because they are just 0/1 indicators. Seeing all 12 month columns confirms that the seasonal features were added correctly.</p><p>The &#8220;Non-Null Count&#8221; column is also important because it shows that every one of these fields is fully populated for all rows. That is a good sign that the earlier preprocessing steps worked cleanly and that there are no missing values that would interrupt model training. The memory usage line at the bottom gives a sense of scale as well: this is a fairly large table, using nearly 500 MB in memory, which helps explain why the notebook checks resources and why efficient data handling matters.</p><p>Overall, this summary verifies that the dataset has the exact ingredients the model expects: a target, a 52-week return history, a ticker identity field, and month indicators, all aligned by ticker and date.</p><h2>Train-test split</h2><p>Because this is time series data, the last portion of the sample is reserved as the hold-out set. In this case, the notebook uses all observations from 2017 as the test period.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;4a9384db-f3f1-4131-886f-78c1fc60065e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">window_size=52
sequence = list(range(1, window_size+1))
ticker = 1
months = 12
n_tickers = data.ticker.nunique()</code></pre></div><p>The cell sets up a few small pieces of configuration that are used later when the model inputs are built. It starts by choosing a window size of 52, which means the model will look at 52 weeks of past return information for each sample. Right after that, it creates a simple sequence of numbers from 1 through 52, which serves as a convenient label or reference for those weekly lag positions. It then assigns a placeholder ticker value of 1 and sets the number of months to 12, matching the twelve calendar months that will be represented in the month input. Finally, it calculates how many unique tickers are present in the dataset, so the model knows the size of the ticker vocabulary when building the embedding layer. Since this cell only defines values and does not print anything or create a figure, there is no saved output.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;c59fa57c-3765-4771-bd3e-883a168ffb0a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">train_data = data.loc[idx[:, :'2016'], :]
test_data = data.loc[idx[:, '2017'],:]</code></pre></div><p>The purpose here is to split the full dataset into a training portion and a held-out test portion based on time. The data appears to be indexed by ticker and date, so the selection uses the date level of the index to separate everything up through 2016 into the training set and everything from 2017 into the test set. That means the model will learn from older observations and then be evaluated on future data it has not seen before, which is especially important for time-dependent financial data because it avoids leaking information from the future into the past.</p><p>Behind the scenes, the first selection pulls all rows whose date is less than or equal to the end of 2016, while the second selection grabs all rows from 2017. The resulting variables hold two matching slices of the same original table, just divided by year. Since this cell only creates these filtered DataFrames and does not print anything, there is no saved output to show.</p><p>For both the training set and the test set, we build a three-part input structure made up of the return history, the stock ticker converted into integer codes, and the month represented as an integer, as illustrated below:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;af0b314e-dbdc-49e8-a402-72e4e94fbad1&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">X_train = [
    train_data.loc[:, sequence].values.reshape(-1, window_size , 1),
    train_data.ticker,
    train_data.filter(like='month')
]
y_train = train_data.fwd_returns
[x.shape for x in X_train], y_train.shape</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;b4467cfc-8216-4892-a86d-944b4e29f337&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">([(1035424, 52, 1), (1035424,), (1035424, 12)], (1035424,))</code></pre></div><p>The purpose here is to assemble the training inputs in exactly the shape the neural network expects. The model is designed to take three separate pieces of information for each stock-week example: a sequence of past returns, the ticker identity, and the month of the observation. Each of those gets pulled from the training table and stored as its own element in a list so it can later be fed into the matching input branch of the model.</p><p>The first item is built from the return-history columns selected by the sequence variable. Those values are converted into a NumPy array and reshaped so every sample becomes a 52-step sequence with one feature at each step. That extra final dimension of size 1 is important because LSTM layers expect sequence data to be three-dimensional, even when there is only a single numeric value per time step. The second item is the ticker column, kept as a one-dimensional array of integer IDs so it can be used by the embedding layer. The third item is formed by taking all columns whose names include month, which gives the one-hot encoded month indicators as a 12-column matrix. Together, these three arrays describe the same set of training observations from different angles: recent price behavior, which stock it is, and what time of year it is.</p><p>After the inputs are prepared, the target values are pulled out separately into y_train by taking the forward return column. That is the number the network will try to predict from the three inputs. The final line checks the shapes of all these arrays, which is a quick sanity check before model training begins. The saved output confirms everything lines up correctly: there are 1,035,424 training examples, the return sequence input has shape 1,035,424 by 52 by 1, the ticker input is a single integer per example, the month input has 12 features per example, and the target vector has one value per example.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;540a0fa8-08ea-415a-9c16-271f19a29d18&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># keep the last year for testing
X_test = [
    test_data.loc[:, list(range(1, window_size+1))].values.reshape(-1, window_size , 1),
    test_data.ticker,
    test_data.filter(like='month')
]
y_test = test_data.fwd_returns
[x.shape for x in X_test], y_test.shape</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;04283eb8-817f-49e8-93a6-73e2cb507e48&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">([(131917, 52, 1), (131917,), (131917, 12)], (131917,))</code></pre></div><p>The purpose of this cell is to assemble the test inputs in exactly the format the model expects, and then quickly verify that those inputs and the target line up correctly. The first item in the test input list is built from the 52 weekly return columns. Those values are pulled from the test set, converted into a NumPy array, and reshaped so each sample becomes a 52-step sequence with one feature at each step. That is why the shape shows 131,917 rows, 52 time steps, and 1 channel.</p><p>The second item is the ticker identifier for each row. It stays one-dimensional because the model treats ticker as a categorical label rather than a sequence or numeric feature. The third item is the set of month indicator columns, which provides a 12-dimensional one-hot-style representation of seasonality for each sample. Together, these three pieces match the three-input model architecture built earlier.</p><p>The target values are taken from the forward returns column and stored separately as the test labels. The final line asks for the shapes of each input and the target so the notebook can confirm everything is aligned before prediction. The saved output shows exactly that: all three test inputs contain 131,917 samples, with the expected dimensions for sequence data, ticker IDs, and month features, and the target array has the same number of samples. That match is important because it confirms the model can evaluate each test example against its correct forward return without any shape mismatch.</p><h2>Define the model structure</h2><p>The Keras functional API is well suited to models that need several inputs or outputs. In this example, the network takes three separate inputs:</p><ul><li><p>Two LSTM layers are stacked on top of each other, with 25 units in the first layer and 10 units in the second</p></li><li><p>An embedding layer learns a compact real-valued representation of the equities</p></li><li><p>The month is represented with one-hot encoding</p></li></ul><p>Only a small amount of code is needed to build this kind of model. Helpful references include:</p><ul><li><p>the <a href="https://keras.io/getting-started/sequential-model-guide/">general Keras documentation</a></p></li><li><p>the <a href="https://keras.io/layers/recurrent/">LTSM documentation</a></p></li></ul><p>When setting up the optimizer, follow the <a href="https://keras.io/optimizers/">Keras guidance for RNNs</a>.</p><p>We start by defining the three inputs and their corresponding shapes, as shown below:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;199e4367-9b9c-4b96-a354-13d082b96d9d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">K.clear_session()</code></pre></div><p>The purpose here is to reset Keras&#8217; internal state before defining or training another model. Clearing the session wipes away the computation graphs, layer names, and model objects that Keras has been holding onto from earlier steps, which helps prevent old models from lingering in memory and avoids naming collisions if new layers or models are created afterward. It is a common housekeeping step in notebook workflows, especially when experimenting with multiple architectures. Since this operation only changes the backend state and does not produce any printed result or visual output, there is no saved output for the cell.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;fc2be329-92f5-4683-b1f8-c51c560289d2&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">n_features = 1</code></pre></div><p>This line sets the number of features used for each time step in the return sequence to 1. In other words, each weekly observation in the LSTM input is treated as a single value rather than a multi-dimensional feature vector. That fits the way the return history has been prepared earlier: the model is working with one sequence of weekly returns, and each week contributes one scalar return value. This small setting becomes important later when the sequence is reshaped for the network, because it tells the model to expect input shaped like a 52-step series with one feature at each step. Since the cell only assigns a value and does not display anything, there is no saved output.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;765f1f05-9561-45fa-a496-bce81a60bc20&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">returns = Input(shape=(window_size, n_features), name='Returns')
tickers = Input(shape=(1,), name='Tickers')
months = Input(shape=(12,), name='Months')</code></pre></div><p>The purpose here is to define the three separate pieces of information that the model will receive for each stock observation. One input is the recent return history, shaped as a sequence with a fixed window length and a set number of features, so it can be passed into the LSTM layers that learn from time-ordered data. A second input is the ticker identifier, stored as a single integer value because it will later be turned into a learned embedding that helps the model distinguish one stock from another. The third input is the month representation, given as a 12-element vector so the model can pick up seasonal patterns tied to the calendar.</p><p>Behind the scenes, these lines are not yet training anything or producing predictions. They are creating symbolic input placeholders in Keras, which act like named entry points for the model graph. Later layers will be connected to these inputs, but at this stage the notebook is just telling the model what shapes to expect. Since nothing is being computed or displayed yet, there is no saved output for the cell.</p><h3>Recurrent sequence layers</h3><p>To build the stacked LSTM block, the first layer is configured with <code>return_sequences</code> set to <code>True</code>. That choice makes the layer output a three-dimensional tensor, which is what the next LSTM layer expects. The code also applies dropout for regularization, and the functional API is used to carry each layer&#8217;s tensor output directly into the following layer:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;e38148d0-688f-4a1c-a751-5e03de29dc8e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">lstm1_units = 25
lstm2_units = 10</code></pre></div><p>The cell sets the size of the two LSTM layers that will be used later in the model. One variable is assigned 25 units for the first recurrent layer, and the other is assigned 10 units for the second. These values act like hyperparameters: they control how much representational capacity each LSTM layer has when it processes the stock return sequence. A larger number of units lets the layer learn more complex patterns, while a smaller number keeps the model more compact. Nothing is produced on screen because the cell is only defining these configuration values for use in the model-building steps that follow.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;596f5827-13e9-4119-8cae-3876ed47017f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">lstm1 = LSTM(units=lstm1_units, 
             input_shape=(window_size, 
                          n_features), 
             name='LSTM1', 
             dropout=.2,
             return_sequences=True)(returns)

lstm_model = LSTM(units=lstm2_units, 
             dropout=.2,
             name='LSTM2')(lstm1)</code></pre></div><p>The purpose here is to build the sequence-processing part of the model: a pair of stacked LSTM layers that read the historical return window and turn it into a compact learned summary. The first LSTM takes the return sequence as input, with the window length and number of features already defined earlier, and it is configured to keep the full sequence of hidden states instead of collapsing everything immediately. That matters because the next LSTM layer needs a sequence to work with, not just a single final value. Dropout is turned on as a regularization step, which randomly ignores some inputs during training so the model is less likely to memorize the training data.</p><p>The second LSTM sits on top of the first one and consumes the sequence produced by it. Unlike the first layer, it does not return a sequence, so it condenses the information into one final vector that represents the whole lookback window. That compressed representation becomes the learned temporal feature summary used later when the model combines this branch with the ticker and month inputs. There is no saved output because the cell only defines these layers and stores the resulting layer objects; nothing is printed or displayed at this stage.</p><h3>Ticker Embedding Layer</h3><p>The embedding layer is configured with three key settings. The <code>input_dim</code> value tells the model how many distinct embeddings it should learn, <code>output_dim</code> sets the length of each embedding vector, and <code>input_length</code> specifies how many items are fed into the layer at once, which in this case is just one ticker for each sample.</p><p>To make the embedding output compatible with the LSTM output and the month features, it has to be reshaped into a flat vector first, as shown below:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;8dd0a406-144a-4d6c-8e86-0e5956555416&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">ticker_embedding = Embedding(input_dim=n_tickers, 
                             output_dim=5, 
                             input_length=1)(tickers)
ticker_embedding = Reshape(target_shape=(5,))(ticker_embedding)</code></pre></div><p>The purpose of this step is to turn each ticker ID into a small learned vector that the neural network can use as a compact representation of the stock itself. Instead of treating the ticker as a simple number with no meaning, the model gives each one its own position in a five-dimensional embedding space, where similar tickers can end up with similar learned representations if that helps prediction. The input dimension is set by the total number of unique tickers, and the output dimension is five, so every ticker is mapped to a vector of length five.</p><p>After that mapping is created, the result still has an extra dimension because the embedding layer produces one vector per ticker input in a format that is convenient for sequence models and other layers. Since the ticker input here represents a single category for each sample, that extra dimension is unnecessary for the later merge step. Reshaping it into a plain five-element vector makes it easier to concatenate with the other branches of the model, such as the LSTM output and the month indicators. No output is shown because the cell is just defining an intermediate model component rather than displaying or calculating a final result.</p><h3>Combine the model branches</h3><p>Now we can join the three tensors together and pass them through dense layers so the model can learn how to translate the time series pattern, the ticker representation, and the month indicators into the target outcome: whether the return in the next week is positive or negative, as shown here:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;f6229a00-f9ae-4e12-a4a8-965d5d902a35&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">merged = concatenate([lstm_model, 
                      ticker_embedding, 
                      months], name='Merged')

bn = BatchNormalization()(merged)
hidden_dense = Dense(10, name='FC1')(bn)

output = Dense(1, name='Output')(hidden_dense)

rnn = Model(inputs=[returns, tickers, months], outputs=output)</code></pre></div><p>The goal here is to assemble the separate model branches into one final prediction network. By this point, the sequence of weekly returns, the ticker identity embedding, and the month indicators have each been processed on their own, and now they are brought together into a single combined representation. The concatenation step merges those three feature streams side by side, so the model can learn from temporal patterns, security-specific effects, and seasonal effects all at once.</p><p>Once those pieces are merged, batch normalization is applied to stabilize the combined activations. Behind the scenes, this helps keep the values flowing into the next layer on a more consistent scale, which usually makes training smoother and less sensitive to the exact distribution of the incoming features. After that, the merged representation is passed through a fully connected layer with 10 hidden units. That layer gives the network a chance to learn a compact interaction between the different inputs before making the final prediction.</p><p>The last layer reduces everything down to a single number, which is the model&#8217;s regression output for the forward return target. After that, the full Keras model is defined by specifying the three inputs and the one output. The resulting object represents the complete network architecture, ready to be compiled and trained. Since there is no saved output for this cell, nothing is displayed when it runs; its effect is to build and name the finished multi-input model so the later training cell can use it.</p><p>This overview describes the more advanced version of the model, which contains 29,371 trainable parameters, in the following way:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;71f6a2a9-9b55-4d2c-9cff-a08e74296a61&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">rnn.summary()</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;4d01ad6c-af92-4e49-b1c6-d8aac690e86a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">Model: "model"
__________________________________________________________________________________________________
Layer (type)                    Output Shape         Param #     Connected to                     
==================================================================================================
Returns (InputLayer)            [(None, 52, 1)]      0                                            
__________________________________________________________________________________________________
Tickers (InputLayer)            [(None, 1)]          0                                            
__________________________________________________________________________________________________
LSTM1 (LSTM)                    (None, 52, 25)       2700        Returns[0][0]                    
__________________________________________________________________________________________________
embedding (Embedding)           (None, 1, 5)         12445       Tickers[0][0]                    
__________________________________________________________________________________________________
LSTM2 (LSTM)                    (None, 10)           1440        LSTM1[0][0]                      
__________________________________________________________________________________________________
reshape (Reshape)               (None, 5)            0           embedding[0][0]                  
__________________________________________________________________________________________________
Months (InputLayer)             [(None, 12)]         0                                            
__________________________________________________________________________________________________
Merged (Concatenate)            (None, 27)           0           LSTM2[0][0]                      
                                                                 reshape[0][0]                    
                                                                 Months[0][0]                     
__________________________________________________________________________________________________
batch_normalization (BatchNorma (None, 27)           108         Merged[0][0]                     
__________________________________________________________________________________________________
FC1 (Dense)                     (None, 10)           280         batch_normalization[0][0]        
__________________________________________________________________________________________________
Output (Dense)                  (None, 1)            11          FC1[0][0]                        
==================================================================================================
Total params: 16,984
Trainable params: 16,930
Non-trainable params: 54
__________________________________________________________________________________________________</code></pre></div><p>The purpose of this line is to print a structural summary of the neural network that was built earlier, so you can inspect the model before using it further. When it runs, Keras walks through every layer in the network and reports how data flows from the inputs to the final prediction, along with the size of each layer&#8217;s output and how many trainable parameters it contains.</p><p>The saved output shows that the model has three separate inputs. One input holds the 52-week return sequence, another holds the ticker identifier, and the third holds the month indicators. From there, the sequence input passes through the first LSTM layer, which keeps the time dimension and produces a sequence of 25 features at each of the 52 time steps. That sequence is then fed into the second LSTM layer, which condenses the temporal information down to a single 10-dimensional vector. In parallel, the ticker input goes through an embedding layer, which learns a compact 5-number representation for each ticker. Because embeddings are stored with a small extra dimension, that output is reshaped into a flat vector before being combined with the other features.</p><p>The month input enters directly as a 12-dimensional vector, since it is already in one-hot form. All three branches are then merged into one 27-dimensional representation, which is why the concatenated layer shows an output shape of 27. After that, batch normalization adjusts the combined activations to make training more stable, and the dense layer with 10 units creates a small fully connected hidden representation. The final dense layer produces a single number, which matches the regression target the model is trying to predict.</p><p>The parameter counts in the output reflect how much the model has learned in each part. The first LSTM has the most parameters among the recurrent layers because it has to learn how to process the full input sequence. The embedding layer&#8217;s parameters come from storing a learned vector for each ticker. The batch normalization layer contributes a few non-trainable values used to track running statistics during training, which is why the summary also separates trainable and non-trainable parameters at the bottom. Overall, the summary confirms that the network is a multi-input regression model that combines sequence information, ticker identity, and seasonality into one prediction.</p><h2>Fit the Model</h2><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;6b894867-847a-4ffd-85d0-8a07a137116e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">optimizer =tf.keras.optimizers.Adam()

rnn.compile(loss='mse',
            optimizer=optimizer)</code></pre></div><p>The purpose of this step is to prepare the neural network for training by choosing how it will learn and what it will try to minimize. An Adam optimizer is created first, which gives the model an efficient, adaptive way to update its weights during gradient descent. Adam is commonly used because it automatically adjusts learning rates for different parameters and usually trains recurrent models like this one more smoothly than plain stochastic gradient descent.</p><p>After that, the model is compiled, which is the moment where Keras turns the network definition into a trainable object. The loss function is set to mean squared error, so the model will be judged by how close its predicted values are to the actual target returns, with larger mistakes penalized more heavily. The optimizer created just above is then attached to the model so Keras knows how to update the weights while minimizing that loss. Since the cell only sets up the training configuration and does not run fitting or evaluation, there is no visible output yet; the effect of this step will show up later when the model starts learning from the training data.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;4fdd0352-8cea-4858-9168-f153d29363e0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">lstm_path = (results_path / 'lstm.regression.h5').as_posix()

checkpointer = ModelCheckpoint(filepath=lstm_path,
                               verbose=1,
                               monitor='val_loss',
                               mode='min',
                               save_best_only=True)</code></pre></div><p>The cell sets up where the model will be saved and prepares a checkpointing rule so training can keep the best version automatically. First it builds the full file path for the saved model by combining the results directory with the filename for the LSTM regression checkpoint, then converts that path into a plain string format that Keras can use. After that, it creates a ModelCheckpoint object, which acts like an automatic save button during training. The key idea is that the model will be monitored using validation loss, and because the goal is to minimize that value, the checkpoint is configured to watch for the lowest val<em>loss. The save</em>best_only setting means Keras will only write the model to disk when it finds a new best validation score, rather than overwriting the file at every epoch. Verbose output is enabled so training will report when a checkpoint is saved. There is no visible output from the cell itself because it is only defining the path and the checkpointing callback, not training the model yet; the actual save message would appear later, during fitting, when a better validation result is reached.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;64119552-c6da-4b07-831c-cbece8553662&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">early_stopping = EarlyStopping(monitor='val_loss', 
                              patience=5,
                              restore_best_weights=True)</code></pre></div><p>This sets up an early stopping rule for model training so the network does not keep learning once validation performance stops improving. The training process will watch the validation loss, which is the error measured on the held-out data after each epoch. If that loss fails to get better for five epochs in a row, training will be stopped automatically. The patience value gives the model a little room for temporary fluctuation, since validation loss can bounce around from epoch to epoch. The restore<em>best</em>weights setting is especially useful because it means that when training ends, the model is rolled back to the version that achieved the lowest validation loss, rather than keeping the weights from the final epoch. There is no visible output from the cell because nothing is being displayed or computed yet beyond defining this training callback for later use.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;549dd50d-baf4-465b-b7f9-1c998fe9f380&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">training = rnn.fit(X_train,
                   y_train,
                   epochs=50,
                   batch_size=64,
                   validation_data=(X_test, y_test),
                   callbacks=[early_stopping, checkpointer],
                   verbose=1)</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;3d1bd41f-1cb2-4df7-ab44-c60e626cbff5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">Epoch 1/50
16174/16179 [============================&gt;.] - ETA: 0s - loss: 0.0097
Epoch 00001: val_loss improved from inf to 0.00157, saving model to results/lstm_embeddings/lstm.regression.h5
16179/16179 [==============================] - 157s 10ms/step - loss: 0.0097 - val_loss: 0.0016
Epoch 2/50
16179/16179 [==============================] - ETA: 0s - loss: 0.0029
Epoch 00002: val_loss improved from 0.00157 to 0.00155, saving model to results/lstm_embeddings/lstm.regression.h5
16179/16179 [==============================] - 155s 10ms/step - loss: 0.0029 - val_loss: 0.0015
Epoch 3/50
16179/16179 [==============================] - ETA: 0s - loss: 0.0029
Epoch 00003: val_loss did not improve from 0.00155
16179/16179 [==============================] - 155s 10ms/step - loss: 0.0029 - val_loss: 0.0016
Epoch 4/50
16173/16179 [============================&gt;.] - ETA: 0s - loss: 0.0028
Epoch 00004: val_loss did not improve from 0.00155
16179/16179 [==============================] - 156s 10ms/step - loss: 0.0028 - val_loss: 0.0015
Epoch 5/50
16178/16179 [============================&gt;.] - ETA: 0s - loss: 0.0028
Epoch 00005: val_loss did not improve from 0.00155
16179/16179 [==============================] - 155s 10ms/step - loss: 0.0028 - val_loss: 0.0016
Epoch 6/50
16178/16179 [============================&gt;.] - ETA: 0s - loss: 0.0028
Epoch 00006: val_loss improved from 0.00155 to 0.00154, saving model to results/lstm_embeddings/lstm.regression.h5
16179/16179 [==============================] - 154s 10ms/step - loss: 0.0028 - val_loss: 0.0015
Epoch 7/50
16179/16179 [==============================] - ETA: 0s - loss: 0.0028
Epoch 00007: val_loss did not improve from 0.00154
16179/16179 [==============================] - 145s 9ms/step - loss: 0.0028 - val_loss: 0.0016
Epoch 8/50
16177/16179 [============================&gt;.] - ETA: 0s - loss: 0.0028
Epoch 00008: val_loss did not improve from 0.00154
16179/16179 [==============================] - 145s 9ms/step - loss: 0.0028 - val_loss: 0.0015
Epoch 9/50
16179/16179 [==============================] - ETA: 0s - loss: 0.0028
Epoch 00009: val_loss improved from 0.00154 to 0.00154, saving model to results/lstm_embeddings/lstm.regression.h5
16179/16179 [==============================] - 149s 9ms/step - loss: 0.0028 - val_loss: 0.0015
Epoch 10/50
16178/16179 [============================&gt;.] - ETA: 0s - loss: 0.0028
Epoch 00010: val_loss did not improve from 0.00154
16179/16179 [==============================] - 144s 9ms/step - loss: 0.0028 - val_loss: 0.0015
Epoch 11/50
16177/16179 [============================&gt;.] - ETA: 0s - loss: 0.0028
Epoch 00011: val_loss did not improve from 0.00154
16179/16179 [==============================] - 148s 9ms/step - loss: 0.0028 - val_loss: 0.0015
Epoch 12/50
16175/16179 [============================&gt;.] - ETA: 0s - loss: 0.0028
Epoch 00012: val_loss did not improve from 0.00154
16179/16179 [==============================] - 144s 9ms/step - loss: 0.0028 - val_loss: 0.0016
Epoch 13/50
16173/16179 [============================&gt;.] - ETA: 0s - loss: 0.0028
Epoch 00013: val_loss improved from 0.00154 to 0.00154, saving model to results/lstm_embeddings/lstm.regression.h5
16179/16179 [==============================] - 147s 9ms/step - loss: 0.0028 - val_loss: 0.0015
Epoch 14/50
16177/16179 [============================&gt;.] - ETA: 0s - loss: 0.0027
Epoch 00014: val_loss did not improve from 0.00154
16179/16179 [==============================] - 148s 9ms/step - loss: 0.0027 - val_loss: 0.0016
Epoch 15/50
16176/16179 [============================&gt;.] - ETA: 0s - loss: 0.0027
Epoch 00015: val_loss did not improve from 0.00154
16179/16179 [==============================] - 145s 9ms/step - loss: 0.0027 - val_loss: 0.0015
Epoch 16/50
16179/16179 [==============================] - ETA: 0s - loss: 0.0027
Epoch 00016: val_loss did not improve from 0.00154
16179/16179 [==============================] - 149s 9ms/step - loss: 0.0027 - val_loss: 0.0015
Epoch 17/50
16174/16179 [============================&gt;.] - ETA: 0s - loss: 0.0027
Epoch 00017: val_loss did not improve from 0.00154
16179/16179 [==============================] - 146s 9ms/step - loss: 0.0027 - val_loss: 0.0016
Epoch 18/50
16177/16179 [============================&gt;.] - ETA: 0s - loss: 0.0027
Epoch 00018: val_loss did not improve from 0.00154
16179/16179 [==============================] - 149s 9ms/step - loss: 0.0027 - val_loss: 0.0015</code></pre></div><p>The model training step begins here, where the network is fit on the training sequences and asked to learn how past weekly returns, ticker identity, and month information relate to the forward return target. It runs for up to 50 epochs with batches of 64 samples at a time, which means the data is processed in many small chunks so the optimizer can update the weights repeatedly without loading everything into one giant step. The validation data is set to the held-out test period, so after each epoch the model is judged on unseen 2017 examples to see whether it is genuinely improving rather than just memorizing the training set.</p><p>The progress log shows that the training loss drops quickly at the start and then settles into a narrow range, while the validation loss also becomes very small and fluctuates only slightly. That pattern suggests the model learns useful structure early on and then reaches a fairly stable plateau. The checkpoint callback is active throughout this process, so whenever the validation loss reaches a new best value, the model is saved to the file in the results folder. That is why the output repeatedly says the validation loss improved and the model was saved to results/lstm_embeddings/lstm.regression.h5. Even when the printed improvement is tiny, Keras still treats it as a new best if it beats the previous lowest validation loss by any amount.</p><p>The early stopping callback is also watching the validation loss at the same time. Since the validation metric stops making meaningful progress for several epochs in a row, training is eventually poised to stop before using all 50 epochs. This is a practical safeguard: it keeps the model from continuing to train once the holdout performance has leveled off, and it also ensures that the saved checkpoint corresponds to the best validation result seen during the run.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;127d0aeb-277e-450d-8e7d-34d97f108dff&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">loss_history = pd.DataFrame(training.history)</code></pre></div><p>The purpose of this step is to take the training record that Keras collected while the model was fitting and turn it into a pandas table that is easier to inspect and analyze. The training process stores metrics such as the loss for each epoch inside a history object, with each metric organized as a list over time. Wrapping that history in a DataFrame converts those lists into columns, so each row corresponds to one epoch and each column corresponds to one tracked value, such as training loss and validation loss if they were recorded. That makes it much simpler to plot the learning curve, compare how the model improved from epoch to epoch, or look for signs of overfitting. There is no visible output from the cell itself because it is just creating and storing this table in memory for later use rather than displaying or saving anything at this moment.</p><h2>Assess model performance</h2><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;9bbfa3af-5502-4d42-ac92-29340646e852&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">test_predict = pd.Series(rnn.predict(X_test).squeeze(), index=y_test.index)</code></pre></div><p>The purpose here is to take the trained recurrent neural network and use it to generate predictions for the held-out test set. The model&#8217;s prediction method is run on the test input data, which returns a value for each sample. Those raw predictions come back in a model-friendly array form, so they are squeezed down to a simple one-dimensional shape and then wrapped in a pandas Series. Using the test target&#8217;s index for that Series is important because it keeps each predicted value aligned with the correct ticker-date observation, which makes later comparison, plotting, and evaluation much easier.</p><p>Nothing is displayed when the cell runs because it only creates and stores the prediction series in memory. The result is a neatly indexed collection of model forecasts that can be lined up directly with the true test returns in the next steps.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;fbe0f728-2d8f-412f-8c5c-45adfd1cfbfc&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">df = y_test.to_frame('ret').assign(y_pred=test_predict)</code></pre></div><p>The goal here is to put the model&#8217;s predictions and the true test returns side by side in one tidy table. The target values for the test period are first converted into a DataFrame with a readable column name, so the actual weekly return is stored as ret. Then the model&#8217;s predicted values are added as a second column called y<em>pred. Behind the scenes, this creates a new DataFrame indexed the same way as y</em>test, so each row still lines up with the correct stock and date. Nothing is displayed because the result is just being assigned to df for later use, likely so the predictions can be analyzed, grouped, or plotted against the actual returns in the following steps.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;675c0c72-0148-456d-b676-cb75e2bba4a9&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">by_date = df.groupby(level='date')
df['deciles'] = by_date.y_pred.apply(pd.qcut, q=5, labels=False, duplicates='drop')</code></pre></div><p>The purpose here is to turn the model&#8217;s predictions into groups that are easy to compare. The data frame is first grouped by date, which matters because stock return predictions are usually evaluated across all stocks on the same day rather than one row at a time. That way, each day gets its own set of predicted returns, and the ranking is relative to the stocks available on that specific date.</p><p>Once the data is split into daily groups, the predicted values are divided into five buckets using quantile cuts. Even though the new column is named deciles, the setting used here creates quintiles, not ten groups. For each date, the predictions are sorted and then sliced into five roughly equal-sized bins, with labels numbered from 0 to 4. The duplicates option helps avoid errors on days where many predictions are tied or where there are too few distinct values to form all the desired cut points.</p><p>The result is added back into the data frame as a new column, so every stock-date observation now carries a group assignment based on how strong its predicted return was relative to other stocks on that same day. There is no saved output because the cell is just creating this classification column in memory; the effect shows up later when the grouped return performance is analyzed.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;462f1803-98b4-4ec6-8482-72bb1ff53b38&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">ic = by_date.apply(lambda x: spearmanr(x.ret, x.y_pred)[0]).mul(100)</code></pre></div><p>The purpose here is to turn the day-by-day prediction results into a single time series of rank correlations, one value per date. The data has already been grouped by date, so the operation walks through each date&#8217;s cross-sectional slice and compares the actual returns against the model&#8217;s predicted returns using Spearman correlation. That correlation is a rank-based measure, so it focuses on whether the model correctly orders stocks on a given day rather than whether it predicts the exact return values. The result of that calculation is then multiplied by 100, which simply rescales the numbers into percentage-style units that are easier to read and plot.</p><p>Behind the scenes, the apply step runs the same small calculation for each date group. For each one, it pulls the actual return column and the predicted return column, feeds them into the correlation function, and keeps only the correlation coefficient from the result. Because the output is not a single number but a value for every date, the saved object is a series indexed by date. There is no displayed output from the cell itself, but the variable created here becomes the input for later evaluation and visualization, where these daily information coefficients are typically summarized or plotted over time.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;bc505e62-7443-4147-87e5-39e81abf2f7a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">df.info()</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;fea1a550-2c22-4c6a-90f2-dd3f4dd62bf8&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">&lt;class 'pandas.core.frame.DataFrame'&gt;
MultiIndex: 131917 entries, ('A', Timestamp('2017-01-01 00:00:00')) to ('ZUMZ', Timestamp('2017-12-31 00:00:00'))
Data columns (total 3 columns):
 #   Column   Non-Null Count   Dtype  
---  ------   --------------   -----  
 0   ret      131917 non-null  float64
 1   y_pred   131917 non-null  float32
 2   deciles  131917 non-null  int64  
dtypes: float32(1), float64(1), int64(1)
memory usage: 3.1+ MB</code></pre></div><p>The purpose here is to quickly inspect the structure of the data frame and confirm that the prediction results were assembled the way expected. Calling the information summary prints a compact snapshot of the table&#8217;s size, indexing scheme, column names, missing-value counts, and data types, which is a convenient way to sanity-check the output before moving on to analysis or saving.</p><p>The printed summary shows that the table contains 131,917 rows and uses a MultiIndex made up of ticker and date values. That tells you the data is organized at the stock-date level, so each row represents one stock on one day within the 2017 evaluation period. The first and last index entries shown, from A on 2017-01-01 to ZUMZ on 2017-12-31, simply reflect the range of observations included in the frame.</p><p>The three listed columns are the actual return, the model&#8217;s prediction, and the decile assignment based on the prediction. The fact that all three columns have the same non-null count means there are no missing values in this result set, which is important because later ranking and grouping calculations depend on complete rows. The dtypes also make sense for the kind of data being stored: actual returns are floating-point numbers, predicted values are stored as float32 to save memory, and decile labels are integers because they represent bucket numbers. The memory usage line at the bottom shows the whole table is relatively small, which is typical once the data has been reduced to just the columns needed for evaluation.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;b674d0e5-4424-417b-b348-95a222262d13&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">test_predict = test_predict.to_frame('prediction')
test_predict.index.names = ['symbol', 'date']
test_predict.to_hdf(results_path / 'predictions.h5', 'predictions')</code></pre></div><p>The purpose here is to take the model&#8217;s test-set predictions, package them into a clean table, and save them to disk so they can be reused later without rerunning the model. First, the predictions are converted into a DataFrame with a single column named prediction. That step is useful because it turns a bare series of numbers into a labeled table, which is easier to inspect, align with other data, and store in a structured file format.</p><p>Next, the index names are set to symbol and date. This matters because the predictions are tied to a specific stock and a specific point in time, so giving those index levels clear names makes the saved file much more informative. It also ensures the results line up properly with the rest of the analysis, since later steps can refer to predictions by ticker and date instead of by unnamed index positions.</p><p>Finally, the DataFrame is written to an HDF5 file in the results folder under the key predictions. The lack of saved output is expected here because the cell is performing a file write rather than displaying anything on screen. The result is a persistent predictions file that contains the model&#8217;s output in a compact, indexed format, ready for later evaluation, plotting, or comparison with other models.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;f0006edd-af51-4739-a57e-8d71770b7dfc&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">rho, p = spearmanr(df.ret, df.y_pred)
print(f'{rho*100:.2f} ({p:.2%})')</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;b60e39d0-185f-42bb-9321-1758de165591&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">4.68 (0.00%)</code></pre></div><p>The purpose of this step is to measure how well the model&#8217;s predictions line up with the actual forward returns. It takes the two columns in the results table, one containing the true return values and the other containing the predicted values, and compares their rankings using Spearman correlation. That matters here because the goal is not just to predict the exact return level, but to see whether higher predicted values tend to correspond to higher actual outcomes.</p><p>Behind the scenes, Spearman correlation first ranks both sets of values and then checks how similar those rank orders are. A positive value means the predictions are generally pointing in the right direction, while a value near zero would mean little useful relationship. The second value returned alongside the correlation is a p-value, which indicates how likely it is to see a result this strong if there were really no relationship at all.</p><p>The printed result, 4.68 (0.00%), reflects both parts of that calculation in a compact form. The first number is the correlation multiplied by 100, so 4.68 means the rank correlation is about 0.0468. That is a small but positive association. The percentage in parentheses is the p-value formatted as a percentage, and 0.00% means it is extremely small, so the correlation is statistically significant even though the magnitude is modest.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;64a5439c-c589-4898-a731-a8c1a9895dfd&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">fig, axes = plt.subplots(ncols=2, figsize=(14,4))
sns.barplot(x='deciles', y='ret', data=df, ax=axes[0])
axes[0].set_title('Weekly Fwd Returns by Predicted Quintile')
axes[0].yaxis.set_major_formatter(FuncFormatter(lambda y, _: '{:.2%}'.format(y))) 
axes[0].set_ylabel('Weekly Returns')
axes[0].set_xlabel('Quintiles')

avg_ic = ic.mean()
title = f'4-Week Rolling IC | Weekly avg: {avg_ic:.2f} | Overall: {rho*100:.2f}'
ic.rolling(4).mean().dropna().plot(ax=axes[1], title=title)
axes[1].axhline(avg_ic, ls='--', c='k', lw=1)
axes[1].axhline(0, c='k', lw=1)
axes[1].set_ylabel('IC')
axes[1].set_xlabel('Date')

sns.despine()
fig.tight_layout()
fig.savefig(results_path / 'lstm_reg');</code></pre></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!PT3Z!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4bd84a1-e321-48b4-a576-abceb5c6517a_1000x280.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!PT3Z!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4bd84a1-e321-48b4-a576-abceb5c6517a_1000x280.png 424w, https://substackcdn.com/image/fetch/$s_!PT3Z!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4bd84a1-e321-48b4-a576-abceb5c6517a_1000x280.png 848w, https://substackcdn.com/image/fetch/$s_!PT3Z!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4bd84a1-e321-48b4-a576-abceb5c6517a_1000x280.png 1272w, https://substackcdn.com/image/fetch/$s_!PT3Z!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4bd84a1-e321-48b4-a576-abceb5c6517a_1000x280.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!PT3Z!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4bd84a1-e321-48b4-a576-abceb5c6517a_1000x280.png" width="1000" height="280" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c4bd84a1-e321-48b4-a576-abceb5c6517a_1000x280.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:280,&quot;width&quot;:1000,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:41829,&quot;alt&quot;:&quot;Output image&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/202848543?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4bd84a1-e321-48b4-a576-abceb5c6517a_1000x280.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Output image" title="Output image" srcset="https://substackcdn.com/image/fetch/$s_!PT3Z!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4bd84a1-e321-48b4-a576-abceb5c6517a_1000x280.png 424w, https://substackcdn.com/image/fetch/$s_!PT3Z!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4bd84a1-e321-48b4-a576-abceb5c6517a_1000x280.png 848w, https://substackcdn.com/image/fetch/$s_!PT3Z!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4bd84a1-e321-48b4-a576-abceb5c6517a_1000x280.png 1272w, https://substackcdn.com/image/fetch/$s_!PT3Z!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4bd84a1-e321-48b4-a576-abceb5c6517a_1000x280.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The purpose of this cell is to turn the model&#8217;s test-period results into two visual summaries: one that shows whether stocks predicted to be better had higher realized forward returns, and another that shows how stable the model&#8217;s ranking skill was through time. It starts by creating a figure with two side-by-side panels, which is why the saved output appears as a two-plot image in a single window.</p><p>On the left, a bar chart is drawn from the DataFrame containing the prediction buckets and realized returns. The predicted values have already been sorted into groups, so each bar represents one quintile of the model&#8217;s ranking. The bars summarize the average weekly forward return for each group, and the error bars show the uncertainty or spread around those averages. The y-axis is formatted as percentages, which makes the returns easier to read in the small decimal values typical of weekly stock performance. The title and axis labels are then added so the plot clearly communicates that this is comparing forward returns across predicted quintiles. In the saved image, the lowest bucket has the weakest average return and the higher buckets generally perform better, which suggests the model has some ability to rank stocks by expected return, even if the pattern is not perfectly smooth.</p><p>The right panel focuses on information coefficient, or IC, which is the rank correlation between predicted and actual returns over time. First, the code computes the average IC across all dates and uses it in the plot title along with the overall rank correlation percentage. Then it smooths the IC series with a 4-period rolling mean before plotting it. That smoothing step is why the line in the saved output looks less jagged than raw daily or weekly correlations would, making it easier to see the general trend in model quality across the year. The horizontal dashed line marks the average IC, and the solid zero line provides a neutral reference point. In the image, the IC moves above and below zero through the year, showing that the model&#8217;s ranking power varies by period rather than staying constant.</p><p>After both plots are created, the notebook removes extra chart borders for a cleaner appearance, tightens the layout so the two panels fit neatly, and saves the figure to disk in the results folder. The saved output you see is the rendered version of that final figure, so its structure and labels directly reflect the plotting steps in the cell.</p><div><hr></div><h2>Notebook 5 of 8: <code>04_multivariate_timeseries</code></h2><p><em>Source file: `04_multivariate_timeseries_processed.ipynb`</em></p><div><hr></div><h1>Multivariate Time Series Regression</h1><p>So far, the examples have focused on one series at a time. Recurrent neural networks are a natural fit for data with multiple time series, and they provide a nonlinear option in place of the Vector Autoregressive, or VAR, models introduced in Chapter 8, Time Series Models.</p><h2>Imports and configuration</h2><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;d84f693f-2a62-43e1-8833-acc2ef9d753f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">import warnings
warnings.filterwarnings('ignore')</code></pre></div><p>This step is just setting up a cleaner run by suppressing warning messages. The warnings module is imported first, and then the warning filter is changed so that future warnings will be hidden from the notebook output. Nothing is computed or displayed here, so there is no saved output. The main effect is practical rather than analytical: later cells can focus on the actual data, plots, and model results without being cluttered by repeated library warnings that are often informative for developers but distracting in a teaching notebook.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;b3e3adbf-34fb-4ed5-b2cb-8f6c6fed112f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">%matplotlib inline

from pathlib import Path
import numpy as np
import pandas as pd
import pandas_datareader.data as web

from sklearn.metrics import mean_absolute_error
from sklearn.preprocessing import minmax_scale

import tensorflow as tf
from tensorflow.keras.callbacks import ModelCheckpoint, EarlyStopping
from tensorflow.keras.models import Sequential, Model
from tensorflow.keras.layers import Dense, LSTM
import tensorflow.keras.backend as K

import matplotlib.pyplot as plt
import seaborn as sns</code></pre></div><p>The cell sets up the tools needed for the rest of the forecasting workflow. It begins by making matplotlib plots display directly inside the notebook, which is helpful because later cells will create figures for the data, the training history, and the predictions. After that, it imports the standard building blocks for working with files, arrays, tables, plotting, machine learning evaluation, data scaling, and deep learning.</p><p>Several of the imports are there because the notebook will work with monthly economic time series pulled from FRED, so it brings in the data reader used to download those series and the pandas and NumPy libraries used to organize and transform them. It also imports mean absolute error and min-max scaling from scikit-learn, since the model will be evaluated with MAE and the transformed data will be rescaled before training.</p><p>The TensorFlow and Keras imports prepare the neural network side of the notebook. The model will be built with a Sequential architecture using LSTM and Dense layers, and the training process will rely on callbacks that save the best model and stop early if validation performance stops improving. The backend import is included so the notebook can manage Keras session state when needed.</p><p>Finally, matplotlib and seaborn are imported for visualization. Together, these imports provide everything needed to load the time series, preprocess it, train the recurrent network, and then inspect the results graphically. There is no visible output from this cell because it only prepares the notebook environment and loads libraries; the real action comes in the later cells that use these imports.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;a8a110e1-4c30-48d0-a767-185dcd8e8ccd&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">gpu_devices = tf.config.experimental.list_physical_devices('GPU')
if gpu_devices:
    print('Using GPU')
    tf.config.experimental.set_memory_growth(gpu_devices[0], True)
else:
    print('Using CPU')</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;968f0f9c-d859-40ff-97b3-9768cc448dbc&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">Using CPU</code></pre></div><p>The cell checks whether TensorFlow can see a GPU on the system, which matters because a GPU can speed up neural network training a great deal. It asks TensorFlow for the list of available GPU devices and stores that result. If at least one GPU is found, it announces that the GPU will be used and turns on memory growth for the first device so TensorFlow only claims the GPU memory it actually needs instead of grabbing everything at once. If no GPU is available, it falls back to the CPU and prints that message instead. The saved output shows &#8220;Using CPU,&#8221; so TensorFlow did not detect a usable GPU in this environment, and the rest of the notebook will run on the processor rather than on graphics hardware.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;bd95fc20-a5ae-47a4-9ef0-6968fd8e7806&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">sns.set_style('whitegrid')
np.random.seed(42)</code></pre></div><p>The purpose here is to set up a consistent look for any plots that come later and to make the random behavior in the notebook reproducible. The first line switches Seaborn to a white grid style, which gives charts a clean background with light grid lines, making time series plots easier to read. The next line fixes NumPy&#8217;s random seed at 42. That means any operations in the notebook that rely on NumPy-generated randomness will produce the same results each time the notebook is run, which is especially helpful for comparing experiments or debugging. There is no visible output because both actions simply change the notebook&#8217;s plotting and randomness settings in memory; they prepare the environment for later cells rather than display anything on their own.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;7a26ceb0-56ab-4554-a7ff-f25eea8624cb&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">results_path = Path('results', 'multivariate_time_series')
if not results_path.exists():
    results_path.mkdir(parents=True)</code></pre></div><p>This cell makes sure there is a place to store the figures and other results produced later in the analysis. It builds a path pointing to a folder named results/multivariate<em>time</em>series, then checks whether that folder already exists on disk. If it does not, the folder is created, including any missing parent directories along the way. Behind the scenes, this is a simple setup step that helps later plotting and saving commands run smoothly, because they can write files into a known location without having to worry about whether the directory is already there. Since the cell only prepares the filesystem and does not print anything or display a figure, there is no saved output.</p><h2>Load the data</h2><p>For a direct comparison, we show how recurrent neural networks can be used to model and forecast multiple time series with the same data set from the VAR example. The data consists of monthly consumer sentiment and industrial production series obtained from the Federal Reserve FRED service in Chapter 8, Time Series Models:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;9000bdb1-9e36-485d-8aec-fd6b8ac344d1&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">df = web.DataReader(['UMCSENT', 'IPGMFN'], 'fred', '1980', '2019-12').dropna()
df.columns = ['sentiment', 'ip']
df.info()</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;3c4f9259-ffd1-43de-a238-9b3f0aa0c760&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">&lt;class 'pandas.core.frame.DataFrame'&gt;
DatetimeIndex: 480 entries, 1980-01-01 to 2019-12-01
Data columns (total 2 columns):
 #   Column     Non-Null Count  Dtype  
---  ------     --------------  -----  
 0   sentiment  480 non-null    float64
 1   ip         480 non-null    float64
dtypes: float64(2)
memory usage: 11.2 KB</code></pre></div><p>The purpose here is to pull the two monthly economic series that will be used in the forecasting example and then take a quick look at what was loaded. The data reader fetches consumer sentiment and industrial production from FRED for the period from January 1980 through December 2019, and the missing values are dropped right away so the dataframe is left with only complete observations. After that, the two original FRED ticker names are replaced with clearer labels, so the columns are easier to work with later: one for sentiment and one for industrial production.</p><p>The call to the dataframe summary then prints a compact check of the result. The saved output shows 480 monthly observations indexed by date, which makes sense for a 40-year span of monthly data. Both columns are stored as float64 values, and there are no remaining missing values in either series after the drop step. This quick inspection confirms that the dataset is ready for the next stages of transformation and modeling, with the time index intact and the two variables aligned month by month.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;a407f5ac-1240-46c3-828e-bea501739698&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">df.head()</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;f7e4d426-23de-4098-a2f4-cea7541e8a45&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">            sentiment       ip
DATE                          
1980-01-01       67.0  46.8770
1980-02-01       66.9  47.9757
1980-03-01       56.5  48.4793
1980-04-01       52.7  47.0662
1980-05-01       51.7  45.6995</code></pre></div><p>The purpose here is simply to take a quick look at the first rows of the dataframe so you can confirm that the data loaded correctly and has the shape you expect before moving on to any modeling steps. Displaying the beginning of the table is a common sanity check in time series work because it lets you verify the column names, the date index, and the kind of values being carried through the pipeline.</p><p>The output shows two variables, sentiment and ip, indexed by DATE. The dates begin in January 1980 and advance monthly, which tells you the series has been organized as a proper time-indexed monthly dataset. The numbers also make sense in their original scale: sentiment is around the 50s and 60s, while industrial production is around the mid-40s. Seeing the first few observations laid out like this confirms that the data has been renamed and combined correctly, and it gives you a concrete view of the raw inputs before later steps transform them for the LSTM model.</p><h2>Data preparation</h2><h3>Making the series stationary</h3><p>We use the same preprocessing step here that was introduced in Chapter 8 on Time Series Models: both series are differenced over a 12 month span, and industrial production is first transformed with a logarithm so the result is more stationary.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;ee161e8e-0352-49d7-9111-b57db04da1e8&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">df_transformed = (pd.DataFrame({'ip': np.log(df.ip).diff(12),
                                'sentiment': df.sentiment.diff(12)})
                  .dropna())</code></pre></div><p>The goal here is to turn the raw monthly series into a form that is easier for the forecasting model to learn from. Industrial production is first put on a logarithmic scale before differencing, which helps stabilize changes in a series that grows over time. Then both industrial production and consumer sentiment are differenced by 12 months, so each value represents how much the series has changed compared with the same month a year earlier. That kind of year-over-year transformation removes a lot of trend and seasonal structure and makes the series more suitable for models that work better with more stationary patterns.</p><p>After those two transformed columns are assembled into a new dataframe, any rows that now contain missing values are removed. Those missing values appear at the beginning because a 12-month difference cannot be computed until at least 12 earlier observations exist. The result is a cleaned dataframe containing only the aligned, transformed observations, ready for the later scaling and sequence-building steps. There is no displayed output from this cell because it is preparing the data quietly in memory rather than printing or plotting anything.</p><h3>Rescaling the data</h3><p>Next, we rescale the transformed data so that all values fall between 0 and 1:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;fc70cdea-9996-42e8-ae4c-a51ceb73a8c5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">df_transformed = df_transformed.apply(minmax_scale)</code></pre></div><p>The goal here is to put the transformed series onto the same numerical scale before feeding them into the neural network. After the differencing and log transformation, the two columns can still have very different ranges, and an LSTM generally trains more smoothly when each input feature lives in a comparable interval. Applying the scaling function column by column rescales each series independently so that its smallest value becomes 0 and its largest value becomes 1, with everything else spread proportionally in between.</p><p>Behind the scenes, the operation takes each column of the transformed dataframe and replaces its values with their min-max normalized versions. Because the dataframe is being overwritten, the original transformed values are no longer kept in this variable; from this point on, the model preparation steps work with the rescaled data. There is no visible output because the cell simply updates the dataframe in memory rather than printing or plotting anything, but it changes the data in an important way for the next stages of the workflow.</p><h3>Plot the raw and transformed time series</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;f508c906-f321-4ce3-927b-efeefc181b96&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">fig, axes = plt.subplots(ncols=2, figsize=(14,4))
columns={'ip': 'Industrial Production', 'sentiment': 'Sentiment'}
df.rename(columns=columns).plot(ax=axes[0], title='Original Series')
df_transformed.rename(columns=columns).plot(ax=axes[1], title='Transformed Series')
sns.despine()
fig.tight_layout()
fig.savefig(results_path / 'multi_rnn', dpi=300)</code></pre></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!cZTr!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4baed9a1-a6f7-4113-b901-9bba90ab2053_998x280.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!cZTr!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4baed9a1-a6f7-4113-b901-9bba90ab2053_998x280.png 424w, https://substackcdn.com/image/fetch/$s_!cZTr!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4baed9a1-a6f7-4113-b901-9bba90ab2053_998x280.png 848w, https://substackcdn.com/image/fetch/$s_!cZTr!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4baed9a1-a6f7-4113-b901-9bba90ab2053_998x280.png 1272w, https://substackcdn.com/image/fetch/$s_!cZTr!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4baed9a1-a6f7-4113-b901-9bba90ab2053_998x280.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!cZTr!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4baed9a1-a6f7-4113-b901-9bba90ab2053_998x280.png" width="998" height="280" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4baed9a1-a6f7-4113-b901-9bba90ab2053_998x280.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:280,&quot;width&quot;:998,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:94799,&quot;alt&quot;:&quot;Output image&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/202848543?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4baed9a1-a6f7-4113-b901-9bba90ab2053_998x280.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Output image" title="Output image" srcset="https://substackcdn.com/image/fetch/$s_!cZTr!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4baed9a1-a6f7-4113-b901-9bba90ab2053_998x280.png 424w, https://substackcdn.com/image/fetch/$s_!cZTr!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4baed9a1-a6f7-4113-b901-9bba90ab2053_998x280.png 848w, https://substackcdn.com/image/fetch/$s_!cZTr!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4baed9a1-a6f7-4113-b901-9bba90ab2053_998x280.png 1272w, https://substackcdn.com/image/fetch/$s_!cZTr!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4baed9a1-a6f7-4113-b901-9bba90ab2053_998x280.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The purpose of this cell is to compare the raw monthly series with the version that has been prepared for the recurrent model, so you can see how the preprocessing changes the data before forecasting starts. It first creates a figure with two side-by-side panels, which gives enough room to look at both series together without crowding the display. The column names are then replaced with more readable labels so the plots are easier to interpret at a glance.</p><p>On the left, the original consumer sentiment and industrial production series are plotted in their raw form. This view shows the strong long-term upward trend in industrial production and the more irregular swings in sentiment over time. On the right, the transformed version of the same two series is plotted after the stationarity step. Because the data have been differenced and scaled, the values are now compressed into a much narrower range and the long-term trend has been removed. That is why the lines on the right look more stable and fluctuate around similar levels instead of steadily rising or falling.</p><p>The saved figure reflects exactly this contrast: the original panel shows the broad economic history in the raw measurements, while the transformed panel shows the normalized, model-ready version that the LSTM will actually learn from. The call to remove extra chart spines gives the plot a cleaner appearance, and the layout adjustment prevents labels and titles from overlapping before the figure is written to disk in the results folder.</p><h3>Convert the data to the format expected by the RNN</h3><p>We could reshape the data straight away and obtain separate, non-overlapping sequences. In that setup, each year would become a single sample, but this only works when the total number of observations can be evenly divided by the window size:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;d74e60e7-974b-4be8-937b-e863f720ee81&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">df.values.reshape(-1, 12, 2).shape</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;635a32ce-366c-4e9e-bdec-a188d39d3405&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">(40, 12, 2)</code></pre></div><p>This step checks the shape of the transformed data after reshaping it into fixed time windows. The underlying dataframe has already been converted into a NumPy array, and the values are being reorganized into groups of 12 time steps with 2 variables in each step. That means each sample now represents one year of monthly observations for the two series, rather than one long continuous table of rows.</p><p>The result, (40, 12, 2), tells us there are 40 such samples, each made up of 12 time points and 2 features. Behind the scenes, NumPy is simply taking the flat sequence of values and laying them out in that three-dimensional form. The shape is useful because recurrent networks expect inputs organized as samples, time steps, and features, so this confirms that the data has been arranged into the structure an LSTM can work with.</p><p>But the sequence data should advance with a sliding window rather than being split into separate, non-overlapping blocks. The <code>create_multivariate_rnn_data</code> function converts a multivariate time series dataset into the format expected by Keras RNN layers, with dimensions of <code>n_samples</code> by <code>window_size</code> by <code>n_series</code>, as shown below:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;1e92ebb9-f27f-4588-bfe1-70f5769e3e94&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def create_multivariate_rnn_data(data, window_size):
    y = data[window_size:]
    n = data.shape[0]
    X = np.stack([data[i: j] 
                  for i, j in enumerate(range(window_size, n))], axis=0)
    return X, y</code></pre></div><p>A helper function is being defined here to turn a regular multivariate time series into the kind of input an LSTM can learn from. The key idea is that a recurrent network does not work with one row at a time in the same way a standard regression model does; instead, it learns from short sequences of past observations. This function prepares those sequences by taking a rolling window over the data.</p><p>First, it sets aside the targets, which are the observations that come after each window. Since the window has a fixed length, everything from that point onward becomes the expected output. Then it gets the total number of observations in the dataset so it can step through the series from start to finish. For each position in time, it collects the preceding block of rows with the chosen window size, and stacks all of those overlapping windows into one three-dimensional array. That stacked array becomes the input data, where each sample contains a sequence of past time steps and each time step contains all variables in the series.</p><p>The result is a pair of arrays that line up naturally for supervised learning: one array of input windows and one array of next-step targets. Nothing is displayed yet because the function is only being created at this point, not executed. It will be used later to reshape the transformed time series into training examples for the RNN.</p><p>We will set the window size to 24 months and then construct the input sequences needed for the RNN model in the following way:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;f89c62ee-c523-4aa6-8971-71274aeffe31&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">window_size = 18</code></pre></div><p>This step sets the length of the input window the recurrent network will look at before making a prediction. By choosing a value of 18, the model is told to use the previous 18 monthly observations from both series together as its context for forecasting the next point. Behind the scenes, this number controls how the time series is broken into overlapping sequences later on: each training example will contain 18 consecutive time steps, and the target will be the observation that comes immediately after that window. Since there is no printed result here, the cell simply stores this setting for use in the later data reshaping step.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;c0ee1c24-1622-4b30-ad2e-d66e5971f010&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">X, y = create_multivariate_rnn_data(df_transformed, window_size=window_size)</code></pre></div><p>This step turns the transformed time series into the kind of input and target data an LSTM expects. The helper function walks through the dataframe with a sliding window of fixed length and, for each position, collects a block of past observations as one training example. Because the data contains two variables, each window keeps both series together at every time step, so the model can learn how they move jointly over time rather than treating them separately. At the end of each window, the function takes the next observation as the prediction target, which creates a supervised learning setup from what was originally just a chronological dataset.</p><p>The result is two arrays: one holding the overlapping sequences that will be fed into the network, and one holding the corresponding next-step values the network should learn to predict. Nothing is printed here because the cell is mainly preparing data rather than displaying it, but it is an essential transition point in the workflow. After this, the transformed monthly series is no longer just a table of values; it has been reshaped into input-output pairs that can be used to train the recurrent model.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;de19c678-d60a-40ce-a7e6-96ad20d3e3a1&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">X.shape, y.shape</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;6991534e-2fda-450a-bd91-5d664c442aed&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">((450, 18, 2), (450, 2))</code></pre></div><p>The purpose here is to quickly verify that the sliding-window transformation produced the expected supervised-learning shapes. The first object, X, contains the input sequences for the LSTM, so its shape shows 450 examples, each made up of 18 time steps and 2 features. Those 2 features are the two transformed monthly series being modeled together. The second object, y, contains the targets the model should learn to predict, so its shape shows 450 matching target rows with 2 values each, again one for each series.</p><p>The result confirms that the data was reshaped correctly for a multivariate sequence-to-one forecasting setup. The 18-step window matches the chosen lookback length, and the equal number of rows in X and y means each input window has a corresponding next-step target. Seeing 450 samples also tells us how many usable training examples remained after differencing, scaling, and converting the full time series into overlapping windows.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;5e1cba80-96d5-47c2-91f4-7d37daeeb361&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">df_transformed.head()</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;2d0decfa-18d9-4e8e-a9e2-64a930871cb8&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">                  ip  sentiment
DATE                           
1981-01-01  0.526669   0.576214
1981-02-01  0.513795   0.502513
1981-03-01  0.542863   0.670017
1981-04-01  0.613397   0.832496
1981-05-01  0.731775   0.914573</code></pre></div><p>The purpose here is simply to look at the beginning of the transformed dataset after the time series have been differenced and scaled. Showing the first few rows is a quick sanity check: it lets you confirm that the transformation worked, that the data is still indexed by date, and that both series are present in the expected order.</p><p>What appears in the output is a small table with two columns, industrial production and sentiment, and a monthly date index starting in 1981. The first date is not the original start of the raw data because annual differencing removes the first 12 months, so the transformed series cannot begin until there is enough history to compute those changes. The values are between 0 and 1 because the data was then scaled to that range, which is exactly what you would expect before feeding it into the recurrent neural network. The numbers themselves are not important individually here; what matters is that they are smooth, numerical, and ready for sequence modeling.</p><p>At this point, the dataset is divided into training and testing portions. The most recent 24 months are held back for evaluating how well the model performs on unseen data, as illustrated below:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;364046ce-a831-4a97-87ef-b4b8c5ca15ce&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">test_size =24
train_size = X.shape[0]-test_size</code></pre></div><p>The purpose here is to split the sequence data into a training portion and a final holdout portion for evaluation. The model has already been turned into overlapping input windows, so the first step is to decide how many of those windows should be kept for training and how many should be reserved for testing. A test size of 24 means the last 24 time steps are set aside as unseen data, which is a common choice in forecasting because it lets us check how well the model performs on the most recent period rather than on random samples from the past.</p><p>From there, the training size is calculated by taking the total number of available windows and subtracting those 24 held-out observations. That gives the number of sequences the model can learn from before it reaches the test period. Nothing is displayed when the cell runs because it is only setting up two values for later use, but those values are important: they control the slice points used to create the training and test sets in the next step, and they help preserve the time order that matters in a forecasting problem.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;2388337f-0d4d-49a7-866b-12381d4e7192&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">X_train, y_train = X[:train_size], y[:train_size]
X_test, y_test = X[train_size:], y[train_size:]</code></pre></div><p>The purpose here is to split the supervised time series data into a training portion and a testing portion while preserving the original order of the observations. The first slice takes the earliest part of the input windows and their matching targets and assigns them to the training sets. The second slice takes everything from the training cutoff onward and assigns it to the test sets.</p><p>Because these are time-ordered sequences, the split is done by position rather than by random shuffling. That matters in forecasting problems, since the model should learn from earlier history and then be evaluated on later, unseen periods. The training arrays therefore contain the past examples used to fit the LSTM, while the test arrays hold the most recent windows reserved for checking how well the model generalizes to future data.</p><p>There is no displayed output because the operation is just an assignment step. It prepares four separate datasets for the next stages of model fitting and evaluation, but it does not print, plot, or calculate anything visible on its own.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;60072d45-55c8-4219-8e64-3ad09734ea6a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">X_train.shape, X_test.shape</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;71077896-8435-4788-8dd3-035f915f3039&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">((426, 18, 2), (24, 18, 2))</code></pre></div><p>The purpose of this cell is to quickly check how the data was split before training and evaluation. It asks for the shapes of the training and test feature arrays, which is a simple but important sanity check after the rolling-window transformation and the time-based split.</p><p>The first shape, 426 by 18 by 2, shows that the training set contains 426 separate examples. Each example is an 18-month window, and each month in that window has two features, one for each time series. The second shape, 24 by 18 by 2, shows that the test set uses the same window structure, but only for 24 examples from the later part of the timeline. Behind the scenes, this confirms that the data was organized into overlapping sequences rather than into one long flat table, which is exactly what an LSTM expects.</p><p>The output makes sense because every sample must preserve both the time dimension and the multivariate structure of the series. The equal window length and feature count in both sets show that the model will see the same input format during training and testing, while the different number of samples reflects the fact that most of the available sequences were reserved for training and only the most recent ones were held out for evaluation.</p><h2>Build the Model Structure</h2><p>We use a comparable network design made up of two LSTM layers placed one after the other, with 12 units in the first layer and 6 in the second. A dense layer with 10 units comes next, and the final output layer contains two units so the model can produce one value for each time series. The model is then compiled with mean absolute error as the loss function and RMSProp as the optimizer, as shown below:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;86a0fef1-99b1-4137-8c5c-d9208cd20183&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">K.clear_session()</code></pre></div><p>Before building or retraining the network, the TensorFlow/Keras backend is cleared so any previous model state, layer definitions, or computational graphs are removed from memory. This is a reset step that helps avoid conflicts from earlier experiments and makes the next model start from a clean slate. It is especially useful in notebooks, where running cells multiple times can otherwise leave old objects around and interfere with the new training run.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;bef26d23-a5d9-4cdc-bae9-acb3151cf8a8&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">n_features = output_size = 2</code></pre></div><p>This line sets two related variables at the same time: the model will work with two input features and it will also produce two output values. That matches the structure of the dataset, which contains two time series being modeled together, so each training example feeds in both series and the prediction tries to estimate both series at the next time step. Keeping the input and output size aligned like this is important because the LSTM is being used as a multivariate forecasting model rather than a single-series predictor.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;7eb4f9a4-3b31-4506-833f-48d2daf208a8&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">lstm_units = 12
dense_units = 6</code></pre></div><p>This step sets the size of two parts of the neural network before the model is built. The first value determines how many memory units the LSTM layer will use, which controls how much temporal pattern information it can retain while it processes the input sequence. The second value sets the number of neurons in the intermediate dense layer, which acts as a compact nonlinear transformation between the LSTM output and the final forecast. Choosing relatively small numbers keeps the model simple and limits the number of trainable parameters, which is useful when working with a modest-sized time series dataset. Since this cell only assigns these settings, it does not produce any visible output; instead, it prepares values that will be used immediately in the model definition that follows.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;7241e7ec-7f66-4c9a-85bd-d3cd5d725af3&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">rnn = Sequential([
    LSTM(units=lstm_units,
         dropout=.1,
         recurrent_dropout=.1,
         input_shape=(window_size, n_features), name='LSTM',
         return_sequences=False),
    Dense(dense_units, name='FC'),
    Dense(output_size, name='Output')
])</code></pre></div><p>A small recurrent neural network is being assembled here for the forecasting task. The model is built as a simple stack of layers, which means the data will flow straight through each layer in sequence from input to output. The first layer is an LSTM, the part of the network designed to read the time window one step at a time and learn patterns that depend on order and memory. Its input shape tells the model to expect each training example as a sequence with the chosen window length and two features at each time step, one for each economic series. The dropout settings add a bit of regularization by randomly ignoring some connections during training, which helps reduce overfitting. Because return sequences is set to false, the LSTM produces just one final summary vector for the whole input window rather than a separate output at every time step.</p><p>That summary then passes into a fully connected hidden layer, which gives the network a chance to combine the LSTM&#8217;s learned features in a more flexible way before making a forecast. The last dense layer produces the final prediction with one value for each target series, so the model outputs both the future industrial production value and the future sentiment value together. No result appears yet because this cell only defines the architecture; the actual learning and predictions happen later once the model is compiled and trained.</p><p>The model contains 1,268 trainable parameters, as displayed below:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;990ca592-b6ba-445b-850b-3b9359e686ab&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">rnn.summary()</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;8d875932-727a-4b9c-898b-ba339be848f5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
LSTM (LSTM)                  (None, 12)                720       
_________________________________________________________________
FC (Dense)                   (None, 6)                 78        
_________________________________________________________________
Output (Dense)               (None, 2)                 14        
=================================================================
Total params: 812
Trainable params: 812
Non-trainable params: 0
_________________________________________________________________</code></pre></div><p>The model summary gives a compact snapshot of the neural network that has just been built, showing how data will flow through it and how many parameters the model has to learn. The first layer is an LSTM, which receives each input sequence and compresses the information across time into a 12-dimensional representation. That is why its output shape is shown as a batch of vectors with 12 values each, and most of the model&#8217;s parameters live here, since the LSTM has to learn both how to keep memory over time and how to combine the two input series.</p><p>The next layer is a fully connected hidden layer with 6 units. Its role is to take the LSTM&#8217;s learned representation and transform it into a smaller, more useful feature space before the final prediction step. Because it is a standard dense layer, its parameter count is much smaller than the LSTM&#8217;s. The final output layer has 2 units, matching the two variables being forecast: consumer sentiment and industrial production. Its output shape reflects that the network produces one prediction for each series at each forecast step.</p><p>The total parameter count, 812, tells us the model is relatively small, which makes sense for a monthly economic forecasting problem with only two variables and a modest training set. The fact that all 812 parameters are trainable means every weight in the network will be updated during training, and there are no frozen layers. The summary therefore confirms the architecture, the output dimensionality, and the overall scale of the model before training and evaluation.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;58d9068a-2223-416c-953d-1898412bae75&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">rnn.compile(loss='mae', optimizer='RMSProp')</code></pre></div><p>The model is being prepared for training by telling Keras exactly how it should measure mistakes and how it should adjust its weights. Using mean absolute error as the loss means the network will be trained to minimize the average size of its prediction errors, treating overestimates and underestimates symmetrically and keeping the objective easy to interpret in the same units as the scaled target values. Choosing RMSProp as the optimizer sets the update rule that will be used to move the weights in the direction that reduces that error, which is a common and effective choice for recurrent networks because it adapts the learning rate as training progresses and tends to work well on sequence data. Nothing is displayed yet because compiling is only a setup step: the model is not learning at this point, but it is being configured so that the next training cell can run with a defined loss function and optimization strategy.</p><h2>Fit the Model</h2><p>We fit the model for up to 50 epochs, use a batch size of 20, and apply early stopping to stop training when the validation performance no longer improves.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;9105c2ef-d6da-46de-ae7f-087535b9a030&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">lstm_path = (results_path / 'lstm.h5').as_posix()

checkpointer = ModelCheckpoint(filepath=lstm_path,
                               verbose=1,
                               monitor='val_loss',
                               mode='min',
                               save_best_only=True)</code></pre></div><p>The purpose here is to set up a checkpoint so the best version of the LSTM model gets saved during training. First, the path for the saved model file is built by taking the results folder and adding the name lstm.h5, then converting that path into a standard string that TensorFlow can work with easily. After that, a ModelCheckpoint callback is created. Its job is to watch the validation loss while the model trains and keep track of the best-performing epoch. Because it is configured with save<em>best</em>only set to true and the mode set to min, it will save a new copy of the model only when val_loss gets lower than before, since lower validation loss means better performance. The verbose setting means training will print a message whenever the checkpoint is updated. Nothing is displayed immediately when this cell runs, because it is only preparing the callback object to be passed into the training step later.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;a6967dc1-4667-48ed-9307-4ea3a24691e7&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">early_stopping = EarlyStopping(monitor='val_loss', 
                              patience=10,
                              restore_best_weights=True)</code></pre></div><p>The purpose here is to set up an automatic stopping rule for training the neural network so it does not keep learning once performance on unseen data stops improving. The training process is monitored using the validation loss, which is the error measured on the held-out validation set after each epoch. By watching that value, the model can tell whether additional training is actually helping generalization or just fitting the training data more tightly.</p><p>The patience setting gives the model some flexibility by allowing up to 10 epochs without improvement before training is stopped. That prevents the process from ending too quickly because of a small temporary fluctuation in the validation loss. At the same time, restore<em>best</em>weights is turned on so that when training eventually stops, the model rolls back to the version from the epoch with the lowest validation loss rather than keeping the last epoch&#8217;s weights. Since this cell only creates the stopping rule and does not run training itself, there is no saved output yet. The effect of this setup appears later, when the model fitting process ends early once validation performance stops getting better.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;dd3d2771-4ee9-4162-95bf-330658b38d82&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">result = rnn.fit(X_train,
                 y_train,
                 epochs=100,
                 batch_size=20,
                 shuffle=False,
                 validation_data=(X_test, y_test),
                 callbacks=[early_stopping, checkpointer],
                 verbose=1)</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;8fa1554d-c731-4b0a-905d-00c32e044579&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">Epoch 1/100
19/22 [========================&gt;.....] - ETA: 0s - loss: 0.2743
Epoch 00001: val_loss improved from inf to 0.04285, saving model to results/multivariate_time_series/lstm.h5
22/22 [==============================] - 1s 25ms/step - loss: 0.2536 - val_loss: 0.0429
Epoch 2/100
20/22 [==========================&gt;...] - ETA: 0s - loss: 0.1013
Epoch 00002: val_loss improved from 0.04285 to 0.03912, saving model to results/multivariate_time_series/lstm.h5
22/22 [==============================] - 0s 13ms/step - loss: 0.0991 - val_loss: 0.0391
Epoch 3/100
20/22 [==========================&gt;...] - ETA: 0s - loss: 0.0956
Epoch 00003: val_loss did not improve from 0.03912
22/22 [==============================] - 0s 12ms/step - loss: 0.0941 - val_loss: 0.0404
Epoch 4/100
19/22 [========================&gt;.....] - ETA: 0s - loss: 0.0965
Epoch 00004: val_loss improved from 0.03912 to 0.03764, saving model to results/multivariate_time_series/lstm.h5
22/22 [==============================] - 0s 14ms/step - loss: 0.0945 - val_loss: 0.0376
Epoch 5/100
18/22 [=======================&gt;......] - ETA: 0s - loss: 0.0910
Epoch 00005: val_loss did not improve from 0.03764
22/22 [==============================] - 0s 12ms/step - loss: 0.0918 - val_loss: 0.0504
Epoch 6/100
21/22 [===========================&gt;..] - ETA: 0s - loss: 0.0903
Epoch 00006: val_loss improved from 0.03764 to 0.03714, saving model to results/multivariate_time_series/lstm.h5
22/22 [==============================] - 0s 13ms/step - loss: 0.0898 - val_loss: 0.0371
Epoch 7/100
20/22 [==========================&gt;...] - ETA: 0s - loss: 0.0898
Epoch 00007: val_loss did not improve from 0.03714
22/22 [==============================] - 0s 12ms/step - loss: 0.0885 - val_loss: 0.0376
Epoch 8/100
19/22 [========================&gt;.....] - ETA: 0s - loss: 0.0908
Epoch 00008: val_loss did not improve from 0.03714
22/22 [==============================] - 0s 13ms/step - loss: 0.0884 - val_loss: 0.0491
Epoch 9/100
19/22 [========================&gt;.....] - ETA: 0s - loss: 0.0899
Epoch 00009: val_loss did not improve from 0.03714
22/22 [==============================] - 0s 12ms/step - loss: 0.0876 - val_loss: 0.0418
Epoch 10/100
19/22 [========================&gt;.....] - ETA: 0s - loss: 0.0906
Epoch 00010: val_loss improved from 0.03714 to 0.03557, saving model to results/multivariate_time_series/lstm.h5
22/22 [==============================] - 0s 13ms/step - loss: 0.0892 - val_loss: 0.0356
Epoch 11/100
19/22 [========================&gt;.....] - ETA: 0s - loss: 0.0916
Epoch 00011: val_loss did not improve from 0.03557
22/22 [==============================] - 0s 13ms/step - loss: 0.0894 - val_loss: 0.0463
Epoch 12/100
18/22 [=======================&gt;......] - ETA: 0s - loss: 0.0883
Epoch 00012: val_loss did not improve from 0.03557
22/22 [==============================] - 0s 13ms/step - loss: 0.0877 - val_loss: 0.0389
Epoch 13/100
18/22 [=======================&gt;......] - ETA: 0s - loss: 0.0882
Epoch 00013: val_loss did not improve from 0.03557
22/22 [==============================] - 0s 13ms/step - loss: 0.0873 - val_loss: 0.0451
Epoch 14/100
18/22 [=======================&gt;......] - ETA: 0s - loss: 0.0879
Epoch 00014: val_loss improved from 0.03557 to 0.03552, saving model to results/multivariate_time_series/lstm.h5
22/22 [==============================] - 0s 14ms/step - loss: 0.0867 - val_loss: 0.0355
Epoch 15/100
20/22 [==========================&gt;...] - ETA: 0s - loss: 0.0854
Epoch 00015: val_loss improved from 0.03552 to 0.03534, saving model to results/multivariate_time_series/lstm.h5
22/22 [==============================] - 0s 12ms/step - loss: 0.0837 - val_loss: 0.0353
Epoch 16/100
19/22 [========================&gt;.....] - ETA: 0s - loss: 0.0864
Epoch 00016: val_loss did not improve from 0.03534
22/22 [==============================] - 0s 13ms/step - loss: 0.0841 - val_loss: 0.0412
Epoch 17/100
22/22 [==============================] - ETA: 0s - loss: 0.0837
Epoch 00017: val_loss did not improve from 0.03534
22/22 [==============================] - 0s 14ms/step - loss: 0.0837 - val_loss: 0.0356
Epoch 18/100
20/22 [==========================&gt;...] - ETA: 0s - loss: 0.0859
Epoch 00018: val_loss did not improve from 0.03534
22/22 [==============================] - 0s 15ms/step - loss: 0.0845 - val_loss: 0.0357
Epoch 19/100
20/22 [==========================&gt;...] - ETA: 0s - loss: 0.0845
Epoch 00019: val_loss did not improve from 0.03534
22/22 [==============================] - 0s 14ms/step - loss: 0.0832 - val_loss: 0.0376
Epoch 20/100
20/22 [==========================&gt;...] - ETA: 0s - loss: 0.0837
Epoch 00020: val_loss did not improve from 0.03534
22/22 [==============================] - 0s 13ms/step - loss: 0.0824 - val_loss: 0.0357
Epoch 21/100
18/22 [=======================&gt;......] - ETA: 0s - loss: 0.0839
Epoch 00021: val_loss did not improve from 0.03534
22/22 [==============================] - 0s 14ms/step - loss: 0.0825 - val_loss: 0.0379
Epoch 22/100
21/22 [===========================&gt;..] - ETA: 0s - loss: 0.0827
Epoch 00022: val_loss did not improve from 0.03534
22/22 [==============================] - 0s 14ms/step - loss: 0.0822 - val_loss: 0.0359
Epoch 23/100
22/22 [==============================] - ETA: 0s - loss: 0.0818
Epoch 00023: val_loss did not improve from 0.03534
22/22 [==============================] - 0s 13ms/step - loss: 0.0818 - val_loss: 0.0375
Epoch 24/100
21/22 [===========================&gt;..] - ETA: 0s - loss: 0.0823
Epoch 00024: val_loss did not improve from 0.03534
22/22 [==============================] - 0s 15ms/step - loss: 0.0820 - val_loss: 0.0359
Epoch 25/100
18/22 [=======================&gt;......] - ETA: 0s - loss: 0.0823
Epoch 00025: val_loss did not improve from 0.03534
22/22 [==============================] - 0s 13ms/step - loss: 0.0810 - val_loss: 0.0471</code></pre></div><p>The model is now being trained on the rolling input windows from the training set, with the goal of learning how the two economic series move together from one month to the next. The fit process runs for up to 100 epochs, but it does not simply train blindly that long. Each epoch updates the network weights using the training sequences, then immediately checks performance on the held-out test set, which is being used here as validation data. Because the data are time ordered, shuffling is turned off so the network sees the sequences in their original chronological order rather than in a random mix.</p><p>The lines printed during training show two losses at every epoch: the training loss and the validation loss. The training loss steadily falls from about 0.25 to around 0.08, which means the model is fitting the training patterns better and better. The validation loss is what matters most for deciding whether the model is actually generalizing to newer, unseen months. Whenever that validation score improves, the checkpoint callback saves the current weights to the file in the results folder, so the best-performing version is preserved automatically. That is why several epochs report that the validation loss improved and the model was saved.</p><p>Early stopping is also watching the validation loss in the background. Once the model goes long enough without meaningful improvement, training will eventually stop before reaching all 100 epochs. In the printed output shown here, you can already see the validation loss bouncing around rather than decreasing smoothly, which is a typical sign that the model is nearing the point where extra training is no longer helping. The saved model file in the results directory is therefore the best version encountered during training, not just the final epoch&#8217;s weights.</p><h2>Review the Results</h2><p>Training ends ahead of schedule at 22 epochs, and the resulting test mean absolute error is 1.71. This is lower than the VAR model&#8217;s test mean absolute error of 1.91, although the two results are not fully comparable because the forecasting setups are not the same.</p><p>However, these two results are not directly comparable. The RNN model generates 24 separate one-step-ahead forecasts, while the VAR model feeds its own predicted values back into the model when producing its out-of-sample forecast. If you want a fairer comparison, you may need to adjust the VAR specification so that both models are evaluated under the same forecasting setup and then compare their performance:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;754c8bab-668c-4322-bb1b-0c5d0365e084&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">pd.DataFrame(result.history).plot();</code></pre></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Quje!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F72929319-9f4b-4a19-afdc-7668fd792b7c_382x248.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Quje!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F72929319-9f4b-4a19-afdc-7668fd792b7c_382x248.png 424w, https://substackcdn.com/image/fetch/$s_!Quje!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F72929319-9f4b-4a19-afdc-7668fd792b7c_382x248.png 848w, https://substackcdn.com/image/fetch/$s_!Quje!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F72929319-9f4b-4a19-afdc-7668fd792b7c_382x248.png 1272w, https://substackcdn.com/image/fetch/$s_!Quje!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F72929319-9f4b-4a19-afdc-7668fd792b7c_382x248.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Quje!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F72929319-9f4b-4a19-afdc-7668fd792b7c_382x248.png" width="382" height="248" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/72929319-9f4b-4a19-afdc-7668fd792b7c_382x248.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:248,&quot;width&quot;:382,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:11720,&quot;alt&quot;:&quot;Output image&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/202848543?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F72929319-9f4b-4a19-afdc-7668fd792b7c_382x248.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Output image" title="Output image" srcset="https://substackcdn.com/image/fetch/$s_!Quje!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F72929319-9f4b-4a19-afdc-7668fd792b7c_382x248.png 424w, https://substackcdn.com/image/fetch/$s_!Quje!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F72929319-9f4b-4a19-afdc-7668fd792b7c_382x248.png 848w, https://substackcdn.com/image/fetch/$s_!Quje!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F72929319-9f4b-4a19-afdc-7668fd792b7c_382x248.png 1272w, https://substackcdn.com/image/fetch/$s_!Quje!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F72929319-9f4b-4a19-afdc-7668fd792b7c_382x248.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The goal here is to turn the recorded training history into a quick visual summary so it is easier to see how the model behaved over the epochs. The history object collected during fitting contains the values of the training loss and the validation loss at the end of each epoch, and wrapping that data in a dataframe makes those two series easy to plot together. When the dataframe is plotted, the result is the small line chart shown in the output, with one curve for loss and one for val_loss.</p><p>The shape of the figure reflects exactly what was tracked during training. The training loss starts high and drops sharply in the first few epochs, which is what you expect when the model is quickly learning the broad patterns in the data. After that, the line flattens out, showing that improvements become smaller and the model is converging. The validation loss stays much lower and wiggles slightly from epoch to epoch instead of falling smoothly. That kind of behavior is common when the validation set is small or when the model is being tuned on a time series that is not especially easy to predict. The plot gives a fast visual check on whether training is still helping, whether the model is overfitting, and whether early stopping likely made sense.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;740d111f-dcf2-4c6d-b836-f346a71e277e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">y_pred = pd.DataFrame(rnn.predict(X_test), 
                      columns=y_test.columns, 
                      index=y_test.index)
y_pred.info()</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;d9dda138-6b5d-4757-b233-38b5ec669311&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">&lt;class 'pandas.core.frame.DataFrame'&gt;
DatetimeIndex: 24 entries, 2018-01-01 to 2019-12-01
Data columns (total 2 columns):
 #   Column     Non-Null Count  Dtype  
---  ------     --------------  -----  
 0   ip         24 non-null     float32
 1   sentiment  24 non-null     float32
dtypes: float32(2)
memory usage: 384.0 bytes</code></pre></div><p>The goal here is to turn the model&#8217;s raw numerical predictions into a pandas DataFrame that lines up exactly with the real test targets. The LSTM produces predictions for the held-out test window, and those predictions are wrapped in a table using the same column names and date index as the actual test set. That way, the forecasted values for industrial production and sentiment can be compared directly to the observed values later on without any alignment issues.</p><p>Behind the scenes, the model returns a two-column array of predicted values, one for each output series. By assigning y<em>test.columns, the result keeps the labels ip and sentiment instead of leaving the columns anonymous. Using y</em>test.index preserves the monthly dates from the test period, so the predictions are tied to the same time stamps as the true observations, running from 2018-01-01 through 2019-12-01. That makes the predictions much easier to inspect, plot, and score against the actual data.</p><p>The saved output confirms that the transformation worked as intended. The printed summary shows a DataFrame with 24 rows, matching the 24 test months, and two float32 columns with no missing values. The float32 type is typical of TensorFlow/Keras outputs and reflects the model&#8217;s numeric predictions. The small memory footprint also makes sense because this is just a compact set of forecast values for a short test period.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;a4fcabbc-66b3-4f6e-b100-5ce362ce84e1&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">test_mae = mean_absolute_error(y_pred, y_test)</code></pre></div><p>The cell computes a single evaluation number that summarizes how far the model&#8217;s forecasts are from the true test values. It uses mean absolute error, which measures the average size of the prediction mistakes without worrying about whether the model was too high or too low. Because the targets and predictions were previously arranged to match the same test months, the function can compare them directly and produce one score for the two output series together. A smaller value means the forecasts are closer to the actual data, so this result is a quick way to judge how well the LSTM performed on unseen time periods.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;bf2f6ef0-61be-4065-97b4-fb5138d2682d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">print(test_mae)</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;699f7ac3-0af0-4a1e-be87-21f0c4b6eecc&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">0.03533523602534612</code></pre></div><p>The purpose here is simply to report the model&#8217;s final test error as a single number. By the time this line runs, the predictions on the held-out test set have already been generated and compared against the true target values, so the variable holding the mean absolute error is ready to be displayed. Printing it sends that value straight to the output area, which is why the cell produces one plain number rather than a table or plot.</p><p>The value shown, 0.03533523602534612, is the average absolute difference between the model&#8217;s predicted values and the actual test values after the data has been transformed and scaled. Because the series were normalized earlier in the workflow, this error is expressed on that scaled 0-to-1 range rather than in the original economic units. A small number like this indicates that, on average, the predictions are fairly close to the true observations in the test period.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;1340136f-aa2e-4a81-9988-c62f8b66764b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">y_test.index</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;8f9f478f-79f5-4384-90d3-cfcda962a1a9&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">DatetimeIndex(['2018-01-01', '2018-02-01', '2018-03-01', '2018-04-01',
               '2018-05-01', '2018-06-01', '2018-07-01', '2018-08-01',
               '2018-09-01', '2018-10-01', '2018-11-01', '2018-12-01',
               '2019-01-01', '2019-02-01', '2019-03-01', '2019-04-01',
               '2019-05-01', '2019-06-01', '2019-07-01', '2019-08-01',
               '2019-09-01', '2019-10-01', '2019-11-01', '2019-12-01'],
              dtype='datetime64[ns]', name='DATE', freq=None)</code></pre></div><p>The purpose here is simply to inspect the time index attached to the test targets, so you can confirm exactly which months the model is being evaluated on. The result shows a DatetimeIndex running from January 2018 through December 2019, which means the final 24 monthly observations were held out as the test period. That matches the earlier train-test split and makes it clear that the forecast evaluation is happening on the most recent part of the series, not on shuffled or randomly selected points.</p><p>Seeing the dates laid out like this is helpful because time series models depend on order. The index preserves the calendar structure of the data, so each predicted value can be aligned back to the correct month. That is why the output is just the sequence of dates rather than raw numbers: it confirms the exact evaluation window and provides the timeline used when the forecast plots and error calculations are built later.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;b338d5f3-fea0-41d0-bbb1-12be8bd2c90a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">fig, axes = plt.subplots(ncols=3, figsize=(17, 4))
pd.DataFrame(result.history).rename(columns={'loss': 'Training',
                                              'val_loss': 'Validation'}).plot(ax=axes[0], title='Train &amp; Validation Error')
axes[0].set_xlabel('Epoch')
axes[0].set_ylabel('MAE')
col_dict = {'ip': 'Industrial Production', 'sentiment': 'Sentiment'}

for i, col in enumerate(y_test.columns, 1):
    y_train.loc['2010':, col].plot(ax=axes[i], label='training', title=col_dict[col])
    y_test[col].plot(ax=axes[i], label='out-of-sample')
    y_pred[col].plot(ax=axes[i], label='prediction')
    axes[i].set_xlabel('')

axes[1].set_ylim(.5, .9)
axes[1].fill_between(x=y_test.index, y1=0.5, y2=0.9, color='grey', alpha=.5)

axes[2].set_ylim(.3, .9)
axes[2].fill_between(x=y_test.index, y1=0.3, y2=0.9, color='grey', alpha=.5)

plt.legend()
fig.suptitle('Multivariate RNN - Results | Test MAE = {:.4f}'.format(test_mae), fontsize=14)
sns.despine()
fig.tight_layout()
fig.subplots_adjust(top=.85)
fig.savefig(results_path / 'multivariate_results', dpi=300);</code></pre></div><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!P7ki!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2bb604ac-5ac7-40da-bd13-25a67d68f8a6_1216x285.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!P7ki!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2bb604ac-5ac7-40da-bd13-25a67d68f8a6_1216x285.png 424w, https://substackcdn.com/image/fetch/$s_!P7ki!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2bb604ac-5ac7-40da-bd13-25a67d68f8a6_1216x285.png 848w, https://substackcdn.com/image/fetch/$s_!P7ki!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2bb604ac-5ac7-40da-bd13-25a67d68f8a6_1216x285.png 1272w, https://substackcdn.com/image/fetch/$s_!P7ki!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2bb604ac-5ac7-40da-bd13-25a67d68f8a6_1216x285.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!P7ki!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2bb604ac-5ac7-40da-bd13-25a67d68f8a6_1216x285.png" width="1216" height="285" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2bb604ac-5ac7-40da-bd13-25a67d68f8a6_1216x285.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:285,&quot;width&quot;:1216,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:70683,&quot;alt&quot;:&quot;Output image&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/202848543?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2bb604ac-5ac7-40da-bd13-25a67d68f8a6_1216x285.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Output image" title="Output image" srcset="https://substackcdn.com/image/fetch/$s_!P7ki!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2bb604ac-5ac7-40da-bd13-25a67d68f8a6_1216x285.png 424w, https://substackcdn.com/image/fetch/$s_!P7ki!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2bb604ac-5ac7-40da-bd13-25a67d68f8a6_1216x285.png 848w, https://substackcdn.com/image/fetch/$s_!P7ki!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2bb604ac-5ac7-40da-bd13-25a67d68f8a6_1216x285.png 1272w, https://substackcdn.com/image/fetch/$s_!P7ki!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2bb604ac-5ac7-40da-bd13-25a67d68f8a6_1216x285.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>The aim here is to pull the training story and the forecast results into one final figure and save it for later inspection. The cell starts by creating a row of three plots, which gives enough room to show the model&#8217;s learning curve alongside the two target series. It then takes the recorded training history, turns it into a small table, renames the loss columns so they read more clearly as training and validation error, and plots those values on the first panel. Because the loss being tracked is mean absolute error, the y-axis is labeled accordingly, and the x-axis is the epoch number, so you can see how performance changed as training progressed.</p><p>After that, the cell prepares a small name mapping so the abbreviated series names can be shown in a friendlier form. It then loops through the two target variables and, for each one, overlays three lines on its own panel: the later part of the training data, the held-out test observations, and the model&#8217;s predictions. The training series is only shown from 2010 onward so the plot focuses on the most relevant recent history rather than the full dataset. The test period and predictions line up on the same dates, which makes it easy to judge how well the model followed the actual movement of each series.</p><p>The gray shaded regions on the last two panels mark the forecast window, making it obvious which part of the timeline belongs to the out-of-sample evaluation. The y-limits are also narrowed on each of those panels so the eye stays focused on the portion of the scale where the series actually move during the test period. That is why the plot looks compact and centered on the recent forecast range instead of showing the full vertical span of the data. The legend is added so the three lines can be distinguished, and the title is updated to include the final test MAE, which appears in the saved figure as 0.0353. That number reflects the error computed earlier from the model&#8217;s predictions, and it is displayed here so the visual summary and the quantitative score reinforce each other.</p><p>Finally, the notebook tidies the figure layout, pulls the top margin down a bit so the title fits cleanly, and saves the finished image to the results folder. The saved output shown below is exactly that figure: training error falling quickly and then flattening out, validation error staying fairly low and stable, and the two forecast panels showing the model tracking the held-out industrial production and sentiment series reasonably closely over the shaded test period.</p><div><hr></div><h2>Notebook 6 of 8: <code>05_sentiment_analysis_imdb</code></h2><p><em>Source file: `05_sentiment_analysis_imdb_processed.ipynb`</em></p><div><hr></div><h1>Sentiment Classification with Word Embeddings and a Recurrent Network</h1><p>RNNs are widely used for many natural language processing problems. In part three of this book, we already worked through an example of sentiment analysis with text.</p><p>Here, we will show how an RNN can be used with text to identify whether a review is positive or negative, and this setup could later be adapted to a more detailed sentiment range. To represent the words in each document, we will rely on word embeddings. As discussed in Chapter 15, Word Embeddings, these embeddings are a strong way to map text into a dense vector form, where distances between words in the learned space reflect semantic meaning drawn from how those words appear in context.</p><p>In the earlier RNN example, we also saw that Keras includes an embedding layer that can learn vectors tailored to the specific task. Another option is to start from pretrained vectors instead.</p><h2>Imports and configuration</h2><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;27f54f8b-7af5-498e-998b-8d325e1e235b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">import warnings
warnings.filterwarnings('ignore')</code></pre></div><p>The purpose here is to quiet down non-essential warning messages so the rest of the notebook runs more cleanly. First, the warnings module is imported, which gives access to Python&#8217;s built-in warning system. Then the warning filter is set to ignore, telling Python not to display warning messages that would normally appear during execution. Behind the scenes, this does not fix or remove the situations that trigger warnings; it simply hides them from view. Since there is no saved output, the effect of the cell is not something visible on the page. Instead, it prepares the environment so later steps can run without being interrupted by repeated or noisy warning messages.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;bd52f883-2996-48e8-a043-bff1f7cacb1f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">%matplotlib inline

from pathlib import Path

import numpy as np
import pandas as pd
from sklearn.metrics import roc_auc_score

import tensorflow as tf
from tensorflow.keras.callbacks import ModelCheckpoint, EarlyStopping
from tensorflow.keras.datasets import imdb
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, GRU, Embedding
from tensorflow.keras.preprocessing.sequence import pad_sequences
import tensorflow.keras.backend as K

import matplotlib.pyplot as plt
import seaborn as sns</code></pre></div><p>The purpose of this cell is to bring together all the tools needed for the sentiment analysis workflow and make the notebook ready for plotting, data handling, model building, and evaluation. The first line turns on inline plotting so that any charts created later appear directly inside the notebook instead of in a separate window. After that, the standard library utility for working with file paths is imported, which will make it easier to create and manage output folders and saved files in a platform-independent way.</p><p>Next come the numerical and data-handling libraries. NumPy is imported for array-based computation, and pandas is brought in for working with tabular results such as training histories or evaluation summaries. The roc<em>auc</em>score function is imported from scikit-learn so the model can later be judged using ROC AUC, a useful metric for binary classification because it focuses on how well the model separates positive from negative reviews across different thresholds.</p><p>The TensorFlow imports prepare the deep learning side of the notebook. The callbacks for model checkpointing and early stopping will later help save the best-performing model and stop training when validation performance stops improving. The IMDB dataset loader is imported so the built-in movie review dataset can be fetched easily. Sequential provides the simple model container used to stack layers in order, while Dense, GRU, and Embedding are the actual neural network layers that will form the sentiment classifier. Pad_sequences is imported because the reviews come in different lengths and must be converted into fixed-size arrays before they can be passed into the network. The Keras backend module is also imported, usually so the session can be cleared before building a new model and avoid leftover state from earlier runs.</p><p>Finally, Matplotlib and Seaborn are imported for visualization. Matplotlib will be used for the actual plotting, and Seaborn will make the plots look cleaner and more readable with nicer default styling. There is no printed output from this cell because it only sets up the environment and loads the libraries needed for later steps; its effect is to prepare everything so the following cells can load data, build the model, train it, and display results smoothly.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;b6fa89ff-b34c-4b1f-a97b-9a989934d97e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">gpu_devices = tf.config.experimental.list_physical_devices('GPU')
if gpu_devices:
    print('Using GPU')
    tf.config.experimental.set_memory_growth(gpu_devices[0], True)
else:
    print('Using CPU')</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;2b356e96-b5ad-4189-9d70-5ad03e2b55e5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">Using CPU</code></pre></div><p>The purpose here is to check what kind of hardware TensorFlow can use before the model starts training. It asks TensorFlow for a list of available GPU devices, and then looks at whether that list is empty or not. If at least one GPU is found, it announces that a GPU is being used and turns on memory growth for the first GPU, which tells TensorFlow to allocate GPU memory gradually instead of grabbing most of it all at once. That can help avoid unnecessary memory reservation and make coexistence with other processes smoother. In this run, no GPU was detected, so the fallback path was taken and the message printed was &#8220;Using CPU.&#8221; That output simply confirms that computation will rely on the processor rather than a graphics card for the rest of the notebook.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;3be69349-c335-41cb-b989-d27280823e7a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">sns.set_style('whitegrid')
np.random.seed(42)</code></pre></div><p>The cell is setting up a cleaner, more consistent environment for any plots and making the random behavior repeatable. Applying the whitegrid style tells Seaborn to draw charts with a light background grid, which usually makes trends and comparisons easier to read. Right after that, the random seed is fixed at 42 for NumPy, so any later operation that relies on NumPy&#8217;s random number generator will produce the same results each time the notebook is run. That matters because it helps keep the experiment reproducible, especially when data splits, weight initialization, or other randomized steps are involved. Since this cell only changes settings and does not create a figure or print anything, there is no visible output when it runs.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;5be14dcc-309e-4383-9ef0-b1e187d6ebc7&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">results_path = Path('results', 'sentiment_imdb')
if not results_path.exists():
    results_path.mkdir(parents=True)</code></pre></div><p>The purpose here is to make sure there is a folder ready to hold the model&#8217;s results. A path object is created for a directory named results/sentiment_imdb, which gives the notebook a clean, organized place to save things like plots or model files later on. After that, the notebook checks whether that folder already exists. If it does not, it creates the directory and any missing parent folders along the way. Since nothing is printed or displayed, there is no saved output, and that makes sense because the cell is only setting up the file system behind the scenes rather than producing a visible result.</p><h2>Load the Review Data</h2><p>To keep the example practical, we will work with the IMDB reviews dataset. It includes 50,000 movie reviews labeled as positive or negative, split evenly between training and test sets, and the labels are balanced within each split. In total, the full vocabulary contains 88,586 tokens.</p><p>Keras includes this dataset directly, and it can be loaded so that every review is stored as a sequence of integers. When loading the data, we can restrict the vocabulary to <code>num_words</code>, remove very common and likely less useful words with <code>skip_top</code>, and discard reviews longer than <code>maxlen</code>. We can also set <code>oov_char</code>, which is the value used for tokens that are left out of the vocabulary because they are too infrequent, as shown below:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;e1fbeacb-904c-49e3-bcb9-52ceedafd6ab&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">vocab_size = 20000</code></pre></div><p>This line sets the size of the vocabulary the model will work with, limiting it to the 20,000 most frequent words in the dataset. Behind the scenes, that means any word outside this range will be treated as unknown rather than given its own separate index, which keeps the input space smaller and the model easier to train. Using a fixed vocabulary size also makes later steps, like embedding lookup and sequence preparation, consistent because the model knows the maximum number of distinct word IDs it may encounter.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;7aca7463-adee-44a3-8372-3ea6d0efff50&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">(X_train, y_train), (X_test, y_test) = imdb.load_data(seed=42, 
                                                      skip_top=0,
                                                      maxlen=None, 
                                                      oov_char=2, 
                                                      index_from=3,
                                                      num_words=vocab_size)</code></pre></div><p>The purpose here is to load the IMDB movie review dataset into memory in a form that can be used for sentiment classification. The dataset comes already tokenized, so instead of raw text you get reviews represented as sequences of integer word indices, along with labels indicating whether each review is positive or negative. The assignment splits the loaded data into training and test parts, giving separate sets for model fitting and final evaluation.</p><p>Several arguments shape what gets loaded. The random seed is set to 42 so that the train-test split is reproducible, meaning the same examples will be chosen each time the notebook runs. The vocabulary is limited by num<em>words to the most frequent words defined earlier, which keeps the dataset manageable and trims away rarer terms. The index</em>from setting shifts the word indices so special tokens can occupy the lower numbers, and oov_char provides a placeholder for words that fall outside the allowed vocabulary. Because maxlen is left as None here, the reviews are not truncated yet; they remain variable-length sequences for now and are only standardized later when padding is applied.</p><p>Since no output is saved for this cell, nothing is displayed on the page when it runs. Its role is simply to prepare the raw training and test arrays that the next steps will reshape and feed into the neural network.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;b7115c34-b4a2-4cb2-8a2e-aa26262470f3&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">ax = sns.displot([len(review) for review in X_train])
ax.set(xscale='log');</code></pre></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!qKbO!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F90dd9472-7cbb-4bae-8e73-32ce58011f21_352x356.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!qKbO!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F90dd9472-7cbb-4bae-8e73-32ce58011f21_352x356.png 424w, https://substackcdn.com/image/fetch/$s_!qKbO!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F90dd9472-7cbb-4bae-8e73-32ce58011f21_352x356.png 848w, https://substackcdn.com/image/fetch/$s_!qKbO!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F90dd9472-7cbb-4bae-8e73-32ce58011f21_352x356.png 1272w, https://substackcdn.com/image/fetch/$s_!qKbO!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F90dd9472-7cbb-4bae-8e73-32ce58011f21_352x356.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!qKbO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F90dd9472-7cbb-4bae-8e73-32ce58011f21_352x356.png" width="352" height="356" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/90dd9472-7cbb-4bae-8e73-32ce58011f21_352x356.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:356,&quot;width&quot;:352,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:8319,&quot;alt&quot;:&quot;Output image&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/202848543?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F90dd9472-7cbb-4bae-8e73-32ce58011f21_352x356.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Output image" title="Output image" srcset="https://substackcdn.com/image/fetch/$s_!qKbO!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F90dd9472-7cbb-4bae-8e73-32ce58011f21_352x356.png 424w, https://substackcdn.com/image/fetch/$s_!qKbO!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F90dd9472-7cbb-4bae-8e73-32ce58011f21_352x356.png 848w, https://substackcdn.com/image/fetch/$s_!qKbO!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F90dd9472-7cbb-4bae-8e73-32ce58011f21_352x356.png 1272w, https://substackcdn.com/image/fetch/$s_!qKbO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F90dd9472-7cbb-4bae-8e73-32ce58011f21_352x356.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The goal here is to look at how long the movie reviews are before they are padded or truncated. The expression inside the plot first measures the length of every review in the training set, so each review is reduced to a single number representing how many tokens it contains. Those lengths are then passed to Seaborn&#8217;s distribution plot, which turns them into a histogram showing how frequently reviews of different sizes appear.</p><p>After the plot is created, the x-axis is switched to a logarithmic scale. That matters because review lengths vary a lot: there are many short and medium reviews, but also a smaller number of very long ones. On a normal linear axis, the shorter reviews would be compressed together and the overall shape would be harder to read. Using a log scale spreads the values out more evenly and makes the full range of lengths easier to see.</p><p>The saved output is the histogram that results from this calculation. Most reviews cluster around the low hundreds of tokens, which is why the tallest bars appear near that region. The long tail stretching to the right shows that there are also some much longer reviews, but they occur less often. The reason the plot looks the way it does is that the IMDB dataset contains many moderate-length reviews and comparatively fewer extremely short or extremely long ones, so the distribution naturally peaks around the typical review length and tapers off on both sides.</p><h2>Prepare the data</h2><p>In the next stage, turn the integer lists into arrays with a uniform size so they can be stacked together and passed into the RNN. The <code>pad_sequence</code> function creates same-length arrays by trimming and padding them to match <code>maxlen</code>, as shown below:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;9fad2e91-4335-4e3e-9958-e90cdbc5eb73&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">maxlen = 100</code></pre></div><p>This line sets the maximum sequence length that the model will work with to 100 tokens. Since the IMDB reviews in the dataset are naturally different lengths, this value acts as the standard size used later when the reviews are padded or truncated. Reviews longer than 100 words will be cut down, while shorter ones will be extended with empty positions so that every input has the same length. That fixed size is important because neural networks expect inputs with a consistent shape, and the recurrent model built later can then process each review in a uniform way. No output appears here because the cell only assigns a value for use in later steps rather than producing a visible result.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;e18bf4c6-9063-45a1-ba08-03d45aa751ce&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">X_train_padded = pad_sequences(X_train, 
                        truncating='pre', 
                        padding='pre', 
                        maxlen=maxlen)

X_test_padded = pad_sequences(X_test, 
                       truncating='pre', 
                       padding='pre', 
                       maxlen=maxlen)</code></pre></div><p>The purpose here is to turn the IMDB review sequences into a uniform shape that a neural network can process efficiently. The raw reviews are lists of word indices, but each review can be a different length, and models like an embedding layer followed by a recurrent network work best when every input example has the same number of time steps. Padding solves that by adding zeros to shorter reviews and trimming longer ones down to a fixed length.</p><p>First, the training reviews are passed through the padding utility to create a new array with exactly the same length for every review. The same transformation is then applied to the test reviews so that both datasets are prepared in exactly the same way. The settings make the adjustment happen at the beginning of each sequence rather than the end, which means shorter reviews are left-padded with zeros and longer reviews lose tokens from the front. That choice is often used when the most recent words in a review are considered more informative, or simply to keep the most recent part of the sequence aligned at the end.</p><p>The maximum length is controlled by the maxlen value defined earlier, so every review is either shortened or extended to match that limit. Behind the scenes, the result is that the model will receive a rectangular input array instead of a jagged collection of lists, which is required for batch training. There is no visible output from the cell because it is a data-preparation step only; it just creates the padded training and test arrays that the model will use in the next stages.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;2ae3c52e-ca8c-46d9-ab03-ab7ccedaa7e6&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">X_train_padded.shape, X_test_padded.shape</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;4c2a6089-5fd9-4667-a01c-913ef071c44b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">((25000, 100), (25000, 100))</code></pre></div><p>This cell is simply checking the shapes of the padded training and test data after the earlier preprocessing step. Since the review texts were converted from variable-length sequences into fixed-length sequences, the shape tells you both how many examples are in each split and how long each sequence now is. The output shows 25,000 training reviews and 25,000 test reviews, and each one has been padded or truncated to a length of 100 tokens. That is why the result appears as two two-dimensional arrays with the same second dimension: the model needs a uniform input size, and this confirms that both datasets are ready to be fed into the neural network.</p><h2>Build the Model Structure</h2><p>Now we can build the RNN structure. The opening layer is responsible for learning the word embeddings. As before, we specify the size of the embedding space with <code>output_dim</code>, tell the layer how many distinct tokens it must represent with <code>input_dim</code>, and indicate the expected length of each sequence through the input length setting.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;d9f62337-da2d-4b5b-82ff-6a14c49b4e8e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">K.clear_session()</code></pre></div><p>This line resets the current Keras backend state before building or training another model. Clearing the session removes any existing computation graphs, layers, variables, and other leftover objects from previous model runs, which helps avoid clutter in memory and prevents older models from interfering with the new one. It is especially useful in notebooks where cells may be executed multiple times, since repeated model creation can otherwise accumulate resources and sometimes lead to confusing behavior. Nothing is displayed as output because the operation simply performs cleanup behind the scenes and returns no visible result.</p><h3>Training Objective and Evaluation Metric</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;06ec8290-45b7-49ae-a6cd-ea6fee424662&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">embedding_size = 100</code></pre></div><p>This line sets the size of the embedding vectors to 100. In the model, each word will be represented by a dense numerical vector with 100 values, which gives the network a compact way to learn relationships between words during training. A smaller size would make the representation less expressive, while a much larger one would add more parameters and make training heavier. Since this cell only assigns a value, it does not produce any visible output; it simply prepares a setting that will be used later when the embedding layer is built.</p><p>This time, the model uses GRUs, which tend to train more quickly and often work better when the dataset is relatively small. Regularization is handled with dropout, as shown below:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;d73fab1d-baad-46cc-9225-8d2cb370cf6b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">rnn = Sequential([
    Embedding(input_dim=vocab_size, 
              output_dim= embedding_size, 
              input_length=maxlen),
    GRU(units=32,  
        dropout=0.2, # comment out to use optimized GPU implementation
        recurrent_dropout=0.2),
    Dense(1, activation='sigmoid')
])
rnn.summary()</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;e54a0830-4167-4101-ac2b-9a4ade3677a4&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">WARNING:tensorflow:Layer gru will not use cuDNN kernel since it doesn't meet the cuDNN kernel criteria. It will use generic GPU kernel as fallback when running on GPU
Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
embedding (Embedding)        (None, 100, 100)          2000000   
_________________________________________________________________
gru (GRU)                    (None, 32)                12864     
_________________________________________________________________
dense (Dense)                (None, 1)                 33        
=================================================================
Total params: 2,012,897
Trainable params: 2,012,897
Non-trainable params: 0
_________________________________________________________________</code></pre></div><p>The purpose of this cell is to assemble the neural network that will read the padded movie reviews and turn them into sentiment predictions. It builds the model layer by layer using a simple sequential stack, which means the output of each layer becomes the input to the next one. The first layer is an embedding layer, whose job is to convert word indices into dense vectors of learned numbers. Because the vocabulary is limited to the most frequent words and each review has been padded to a fixed length of 100 tokens, the embedding layer produces a sequence of 100 vectors, each with 100 learned features. That is why the summary shows an output shape of 100 by 100 for that layer, and why the parameter count is large: the model is learning a separate vector representation for each word in the vocabulary.</p><p>The next layer is a GRU, which is a recurrent layer designed to scan through the sequence and capture patterns that depend on word order. Instead of keeping the full 100-step sequence, it compresses the information into a single 32-value representation that summarizes the review. The dropout settings tell the layer to randomly ignore some connections during training, which helps reduce overfitting, although it also means the faster cuDNN version cannot be used here. That is exactly what the warning in the output is saying: the GRU does not meet the conditions for the optimized GPU kernel, so TensorFlow falls back to a more general implementation. After that comes the final dense layer with one neuron and a sigmoid activation. Its job is to take the GRU&#8217;s summary vector and convert it into a number between 0 and 1, which can be interpreted as the probability that the review is positive.</p><p>The model summary printed at the end confirms the architecture and shows how many parameters are being learned at each stage. Most of the parameters live in the embedding layer, since it stores a vector for each word in the vocabulary. The GRU adds a smaller number of parameters to learn how sequences behave over time, and the final dense layer adds just a few more to make the binary decision. Since all of these weights are trainable, the total and trainable parameter counts are the same, and there are no frozen parts in the network.</p><p>The finished model contains more than 2 million trainable parameters.</p><p>We configure the model to track the custom AUC metric described earlier and fit it with early stopping enabled:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;913d1392-3889-4cde-97f2-801cbe3d7b67&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">rnn.compile(loss='binary_crossentropy', 
            optimizer='RMSProp', 
            metrics=['accuracy', 
                     tf.keras.metrics.AUC(name='AUC')])</code></pre></div><p>The model is being prepared for training by setting the rules it will use to learn from the sentiment data. The loss function is binary cross-entropy, which is the standard choice when the task is to decide between two classes, such as positive or negative review. Behind the scenes, this tells the network to compare its predicted probability with the true label and measure how far off it is in a way that works well for probabilities.</p><p>The optimizer is RMSProp, which controls how the model adjusts its weights after each batch of examples. It helps the network move through the parameter space efficiently by adapting the learning steps based on recent gradient history, which is especially useful for recurrent models like this one.</p><p>The metrics list adds two ways to monitor performance during training and evaluation. Accuracy shows the fraction of reviews classified correctly, while the AUC metric measures how well the model separates positive from negative examples across different decision thresholds. Because AUC looks at ranking quality rather than just a single cutoff, it gives a more informative picture for a probability-based binary classifier.</p><p>There is no saved output for this cell because compiling a model does not train it or produce predictions. Instead, it simply configures the network so that the next training step knows which objective to minimize and which performance measures to report.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;57879fcc-2c49-44d9-97b6-00e1b4fcfbcd&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">rnn_path = (results_path / 'lstm.h5').as_posix()

checkpointer = ModelCheckpoint(filepath=rnn_path,
                               verbose=1,
                               monitor='val_AUC',
                               mode='max',
                               save_best_only=True)</code></pre></div><p>The cell sets up where the model checkpoints will be saved during training. It first builds a file path inside the results folder and gives the saved model file a name ending in .h5, which is a common format for storing Keras models. After that, it creates a ModelCheckpoint callback, which is a training helper that watches a chosen metric and writes the model to disk whenever performance improves.</p><p>Here the callback is configured to look at validation AUC, so it is not saving models after every epoch blindly. Instead, it compares the current epoch&#8217;s validation AUC with the best one seen so far, and only keeps the model if that score is higher. The mode is set to max because AUC is better when it increases. The verbose setting means training will print a message when a new best model is saved, and save<em>best</em>only makes sure only the strongest version is preserved. Since this cell only defines the path and the checkpointing rule, there is no visible output yet; the effect will show up later when training runs and the best model gets written to that file.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;976e0bd3-11db-4716-adb8-5a56923fcd91&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">early_stopping = EarlyStopping(monitor='val_AUC', 
                               mode='max',
                              patience=5,
                              restore_best_weights=True)</code></pre></div><p>This sets up an early stopping rule for model training, using the validation AUC as the signal to watch. As training runs, Keras will keep checking whether the model is still improving on the validation data, and because the goal metric is AUC, higher values are better, so the monitoring mode is set to maximize it. The patience value of 5 means training is allowed to continue for five epochs after the last improvement, giving the model a little time in case performance briefly levels off before rising again. If no better validation AUC appears during that window, training stops automatically. The restore<em>best</em>weights option ensures that once training ends, the model reverts to the weights from the epoch that achieved the best validation AUC, rather than keeping the possibly worse weights from the final epoch. That makes this callback especially useful for preventing overfitting while preserving the strongest version of the model seen during training.</p><p>Training ends after eight epochs, and we restore the best model weights, which gives a strong test AUC score of 0.9346:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;bb93c9ef-390b-4e2f-b1a9-48193668b422&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">training = rnn.fit(X_train_padded,
                   y_train,
                   batch_size=32,
                   epochs=100,
                   validation_data=(X_test_padded, y_test),
                   callbacks=[early_stopping, checkpointer],
                   verbose=1)</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;e91c18ee-0865-42ae-9ac9-73d924d435f5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">Epoch 1/100
782/782 [==============================] - ETA: 0s - loss: 0.4345 - accuracy: 0.7942 - AUC: 0.8801
Epoch 00001: val_AUC improved from -inf to 0.93268, saving model to results/sentiment_imdb/lstm.h5
782/782 [==============================] - 117s 150ms/step - loss: 0.4345 - accuracy: 0.7942 - AUC: 0.8801 - val_loss: 0.3455 - val_accuracy: 0.8501 - val_AUC: 0.9327
Epoch 2/100
782/782 [==============================] - ETA: 0s - loss: 0.2887 - accuracy: 0.8803 - AUC: 0.9492
Epoch 00002: val_AUC improved from 0.93268 to 0.93450, saving model to results/sentiment_imdb/lstm.h5
782/782 [==============================] - 116s 148ms/step - loss: 0.2887 - accuracy: 0.8803 - AUC: 0.9492 - val_loss: 0.3317 - val_accuracy: 0.8568 - val_AUC: 0.9345
Epoch 3/100
782/782 [==============================] - ETA: 0s - loss: 0.2442 - accuracy: 0.9023 - AUC: 0.9634
Epoch 00003: val_AUC did not improve from 0.93450
782/782 [==============================] - 117s 149ms/step - loss: 0.2442 - accuracy: 0.9023 - AUC: 0.9634 - val_loss: 0.4815 - val_accuracy: 0.8212 - val_AUC: 0.9343
Epoch 4/100
782/782 [==============================] - ETA: 0s - loss: 0.2143 - accuracy: 0.9160 - AUC: 0.9716
Epoch 00004: val_AUC improved from 0.93450 to 0.94048, saving model to results/sentiment_imdb/lstm.h5
782/782 [==============================] - 116s 148ms/step - loss: 0.2143 - accuracy: 0.9160 - AUC: 0.9716 - val_loss: 0.3312 - val_accuracy: 0.8645 - val_AUC: 0.9405
Epoch 5/100
782/782 [==============================] - ETA: 0s - loss: 0.1901 - accuracy: 0.9269 - AUC: 0.9774
Epoch 00005: val_AUC improved from 0.94048 to 0.94152, saving model to results/sentiment_imdb/lstm.h5
782/782 [==============================] - 116s 148ms/step - loss: 0.1901 - accuracy: 0.9269 - AUC: 0.9774 - val_loss: 0.3367 - val_accuracy: 0.8658 - val_AUC: 0.9415
Epoch 6/100
782/782 [==============================] - ETA: 0s - loss: 0.1693 - accuracy: 0.9361 - AUC: 0.9819
Epoch 00006: val_AUC did not improve from 0.94152
782/782 [==============================] - 116s 148ms/step - loss: 0.1693 - accuracy: 0.9361 - AUC: 0.9819 - val_loss: 0.3186 - val_accuracy: 0.8632 - val_AUC: 0.9399
Epoch 7/100
782/782 [==============================] - ETA: 0s - loss: 0.1519 - accuracy: 0.9426 - AUC: 0.9851
Epoch 00007: val_AUC did not improve from 0.94152
782/782 [==============================] - 115s 147ms/step - loss: 0.1519 - accuracy: 0.9426 - AUC: 0.9851 - val_loss: 0.5009 - val_accuracy: 0.8056 - val_AUC: 0.9354
Epoch 8/100
782/782 [==============================] - ETA: 0s - loss: 0.1364 - accuracy: 0.9505 - AUC: 0.9878
Epoch 00008: val_AUC did not improve from 0.94152
782/782 [==============================] - 117s 150ms/step - loss: 0.1364 - accuracy: 0.9505 - AUC: 0.9878 - val_loss: 0.3860 - val_accuracy: 0.8547 - val_AUC: 0.9337
Epoch 9/100
782/782 [==============================] - ETA: 0s - loss: 0.1206 - accuracy: 0.9564 - AUC: 0.9902
Epoch 00009: val_AUC did not improve from 0.94152
782/782 [==============================] - 119s 152ms/step - loss: 0.1206 - accuracy: 0.9564 - AUC: 0.9902 - val_loss: 0.3833 - val_accuracy: 0.8562 - val_AUC: 0.9343
Epoch 10/100
782/782 [==============================] - ETA: 0s - loss: 0.1061 - accuracy: 0.9620 - AUC: 0.9922
Epoch 00010: val_AUC did not improve from 0.94152
782/782 [==============================] - 118s 151ms/step - loss: 0.1061 - accuracy: 0.9620 - AUC: 0.9922 - val_loss: 0.3820 - val_accuracy: 0.8545 - val_AUC: 0.9315</code></pre></div><p>The model training begins here, where the recurrent network is fit on the padded IMDB review sequences and the corresponding sentiment labels. The training set is used to learn the parameters of the embedding layer and GRU, while the test set is supplied as validation data so the model can be checked after every epoch on reviews it has not trained on. A batch size of 32 means the optimizer updates the weights after seeing 32 reviews at a time, and the process is allowed to run for up to 100 epochs, although other controls can stop it earlier if performance stops improving.</p><p>As training starts, the output shows one epoch at a time, along with the loss, accuracy, and AUC measured on the training data, plus the validation loss, validation accuracy, and validation AUC measured on the held-out set. In the first epoch, the model is already learning a useful separation between positive and negative reviews, and the validation AUC jumps to 0.93268, which triggers the checkpoint callback to save the model to the file in the results folder. That saved message appears because the model is configured to keep the version with the best validation AUC so far, rather than just the latest one.</p><p>On the next few epochs, the training scores keep improving, which is a sign that the model is fitting the training data better and better. The validation AUC improves a little more in epoch 2, then briefly stalls in epoch 3, then reaches its best value so far in epoch 4 and again in epoch 5. Each time that happens, the checkpoint saves the new best model over the previous one. This is why the output repeatedly mentions that the validation AUC improved and that the model is being saved.</p><p>After epoch 5, the validation AUC stops getting better, even though the training metrics continue to rise. That pattern suggests the model is starting to specialize more strongly to the training set and is not gaining further generalization performance on the validation data. You can see this especially in the growing gap between training accuracy and validation accuracy. Because early stopping is watching validation AUC, it will eventually stop training once there has been no improvement for several epochs, and it will restore the best weights it saw earlier. The training output shown here reaches epoch 10, but the repeated &#8220;did not improve&#8221; messages make it clear that the model is no longer finding a better validation AUC after epoch 5, which is exactly the situation those callbacks are designed to detect.</p><h2>Assess the Results</h2><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;e65e9d21-542c-4a80-bb99-4edc05bf699c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">history = pd.DataFrame(training.history)
history.index += 1</code></pre></div><p>The purpose here is to turn the training history into a more convenient tabular form. After the model has finished fitting, the training process stores all of the recorded values in a history object, including things like loss, accuracy, and validation metrics for each epoch. Converting that history into a pandas DataFrame makes it easier to inspect, plot, or analyze the results later because each metric becomes a column and each epoch becomes a row.</p><p>Right after that, the index is shifted up by one so the epochs are numbered starting at 1 instead of 0. That makes the table line up with the way training is usually described to people, since epoch counts are typically shown as 1, 2, 3, and so on rather than zero-based numbering. There is no saved output from the cell because nothing is being printed or displayed yet; it simply prepares a cleaner version of the training records for the next steps in the notebook.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;8efcee4e-7e05-4173-a4cd-bec722523588&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">fig, axes = plt.subplots(ncols=2, figsize=(14, 4))
df1 = (history[['accuracy', 'val_accuracy']]
       .rename(columns={'accuracy': 'Training',
                        'val_accuracy': 'Validation'}))
df1.plot(ax=axes[0], title='Accuracy', xlim=(1, len(history)))

axes[0].axvline(df1.Validation.idxmax(), ls='--', lw=1, c='k')

df2 = (history[['AUC', 'val_AUC']]
       .rename(columns={'AUC': 'Training',
                        'val_AUC': 'Validation'}))
df2.plot(ax=axes[1], title='Area under the ROC Curve', xlim=(1, len(history)))

axes[1].axvline(df2.Validation.idxmax(), ls='--', lw=1, c='k')

for i in [0, 1]:
    axes[i].set_xlabel('Epoch')

sns.despine()
fig.tight_layout()
fig.savefig(results_path / 'rnn_imdb_cv', dpi=300)</code></pre></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!HVtw!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F108dae0a-26a4-456b-9f2b-86324a67ee90_1000x280.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!HVtw!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F108dae0a-26a4-456b-9f2b-86324a67ee90_1000x280.png 424w, https://substackcdn.com/image/fetch/$s_!HVtw!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F108dae0a-26a4-456b-9f2b-86324a67ee90_1000x280.png 848w, https://substackcdn.com/image/fetch/$s_!HVtw!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F108dae0a-26a4-456b-9f2b-86324a67ee90_1000x280.png 1272w, https://substackcdn.com/image/fetch/$s_!HVtw!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F108dae0a-26a4-456b-9f2b-86324a67ee90_1000x280.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!HVtw!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F108dae0a-26a4-456b-9f2b-86324a67ee90_1000x280.png" width="1000" height="280" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/108dae0a-26a4-456b-9f2b-86324a67ee90_1000x280.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:280,&quot;width&quot;:1000,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:36464,&quot;alt&quot;:&quot;Output image&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/202848543?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F108dae0a-26a4-456b-9f2b-86324a67ee90_1000x280.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Output image" title="Output image" srcset="https://substackcdn.com/image/fetch/$s_!HVtw!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F108dae0a-26a4-456b-9f2b-86324a67ee90_1000x280.png 424w, https://substackcdn.com/image/fetch/$s_!HVtw!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F108dae0a-26a4-456b-9f2b-86324a67ee90_1000x280.png 848w, https://substackcdn.com/image/fetch/$s_!HVtw!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F108dae0a-26a4-456b-9f2b-86324a67ee90_1000x280.png 1272w, https://substackcdn.com/image/fetch/$s_!HVtw!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F108dae0a-26a4-456b-9f2b-86324a67ee90_1000x280.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The purpose here is to turn the training history into an easy-to-read comparison of how the model performed on the training set versus the validation set over time, and then save that visualization for later use. It starts by creating a figure with two side-by-side panels, which gives room to show accuracy on one side and ROC AUC on the other without crowding the labels or lines.</p><p>The next step reshapes the recorded training history into a cleaner form for plotting. The first table keeps the accuracy values from training and validation, then renames the columns so the legend reads &#8220;Training&#8221; and &#8220;Validation&#8221; instead of the raw metric names. That cleaned-up table is plotted on the left axis, with the x-axis limited to the range of actual epochs. The result is the accuracy chart shown in the saved output, where the blue line rises steadily as the model gets better at fitting the training data, while the orange validation line moves more unevenly. The dashed vertical line marks the epoch where validation accuracy reached its highest point, which helps highlight the point at which performance on unseen data was best.</p><p>The same process is repeated for AUC on the right panel. AUC is especially useful here because it measures how well the model separates positive from negative reviews across all classification thresholds, not just at one cutoff. After renaming the columns, the history is plotted again, and another dashed vertical line is drawn at the validation peak. In the saved figure, the training AUC climbs smoothly toward the top of the chart, while validation AUC improves early and then levels off, which is a typical sign that the model keeps learning the training data even after validation performance stops improving.</p><p>After both plots are drawn, the x-axis label is set to &#8220;Epoch&#8221; on each panel so it is clear that the horizontal direction tracks training progress. The styling is then cleaned up by removing extra plot borders and tightening the layout so the two charts fit neatly side by side. Finally, the figure is saved to disk at the specified results path, which is why the output appears as a rendered image and also gets written out as a reusable file.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;83969f49-68d6-4ca5-b7c2-f1743c7b34ab&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">y_score = rnn.predict(X_test_padded)
y_score.shape</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;032941ff-a0c8-45bb-a534-16d9c0206269&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">(25000, 1)</code></pre></div><p>The purpose here is to use the trained recurrent neural network to generate sentiment scores for every review in the padded test set. The model takes the fixed-length review sequences and runs them forward through the embedding layer, the GRU layer, and the final sigmoid output layer, producing one probability-like value per review that represents how likely the model thinks the review is positive.</p><p>The prediction step returns those values in an array called y_score. Because the test set contains 25,000 reviews and the model is set up for binary classification, each review gets a single output score, so the result is shaped as 25,000 rows and 1 column. That is why the saved output shows (25000, 1): it confirms that the model has produced one sentiment prediction for each test example, with each prediction stored as a single numeric value rather than as separate class labels.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;feb8ab92-5e7d-4e2f-baab-2ac360ffdc43&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">roc_auc_score(y_score=y_score.squeeze(), y_true=y_test)</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;cb989475-33b4-4811-84b1-6b0247d65d5b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">0.941730672</code></pre></div><p>This final evaluation step measures how well the model separates positive reviews from negative ones using the predicted probabilities it produced for the test set. The scores are first squeezed down to a simple one-dimensional array, which matches the shape expected for a binary classification metric like ROC AUC, and then compared against the true test labels. ROC AUC focuses on ranking quality rather than a fixed decision threshold, so it tells you how often the model assigns a higher score to a genuinely positive review than to a genuinely negative one. The resulting value, 0.941730672, is quite strong and means the model is doing a very good job overall at distinguishing the two classes.</p><div><hr></div><h2>Notebook 7 of 8: <code>06_sentiment_analysis_pretrained_embeddings</code></h2><p><em>Source file: `06_sentiment_analysis_pretrained_embeddings_processed.ipynb`</em></p><div><hr></div><h1>Sentiment analysis using pretrained word embeddings</h1><p>In Chapter 15 on word embeddings, we looked at ways to train embeddings that are specific to a particular domain. Methods such as Word2vec and other similar algorithms can generate strong word representations, but they usually depend on very large corpora. Because of that, it is common for research teams to release word vectors that have already been trained on massive datasets, much like the pretrained weights used in deep learning transfer learning, which we covered in the previous chapter.</p><p>In this section, we will show how to work with pretrained Global Vectors for Word Representation, or GloVe, from the Stanford NLP group, using the IMDB review dataset.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;8a235287-c3a2-4798-9bab-88841751ed62&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">%matplotlib inline

from pathlib import Path

import numpy as np
import pandas as pd

from sklearn.metrics import roc_auc_score

import tensorflow as tf
from tensorflow.keras.callbacks import ModelCheckpoint, EarlyStopping
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, GRU, Embedding
from tensorflow.keras.preprocessing.sequence import pad_sequences
from tensorflow.keras.preprocessing.text import Tokenizer
import tensorflow.keras.backend as K

import matplotlib.pyplot as plt
import seaborn as sns</code></pre></div><p>The cell sets up the environment needed for the rest of the notebook by bringing in the libraries for data handling, modeling, evaluation, and plotting. It also enables inline Matplotlib output so any figures created later will appear directly inside the notebook instead of opening in a separate window. After that, it imports tools for working with file paths, numerical arrays, tabular data, AUC scoring, and the TensorFlow and Keras components used to build and train the neural network.</p><p>The deep learning imports prepare the pieces for a text model: a Sequential container to stack layers, an Embedding layer to turn word indices into vectors, a GRU layer to process the sequence information, and a Dense layer for the final binary prediction. The padding and tokenization utilities are imported now because the text will later need to be converted into fixed-length numeric sequences before it can be fed into the model. Training callbacks such as ModelCheckpoint and EarlyStopping are also loaded so the training process can automatically save the best-performing model and stop when validation performance stops improving. Finally, Matplotlib and Seaborn are imported for visualizing the training history and setting a consistent plotting style.</p><p>There is no saved output here because the cell is just preparing the notebook&#8217;s working environment. Its effect is to make the later cells runnable, but it does not yet compute anything or display any results.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;14caa994-ae33-4de9-8b8e-29d807c35711&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">gpu_devices = tf.config.experimental.list_physical_devices('GPU')
if gpu_devices:
    print('Using GPU')
    tf.config.experimental.set_memory_growth(gpu_devices[0], True)
else:
    print('Using CPU')</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;b224f63b-b590-4e1e-9656-0ba4d390ef27&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">Using CPU</code></pre></div><p>The purpose here is to check what kind of hardware TensorFlow can use before the model work begins. It asks TensorFlow to list the available GPU devices on the machine, and then looks at the result to decide whether accelerated computation is possible. If at least one GPU is found, the notebook announces that it is using the GPU and turns on memory growth for the first one, which tells TensorFlow not to grab all of the GPU memory at once. That helps avoid problems where TensorFlow reserves more memory than it needs right away. If no GPU is available, it falls back to the CPU instead.</p><p>The saved output shows &#8220;Using CPU,&#8221; which means TensorFlow did not detect any GPU device in the environment. That is why the notebook took the fallback branch and printed the CPU message. Nothing else is displayed because the cell is only performing this environment check and making a small runtime configuration choice, not producing data or model results.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;c91705b7-4cf1-4a97-a201-4293af327c93&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">sns.set_style('whitegrid')
np.random.seed(42)</code></pre></div><p>The purpose of this cell is to set up a consistent look for later plots and make the notebook&#8217;s random behavior more repeatable. First, the plotting style is changed to a white background with grid lines, which gives figures a cleaner, more readable appearance and makes it easier to compare values visually when graphs are displayed later. Then the NumPy random seed is fixed at 42. That means any later step that relies on NumPy&#8217;s random number generation will produce the same results each time the notebook is run, which is useful for reproducibility and for making debugging easier. Nothing is printed or displayed here, so there is no saved output; the effect of the cell is simply to quietly adjust the plotting style and the random state for the rest of the notebook.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;dd29e397-ff01-4eda-b668-a9caa51d98ff&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">results_path = Path('results', 'sentiment_imdb')
if not results_path.exists():
    results_path.mkdir(parents=True)</code></pre></div><p>The purpose here is to make sure there is a place on disk where the notebook can save later results, such as model checkpoints or plots. A Path object is created for the folder named results/sentiment_imdb, which gives a convenient, platform-independent way to work with file paths. The next step checks whether that folder already exists. If it does not, the folder is created, and the parents option allows any missing intermediate folders to be made as well. Nothing is printed or displayed because the cell is only preparing the filesystem in the background. When it runs successfully, it quietly ensures that any later attempt to write files into this location will not fail because the directory is missing.</p><h2>Read in the review files</h2><p>We will pull in the IMDB dataset directly from its original source so that we can handle the preprocessing ourselves.</p><p>Data source: <a href="http://ai.stanford.edu/~amaas/data/sentiment/">Stanford IMDB Reviews Dataset</a></p><p>Download the dataset, extract it, and move the files into a newly created <code>data</code> directory. After that, your folder layout should resemble this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;84382f8a-303c-401d-9cb1-d8d8f5760ce3&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">19_recurrent_neural_nets
 |-data
     |-aclimdb
          |-train
              |-neg
              |-pos
              ...
          |-test
          |-imdb.vocab</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;24593efc-9239-4090-8076-96186966740a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">path = Path('data', 'aclImdb')</code></pre></div><p>This line creates a Path object that points to the dataset folder where the IMDB review files are stored. Instead of working with a plain text string, it uses a Path so later file operations can be handled more cleanly and readably. The result is simply a reusable reference to the directory named data/aclImdb, which serves as the starting location for everything that follows when the reviews are loaded from disk. Since the cell only assigns a path and does not display anything or compute a visible result, there is no saved output.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;90ccfa4f-b285-4ac6-a97e-f1d8f24dcebf&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">files = path.glob('**/*.txt')
len(list(files))</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;59aeea92-6101-42cb-8a1e-57243c45d82e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">50003</code></pre></div><p>The cell is checking how many text files are present under the folder pointed to by the path variable. It first creates a recursive file search that looks through the directory and all of its subdirectories for anything ending in .txt, which matches the review files stored in the dataset. Then it converts that search result into a list so the matches can be counted, and finally it asks for the length of that list.</p><p>The saved output, 50003, is the total number of text files found in that directory tree. That number makes sense because the dataset contains many individual review files spread across training and test folders, along with a small number of extra text files that are not review data. Counting them here is a quick sanity check that the dataset has been unpacked correctly and that the file structure is what the later loading code expects.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;05d36758-a36f-4c1d-9067-dc40a489f156&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">files = path.glob('*/**/*.txt')
outcomes = set()
data = []
for f in files:
    if f.stem.startswith(('urls_', 'imdbEr')):
        continue
    _, _, data_set, outcome = f.parent.as_posix().split('/')
    if outcome == 'unsup':
        continue
    data.append([data_set, int(outcome == 'pos'),
                 f.read_text(encoding='latin1')])</code></pre></div><p>The purpose here is to walk through the IMDB folder structure, collect each review file, and turn the raw text files into a simple list of records that can be used later for training and testing. It starts by looking for every text file under the target directory, including files in nested folders, so it can gather all the review documents without having to name them one by one.</p><p>As it loops through those files, it first skips a few filenames that are not actual reviews. The files whose names begin with the special prefixes are metadata or helper files rather than sentiment examples, so they are ignored. Next, the code figures out where each review belongs by reading the folder names in its path. From that, it extracts whether the review came from the training or test split, and whether it was stored in the positive or negative folder. Reviews in the unlabeled subset are excluded as well, because they do not provide a sentiment label and therefore cannot be used for supervised learning.</p><p>For every remaining review, the code adds one row to a list containing three pieces of information: the split name, a numeric label for sentiment, and the full text of the review itself. The label is converted into 1 for positive reviews and 0 for negative reviews, which makes it easier to use later with a binary classifier. The text is read directly from disk using the Latin-1 character encoding, which helps avoid problems if the reviews contain characters that would not decode cleanly under a stricter default encoding.</p><p>There is no saved output because the cell is only preparing data in memory. Its result is the list of collected review records, which will be used in the next steps to build a structured dataset.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;ebe89a10-cf02-4624-9c3b-133c4160b8fa&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">data = pd.DataFrame(data, columns=['dataset', 'label', 'review'])</code></pre></div><p>The purpose of this line is to turn the collected review records into a structured table that is easier to work with later. Up to this point, the review information has been gathered as a plain Python collection of rows, with each row representing one review and its associated metadata. By passing that collection into a pandas DataFrame and assigning the column names at the same time, the data becomes organized into three clearly labeled fields: which split it belongs to, whether the review is positive or negative, and the review text itself.</p><p>Behind the scenes, pandas is arranging each row into a tabular format where every review becomes one record in the table and each of the three pieces of information gets its own column. This makes the data much more convenient for filtering, splitting into training and test sets, and feeding into the later text-processing steps. There is no saved output because nothing is being printed or displayed here; the important effect is the creation of the DataFrame itself, which will be used by the next cells.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;481d12c8-3696-4098-bf07-ef75110a82b0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">data.info()</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;3a2db0c7-dc1d-4685-9dda-d90fb13f9336&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">&lt;class 'pandas.core.frame.DataFrame'&gt;
RangeIndex: 50000 entries, 0 to 49999
Data columns (total 3 columns):
 #   Column   Non-Null Count  Dtype 
---  ------   --------------  ----- 
 0   dataset  50000 non-null  object
 1   label    50000 non-null  int64 
 2   review   50000 non-null  object
dtypes: int64(1), object(2)
memory usage: 1.1+ MB</code></pre></div><p>A quick structural check of the review table is being performed here to confirm that the dataset was loaded the way the earlier steps intended. Calling the information summary on the DataFrame prints a compact overview of its shape, column names, data types, and how many non-missing values each column contains. The output shows 50,000 total rows, which matches the full IMDB corpus after combining the training and test reviews.</p><p>The three columns are exactly the ones expected: a dataset field identifying whether each review came from the training or test split, a label field stored as integers, and the review text itself stored as objects. The summary also shows that every row has a value in each column, so there are no missing entries to worry about before moving on. The memory usage line gives a rough sense of how much space the table occupies in memory, which is useful when working with large text datasets because the review column, being textual, takes up most of that space.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;31f41762-4efc-40ca-a160-56aec764efc0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">train_data = data.loc[data.dataset=='train', ['label', 'review']]
test_data = data.loc[data.dataset=='test', ['label', 'review']]</code></pre></div><p>The goal here is to separate the full review table into the two standard splits used for model development: training data and test data. The first line filters the larger DataFrame down to only the rows marked as belonging to the training set, and it keeps just the two columns needed for sentiment work, the label and the review text. The second line does the same thing for the test set. Behind the scenes, pandas applies the boolean condition on the dataset column, finds all matching rows, and returns a smaller DataFrame view containing only those selected records.</p><p>Nothing is displayed because the cell only creates two new variables and does not ask to print them or show a preview. Its purpose is purely organizational: from this point on, the positive and negative reviews are cleanly separated into training and test collections, which makes the later tokenization, padding, and evaluation steps much easier to manage.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;1cee33ae-9051-4174-b76b-a0740f62b18e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">train_data.label.value_counts()</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;0d98e93d-1c56-4f65-ac7e-929a6d526fc4&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">0    12500
1    12500
Name: label, dtype: int64</code></pre></div><p>The purpose of this line is to check how many examples there are in each sentiment class within the training data. By asking for the value counts of the label column, pandas tallies up each unique label and reports how often it appears. Because the labels are encoded as 0 for negative reviews and 1 for positive reviews, the result gives a quick view of whether the training set is balanced or skewed toward one class.</p><p>The saved output shows that both classes appear exactly 12,500 times. That means the training set is perfectly balanced, with the same number of negative and positive reviews. The output is displayed as a small table-like Series, where the label values are listed alongside their counts, and the name of the column is shown at the bottom. This kind of check is useful before model training because a balanced dataset makes accuracy easier to interpret and reduces the chance that the model can look good simply by favoring one class.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;2c835b42-4953-461c-b927-53a15fd14ee8&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">test_data.label.value_counts()</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;c9527ae5-a4f8-4c01-9929-413277add5f0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">0    12500
1    12500
Name: label, dtype: int64</code></pre></div><p>This step checks how many reviews of each class are present in the test set by counting the values in the label column. Since the labels were encoded as 0 for negative reviews and 1 for positive reviews, the result shows the class balance directly. The saved output reports 12,500 reviews with label 0 and 12,500 reviews with label 1, which means the test data is perfectly balanced between the two sentiment classes. That kind of balance is useful because it avoids the model being biased toward one class simply because it appears more often than the other. The output is a simple frequency table, so it naturally appears as two rows with the label values as the index and the counts beside them.</p><h2>Data Preparation</h2><h3>Text tokenizer</h3><p>Keras includes a tokenizer, and we will use it here to turn the text documents into sequences of integers.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;725b1b53-96e2-4620-9e69-2dac1c12e372&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">num_words = 10000
t = Tokenizer(num_words=num_words, 
              lower=True, 
              oov_token=2)
t.fit_on_texts(train_data.review)</code></pre></div><p>The purpose here is to build a text tokenizer from the training reviews so the model can later work with numbers instead of raw words. The first line sets a limit on the vocabulary size, keeping only the 10,000 most useful words. That matters because natural language contains a huge number of rare words, and reducing the vocabulary helps keep the model simpler and more efficient.</p><p>Next, a tokenizer object is created with a few important settings. It will convert text to lowercase before processing, so words like &#8220;Good&#8221; and &#8220;good&#8221; are treated as the same token. It is also given an out-of-vocabulary token, which provides a fallback for words that were not seen often enough to be included in the limited vocabulary. Behind the scenes, the tokenizer will later assign integer IDs to words based on how frequently they appear in the training data.</p><p>The final line fits the tokenizer on the training reviews. This is the step where it scans through all the review text in train_data.review, learns the word frequencies, and builds the internal word-to-index mapping. Nothing is displayed in the saved output because this cell is setting up a preprocessing tool rather than producing a visible result. The important outcome is the tokenizer&#8217;s vocabulary, which will be used in the next steps to turn reviews into sequences of integers.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;aebd876d-0c7d-44e9-8f89-492dee39444c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">vocab_size = len(t.word_index) + 1
vocab_size</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;f15b3ed7-375d-4597-b340-7ace68c691a6&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">88586</code></pre></div><p>The goal here is to determine how large the tokenizer&#8217;s vocabulary is so the model can later use the correct input dimension for the embedding layer. The tokenizer has already built a word index that assigns an integer to each distinct word it saw in the training data, and taking its length gives the number of unique words known to that tokenizer. Adding 1 accounts for the reserved index 0, which is typically used for padding and therefore is not counted as a real word. The final value, 88586, shows that the tokenizer has learned 88,585 distinct word entries plus that extra padding slot. That single number is important because it defines the size of the lookup table the neural network will need when it converts word indices into vectors.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;8511243b-0dfa-45af-a498-33429847e954&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">train_data_encoded = t.texts_to_sequences(train_data.review)
test_data_encoded = t.texts_to_sequences(test_data.review)</code></pre></div><p>The goal here is to turn the review text into numbers that the neural network can work with. The tokenizer that was fitted earlier has already built a vocabulary and assigned an integer index to each known word, so this step uses that mapping to replace each review with a sequence of token IDs. Each review becomes a list of integers in the order the words appear, which preserves the structure of the sentence while converting it into a machine-readable form.</p><p>The training reviews are converted first, and then the test reviews are converted using the same tokenizer. Using the same fitted tokenizer matters because it keeps the word-to-number mapping consistent between training and testing. Words that were not seen during fitting are handled through the tokenizer&#8217;s out-of-vocabulary setting, so they still get represented in a controlled way rather than being lost entirely. Nothing is displayed as output here because the operation is just transforming the data and storing the encoded sequences in new variables for the next preprocessing step.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;2b4eaa31-6f1f-4bf3-a174-c9a1b13eac54&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">max_length = 100</code></pre></div><p>The purpose of this line is to set a fixed length for every review sequence that will be fed into the model. By assigning the value 100 to max_length, the notebook establishes a consistent size for the padded text data used later in training and evaluation. That matters because reviews naturally come in very different lengths, but neural networks need inputs with the same shape in every example. This value is then used when the integer word sequences are padded or truncated, so shorter reviews will be extended with zeros and longer ones will be cut down to the first 100 tokens. Since this cell only assigns a number and does not perform any computation or display anything, there is no saved output.</p><h3>Pad and truncate the review sequences</h3><p>We also rely on the <code>pad_sequences</code> function to turn the uneven lists of tokens into fixed-length arrays by padding shorter reviews and truncating longer ones for both the training and test datasets:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;9531a63c-47db-43b4-9ddb-224037936b52&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">X_train_padded = pad_sequences(train_data_encoded, 
                            maxlen=max_length, 
                            padding='post',
                           truncating='post')
y_train = train_data['label']
X_train_padded.shape</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;f30191bd-f3e6-45df-a0ca-0624ce2be407&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">(25000, 100)</code></pre></div><p>The purpose here is to turn the training reviews from variable-length token sequences into a single, consistent array that the neural network can process. The reviews have already been converted into lists of word indices, but those lists are not all the same length, so they need to be standardized before training. Padding is used for that standardization: shorter reviews are extended with zeros at the end, and longer reviews are cut off at the end, with both behaviors controlled to match the same maximum length setting used earlier in the pipeline. At the same time, the training labels are pulled out from the data so they can be paired with the padded review sequences during model fitting.</p><p>The saved output shows the shape of the padded training array, and that shape is 25,000 rows by 100 columns. The 25,000 rows mean there is one processed sequence for each training review, and the 100 columns reflect the fixed sequence length chosen for every review. That is exactly what you would expect after padding and truncation: every review, no matter how long or short it originally was, is now represented as a uniform 100-token sequence ready for input to the model.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;9d267b2a-c667-4e0f-888a-3e5ee84f1850&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">X_test_padded = pad_sequences(test_data_encoded, 
                            maxlen=max_length, 
                            padding='post',
                           truncating='post')
y_test = test_data['label']
X_test_padded.shape</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;142df920-3807-469b-b46d-81024911b2c7&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">(25000, 100)</code></pre></div><p>The goal here is to turn the encoded test reviews into the same fixed-length format used for training. The test reviews have already been converted into sequences of word indices, but those sequences can vary in length from one review to another. Padding and truncating them makes every example line up to a common length so the neural network can process them in a single batch.</p><p>The sequences are adjusted to a maximum length of 100. Reviews shorter than that are extended with zeros at the end, while longer reviews are cut off at the end as well. Using post-padding and post-truncation keeps the earlier words at the front of each review and reserves the end for either added zeros or dropped excess text. After that, the labels from the test DataFrame are pulled out into y_test so the model has the correct target values for evaluation.</p><p>The final line asks for the shape of the padded test array, and the saved output shows (25000, 100). That means there are 25,000 test reviews in total, and each one has been represented as a sequence of exactly 100 token positions. The output confirms that the preprocessing worked as intended and that the test set is now in the same consistent shape the model expects.</p><h2>Load pretrained embeddings</h2><p>Assuming the GloVe files have already been downloaded and extracted to the path used in the code, the next step is to build a lookup dictionary that links each GloVe word token to its 100-dimensional vector representation.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;9c6dad82-bf25-4125-8e71-4baa161a539b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># load the whole embedding into memory
glove_path = Path('..', 'data', 'glove', 'glove.6B.100d.txt')
embeddings_index = dict()

for line in glove_path.open(encoding='latin1'):
    values = line.split()
    word = values[0]
    try:
        coefs = np.asarray(values[1:], dtype='float32')
    except:
        continue
    embeddings_index[word] = coefs</code></pre></div><p>The purpose of this cell is to read the pretrained GloVe word vectors from disk and keep them in memory so they can be matched later with the vocabulary built from the reviews. It starts by pointing to the file that contains the 100-dimensional GloVe embeddings, then creates an empty dictionary that will eventually hold every word it successfully reads along with its vector representation.</p><p>It then opens the embedding file and processes it one line at a time. Each line in a GloVe file typically begins with a word, followed by a long list of numbers that represent that word in vector form. The line is split into separate pieces, the first piece is treated as the word itself, and the remaining pieces are interpreted as the numerical coordinates for that word. Those coordinates are converted into a NumPy array of 32-bit floating-point values, which is the format neural network code can work with efficiently.</p><p>There is a small safeguard here as well: if a line cannot be converted cleanly into numbers, it is skipped instead of causing the whole load process to fail. For all valid lines, the word becomes a key in the dictionary and its vector becomes the associated value. By the time the file has been read through completely, the dictionary contains a lookup table for pretrained embeddings, which will later be used to build the model&#8217;s embedding matrix.</p><p>There is no saved output from the cell because it is performing file loading and data preparation rather than printing or displaying anything. The result is stored in memory quietly, ready for the next step in the notebook.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;5ef779f0-b38c-4b24-8e35-cc0bca97a183&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">print('Loaded {:,d} word vectors.'.format(len(embeddings_index)))</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;1746fe1b-2222-4c90-bf22-838cff329992&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">Loaded 399,883 word vectors.</code></pre></div><p>The purpose of this line is to confirm that the pretrained GloVe vocabulary has been read successfully and to show how many word-to-vector mappings are now available in memory. The program takes the size of the dictionary holding the embeddings, formats that number with commas for readability, and prints it to the screen. That is why the saved output says &#8220;Loaded 399,883 word vectors.&#8221; The number reflects the total count of unique words found in the embedding file and stored in the lookup table, so it acts as a quick checkpoint that the loading step worked and that the model now has a large set of pretrained vectors to draw from when building the embedding matrix.</p><p>About three hundred and forty thousand word vectors are available, and we use them to build an embedding matrix aligned with the vocabulary. This lets the RNN look up each embedding directly from the token index.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;061e817d-2af7-44ad-816f-a3a83bd80773&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">embedding_matrix = np.zeros((vocab_size, 100))
for word, i in t.word_index.items():
    embedding_vector = embeddings_index.get(word)
    if embedding_vector is not None:
        embedding_matrix[i] = embedding_vector</code></pre></div><p>A matrix is created to hold the pretrained word vectors in a form the model can use. Its shape matches the tokenizer vocabulary size and the 100-dimensional GloVe embeddings, so each row can correspond to one word index and each column position stores one feature of that word&#8217;s vector. Starting with zeros means that every word begins with no embedding information until a match is found.</p><p>The loop then goes through the tokenizer&#8217;s word index, which maps each word to the integer the model will use for it. For each word, the code looks up its pretrained vector in the dictionary of loaded GloVe embeddings. When a match exists, that 100-number vector is copied into the appropriate row of the matrix, using the tokenizer&#8217;s index so the row order lines up with the integer sequences produced earlier. Words that are not present in the GloVe file are left as zero rows, which means the model will have no pretrained representation for them.</p><p>Nothing is printed because the cell is just building data in memory for later use. The result is an embedding matrix ready to be plugged into the embedding layer, where it will act as a lookup table that converts word indices into their pretrained semantic representations.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;ebb24125-4a60-4bae-8aa3-69d92238e3a9&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">embedding_matrix.shape</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;b00e6497-01b7-441a-84f7-caa8c06c2348&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">(88586, 100)</code></pre></div><p>This cell checks the size of the embedding matrix that was built from the tokenizer vocabulary and the pretrained GloVe vectors. The first number, 88,586, is the total number of rows in the matrix, which means there is one row reserved for each vocabulary index the tokenizer knows about. The second number, 100, shows that each row contains a 100-dimensional vector, matching the GloVe word embeddings that were loaded earlier. The saved output, (88586, 100), confirms that the matrix is ready in the expected shape for the embedding layer to look up word vectors by index during training.</p><h2>Set Up the Model Structure</h2><p>The key distinction from the RNN model used in the previous example is that the embedding layer will receive the pretrained embedding matrix and keep those weights frozen throughout training, so they do not get updated:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;8a62013a-220d-4907-82f2-b63597ae2fac&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">embedding_size = 100</code></pre></div><p>This cell simply sets the dimensionality of the word embeddings to 100. That number matters because each word vector in the pretrained GloVe file has 100 values, so using the same size here keeps the later embedding matrix and the model&#8217;s embedding layer aligned. By storing the value in a single variable, the notebook can reuse it consistently in later cells instead of hard-coding 100 in multiple places, which makes the setup easier to read and less error-prone.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;927d1f85-3ca6-4613-874d-a6ab9c51157d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">rnn = Sequential([
    Embedding(input_dim=vocab_size, 
              output_dim= embedding_size, 
              input_length=max_length,
              weights=[embedding_matrix], 
              trainable=False),
    GRU(units=32,  dropout=0.2, recurrent_dropout=0.2),
    Dense(1, activation='sigmoid')
])
rnn.summary()</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;73f1ab29-d398-4f57-bef6-077aad0fe539&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">WARNING:tensorflow:Layer gru will not use cuDNN kernel since it doesn't meet the cuDNN kernel criteria. It will use generic GPU kernel as fallback when running on GPU
Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
embedding (Embedding)        (None, 100, 100)          8858600   
_________________________________________________________________
gru (GRU)                    (None, 32)                12864     
_________________________________________________________________
dense (Dense)                (None, 1)                 33        
=================================================================
Total params: 8,871,497
Trainable params: 12,897
Non-trainable params: 8,858,600
_________________________________________________________________</code></pre></div><p>The purpose of this cell is to assemble the sentiment classifier and then print a structural summary so you can verify that the layers, shapes, and parameter counts all line up with the preprocessing choices made earlier. The model is built as a simple sequence-processing network: it starts by turning each padded review into a sequence of dense word vectors using the embedding layer, then reads that sequence with a GRU layer, and finally produces a single probability with a sigmoid output for binary sentiment prediction.</p><p>The first layer uses the pretrained embedding matrix, so each word index is mapped to a 100-dimensional vector rather than being learned from scratch. Because the embeddings are marked as non-trainable, those weights stay fixed during training, which is why the summary later shows almost all of the embedding parameters as non-trainable. The input length matches the fixed review length used earlier, so the network expects sequences of 100 tokens. Behind the scenes, this layer is doing lookup table indexing: every integer in the review sequence is replaced by the corresponding GloVe vector, and the padded zeros stay as zero vectors.</p><p>The next layer is a GRU with 32 units. Its job is to process the word vectors in order and compress the whole review into a learned representation that captures sentiment-related patterns across the sequence. The dropout settings tell it to randomly ignore some inputs and some recurrent connections during training, which helps reduce overfitting. The warning in the output appears because TensorFlow is unable to use the faster cuDNN-optimized GRU implementation with these settings, so it falls back to a more general GPU kernel instead. That warning is expected when features like recurrent dropout are enabled.</p><p>The last layer is a single dense unit with sigmoid activation, which converts the GRU&#8217;s output into one number between 0 and 1. That number represents the model&#8217;s predicted probability that a review is positive. The summary confirms the shape flow through the network: the embedding layer outputs a sequence of 100 vectors of size 100, the GRU condenses that into a 32-dimensional vector, and the final dense layer reduces it to a single prediction.</p><p>The parameter counts in the summary reflect the design choices here. The embedding layer contains most of the weights, but they are frozen, so they are counted as non-trainable. Only the GRU and dense layers contribute trainable parameters, which is why the trainable total is much smaller than the overall parameter count. The summary is a useful checkpoint because it confirms that the model is wired correctly before training begins and that the pretrained embeddings are being used in the intended fixed form.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;b0fabdf2-411b-4db4-8ef5-5acc481f297d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">rnn.compile(loss='binary_crossentropy',
            optimizer='RMSProp',
            metrics=['accuracy', 
                     tf.keras.metrics.AUC(name='AUC')])</code></pre></div><p>The model is being prepared for training by telling Keras exactly how to judge its predictions and how to update its weights. The loss function is set to binary cross-entropy, which is the standard choice when the task has only two possible labels, such as positive versus negative sentiment. Behind the scenes, this loss measures how far the model&#8217;s predicted probabilities are from the true 0-or-1 targets, and it gives the optimizer a signal to improve the network step by step.</p><p>RMSProp is chosen as the optimizer, so once training begins it will handle the weight updates by adjusting the learning rate adaptively for each parameter. That often works well for recurrent networks like this one because it can make training steadier than a plain gradient descent approach. The metrics list adds two ways of tracking performance during training: accuracy, which counts how often the predicted class matches the true label, and AUC, which is especially useful for binary classification because it measures how well the model separates the two classes across different decision thresholds.</p><p>There is no saved output here because compiling a model usually does not produce visible results. Instead, it quietly sets up the training configuration so that the next step can run with the chosen loss, optimizer, and evaluation measures already in place.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;74aa6d49-9039-4f89-8c2a-ee366ff28239&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">rnn_path = (results_path / 'lstm.pretrained.h5').as_posix()

checkpointer = ModelCheckpoint(filepath=rnn_path,
                               verbose=1,
                               monitor='val_AUC',
                               mode='max',
                               save_best_only=True)</code></pre></div><p>The cell prepares a checkpoint location for saving the model during training, then sets up a callback that will automatically write out the best version of the network as validation performance improves. First, a path is built inside the results folder and converted into a standard string path so Keras can use it easily. The filename chosen suggests a saved model file for the pretrained recurrent network, even though the exact architecture was built earlier.</p><p>After that, a ModelCheckpoint object is created and configured to watch the validation AUC metric. Because the goal is to maximize AUC, the monitoring mode is set to &#8220;max,&#8221; so only higher values are treated as improvements. The save<em>best</em>only setting means the file will be overwritten only when a new best validation AUC is reached, rather than saving a copy after every epoch. The verbose setting tells Keras to report when a new checkpoint is written during training. Since there is no saved output for this cell, nothing is printed immediately; its purpose is to define the checkpointing behavior that will take effect later when the model begins training.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;7bc13c6b-415a-41f0-a9f5-e50a0b054c1c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">early_stopping = EarlyStopping(monitor='val_AUC',
                               patience=5,
                               mode='max',
                               restore_best_weights=True)</code></pre></div><p>This cell sets up a training safeguard that watches the model&#8217;s validation AUC and decides when learning has stopped improving. The callback is configured to monitor the validation metric named val<em>AUC, which means it will check performance on the validation data after each epoch rather than looking only at the training results. Since the goal is to maximize AUC, the mode is set to max, so the callback treats higher values as better. The patience value of 5 gives the model a small grace period: if the validation AUC does not improve for five consecutive epochs, training will be stopped early. The restore</em>best_weights option makes the process more useful by rolling the model back to the version from the epoch with the best validation AUC, instead of leaving it at the last epoch reached. There is no visible output because the cell is only creating this callback object and storing it for use later during model training.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;50759876-b99a-44b2-92b1-b1e86b4a0726&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">training = rnn.fit(X_train_padded,
                   y_train,
                   batch_size=32,
                   epochs=100,
                   validation_data=(X_test_padded,
                                    y_test),
                   callbacks=[early_stopping,
                              checkpointer],
                   verbose=1)</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;ba806f7e-f3a5-4c7c-83c0-216f602c640c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">Epoch 1/100
782/782 [==============================] - ETA: 0s - loss: 0.6505 - accuracy: 0.6087 - AUC: 0.6565
Epoch 00001: val_AUC improved from -inf to 0.80524, saving model to results/sentiment_imdb/lstm.pretrained.h5
782/782 [==============================] - 111s 141ms/step - loss: 0.6505 - accuracy: 0.6087 - AUC: 0.6565 - val_loss: 0.7026 - val_accuracy: 0.6586 - val_AUC: 0.8052
Epoch 2/100
782/782 [==============================] - ETA: 0s - loss: 0.5053 - accuracy: 0.7570 - AUC: 0.8316
Epoch 00002: val_AUC improved from 0.80524 to 0.86949, saving model to results/sentiment_imdb/lstm.pretrained.h5
782/782 [==============================] - 111s 143ms/step - loss: 0.5053 - accuracy: 0.7570 - AUC: 0.8316 - val_loss: 0.4681 - val_accuracy: 0.7776 - val_AUC: 0.8695
Epoch 3/100
782/782 [==============================] - ETA: 0s - loss: 0.4538 - accuracy: 0.7872 - AUC: 0.8679
Epoch 00003: val_AUC improved from 0.86949 to 0.88737, saving model to results/sentiment_imdb/lstm.pretrained.h5
782/782 [==============================] - 117s 149ms/step - loss: 0.4538 - accuracy: 0.7872 - AUC: 0.8679 - val_loss: 0.4352 - val_accuracy: 0.7931 - val_AUC: 0.8874
Epoch 4/100
782/782 [==============================] - ETA: 0s - loss: 0.4303 - accuracy: 0.8000 - AUC: 0.8823
Epoch 00004: val_AUC improved from 0.88737 to 0.89225, saving model to results/sentiment_imdb/lstm.pretrained.h5
782/782 [==============================] - 109s 139ms/step - loss: 0.4303 - accuracy: 0.8000 - AUC: 0.8823 - val_loss: 0.4404 - val_accuracy: 0.7947 - val_AUC: 0.8922
Epoch 5/100
782/782 [==============================] - ETA: 0s - loss: 0.4138 - accuracy: 0.8062 - AUC: 0.8915
Epoch 00005: val_AUC improved from 0.89225 to 0.89876, saving model to results/sentiment_imdb/lstm.pretrained.h5
782/782 [==============================] - 109s 139ms/step - loss: 0.4138 - accuracy: 0.8062 - AUC: 0.8915 - val_loss: 0.4284 - val_accuracy: 0.7994 - val_AUC: 0.8988
Epoch 6/100
782/782 [==============================] - ETA: 0s - loss: 0.4064 - accuracy: 0.8123 - AUC: 0.8959
Epoch 00006: val_AUC improved from 0.89876 to 0.90227, saving model to results/sentiment_imdb/lstm.pretrained.h5
782/782 [==============================] - 109s 139ms/step - loss: 0.4064 - accuracy: 0.8123 - AUC: 0.8959 - val_loss: 0.4015 - val_accuracy: 0.8176 - val_AUC: 0.9023
Epoch 7/100
782/782 [==============================] - ETA: 0s - loss: 0.3959 - accuracy: 0.8201 - AUC: 0.9016
Epoch 00007: val_AUC improved from 0.90227 to 0.90520, saving model to results/sentiment_imdb/lstm.pretrained.h5
782/782 [==============================] - 108s 138ms/step - loss: 0.3959 - accuracy: 0.8201 - AUC: 0.9016 - val_loss: 0.4228 - val_accuracy: 0.8072 - val_AUC: 0.9052
Epoch 8/100
782/782 [==============================] - ETA: 0s - loss: 0.3884 - accuracy: 0.8230 - AUC: 0.9054
Epoch 00008: val_AUC improved from 0.90520 to 0.90554, saving model to results/sentiment_imdb/lstm.pretrained.h5
782/782 [==============================] - 118s 151ms/step - loss: 0.3884 - accuracy: 0.8230 - AUC: 0.9054 - val_loss: 0.4152 - val_accuracy: 0.8064 - val_AUC: 0.9055
Epoch 9/100
782/782 [==============================] - ETA: 0s - loss: 0.3814 - accuracy: 0.8264 - AUC: 0.9090
Epoch 00009: val_AUC improved from 0.90554 to 0.90921, saving model to results/sentiment_imdb/lstm.pretrained.h5
782/782 [==============================] - 107s 137ms/step - loss: 0.3814 - accuracy: 0.8264 - AUC: 0.9090 - val_loss: 0.3939 - val_accuracy: 0.8167 - val_AUC: 0.9092
Epoch 10/100
782/782 [==============================] - ETA: 0s - loss: 0.3766 - accuracy: 0.8275 - AUC: 0.9112
Epoch 00010: val_AUC improved from 0.90921 to 0.90976, saving model to results/sentiment_imdb/lstm.pretrained.h5
782/782 [==============================] - 108s 138ms/step - loss: 0.3766 - accuracy: 0.8275 - AUC: 0.9112 - val_loss: 0.3896 - val_accuracy: 0.8194 - val_AUC: 0.9098
Epoch 11/100
782/782 [==============================] - ETA: 0s - loss: 0.3698 - accuracy: 0.8348 - AUC: 0.9147
Epoch 00011: val_AUC improved from 0.90976 to 0.91103, saving model to results/sentiment_imdb/lstm.pretrained.h5
782/782 [==============================] - 142s 182ms/step - loss: 0.3698 - accuracy: 0.8348 - AUC: 0.9147 - val_loss: 0.3808 - val_accuracy: 0.8267 - val_AUC: 0.9110
Epoch 12/100
782/782 [==============================] - ETA: 0s - loss: 0.3644 - accuracy: 0.8345 - AUC: 0.9174
Epoch 00012: val_AUC improved from 0.91103 to 0.91111, saving model to results/sentiment_imdb/lstm.pretrained.h5
782/782 [==============================] - 106s 136ms/step - loss: 0.3644 - accuracy: 0.8345 - AUC: 0.9174 - val_loss: 0.3912 - val_accuracy: 0.8201 - val_AUC: 0.9111
Epoch 13/100
782/782 [==============================] - ETA: 0s - loss: 0.3600 - accuracy: 0.8369 - AUC: 0.9195
Epoch 00013: val_AUC improved from 0.91111 to 0.91143, saving model to results/sentiment_imdb/lstm.pretrained.h5
782/782 [==============================] - 111s 142ms/step - loss: 0.3600 - accuracy: 0.8369 - AUC: 0.9195 - val_loss: 0.3763 - val_accuracy: 0.8279 - val_AUC: 0.9114
Epoch 14/100
782/782 [==============================] - ETA: 0s - loss: 0.3553 - accuracy: 0.8398 - AUC: 0.9217
Epoch 00014: val_AUC improved from 0.91143 to 0.91288, saving model to results/sentiment_imdb/lstm.pretrained.h5
782/782 [==============================] - 108s 138ms/step - loss: 0.3553 - accuracy: 0.8398 - AUC: 0.9217 - val_loss: 0.3769 - val_accuracy: 0.8277 - val_AUC: 0.9129
Epoch 15/100
782/782 [==============================] - ETA: 0s - loss: 0.3505 - accuracy: 0.8433 - AUC: 0.9239
Epoch 00015: val_AUC improved from 0.91288 to 0.91290, saving model to results/sentiment_imdb/lstm.pretrained.h5
782/782 [==============================] - 110s 141ms/step - loss: 0.3505 - accuracy: 0.8433 - AUC: 0.9239 - val_loss: 0.3991 - val_accuracy: 0.8195 - val_AUC: 0.9129
Epoch 16/100
782/782 [==============================] - ETA: 0s - loss: 0.3488 - accuracy: 0.8429 - AUC: 0.9247
Epoch 00016: val_AUC did not improve from 0.91290
782/782 [==============================] - 110s 140ms/step - loss: 0.3488 - accuracy: 0.8429 - AUC: 0.9247 - val_loss: 0.4081 - val_accuracy: 0.8132 - val_AUC: 0.9118
Epoch 17/100
782/782 [==============================] - ETA: 0s - loss: 0.3450 - accuracy: 0.8452 - AUC: 0.9264
Epoch 00017: val_AUC improved from 0.91290 to 0.91368, saving model to results/sentiment_imdb/lstm.pretrained.h5
782/782 [==============================] - 110s 140ms/step - loss: 0.3450 - accuracy: 0.8452 - AUC: 0.9264 - val_loss: 0.3795 - val_accuracy: 0.8306 - val_AUC: 0.9137
Epoch 18/100
782/782 [==============================] - ETA: 0s - loss: 0.3430 - accuracy: 0.8460 - AUC: 0.9272
Epoch 00018: val_AUC did not improve from 0.91368
782/782 [==============================] - 108s 138ms/step - loss: 0.3430 - accuracy: 0.8460 - AUC: 0.9272 - val_loss: 0.3891 - val_accuracy: 0.8221 - val_AUC: 0.9127
Epoch 19/100
782/782 [==============================] - ETA: 0s - loss: 0.3376 - accuracy: 0.8477 - AUC: 0.9296
Epoch 00019: val_AUC did not improve from 0.91368
782/782 [==============================] - 106s 135ms/step - loss: 0.3376 - accuracy: 0.8477 - AUC: 0.9296 - val_loss: 0.3822 - val_accuracy: 0.8267 - val_AUC: 0.9134
Epoch 20/100
782/782 [==============================] - ETA: 0s - loss: 0.3368 - accuracy: 0.8516 - AUC: 0.9300
Epoch 00020: val_AUC improved from 0.91368 to 0.91385, saving model to results/sentiment_imdb/lstm.pretrained.h5
782/782 [==============================] - 105s 135ms/step - loss: 0.3368 - accuracy: 0.8516 - AUC: 0.9300 - val_loss: 0.3994 - val_accuracy: 0.8138 - val_AUC: 0.9139
Epoch 21/100
782/782 [==============================] - ETA: 0s - loss: 0.3340 - accuracy: 0.8532 - AUC: 0.9312
Epoch 00021: val_AUC did not improve from 0.91385
782/782 [==============================] - 105s 135ms/step - loss: 0.3340 - accuracy: 0.8532 - AUC: 0.9312 - val_loss: 0.3741 - val_accuracy: 0.8297 - val_AUC: 0.9131
Epoch 22/100
782/782 [==============================] - ETA: 0s - loss: 0.3310 - accuracy: 0.8509 - AUC: 0.9324
Epoch 00022: val_AUC improved from 0.91385 to 0.91487, saving model to results/sentiment_imdb/lstm.pretrained.h5
782/782 [==============================] - 107s 137ms/step - loss: 0.3310 - accuracy: 0.8509 - AUC: 0.9324 - val_loss: 0.3795 - val_accuracy: 0.8306 - val_AUC: 0.9149</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;ec688d1b-ca18-441c-8feb-bcde4e16c1b5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">Epoch 23/100
782/782 [==============================] - ETA: 0s - loss: 0.3276 - accuracy: 0.8534 - AUC: 0.9339
Epoch 00023: val_AUC did not improve from 0.91487
782/782 [==============================] - 107s 137ms/step - loss: 0.3276 - accuracy: 0.8534 - AUC: 0.9339 - val_loss: 0.3904 - val_accuracy: 0.8244 - val_AUC: 0.9135
Epoch 24/100
782/782 [==============================] - ETA: 0s - loss: 0.3266 - accuracy: 0.8554 - AUC: 0.9343
Epoch 00024: val_AUC did not improve from 0.91487
782/782 [==============================] - 108s 138ms/step - loss: 0.3266 - accuracy: 0.8554 - AUC: 0.9343 - val_loss: 0.3725 - val_accuracy: 0.8280 - val_AUC: 0.9141
Epoch 25/100
782/782 [==============================] - ETA: 0s - loss: 0.3221 - accuracy: 0.8580 - AUC: 0.9361
Epoch 00025: val_AUC did not improve from 0.91487
782/782 [==============================] - 123s 158ms/step - loss: 0.3221 - accuracy: 0.8580 - AUC: 0.9361 - val_loss: 0.3825 - val_accuracy: 0.8276 - val_AUC: 0.9142
Epoch 26/100
782/782 [==============================] - ETA: 0s - loss: 0.3206 - accuracy: 0.8590 - AUC: 0.9367
Epoch 00026: val_AUC did not improve from 0.91487
782/782 [==============================] - 127s 162ms/step - loss: 0.3206 - accuracy: 0.8590 - AUC: 0.9367 - val_loss: 0.3769 - val_accuracy: 0.8284 - val_AUC: 0.9144
Epoch 27/100
782/782 [==============================] - ETA: 0s - loss: 0.3195 - accuracy: 0.8608 - AUC: 0.9372
Epoch 00027: val_AUC did not improve from 0.91487
782/782 [==============================] - 125s 160ms/step - loss: 0.3195 - accuracy: 0.8608 - AUC: 0.9372 - val_loss: 0.3782 - val_accuracy: 0.8279 - val_AUC: 0.9142</code></pre></div><p>The cell starts the actual model training run by fitting the recurrent network on the padded training reviews and checking its progress on the held-out test reviews after every epoch. The batch size is set to 32, so the data is processed in small chunks rather than all at once, which is a standard way to make training manageable in memory. The model is allowed to run for up to 100 epochs, but that is just an upper limit because the stopping callback can end training earlier if validation performance stops improving. The validation_data argument tells the model to calculate loss, accuracy, and AUC on the test set at the end of each epoch, and the callbacks list attaches two pieces of training logic: one that saves the best-performing model so far and another that watches validation AUC and eventually halts training if it plateaus. The result is stored in a variable named training, which keeps the full training history for later inspection and plotting.</p><p>The saved output shows the training loop advancing epoch by epoch and reporting both the learning progress on the training set and the model&#8217;s behavior on the validation set. In the first epoch, the network begins with moderate performance, which is typical because the weights are still being adjusted from their initial state. As the epochs continue, the loss steadily drops while accuracy and AUC rise, indicating that the model is learning to separate positive from negative reviews better and better. Each time the validation AUC improves, the checkpoint callback prints a message saying the model was saved to the results file, which explains why that message appears repeatedly early on. The filename reflects the saved best checkpoint, so the model on disk is always the version with the strongest validation AUC seen so far.</p><p>Later in training, the validation AUC stops improving every epoch, and the output begins to alternate between improvement messages and notices that the score did not get better. That pattern is exactly what the early-stopping and checkpoint logic is designed to track: the model keeps training as long as there is still meaningful progress, but once validation performance starts leveling off, improvements become less frequent and the callback starts counting those non-improving epochs. By the final visible epoch, the model is still training, but the validation scores are hovering in a narrow range around the best value reached so far, which suggests the network has reached a fairly stable performance level on this task.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;e780ceea-d612-4e7b-b649-7e405bbe0195&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">y_score = rnn.predict(X_test_padded)
roc_auc_score(y_score=y_score.squeeze(), y_true=y_test)</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;1cd969bd-cb0d-455c-a411-bc587843e892&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">0.914964528</code></pre></div><p>The model first uses its learned weights to make predictions for every padded review in the test set. The prediction step produces a score for each example, and because the final layer uses a sigmoid activation, those scores represent estimated probabilities that a review is positive. The result is stored so it can be reused immediately in the next line.</p><p>After that, the predictions are compared with the true test labels using ROC AUC. This metric looks at how well the model ranks positive reviews above negative ones across all possible decision thresholds, rather than judging it at just one cutoff. The predicted scores are squeezed down into a one-dimensional array so they match the label format expected by the metric function. The saved output, 0.914964528, is the resulting AUC value, which means the model has strong ability to distinguish positive from negative reviews on the test set. The number appears by itself because that is the single scalar returned by the evaluation call.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;d538d549-8dce-4b98-8d4d-232d3a7febcb&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">df = pd.DataFrame(training.history)
best_auc = df.val_AUC.max()
best_acc = df.val_accuracy.max()

fig, axes = plt.subplots(ncols=2, figsize=(14,4))
df.index = df.index.to_series().add(1)
df[['AUC', 'val_AUC']].plot(ax=axes[0], 
                            title=f'AUC | Best: {best_auc:.4f}', 
                            legend=False, 
                            xlim=(1, 33),
                            ylim=(.7, .95))

axes[0].axvline(df.val_AUC.idxmax(), ls='--', lw=1, c='k')
df[['accuracy', 'val_accuracy']].plot(ax=axes[1], 
                                              title=f'Accuracy | Best: {best_acc:.2%}', 
                                              legend=False, 
                                              xlim=(1, 33),
                                      ylim=(.7, .9))
axes[1].axvline(df.val_accuracy.idxmax(), ls='--', lw=1, c='k')
axes[0].set_xlabel('Epoch')
axes[0].set_ylabel('AUC')
axes[1].set_xlabel('Epoch')
axes[1].set_ylabel('Accuracy')
fig.suptitle('Sentiment Analysis - Pretrained Vectors', fontsize=14)
fig.legend(['Train', 'Validation'], loc='center right')

sns.despine()
fig.tight_layout()
fig.subplots_adjust(top=.9)
fig.savefig(results_path / 'imdb_pretrained', dpi=300);</code></pre></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!vmrS!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa080f418-3cfa-4902-b647-14967bb3a5b8_1006x285.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!vmrS!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa080f418-3cfa-4902-b647-14967bb3a5b8_1006x285.png 424w, https://substackcdn.com/image/fetch/$s_!vmrS!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa080f418-3cfa-4902-b647-14967bb3a5b8_1006x285.png 848w, https://substackcdn.com/image/fetch/$s_!vmrS!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa080f418-3cfa-4902-b647-14967bb3a5b8_1006x285.png 1272w, https://substackcdn.com/image/fetch/$s_!vmrS!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa080f418-3cfa-4902-b647-14967bb3a5b8_1006x285.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!vmrS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa080f418-3cfa-4902-b647-14967bb3a5b8_1006x285.png" width="1006" height="285" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a080f418-3cfa-4902-b647-14967bb3a5b8_1006x285.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:285,&quot;width&quot;:1006,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:40177,&quot;alt&quot;:&quot;Output image&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/202848543?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa080f418-3cfa-4902-b647-14967bb3a5b8_1006x285.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Output image" title="Output image" srcset="https://substackcdn.com/image/fetch/$s_!vmrS!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa080f418-3cfa-4902-b647-14967bb3a5b8_1006x285.png 424w, https://substackcdn.com/image/fetch/$s_!vmrS!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa080f418-3cfa-4902-b647-14967bb3a5b8_1006x285.png 848w, https://substackcdn.com/image/fetch/$s_!vmrS!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa080f418-3cfa-4902-b647-14967bb3a5b8_1006x285.png 1272w, https://substackcdn.com/image/fetch/$s_!vmrS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa080f418-3cfa-4902-b647-14967bb3a5b8_1006x285.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The purpose of this cell is to turn the recorded training history into a clear visual summary of how the model performed over time, and then save that summary for later use. It begins by converting the history object from training into a DataFrame, which makes the epoch-by-epoch values for loss, AUC, accuracy, and validation metrics easier to work with. From there, it pulls out the best validation AUC and best validation accuracy so those peak values can be highlighted in the plot titles.</p><p>Next, the cell creates a figure with two side-by-side panels. The history index is shifted so the epochs are labeled starting at 1 instead of 0, which makes the chart easier to read. The left panel plots training AUC and validation AUC together, while the right panel plots training accuracy and validation accuracy together. The titles include the best validation score for each metric, so the viewer can immediately see the strongest point reached during training. The vertical dashed lines mark the exact epoch where each validation metric reached its maximum, which helps connect the summary number in the title to the corresponding place in the curve.</p><p>After the curves are drawn, the axes are labeled so it is obvious that the horizontal direction represents epochs and the vertical direction represents either AUC or accuracy. The overall figure title identifies the experiment as sentiment analysis with pretrained vectors, and a shared legend is added on the right so the blue and orange lines are interpreted consistently across both panels. The styling steps that follow remove extra plot borders, tighten the layout, and make room for the title so the figure looks polished and balanced.</p><p>The saved output shows exactly what this sequence produces: a two-panel training history figure where AUC rises quickly and then levels off, accuracy increases more gradually, and the validation curves stay a bit below the training curves. The dashed black lines appear at the epochs where validation performance peaks, and the titles report those best values, which is why the plot is both informative and easy to scan. Finally, the figure is written to the results folder as an image file, so the visual summary is preserved even after the notebook finishes running.</p><div><hr></div><h2>Notebook 8 of 8: <code>07_sec_filings_return_prediction</code></h2><p><em>Source file: `07_sec_filings_return_prediction_processed.ipynb`</em></p><div><hr></div><h1>Using RNNs and Word Embeddings to Forecast Returns from SEC Filings</h1><p>RNNs are widely used across many natural language processing problems. In part three of this book, we already saw how text data can be used for sentiment analysis.</p><p>Here, we will use an RNN on SEC filings to learn task-specific word embeddings, as discussed in Chapter 16, and to estimate the stock return for the week following each filing date.</p><h2>Imports and Configuration</h2><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;0bca5f4f-96db-4313-b89a-b555142e70a5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">import warnings
warnings.filterwarnings('ignore')</code></pre></div><p>The purpose here is simply to quiet down warning messages so the rest of the notebook runs without a lot of unnecessary noise. First, the warnings module is imported, which gives access to Python&#8217;s built-in warning system. Then the warning filter is changed so that warnings are ignored. After that setting is applied, any non-fatal warning that would normally appear during execution is suppressed, which can make the notebook output much easier to read. Since this cell only changes that global warning behavior and does not actually produce anything to display, there is no saved output.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;8a08a3dc-b89c-4fc7-bb93-e0b6cb35d265&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">%matplotlib inline

from pathlib import Path
from time import time
from collections import Counter
from datetime import datetime, timedelta
from tqdm import tqdm 

import numpy as np
import pandas as pd
from scipy.stats import spearmanr
import yfinance as yf

from gensim.models.word2vec import LineSentence
from gensim.models.phrases import Phrases, Phraser

from sklearn.model_selection import train_test_split

import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import (Dense, GRU, Bidirectional,
                                     Embedding, BatchNormalization, Dropout)
from tensorflow.keras.preprocessing.sequence import pad_sequences
from tensorflow.keras.callbacks import EarlyStopping
from tensorflow.keras.metrics import RootMeanSquaredError, MeanAbsoluteError
import tensorflow.keras.backend as K

import matplotlib.pyplot as plt
import seaborn as sns</code></pre></div><p>This cell sets up the main tools the rest of the notebook will rely on. It begins by making plots display inline, which means any charts created later will appear directly in the notebook rather than in a separate window. After that, it imports a collection of standard utilities for working with files, dates, timing, counting values, and showing progress bars, since the workflow involves moving through many filings and tracking how long the processing takes.</p><p>It then brings in the numerical and data libraries used throughout the pipeline. NumPy and pandas handle arrays and tables, while SciPy provides the Spearman correlation calculation used later to evaluate predictions. YFinance is included for pulling stock price data. Gensim&#8217;s sentence and phrase tools are imported because the text will be tokenized into sentences and then combined into common multi-word phrases such as bigrams and trigrams.</p><p>Next come the machine learning tools. Scikit-learn supplies the train-test split function, and TensorFlow Keras provides the neural network pieces: the sequential model container, dense layers, the GRU recurrent layer, bidirectional wrapping, embeddings, normalization, dropout, padding utilities, early stopping, and error metrics like RMSE and MAE. The backend module is imported as well for lower-level TensorFlow operations if needed later.</p><p>Finally, Matplotlib and Seaborn are imported for visualization. These libraries will be used to plot training curves and prediction results, making it easier to inspect how well the model learns and how its outputs compare with the actual returns.</p><p>There is no saved output here because the cell is purely about preparation. Its job is to load everything needed so later cells can read data, transform text, train the neural network, and visualize the results without repeatedly importing the same tools.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;f1cfa7f9-a29f-4a58-94e7-2569cd1d17f5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">gpu_devices = tf.config.experimental.list_physical_devices('GPU')
if gpu_devices:
    print('Using GPU')
    tf.config.experimental.set_memory_growth(gpu_devices[0], True)
else:
    print('Using CPU')</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;3c88d12d-b40a-4f49-953a-b61f81f6050c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">Using CPU</code></pre></div><p>The cell checks whether TensorFlow can see a GPU on the machine and then chooses a sensible setup based on what it finds. It asks TensorFlow for the available physical GPU devices, which returns a list. If that list is not empty, the cell would announce that a GPU is being used and turn on memory growth for the first GPU so TensorFlow does not reserve all GPU memory at once. That setting is helpful because it lets memory usage expand only as needed, which is safer when other processes may also need the GPU.</p><p>In the saved output, the message says &#8220;Using CPU,&#8221; which means no GPU device was detected by TensorFlow at runtime. So the code followed the else branch instead of the GPU branch. Behind the scenes, that means the rest of the notebook will run on the CPU, which is usually slower for neural network training, but it still allows the workflow to continue normally.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;b7ffb2a6-1cac-462a-a6dc-d346b489aafe&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">np.random.seed(42)
tf.random.set_seed(42)</code></pre></div><p>The purpose of this cell is to make the notebook&#8217;s randomness reproducible. By setting the NumPy random seed and the TensorFlow random seed to the same fixed value, it ensures that any later steps that rely on random number generation will start from a consistent state each time the notebook is run.</p><p>Behind the scenes, many parts of machine learning workflows use randomness, such as shuffling data, initializing model weights, or choosing batches during training. Without fixed seeds, those operations can lead to slightly different results from one run to the next. Using the same seed helps make experiments easier to compare and debug, because the sequence of random values will be the same each time.</p><p>There is no saved output because nothing needs to be displayed here. The effect of the cell happens quietly in the background by changing the random state for the rest of the notebook.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;d4bcbcf8-b623-4f4b-a830-1f2a66f7ae7c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">idx = pd.IndexSlice
sns.set_style('whitegrid')</code></pre></div><p>The purpose here is to set up a couple of reusable plotting and indexing helpers for later cells. First, an index-slicing shortcut is assigned so that complex pandas selections can be written more conveniently when working with multi-level indexes. That kind of shorthand is useful when filtering or slicing time series and panel-style data, because it makes those selections easier to read and reuse. Then the plotting style is switched to a white-grid theme in Seaborn, which changes the default look of future charts to have a cleaner background and visible grid lines. There is no visible output because neither line produces a printed result or a plot; they simply prepare the notebook environment for later data manipulation and visualization.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;2ae6c0aa-f1ce-4bfe-9e2b-ffd8e97de9b1&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def format_time(t):
    m, s = divmod(t, 60)
    h, m = divmod(m, 60)
    return f'{h:02.0f}:{m:02.0f}:{s:02.0f}'</code></pre></div><p>This cell defines a small helper for turning a time value into a human-readable clock-style string. It takes a number of seconds and breaks it down into hours, minutes, and remaining seconds using repeated division and remainder operations. The first split separates total seconds into minutes and seconds, and the second split separates those minutes into hours and leftover minutes. The result is then formatted so each part always appears with two digits, which makes timings easier to read and compare later on. Since the cell only creates the function and does not call it, there is no saved output yet; it simply prepares a utility that can be reused whenever the notebook wants to display elapsed time in a neat format.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;db8d9d30-db1d-41b8-a1d6-3c646c26993a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">deciles = np.arange(.1, 1, .1).round(1)</code></pre></div><p>The cell creates a simple NumPy array of decile cut points, starting at 0.1 and increasing by 0.1 up to 0.9. Rounding to one decimal place keeps the values neat and avoids tiny floating-point artifacts that can sometimes appear when generating evenly spaced numbers. The result is not displayed because the cell only defines a variable for later use, likely to support descriptive statistics or percentile-based summaries elsewhere in the notebook.</p><h2>Retrieve stock price history</h2><h3>File locations</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;65fcfa48-c24d-48f4-a66c-6704bf872b40&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">data_path = Path('..', 'data', 'sec-filings')</code></pre></div><p>This line sets up a path object pointing to the folder that contains the SEC filing data. The notebook uses that folder as the base location for later file reads and writes, so defining it once here makes the rest of the workflow easier to manage and less error-prone. Instead of hard-coding the same long directory string repeatedly, the path is stored in a reusable variable that can be combined with other folder or file names later on. Since nothing is printed or displayed, there is no visible output from the cell; its purpose is simply to prepare a reference to the data directory for later steps.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;b537b975-f50f-4109-abe9-88b7955fecc8&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">results_path = Path('results', 'sec-filings')

selected_section_path = results_path / 'ngrams_1'
ngram_path = results_path / 'ngrams'
vector_path = results_path / 'vectors'

for path in [vector_path, selected_section_path, ngram_path]:
    if not path.exists():
        path.mkdir(parents=True)</code></pre></div><p>The cell is setting up a few output folders that later steps in the workflow will use to store processed text and model inputs. It first defines a base results directory for the SEC filings project, then builds three more paths underneath it: one for the cleaned one-gram text, one for the n-gram versions of the text, and one for the integer vector files. After that, it checks each of those folders one by one and creates any that are missing, including parent directories if needed. That means the notebook can safely save intermediate files later without failing just because a directory was never created before. Since nothing is printed or returned, there is no visible output from the cell; its effect is purely to prepare the file system for the preprocessing pipeline that follows.</p><h3>Retrieve filing details</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;9ffc9817-8bcd-4d33-8bf6-23fb010d42ab&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">filing_index = (pd.read_csv(data_path / 'filing_index.csv',
                            parse_dates=['DATE_FILED'])
                .rename(columns=str.lower))
filing_index.index += 1</code></pre></div><p>The goal here is to load the master filing index that will act as the lookup table for everything else in the workflow. It reads the filing<em>index.csv file from the data directory, and while doing so it tells pandas to treat the DATE</em>FILED column as a real date rather than plain text. That matters because later steps need to compare filing dates with stock-price windows, and date-aware values make those calculations much easier and less error-prone.</p><p>After the file is loaded, the column names are converted to lowercase. That is mostly a housekeeping step, but it helps keep the dataset consistent and avoids problems later if other parts of the notebook refer to columns in lowercase form. The final line shifts the DataFrame index so it starts at 1 instead of 0. That does not change the actual filing records, but it makes the row labels line up better with the filing IDs used elsewhere in the project, which is useful when those IDs are treated as one-based identifiers.</p><p>There is no saved output from this cell because it only prepares the filing_index table in memory. Its effect is to make a cleaned, date-parsed version of the index available for the next stages of the notebook.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;a914bb5d-c33d-4fac-9470-ef6a15464002&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">filing_index.info()</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;6dbf72b8-dbb3-485a-a25a-a930ce3c7438&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">&lt;class 'pandas.core.frame.DataFrame'&gt;
RangeIndex: 22631 entries, 1 to 22631
Data columns (total 11 columns):
 #   Column        Non-Null Count  Dtype         
---  ------        --------------  -----         
 0   cik           22631 non-null  int64         
 1   company_name  22631 non-null  object        
 2   form_type     22631 non-null  object        
 3   date_filed    22631 non-null  datetime64[ns]
 4   edgar_link    22631 non-null  object        
 5   quarter       22631 non-null  int64         
 6   ticker        22631 non-null  object        
 7   sic           22461 non-null  object        
 8   exchange      20619 non-null  object        
 9   hits          22555 non-null  object        
 10  year          22631 non-null  int64         
dtypes: datetime64[ns](1), int64(3), object(7)
memory usage: 1.9+ MB</code></pre></div><p>The purpose here is to inspect the structure of the filing metadata table before using it for the rest of the workflow. A quick summary of the DataFrame helps confirm that the data loaded correctly, shows how many filing records are available, and reveals whether any columns have missing values or unexpected data types.</p><p>The output reports that the table contains 22,631 rows and 11 columns, so each row represents one filing record. Most of the fields are present for every record, but a few columns have some missing entries: the SIC industry code is missing for a small number of rows, the exchange field has more gaps, and the hits field also has a few missing values. That kind of information is important because it tells you which fields can be trusted as complete and which may need caution later.</p><p>The data types in the output also show that the table is already in a useful form. The filing date has been parsed as a datetime column, which means it can be used for time-based filtering and alignment with market data. The CIK, quarter, and year fields are stored as integers, while the rest are text-based object columns such as company name, form type, ticker, and EDGAR link. The memory usage line gives a sense of the table size in memory, which is modest enough for exploratory analysis and further processing.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;0400b04d-3282-42aa-aa4b-dd4d5a8194e3&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">filing_index.head()</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;da168336-78eb-4922-8354-237248d8f077&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">       cik                  company_name form_type date_filed  \
1  1000180                  SANDISK CORP      10-K 2013-02-19   
2  1000209      MEDALLION FINANCIAL CORP      10-K 2013-03-13   
3  1000228              HENRY SCHEIN INC      10-K 2013-02-13   
4  1000229         CORE LABORATORIES N V      10-K 2013-02-19   
5  1000232  KENTUCKY BANCSHARES INC  KY       10-K 2013-03-28   

                                    edgar_link  quarter ticker   sic exchange  \
1  edgar/data/1000180/0001000180-13-000009.txt        1   SNDK  3572   NASDAQ   
2  edgar/data/1000209/0001193125-13-103504.txt        1   TAXI  6199   NASDAQ   
3  edgar/data/1000228/0001000228-13-000010.txt        1   HSIC  5047   NASDAQ   
4  edgar/data/1000229/0001000229-13-000009.txt        1    CLB  1389     NYSE   
5  edgar/data/1000232/0001104659-13-025094.txt        1   KTYB  6022      OTC   

  hits  year  
1    3  2013  
2    0  2013  
3    3  2013  
4    2  2013  
5    0  2013</code></pre></div><p>The purpose here is to take a quick first look at the filing metadata table and make sure it has loaded the way expected. Showing the first few rows is a simple sanity check: it confirms that the dataset contains one row per filing, and it lets you inspect the main identifying columns before moving on to any matching with stock prices or text processing.</p><p>The displayed result shows the top entries of the table, which is why only five filings appear. Each row includes a company identifier, the company name, the filing form type, the date the filing was submitted, a link to the SEC filing text, the fiscal quarter, ticker symbol, industry code, exchange, a hits count, and the year. Seeing values like the filing date and ticker side by side is especially useful because those are the fields that later let the workflow connect a specific SEC filing to market data for the same company and time period.</p><p>The output also tells you something about the structure and cleanliness of the dataset. The index starts at 1 rather than 0, the dates are already in a readable form, and the table includes both SEC-specific identifiers and market-oriented identifiers like ticker and exchange. The repeated appearance of 10-K filings in these early rows suggests that the dataset is focused on annual reports, which makes sense because those filings are rich in narrative content and are often used for later return prediction.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;3ac00786-f721-4fab-b292-527ce0c2e079&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">filing_index.ticker.nunique()</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;035832f7-a36b-4ac1-af90-2255b6469475&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">6630</code></pre></div><p>The goal here is to check how many distinct stock tickers appear in the filing index. By looking at the ticker column and counting only the unique values, the cell gives a quick sense of the breadth of the dataset rather than the total number of filings. The saved result, 6630, means there are 6,630 different companies or symbols represented in the filing records. That number is useful because it tells us the corpus is spread across a fairly large and diverse set of tickers, which matters later when the filings are matched up with market data and used for modeling.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;16c5514a-e774-4465-a86e-657cbceef131&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">filing_index.date_filed.describe()</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;2bb7de46-e8f6-430e-95d7-5ff20e86301e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">count                   22631
unique                    980
top       2014-03-31 00:00:00
freq                      442
first     2013-01-02 00:00:00
last      2016-12-30 00:00:00
Name: date_filed, dtype: object</code></pre></div><p>The purpose here is to get a quick summary of the filing dates in the dataset, so you can see how much time the records cover and whether the dates are spread out or clustered. The date_filed column is treated like a standard descriptive series, and the summary statistics are calculated automatically for it.</p><p>The output shows that there are 22,631 filing date entries in total. Among those, 980 dates are distinct, which means many filings share the same filing day. The most common date is 2014-03-31 00:00:00, and it appears 442 times, so that day had an especially large number of filings. The first and last values show the span of the dataset, running from 2013-01-02 00:00:00 through 2016-12-30 00:00:00. Behind the scenes, because this is a date-like field rather than a numeric one, the summary focuses on counts and date-related extremes instead of averages or quartiles.</p><h3>Retrieve stock price history with yfinance</h3><p><code>yfinance</code> may occasionally fail when the connection is interrupted. If that happens, it is a good idea to save intermediate outputs so you can resume without repeating the entire process.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;5b75d45e-1c0a-448b-a362-ce095ec95b60&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">yf_data, missing = [], []
for i, (symbol, dates) in enumerate(filing_index.groupby('ticker').date_filed, 1):
    
    if i % 250 == 0:
        print(i, len(yf_data), len(set(missing)), flush=True)
    
    ticker = yf.Ticker(symbol)
    for filing, date in dates.to_dict().items():
        start = date - timedelta(days=93)
        end = date + timedelta(days=31)
        df = ticker.history(start=start, end=end)
        if df.empty:
            missing.append(symbol)
        else:
            yf_data.append(df.assign(ticker=symbol, filing=filing))</code></pre></div><p>The cell is gathering historical stock prices for each filing so later the text data can be paired with market reactions around the filing date. It starts by creating two empty containers: one for the price histories that are successfully downloaded, and another for ticker symbols where the download comes back empty. It then groups the filing index by ticker, so each symbol is handled once and all of its filing dates are processed together.</p><p>As the loop runs, it keeps track of progress with a small status print every 250 tickers. That message would show how far the download has gotten, how many price records have been collected so far, and how many distinct tickers have failed. For each ticker, it creates a Yahoo Finance object and then loops through that ticker&#8217;s filing dates. Around each filing date, it defines a window that begins 93 days before the filing and ends 31 days after it. That wider window gives enough trading history to measure performance before and after the filing event.</p><p>For each filing date, it asks Yahoo Finance for the price history in that date range. If no rows come back, the ticker is recorded as missing so it can be handled later through another data source. If rows do come back, the price table is stored after adding two identifying pieces of information: the ticker symbol and the filing ID. Those extra columns are important because they let the price rows be matched back to the exact filing that triggered the download. There is no saved output for this cell because it mainly performs data collection and only prints progress occasionally; if those progress points were not reached during execution, nothing would be displayed.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;49c642e8-c7f0-4976-a606-a95e6f12f9b5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">yf_data = pd.concat(yf_data).rename(columns=str.lower)</code></pre></div><p>This step takes the collection of Yahoo Finance results gathered earlier and stacks them into one continuous table. Each piece in the list is a separate price data frame, likely coming from a different filing or ticker window, and concatenating them makes it possible to treat all of those records as one dataset instead of many small ones. Renaming the columns to lowercase at the same time standardizes the table so later processing can refer to fields like open, high, low, close, and volume consistently without worrying about capitalization differences. Nothing is displayed here because the operation simply reshapes and cleans the data in memory, preparing it for the next stages where the price information will be saved, filtered, and matched to filings.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;6780de45-9770-4d46-9190-e55a29894fee&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">yf_data.to_hdf(results_path / 'sec_returns.h5', 'data/yfinance')</code></pre></div><p>This step saves the Yahoo Finance price data that has already been gathered and cleaned into an HDF5 file inside the results folder. HDF5 is a compact storage format that is useful when working with tabular financial data because it keeps large datasets organized and makes them easy to load back later without repeating the download process. The data is written under the key data/yfinance, which means it can be retrieved later from the same file as a named dataset rather than as a loose CSV. Since the cell only performs a save operation, there is no visible output; the result is the creation or update of the sec_returns.h5 file on disk.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;8b9f752c-a362-475d-9c7c-c5aae1e85632&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">yf_data = pd.read_hdf(results_path / 'sec_returns.h5', 'data/yfinance')</code></pre></div><p>The purpose here is to load the stock price data that was previously saved after being collected from Yahoo Finance. The file being read is an HDF5 store, which is a convenient format for keeping a large table on disk and bringing it back into memory later without having to download or rebuild it again. The specific key being requested, <code>data/yfinance</code>, points to the Yahoo Finance portion of that saved dataset.</p><p>Once this line runs, the table is pulled into a pandas DataFrame and assigned to <code>yf_data</code>. From that point on, the notebook can use this object as the working copy of the Yahoo-derived price history for the filing-related analysis. There is no visible output because the line is only loading data, not printing it or displaying it, so the result is simply that the variable becomes available for the next steps.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;38f851c0-5f01-47aa-b45e-df3966d90ecf&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">yf_data.ticker.nunique()</code></pre></div><p>The purpose here is to get a quick count of how many distinct stock tickers are represented in the downloaded price data. The expression looks at the ticker column in the table of Yahoo Finance data and asks for the number of unique values, which is a simple way to measure how many different companies are covered in that dataset. Behind the scenes, the data structure scans the ticker entries, identifies repeated symbols, and collapses them down to a single count. Since this is just a summary query and not a display or print operation, there is no saved output shown, even though the calculation itself still helps confirm the breadth of the data before moving on to later filtering and modeling steps.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;b9620936-b4b7-4092-ab40-a49ec10c5fe8&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">yf_data.info()</code></pre></div><p>The purpose here is to inspect the structure of the Yahoo Finance data table after it has been loaded and cleaned. Calling the information summary on the dataframe gives a quick snapshot of how many rows and columns are present, what each column is named, what type of data each column holds, and how much missing data there may be. That makes it a useful checkpoint before moving on to later steps, because it helps confirm that the market data was imported in the expected shape and that the key fields needed for analysis are available. Since there is no saved output shown, the cell is functioning as a diagnostic check rather than producing a new table or visualization.</p><h3>Recover some of the missing price history from Quandl</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;5fd714bc-885f-4f4d-b5d8-628ffeb3e513&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">to_do = (filing_index.loc[~filing_index.ticker.isin(yf_data.ticker.unique()), 
                          ['ticker', 'date_filed']])</code></pre></div><p>The cell is creating a small lookup table of filings whose tickers were not found in the Yahoo Finance price data that had already been downloaded. It starts from the filing index, checks each ticker against the list of unique tickers present in the Yahoo Finance dataset, and keeps only the rows where the ticker is missing from that set. From those filtered rows, it extracts just the ticker symbol and the filing date, since those are the two pieces of information needed to try another source for the price history later on.</p><p>Behind the scenes, the filter is using a logical test that marks each filing as either present or absent in the Yahoo Finance results. The tilde in front of that test flips it, so only the missing tickers are selected. The result is stored in a new table called to_do, which is essentially a work list for the next step of the pipeline. Since the cell only builds and assigns this table without printing it, there is no saved output.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;6e766513-0b33-4896-960e-252ec8c53cb8&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">to_do.date_filed.min()</code></pre></div><p>This line checks the earliest filing date in the dataset by looking at the date<em>filed column and asking for its minimum value. Since date</em>filed is a column of dates, taking the minimum returns the first or oldest filing date available among all the records. Nothing is displayed here because the result was not saved as output, but if it were, it would be a single date value rather than a table or chart. This kind of quick check is often used to understand the time span covered by the filings before doing more detailed analysis.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;06bd3371-6cab-400d-a746-0c18cfea1415&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">quandl_tickers = (pd.read_hdf('../data/assets.h5', 'quandl/wiki/prices')
                  .loc[idx['2012':, :], :]
                  .index.unique('ticker'))
quandl_tickers = list(set(quandl_tickers).intersection(set(to_do.ticker)))</code></pre></div><p>The purpose here is to identify which stock tickers still have usable price data available from the local Quandl store after 2012, and then narrow that list down to only the tickers that actually still need to be processed. It starts by reading the Quandl price table out of the HDF5 file and selecting only the rows from 2012 onward. From that filtered data, it pulls out the unique ticker symbols, so the result is a list of companies for which historical pricing exists in that store during the relevant time period.</p><p>Next, that list is compared with the tickers in the to-do set. Taking the intersection leaves only the symbols that appear in both places: tickers that are present in the Quandl data and also still require work in the pipeline. Nothing is printed because the cell is just building an intermediate list for later use, not displaying it. The end result is a smaller set of candidate tickers that can be used in the next step when the notebook fills in missing price histories.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;f40f1b7b-89a6-4d9f-a247-1d333c2f78fd&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">len(quandl_tickers)</code></pre></div><p>This cell is simply checking how many unique ticker symbols are present in the Quandl-based price data that was collected to fill gaps in the Yahoo Finance data. By asking for the length of that ticker list, it gives a quick count of how many distinct stocks are represented in this fallback dataset. Since the result is just a single number and the notebook does not show a saved output here, the cell is being used as a small inspection step rather than something that creates a file or table.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;74a55acc-389a-40f7-a4a5-7f08ebbde16d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">to_do = filing_index.loc[filing_index.ticker.isin(quandl_tickers), ['ticker', 'date_filed']]</code></pre></div><p>The purpose here is to narrow the filing index down to just the records whose ticker symbols are present in the list of stocks available from Quandl. It looks through the full filing index, checks which rows have a ticker that appears in the Quandl ticker set, and then keeps only the ticker and filing date columns for those matching rows. The result is a smaller table of filing-date pairs that can be used later when filling in missing price history from the Quandl source. Because nothing is displayed or printed, there is no visible output from the cell; it simply prepares this filtered subset in memory for the next steps of the workflow.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;a2ecb355-80b8-4151-bc34-1cd90d635fdd&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">to_do.info()</code></pre></div><p>The purpose of this step is to inspect the task list stored in the object named to<em>do. Calling the information summary for it is a quick way to check what kind of data structure it is, how many entries it contains, whether any values are missing, and what the basic types look like. That kind of snapshot is useful before doing any further processing, because it confirms that the object was created correctly and gives a first sense of its shape and contents. Since there is no saved output here, the cell is simply preparing the notebook for the next stage by letting the user see the internal structure of to</em>do when it is executed.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;e11ee035-637d-46d0-8160-4f87db3ca754&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">ohlcv = ['adj_open', 'adj_high', 'adj_low', 'adj_close', 'adj_volume']</code></pre></div><p>This line sets up a simple list of column names for adjusted market data: adjusted open, high, low, close, and volume. It serves as a small reference object that can be reused later whenever the notebook needs to work with a standardized set of price fields. Using adjusted values is important because they account for corporate actions like splits and dividends, which makes the time series more suitable for analysis and return calculations. Since the cell only defines a Python list, there is no printed output or visible result at execution time; it just prepares a convenient label collection for later steps.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;57c474d1-b7c5-4dd0-a594-0014f3934880&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">quandl = (pd.read_hdf('../data/assets.h5', 'quandl/wiki/prices')
          .loc[idx['2012': , quandl_tickers], ohlcv]
          .rename(columns=lambda x: x.replace('adj_', '')))</code></pre></div><p>The purpose here is to pull in a clean slice of the local Quandl price dataset so it can be used as a backup source of stock history. The data is read from an HDF5 file that already stores a large panel of price records, and then it is narrowed down to only the rows from 2012 onward and only the tickers that are needed later in the analysis. The selection also keeps just the standard price and volume columns, so the table is trimmed to the exact market fields that matter for modeling.</p><p>After that, the column names are simplified by removing the adjusted-price prefix. That means fields such as adjusted open, adjusted high, adjusted low, and adjusted close are converted to plain open, high, low, and close names. The result is a more convenient dataset with adjusted historical prices, but with labels that match the rest of the workflow. Since nothing is printed or displayed, there is no saved output; the cell simply prepares this cleaned Quandl table in memory for later use.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;5886a6c8-3375-4594-b9b1-d869dfa68773&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">quandl.info()</code></pre></div><p>This cell is a quick check on the Quandl data store. When it runs, it asks pandas to show the structure and summary of the <code>quandl</code> object, which is usually a DataFrame loaded from the local price history file. The result would normally tell you how many rows and columns are available, what the column names are, how many non-missing values each column contains, and the data types for each field. That kind of summary is useful here because the notebook needs to know whether the backup price dataset actually contains the stock history needed to fill in gaps from the Yahoo Finance download. Since there is no saved output, the cell is just performing that inspection at runtime rather than displaying a recorded result.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;0f73246b-d6ff-44c8-9764-a829a6faaccc&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">quandl_data = []
for i, (symbol, dates) in enumerate(to_do.groupby('ticker').date_filed, 1):
    if i % 100 == 0:
        print(i, end=' ', flush=True)
    for filing, date in dates.to_dict().items():
        start = date - timedelta(days=93)
        end = date + timedelta(days=31)
        quandl_data.append(quandl.loc[idx[start:end, symbol], :].reset_index('ticker').assign(filing=filing))
quandl_data = pd.concat(quandl_data)</code></pre></div><p>The purpose here is to collect historical price data from the local Quandl store for the filings that still need coverage. It starts by creating an empty list that will hold one slice of price history for each filing. The loop then groups the remaining filing records by ticker symbol, so all filings for the same stock are handled together instead of repeating work unnecessarily. As it moves through those groups, it prints a progress number every 100 symbols, which is just a simple way to show that the process is still running during what can be a fairly long data-gathering step.</p><p>For each ticker, the inner loop goes through that ticker&#8217;s filing dates one by one. Around each filing date, it defines a window that stretches 93 days before the filing and 31 days after it. That window is wide enough to capture both the pre-filing history and the post-filing reaction period that will later be used in the analysis. The code then pulls the matching rows from the Quandl price table for that exact ticker and date range, removes the ticker level from the index so the result is easier to work with, and adds a filing ID column so the price rows can be tied back to the correct filing. Each of these extracted slices is appended to the list.</p><p>At the end, all of those individual slices are combined into one large table. Since the cell is only building and assembling data rather than displaying anything, there is no saved output. The important result is the <code>quandl_data</code> dataframe, which now contains filing-centered price windows ready to be merged with the rest of the price data.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;1c78c1f6-a611-4ab2-8053-ed125867ba24&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">quandl_data.to_hdf(results_path / 'sec_returns.h5', 'data/quandl')</code></pre></div><p>This step saves the Quandl-derived market data into an HDF5 file so it can be reused later without having to reload or recompute it. The data is written under the results directory in a file named sec_returns.h5, and it is stored with the key data/quandl so it can be retrieved as a named table from the same file. Behind the scenes, HDF5 acts like a structured container for large datasets, which is useful here because the price history can be sizable and may need to be accessed repeatedly in later steps. There is no displayed output because the operation is simply writing the data to disk, so the cell completes quietly once the save finishes.</p><h3>Merge, sanitize, and save results</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;c4f8ea98-e7d2-4741-919d-deee88f8bd2c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">data = (pd.read_hdf(results_path / 'sec_returns.h5', 'data/yfinance')
        .drop(['dividends', 'stock splits'], axis=1)
        .append(pd.read_hdf(results_path / 'sec_returns.h5',
                            'data/quandl')))</code></pre></div><p>The goal here is to gather the stock-price data into one combined table so the later modeling steps can work from a single source instead of having to treat Yahoo Finance and Quandl separately. It starts by loading the Yahoo Finance portion from an HDF5 file, specifically the stored table under the yfinance key. Once that table is in memory, the two columns that are not needed for the return modeling task, dividends and stock splits, are removed. That leaves only the core price and volume information that will be useful for aligning prices with filing dates and computing returns.</p><p>After cleaning the Yahoo Finance records, the cell brings in the Quandl-based data from the same HDF5 file and appends it underneath the Yahoo data. The result is one larger dataframe named data that merges both price sources into a single dataset. Since this step only prepares and combines data without printing anything, there is no visible output. The effect is still important, though, because it creates the unified price table that the rest of the analysis can rely on.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;92c3e8bb-7c76-4f9c-823f-380be7229efd&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">data = data.loc[:, ['filing', 'ticker', 'open', 'high', 'low', 'close', 'volume']]</code></pre></div><p>The purpose here is to narrow the price dataset down to just the columns needed for the later modeling steps. From the larger market-data table, only the filing identifier, ticker symbol, and the core trading fields are kept: open, high, low, close, and volume. Everything else is dropped so the table becomes simpler and easier to work with.</p><p>Behind the scenes, this is a column selection operation that preserves the existing rows but trims away any extra information that is not part of the downstream return calculations or sequence alignment. Keeping the filing and ticker columns is important because they link each price record back to the correct document and symbol, while the OHLCV fields are the standard inputs used for financial analysis. Since this is just a filtering step, there is no printed output; the effect is that the <code>data</code> table is immediately replaced with a cleaner version containing only those seven columns.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;06e8142a-b08b-449b-aa9a-7331ab84770b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">data.info()</code></pre></div><p>This cell is a quick structural check of the current data table. It asks pandas to summarize the contents of the DataFrame so you can see how many rows there are, what columns are present, what data type each column uses, and how many non-missing values each one contains. That kind of summary is useful before moving further in the workflow because it confirms whether the dataset loaded correctly and whether any important fields have missing data or unexpected types.</p><p>Since there is no saved output here, the cell is simply preparing you to inspect the table&#8217;s shape and cleanliness at runtime. When executed in a notebook, it would display the DataFrame summary directly in the output area, giving an at-a-glance view of the dataset&#8217;s structure.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;99b6ffd7-214e-4dd5-ac6a-ce1decf9a5cf&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">data[['filing', 'ticker']].nunique()</code></pre></div><p>The purpose here is to get a quick sense of how many distinct filings and ticker symbols are present in the data frame. By selecting just the filing and ticker columns and asking for the number of unique values in each one, the cell is checking the variety of the dataset along these two key dimensions. Behind the scenes, pandas scans each column separately and counts how many different entries appear, ignoring repeated rows. Since there is no saved output shown, the result would normally be a small summary telling you the unique count for filing IDs and the unique count for tickers, which helps confirm the dataset&#8217;s breadth before moving on to later analysis.</p>
      <p>
          <a href="https://onepagecode.substack.com/p/recurrent-neural-networks-for-quant">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Deep Learning for Quant Trading: Training Neural Networks from Scratch]]></title><description><![CDATA[A step-by-step implementation of feedforward networks, backpropagation, and optimization using NumPy]]></description><link>https://onepagecode.substack.com/p/deep-learning-for-quant-trading-training</link><guid isPermaLink="false">https://onepagecode.substack.com/p/deep-learning-for-quant-trading-training</guid><dc:creator><![CDATA[Onepagecode]]></dc:creator><pubDate>Wed, 24 Jun 2026 20:01:40 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!Js6_!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4690458a-ec1e-48c3-a95f-68f78fbdda4e_335x323.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h4>Use the URL at the end of this to download the source code!</h4><p>Please complete the setup by following the installation instructions in the linked document.</p><h2>Imports and Settings</h2><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;58c7d92d-7171-4022-b16b-cdb19dd09285&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">import warnings
warnings.filterwarnings('ignore')</code></pre></div><p>The purpose here is simply to silence warning messages so the rest of the notebook runs more cleanly. The warnings module is imported first, which gives access to Python&#8217;s built-in warning system, and then a filter is set to ignore warnings. After that line runs, warning messages that would normally appear in the output are suppressed, which helps keep the notebook focused on the main results rather than on repeated notices or minor alerts from libraries. Since nothing is being printed or displayed, there is no saved output for this cell.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;135bdb59-315e-44d1-8d92-75c9d3c3b83b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">%matplotlib inline

from pathlib import Path
from copy import deepcopy

import numpy as np
import pandas as pd

import sklearn
from sklearn.datasets import make_circles

import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap
from mpl_toolkits.mplot3d import Axes3D  # 3D plots
import seaborn as sns</code></pre></div><p>The purpose of this cell is to gather the tools needed for the rest of the notebook and to make sure plots appear directly inside the notebook. The matplotlib inline setting tells Jupyter to render figures beneath the cells instead of opening them in a separate window, which is important because later cells will create several visualizations.</p><p>After that, the cell imports a few standard utilities. Path is used for working with file locations, such as creating or saving to a results folder. deepcopy is brought in so objects can be copied safely without sharing the same underlying data, which is especially useful when checking gradients or experimenting with parameters. NumPy and pandas are imported for numerical work and data handling, while sklearn provides the synthetic two-circle dataset used in the example. Matplotlib supplies the plotting functions, and the ListedColormap and 3D plotting tools are prepared for drawing colored decision regions and three-dimensional surfaces. Seaborn is included for cleaner plot styling.</p><p>There is no saved output because this cell only sets up the environment. Its effect is felt later, when the notebook can generate data, compute with arrays, and display the training and decision-boundary plots without needing any additional setup.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;1532c1d6-ce68-4328-9fb9-e4462e736a39&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># plotting style
sns.set_style('white')
# for reproducibility
np.random.seed(seed=42)</code></pre></div><p>The purpose of this cell is to set up a consistent visual and random environment before the rest of the notebook continues. First, the plotting style is switched to a clean white background, which affects how future figures will look. That choice makes charts easier to read because the grid and axes stand out clearly without extra visual noise. Next, the random number generator is seeded with a fixed value, which is important whenever later steps involve randomness, such as initializing network weights or generating any random samples. By locking the seed to the same value every time, the notebook produces the same results on repeated runs, making the experiments reproducible and easier to debug.</p><p>There is no saved output because nothing is meant to be displayed here. The cell quietly changes settings behind the scenes so that subsequent plots follow the chosen style and later random operations behave predictably.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;0f113259-d928-4078-b63e-3a998a15f8d5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">results_path = Path('results')
if not results_path.exists():
    results_path.mkdir()</code></pre></div><p>The cell makes sure there is a folder named results available for anything the notebook wants to save later, such as figures or other generated files. It first creates a Path object pointing to that directory, then checks whether the folder already exists. If it does not, the folder is created on disk. This is a small setup step, but it matters because later parts of the notebook can safely write output into the same place without needing to repeat this check or worry about the directory being missing. Since the cell only prepares the filesystem and does not print anything or display anything, there is no visible output when it runs.</p><h2>Data Provided to the Network</h2><h3>Create synthetic data</h3><p>The target variable <code>y</code> contains two classes produced from concentric circular patterns, and the classes cannot be separated with a straight line because class 0 encloses class 1.</p><p>We create 50,000 random points arranged as two circles that share the same center but have different radii by using scikit-learn&#8217;s make_circles function, which gives us a classification problem that cannot be separated with a straight line.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;3feecd9a-0e16-4a17-a10d-c63c597e2dc0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># dataset params
N = 50000
factor = 0.1
noise = 0.1</code></pre></div><p>This cell sets the basic parameters that will be used to generate the synthetic classification data later on. The value of N establishes how many examples will be created in the dataset, and here it is set to a fairly large number so the network will have plenty of points to learn from. The factor parameter controls how close the two circular classes are to each other, which affects how tightly the circles are nested. The noise parameter determines how much randomness is added to the points, making the task more realistic and slightly harder by blurring the boundaries between the classes. Since nothing is calculated or displayed yet, there is no output here; the cell simply stores these settings so the next steps can use them when building the data.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;c8f16004-a747-40ff-88ca-4be8b3b97b0d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">n_iterations = 50000
learning_rate = 0.0001
momentum_factor = .5</code></pre></div><p>This cell sets the main training hyperparameters that will be used later when the network is optimized. The first value determines how many times the model will go through the full training loop, so a large number of iterations gives the optimizer many chances to gradually improve the weights. The next value is the learning rate, which controls how large each parameter update will be after the gradients are computed. Here it is set very small, so the model will move in cautious steps rather than making big jumps that might overshoot a good solution. The final value is the momentum factor, which tells the optimizer how much of the previous update direction to carry forward into the next step. With a value of 0.5, the training process keeps some memory of recent progress, which can help smooth out the path through the loss surface and make learning more stable.</p><p>Nothing is printed or plotted here, so there is no visible output. The purpose of the cell is simply to store these settings so that the training function can use them consistently when the network is trained in the next steps.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;099ceb64-6a83-4b28-8dcb-a60ddb06e393&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># generate data
X, y = make_circles(
    n_samples=N,
    shuffle=True,
    factor=factor,
    noise=noise)</code></pre></div><p>The purpose here is to create the synthetic dataset that the neural network will later learn from. The data is generated with a standard toy problem made of two concentric circles, which is useful because the two classes are not separable by a straight line. That makes it a good example for showing why a hidden layer is needed in the first place.</p><p>The generation step asks for the full sample size already defined earlier, then shuffles the points so they are mixed together rather than ordered by class. The factor controls how far apart the inner circle is from the outer one, and the noise parameter adds randomness so the points are not perfectly arranged. A little noise makes the task more realistic and prevents the model from seeing an artificially clean pattern.</p><p>When this cell finishes, it stores the feature matrix in X and the class labels in y. X contains the two-dimensional coordinates for each point, while y tells which circle each point belongs to. There is no visible output because the cell is just preparing data in memory rather than printing anything or drawing a figure. The real effect of the cell shows up later when these points are plotted and fed into the network for training.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;9de81fd9-5773-4361-8d59-c52fc4d11faf&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># define outcome matrix
Y = np.zeros((N, 2))
for c in [0, 1]:
    Y[y == c, c] = 1</code></pre></div><p>The purpose here is to convert the class labels into a one-hot encoded outcome matrix that the neural network can use during training. Instead of storing each label as a single number like 0 or 1, the labels are expanded into two-column rows, where the correct class is marked with a 1 and the other class is marked with a 0. So a sample from class 0 becomes [1, 0], and a sample from class 1 becomes [0, 1].</p><p>The first line creates an array of zeros with one row for every sample and two columns, matching the two classes in the problem. The loop then goes through each class value, finds all samples whose label matches that class, and places a 1 in the corresponding column for those rows. Behind the scenes, this is a clean way to build the target matrix expected by the softmax and cross-entropy setup used later in training.</p><p>There is no saved output because nothing is printed or plotted, but the important result is that Y is now ready to serve as the network&#8217;s training target in a format that lines up with the two output neurons.</p><p>The input matrix <code>X</code> contains the two features for every sample, arranged row by row across <code>N</code> observations. The label matrix <code>Y</code> stores the corresponding one-hot encoded targets, also with one row per sample and two columns for the two classes.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;32fda1d1-1b52-40a2-824e-e90cd5dd565c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">f'Shape of: X: {X.shape} | Y: {Y.shape} | y: {y.shape}'</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;ae04750e-e31d-4fb3-9861-82a7826f2b2d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">'Shape of: X: (50000, 2) | Y: (50000, 2) | y: (50000,)'</code></pre></div><p>A quick shape check is being produced here to confirm that the dataset and its labels are organized exactly as expected before moving on. The expression builds a readable message that reports the dimensions of the feature matrix X, the one-hot encoded label matrix Y, and the original label vector y. The saved output shows X as having 50,000 rows and 2 columns, which means there are 50,000 data points and each one has two input features. Y also has 50,000 rows, but now with 2 columns, reflecting the one-hot encoding used for the two classes. The original y array has shape 50,000, since it stores a single class label for each sample. Seeing these shapes line up like this is important because it confirms that the inputs and targets are aligned correctly for the neural network training that follows.</p><h3>Plot the dataset</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;2586fc74-aa9f-43f8-a2cc-07dfc82f27ae&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">ax = sns.scatterplot(x=X[:, 0], 
                y=X[:, 1], 
                hue=y,
               style=y,
               markers=['_', '+'])
ax.set_title('Synthetic Classification Data')
sns.despine()
plt.tight_layout()
plt.savefig(results_path / 'ffnn_data', dpi=300);</code></pre></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!NrnK!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7e3431e-22ff-4dd9-b5a8-dcdad1b6518d_424x280.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!NrnK!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7e3431e-22ff-4dd9-b5a8-dcdad1b6518d_424x280.png 424w, https://substackcdn.com/image/fetch/$s_!NrnK!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7e3431e-22ff-4dd9-b5a8-dcdad1b6518d_424x280.png 848w, https://substackcdn.com/image/fetch/$s_!NrnK!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7e3431e-22ff-4dd9-b5a8-dcdad1b6518d_424x280.png 1272w, https://substackcdn.com/image/fetch/$s_!NrnK!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7e3431e-22ff-4dd9-b5a8-dcdad1b6518d_424x280.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!NrnK!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7e3431e-22ff-4dd9-b5a8-dcdad1b6518d_424x280.png" width="424" height="280" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f7e3431e-22ff-4dd9-b5a8-dcdad1b6518d_424x280.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:280,&quot;width&quot;:424,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:47315,&quot;alt&quot;:&quot;Output image&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/202847809?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7e3431e-22ff-4dd9-b5a8-dcdad1b6518d_424x280.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Output image" title="Output image" srcset="https://substackcdn.com/image/fetch/$s_!NrnK!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7e3431e-22ff-4dd9-b5a8-dcdad1b6518d_424x280.png 424w, https://substackcdn.com/image/fetch/$s_!NrnK!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7e3431e-22ff-4dd9-b5a8-dcdad1b6518d_424x280.png 848w, https://substackcdn.com/image/fetch/$s_!NrnK!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7e3431e-22ff-4dd9-b5a8-dcdad1b6518d_424x280.png 1272w, https://substackcdn.com/image/fetch/$s_!NrnK!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7e3431e-22ff-4dd9-b5a8-dcdad1b6518d_424x280.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The purpose here is to turn the synthetic 2D dataset into a clear visual summary so the two classes can be inspected before any modeling begins. The x-coordinates and y-coordinates from the feature matrix are plotted against each other, and the class label is used twice: once to color the points and once to choose the marker style. That makes the two categories easy to distinguish visually, even though they occupy the same overall space. The marker choices are intentionally simple, so the main visual difference comes from the colors and the circular arrangement of the data.</p><p>After the points are drawn, the plot title is added to describe what is being shown. The axis styling is cleaned up by removing the top and right borders, which gives the figure a lighter, more presentation-friendly look. The layout is tightened so the labels and legend fit neatly without wasting space, and then the figure is saved to the results folder for later reuse.</p><p>The saved image shows exactly the kind of problem the network will need to solve: one class forms a dense inner cluster near the origin, while the other class wraps around it as a much larger outer ring. The separation is clearly not a straight line, which is why this dataset is useful for demonstrating a hidden layer. A linear classifier would struggle here, but a small neural network can learn a curved boundary that follows the shape of the circles.</p><h2>Neural Network Structure</h2><h3>Hidden Layer Responses</h3><p>The hidden layer, written as h, maps the two-dimensional input into a three-dimensional representation. For that reason, the hidden-layer weight matrix, W to the h power, has two rows and three columns, and the hidden-layer bias vector, b to the h power, contains three entries:</p><p>The hidden layer parameters are organized as a weight matrix with two input features and three hidden units, together with a bias vector for the three hidden units. The weight matrix contains the six hidden weights, arranged so that each row corresponds to one input feature and each column corresponds to one hidden neuron. The bias vector contains one bias term for each hidden unit.</p><p>The hidden layer scores, written as Z of h, are obtained by multiplying the input matrix X, which has N rows and 2 columns, by the hidden weight matrix W of h, which has 2 rows and 3 columns, and then adding the hidden bias vector b of h, which has 1 row and 3 columns:</p><p>The hidden-layer pre-activation matrix has one row per sample and three columns for the hidden units. It is obtained by multiplying the input matrix, which contains the two input features for each sample, by the hidden-layer weight matrix, and then adding the hidden-layer bias vector to every row.</p><p>The logistic sigmoid function transforms the hidden pre-activation values in <code>Z^h</code> through a non-linear mapping, producing the hidden-layer activations as a matrix with one row per sample and three hidden units in each row.</p><p>The hidden-layer output matrix, written as H, has one row per sample and three columns, so its shape is N by 3. It is obtained by applying the sigmoid activation to the result of multiplying the input matrix X by the hidden-layer weight matrix Wh and then adding the hidden-layer bias bh. In other words, each hidden unit receives the weighted input plus bias, and sigmoid turns that value into an activation between zero and one. The matrix can be viewed row by row as the activations for all N observations, with the first sample producing h11, h12, and h13, and the last sample producing hN1, hN2, and hN3.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;ed943552-6f53-4bf9-aa5c-7078aba00d03&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def logistic(z):
    """Logistic function."""
    return 1 / (1 + np.exp(-z))</code></pre></div><p>This cell defines the logistic, or sigmoid, activation function that the neural network uses in its hidden layer. It takes an input value or array called z and transforms each entry into a number between 0 and 1 by applying the standard sigmoid formula. Behind the scenes, values far below zero get pushed close to 0, values far above zero get pushed close to 1, and values near zero land around 0.5. That smooth squashing behavior is useful because it introduces nonlinearity, which is what allows the network to model the curved class boundary in the dataset instead of being limited to a straight line.</p><p>Nothing is displayed when the cell runs because it only creates a reusable function. Later cells can call this function whenever they need to turn raw weighted sums into hidden-layer activations.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;5b10c822-f2b5-4d15-97b3-aa6b50308076&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def hidden_layer(input_data, weights, bias):
    """Compute hidden activations"""
    return logistic(input_data @ weights + bias)</code></pre></div><p>This cell defines the function that computes the activations of the hidden layer in the neural network. It takes the incoming data, multiplies it by the hidden-layer weight matrix, adds the bias term, and then passes the result through the logistic sigmoid function. The matrix multiplication combines each input feature with each hidden unit&#8217;s weights to produce a pre-activation score for every hidden neuron, and the bias shifts those scores so the model is not forced to pass through the origin. Applying the sigmoid then squeezes those scores into values between 0 and 1, which gives the hidden layer its nonlinear behavior. That nonlinearity is what allows the network to transform the circular data into a representation that can later be separated more easily by the output layer. There is no printed result here because the cell only creates the function; it prepares a building block that the later forward pass and training steps will call whenever the hidden activations need to be computed.</p><h3>Output Layer Activations</h3><p>The output layer values, called Z for the output layer, form a matrix with N rows and 2 columns. They are obtained by multiplying the hidden-layer activation matrix H, which has N rows and 3 columns, by the output weight matrix W for the output layer, which has 3 rows and 2 columns, and then adding the output bias vector b for the output layer, which has 1 row and 2 columns:</p><p>The output layer forms the score matrix for all samples by multiplying the hidden layer activations, which have three columns, by the output weight matrix, which maps those three hidden units to two output classes, and then adding the output bias row to every sample.</p><p>The softmax function, written here as varsigma, converts the raw class scores into probabilities between 0 and 1 and makes sure they add up to 1 for each sample. The output is an N by 2 matrix, with one column assigned to each class label.</p><p>The model&#8217;s predicted output matrix for all N samples has N rows and 2 columns. It is produced by applying the softmax function to the output layer scores, which come from multiplying the hidden layer activations by the output weight matrix and then adding the output bias. In words, each class score is exponentiated and then divided by the sum of the exponentiated scores across all classes, so the two numbers in every row represent the predicted probabilities for the two classes. Written entry by entry, this forms a matrix whose rows contain the two predicted values for each sample, such as the first sample&#8217;s two probabilities, then the next sample&#8217;s two probabilities, and so on through sample N.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;14eea9a9-733c-412f-9ac8-5f8d78853bc9&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def softmax(z):
    """Softmax function"""
    return np.exp(z) / np.sum(np.exp(z), axis=1, keepdims=True)</code></pre></div><p>The purpose here is to define the softmax function, which turns a set of raw output scores into probabilities. For each row of input values, the function exponentiates the scores so that larger values become more prominent, and then divides by the sum of those exponentials across the row. That normalization step is what makes the results add up to 1 for each sample, so the output can be interpreted as a probability distribution over classes. The use of axis 1 and keepdims=True means the division happens separately for each example in the batch while preserving the two-dimensional shape needed for later calculations. Since the cell only defines the function and does not call it, there is no saved output yet.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;898e3c3c-054d-42cf-a674-7ce5e5fd3cb5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def output_layer(hidden_activations, weights, bias):
    """Compute the output y_hat"""
    return softmax(hidden_activations @ weights + bias)</code></pre></div><p>This step defines the final layer of the network, the part that turns the hidden layer&#8217;s features into predicted class probabilities. It takes the activations coming out of the hidden layer, multiplies them by the output weights, adds the output bias, and then passes the result through the softmax function. Behind the scenes, that last step is what converts raw scores into values that add up to 1 for each example, so the model can interpret them as probabilities for the two classes. Since the cell only creates a function and does not call it, there is no saved output yet; it simply prepares the output-stage computation that will be used later during forward propagation and training.</p><h3>Computing the Output of the Network</h3><p>The <code>forward_prop</code> function ties the earlier steps together and computes the network&#8217;s output activations from the input data using the current weights and biases. The <code>predict</code> function then takes those weights, biases, and inputs and returns the binary class labels.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;14731b32-2a4c-41b0-bc4b-4efc9742c37c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def forward_prop(data, hidden_weights, hidden_bias, output_weights, output_bias):
    """Neural network as function."""
    hidden_activations = hidden_layer(data, hidden_weights, hidden_bias)
    return output_layer(hidden_activations, output_weights, output_bias)</code></pre></div><p>A small helper function is being defined here to package the full forward pass of the neural network into a single step. It takes the input data together with all of the learned parameters for both layers, first sends the data through the hidden layer to compute the hidden activations, and then feeds those activations into the output layer to produce the final predictions. Behind the scenes, that means it combines the linear transformation and sigmoid-style nonlinearity in the hidden layer with the final scoring and softmax conversion in the output layer. The result is a neat wrapper that represents the network as a function from raw input features to class probabilities, which makes later steps such as training, prediction, and gradient checking much simpler because they can call one routine instead of repeating the two-layer computation each time. There is no saved output because the cell only defines the function; nothing is printed or displayed yet.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;70d59533-1013-4251-af20-f7823e49756b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def predict(data, hidden_weights, hidden_bias, output_weights, output_bias):
    """Predicts class 0 or 1"""
    y_pred_proba = forward_prop(data,
                                hidden_weights,
                                hidden_bias,
                                output_weights,
                                output_bias)
    return np.around(y_pred_proba)</code></pre></div><p>The purpose here is to turn the network&#8217;s output probabilities into an actual class prediction. The model itself produces two probabilities, one for each class, after the forward pass. Rather than returning those raw probabilities, this helper runs the data through the full forward propagation step and then rounds the resulting values to the nearest whole number. For a binary classifier using softmax outputs, that rounding converts a probability vector into a one-hot-like class decision, so the larger probability is effectively turned into a 1 and the smaller one into a 0.</p><p>Behind the scenes, the function does not learn anything new or change the network parameters. It simply reuses the already defined forward pass so that prediction is consistent with how the model was trained. The saved output is empty because the cell only defines the function; nothing is computed or displayed yet. The function becomes useful later when the trained network needs to label new points or evaluate how well it separates the two classes.</p><h3>Cross-Entropy Loss</h3><p>The cost function, J, is based on cross-entropy loss. This loss measures how far the predicted class probabilities for each sample and class are from the true outcomes by summing those discrepancies across all samples.</p><p>The loss function, written for the entire training set, is the total of the individual sample losses. For each example, the contribution is the cross entropy between the true one hot label vector and the predicted probability vector. In other words, the overall cost is found by summing, across all samples and all classes, the true class indicator multiplied by the logarithm of the predicted probability for that class, with a negative sign applied so that better predictions produce smaller loss.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;a30a7748-c7a4-4134-affe-3bcab5efc35f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def loss(y_hat, y_true):
    """Cross-entropy"""
    return - (y_true * np.log(y_hat)).sum()</code></pre></div><p>This cell defines the loss function used to measure how well the network&#8217;s predictions match the true labels. The function computes cross-entropy, which is a standard choice for classification because it strongly penalizes the model when it assigns low probability to the correct class. Inside the function, the predicted probabilities are first passed through a logarithm, then multiplied by the one-hot encoded true labels, and finally summed with a negative sign. Because the labels are one-hot, only the probability assigned to the correct class contributes for each example, so the result reflects how confidently the model supported the right answer across the dataset. There is no saved output because the cell only creates this reusable function; nothing is printed or displayed yet.</p><h2>Backpropagation</h2><p>Backpropagation adjusts each parameter by using the loss gradient for that parameter, and those gradients are obtained through repeated application of the chain rule.</p><h3>Gradient of the Loss Function</h3><p>The gradient of the loss function, J, with respect to each output-layer activation, meaning the activation produced for sample i where i runs from 1 through N, has a very simple form:</p><p>The gradient of the loss with respect to the output pre-activation for class i is the output error term, which equals the predicted probability for that class minus the true label value.</p><p>For a fuller derivation, refer to <a href="https://math.stackexchange.com/questions/945871/derivative-of-softmax-loss-function">this discussion</a> and <a href="https://deepnotes.io/softmax-crossentropy">this explanation</a>.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;a6bc11ab-c44a-4a09-b3e5-63e5cb90842b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def loss_gradient(y_hat, y_true):
    """output layer gradient"""
    return y_hat - y_true</code></pre></div><p>The purpose here is to define the gradient of the loss with respect to the network&#8217;s output probabilities. Since the model is using softmax at the final layer together with cross-entropy loss, the derivative simplifies to a very compact form: the predicted probabilities minus the true one-hot labels. That means the error signal at the output layer is strongest for classes the model predicted too confidently and too weak for the correct class. Behind the scenes, this expression is the key starting point for backpropagation, because it turns the difference between prediction and target into the signal that gets passed backward through the rest of the network. There is no printed result from the cell itself, because it only defines a function that will be used later when gradients are computed during training or gradient checking.</p><h3>Gradients for the Output Layer</h3><h4>Gradients for the Output Weights</h4><p>To send the update information backward to the weights in the output layer, we first compute how the loss changes when the output weight matrix is adjusted:</p><p>The gradient of the loss with respect to the output weight matrix is obtained by multiplying the transpose of the hidden layer activations by the output error term. In other words, the derivative of J with respect to Wo equals the transpose of H times the difference between the predicted labels and the true labels, which is also the output layer delta.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;feec8182-3fb6-4a27-a4b2-06481c05d1f5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def output_weight_gradient(H, loss_grad):
    """Gradients for the output layer weights"""
    return  H.T @ loss_grad</code></pre></div><p>The purpose here is to define the gradient for the weights in the output layer, which is one of the key pieces needed for backpropagation. The function takes the hidden-layer activations and the gradient of the loss with respect to the output layer&#8217;s pre-activation values, then combines them to measure how each output weight contributed to the final error.</p><p>Behind the scenes, the hidden activations are arranged so that each row represents one training example and each column represents one hidden unit. Taking the transpose of that matrix lines up those hidden-unit values with the corresponding error signal from the output layer, and the matrix multiplication accumulates the contribution from every example in the batch. The result is a matrix of the same shape as the output weights, telling us how much each hidden-to-output connection should change to reduce the loss.</p><p>There is no saved output because the cell only defines a helper function. Nothing is computed or displayed yet; the function simply becomes available for later gradient calculations during training and gradient checking.</p><h4>Updating the Output Bias</h4><p>To adjust the bias terms in the output layer, we use the same chain rule approach and compute the partial derivative of the loss with respect to the bias vector:</p><p>The gradient of the loss with respect to the output bias is obtained by chaining together the sensitivity of the loss to the predicted probabilities, the effect of the softmax scores on those probabilities, and the dependence of the output scores on the bias term. After simplifying, this becomes the sum over all training examples of the output-layer error for each example. In other words, the bias gradient is the total of the output deltas across the full batch.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;c0e295aa-49a3-4d42-8e96-9ae455ba79ba&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def output_bias_gradient(loss_grad):
    """Gradients for the output layer bias"""
    return np.sum(loss_grad, axis=0, keepdims=True)</code></pre></div><p>This function computes the gradient for the bias term in the output layer. In backpropagation, each training example contributes a small error signal at the output, and the bias gradient is found by adding those signals together across all samples in the batch. Summing along the first axis collapses the per-example gradients into one row of values, which matches the shape of the output bias itself. Keeping the dimensions intact makes it easy to use the result later when updating the model parameters. Since the cell only defines the helper function and does not call it, there is no printed output yet.</p><h3>Gradients for the hidden layer</h3><p>The hidden-layer error signal is the gradient of the loss with respect to the hidden pre-activation values. It is obtained by chaining the gradient of the loss with respect to the hidden activations, the gradient of the output pre-activations with respect to the hidden activations, and the gradient of the hidden activations with respect to the hidden pre-activations.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;24c34e63-7c78-4ad7-8ced-7544ff2f5001&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def hidden_layer_gradient(H, out_weights, loss_grad):
    """Error at the hidden layer.
    H * (1-H) * (E . Wo^T)"""
    return H * (1 - H) * (loss_grad @ out_weights.T)</code></pre></div><p>The purpose here is to compute the error signal for the hidden layer during backpropagation. Once the network has already measured how wrong the output layer is, that error has to be passed backward through the output weights so the hidden units can also be updated. The function takes the hidden activations, the weights connecting hidden units to the output layer, and the gradient coming from the loss at the output side.</p><p>First, the output-layer gradient is moved back through the output weights by multiplying it with the transpose of those weights. That tells us how much each hidden unit contributed to the final prediction error. Then that backward signal is adjusted by the derivative of the sigmoid activation used in the hidden layer. Since sigmoid units compress their inputs into values between 0 and 1, their sensitivity is highest in the middle and smaller near the extremes, so the factor H times one minus H captures that local slope. Multiplying these two pieces gives the full hidden-layer gradient: the part of the output error attributable to each hidden unit, scaled by how responsive that unit currently is.</p><p>Nothing is displayed when the cell runs because the function is only being defined, not called. Its effect is to make this backpropagation step available for later use when gradients are computed during training.</p><h4>Gradient for the Hidden Weights</h4><p>The gradient of the loss with respect to the hidden-layer weight matrix is found by taking the transpose of the input matrix and multiplying it by the hidden-layer error term.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;ce14b3f0-b59d-48dc-813f-3fcc1aeba292&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def hidden_weight_gradient(X, hidden_layer_grad):
    """Gradient for the weight parameters at the hidden layer"""
    return X.T @ hidden_layer_grad</code></pre></div><p>This helper computes the gradient for the weights that connect the input features to the hidden layer. Its job is to take the input matrix and the error signal coming back from the hidden layer, then combine them in the way backpropagation requires so we know how each input-to-hidden weight should be adjusted.</p><p>The key idea is that every hidden weight affects the loss through the hidden units, and the update for each weight depends on both the input value feeding into that weight and the gradient arriving at the hidden unit. Transposing the input matrix lines up the dimensions so each input feature can be paired with each hidden-unit gradient across all training examples. The matrix multiplication then aggregates those contributions over the whole dataset, producing the full gradient array for the hidden-layer weights.</p><p>Because the cell only defines a function, there is no saved output. Its effect is to prepare this reusable gradient calculation for later use during backpropagation and training.</p><h4>Gradient of the Hidden Bias</h4><p>The gradient of the loss with respect to the hidden layer bias is found by chaining together the effect of the loss on the hidden activations, then on the hidden preactivation values, and finally on the bias itself. Since the bias contributes equally to every sample, the result is the total of the hidden layer error terms across all data points.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;7cbbeb24-fd28-45f8-8c07-632c17f01434&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def hidden_bias_gradient(hidden_layer_grad):
    """Gradient for the bias parameters at the output layer"""
    return np.sum(hidden_layer_grad, axis=0, keepdims=True)</code></pre></div><p>This defines the gradient for the hidden-layer bias term. The idea is that once the error signal has been worked back to the hidden layer, each hidden unit needs a single bias update that reflects how much that unit contributed to the overall loss across all training examples. Because a bias affects every example in the same way, the gradient is found by summing the hidden-layer gradient across the sample dimension. Keeping the dimensions with keepdims=True preserves the row-vector shape, which makes the result easy to use later when updating the parameters. Since the cell only creates this helper function and does not call it, there is no displayed output.</p><h2>Set Up the Weights</h2><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;75204509-0ee5-41a3-80be-3c0dc9588eb6&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def initialize_weights():
    """Initialize hidden and output weights and biases"""

    # Initialize hidden layer parameters
    hidden_weights = np.random.randn(2, 3)
    hidden_bias = np.random.randn(1, 3)

    # Initialize output layer parameters
    output_weights = np.random.randn(3, 2)
    output_bias = np.random.randn(1, 2)
    return hidden_weights, hidden_bias, output_weights, output_bias</code></pre></div><p>The purpose of this cell is to create a small random starting point for the neural network before training begins. The function bundles together all of the learnable parameters: the weights and biases for the hidden layer, and the weights and biases for the output layer. Since the network has two input features and three hidden units, the hidden weight matrix needs to connect 2 inputs to 3 hidden neurons, and the hidden bias needs one value for each hidden neuron. The output layer then takes those 3 hidden activations and maps them to 2 output classes, so its weight matrix has 3 rows and 2 columns, with a matching bias vector of length 2.</p><p>Behind the scenes, each parameter is filled with values drawn from a standard normal distribution, so the network starts with random numbers rather than zeros. That randomness is important because it gives different neurons different initial conditions, which helps the model learn useful patterns during gradient-based training. The function then returns all four arrays together so the rest of the training pipeline can use them as the initial model state.</p><p>There is no saved output here because the cell only defines the function; nothing is printed or displayed yet. The effect of running it appears later, when the function is called to initialize the network before training or gradient checking.</p><h2>Calculate the gradients</h2><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;a79102c2-fe15-44be-a4cc-a4d6eb247cdb&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def compute_gradients(X, y_true, w_h, b_h, w_o, b_o):
    """Evaluate gradients for parameter updates"""

    # Compute hidden and output layer activations
    hidden_activations = hidden_layer(X, w_h, b_h)
    y_hat = output_layer(hidden_activations, w_o, b_o)

    # Compute the output layer gradients
    loss_grad = loss_gradient(y_hat, y_true)
    out_weight_grad = output_weight_gradient(hidden_activations, loss_grad)
    out_bias_grad = output_bias_gradient(loss_grad)

    # Compute the hidden layer gradients
    hidden_layer_grad = hidden_layer_gradient(hidden_activations, w_o, loss_grad)
    hidden_weight_grad = hidden_weight_gradient(X, hidden_layer_grad)
    hidden_bias_grad = hidden_bias_gradient(hidden_layer_grad)

    return [hidden_weight_grad, hidden_bias_grad, out_weight_grad, out_bias_grad]</code></pre></div><p>The purpose of this cell is to bundle the whole backpropagation step into one helper so the network can later be trained repeatedly without re-deriving anything each time. It takes the input data, the true labels, and the current weights and biases, then runs a full forward pass to see what the network is predicting at the moment. First it sends the inputs through the hidden layer to get the hidden activations, and then it passes those activations through the output layer to produce the predicted probabilities.</p><p>Once the predictions are available, the cell moves backward through the network to measure how each parameter should change to reduce the loss. It starts at the output layer by computing the loss gradient, which captures the difference between the predicted probabilities and the true one-hot labels. From that signal it computes the gradient for the output weights and the output bias. Then it propagates the error back into the hidden layer, where the gradient is adjusted by the output weights and the activation function&#8217;s derivative. Using that hidden-layer error signal, it calculates the gradients for the hidden weights and hidden bias.</p><p>The final return value is an ordered collection of the four gradients: hidden weights, hidden bias, output weights, and output bias. That order matters because it matches the way the parameters are usually updated later in training. There is no saved output because the cell only defines a function; nothing is printed or displayed yet.</p><h2>Verify the Gradients</h2><p>Backpropagation involves many parameter inputs, so it is easy to introduce errors. One practical way to verify that the implementation is correct is to compare the output change caused by a tiny perturbation in the parameters with the output change predicted by the gradient that was calculated. For a fuller explanation, see <a href="http://ufldl.stanford.edu/wiki/index.php/Gradient_checking_and_advanced_optimization">here</a>.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;94feb43f-0c31-4b12-bf90-78a67af902c6&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># change individual parameters by +/- eps
eps = 1e-4

# initialize weights and biases
params = initialize_weights()

# Get all parameter gradients
grad_params = compute_gradients(X, Y, *params)

# Check each parameter matrix
for i, param in enumerate(params):
    # Check each matrix entry
    rows, cols = param.shape
    for row in range(rows):
        for col in range(cols):
            # change current entry by +/- eps
            params_low = deepcopy(params)
            params_low[i][row, col] -= eps

            params_high = deepcopy(params)
            params_high[i][row, col] += eps

            # Compute the numerical gradient
            loss_high = loss(forward_prop(X, *params_high), Y)
            loss_low = loss(forward_prop(X, *params_low), Y)
            numerical_gradient = (loss_high - loss_low) / (2 * eps)

            backprop_gradient = grad_params[i][row, col]
            
            # Raise error if numerical and backprop gradient differ
            assert np.allclose(numerical_gradient, backprop_gradient), ValueError(
                    f'Numerical gradient of {numerical_gradient:.6f} not close to '
                    f'backprop gradient of {backprop_gradient:.6f}!')

print('No gradient errors found')</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;44040257-38c3-4781-a234-a723cc0a06fd&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">No gradient errors found</code></pre></div><p>The purpose of this cell is to verify that the backpropagation formulas are correct before any training begins. It starts by choosing a very small change size, called epsilon, which will be used to test how sensitive the loss is to tiny shifts in each parameter. Then it creates a fresh set of random weights and biases, and computes the gradients for those parameters using the backpropagation implementation.</p><p>From there, the cell checks every single entry in every parameter matrix one by one. For each value, it makes two nearly identical copies of the model parameters: one slightly smaller and one slightly larger at just that one position. It then runs the network forward with each modified version, calculates the loss in both cases, and uses the difference between those two losses to estimate the numerical gradient. This finite-difference estimate is a standard way to approximate the true derivative directly from the loss surface.</p><p>That numerical estimate is then compared against the gradient produced by backpropagation for the same parameter entry. If the two values are not close enough, the assertion triggers an error and reports exactly which gradient disagreed. This makes the cell a careful consistency check: if the mathematical derivation or its NumPy implementation were wrong, the mismatch would show up here before training ever starts.</p><p>The saved output, &#8220;No gradient errors found,&#8221; means every numerical gradient matched the corresponding backpropagation gradient within the allowed tolerance. In other words, the model&#8217;s gradient calculations are behaving as expected, and the training code can rely on them.</p><h2>Train the Network</h2><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;a37a1b88-6f34-4499-ad01-4f41a6adc7cc&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def update_momentum(X, y_true, param_list,
                    Ms, momentum_term,
                    learning_rate):
    """Update the momentum matrices."""
    # param_list = [hidden_weight, hidden_bias, out_weight, out_bias]
    # gradients = [hidden_weight_grad, hidden_bias_grad,
    #               out_weight_grad, out_bias_grad]
    gradients = compute_gradients(X, y_true, *param_list)
    return [momentum_term * momentum - learning_rate * grads
            for momentum, grads in zip(Ms, gradients)]</code></pre></div><p>The purpose here is to turn the current model parameters and the training data into a new set of momentum updates. The function first calls the gradient computation routine, which runs a forward pass through the network and then backpropagates the error to get the derivatives for each parameter group. Those gradients are collected in the same order as the parameters themselves, so each weight matrix and bias vector has a matching gradient.</p><p>After that, the function combines the new gradients with the previous momentum values. For each parameter, it keeps part of the old momentum, scaled by the momentum factor, and then subtracts a step in the direction of the gradient, scaled by the learning rate. The result is not the updated parameters directly, but the velocity terms that will be used to move the parameters in the next step. That is how momentum works behind the scenes: it smooths out the updates by letting the optimizer keep some memory of earlier gradients, which can help the training process move more steadily through the loss landscape.</p><p>There is no saved output because the cell only defines this helper function. Nothing is printed or displayed yet; the function simply becomes available for later training steps, where it will be used together with the parameter update function to actually adjust the network during optimization.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;83e0d94e-672c-470d-9997-67968d840e44&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def update_params(param_list, Ms):
    """Update the parameters."""
    # param_list = [Wh, bh, Wo, bo]
    # Ms = [MWh, Mbh, MWo, Mbo]
    return [P + M for P, M in zip(param_list, Ms)]</code></pre></div><p>The purpose here is to apply the momentum-adjusted changes to the model&#8217;s parameters after the gradients have already been turned into update steps. The function takes two parallel lists: one containing the current parameters of the network, and the other containing the momentum terms that were computed from the gradients. It then pairs each parameter with its matching update using zip and adds them together. Because the momentum terms already encode the direction and size of the step to take, adding them to the parameters produces the next version of the weights and biases. Nothing is printed or displayed when the cell runs, since the function definition only prepares this reusable update rule for later training steps.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;f1d84654-1228-40b2-9a2a-f3b26efab0a7&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def train_network(iterations=1000, lr=.01, mf=.1):
    # Initialize weights and biases
    param_list = list(initialize_weights())

    # Momentum Matrices = [MWh, Mbh, MWo, Mbo]
    Ms = [np.zeros_like(M) for M in param_list]

    train_loss = [loss(forward_prop(X, *param_list), Y)]
    for i in range(iterations):
        if i % 1000 == 0: print(f'{i:,d}', end=' ', flush=True)
        # Update the moments and the parameters
        Ms = update_momentum(X, Y, param_list, Ms, mf, lr)

        param_list = update_params(param_list, Ms)
        train_loss.append(loss(forward_prop(X, *param_list), Y))

    return param_list, train_loss</code></pre></div><p>The purpose here is to package the whole training process into a single reusable function. It starts by creating the network&#8217;s initial parameters, which means the hidden-layer weights and biases and the output-layer weights and biases are all sampled once at random. Those values are collected into one list so they can be passed around together during training.</p><p>Right after that, a matching set of momentum arrays is created. These start out as zeros and have the same shapes as the parameters they correspond to. Behind the scenes, these arrays act like memory for the optimizer: instead of updating the weights only from the current gradient, the training step also keeps part of the previous update, which helps smooth the optimization path and can make learning less jittery.</p><p>Before any updates happen, the function measures the current loss on the full training set and stores it as the first entry in the loss history. Then it enters the training loop. At each iteration, it prints the iteration number every 1000 steps so progress can be monitored without flooding the output. The actual learning happens in two stages: first the momentum values are updated using the current gradients, learning rate, and momentum factor; then those momentum values are applied to adjust the parameters themselves. After each update, the function computes the new loss again and appends it to the history list, so the training curve can be inspected later.</p><p>When the loop finishes, the function returns both the final learned parameters and the full sequence of loss values. There is no saved output shown for this cell because the cell only defines the training function; it does not run training yet.</p><p>If you have a chance, try the different parameter settings and compare how they perform.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;f432f59d-2299-4202-ab31-8461920960e0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># n_iterations = 20000
# results = {}
# for learning_rate in [.01, .02, .05, .1, .25]:
#     for momentum_factor in [0, .01, .05, .1, .5]:
#         print(learning_rate, momentum_factor)
#         trained_params, train_loss = train_network(iterations=n_iterations, lr=learning_rate, mf=momentum_factor)
#         results[(learning_rate, momentum_factor)] = train_loss[::1000]</code></pre></div><p>The purpose of this cell is to set up a small experiment for comparing different training settings. It is essentially preparing a sweep over several learning rates and momentum values so the model can be trained multiple times under different optimization choices. The first line fixes the number of training iterations for each run, and the next line creates an empty dictionary to store the results from each combination.</p><p>The nested loops then move through each learning rate and each momentum factor in turn. For every pair, the cell prints the values so you can see which setting is being tested, then calls the training routine with those hyperparameters. That training routine returns the learned parameters and the loss history, and only every thousandth loss value is kept so the results are easier to compare later. Each loss trace is saved in the dictionary using the learning rate and momentum pair as the key, which makes it simple to look up or plot the behavior of any particular run afterward.</p><p>There is no saved output because the experiment code is commented out, so nothing actually runs here. As written, the cell serves as a ready-made template for a grid search over optimization settings, but it does not execute any training until those comment markers are removed.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;95795998-6201-4ea7-b91c-e7132651ec96&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">trained_params, train_loss = train_network(iterations=n_iterations, 
                                           lr=learning_rate, 
                                           mf=momentum_factor)</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;f2a861a9-27d0-45f4-bd5b-9ca466c294c7&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">0 1,000 2,000 3,000 4,000 5,000 6,000 7,000 8,000 9,000 10,000 11,000 12,000 13,000 14,000 15,000 16,000 17,000 18,000 19,000 20,000 21,000 22,000 23,000 24,000 25,000 26,000 27,000 28,000 29,000 30,000 31,000 32,000 33,000 34,000 35,000 36,000 37,000 38,000 39,000 40,000 41,000 42,000 43,000 44,000 45,000 46,000 47,000 48,000 49,000</code></pre></div><p>The cell starts the actual training run for the neural network by calling the training routine with the chosen number of iterations, learning rate, and momentum factor. Inside that routine, the model begins from randomly initialized weights and biases, then repeatedly cycles through forward propagation, loss calculation, gradient computation, and parameter updates. As training progresses, the two sets of parameters are adjusted together so the network gradually improves its ability to separate the two classes in the circle dataset.</p><p>The saved output is the progress display from that long optimization loop. The numbers appear at regular intervals because the training function is set up to print updates every 1,000 iterations, giving a simple checkpoint view of how far the run has advanced. Since the training is being done for 50,000 iterations, the output counts upward from 0 through 49,000, showing that the loop is steadily moving through the full training schedule. There is no final loss value printed here, so the visible result of the cell is mainly confirmation that the model is being trained as intended and that the optimization process is running over the full range of iterations.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;f200d05b-cd73-463d-9df1-54759d0ace7d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">hidden_weights, hidden_bias, output_weights, output_bias = trained_params</code></pre></div><p>The cell simply takes the four learned parameter arrays produced by training and unpacks them into separate names for the hidden layer weights, hidden layer bias, output layer weights, and output layer bias. At this point, the network has already been trained, so these values represent the final fitted model rather than random starting values. Separating them back out makes it easier to use the trained network for later steps such as generating predictions, drawing the decision boundary, or inspecting what each layer has learned. Since the assignment only stores existing values into new variables, nothing is printed or displayed when the cell runs.</p><h3>Visualize the Training Loss</h3><p>This figure shows how the training loss changes across 50 thousand iterations when the model is trained on 50 thousand samples using a momentum value of 0.5 and a learning rate of 1e-4.</p><p>The curve indicates that the loss does not begin to fall until after roughly 5 thousand iterations, but once it starts improving, the drop becomes very steep. Since stochastic gradient descent was not used here, convergence would probably have been much faster with that approach.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;11621bed-fc46-48d4-b26d-61c0bf764fd0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">ax = pd.Series(train_loss).plot(figsize=(12, 3), title='Loss per Iteration', xlim=(0, n_iterations), logy=True)
ax.set_xlabel('Iteration')
ax.set_ylabel('$\\log \\xi$', fontsize=12)
sns.despine()
plt.tight_layout()
plt.savefig(results_path / 'ffnn_loss', dpi=300)</code></pre></div><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!yJ6a!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F070023fc-318f-452c-8495-5d5bea2acf9b_856x208.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!yJ6a!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F070023fc-318f-452c-8495-5d5bea2acf9b_856x208.png 424w, https://substackcdn.com/image/fetch/$s_!yJ6a!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F070023fc-318f-452c-8495-5d5bea2acf9b_856x208.png 848w, https://substackcdn.com/image/fetch/$s_!yJ6a!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F070023fc-318f-452c-8495-5d5bea2acf9b_856x208.png 1272w, https://substackcdn.com/image/fetch/$s_!yJ6a!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F070023fc-318f-452c-8495-5d5bea2acf9b_856x208.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!yJ6a!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F070023fc-318f-452c-8495-5d5bea2acf9b_856x208.png" width="856" height="208" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/070023fc-318f-452c-8495-5d5bea2acf9b_856x208.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:208,&quot;width&quot;:856,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:11219,&quot;alt&quot;:&quot;Output image&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/202847809?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F070023fc-318f-452c-8495-5d5bea2acf9b_856x208.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Output image" title="Output image" srcset="https://substackcdn.com/image/fetch/$s_!yJ6a!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F070023fc-318f-452c-8495-5d5bea2acf9b_856x208.png 424w, https://substackcdn.com/image/fetch/$s_!yJ6a!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F070023fc-318f-452c-8495-5d5bea2acf9b_856x208.png 848w, https://substackcdn.com/image/fetch/$s_!yJ6a!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F070023fc-318f-452c-8495-5d5bea2acf9b_856x208.png 1272w, https://substackcdn.com/image/fetch/$s_!yJ6a!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F070023fc-318f-452c-8495-5d5bea2acf9b_856x208.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>The purpose of this cell is to turn the recorded training losses into a clear visual summary of how learning progressed over time. It starts by wrapping the list of loss values in a pandas Series so it can be plotted directly, then draws that series as a line chart with a wide, short figure size that makes long training runs easy to read. The title and axis limits are set so the plot spans the full training range from the first to the last iteration, and the y-axis is displayed on a logarithmic scale. That log scale is important because the loss changes by large factors during training, and plotting it this way makes both the early sharp drop and the later slower improvements visible on the same chart.</p><p>Next, the axes are labeled to show that the horizontal direction is iteration number and the vertical direction is the logarithm of the loss. The call to remove the top and right borders makes the figure cleaner and easier to interpret, and the layout adjustment helps prevent labels from being cut off. Finally, the plot is saved to the results folder so it can be reused outside the notebook.</p><p>The saved figure reflects exactly this sequence of operations. It shows a very high loss at the beginning, followed by a steep decline in the early iterations and then a much gentler downward slope as training continues. That shape is what you would expect from gradient-based learning: the model starts out poorly fitted, quickly finds a much better region of parameter space, and then makes smaller refinements as it approaches a solution. The fact that the curve keeps trending downward rather than flattening immediately suggests the network is still improving throughout training, even if the gains become more gradual over time.</p><h2>Decision Boundary</h2><p>The plots below illustrate the mapping learned by the neural network when a three unit hidden layer is applied to two dimensional data containing two classes that cannot be separated by a straight line, as shown in the left panel. The decision boundary correctly classifies nearly all points, and additional training would likely reduce the remaining mistakes.</p><p>The middle plot displays the feature representation produced by the hidden layer. During training, the network adjusts the hidden weights so that the original two dimensional inputs are transformed into a three dimensional space where the two classes can be separated with a linear boundary.</p><p>The final plot shows how the output layer carries out that separation by using a threshold of 0.5 on the output value.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;0c8de253-2623-4080-9023-fb37080f4de7&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">n_vals = 200
x1 = np.linspace(-1.5, 1.5, num=n_vals)
x2 = np.linspace(-1.5, 1.5, num=n_vals)
xx, yy = np.meshgrid(x1, x2)  # create the grid

# Initialize and fill the feature space
feature_space = np.zeros((n_vals, n_vals))
for i in range(n_vals):
    for j in range(n_vals):
        X_ = np.asarray([xx[i, j], yy[i, j]])
        feature_space[i, j] = np.argmax(predict(X_, *trained_params))</code></pre></div><p>The purpose here is to evaluate the trained neural network across a dense grid of points in the input plane so the model&#8217;s behavior can later be visualized as a surface or decision map. It starts by choosing 200 evenly spaced values along each axis from about minus 1.5 to 1.5, which creates a square region covering the area where the circular data lives. Those two one-dimensional arrays are then combined into a full grid of coordinate pairs, so every position in that square has a matching x and y location.</p><p>After the grid is built, an empty two-dimensional array is created to store the model&#8217;s predicted class at each grid point. The nested loops then walk through every coordinate in the grid one by one. For each position, the pair of coordinates is assembled into a tiny input sample shaped the way the network expects, and the trained model is asked to predict class probabilities for that point. The prediction is converted into a class label by taking the index of the larger output probability, and that label is written into the matching spot in the feature-space array.</p><p>Nothing is displayed yet, which is why there is no saved output from this cell. Its job is preparatory: it builds the full grid of predicted classes that later plotting cells can use to draw the learned decision boundary or a colored surface. The resulting array captures how the network partitions the plane into class regions, point by point, based on everything it learned during training.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;a0015487-9e54-448f-81ed-9588390544d0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># Create a color map to show the classification colors of each grid point
cmap = ListedColormap([sns.xkcd_rgb["pale red"],
                       sns.xkcd_rgb["denim blue"]])

# Plot the classification plane with decision boundary and input samples
plt.contourf(xx, yy, feature_space, cmap=cmap, alpha=.25)

# Plot both classes on the x1, x2 plane
data = pd.DataFrame(X, columns=['$x_1$', '$x_2$']).assign(Class=pd.Series(y).map({0:'negative', 1:'positive'}))
sns.scatterplot(x='$x_1$', y='$x_2$', hue='Class', data=data, style=y, markers=['_', '+'], legend=False)
plt.title('Decision Boundary')
sns.despine()
plt.tight_layout()
plt.savefig(results_path / 'boundary', dpi=300);</code></pre></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Dt3-!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08814af7-e41f-4941-9445-85cffbba3e0b_424x280.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Dt3-!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08814af7-e41f-4941-9445-85cffbba3e0b_424x280.png 424w, https://substackcdn.com/image/fetch/$s_!Dt3-!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08814af7-e41f-4941-9445-85cffbba3e0b_424x280.png 848w, https://substackcdn.com/image/fetch/$s_!Dt3-!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08814af7-e41f-4941-9445-85cffbba3e0b_424x280.png 1272w, https://substackcdn.com/image/fetch/$s_!Dt3-!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08814af7-e41f-4941-9445-85cffbba3e0b_424x280.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Dt3-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08814af7-e41f-4941-9445-85cffbba3e0b_424x280.png" width="424" height="280" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/08814af7-e41f-4941-9445-85cffbba3e0b_424x280.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:280,&quot;width&quot;:424,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:39470,&quot;alt&quot;:&quot;Output image&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/202847809?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08814af7-e41f-4941-9445-85cffbba3e0b_424x280.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Output image" title="Output image" srcset="https://substackcdn.com/image/fetch/$s_!Dt3-!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08814af7-e41f-4941-9445-85cffbba3e0b_424x280.png 424w, https://substackcdn.com/image/fetch/$s_!Dt3-!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08814af7-e41f-4941-9445-85cffbba3e0b_424x280.png 848w, https://substackcdn.com/image/fetch/$s_!Dt3-!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08814af7-e41f-4941-9445-85cffbba3e0b_424x280.png 1272w, https://substackcdn.com/image/fetch/$s_!Dt3-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08814af7-e41f-4941-9445-85cffbba3e0b_424x280.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The purpose here is to visualize the model&#8217;s decision boundary directly in the original two-dimensional input space and compare it against the training samples. To do that, a two-color colormap is created first so the background can show the two predicted classes with soft, readable shading. The classification scores that were already computed on a dense grid of points are then drawn as a filled contour plot, which paints each region of the plane according to the class the network assigns there. Because the contour is semi-transparent, it acts like a backdrop rather than hiding everything else.</p><p>After the background is in place, the original data points are assembled into a small table with the two input coordinates and a human-readable class label. Those samples are plotted on top of the decision regions with different colors and markers, so you can see where the real examples fall relative to the boundary the network learned. The title is added for clarity, the plot styling is cleaned up by removing extra spines, and the layout is tightened so the figure is neatly arranged. Finally, the image is saved to the results folder for later use.</p><p>The saved figure shows exactly what this sequence is meant to reveal. The pale background areas represent the model&#8217;s predicted class regions, while the orange and blue points show the two circular classes in the dataset. The boundary curves around the inner cluster rather than drawing a straight line, which is the key sign that the neural network has learned a nonlinear separation. The inner blue group is surrounded by the outer orange ring, so a linear classifier would struggle here, but the curved boundary indicates the network has captured the circular structure of the problem.</p><h2>Hidden-Layer Projection</h2><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;c41b40d2-45fc-4250-8b74-128bee5d45d1&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">n_vals = 25
x1 = np.linspace(-1.5, 1.5, num=n_vals)
x2 = np.linspace(-1.5, 1.5, num=n_vals)
xx, yy = np.meshgrid(x1, x2)  # create the grid
X_ = np.array([xx.ravel(), yy.ravel()]).T</code></pre></div><p>The purpose here is to build a regular grid of 2D points that can later be fed through the trained network for visualization. It starts by choosing 25 evenly spaced values between -1.5 and 1.5 for the first coordinate and then does the same for the second coordinate. Those two one-dimensional ranges are combined into a meshgrid, which creates every possible pairing of x1 and x2 across the square region. From there, the grid is flattened and rearranged into a two-column array where each row represents one point in the plane.</p><p>Behind the scenes, this turns a neat square lattice into a format that matches the network&#8217;s expected input shape. Instead of thinking about the points as a 25 by 25 grid, the data is converted into a list of 625 coordinate pairs. That makes it easy to pass all the locations through the model at once when plotting a decision boundary or a probability surface later on. Since the cell is only preparing coordinates and not displaying anything yet, there is no saved output.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;70b86585-b396-4fba-8d0b-e83f19d17c7c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">fig = plt.figure(figsize=(6, 4))
with sns.axes_style("whitegrid"):
    ax = Axes3D(fig)

ax.plot(*hidden_layer(X[y == 0], hidden_weights, hidden_bias).T,
        '_', label='negative class', alpha=0.75)
ax.plot(*hidden_layer(X[y == 1], hidden_weights, hidden_bias).T,
        '+', label='positive class', alpha=0.75)

ax.set_xlabel('$h_1$', fontsize=12)
ax.set_ylabel('$h_2$', fontsize=12)
ax.set_zlabel('$h_3$', fontsize=12)
ax.view_init(elev=30, azim=-20)
# plt.legend(loc='best')
plt.title('Projection of X onto the hidden layer H')
sns.despine()
plt.tight_layout()
plt.savefig(results_path / 'projection3d', dpi=300)</code></pre></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Js6_!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4690458a-ec1e-48c3-a95f-68f78fbdda4e_335x323.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Js6_!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4690458a-ec1e-48c3-a95f-68f78fbdda4e_335x323.png 424w, https://substackcdn.com/image/fetch/$s_!Js6_!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4690458a-ec1e-48c3-a95f-68f78fbdda4e_335x323.png 848w, https://substackcdn.com/image/fetch/$s_!Js6_!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4690458a-ec1e-48c3-a95f-68f78fbdda4e_335x323.png 1272w, https://substackcdn.com/image/fetch/$s_!Js6_!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4690458a-ec1e-48c3-a95f-68f78fbdda4e_335x323.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Js6_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4690458a-ec1e-48c3-a95f-68f78fbdda4e_335x323.png" width="335" height="323" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4690458a-ec1e-48c3-a95f-68f78fbdda4e_335x323.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:323,&quot;width&quot;:335,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:50463,&quot;alt&quot;:&quot;Output image&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/202847809?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4690458a-ec1e-48c3-a95f-68f78fbdda4e_335x323.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Output image" title="Output image" srcset="https://substackcdn.com/image/fetch/$s_!Js6_!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4690458a-ec1e-48c3-a95f-68f78fbdda4e_335x323.png 424w, https://substackcdn.com/image/fetch/$s_!Js6_!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4690458a-ec1e-48c3-a95f-68f78fbdda4e_335x323.png 848w, https://substackcdn.com/image/fetch/$s_!Js6_!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4690458a-ec1e-48c3-a95f-68f78fbdda4e_335x323.png 1272w, https://substackcdn.com/image/fetch/$s_!Js6_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4690458a-ec1e-48c3-a95f-68f78fbdda4e_335x323.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The cell creates a 3D visualization of how the original two-dimensional data looks after it has been transformed by the network&#8217;s hidden layer. Instead of plotting the raw input points, it first feeds the samples through the hidden-layer function, which turns each 2D example into a point in a three-dimensional hidden-feature space. That matters because the network is trying to make a difficult nonlinear problem easier to separate by moving it into a representation where the classes have a simpler structure.</p><p>The plot is set up with a white grid style and a 3D axis, so the result has depth and can show all three hidden units at once. The points for the negative class and positive class are handled separately, which is why the figure uses different markers for each group. Each class is plotted using the three hidden activations as its x, y, and z coordinates, so the axes are labeled as h1, h2, and h3. What appears in the saved image is the hidden-layer embedding of the training set: the two classes no longer just sit in the original circular arrangement, but instead form two distinct clouds with a noticeable geometric structure. That shape reflects how the sigmoid hidden units respond differently to the two classes and shows that the network has learned a nonlinear projection that helps separate them.</p><p>The viewpoint is then adjusted with a chosen elevation and azimuth so the 3D arrangement is easier to read. The title is added to make clear that the figure is showing the projection of X onto the hidden layer H. Finally, the plot is tightened up, cleaned with despine, and saved to the results folder. The saved output displays that final 3D scatter plot, with the two classes spread through hidden space in a way that suggests the network has extracted useful features for classification.</p><h2>Plot of the Network Output Surface</h2><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;cd79691d-8e06-48da-94f3-6eb81546dd3a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">zz = forward_prop(X_, hidden_weights, hidden_bias, output_weights, output_bias)[:, 1].reshape(25, -1)
zz.shape</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;c7ffc69a-f538-4ecf-b652-fec14b989f1c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">(25, 25)</code></pre></div><p>The purpose here is to take the model&#8217;s output probabilities for a grid of input points and reshape them into a form that is easy to visualize as a surface or heatmap. The forward propagation step produces, for each point in X<em>, a pair of class probabilities from the trained network. By selecting the second probability column, the cell focuses on the model&#8217;s estimated likelihood for one of the two classes. That one-dimensional list of probabilities is then reshaped into a 25 by 25 array, which matches the grid structure of the input points. Behind the scenes, this works because X</em> was built from a regular mesh of 625 points laid out as 25 rows and 25 columns in the input space, so the corresponding predictions can be folded back into the same layout.</p><p>The saved output confirms that the reshape worked as intended. The reported shape of (25, 25) means the probability values now sit in a matrix that mirrors the original grid, making it suitable for plotting a smooth decision surface or contour view.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;a9fdc1b8-214a-47e1-ab07-b3788a521cef&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">fig = plt.figure()
with sns.axes_style("whitegrid"):
    ax = fig.gca(projection='3d')
ax.plot_surface(xx, yy, zz, alpha=.25)
ax.set_title('Learned Function')
ax.set_xlabel('$x_1$', fontsize=12)
ax.set_ylabel('$x_2$', fontsize=12)
ax.set_zlabel('$y$', fontsize=12)
ax.view_init(elev=45, azim=-20)
sns.despine()
fig.tight_layout()
fig.savefig(results_path / 'surface', dpi=300);</code></pre></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!DipQ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe64812a-54dc-4eed-9ac3-67631ca08dea_295x295.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!DipQ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe64812a-54dc-4eed-9ac3-67631ca08dea_295x295.png 424w, https://substackcdn.com/image/fetch/$s_!DipQ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe64812a-54dc-4eed-9ac3-67631ca08dea_295x295.png 848w, https://substackcdn.com/image/fetch/$s_!DipQ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe64812a-54dc-4eed-9ac3-67631ca08dea_295x295.png 1272w, https://substackcdn.com/image/fetch/$s_!DipQ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe64812a-54dc-4eed-9ac3-67631ca08dea_295x295.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!DipQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe64812a-54dc-4eed-9ac3-67631ca08dea_295x295.png" width="295" height="295" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/be64812a-54dc-4eed-9ac3-67631ca08dea_295x295.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:295,&quot;width&quot;:295,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:57726,&quot;alt&quot;:&quot;Output image&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/202847809?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe64812a-54dc-4eed-9ac3-67631ca08dea_295x295.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Output image" title="Output image" srcset="https://substackcdn.com/image/fetch/$s_!DipQ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe64812a-54dc-4eed-9ac3-67631ca08dea_295x295.png 424w, https://substackcdn.com/image/fetch/$s_!DipQ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe64812a-54dc-4eed-9ac3-67631ca08dea_295x295.png 848w, https://substackcdn.com/image/fetch/$s_!DipQ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe64812a-54dc-4eed-9ac3-67631ca08dea_295x295.png 1272w, https://substackcdn.com/image/fetch/$s_!DipQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe64812a-54dc-4eed-9ac3-67631ca08dea_295x295.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The purpose of this cell is to turn the model&#8217;s predicted values over a two-dimensional grid into a 3D surface plot, so you can see the learned function as a smooth landscape instead of just as class labels. It begins by creating a fresh figure and then switches the axes styling to a white grid, which gives the plot a clean scientific look and makes the 3D mesh easier to read. After that, it creates a 3D axis on the figure, which is what allows the next plotting step to draw a surface rather than a flat line or scatter plot.</p><p>The surface itself is drawn from the three arrays already prepared earlier: xx and yy represent the grid of input coordinates across the x1 and x2 directions, and zz holds the model&#8217;s output value at each of those grid points. Plotting those three arrays together produces a continuous sheet that shows how the network responds across the input space. The transparency is set fairly low, so the surface doesn&#8217;t overpower the figure and the grid structure remains visible.</p><p>Once the surface is drawn, the cell labels the plot with a title and axis names so the meaning of each dimension is clear. The x and y axes correspond to the two input features, while the vertical axis shows the model output. The view angle is then adjusted to a particular elevation and rotation so the shape of the surface is easier to interpret from an informative perspective. That choice of angle helps reveal the steep central region and the flatter surrounding areas.</p><p>The call to despine removes some of the extra axis framing for a cleaner presentation, and tight layout makes sure the labels and plot elements fit neatly within the figure boundaries. Finally, the figure is saved to disk at the end, so the rendered visualization is preserved for later use.</p><p>The saved output shows a tall, narrow rise in the middle of an otherwise low surface, which reflects how the trained network has concentrated high output values in the region associated with one class and low values elsewhere. The shape is not a simple plane because the model has learned a nonlinear decision function. The stepped, grid-like appearance comes from evaluating the network on a discrete mesh of input points and then connecting those values into a surface, so the plot visualizes both the geometry of the input space and the smoothness of the learned mapping.</p><h2>Summary</h2><p>In short, this notebook has shown that a small network with one hidden layer of three units and 17 total parameters can learn a nonlinear classification task when trained with backpropagation and gradient descent with momentum.</p><p>Next, we will revisit several important design decisions that help when building and training larger models. After that, we will move on to widely used deep learning libraries, which supply many of these pieces directly and handle automatic differentiation so that gradient computation and backpropagation become much easier to implement.</p><div><hr></div><h2>Notebook 2 of 5: <code>02_how_to_use_tensorflow</code></h2><p><em>Source file: `02_how_to_use_tensorflow_processed.ipynb`</em></p><div><hr></div><h1>Getting started with TensorFlow 2</h1><p>Keras was created as a high-level, or meta, API to make it easier and faster to experiment with the design and training of deep neural networks using backends such as TensorFlow, Theano, or CNTK. In 2017, it became part of TensorFlow, and with the TensorFlow 2.0 release it is intended to serve as the main interface. You can also mix TensorFlow and Keras code in the same project, which lets you use Keras for its simpler abstractions while still adding custom TensorFlow graph operations when needed.</p><p>Please refer to the setup steps in <code>Installation Guide.md</code> located in the project root.</p><h2>Imports and configuration</h2><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;769d6460-663e-4e0d-aeeb-16a57d975040&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">import warnings
warnings.filterwarnings('ignore')</code></pre></div><p>The purpose here is to quiet down warning messages so the rest of the notebook runs more cleanly. The warnings module is imported first, which gives access to Python&#8217;s built-in warning system, and then the warning filter is set to ignore warnings. After that, any warnings that would normally be printed to the output area are suppressed, so the notebook stays focused on the results that matter rather than on routine notices or deprecation messages. Since this cell only changes how warnings are displayed and does not produce a result itself, there is no saved output.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;b1b2495e-9889-47a4-9e1d-4379caeebc6e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">%matplotlib inline

from pathlib import Path
from copy import deepcopy
import numpy as np
import pandas as pd

import sklearn
from sklearn.datasets import make_circles # To generate the dataset

import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Activation
from tensorflow.keras import optimizers
from tensorflow.keras.callbacks import TensorBoard

import matplotlib
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap
from mpl_toolkits.mplot3d import Axes3D  # 3D plots

import seaborn as sns</code></pre></div><p>The purpose here is to gather everything needed for the rest of the notebook and to prepare the notebook interface for plotting. The line that enables inline plotting tells Jupyter to draw figures directly beneath the cell instead of opening separate windows, which is why later charts can appear right inside the notebook.</p><p>After that, the cell imports a set of standard tools that will support data handling, model building, and visualization. The file and copy utilities are brought in for working with paths and making independent copies of objects when needed. NumPy and pandas provide general-purpose numerical and tabular data support, while scikit-learn supplies the synthetic circles dataset generator that will create the toy classification problem. TensorFlow and several Keras components are imported next because they will be used to define, train, and monitor a neural network. The model-building pieces include a simple sequential model container, dense layers, activation functions, and an optimizer module, along with TensorBoard so training results can be logged and explored later.</p><p>The plotting libraries come in last. Matplotlib provides the basic plotting machinery, including support for customizing colors and drawing 3D figures if needed, while Seaborn will be used for cleaner statistical plots. Nothing is printed yet, so there is no saved output from this cell; its job is simply to load the tools that the following cells rely on.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;a8ab5eb3-0348-43d3-acc8-d8b60c2d9ffb&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># plotting style
sns.set_style('white')
# for reproducibility
np.random.seed(seed=42)</code></pre></div><p>The purpose here is to set up a consistent visual style for any plots that come later and to make the random parts of the notebook repeatable. The plotting style is switched to a clean white background, which gives charts a lighter, simpler appearance and helps the data stand out more clearly against the axes. After that, the random number generator for NumPy is seeded with a fixed value, so any future random operations that rely on NumPy will produce the same results each time the notebook is run. There is no saved output because nothing is being displayed or printed yet; the cell is quietly preparing the environment for the next steps, especially for plots and synthetic data generation that need to behave consistently.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;38473182-2365-412c-a0da-1be9a4da2b6f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">gpu_devices = tf.config.experimental.list_physical_devices('GPU')
if gpu_devices:
    print('Using GPU')
    tf.config.experimental.set_memory_growth(gpu_devices[0], True)
else:
    print('Using CPU')</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;478d041f-06fc-4b95-ab52-080ec81752a8&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">Using CPU</code></pre></div><p>The cell checks what kind of hardware TensorFlow can see before any heavier computation starts. It asks TensorFlow for the available physical devices of type GPU, and then uses that result to decide how the session should behave. If at least one GPU is found, the message would announce that the GPU is being used, and TensorFlow would be told to grow GPU memory gradually instead of taking all of it at once. That memory-growth setting is a practical safeguard, especially in shared or limited environments, because it helps prevent TensorFlow from reserving more GPU memory than it immediately needs.</p><p>The saved output shows that no GPU was detected, so the fallback path was taken instead. That is why the cell prints &#8220;Using CPU.&#8221; In other words, TensorFlow looked for a GPU, found an empty list, and the notebook continued with CPU-based execution. The output is short because the cell&#8217;s only visible effect in that case is the single status message confirming which device will be used.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;4dbfb872-9ae1-450f-80d7-7c2f2296f8e5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">results_path = Path('results')
if not results_path.exists():
    results_path.mkdir()</code></pre></div><p>The purpose here is to make sure there is a place on disk where the notebook can store anything it produces later, especially training logs and other saved results. A Path object is created for a folder named results, and then the notebook checks whether that folder already exists. If it does not, the folder is created. Behind the scenes, this is a simple safety step: it avoids errors later when other cells try to write files into that location. Since there is no saved output, nothing is printed to the screen; the effect is just a quiet setup step that prepares the working directory for the rest of the notebook.</p><h2>Data Input</h2><h3>Create synthetic data</h3><p>The target variable <code>y</code> contains two classes created from circular patterns, and they cannot be separated with a straight line because class 0 encircles class 1.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;f475c3ca-0715-4ccc-8163-e5fc22edccb7&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># dataset params
N = 50000
factor = 0.1
noise = 0.1</code></pre></div><p>This cell sets the basic settings for the synthetic dataset that will be generated next. The value for N controls how many examples the dataset will contain, so choosing 50,000 creates a fairly large sample for training and testing the neural network. The factor value is used later to control how tightly the two circular classes are nested inside one another, so a small value like 0.1 keeps the inner circle much smaller than the outer one. The noise value adds randomness to the points, which makes the problem less perfectly clean and more realistic, since the samples will not lie exactly on ideal circles.</p><p>There is no output here because nothing is being displayed yet. The cell simply prepares a few parameters that other cells will use when the data is generated, so its effect is to establish the shape and difficulty of the classification problem rather than to produce a visible result immediately.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;fc150b9a-dd77-494d-8f79-8c33543a8c3c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># generate data
X, y = make_circles(
    n_samples=N,
    shuffle=True,
    factor=factor,
    noise=noise)</code></pre></div><p>This step creates the synthetic dataset that the rest of the notebook uses for classification. The goal is to generate a set of two-dimensional points arranged in a pattern that is not easy to separate with a straight line, so the neural network has something meaningful to learn. The data comes from a circles generator, which produces two concentric ring-shaped classes. The number of points is set by the sample count defined earlier, and the points are shuffled so the class order is mixed rather than grouped together. The factor controls how far apart the two rings are, while the noise adds randomness so the points are not perfectly regular.</p><p>The result is stored in two variables: X for the input coordinates and y for the class labels. Since the cell only prepares the dataset and does not print or plot anything, there is no saved output. The effect of the cell is still important, though, because it defines the training data that later cells will reshape, visualize, and feed into the neural network.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;7f626dfc-c90a-4332-82d1-75fbeaa783b9&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># define outcome matrix
Y = np.zeros((N, 2))
for c in [0, 1]:
    Y[y == c, c] = 1</code></pre></div><p>The goal here is to turn the original class labels into a format that the neural network can work with more easily. A new array is created with one row for each sample and two columns, matching the two possible classes in the problem. At first, every entry is set to zero, so the matrix starts as all zeros.</p><p>The loop then goes through the two class values, 0 and 1. For each class, it finds all samples whose label matches that class and sets the corresponding column in the new matrix to 1. That means samples from class 0 get a 1 in the first column, and samples from class 1 get a 1 in the second column. The result is a one-hot encoded target matrix, where each row contains a simple binary indicator of the class.</p><p>There is no saved output because this cell only prepares the target data in memory. Nothing is printed or displayed, but the variable Y is now ready to be used as the model&#8217;s training target.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;1082b1fc-5e9c-475b-ad88-93fba6217b4e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">f'Shape of: X: {X.shape} | Y: {Y.shape} | y: {y.shape}'</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;4a15b1ed-3cfd-482e-a9c0-1f4069e81464&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">'Shape of: X: (50000, 2) | Y: (50000, 2) | y: (50000,)'</code></pre></div><p>The purpose here is to quickly check that the generated dataset has the shapes the model expects before moving on to training. The expression builds a formatted text message from the shape information stored in the three variables: X for the input features, Y for the one-hot encoded targets, and y for the original class labels. Behind the scenes, the shape attribute reports the dimensions of each array, so you can confirm not just the number of samples but also how many values each sample contains.</p><p>The saved output shows that X has shape (50000, 2), which means there are 50,000 data points and each one has two input coordinates. Y also has shape (50000, 2), which matches the one-hot encoding of a binary target, giving two output columns per sample. The original labels y have shape (50000,), which is a one-dimensional array with one class label per sample. That combination confirms the data was prepared consistently: two input features, two encoded target values, and one original label for each example.</p><h3>Show the data visually</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;81e0a2cd-568a-4628-b5c8-cfda4c435bd1&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">sns.scatterplot(x=X[:, 0], 
                y=X[:, 1], 
                hue=y,
               style=y,
               markers=['_', '+']);</code></pre></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!um3x!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5522c7ad-d51c-465a-ada1-492b01f9360b_380x248.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!um3x!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5522c7ad-d51c-465a-ada1-492b01f9360b_380x248.png 424w, https://substackcdn.com/image/fetch/$s_!um3x!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5522c7ad-d51c-465a-ada1-492b01f9360b_380x248.png 848w, https://substackcdn.com/image/fetch/$s_!um3x!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5522c7ad-d51c-465a-ada1-492b01f9360b_380x248.png 1272w, https://substackcdn.com/image/fetch/$s_!um3x!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5522c7ad-d51c-465a-ada1-492b01f9360b_380x248.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!um3x!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5522c7ad-d51c-465a-ada1-492b01f9360b_380x248.png" width="380" height="248" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5522c7ad-d51c-465a-ada1-492b01f9360b_380x248.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:248,&quot;width&quot;:380,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:35469,&quot;alt&quot;:&quot;Output image&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/202847809?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5522c7ad-d51c-465a-ada1-492b01f9360b_380x248.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Output image" title="Output image" srcset="https://substackcdn.com/image/fetch/$s_!um3x!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5522c7ad-d51c-465a-ada1-492b01f9360b_380x248.png 424w, https://substackcdn.com/image/fetch/$s_!um3x!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5522c7ad-d51c-465a-ada1-492b01f9360b_380x248.png 848w, https://substackcdn.com/image/fetch/$s_!um3x!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5522c7ad-d51c-465a-ada1-492b01f9360b_380x248.png 1272w, https://substackcdn.com/image/fetch/$s_!um3x!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5522c7ad-d51c-465a-ada1-492b01f9360b_380x248.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>A scatter plot is being created to show the two classes in the dataset directly in the plane defined by the two input features. The horizontal positions come from the first column of X, and the vertical positions come from the second column, so every sample is placed according to its 2D coordinates. Coloring the points by y separates the two classes visually, and using style with the same labels gives each class a distinct marker shape as well. The custom markers help reinforce that there are two different groups, even before looking at the colors.</p><p>The saved figure makes the structure of the data very clear: one class forms a ring around the other. That circular pattern is exactly why the problem is interesting for a neural network, because a straight line would not be able to separate the classes well. The outer ring is shown in blue with one marker style, while the inner cluster appears in orange with another, so the plot immediately shows a nonlinearly separable dataset.</p><h2>Construct the Keras Model</h2><p>Keras offers two main ways to define models: the straightforward Sequential API and the more adaptable Functional API. Here, we will start with Sequential, and later chapters will use the Functional API for more involved models.</p><p>Building a model only requires creating a Sequential instance and passing in a list of layers in the order they should be applied, along with each layer&#8217;s settings such as the number of units, the activation function, and the layer name.</p><h3>Set Up the Model Structure</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;90ac78f7-e639-4af9-a2d5-dd94571d7a0f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">model = Sequential([
    Dense(units=3, input_shape=(2,), name='hidden'),
    Activation('sigmoid', name='logistic'),
    Dense(2, name='output'),
    Activation('softmax', name='softmax'),
])</code></pre></div><p>The model is being assembled as a small feedforward network for a problem with two input features and two output classes. It starts with a dense hidden layer containing three neurons, and the input shape is set to two because each example in the dataset has exactly two coordinates. Giving the hidden layer a name makes it easier to inspect later, especially when looking at weights or layer summaries.</p><p>After that hidden layer, a sigmoid activation is added. This introduces nonlinearity, which is important because the classes are arranged in a curved pattern and cannot be separated well by a straight line alone. The next dense layer produces two raw output values, one for each class. Those values are then passed through a softmax activation, which turns them into probabilities that add up to one. That makes the network&#8217;s final prediction easy to interpret as a choice between the two classes.</p><p>No output is shown for the cell because it only defines the architecture. The actual effects appear later when the model is compiled, trained, and summarized, at which point Keras uses this layer stack to learn from the data.</p><p>The first hidden layer must be told how many features appear in the input matrix it gets from the input layer, and that is provided through the <code>input_shape</code> argument. In this example, there are only two features. The number of rows processed at training time is not fixed in the model definition; instead, Keras determines it from the <code>batch_size</code> value that will be supplied to the <code>fit</code> method later.</p><p>For every layer after the first one, Keras uses the <code>units</code> setting from the preceding layer to determine the size of the incoming data.</p><p>Keras includes a wide selection of ready-made components, such as recurrent and convolutional layers, several regularization choices, many loss functions and optimizers, and tools for preprocessing, visualization, and logging. For more detail, the project documentation on GitHub is a useful reference. It can also be extended when the built-in options are not enough.</p><p>Calling the model&#8217;s <code>summary</code> method gives a compact overview of the network structure. This includes the types of layers, their output shapes, and the total number of parameters:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;d7f73855-cff3-425b-8a6b-be4e42ea2510&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">model.summary()</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;72cfaf35-f1f5-417f-8cbb-1ca60c605e4b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
hidden (Dense)               (None, 3)                 9         
_________________________________________________________________
logistic (Activation)        (None, 3)                 0         
_________________________________________________________________
output (Dense)               (None, 2)                 8         
_________________________________________________________________
softmax (Activation)         (None, 2)                 0         
=================================================================
Total params: 17
Trainable params: 17
Non-trainable params: 0
_________________________________________________________________</code></pre></div><p>The purpose here is to inspect the neural network that has just been built and verify its structure before moving on. Printing the model summary gives a compact architectural report, which is especially useful in Keras because it shows how the layers are connected, what shape of data flows through them, and how many parameters the network will learn.</p><p>The output starts by naming the model as &#8220;sequential,&#8221; which tells you the layers are stacked one after another in a simple linear order. The first learned layer is the hidden dense layer, and its output shape is shown as an unspecified batch size followed by 3 units. The &#8220;None&#8221; in that shape stands for the number of samples in a batch, which can vary from one training step to another. The parameter count of 9 comes from the fact that each of the 3 neurons receives 2 input values plus its own bias term, so there are 3 times 3 trainable values in total.</p><p>Right after that comes the activation layer labeled &#8220;logistic.&#8221; It keeps the same output shape because activation layers do not change the size of the data; they only apply a nonlinear transformation to the numbers already produced by the previous layer. Since activation layers have no weights of their own, their parameter count is 0.</p><p>The next dense layer is the output layer, which produces 2 values per sample. That matches the binary classification setup where the model predicts probabilities for two classes. Its parameter count of 8 follows the same pattern as before: each of the 2 output neurons receives 3 incoming values from the hidden layer, plus a bias term for each neuron. Finally, the softmax activation turns those two raw output scores into class probabilities that add up to 1, which is why its output shape stays at 2 and it also has no parameters.</p><p>At the bottom, the summary totals everything up: 17 trainable parameters and no non-trainable ones. That confirms the entire network is very small and fully learnable, which makes sense for a simple toy classification problem.</p><h2>Compile the Model</h2><p>The next step is to compile the Sequential model so its training behavior is fully defined. At this point, we specify the optimizer, the loss function, and one or more metrics that will be tracked while the model learns:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;3095002a-4fa6-4976-83e4-f663485b5c4d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">model.compile(optimizer='rmsprop',
              loss='binary_crossentropy',
              metrics=['accuracy'])</code></pre></div><p>Before training can begin, the model needs to be configured with the rules it will use to learn from data. This step tells Keras three important things: how the network should update its weights, how to measure how wrong its predictions are, and which performance score to keep track of while training. The optimizer here is RMSprop, which is a gradient-based method that adjusts the weights in small, adaptive steps so the network can gradually improve. The loss function is binary crossentropy, which is a common choice when the task is binary classification and the model is predicting class probabilities. The accuracy metric is included so Keras can report, at each epoch, how often the model&#8217;s predictions match the true labels.</p><p>Behind the scenes, this command does not train the model yet. Instead, it prepares the model&#8217;s internal training machinery so that the next call to fit knows exactly how to compute errors, how to update parameters, and what summaries to display. Since there is no saved output, nothing is printed at this stage; the effect of the cell is simply to ready the network for the learning process that comes next.</p><h2>TensorBoard Callback</h2><p>Keras supports callbacks, which let you attach extra behavior while training is running. One common use is recording training details so they can be viewed interactively in TensorBoard, which is covered in the next section.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;403c6e8d-1971-4028-a038-6427845c3f70&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">tb_callback = TensorBoard(log_dir=results_path / 'tensorboard', 
                          histogram_freq=1, 
                          write_graph=True, 
                          write_images=True)</code></pre></div><p>This cell prepares TensorBoard logging for the training run. It creates a callback object and points it at the folder where the notebook will store its TensorBoard files, so every time the model trains, TensorFlow can record information there for later inspection. The directory path is built from the existing results location and a dedicated tensorboard subfolder, which keeps these logs separate from any other saved artifacts.</p><p>The extra settings tell TensorFlow to collect more than just the basic scalar values. Histogram logging means it will save distributions of weights and other values once per epoch, which helps you see how parameters change over time. Writing the graph stores the model structure so TensorBoard can display the network architecture. Writing images allows image-based summaries to be saved if they are produced. There is no visible output here because the cell only creates the callback object; nothing is being trained or displayed yet. The real effect will show up later when this callback is passed into model fitting and TensorBoard can read the recorded files.</p><h2>Train the Model</h2><p>To start training, we invoke the model&#8217;s <code>fit</code> method and supply the training data along with a few additional settings:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;28af7af6-5977-4ac8-8165-6af7d43a4f55&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">training=model.fit(X, 
          Y, 
          epochs=50,
          validation_split=.2,
          batch_size=128, 
          verbose=1, 
          callbacks=[tb_callback])</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;9b764d93-8aae-4f72-aa40-627bcc9920e2&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">Epoch 1/50
  1/313 [..............................] - ETA: 0s - loss: 0.7017 - accuracy: 0.3125WARNING:tensorflow:From /home/stefan/.pyenv/versions/miniconda3-latest/envs/ml4t-dl/lib/python3.8/site-packages/tensorflow/python/ops/summary_ops_v2.py:1277: stop (from tensorflow.python.eager.profiler) is deprecated and will be removed after 2020-07-01.
Instructions for updating:
use `tf.profiler.experimental.stop` instead.
WARNING:tensorflow:Callbacks method `on_train_batch_end` is slow compared to the batch time (batch time: 0.0010s vs `on_train_batch_end` time: 0.0070s). Check your callbacks.
313/313 [==============================] - 0s 1ms/step - loss: 0.6932 - accuracy: 0.5534 - val_loss: 0.6929 - val_accuracy: 0.6943
Epoch 2/50
313/313 [==============================] - 0s 739us/step - loss: 0.6917 - accuracy: 0.6453 - val_loss: 0.6908 - val_accuracy: 0.6600
Epoch 3/50
313/313 [==============================] - 0s 707us/step - loss: 0.6888 - accuracy: 0.6600 - val_loss: 0.6863 - val_accuracy: 0.7242
Epoch 4/50
313/313 [==============================] - 0s 729us/step - loss: 0.6815 - accuracy: 0.7295 - val_loss: 0.6760 - val_accuracy: 0.7016
Epoch 5/50
313/313 [==============================] - 0s 746us/step - loss: 0.6663 - accuracy: 0.7814 - val_loss: 0.6559 - val_accuracy: 0.8286
Epoch 6/50
313/313 [==============================] - 0s 739us/step - loss: 0.6414 - accuracy: 0.8174 - val_loss: 0.6264 - val_accuracy: 0.8431
Epoch 7/50
313/313 [==============================] - 0s 787us/step - loss: 0.6068 - accuracy: 0.8352 - val_loss: 0.5882 - val_accuracy: 0.8432
Epoch 8/50
313/313 [==============================] - 0s 704us/step - loss: 0.5656 - accuracy: 0.8475 - val_loss: 0.5454 - val_accuracy: 0.8508
Epoch 9/50
313/313 [==============================] - 0s 689us/step - loss: 0.5219 - accuracy: 0.8569 - val_loss: 0.5027 - val_accuracy: 0.8514
Epoch 10/50
313/313 [==============================] - 0s 748us/step - loss: 0.4796 - accuracy: 0.8629 - val_loss: 0.4613 - val_accuracy: 0.8613
Epoch 11/50
313/313 [==============================] - 0s 737us/step - loss: 0.4398 - accuracy: 0.8694 - val_loss: 0.4241 - val_accuracy: 0.8689
Epoch 12/50
313/313 [==============================] - 0s 838us/step - loss: 0.4053 - accuracy: 0.8749 - val_loss: 0.3917 - val_accuracy: 0.8731
Epoch 13/50
313/313 [==============================] - 0s 692us/step - loss: 0.3754 - accuracy: 0.8803 - val_loss: 0.3648 - val_accuracy: 0.8759
Epoch 14/50
313/313 [==============================] - 0s 780us/step - loss: 0.3503 - accuracy: 0.8846 - val_loss: 0.3408 - val_accuracy: 0.8844
Epoch 15/50
313/313 [==============================] - 0s 688us/step - loss: 0.3271 - accuracy: 0.8888 - val_loss: 0.3176 - val_accuracy: 0.8893
Epoch 16/50
313/313 [==============================] - 0s 723us/step - loss: 0.3037 - accuracy: 0.8945 - val_loss: 0.2934 - val_accuracy: 0.8946
Epoch 17/50
313/313 [==============================] - 0s 741us/step - loss: 0.2778 - accuracy: 0.9010 - val_loss: 0.2656 - val_accuracy: 0.9020
Epoch 18/50
313/313 [==============================] - 0s 734us/step - loss: 0.2475 - accuracy: 0.9105 - val_loss: 0.2334 - val_accuracy: 0.9145
Epoch 19/50
313/313 [==============================] - 0s 722us/step - loss: 0.2146 - accuracy: 0.9255 - val_loss: 0.1997 - val_accuracy: 0.9318
Epoch 20/50
313/313 [==============================] - 0s 788us/step - loss: 0.1806 - accuracy: 0.9448 - val_loss: 0.1655 - val_accuracy: 0.9545
Epoch 21/50
313/313 [==============================] - 0s 770us/step - loss: 0.1478 - accuracy: 0.9721 - val_loss: 0.1337 - val_accuracy: 0.9872
Epoch 22/50
313/313 [==============================] - 0s 807us/step - loss: 0.1176 - accuracy: 0.9942 - val_loss: 0.1048 - val_accuracy: 0.9982
Epoch 23/50
313/313 [==============================] - 0s 820us/step - loss: 0.0915 - accuracy: 0.9990 - val_loss: 0.0808 - val_accuracy: 0.9994
Epoch 24/50
313/313 [==============================] - 0s 904us/step - loss: 0.0701 - accuracy: 0.9995 - val_loss: 0.0613 - val_accuracy: 0.9998
Epoch 25/50
313/313 [==============================] - 0s 930us/step - loss: 0.0530 - accuracy: 0.9998 - val_loss: 0.0463 - val_accuracy: 0.9999
Epoch 26/50
313/313 [==============================] - 0s 920us/step - loss: 0.0399 - accuracy: 0.9999 - val_loss: 0.0347 - val_accuracy: 0.9999
Epoch 27/50
313/313 [==============================] - 0s 794us/step - loss: 0.0299 - accuracy: 0.9999 - val_loss: 0.0260 - val_accuracy: 1.0000
Epoch 28/50
313/313 [==============================] - 0s 731us/step - loss: 0.0225 - accuracy: 0.9999 - val_loss: 0.0196 - val_accuracy: 1.0000
Epoch 29/50
313/313 [==============================] - 0s 762us/step - loss: 0.0170 - accuracy: 1.0000 - val_loss: 0.0148 - val_accuracy: 1.0000
Epoch 30/50
313/313 [==============================] - 0s 732us/step - loss: 0.0129 - accuracy: 1.0000 - val_loss: 0.0113 - val_accuracy: 1.0000
Epoch 31/50
313/313 [==============================] - 0s 749us/step - loss: 0.0099 - accuracy: 1.0000 - val_loss: 0.0087 - val_accuracy: 1.0000
Epoch 32/50
313/313 [==============================] - 0s 800us/step - loss: 0.0077 - accuracy: 1.0000 - val_loss: 0.0068 - val_accuracy: 1.0000
Epoch 33/50
313/313 [==============================] - 0s 1ms/step - loss: 0.0060 - accuracy: 1.0000 - val_loss: 0.0054 - val_accuracy: 1.0000
Epoch 34/50
313/313 [==============================] - 0s 894us/step - loss: 0.0048 - accuracy: 1.0000 - val_loss: 0.0043 - val_accuracy: 1.0000
Epoch 35/50
313/313 [==============================] - 0s 626us/step - loss: 0.0039 - accuracy: 0.9999 - val_loss: 0.0035 - val_accuracy: 1.0000
Epoch 36/50
313/313 [==============================] - 0s 712us/step - loss: 0.0032 - accuracy: 1.0000 - val_loss: 0.0029 - val_accuracy: 1.0000
Epoch 37/50
313/313 [==============================] - 0s 643us/step - loss: 0.0026 - accuracy: 0.9999 - val_loss: 0.0024 - val_accuracy: 1.0000
Epoch 38/50
313/313 [==============================] - 0s 689us/step - loss: 0.0022 - accuracy: 1.0000 - val_loss: 0.0020 - val_accuracy: 1.0000
Epoch 39/50
313/313 [==============================] - 0s 852us/step - loss: 0.0019 - accuracy: 0.9999 - val_loss: 0.0017 - val_accuracy: 1.0000
Epoch 40/50
313/313 [==============================] - 0s 682us/step - loss: 0.0016 - accuracy: 0.9999 - val_loss: 0.0015 - val_accuracy: 1.0000
Epoch 41/50
313/313 [==============================] - 0s 687us/step - loss: 0.0014 - accuracy: 0.9999 - val_loss: 0.0013 - val_accuracy: 1.0000
Epoch 42/50
313/313 [==============================] - 0s 656us/step - loss: 0.0013 - accuracy: 0.9999 - val_loss: 0.0012 - val_accuracy: 1.0000
Epoch 43/50
313/313 [==============================] - 0s 979us/step - loss: 0.0011 - accuracy: 0.9999 - val_loss: 0.0011 - val_accuracy: 1.0000
Epoch 44/50
313/313 [==============================] - 0s 900us/step - loss: 0.0010 - accuracy: 0.9999 - val_loss: 9.5180e-04 - val_accuracy: 1.0000
Epoch 45/50
313/313 [==============================] - 0s 757us/step - loss: 9.3114e-04 - accuracy: 0.9999 - val_loss: 8.5783e-04 - val_accuracy: 1.0000
Epoch 46/50
313/313 [==============================] - 0s 777us/step - loss: 8.5770e-04 - accuracy: 0.9999 - val_loss: 7.9056e-04 - val_accuracy: 1.0000
Epoch 47/50
313/313 [==============================] - 0s 803us/step - loss: 7.8891e-04 - accuracy: 0.9999 - val_loss: 7.2263e-04 - val_accuracy: 1.0000
Epoch 48/50
313/313 [==============================] - 0s 765us/step - loss: 7.4050e-04 - accuracy: 0.9999 - val_loss: 6.7238e-04 - val_accuracy: 1.0000
Epoch 49/50
313/313 [==============================] - 0s 743us/step - loss: 6.9102e-04 - accuracy: 0.9999 - val_loss: 6.3254e-04 - val_accuracy: 1.0000
Epoch 50/50
313/313 [==============================] - 0s 703us/step - loss: 6.4889e-04 - accuracy: 0.9999 - val_loss: 5.9569e-04 - val_accuracy: 1.0000</code></pre></div><p>The purpose here is to start training the neural network on the synthetic circle data and keep track of how well it learns as it goes. The model is given the input features X and the one-hot encoded targets Y, and training is set to run for 50 epochs, which means the full dataset will be passed through the network 50 times. A validation split of 20 percent is reserved from the training data so the model can be checked on unseen examples after each epoch, giving a better sense of whether it is genuinely learning patterns rather than just memorizing the training set. The batch size of 128 means the weights are updated after processing 128 samples at a time, and the TensorBoard callback is attached so the training statistics are also recorded for later inspection.</p><p>The saved output shows Keras reporting progress epoch by epoch. At the start, the loss is around 0.70 and the accuracy is only a little above chance, which is what you would expect from a small network that has not yet learned the structure of the data. The warning messages appear because TensorBoard logging adds some overhead, and TensorFlow is noting that one of the callback operations is slower than the batch computation itself. That does not stop training; it just explains why those messages are printed during the first epoch. As the epochs continue, the loss steadily falls and the accuracy rises, which means the model is adjusting its weights to make better predictions. The validation loss and validation accuracy improve in parallel, showing that the network is not only fitting the training split but also generalizing well to the held-out portion.</p><p>By the later epochs, both training and validation accuracy reach almost perfect values, and the losses shrink to very small numbers. This makes sense for a simple synthetic problem with a clear underlying pattern and a model that is expressive enough to capture the curved boundary between the two classes. The final lines show the network ending with extremely low loss and nearly 100 percent accuracy on both training and validation data, confirming that it has learned an effective decision rule for the circles dataset.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;2877ac83-158b-47fc-ad9e-5428e4a47785&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">fig, axes = plt.subplots(ncols=2, figsize=(14,4))
pd.DataFrame(training.history)[['accuracy', 'val_accuracy']].plot(ax=axes[0])
pd.DataFrame(training.history)[['loss', 'val_loss']].plot(ax=axes[1])
sns.despine()
fig.tight_layout();</code></pre></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!U6Qs!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7c7a3f06-e225-4ae4-a2c8-9f30673c9222_1000x280.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!U6Qs!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7c7a3f06-e225-4ae4-a2c8-9f30673c9222_1000x280.png 424w, https://substackcdn.com/image/fetch/$s_!U6Qs!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7c7a3f06-e225-4ae4-a2c8-9f30673c9222_1000x280.png 848w, https://substackcdn.com/image/fetch/$s_!U6Qs!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7c7a3f06-e225-4ae4-a2c8-9f30673c9222_1000x280.png 1272w, https://substackcdn.com/image/fetch/$s_!U6Qs!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7c7a3f06-e225-4ae4-a2c8-9f30673c9222_1000x280.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!U6Qs!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7c7a3f06-e225-4ae4-a2c8-9f30673c9222_1000x280.png" width="1000" height="280" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7c7a3f06-e225-4ae4-a2c8-9f30673c9222_1000x280.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:280,&quot;width&quot;:1000,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:25295,&quot;alt&quot;:&quot;Output image&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/202847809?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7c7a3f06-e225-4ae4-a2c8-9f30673c9222_1000x280.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Output image" title="Output image" srcset="https://substackcdn.com/image/fetch/$s_!U6Qs!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7c7a3f06-e225-4ae4-a2c8-9f30673c9222_1000x280.png 424w, https://substackcdn.com/image/fetch/$s_!U6Qs!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7c7a3f06-e225-4ae4-a2c8-9f30673c9222_1000x280.png 848w, https://substackcdn.com/image/fetch/$s_!U6Qs!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7c7a3f06-e225-4ae4-a2c8-9f30673c9222_1000x280.png 1272w, https://substackcdn.com/image/fetch/$s_!U6Qs!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7c7a3f06-e225-4ae4-a2c8-9f30673c9222_1000x280.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>A pair of training-history plots is being created here so the model&#8217;s progress over time can be inspected visually. The first line opens a figure with two side-by-side axes, giving room for one plot of accuracy and one plot of loss. Then the stored training history is converted into a table-like DataFrame, and the accuracy and validation accuracy columns are drawn on the left axis. Using the same history data again, the loss and validation loss columns are drawn on the right axis. Because the history object records one value per epoch, each line traces how that metric changed from the start of training to the end.</p><p>The saved figure reflects exactly that structure. On the left, both accuracy curves rise quickly and then level off near 1.0, showing that the model learned the classification task very well and that performance on the validation split tracked the training performance closely. On the right, both loss curves fall steadily toward zero, which is what you would expect as the model becomes more confident and makes fewer mistakes. The two lines in each panel stay fairly close together, suggesting that training and validation behavior are similar and there is not much sign of overfitting. The call to remove extra plot borders and tighten the layout just makes the final display cleaner and easier to read.</p><h2>Retrieve the layer weights</h2><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;f0b82504-eb1c-421c-ba87-31e99c9966de&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">hidden = model.get_layer('hidden').get_weights()</code></pre></div><p>This step reaches into the trained network and pulls out the weights from the layer named &#8220;hidden.&#8221; Keras stores each layer&#8217;s learned parameters internally, so after training you can ask the model for a specific layer object and then retrieve its weights. For a dense layer like this one, the returned weights normally come as two arrays: the matrix of connection weights between the input features and the hidden units, and the bias values for each hidden unit. Saving them into a variable makes it possible to inspect how the network has adjusted itself during training, even though nothing is displayed yet. Since the cell only assigns the result to a variable, there is no visible output here; it simply prepares the hidden layer parameters for later examination.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;fa6ebac0-a924-4fba-98b0-7fe26e47b6ea&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">[t.shape for t in hidden]</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;ece640b4-4c05-4c38-b45e-d829443f4401&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">[(2, 3), (3,)]</code></pre></div><p>This line checks the shapes of the arrays stored in the hidden layer&#8217;s weights. Because the hidden layer has 2 inputs and 3 neurons, the weight matrix needs one value for every input-to-neuron connection, which gives a shape of 2 by 3. The second array is the bias vector, and since there is one bias value per neuron, its shape is just 3. The output therefore lists two shapes: one for the connection weights and one for the biases, confirming that the layer contains the expected number of learned parameters.</p><h2>Visualize the Decision Boundary</h2><p>The plot of the decision boundary looks much like the one produced by the hand-built network. What stands out, however, is that training with Keras completes several times faster.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;5789b6e9-857a-40e9-b039-f2c6bced07f1&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">n_vals = 200
x1 = np.linspace(-1.5, 1.5, num=n_vals)
x2 = np.linspace(-1.5, 1.5, num=n_vals)
xx, yy = np.meshgrid(x1, x2)  # create the grid</code></pre></div><p>The purpose here is to prepare a regular grid of coordinates that can later be used to probe the model across the two-dimensional input space. A total of 200 values is chosen for each axis, and those values are spread evenly from -1.5 to 1.5 using a linear spacing. That creates two one-dimensional arrays: one for horizontal positions and one for vertical positions.</p><p>Those two arrays are then combined into a mesh grid, which turns the separate x and y coordinate lists into two matching 2D arrays. Each position in the first array pairs with each position in the second array, so together they represent every point on a square grid covering the full range. Behind the scenes, this is a convenient way to build a dense set of test points without manually listing them one by one. No output appears because the cell is only setting up these coordinate arrays for a later visualization step, not printing or plotting anything yet.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;27f6073d-0853-46d5-ae3b-31f090060d71&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">X_ = np.array([xx.ravel(), yy.ravel()]).T</code></pre></div><p>The purpose of this line is to turn the grid of x and y coordinates into a single list of 2D points that can be fed into the model for prediction. The values xx and yy are typically two matching arrays built from a mesh over the input space, where each pair of entries represents one location on the plane. By flattening both arrays with ravel and then stacking them side by side, the result becomes a two-column array where every row is one point with an x coordinate and a y coordinate. The transpose at the end makes sure the shape is organized the way the model expects: one sample per row, two features per sample.</p><p>Behind the scenes, this prepares the full decision surface for evaluation. Instead of predicting only on the original training examples, the model can now make a prediction for every point on the grid. That is what later allows the notebook to color in the background of the plot and show the learned decision boundary across the entire 2D region.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;c57cfe1c-22de-487f-acd9-8586fb387d5d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">y_hat = np.argmax(model.predict(X_), axis=1)</code></pre></div><p>The goal here is to turn the model&#8217;s raw probability predictions into a single class label for each input point. The model produces a prediction for both classes at every location in the grid of points being evaluated, so the first step is to run those points through the network and get the output probabilities. Then, for each row of predictions, the highest-probability class is selected. Taking the index of the maximum value across axis 1 means the model&#8217;s two output scores are compared for each sample, and whichever class has the larger score becomes the predicted label. The resulting array, y_hat, is therefore a one-dimensional set of class assignments that can be used for plotting the decision regions or comparing the classifier&#8217;s choices across the input space. Since this step only computes and stores the predicted labels, there is no visible output in the cell itself.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;aa373e51-c839-4386-83b1-ac2a04f3a55a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># Create a color map to show the classification colors of each grid point
cmap = ListedColormap([sns.xkcd_rgb["pale red"],
                       sns.xkcd_rgb["denim blue"]])

# Plot the classification plane with decision boundary and input samples
plt.contourf(xx, yy, y_hat.reshape(n_vals, -1), cmap=cmap, alpha=.25)

# Plot both classes on the x1, x2 plane
data = pd.DataFrame(X, columns=['$x_1$', '$x_2$']).assign(Class=pd.Series(y).map({0:'negative', 1:'positive'}))
sns.scatterplot(x='$x_1$', y='$x_2$', hue='Class', data=data, style=y, markers=['_', '+'], legend=False)
sns.despine()
plt.title('Decision Boundary');</code></pre></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!u98e!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9edb8a20-1680-4ffe-a8a5-adc29ff7adb4_404x280.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!u98e!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9edb8a20-1680-4ffe-a8a5-adc29ff7adb4_404x280.png 424w, https://substackcdn.com/image/fetch/$s_!u98e!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9edb8a20-1680-4ffe-a8a5-adc29ff7adb4_404x280.png 848w, https://substackcdn.com/image/fetch/$s_!u98e!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9edb8a20-1680-4ffe-a8a5-adc29ff7adb4_404x280.png 1272w, https://substackcdn.com/image/fetch/$s_!u98e!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9edb8a20-1680-4ffe-a8a5-adc29ff7adb4_404x280.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!u98e!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9edb8a20-1680-4ffe-a8a5-adc29ff7adb4_404x280.png" width="404" height="280" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9edb8a20-1680-4ffe-a8a5-adc29ff7adb4_404x280.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:280,&quot;width&quot;:404,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:37067,&quot;alt&quot;:&quot;Output image&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/202847809?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9edb8a20-1680-4ffe-a8a5-adc29ff7adb4_404x280.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Output image" title="Output image" srcset="https://substackcdn.com/image/fetch/$s_!u98e!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9edb8a20-1680-4ffe-a8a5-adc29ff7adb4_404x280.png 424w, https://substackcdn.com/image/fetch/$s_!u98e!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9edb8a20-1680-4ffe-a8a5-adc29ff7adb4_404x280.png 848w, https://substackcdn.com/image/fetch/$s_!u98e!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9edb8a20-1680-4ffe-a8a5-adc29ff7adb4_404x280.png 1272w, https://substackcdn.com/image/fetch/$s_!u98e!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9edb8a20-1680-4ffe-a8a5-adc29ff7adb4_404x280.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The purpose here is to turn the model&#8217;s predictions into a picture of the learned decision boundary and then place the original data points on top so you can judge how well the classifier separated the two classes. First, a two-color map is created so the background of the plot can clearly distinguish the two predicted classes across the 2D input space. The soft red and blue shades are chosen to match the two class regions visually without overpowering the points that will be drawn later.</p><p>Next, the code fills in the plane using the model&#8217;s predicted class for each point on the grid. The grid values have already been generated elsewhere, and the predictions have been reshaped to match that grid, so each tiny region of the plane can be colored according to which class the model thinks belongs there. The low transparency makes this background visible but still light enough that the real data can stand out on top of it.</p><p>After that, the original training data are assembled into a small table with the two input coordinates and a class label. Those labels are turned into readable names, and then a scatter plot is drawn over the background. The styling uses different marker shapes for the two classes, while the hue colors help reinforce which points belong to which class. Because the plot is being shown in a 2D feature space, this overlay directly reveals how the learned boundary relates to the actual circular data.</p><p>The saved image shows exactly that result: a soft red background on the outside, a blue region in the middle, and a curved boundary between them. The orange points form the larger outer ring, while the blue points cluster in the center, and the boundary bends around them instead of staying as a straight line. That curved separation is the important visual takeaway, because it shows that the network has learned a nonlinear rule that fits the circle-shaped classes much better than a simple linear model could.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;b70e7afa-3041-4f32-9917-967106c1c582&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">%load_ext tensorboard</code></pre></div><p>This line loads the TensorBoard Jupyter extension into the notebook environment so that TensorBoard can be launched from within the notebook later on. Behind the scenes, it tells Jupyter to register the special TensorBoard commands, which makes the interactive log viewer available without leaving the notebook interface. Since this cell only sets up the extension and does not run a visualization by itself, there is no saved output yet.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;f7649021-9912-46fc-a46a-716715003730&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">%tensorboard --logdir results/tensorboard/</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;f0ea0a32-6d20-4495-bca0-863748229630&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">&lt;IPython.core.display.HTML object&gt;</code></pre></div><p>This launches TensorBoard and points it at the directory where the training logs were written earlier. The log directory contains the files TensorFlow created during model training, such as summaries for loss, accuracy, graphs, and any other tracked information the callback was asked to save. When this command runs inside a Jupyter notebook, it does not print ordinary text output; instead, it creates an embedded browser view so TensorBoard can be opened right inside the notebook.</p><p>The saved output reflects that behavior. The HTML object is just the notebook&#8217;s internal representation of the embedded display, and the iframe is what actually shows TensorBoard on the page. The script underneath sets the iframe to load the local TensorBoard server, which is typically running on port 6006. That is why the output looks like an empty frame at first: the notebook is preparing a live interactive dashboard, not rendering a static image. Once loaded, TensorBoard can be used to inspect the training curves and other logged results from the model.</p><div><hr></div><h2>Notebook 3 of 5: <code>03_how_to_use_pytorch</code></h2><p><em>Source file: `03_how_to_use_pytorch_processed.ipynb`</em></p><div><hr></div><h1>Getting started with PyTorch</h1><p>PyTorch originated in the Facebook AI Research group under Yann LeCun, and its first alpha release appeared in September 2016. It is built to work closely with Python tools such as NumPy, offers strong support for GPU acceleration, and includes automatic differentiation through its autograd system. Compared with Keras, it gives the user more direct control because it exposes a lower-level API. It is widely used as a research-oriented deep learning framework, and it can also serve as a drop-in alternative to NumPy when GPU computation is needed.</p><p>Unlike systems that depend on static computation graphs, such as Theano or TensorFlow, PyTorch uses eager execution. Instead of defining a network ahead of time and compiling it for fixed execution, it performs automatic differentiation through autograd while the tensor operations are being carried out. In other words, gradients are computed as the code runs, which makes it easier to adjust parts of a model dynamically. This programming style is often described as define by run, since the backpropagation behavior follows the actual execution of the code, and each pass through the code can differ. The official documentation includes a thorough tutorial on this approach.</p><ul><li><p><a href="https://pytorch.org/docs">PyTorch Documentation</a></p></li><li><p><a href="https://pytorch.org/tutorials">PyTorch Tutorials</a></p></li></ul><p>Its blend of flexibility, a Python-centered design, and fast execution has helped PyTorch become widely adopted. That popularity has also encouraged the growth of many companion libraries that broaden what it can do.</p><h2>Imports and configuration</h2><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;e19c1a5c-5b22-46d6-a1f8-4fe6ee96b8ee&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">import warnings
warnings.filterwarnings('ignore')</code></pre></div><p>The purpose of this cell is to keep the notebook output clean by turning off warning messages. Python libraries sometimes emit warnings to point out deprecated features, minor compatibility issues, or other situations that are not fatal errors. By importing the warnings module and setting it to ignore warnings, the notebook stops those messages from appearing during later cells. Since there is no saved output, nothing is displayed here; the effect is purely behind the scenes, making the rest of the notebook easier to read without distracting warning text.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;f03bfc81-4b8e-40fc-9adc-bd5dd32a1293&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">%matplotlib inline

from pprint import pprint

import numpy as np
import pandas as pd

from sklearn.metrics import accuracy_score
from sklearn.datasets import make_circles

import torch
import torch.utils.data as utils
import torch.nn as nn
from torch.autograd import Variable
from livelossplot import PlotLosses

from matplotlib.colors import ListedColormap
import matplotlib.pyplot as plt
import seaborn as sns</code></pre></div><p>This cell sets up the tools the rest of the notebook will rely on. It first tells Jupyter to render plots directly inside the notebook, so any figures created later will appear right below the code instead of in a separate window. After that, it imports a few general-purpose utilities for printing, numerical work, and tabular data handling, along with scikit-learn helpers for measuring accuracy and generating the synthetic two-circle dataset.</p><p>The PyTorch imports bring in the main pieces needed for building and training a neural network: the core library, data-loading utilities, and the neural network module collection. The Variable import reflects an older PyTorch style of wrapping tensors, and the live loss plotting package is brought in so training progress can be visualized interactively later on. Finally, the plotting imports prepare the notebook for drawing the decision boundary and data points with customized colors and styling.</p><p>There is no saved output from this cell because importing libraries and setting notebook display behavior usually happens quietly. Its role is simply to prepare the environment so the following cells can create data, train the model, and display results without having to repeat these setup steps.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;06cf9d49-8e63-4ef5-b30e-aab17f88961c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">sns.set_style('white')</code></pre></div><p>This line changes the default Seaborn plotting style so that future charts use a white background. Behind the scenes, it updates Matplotlib&#8217;s style settings through Seaborn, which affects the look of plots created after this point rather than producing any visible output on its own. Because it only sets a global display preference, there is nothing saved in the output area.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;b8b4aad3-93f6-432a-acf0-eb563a0a3192&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">input_size = 2         # Input data dimensionality
hidden_size = 3        # The number of nodes at the hidden layer
num_classes = 2        # The number of output classes
num_epochs = 20         # The number of times entire dataset is trained
batch_size = 20        # The size of input data for one iteration
learning_rate = 0.01  # The speed of convergence</code></pre></div><p>This cell sets the basic training settings that the rest of the model will rely on. The first few values describe the shape of the problem and the size of the network: there are two input features, a small hidden layer with three nodes, and two possible output classes. Those numbers need to match the dataset and the model architecture, so they establish the dimensions the network will use when it is built.</p><p>The remaining values control how training will proceed. The model will be trained for 20 full passes through the dataset, the data will be processed in batches of 20 samples at a time, and the learning rate is set to 0.01 so the optimizer makes relatively small parameter updates on each step. Nothing is printed here, so there is no saved output; the cell simply prepares these configuration values for later cells that create the network and run the training loop.</p><h2>Generate the dataset</h2><h3>Generate Synthetic Data</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;23d18ab8-f979-4a0a-9cc0-b1ac05666e9f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># dataset params
N = 50000
factor = 0.1
noise = 0.1</code></pre></div><p>This step sets the basic parameters for the synthetic dataset that will be created later. The value of N controls how many total samples will be generated, so 50,000 points means the model will have a fairly large training set to learn from. The factor value determines how tightly the two circles are nested inside each other, with a smaller value making the inner circle much smaller relative to the outer one. The noise value adds randomness to the points, so the circles are not perfectly clean and the classification task becomes a little more realistic and slightly harder. Since there is no saved output here, nothing is displayed yet; these lines simply store the chosen dataset settings so the next cell can use them when generating the data.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;434eaa86-8a21-4b47-99e2-8cac5fa51e32&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># generate data
X, y = make_circles(
    n_samples=N,
    shuffle=False,
    factor=factor,
    noise=noise)</code></pre></div><p>The purpose of this step is to create the synthetic dataset that the rest of the notebook will use for training and visualization. A two-class problem is generated with a circular pattern, which is a useful choice because it is simple to describe but not linearly separable, so it gives the neural network something meaningful to learn.</p><p>The function is asked to produce a total of N samples, and the samples are not shuffled. Keeping them in order makes the data generation more predictable, which can be helpful for a teaching example. The factor setting controls how tightly the inner circle sits relative to the outer one, and the noise setting adds randomness so the classes are not perfectly clean. That noise makes the task more realistic and prevents the problem from being too trivial.</p><p>The result is stored in two arrays: X contains the two-dimensional coordinates for each point, and y contains the corresponding class label for each point. There is no visible output from the cell because its job is simply to prepare data in memory rather than display anything. The generated arrays are then ready to be converted into PyTorch tensors and fed into the training pipeline.</p><h3>Convert the data into Torch tensors</h3><p>We start by turning the NumPy or pandas inputs into torch tensors. Moving data back and forth between NumPy and PyTorch is simple:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;b8daaa67-97a4-4fa5-8fb1-7137e95b008b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">X_tensor = torch.from_numpy(X)
y_tensor = torch.from_numpy(y)</code></pre></div><p>The purpose here is to take the synthetic data that was created earlier and turn it into PyTorch tensors. That matters because PyTorch works most naturally with its own tensor objects, especially once you want to build a model, compute gradients, and train with the rest of the PyTorch tools.</p><p>The first line converts the feature array into a tensor. Those features represent the two input coordinates for each sample, so the resulting tensor keeps the same numeric values and shape, just in a format PyTorch can work with directly. The second line does the same thing for the labels, turning the class targets into a tensor as well. Since no output is saved for this cell, nothing is displayed when it runs; the important effect is that these two tensor variables are now available for the later steps that package the data into a dataset and feed it through a DataLoader during training.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;9f8aa1b6-aed1-4556-82b5-80c22edc5609&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">X_tensor.shape, y_tensor.shape</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;fbea8bcb-aed2-4b3a-9de4-55a1d62ec5b6&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">(torch.Size([50000, 2]), torch.Size([50000]))</code></pre></div><p>The purpose here is to check the size of the two tensors created from the synthetic dataset. The feature tensor has 50,000 rows and 2 columns, which matches the 2D circle data where each sample has two coordinates. The label tensor has 50,000 entries, one class label for each sample. The output confirms that the features and targets are aligned correctly: every point in the dataset has a corresponding label, and the shapes are exactly what the later training code expects.</p><h3>Build a Torch Dataset</h3><p>We can first wrap these PyTorch tensors in a TensorDataset and then create a DataLoader that also specifies the batch size:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;9bfe8502-4119-4a5a-949b-60bb0df5c8bb&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">dataset = utils.TensorDataset(X_tensor,y_tensor)</code></pre></div><p>This step bundles the feature tensor and the label tensor into a single dataset object that PyTorch can work with more easily. The features and targets were already converted from NumPy arrays into tensors earlier, and now they are being paired sample by sample so each input point stays matched with its correct class label. Behind the scenes, the dataset object just stores those two tensors together and provides a way to retrieve one training example at a time or in batches later on.</p><p>Creating this paired dataset is important because it becomes the foundation for the data-loading pipeline used during training. Once the samples are organized this way, a DataLoader can later read from the dataset, shuffle the examples, and form minibatches automatically. Since there is no saved output here, nothing is displayed yet; the cell simply prepares the data in a format that the next training steps can use.</p><h3>Set up the Torch DataLoader</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;0786d5df-ee07-4a80-b71a-c1909b459303&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">dataloader = utils.DataLoader(dataset,
                              batch_size=batch_size,
                              shuffle=True)</code></pre></div><p>This step creates the data loader that will feed training examples to the model in small batches. Rather than handing the entire dataset to the network at once, the data is wrapped so PyTorch can deliver it batch by batch, which is much more efficient and is the standard way to train on larger datasets.</p><p>The batch size controls how many samples are grouped together each time the loader yields data. Because shuffling is turned on, the order of the samples will be randomized every time a new pass through the data begins. That randomness matters during training because it helps the model avoid learning patterns tied to the original ordering of the dataset and usually leads to better optimization behavior.</p><p>Nothing is printed here, so there is no saved output to interpret. The effect of the cell is simply to prepare a reusable object that the later training loop can iterate over when it needs mini-batches of features and labels.</p><h2>Construct the Network</h2><h3>Model structure</h3><p>PyTorch expresses a neural network by building a custom <code>Net()</code> class. The key method is <code>forward</code>, which describes how data moves through the model. Once that forward pass is defined, <code>autograd</code> can automatically construct the matching backward pass and calculate gradients for the parameters.</p><p>The forward method is not limited to a narrow set of operations. Any valid Tensor operation can be used there, which gives PyTorch a great deal of freedom in model design. In this example, the implementation is intentionally simple: after setting up the layer attributes, the Tensor is passed through a sequence of straightforward input and output transformations.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;a33d2de1-54f5-4299-af62-76cb207ab0c0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">class Net(nn.Module):
    def __init__(self, input_size, hidden_size, num_classes):
        super(Net, self).__init__()                    # Inherited from the parent class nn.Module
        self.fc1 = nn.Linear(input_size, hidden_size)  
        self.logistic = nn.LogSigmoid()                          
        self.fc2 = nn.Linear(hidden_size, num_classes)
        self.softmax = nn.Softmax(dim=1)
    
    def forward(self, x):
        """Forward pass: stacking each layer together"""
        out = self.fc1(x)
        out = self.logistic(out)
        out = self.fc2(out)
        out = self.softmax(out)
        return out</code></pre></div><p>A small neural network is being defined here as a reusable PyTorch model class. The goal is to describe the model&#8217;s structure once so it can later be instantiated and trained on the dataset. By inheriting from PyTorch&#8217;s module base class, the network automatically fits into PyTorch&#8217;s system for tracking parameters, computing gradients, and switching between training and evaluation behavior.</p><p>The initialization step sets up the layers the model will use. First, a linear layer takes the two input features and maps them into a hidden representation with three units. Then a LogSigmoid activation is inserted to introduce nonlinearity, which is what allows the network to learn more than a straight line decision rule. After that, another linear layer maps the hidden values to the two output classes. A softmax layer is placed at the end so the final outputs can be interpreted as class probabilities that sum to one across the two classes.</p><p>The forward method defines the exact path data takes through the model when a batch is passed in. The input moves through the first linear transformation, then through the nonlinear activation, then through the second linear layer, and finally through softmax. PyTorch uses this forward pass to build the computation graph behind the scenes, so when training begins it will know how to propagate gradients backward through each operation automatically.</p><p>There is no saved output because the cell only defines the model; it does not run any data through it yet. The visible effect of executing it is that the Net class becomes available for later use, so a network can be created, trained, and asked to make predictions.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;a833180c-7702-46ac-a1e5-08ce680c8b53&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">net = Net(input_size, hidden_size, num_classes)
net</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;a052ca20-e863-488a-9b36-8bd61e121cf1&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">Net(
  (fc1): Linear(in_features=2, out_features=3, bias=True)
  (logistic): LogSigmoid()
  (fc2): Linear(in_features=3, out_features=2, bias=True)
  (softmax): Softmax(dim=1)
)</code></pre></div><p>A new instance of the neural network is created here using the chosen input size, hidden size, and number of output classes. That means the model is being set up with two input features, a small hidden layer of three neurons, and two output units for the binary classification task. Because the network is built as a PyTorch module, creating it also initializes the learnable weights and biases inside each layer.</p><p>The next line asks Jupyter to display the model object, so the saved output is a textual summary of its structure. That summary shows the first linear layer mapping from 2 inputs to 3 hidden units, followed by a LogSigmoid activation, then a second linear layer mapping from 3 hidden units to 2 outputs, and finally a Softmax layer applied across the class dimension. The printed representation is useful because it confirms the model has been assembled correctly and makes the architecture easy to inspect at a glance before training begins.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;7670b059-0194-439b-b6a5-8c4773601ace&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">pprint(list(net.parameters()))</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;ebb0eeac-1b99-4107-9294-ae842e49e6ea&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">[Parameter containing:
tensor([[-0.0076, -0.1194],
        [-0.1808, -0.5467],
        [ 0.3306, -0.6842]], requires_grad=True),
 Parameter containing:
tensor([-0.1182,  0.4586,  0.3842], requires_grad=True),
 Parameter containing:
tensor([[ 0.4405, -0.1387,  0.1524],
        [-0.4791, -0.0536,  0.5147]], requires_grad=True),
 Parameter containing:
tensor([ 0.4601, -0.1767], requires_grad=True)]</code></pre></div><p>The purpose here is to inspect the model&#8217;s learned parameters so far, right after the network has been defined and before or during training. By turning the parameter iterator into a list and sending it to pretty-printing, the notebook makes the weights and biases visible in a readable way instead of leaving them hidden inside the model object. That is a useful sanity check because it shows exactly what the optimizer will update during training.</p><p>The output contains four parameter tensors, which matches the two linear layers in the network. The first tensor is the weight matrix for the first linear layer, and its shape reflects the fact that the model takes 2 input features and maps them to 3 hidden units. The next tensor is the bias vector for that same layer, with one bias value for each hidden unit. The third tensor is the weight matrix for the second linear layer, connecting the 3 hidden units to the 2 output classes. The final tensor is the bias vector for the output layer.</p><p>Each parameter is marked as requiring gradients, which is exactly what we want in a trainable neural network. That flag tells PyTorch&#8217;s autograd system to keep track of these values during the forward pass so it can compute gradients for them during backpropagation. The specific numbers shown are small random initial values rather than anything meaningful yet, which is why they look arbitrary and unstructured. They are simply the starting point that training will gradually adjust.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;080a32f7-3b8a-433e-b0fc-fe92095c8de1&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">list(net.parameters())[0]</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;bfa17434-b011-4ad3-8c38-3558ebb19f9b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">Parameter containing:
tensor([[-0.0076, -0.1194],
        [-0.1808, -0.5467],
        [ 0.3306, -0.6842]], requires_grad=True)</code></pre></div><p>The cell is simply looking at the first learnable tensor inside the network, which is the weight matrix of the first linear layer. PyTorch stores a model&#8217;s adjustable values as parameters, and calling the parameter list lets you inspect them directly. Since the first layer maps the 2 input features into 3 hidden units, this weight tensor has three rows and two columns, with each row holding the two weights feeding into one hidden neuron.</p><p>The saved output shows the current values of those weights after training. The small positive and negative numbers reflect how the model has adapted its first layer to start separating the two classes. The fact that <code>requires_grad=True</code> appears in the output is important: it means PyTorch is tracking this tensor so gradients can still be computed for it during backpropagation if training continues.</p><h3>Turn on GPU support</h3><p>To run the model on a GPU, call <code>net.cuda()</code>. The PyTorch documentation explains how to move tensors to the CPU, a single GPU, or multiple GPUs.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;59c91bf9-7c3a-40b6-9c2a-56792f704b8d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># net.cuda()</code></pre></div><p>This line is a reminder that the model can be moved from the CPU to a GPU if one is available. The hash at the start means it is commented out, so it does nothing when the cell runs. If it were enabled, the network and its parameters would be transferred to CUDA memory so later training and inference could run on the graphics card instead of the processor. That is often much faster for neural network workloads, especially when there are many batches or a larger model, but it also requires a compatible GPU and the proper PyTorch CUDA setup. Since there is no saved output, the practical result here is simply that nothing changes at execution time; the cell is acting as a note about an optional hardware acceleration step rather than an active operation.</p><h3>Set Up the Loss Function</h3><p>We still need to set up a loss function and an optimizer, and we can do that with a few of the built-in choices:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;90e22616-0a80-4f69-a8b8-bf5c8064f0d5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">criterion = nn.CrossEntropyLoss()</code></pre></div><p>This line sets up the loss function the model will use during training. Cross-entropy loss is the standard choice for classification tasks like this one because it measures how far the model&#8217;s predicted class probabilities are from the true class labels. Behind the scenes, PyTorch will use this criterion to compare the network&#8217;s output for each batch against the correct answers and produce a single number that represents the training error. That number is what the optimizer will try to reduce by adjusting the network&#8217;s weights. Since there is no saved output, the cell is simply preparing an important training component rather than displaying anything on screen.</p><h3>Choose the Optimizer</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;d746d3ae-484f-4ac6-858b-23b9ece337f0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">optimizer = torch.optim.Adam(net.parameters(), lr=learning_rate)</code></pre></div><p>This line sets up the optimizer, which is the part of training that actually adjusts the model&#8217;s weights after the loss has been computed and gradients have been backpropagated. Here, Adam is chosen, one of the most commonly used optimization algorithms in deep learning because it combines momentum-like behavior with adaptive learning rates for each parameter. The model&#8217;s parameters are passed in so the optimizer knows exactly which values it is allowed to update, and the learning rate controls how large each step should be when it moves those parameters in the direction suggested by the gradients. Since the cell produces no saved output, its effect is entirely in preparing the training process: after this line runs, the model is ready to be trained using Adam in the upcoming loop.</p><h2>Fit the Model</h2><h3>Simple Training Run</h3><p>Model training is organized as two nested loops. The outer loop corresponds to one epoch, meaning one complete pass through the training set. Inside that, the inner loop iterates over the minibatches returned by the DataLoader. Together, these loops carry out the forward computation and the backward gradient update step used by the learning procedure. It is also important to make sure the data are cast to the right types for the objects and functions being used. For example, the labels must be integers, while the input features should be floating-point values:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;51e385c0-093d-459c-a266-0201720c6499&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">for epoch in range(num_epochs):
    print(epoch)
    for i, (features, label) in enumerate(dataloader):
        
        features = Variable(features.float())         
        label = Variable(label.long())

        # Initialize the hidden weights
        optimizer.zero_grad()  
        
        # Forward pass: compute output given features
        outputs = net(features)
        
        # Compute the loss
        loss = criterion(outputs, label)
        # Backward pass: compute the gradients
        loss.backward()
        # Update the weights
        optimizer.step()</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;f70a500f-afc2-4e03-aaba-9ad7c9691fff&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19</code></pre></div><p>The purpose of this cell is to train the neural network for a set number of epochs using minibatches from the DataLoader. The outer loop steps through the full training process one epoch at a time, and the printed numbers in the saved output show that progression clearly: the model runs through epochs 0 to 19, matching the 20 training passes defined earlier.</p><p>At the start of each epoch, the epoch number is printed so you can see training advance in the notebook output. Then the inner loop pulls batches of features and labels from the DataLoader. Each batch is converted into the right PyTorch data types before being used: the input features become floating-point values because neural networks perform their computations in floating point, and the labels become long integers because the loss function expects class indices in that format. Wrapping them in Variable reflects older PyTorch style, but the main idea is simply to prepare the tensors for automatic differentiation and training.</p><p>Next, the optimizer&#8217;s gradients are cleared. That step is important because PyTorch accumulates gradients by default, so without resetting them the gradients from earlier batches would keep piling up and interfere with learning. After that, the features are passed through the network to produce predictions. Those predictions are compared with the true labels using the loss function, which measures how far the model&#8217;s outputs are from the correct classes. Once the loss is computed, backpropagation calculates gradients for every learnable parameter in the network, and the optimizer uses those gradients to update the weights. Repeating this batch by batch, epoch by epoch, gradually helps the model learn the decision boundary for the classification task.</p><p>The saved output is just the stream of epoch numbers printed during training, so it does not show the loss or accuracy directly. Instead, it confirms that the loop completed all 20 epochs successfully, with each number marking one full pass through the training data.</p><h3>Showing loss values as training progresses</h3><p>The example below shows how to use the <code>livelossplot</code> package to display training losses as the model learns, in a way similar to the built-in behavior Keras provides.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;a77614fd-c600-4536-94bc-3474fe1f24bf&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">liveloss = PlotLosses()
for epoch in range(num_epochs):
    print(epoch)
    logs = {}
    running_loss = 0.0
    running_corrects = 0    
    for i, (features, label) in enumerate(dataloader):
        
        features = Variable(features.float())         
        label = Variable(label.long())

        # Intialize the hidden weight to all zeros
        optimizer.zero_grad()  
        
        # Forward pass: compute the output class given a image
        outputs = net(features)
        
        # Compute the loss: difference between the output class and the pre-given label
        loss = criterion(outputs, label)
        # Backward pass: compute the weight
        loss.backward()
        # Optimizer: update the weights of hidden nodes
        optimizer.step()
        
        _, preds = torch.max(outputs, 1)
        running_loss += loss.detach() * features.size(0)
        running_corrects += torch.sum(preds == label.data)

        epoch_loss = running_loss / len(dataset)
        epoch_acc = running_corrects.float() / len(dataloader.dataset)        
        logs['log loss'] = loss.item()
        logs['accuracy'] = epoch_acc.item()

    liveloss.update(logs)
    liveloss.draw()</code></pre></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!hHhR!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3738fae1-fbbd-426b-87e5-35c03249d6b4_856x568.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!hHhR!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3738fae1-fbbd-426b-87e5-35c03249d6b4_856x568.png 424w, https://substackcdn.com/image/fetch/$s_!hHhR!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3738fae1-fbbd-426b-87e5-35c03249d6b4_856x568.png 848w, https://substackcdn.com/image/fetch/$s_!hHhR!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3738fae1-fbbd-426b-87e5-35c03249d6b4_856x568.png 1272w, https://substackcdn.com/image/fetch/$s_!hHhR!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3738fae1-fbbd-426b-87e5-35c03249d6b4_856x568.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!hHhR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3738fae1-fbbd-426b-87e5-35c03249d6b4_856x568.png" width="856" height="568" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3738fae1-fbbd-426b-87e5-35c03249d6b4_856x568.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:568,&quot;width&quot;:856,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:45319,&quot;alt&quot;:&quot;Output image&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/202847809?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3738fae1-fbbd-426b-87e5-35c03249d6b4_856x568.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Output image" title="Output image" srcset="https://substackcdn.com/image/fetch/$s_!hHhR!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3738fae1-fbbd-426b-87e5-35c03249d6b4_856x568.png 424w, https://substackcdn.com/image/fetch/$s_!hHhR!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3738fae1-fbbd-426b-87e5-35c03249d6b4_856x568.png 848w, https://substackcdn.com/image/fetch/$s_!hHhR!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3738fae1-fbbd-426b-87e5-35c03249d6b4_856x568.png 1272w, https://substackcdn.com/image/fetch/$s_!hHhR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3738fae1-fbbd-426b-87e5-35c03249d6b4_856x568.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;f4cd6e93-7941-4d8b-bd1c-d5f5620ba9c0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">accuracy
&#9;accuracy         &#9; (min:    1.000, max:    1.000, cur:    1.000)
log loss
&#9;log loss         &#9; (min:    0.313, max:    0.313, cur:    0.313)</code></pre></div><p>The goal here is to run another training pass over the model while watching its behavior live. A live loss tracker is created first, and then the model is trained for the full number of epochs using minibatches from the data loader. At the start of each epoch, the epoch number is printed, and a fresh set of logging values is prepared so the code can keep track of how training is going.</p><p>Inside the batch loop, the feature tensors are converted to floating point and the labels are converted to integer class indices, which is what the loss function expects. Before each update, the optimizer clears out any gradients left over from the previous batch. Then the data is sent through the network to produce outputs, the loss is computed by comparing those outputs with the true labels, and backpropagation calculates gradients for every learnable parameter. After that, the optimizer uses those gradients to adjust the weights.</p><p>The code also measures performance as it goes. It picks the predicted class for each example by taking the larger of the two output scores, then compares those predictions with the true labels to accumulate both the running loss and the number of correct predictions. From those totals, it computes an epoch-level loss and accuracy, and stores the most recent values in the logging dictionary.</p><p>Once the epoch finishes, the live plotter receives the latest logs and redraws the training curves. That is why the saved output shows a figure with two panels: one for accuracy and one for log loss. The command-line text above the plots reports the latest tracked values as well. The accuracy is displayed as 1.000, which means the model is classifying the training samples perfectly at this point, while the loss is around 0.313, showing that the predicted probabilities are still not infinitely confident even though the class decisions are correct. The plots themselves look a little noisy because the figures are being updated as training progresses, and the scales are very tight, so even small changes in the logged values create visible wiggles.</p><h2>Make predictions</h2><p>To generate predictions from the trained model, we feed in the feature data and then turn the result into a NumPy array. The output gives the softmax probability for each of the two classes:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;3c97b7b2-2e17-49f7-80b6-4d8651840a16&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">test_value = Variable(torch.from_numpy(X)).float()
prediction = net(test_value).data.numpy()</code></pre></div><p>The purpose here is to run the trained network on the full set of input points and turn its results into a NumPy array that can be worked with outside of PyTorch. First, the feature matrix is converted from a NumPy array into a Torch tensor, wrapped in a Variable, and cast to floating point so it matches the type expected by the model. That step matters because neural networks in PyTorch operate on tensors, and the linear layers inside the network require floating-point inputs. Then the tensor is passed through the model to produce predictions for every sample in X.</p><p>Behind the scenes, the network applies its learned weights and activations to each input row and produces output scores for the two classes. The .data part detaches the result from the autograd system, which is a way of saying the prediction values are being treated as plain numbers rather than something the framework should continue tracking for gradient computation. Finally, those values are converted into a NumPy array so they can be used with standard Python and scientific computing tools such as accuracy calculations or plotting. Since there is no saved output, the main effect of running the cell is to prepare prediction values for the next evaluation step.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;706872d9-d0ef-4813-89e2-427ecb12f002&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">prediction.shape</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;058d5025-7ba8-48ed-a299-143bd0b38d62&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">(50000, 2)</code></pre></div><p>This line checks the shape of the model&#8217;s prediction array so you can confirm how many samples were processed and how many values were produced for each one. The result shown as (50000, 2) means there is one row for each of the 50,000 data points and two columns for the two class scores or probabilities. That matches the binary classification setup: for every input point, the network returns a pair of outputs, one for each class. Seeing this shape is a useful sanity check because it tells you the prediction step worked as expected and produced an output aligned with the dataset size and the number of classes.</p><p>From this point onward, the same workflow can be used to calculate loss measures or to plot the outcome again, which recreates the decision boundary shown earlier.</p><h3>Evaluate the Predictions</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;651f8984-907f-4037-9fdc-7ef7359e84e0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">accuracy_score(y_true=y, y_pred=np.argmax(prediction, axis=1))</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;80c4c648-2477-4192-bb5f-8006a5e2ea62&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">1.0</code></pre></div><p>The purpose here is to measure how well the trained model classified the dataset. The saved output of 1.0 means the predicted labels matched the true labels for every example, so the accuracy is perfect on this data.</p><p>The calculation works by taking the model&#8217;s prediction scores for each sample and converting them into a single class choice with argmax along the class axis. That picks whichever class received the higher probability or score for each point. Those predicted class indices are then compared with the true labels using the accuracy metric, which simply counts the fraction of correct matches.</p><p>Because the result is 1.0, the model got every training example right. That makes sense in a setting where the network has already been trained on this same dataset and the problem is relatively simple and synthetic, so the learned decision boundary can separate the two classes very effectively.</p><h2>Plot the Decision Boundary</h2><h3>Construct the Feature Space</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;fe61b957-8b32-40f9-94af-b09e24c4c557&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">n_vals = 200
x1 = np.linspace(-1.5, 1.5, num=n_vals)
x2 = np.linspace(-1.5, 1.5, num=n_vals)
xx, yy = np.meshgrid(x1, x2)  # create the grid</code></pre></div><p>The purpose here is to set up a regular grid of points across the two-dimensional input space so the model can later be evaluated everywhere on that plane. The first line chooses how fine the grid will be by setting the number of values to 200, which gives enough resolution for a smooth visualization without becoming too expensive to compute. The next two lines create evenly spaced values from -1.5 to 1.5 for each axis, covering the same range in both directions so the plot will be square and centered around the region where the data lives.</p><p>After that, the two one-dimensional arrays are combined into a full mesh grid. This turns the single list of x-values and the single list of y-values into two 2D arrays, one containing all the horizontal coordinates and the other containing all the vertical coordinates. Behind the scenes, each pair of matching positions in these arrays represents one point on the plane. That grid is what makes it possible to feed a dense set of coordinates into the network later and see how it classifies different parts of the space.</p><p>There is no saved output because nothing is being displayed yet. The cell simply prepares the coordinate system that the later prediction and plotting steps will use to draw the decision boundary.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;941a1bcb-7a55-4bdf-9f9b-797005eaf786&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">X_test = np.array([xx.ravel(), yy.ravel()]).T
X_test = torch.from_numpy(X_test)
X_test.shape</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;a5283e72-4acb-4575-9bc4-ba769b6b7b1f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">torch.Size([40000, 2])</code></pre></div><p>The purpose of this step is to turn the two coordinate grids used for plotting into a long list of individual points that can be passed through the neural network. The meshgrid arrays contain a full square of x and y locations, but the model expects one input row per point, with two features in each row. Flattening both grids and stacking them side by side creates exactly that kind of table, where each row represents one location in the plane.</p><p>After that, the NumPy array is converted into a PyTorch tensor so it can be used in the same way as the training data was. That matters because the network was built in PyTorch and expects tensor inputs. The final line asks for the tensor&#8217;s shape, and the saved output shows torch.Size([40000, 2]). That tells us there are 40,000 grid points in total, each with 2 coordinates. The number 40,000 comes from the mesh resolution: the grid is dense enough to give a smooth decision boundary later, and the two columns confirm that each point still has the x and y values the model needs.</p><h3>Predict Across the Feature Space</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;f3553c95-8551-427c-abac-8f3c47a3b226&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">zz = net(Variable(X_test).float()).data.numpy()
zz.shape</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;139057e7-cc6b-487f-9a3f-00471bf04387&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">(40000, 2)</code></pre></div><p>The purpose here is to run the trained neural network on every point in the dense grid used for the decision-boundary plot, then check the shape of the resulting predictions. The grid points are stored in X_test, and they are first wrapped as a Variable and converted to floating-point values so they match the input type the network expects. Passing them through net produces, for each grid point, a pair of output values corresponding to the two classes. The result is then moved out of PyTorch into a NumPy array so it can be handled easily with the plotting tools used later.</p><p>The saved output shows that the prediction array has shape (40000, 2). That means there are 40,000 grid points in the mesh, and for each one the model returns two class scores or probabilities. This shape is exactly what you would expect from evaluating a two-class classifier over a flattened 2D grid: one row per point, and one column per class. It confirms that the network has produced a full set of predictions ready to be reshaped back into the grid form needed for the contour plot.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;90500dca-3a37-4a78-82d3-c52603472a05&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># Create a color map to show the classification colors of each grid point
cmap = ListedColormap([sns.xkcd_rgb["pale red"],
                       sns.xkcd_rgb["denim blue"]])</code></pre></div><p>A two-color map is being prepared here so the later decision-boundary plot can shade each region according to the predicted class. The two colors are pulled from Seaborn&#8217;s named XKCD palette, which gives a soft red for one class and a denim blue for the other. Wrapping them in a ListedColormap tells Matplotlib to treat the colors as a fixed set of discrete categories rather than as a smooth gradient, which is exactly what you want for a classification map. Nothing is displayed yet, because this step is only defining the palette that will be reused when the grid of predictions is plotted.</p><h3>Visualize the decision boundary</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;cc8227ae-9530-4af2-9df2-d36d33513257&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># Plot the classification plane with decision boundary and input samples
plt.contourf(xx, yy, np.argmax(zz, axis=1).reshape(n_vals, -1), cmap=cmap, alpha=.25)

# Plot both classes on the x1, x2 plane
data = pd.DataFrame(X, columns=['$x_1$', '$x_2$']).assign(Class=pd.Series(y).map({0:'negative', 1:'positive'}))
sns.scatterplot(x='$x_1$', y='$x_2$', hue='Class', data=data, style=y, markers=['_', '+'], legend=False)
plt.title('Decision Boundary');</code></pre></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!w3o6!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5cb01b25-3783-4dca-b64f-beef3a280716_404x280.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!w3o6!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5cb01b25-3783-4dca-b64f-beef3a280716_404x280.png 424w, https://substackcdn.com/image/fetch/$s_!w3o6!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5cb01b25-3783-4dca-b64f-beef3a280716_404x280.png 848w, https://substackcdn.com/image/fetch/$s_!w3o6!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5cb01b25-3783-4dca-b64f-beef3a280716_404x280.png 1272w, https://substackcdn.com/image/fetch/$s_!w3o6!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5cb01b25-3783-4dca-b64f-beef3a280716_404x280.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!w3o6!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5cb01b25-3783-4dca-b64f-beef3a280716_404x280.png" width="404" height="280" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5cb01b25-3783-4dca-b64f-beef3a280716_404x280.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:280,&quot;width&quot;:404,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:37878,&quot;alt&quot;:&quot;Output image&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/202847809?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5cb01b25-3783-4dca-b64f-beef3a280716_404x280.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Output image" title="Output image" srcset="https://substackcdn.com/image/fetch/$s_!w3o6!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5cb01b25-3783-4dca-b64f-beef3a280716_404x280.png 424w, https://substackcdn.com/image/fetch/$s_!w3o6!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5cb01b25-3783-4dca-b64f-beef3a280716_404x280.png 848w, https://substackcdn.com/image/fetch/$s_!w3o6!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5cb01b25-3783-4dca-b64f-beef3a280716_404x280.png 1272w, https://substackcdn.com/image/fetch/$s_!w3o6!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5cb01b25-3783-4dca-b64f-beef3a280716_404x280.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The purpose of this cell is to turn the model&#8217;s predictions into a visual decision boundary and then place the original training samples on top so you can see how well the classifier has learned the shape of the problem. The first line uses the grid of points that was evaluated earlier and converts the model&#8217;s class probabilities into a single class label for each point by taking the larger of the two predicted values. That label map is reshaped back into the same two-dimensional layout as the mesh grid, and then filled contours are drawn with a transparent color map. Behind the scenes, this creates a shaded background over the plane, where each region represents the class the network would predict for points in that area.</p><p>After the background is drawn, the original dataset is assembled into a table with the two input coordinates and a human-readable class name. The labels are mapped from numeric values into the words &#8220;negative&#8221; and &#8220;positive,&#8221; which makes the scatter plot easier to interpret. Seaborn then plots the samples on top of the contour map, using different marker styles for the two classes and suppressing the legend so the figure stays uncluttered. The title is added at the end to label the visualization.</p><p>The saved figure shows exactly the result of those steps: a colored decision region in the background, with the two concentric classes overlaid as scattered points. The model has learned a curved boundary that separates the inner cluster from the outer ring, so the plot makes it easy to see both the learned classification surface and how the training samples fall within it.</p><div><hr></div><h2>Notebook 4 of 5: <code>04_optimizing_a_NN_architecture_for_trading</code></h2><p><em>Source file: `04_optimizing_a_NN_architecture_for_trading_processed.ipynb`</em></p><div><hr></div><h1>Train a deep neural network to forecast asset price returns</h1><p>In real-world use, it is usually necessary to test several design choices rather than relying on a single assumed architecture, since the best fit for a given dataset is rarely obvious in advance.</p><p>This section examines different ways to construct a basic feedforward neural network for predicting asset price returns one day ahead.</p><h2>Imports and configuration</h2><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;3cf994da-cfc3-4879-9dce-64dc4ee1ab2c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">import warnings
warnings.filterwarnings('ignore')</code></pre></div><p>The purpose here is to quiet down Python&#8217;s warning messages so they do not clutter the notebook output. Warnings are often useful during development because they can point out deprecated functions, unstable behavior, or other issues that may need attention, but they can also become distracting when the goal is to focus on the main results of an experiment. By setting the warning filter to ignore, the notebook tells Python to suppress those messages from this point onward. Since the cell only changes how warnings are displayed and does not run any calculation or print anything, there is no visible output afterward.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;1b225f68-2509-41ee-9173-a86bcd6c6e7d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">%matplotlib inline

import os, sys
from ast import literal_eval as make_tuple
from time import time
from pathlib import Path
from itertools import product
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import statsmodels.api as sm
from scipy.stats import spearmanr
import seaborn as sns

from sklearn.preprocessing import StandardScaler

import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Dropout, Activation</code></pre></div><p>The cell sets up the notebook environment for the neural-network experiments that follow. It first enables inline plotting so any figures created later will appear directly inside the notebook rather than in a separate window. After that, it brings in the standard tools needed for the rest of the workflow: operating system and path utilities for working with files and directories, a helper for turning text into tuples, timing support for measuring run time, itertools for generating combinations of hyperparameters, and the main scientific Python libraries for numerical work, data handling, plotting, statistics, and visualization.</p><p>A few of the imports are especially important for the modeling task. StandardScaler will later be used to normalize the feature values before training, which is a common step for neural networks because they tend to train more reliably when inputs are on comparable scales. TensorFlow and the Keras objects are also loaded here, since they provide the machinery for building the feedforward network itself. Sequential is the model container, while Dense, Dropout, and Activation are the basic building blocks used to stack fully connected layers, apply regularization, and choose nonlinear transformations. Because this cell only prepares the environment and defines no calculations or display commands, there is no saved output, which is exactly what you would expect.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;3d0ddc9d-3ff7-44e3-b3b4-26ddff3fa875&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">gpu_devices = tf.config.experimental.list_physical_devices('GPU')
if gpu_devices:
    print('Using GPU')
    tf.config.experimental.set_memory_growth(gpu_devices[0], True)
else:
    print('Using CPU')</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;5727ec6e-8cea-480b-9137-b30cdab0c4d4&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">Using CPU</code></pre></div><p>The purpose here is to check what kind of hardware TensorFlow can use before any model training begins. It first asks TensorFlow for the list of available GPU devices on the system. If that list is not empty, the notebook would announce that a GPU is being used and turn on memory growth for the first GPU, which tells TensorFlow to allocate GPU memory gradually as needed instead of grabbing it all at once. That can help avoid memory-related issues when working with other processes or when the exact memory needs are not known in advance.</p><p>Because no GPU is detected, the logic falls into the alternative branch and reports that the CPU will be used instead. The saved output shows exactly that message, which means TensorFlow did not find any compatible GPU device in the environment at runtime. In practice, that means all later model training and inference in the notebook will run on the processor rather than on specialized GPU hardware, which is usually slower but still fully functional.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;9681bc2e-1812-4837-a7d9-f8387e5fbc9d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">sys.path.insert(1, os.path.join(sys.path[0], '..'))
from utils import MultipleTimeSeriesCV, format_time</code></pre></div><p>This step prepares the notebook to use helper tools that live outside the standard Python library. First, it adjusts the import search path so that Python will look one level above the current directory when trying to find modules. That matters because the utilities needed here are stored in a local file rather than installed as a package. After that path update, the notebook imports two custom helpers: one is a specialized cross-validation splitter designed for time-ordered financial data, and the other is a small formatting function for turning elapsed time into a readable display. There is no visible output because nothing is being computed or displayed yet; the cell is simply setting up access to functionality that later cells will rely on.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;d3ad70cb-5c33-479d-8fda-f6449d3ea78f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">np.random.seed(42)
sns.set_style('whitegrid')
idx = pd.IndexSlice</code></pre></div><p>The purpose of this cell is to set up a predictable and consistent working environment before any modeling or plotting happens. First, the random number generator is seeded with a fixed value so that any later operation that relies on randomness will produce the same results each time the notebook is run. That matters for reproducibility, especially when models are trained, data are shuffled, or parameter combinations are sampled in different orders.</p><p>Next, the plotting style is changed to a whitegrid theme, which gives future charts a cleaner background with light grid lines. That makes figures easier to read and compare, particularly for time series or performance plots. Nothing is drawn yet, so there is no saved output, but the visual style setting quietly affects all later plots in the notebook.</p><p>Finally, an IndexSlice shortcut is created from pandas. This is a convenience tool for selecting pieces of a multi-indexed DataFrame more easily later on, especially when filtering by combinations of dates and symbols. It does not produce visible output either; it simply prepares a shorthand that will make later data slicing statements shorter and clearer.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;63353e56-d3d1-42cf-8e0c-a566dc6d23f6&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">DATA_STORE = '../data/assets.h5'</code></pre></div><p>This line sets up a file path that points to an HDF5 data store named assets.h5 in a parent data directory. It acts as a convenient reference for later cells so the notebook can open the same dataset without repeating the full path each time. Nothing is loaded or displayed yet, so there is no visible output. The statement simply stores the location as a variable, making the data source easy to reuse if the notebook needs to read from that file later.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;04c799c3-be0a-4413-ab44-c79169780818&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">results_path = Path('results')
if not results_path.exists():
    results_path.mkdir()
    
checkpoint_path = results_path / 'logs'</code></pre></div><p>The purpose here is to make sure the folder structure for the experiment exists before any model training or saving begins. First, a path object is created for a directory named results, which is where later outputs such as scores, plots, and prediction files will be stored. The next step checks whether that directory is already present on disk. If it is missing, the directory is created so the notebook will not run into file-saving errors later.</p><p>After that, a second path is defined for a logs subfolder inside results. This subfolder is intended for checkpoint files, which are the saved model weights written during training. Nothing is written yet, so there is no visible output from the cell. Its role is simply preparatory: it sets up the locations that later cells will use when they need to persist training progress and experiment results.</p><h2>Build a stock return series for forecasting asset price changes</h2><p>To build the trading strategy, we rely on daily stock returns for about 995 US stocks over the eight-year span from 2010 through 2017. We combine these returns with the Chapter 12 feature set, which includes volatility and momentum measures, along with lagged returns ranked both across the full cross-section and within sectors.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;72fa1370-4e67-4ce0-ad38-af49f1973665&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">data = pd.read_hdf('../12_gradient_boosting_machines/data.h5', 'model_data').dropna().sort_index()</code></pre></div><p>The first step is to load the prepared modeling dataset from the HDF5 file used earlier in the workflow. The data are read under the key for the main model table, then any rows with missing values are removed so the later training and evaluation steps work with a clean set of observations. After that, the rows are sorted by their index, which is important when the data are organized by time and asset because later time-series operations assume the records are in a consistent order. Nothing is displayed here because the cell is simply preparing the dataset in memory, so its effect is to set up a clean, ordered DataFrame for the next steps rather than to produce visible output.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;e7eeedbd-7521-4715-a228-a957cc0c633f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">data.info(show_counts=True)</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;7a7d7f09-c7fc-410a-8e2e-c0c59a4a397d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">&lt;class 'pandas.core.frame.DataFrame'&gt;
MultiIndex: 1911344 entries, ('A', Timestamp('2010-04-06 00:00:00')) to ('ZION', Timestamp('2017-11-29 00:00:00'))
Data columns (total 34 columns):
 #   Column           Non-Null Count    Dtype  
---  ------           --------------    -----  
 0   dollar_vol       1911344 non-null  float64
 1   dollar_vol_rank  1911344 non-null  float64
 2   rsi              1911344 non-null  float64
 3   bb_high          1911344 non-null  float64
 4   bb_low           1911344 non-null  float64
 5   NATR             1911344 non-null  float64
 6   ATR              1911344 non-null  float64
 7   PPO              1911344 non-null  float64
 8   MACD             1911344 non-null  float64
 9   sector           1911344 non-null  int64  
 10  r01              1911344 non-null  float64
 11  r05              1911344 non-null  float64
 12  r10              1911344 non-null  float64
 13  r21              1911344 non-null  float64
 14  r42              1911344 non-null  float64
 15  r63              1911344 non-null  float64
 16  r01dec           1911344 non-null  float64
 17  r05dec           1911344 non-null  float64
 18  r10dec           1911344 non-null  float64
 19  r21dec           1911344 non-null  float64
 20  r42dec           1911344 non-null  float64
 21  r63dec           1911344 non-null  float64
 22  r01q_sector      1911344 non-null  float64
 23  r05q_sector      1911344 non-null  float64
 24  r10q_sector      1911344 non-null  float64
 25  r21q_sector      1911344 non-null  float64
 26  r42q_sector      1911344 non-null  float64
 27  r63q_sector      1911344 non-null  float64
 28  r01_fwd          1911344 non-null  float64
 29  r05_fwd          1911344 non-null  float64
 30  r21_fwd          1911344 non-null  float64
 31  year             1911344 non-null  int64  
 32  month            1911344 non-null  int64  
 33  weekday          1911344 non-null  int64  
dtypes: float64(30), int64(4)
memory usage: 503.9+ MB</code></pre></div><p>The purpose here is to take a quick inventory of the dataset before any modeling happens, so you can confirm its shape, indexing, column types, and whether anything is missing. When the dataframe&#8217;s info summary is printed, it shows that the data is organized as a MultiIndex with 1,911,344 rows, running from ticker A on 2010-04-06 through ticker ZION on 2017-11-29. That tells you immediately this is panel data: many securities observed across many dates.</p><p>The column listing confirms that the table contains 34 variables in total. Most of them are floating-point features, which is what you would expect for market indicators, returns, and transformed return features. A few columns are integers, including the sector label and the calendar-time fields for year, month, and weekday. Seeing the non-null counts all match the total row count is especially useful, because it shows that every column is fully populated here, so there are no missing values to impute or filter out before training. The memory usage line at the end also gives a sense of scale: this is a fairly large dataframe, taking about 504 MB in memory, which explains why later steps like cross-validation and repeated model fitting will be computationally expensive.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;6b80f38b-9cf5-4249-aea0-80a3ba5fe589&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">outcomes = data.filter(like='fwd').columns.tolist()</code></pre></div><p>The cell is identifying which columns in the dataset represent future return targets. It looks through the data and keeps only the column names that contain the string indicating a forward-looking return, then converts that filtered set into a regular Python list. The result is stored for later use as the collection of outcome variables, which makes it easier to separate prediction targets from the feature columns. Since the cell only creates this list and does not print anything or display a table, there is no saved output.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;40b03403-9de8-4c3f-adb4-cb01adaac9a8&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">lookahead = 1
outcome= f'r{lookahead:02}_fwd'</code></pre></div><p>This cell sets up the prediction target for the model. It first chooses a one-day lookahead, meaning the model will be trained to predict what happens on the next trading day rather than several days or weeks ahead. From that number, it then builds the name of the outcome column in the dataset by formatting the lookahead as a two-digit value and attaching the forward-return label. With a lookahead of 1, the resulting target name becomes the one-day forward return field, which is the variable the later modeling steps will use as the response. Since the cell only assigns these values and does not display anything, there is no visible output.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;7f115121-80bf-489d-b877-fe6fd6e4a76d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">X_cv = data.loc[idx[:, :'2017'], :].drop(outcomes, axis=1)
y_cv = data.loc[idx[:, :'2017'], outcome]</code></pre></div><p>The cell prepares the modeling data for cross-validation by splitting the full dataset into input features and the prediction target, while also restricting both to the historical period through 2017. It uses the multi-index on the data to select all symbols but only dates up to and including 2017, which keeps the later evaluation focused on past information rather than more recent observations. From that filtered slice, the first line removes all outcome columns so only the explanatory variables remain in the feature set used for training and validation. The second line pulls out the single target series identified earlier, which is the one-day-ahead return the model is meant to predict. Since the cell only assigns these two objects and does not print, plot, or return anything, there is no saved output.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;c0f481eb-5ddc-4dec-ac44-bb6dfd8c3303&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">len(X_cv.index.get_level_values('symbol').unique())</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;73497dc8-4791-4799-8a8f-d40d955eeb4b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">995</code></pre></div><p>The cell checks how many distinct assets are represented in the cross-validation dataset by taking the unique values from the symbol level of the index and counting them. Because the data is organized as a panel, each symbol corresponds to one asset or stock, so this count tells us how broad the universe is. The result, 995, means there are 995 different symbols included in the modeling sample. That number is useful for understanding the scale of the dataset and how many separate asset histories the model is learning from across the time periods used in training and validation.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;1039d149-06a6-4a19-b715-c52528d88cf2&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">X_cv.info(null_counts=True)</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;67ab054b-02f0-45a9-aee1-de21abf59159&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">&lt;class 'pandas.core.frame.DataFrame'&gt;
MultiIndex: 1911344 entries, ('A', Timestamp('2010-04-06 00:00:00')) to ('ZION', Timestamp('2017-11-29 00:00:00'))
Data columns (total 31 columns):
 #   Column           Non-Null Count    Dtype  
---  ------           --------------    -----  
 0   dollar_vol       1911344 non-null  float64
 1   dollar_vol_rank  1911344 non-null  float64
 2   rsi              1911344 non-null  float64
 3   bb_high          1911344 non-null  float64
 4   bb_low           1911344 non-null  float64
 5   NATR             1911344 non-null  float64
 6   ATR              1911344 non-null  float64
 7   PPO              1911344 non-null  float64
 8   MACD             1911344 non-null  float64
 9   sector           1911344 non-null  int64  
 10  r01              1911344 non-null  float64
 11  r05              1911344 non-null  float64
 12  r10              1911344 non-null  float64
 13  r21              1911344 non-null  float64
 14  r42              1911344 non-null  float64
 15  r63              1911344 non-null  float64
 16  r01dec           1911344 non-null  float64
 17  r05dec           1911344 non-null  float64
 18  r10dec           1911344 non-null  float64
 19  r21dec           1911344 non-null  float64
 20  r42dec           1911344 non-null  float64
 21  r63dec           1911344 non-null  float64
 22  r01q_sector      1911344 non-null  float64
 23  r05q_sector      1911344 non-null  float64
 24  r10q_sector      1911344 non-null  float64
 25  r21q_sector      1911344 non-null  float64
 26  r42q_sector      1911344 non-null  float64
 27  r63q_sector      1911344 non-null  float64
 28  year             1911344 non-null  int64  
 29  month            1911344 non-null  int64  
 30  weekday          1911344 non-null  int64  
dtypes: float64(27), int64(4)
memory usage: 460.1+ MB</code></pre></div><p>The purpose here is to inspect the feature matrix being used for model training and make sure it is fully populated and structured the way the later neural network workflow expects. Calling the DataFrame&#8217;s info display gives a compact inventory of the dataset: how many rows it contains, how the index is organized, which columns are present, what types they have, and whether any values are missing.</p><p>The output shows that the data is indexed by a MultiIndex with 1,911,344 rows, running from the earliest asset-date pair through the latest one shown. That tells us this is panel-style market data, where each row corresponds to one stock on one trading day. There are 31 feature columns in total, covering things like volume, technical indicators, past-return measures, sector identity, and simple calendar variables such as year, month, and weekday. The fact that every column reports the full number of non-null values means the matrix is complete at this stage, so there are no missing entries left to interfere with scaling, cross-validation, or neural network training. The mix of float64 and int64 types also shows that most inputs are continuous numerical features, with a few integer-coded categorical or date-related fields. The large memory usage confirms that this is a substantial dataset, which explains why the later modeling steps rely on careful time-based splits and efficient training rather than simple one-shot fitting.</p><h2>Automate model creation</h2><p>The <code>make_model</code> function below shows how the architecture search can be configured in a flexible way. The <code>dense_layers</code> argument supplies a list of integers that determines both how many hidden layers the network has and how many units each layer contains. Dropout is included as a regularization setting and is given as a decimal between 0 and 1, representing the chance that a unit will be omitted during a training step.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;f8740de6-b093-4bf9-aa77-15bbb351ab1a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def make_model(dense_layers, activation, dropout):
    '''Creates a multi-layer perceptron model
    
    dense_layers: List of layer sizes; one number per layer
    '''

    model = Sequential()
    for i, layer_size in enumerate(dense_layers, 1):
        if i == 1:
            model.add(Dense(layer_size, input_dim=X_cv.shape[1]))
            model.add(Activation(activation))
        else:
            model.add(Dense(layer_size))
            model.add(Activation(activation))
    model.add(Dropout(dropout))
    model.add(Dense(1))

    model.compile(loss='mean_squared_error',
                  optimizer='Adam')

    return model</code></pre></div><p>This cell sets up a reusable recipe for building a feedforward neural network, so later experiments can create many different model variants without rewriting the architecture each time. The function takes three choices as inputs: the sizes of the hidden layers, the activation function to apply after each layer, and the dropout rate to use near the end.</p><p>The model is constructed layer by layer using a sequential stack, which means each layer feeds directly into the next. For the first hidden layer, the network also needs to know the number of input features, so the function uses the width of the training feature matrix to set that input size. After that first layer, the same activation function is applied, and the pattern repeats for any additional hidden layers listed in the layer-size sequence. This makes the function flexible enough to handle shallow or deeper networks with different widths.</p><p>Once the hidden layers are in place, the model adds dropout, which randomly switches off a fraction of units during training to reduce overfitting and encourage the network to learn more robust patterns. The final layer has a single output neuron because the task is to predict one continuous value: the next-day return. The model is then compiled with mean squared error as the loss function, which is standard for regression, and Adam as the optimizer, which will handle the weight updates during training.</p><p>There is no saved output from the cell because nothing is being trained or displayed yet. The cell simply defines the function so that it can be called later to generate specific model versions for the different hyperparameter combinations.</p><h2>Evaluate several model settings with TensorFlow through cross-validation</h2><h3>Splitting the Data into Training and Test Sets</h3><p>We divide the sample so that cross-validation is done on the training portion, while the most recent 12 months of available observations are reserved as the holdout test set:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;8b090b6a-0d62-4974-8416-676f1ca8330e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">n_splits = 12
train_period_length=21 * 12 * 4
test_period_length=21 * 3</code></pre></div><p>This cell sets the basic time-series cross-validation schedule that will be used later in the experiment. It chooses 12 separate splits, which means the model will be evaluated repeatedly on different train-and-test windows rather than on one single division of the data. The training window is defined as 21 trading days times 12 months times 4 years, so each fold uses about four years of daily data for fitting the model. The test window is set to 21 trading days times 3 months, which gives roughly one quarter of a year for validation in each split.</p><p>Nothing is displayed because the cell only assigns values to variables. Those settings are important behind the scenes because they control how much history the model sees before each validation period and how long each out-of-sample evaluation period lasts. Later cells use these numbers when the rolling time-series splitter is created, so this small setup step determines the structure of the entire cross-validation procedure.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;faec1c9e-3145-44d9-88ba-a34e0ea174c1&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">cv = MultipleTimeSeriesCV(n_splits=n_splits,
                          train_period_length=train_period_length,
                          test_period_length=test_period_length,
                          lookahead=lookahead)</code></pre></div><p>This cell sets up the time-based cross-validation scheme that will be used for the model experiment. Instead of splitting the data randomly, it creates a custom splitter designed for financial time series, where preserving chronological order matters. The number of folds, the length of each training window, the length of each validation window, and the one-day lookahead are all passed in here so the splitter knows exactly how to carve the historical data into rolling train-and-test periods.</p><p>Behind the scenes, this object does not train anything yet or produce predictions. It simply prepares the rules for how the data will be divided later on, making sure each validation fold comes after its corresponding training fold in time. That setup is essential for avoiding look-ahead bias and for getting a realistic estimate of how the model might perform on future data. Since the cell only creates the splitter and does not print or display anything, there is no visible output when it runs.</p><h3>Set up the cross-validation settings</h3><p>Now we only need to set up the Keras model with <code>make_model</code>, configure cross-validation with the <code>OneStepTimeSeriesSplit</code> approach discussed in chapter 6 on The Machine Learning Process and the sections that follow, and specify the hyperparameters for the search.</p><p>The experiment considers a handful of network layouts with one or two hidden layers, together with both relu and tanh activations and several dropout settings. It would also be reasonable to test alternative optimizers, but that variation was left out so the study would stay within the bounds of an already expensive computation.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;0da27164-36e8-4dad-930a-43cdd9c293f3&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">dense_layer_opts = [(16, 8), (32, 16), (32, 32), (64, 32)]
activation_opts = ['tanh']
dropout_opts = [0, .1, .2]</code></pre></div><p>The purpose here is to set up the search space for the neural network experiment by defining which architectural choices will be tried later. One list contains the possible pairs of hidden-layer sizes, so each option represents a two-layer feedforward network with different widths. Another list fixes the activation function choices, and here only one activation is included, so every model in this stage will use the same nonlinearity. A third list specifies the dropout rates to test, ranging from no dropout to moderate regularization.</p><p>Nothing is displayed because the cell is only creating these option lists in memory. The lack of output is exactly what you would expect: there is no calculation to summarize yet, just a small configuration step that prepares the combinations for the upcoming model runs.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;91884d08-d189-4edb-a577-b3aa4fe29631&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">param_grid = list(product(dense_layer_opts, activation_opts, dropout_opts))
np.random.shuffle(param_grid)</code></pre></div><p>The purpose of this cell is to build the list of neural network configurations that will be tested later and then randomize their order. It starts by combining the available choices for layer sizes, activation functions, and dropout rates into every possible parameter combination. Each combination represents one candidate model setup, so turning those options into a full grid gives the search procedure a clear set of architectures to try.</p><p>After that, the order of the combinations is shuffled. That does not change which models will be evaluated, but it does change the sequence in which they are visited. Shuffling is useful because it prevents the experiment from always running the settings in the same fixed order, which can make the search feel more balanced and can help avoid any unintended dependence on ordering. Since the cell only prepares a Python list in memory and does not print anything or create a figure, there is no saved output.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;31a9e94a-8215-4c0e-aaf8-91a50a37a514&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">len(param_grid)</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;d221836b-8a21-47ae-bb3f-772b491cb775&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">12</code></pre></div><p>The cell is checking how many hyperparameter combinations are in the search space. Since the earlier setup builds a list of model settings by combining the available layer configurations, activation choice, and dropout rates, taking the length of that list gives the total number of distinct model variants that will be tried. The result, 12, reflects the full set of combinations currently defined in the grid, so it confirms that the experiment will evaluate a dozen different configurations rather than just a small handful or an empty set.</p><p>To start the hyperparameter search, we create a <code>GridSearchCV</code> instance, set the <code>fit_params</code> that will be forwarded to the Keras model&#8217;s <code>fit</code> call, and then supply the training data when calling <code>fit</code> on the <code>GridSearchCV</code> object:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;e928b204-b0cf-454e-a37e-e7369cfffc25&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def get_train_valid_data(X, y, train_idx, test_idx):
    x_train, y_train = X.iloc[train_idx, :], y.iloc[train_idx]
    x_val, y_val = X.iloc[test_idx, :], y.iloc[test_idx]
    return x_train, y_train, x_val, y_val</code></pre></div><p>A small helper function is being set up here to make the cross-validation loop easier to read and reuse. Its job is simply to take the full feature table and target series, along with two sets of row positions that mark the training period and the validation period, and split the data into the four pieces needed for model fitting and evaluation.</p><p>The function pulls the training features and targets from the rows indicated by the training indices, then does the same for the validation rows. Because the data is stored in pandas objects, it uses positional selection, which means the split follows the exact index positions produced by the time-series cross-validation scheme rather than any label-based lookup. That matters in a financial time-series setting, where preserving the intended order of observations is essential and random shuffling would break the logic of the evaluation.</p><p>At the end, the function returns the four subsets in a fixed order: training features, training targets, validation features, and validation targets. There is no saved output because nothing is meant to be displayed yet; the cell only defines a reusable utility that later cells can call repeatedly during model training and testing.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;5ede38a2-14fc-431f-8150-5236b0263fc8&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">ic = []
scaler = StandardScaler()
for params in param_grid:
    dense_layers, activation, dropout = params
    for batch_size in [64, 256]:
        print(dense_layers, activation, dropout, batch_size)
        checkpoint_dir = checkpoint_path / str(dense_layers) / activation / str(dropout) / str(batch_size)
        if not checkpoint_dir.exists():
            checkpoint_dir.mkdir(parents=True, exist_ok=True)
        start = time()
        for fold, (train_idx, test_idx) in enumerate(cv.split(X_cv)):
            # get train &amp; validation data
            x_train, y_train, x_val, y_val = get_train_valid_data(X_cv, y_cv, train_idx, test_idx)
            
            # scale features
            x_train = scaler.fit_transform(x_train)
            x_val = scaler.transform(x_val)
            
            # set up dataframes to log results
            preds = y_val.to_frame('actual')
            r = pd.DataFrame(index=y_val.groupby(level='date').size().index)
            
            # create model based on validation parameters
            model = make_model(dense_layers, activation, dropout)
            
            # cross-validate for 20 epochs
            for epoch in range(20):            
                model.fit(x_train,
                          y_train,
                          batch_size=batch_size,
                          epochs=1,
                          verbose=0,
                          shuffle=True,
                          validation_data=(x_val, y_val))
                model.save_weights((checkpoint_dir / f'ckpt_{fold}_{epoch}').as_posix())
                preds[epoch] = model.predict(x_val).squeeze()
                r[epoch] = preds.groupby(level='date').apply(lambda x: spearmanr(x.actual, x[epoch])[0]).to_frame(epoch)
                print(format_time(time()-start), f'{fold + 1:02d} | {epoch + 1:02d} | {r[epoch].mean():7.4f} | {r[epoch].median():7.4f}')
            ic.append(r.assign(dense_layers=str(dense_layers), 
                               activation=activation, 
                               dropout=dropout,
                               batch_size=batch_size,
                               fold=fold))       

        t = time()-start
        pd.concat(ic).to_hdf(results_path / 'scores.h5', 'ic_by_day')</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;c59e3b95-a18d-4151-ac4e-bfda65c5015a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">(64, 32) tanh 0.1 64
00:00:10 01 | 01 | -0.0051 |  0.0013
00:00:18 01 | 02 |  0.0002 | -0.0062
00:00:26 01 | 03 |  0.0110 | -0.0003
00:00:34 01 | 04 |  0.0144 |  0.0097
00:00:42 01 | 05 |  0.0070 |  0.0061
00:00:50 01 | 06 |  0.0289 |  0.0227
00:00:59 01 | 07 |  0.0156 |  0.0342
00:01:07 01 | 08 |  0.0090 |  0.0127
00:01:15 01 | 09 |  0.0084 |  0.0096
00:01:23 01 | 10 |  0.0172 |  0.0296
00:01:31 01 | 11 |  0.0165 |  0.0217
00:01:39 01 | 12 |  0.0016 |  0.0086
00:01:47 01 | 13 |  0.0191 |  0.0344
00:01:55 01 | 14 |  0.0088 |  0.0064
00:02:03 01 | 15 |  0.0058 |  0.0121
00:02:10 01 | 16 |  0.0203 |  0.0227
00:02:18 01 | 17 |  0.0220 |  0.0177
00:02:26 01 | 18 |  0.0140 |  0.0083
00:02:34 01 | 19 | -0.0011 |  0.0009
00:02:41 01 | 20 |  0.0048 |  0.0015
00:02:50 02 | 01 |  0.0008 | -0.0016
00:02:58 02 | 02 | -0.0116 | -0.0011
00:03:06 02 | 03 |  0.0050 |  0.0096
00:03:14 02 | 04 | -0.0017 |  0.0076
00:03:22 02 | 05 | -0.0000 | -0.0086
00:03:30 02 | 06 |  0.0187 |  0.0401
00:03:39 02 | 07 | -0.0055 |  0.0081
00:03:48 02 | 08 |  0.0009 |  0.0016
00:03:57 02 | 09 |  0.0085 |  0.0039
00:04:05 02 | 10 | -0.0056 |  0.0070
00:04:13 02 | 11 |  0.0095 |  0.0083
00:04:21 02 | 12 |  0.0073 | -0.0036
00:04:29 02 | 13 |  0.0066 |  0.0119
00:04:38 02 | 14 | -0.0019 |  0.0046
00:04:46 02 | 15 | -0.0227 | -0.0106
00:04:54 02 | 16 |  0.0030 |  0.0049
00:05:03 02 | 17 | -0.0115 |  0.0013
00:05:11 02 | 18 | -0.0062 | -0.0058
00:05:19 02 | 19 |  0.0136 |  0.0137
00:05:28 02 | 20 |  0.0096 | -0.0055
00:05:37 03 | 01 |  0.0217 |  0.0118
00:05:45 03 | 02 |  0.0093 |  0.0221
00:05:54 03 | 03 |  0.0027 | -0.0191
00:06:02 03 | 04 |  0.0124 |  0.0063
00:06:10 03 | 05 |  0.0031 | -0.0039
00:06:18 03 | 06 | -0.0156 | -0.0395
00:06:27 03 | 07 |  0.0055 | -0.0013
00:06:35 03 | 08 |  0.0029 | -0.0028
00:06:43 03 | 09 |  0.0149 |  0.0163
00:06:52 03 | 10 | -0.0103 | -0.0060
00:07:00 03 | 11 | -0.0265 | -0.0366
00:07:09 03 | 12 | -0.0069 | -0.0074
00:07:17 03 | 13 | -0.0134 | -0.0183
00:07:26 03 | 14 |  0.0097 | -0.0067
00:07:34 03 | 15 | -0.0076 | -0.0049
00:07:43 03 | 16 | -0.0045 | -0.0078
00:07:52 03 | 17 |  0.0106 |  0.0109
00:08:00 03 | 18 | -0.0216 | -0.0197
00:08:09 03 | 19 | -0.0024 |  0.0017
00:08:17 03 | 20 | -0.0069 |  0.0007
00:08:27 04 | 01 | -0.0329 | -0.0321
00:08:36 04 | 02 | -0.0085 | -0.0062
00:08:44 04 | 03 |  0.0070 |  0.0038
00:08:53 04 | 04 | -0.0123 | -0.0092
00:09:01 04 | 05 |  0.0141 |  0.0021
00:09:10 04 | 06 |  0.0397 |  0.0368
00:09:19 04 | 07 |  0.0160 |  0.0159
00:09:27 04 | 08 | -0.0166 | -0.0049
00:09:36 04 | 09 |  0.0055 |  0.0045
00:09:45 04 | 10 | -0.0035 | -0.0133
00:09:54 04 | 11 |  0.0236 |  0.0080
00:10:04 04 | 12 | -0.0068 | -0.0152
00:10:13 04 | 13 |  0.0244 |  0.0329
00:10:21 04 | 14 |  0.0323 |  0.0091
00:10:30 04 | 15 |  0.0189 |  0.0187
00:10:39 04 | 16 |  0.0270 |  0.0115
00:10:47 04 | 17 | -0.0091 | -0.0087
00:10:56 04 | 18 | -0.0217 | -0.0162
00:11:04 04 | 19 | -0.0476 | -0.0535
00:11:13 04 | 20 |  0.0248 |  0.0033
00:11:23 05 | 01 |  0.0176 |  0.0181
00:11:31 05 | 02 | -0.0133 | -0.0183
00:11:40 05 | 03 | -0.0115 | -0.0144
00:11:48 05 | 04 |  0.0149 |  0.0123
00:11:57 05 | 05 |  0.0087 | -0.0025
00:12:06 05 | 06 |  0.0086 |  0.0118
00:12:14 05 | 07 | -0.0032 |  0.0227
00:12:23 05 | 08 |  0.0021 |  0.0031
00:12:31 05 | 09 | -0.0196 | -0.0109
00:12:40 05 | 10 |  0.0011 |  0.0049
00:12:49 05 | 11 | -0.0096 | -0.0101
00:12:57 05 | 12 | -0.0137 | -0.0151
00:13:06 05 | 13 |  0.0036 | -0.0083
00:13:14 05 | 14 |  0.0038 |  0.0103
00:13:23 05 | 15 | -0.0090 | -0.0090
00:13:32 05 | 16 | -0.0110 | -0.0237
00:13:41 05 | 17 | -0.0170 | -0.0163
00:13:51 05 | 18 | -0.0208 | -0.0472
00:14:00 05 | 19 | -0.0158 | -0.0416
00:14:09 05 | 20 |  0.0097 |  0.0196
00:14:21 06 | 01 |  0.0160 |  0.0209
00:14:32 06 | 02 |  0.0311 |  0.0189
00:14:42 06 | 03 |  0.0388 |  0.0232
00:14:53 06 | 04 |  0.0287 |  0.0140
00:15:04 06 | 05 |  0.0378 |  0.0338
00:15:14 06 | 06 |  0.0268 |  0.0211
00:15:24 06 | 07 |  0.0351 |  0.0266
00:15:34 06 | 08 |  0.0492 |  0.0299
00:15:45 06 | 09 |  0.0553 |  0.0358
00:15:55 06 | 10 |  0.0231 |  0.0139
00:16:04 06 | 11 |  0.0509 |  0.0549
00:16:14 06 | 12 |  0.0290 |  0.0130
00:16:22 06 | 13 |  0.0461 |  0.0541
00:16:30 06 | 14 |  0.0447 |  0.0471
00:16:39 06 | 15 |  0.0285 |  0.0269
00:16:47 06 | 16 |  0.0316 |  0.0406
00:16:55 06 | 17 |  0.0375 |  0.0297
00:17:04 06 | 18 |  0.0189 |  0.0198
00:17:12 06 | 19 |  0.0335 |  0.0258
00:17:20 06 | 20 |  0.0124 | -0.0001
00:17:30 07 | 01 | -0.0086 | -0.0051
00:17:38 07 | 02 | -0.0330 | -0.0368
00:17:47 07 | 03 | -0.0102 | -0.0093
00:17:56 07 | 04 |  0.0154 |  0.0268
00:18:04 07 | 05 |  0.0167 |  0.0251
00:18:13 07 | 06 |  0.0351 |  0.0209
00:18:22 07 | 07 |  0.0137 |  0.0346
00:18:31 07 | 08 |  0.0083 |  0.0199
00:18:40 07 | 09 |  0.0123 |  0.0327
00:18:50 07 | 10 |  0.0364 |  0.0644
00:19:00 07 | 11 |  0.0114 |  0.0116
00:19:10 07 | 12 |  0.0249 |  0.0406
00:19:19 07 | 13 |  0.0299 |  0.0380
00:19:28 07 | 14 |  0.0158 |  0.0409
00:19:37 07 | 15 |  0.0211 |  0.0420
00:19:46 07 | 16 |  0.0051 |  0.0080
00:19:55 07 | 17 |  0.0242 |  0.0081
00:20:03 07 | 18 | -0.0091 |  0.0219
00:20:11 07 | 19 |  0.0396 |  0.0253
00:20:19 07 | 20 |  0.0252 |  0.0255
00:20:28 08 | 01 |  0.0348 |  0.0529
00:20:36 08 | 02 | -0.0048 | -0.0090
00:20:44 08 | 03 |  0.0302 |  0.0346
00:20:52 08 | 04 |  0.0105 |  0.0146
00:21:01 08 | 05 |  0.0065 |  0.0023
00:21:09 08 | 06 |  0.0241 |  0.0294
00:21:16 08 | 07 |  0.0250 |  0.0208
00:21:24 08 | 08 |  0.0042 |  0.0219
00:21:32 08 | 09 |  0.0050 | -0.0004
00:21:40 08 | 10 |  0.0101 |  0.0002
00:21:48 08 | 11 |  0.0219 |  0.0306
00:21:56 08 | 12 |  0.0137 |  0.0259
00:22:04 08 | 13 |  0.0141 |  0.0153
00:22:13 08 | 14 |  0.0269 |  0.0201
00:22:21 08 | 15 |  0.0081 | -0.0028
00:22:29 08 | 16 |  0.0133 |  0.0099
00:22:37 08 | 17 |  0.0124 |  0.0181
00:22:46 08 | 18 |  0.0182 |  0.0367
00:22:54 08 | 19 |  0.0112 |  0.0293
00:23:01 08 | 20 |  0.0403 |  0.0490
00:23:11 09 | 01 | -0.0044 | -0.0233
00:23:19 09 | 02 |  0.0210 |  0.0274
00:23:27 09 | 03 |  0.0336 |  0.0255
00:23:35 09 | 04 |  0.0127 |  0.0014
00:23:43 09 | 05 |  0.0193 |  0.0180
00:23:51 09 | 06 |  0.0131 |  0.0102
00:23:59 09 | 07 |  0.0206 |  0.0308
00:24:07 09 | 08 |  0.0191 |  0.0124
00:24:15 09 | 09 |  0.0213 |  0.0235
00:24:24 09 | 10 |  0.0131 |  0.0110
00:24:32 09 | 11 |  0.0084 |  0.0018
00:24:40 09 | 12 |  0.0042 |  0.0007
00:24:47 09 | 13 |  0.0243 |  0.0269
00:24:56 09 | 14 |  0.0093 |  0.0106
00:25:04 09 | 15 |  0.0063 |  0.0068
00:25:12 09 | 16 |  0.0095 |  0.0176
00:25:20 09 | 17 |  0.0043 |  0.0161
00:25:28 09 | 18 |  0.0029 |  0.0048
00:25:36 09 | 19 | -0.0015 | -0.0024
00:25:44 09 | 20 |  0.0021 |  0.0113
00:25:53 10 | 01 | -0.0095 | -0.0189
00:26:01 10 | 02 |  0.0327 |  0.0359
00:26:09 10 | 03 | -0.0115 | -0.0056
00:26:17 10 | 04 | -0.0210 | -0.0314
00:26:25 10 | 05 |  0.0008 | -0.0059
00:26:33 10 | 06 |  0.0245 |  0.0065
00:26:41 10 | 07 |  0.0302 |  0.0283
00:26:49 10 | 08 |  0.0057 |  0.0047
00:26:57 10 | 09 | -0.0075 | -0.0049
00:27:05 10 | 10 | -0.0072 |  0.0029
00:27:13 10 | 11 |  0.0265 |  0.0271
00:27:21 10 | 12 |  0.0139 |  0.0329
00:27:29 10 | 13 |  0.0153 |  0.0079
00:27:37 10 | 14 |  0.0189 |  0.0076
00:27:45 10 | 15 | -0.0021 | -0.0152
00:27:53 10 | 16 |  0.0116 | -0.0136
00:28:01 10 | 17 | -0.0029 | -0.0053
00:28:09 10 | 18 |  0.0194 |  0.0204
00:28:17 10 | 19 | -0.0092 | -0.0084
00:28:25 10 | 20 |  0.0312 |  0.0170
00:28:34 11 | 01 |  0.0142 |  0.0038
00:28:42 11 | 02 |  0.0042 |  0.0061
00:28:50 11 | 03 |  0.0084 |  0.0100
00:28:59 11 | 04 |  0.0112 |  0.0030
00:29:07 11 | 05 |  0.0169 |  0.0115
00:29:15 11 | 06 |  0.0274 |  0.0276
00:29:22 11 | 07 |  0.0251 |  0.0199
00:29:30 11 | 08 |  0.0311 |  0.0261
00:29:39 11 | 09 |  0.0182 |  0.0143
00:29:47 11 | 10 |  0.0286 |  0.0286
00:29:55 11 | 11 | -0.0030 | -0.0153
00:30:03 11 | 12 |  0.0414 |  0.0329
00:30:11 11 | 13 |  0.0234 |  0.0162
00:30:19 11 | 14 |  0.0320 |  0.0298
00:30:27 11 | 15 |  0.0307 |  0.0260
00:30:35 11 | 16 |  0.0298 |  0.0339
00:30:43 11 | 17 | -0.0007 | -0.0005
00:30:51 11 | 18 |  0.0156 |  0.0121
00:30:59 11 | 19 |  0.0222 |  0.0177
00:31:08 11 | 20 |  0.0231 |  0.0143
00:31:17 12 | 01 |  0.0076 |  0.0045</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;997199b0-1330-4f27-bab5-0ee51dd2c1d8&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">00:31:25 12 | 02 |  0.0133 |  0.0058
00:31:33 12 | 03 |  0.0094 |  0.0001
00:31:41 12 | 04 |  0.0163 |  0.0168
00:31:49 12 | 05 |  0.0046 |  0.0050
00:31:57 12 | 06 |  0.0111 |  0.0386
00:32:05 12 | 07 |  0.0046 | -0.0144
00:32:14 12 | 08 |  0.0085 |  0.0197
00:32:22 12 | 09 |  0.0056 | -0.0001
00:32:30 12 | 10 | -0.0037 | -0.0360
00:32:38 12 | 11 |  0.0078 |  0.0028
00:32:46 12 | 12 |  0.0073 |  0.0035
00:32:54 12 | 13 |  0.0026 |  0.0058
00:33:02 12 | 14 |  0.0100 |  0.0128
00:33:10 12 | 15 | -0.0003 |  0.0002
00:33:18 12 | 16 | -0.0034 | -0.0100
00:33:26 12 | 17 | -0.0009 | -0.0083
00:33:34 12 | 18 | -0.0100 | -0.0172
00:33:42 12 | 19 | -0.0097 | -0.0171
00:33:51 12 | 20 | -0.0114 | -0.0260
(64, 32) tanh 0.1 256
00:00:05 01 | 01 |  0.0119 | -0.0027
00:00:08 01 | 02 |  0.0188 |  0.0172
00:00:11 01 | 03 |  0.0018 |  0.0017
00:00:15 01 | 04 |  0.0083 |  0.0137
00:00:18 01 | 05 | -0.0015 |  0.0026
00:00:21 01 | 06 |  0.0061 |  0.0183
00:00:24 01 | 07 | -0.0106 | -0.0080
00:00:28 01 | 08 |  0.0195 |  0.0237
00:00:31 01 | 09 | -0.0051 | -0.0121
00:00:34 01 | 10 |  0.0137 |  0.0162
00:00:38 01 | 11 |  0.0146 |  0.0191
00:00:41 01 | 12 |  0.0203 |  0.0295
00:00:44 01 | 13 |  0.0105 |  0.0181
00:00:48 01 | 14 |  0.0119 |  0.0123
00:00:51 01 | 15 |  0.0081 |  0.0031
00:00:54 01 | 16 |  0.0090 |  0.0019
00:00:58 01 | 17 |  0.0098 |  0.0137
00:01:01 01 | 18 |  0.0088 |  0.0059
00:01:04 01 | 19 |  0.0109 |  0.0118
00:01:08 01 | 20 |  0.0010 |  0.0009
00:01:12 02 | 01 |  0.0033 | -0.0003
00:01:15 02 | 02 |  0.0095 |  0.0368
00:01:19 02 | 03 | -0.0104 | -0.0189
00:01:22 02 | 04 | -0.0133 | -0.0014
00:01:25 02 | 05 |  0.0102 | -0.0033
00:01:28 02 | 06 |  0.0209 |  0.0483
00:01:32 02 | 07 |  0.0106 |  0.0025
00:01:35 02 | 08 | -0.0120 | -0.0291
00:01:38 02 | 09 | -0.0005 |  0.0020
00:01:42 02 | 10 |  0.0010 |  0.0020
00:01:45 02 | 11 | -0.0005 |  0.0014
00:01:48 02 | 12 |  0.0136 |  0.0156
00:01:52 02 | 13 |  0.0176 |  0.0111
00:01:55 02 | 14 |  0.0028 | -0.0083
00:01:58 02 | 15 |  0.0096 |  0.0233
00:02:02 02 | 16 | -0.0039 | -0.0022
00:02:05 02 | 17 |  0.0006 |  0.0081
00:02:08 02 | 18 | -0.0033 | -0.0034
00:02:12 02 | 19 | -0.0078 | -0.0064
00:02:15 02 | 20 |  0.0129 | -0.0009
00:02:19 03 | 01 |  0.0006 | -0.0078
00:02:23 03 | 02 |  0.0048 |  0.0032
00:02:26 03 | 03 | -0.0025 | -0.0069
00:02:30 03 | 04 |  0.0226 |  0.0300
00:02:33 03 | 05 | -0.0091 |  0.0086
00:02:36 03 | 06 |  0.0097 |  0.0107
00:02:39 03 | 07 |  0.0172 |  0.0217
00:02:43 03 | 08 |  0.0154 |  0.0149
00:02:46 03 | 09 | -0.0200 | -0.0400
00:02:50 03 | 10 | -0.0241 | -0.0427
00:02:54 03 | 11 | -0.0083 | -0.0120
00:02:57 03 | 12 |  0.0004 | -0.0203
00:03:01 03 | 13 |  0.0016 |  0.0004
00:03:05 03 | 14 | -0.0082 | -0.0282
00:03:08 03 | 15 | -0.0004 | -0.0133
00:03:12 03 | 16 |  0.0068 |  0.0135
00:03:15 03 | 17 | -0.0121 | -0.0440
00:03:19 03 | 18 |  0.0043 | -0.0100
00:03:23 03 | 19 |  0.0038 |  0.0052
00:03:26 03 | 20 |  0.0063 |  0.0077
00:03:32 04 | 01 |  0.0207 |  0.0241
00:03:35 04 | 02 | -0.0041 |  0.0075
00:03:39 04 | 03 | -0.0450 | -0.0648
00:03:43 04 | 04 |  0.0087 | -0.0041
00:03:46 04 | 05 |  0.0005 | -0.0241
00:03:50 04 | 06 | -0.0001 |  0.0110
00:03:54 04 | 07 | -0.0117 | -0.0248
00:03:57 04 | 08 |  0.0125 | -0.0197
00:04:01 04 | 09 |  0.0092 | -0.0083
00:04:05 04 | 10 |  0.0229 |  0.0255
00:04:08 04 | 11 |  0.0242 |  0.0063
00:04:12 04 | 12 | -0.0143 | -0.0230
00:04:16 04 | 13 | -0.0222 | -0.0261
00:04:19 04 | 14 |  0.0089 | -0.0113
00:04:23 04 | 15 |  0.0018 | -0.0034
00:04:27 04 | 16 | -0.0018 |  0.0089
00:04:30 04 | 17 |  0.0120 |  0.0029
00:04:34 04 | 18 | -0.0138 |  0.0051
00:04:37 04 | 19 | -0.0272 | -0.0155
00:04:40 04 | 20 |  0.0084 | -0.0157
00:04:44 05 | 01 | -0.0034 |  0.0032
00:04:48 05 | 02 |  0.0233 |  0.0297
00:04:51 05 | 03 |  0.0007 | -0.0147
00:04:55 05 | 04 |  0.0039 |  0.0097
00:04:58 05 | 05 |  0.0140 |  0.0212
00:05:01 05 | 06 |  0.0037 |  0.0037
00:05:05 05 | 07 | -0.0052 |  0.0042
00:05:08 05 | 08 |  0.0100 |  0.0042
00:05:11 05 | 09 |  0.0155 |  0.0222
00:05:15 05 | 10 |  0.0074 |  0.0070
00:05:18 05 | 11 |  0.0063 |  0.0198
00:05:21 05 | 12 |  0.0054 | -0.0035
00:05:24 05 | 13 |  0.0018 |  0.0124
00:05:28 05 | 14 | -0.0037 |  0.0053
00:05:31 05 | 15 | -0.0079 |  0.0036
00:05:34 05 | 16 |  0.0003 | -0.0011
00:05:37 05 | 17 |  0.0006 | -0.0132
00:05:40 05 | 18 |  0.0032 | -0.0006
00:05:44 05 | 19 | -0.0043 | -0.0053
00:05:47 05 | 20 |  0.0035 |  0.0066
00:05:51 06 | 01 |  0.0159 | -0.0027
00:05:55 06 | 02 | -0.0193 | -0.0404
00:05:58 06 | 03 |  0.0241 |  0.0186
00:06:01 06 | 04 | -0.0183 | -0.0256
00:06:05 06 | 05 |  0.0223 |  0.0153
00:06:08 06 | 06 |  0.0295 |  0.0297
00:06:12 06 | 07 |  0.0233 |  0.0397
00:06:16 06 | 08 |  0.0199 |  0.0220
00:06:19 06 | 09 |  0.0224 |  0.0176
00:06:22 06 | 10 |  0.0216 |  0.0288
00:06:25 06 | 11 |  0.0275 |  0.0382
00:06:28 06 | 12 |  0.0174 | -0.0008
00:06:32 06 | 13 |  0.0208 |  0.0278
00:06:35 06 | 14 |  0.0236 |  0.0281
00:06:38 06 | 15 |  0.0142 |  0.0166
00:06:41 06 | 16 |  0.0104 | -0.0084
00:06:45 06 | 17 |  0.0327 |  0.0212
00:06:48 06 | 18 |  0.0080 |  0.0154
00:06:51 06 | 19 |  0.0191 |  0.0286
00:06:55 06 | 20 |  0.0238 |  0.0203
00:06:60 07 | 01 |  0.0196 |  0.0165
00:07:03 07 | 02 |  0.0128 |  0.0306
00:07:06 07 | 03 |  0.0223 |  0.0254
00:07:10 07 | 04 |  0.0194 |  0.0137
00:07:13 07 | 05 |  0.0222 | -0.0005
00:07:16 07 | 06 |  0.0249 |  0.0227
00:07:20 07 | 07 |  0.0362 |  0.0333
00:07:23 07 | 08 |  0.0265 |  0.0638
00:07:26 07 | 09 |  0.0140 |  0.0234
00:07:30 07 | 10 |  0.0088 | -0.0073
00:07:33 07 | 11 |  0.0036 | -0.0019
00:07:37 07 | 12 |  0.0025 | -0.0048
00:07:41 07 | 13 |  0.0299 | -0.0081
00:07:45 07 | 14 |  0.0144 |  0.0038
00:07:48 07 | 15 |  0.0180 | -0.0050
00:07:52 07 | 16 |  0.0166 | -0.0002
00:07:56 07 | 17 | -0.0061 | -0.0225
00:07:59 07 | 18 |  0.0004 | -0.0268
00:08:03 07 | 19 |  0.0114 | -0.0018
00:08:07 07 | 20 |  0.0194 | -0.0033
00:08:11 08 | 01 |  0.0005 |  0.0176
00:08:15 08 | 02 |  0.0077 |  0.0174
00:08:18 08 | 03 |  0.0099 |  0.0030
00:08:22 08 | 04 |  0.0282 |  0.0265
00:08:25 08 | 05 |  0.0358 |  0.0127
00:08:28 08 | 06 |  0.0214 |  0.0228
00:08:32 08 | 07 |  0.0378 |  0.0379
00:08:35 08 | 08 |  0.0026 |  0.0193
00:08:39 08 | 09 |  0.0258 |  0.0073
00:08:42 08 | 10 |  0.0143 |  0.0070
00:08:46 08 | 11 |  0.0150 |  0.0113
00:08:50 08 | 12 |  0.0308 |  0.0253
00:08:53 08 | 13 |  0.0238 |  0.0238
00:08:57 08 | 14 |  0.0239 |  0.0207
00:09:01 08 | 15 |  0.0256 |  0.0302
00:09:04 08 | 16 |  0.0363 |  0.0122
00:09:08 08 | 17 |  0.0315 |  0.0242
00:09:12 08 | 18 |  0.0353 |  0.0207
00:09:15 08 | 19 |  0.0210 |  0.0131
00:09:19 08 | 20 |  0.0141 |  0.0059
00:09:23 09 | 01 |  0.0041 | -0.0001
00:09:27 09 | 02 | -0.0143 | -0.0108
00:09:31 09 | 03 |  0.0030 | -0.0066
00:09:34 09 | 04 |  0.0021 |  0.0035
00:09:38 09 | 05 |  0.0151 |  0.0130
00:09:41 09 | 06 | -0.0010 | -0.0111
00:09:45 09 | 07 |  0.0341 |  0.0414
00:09:49 09 | 08 |  0.0169 |  0.0041
00:09:52 09 | 09 |  0.0259 |  0.0278
00:09:56 09 | 10 |  0.0287 |  0.0204
00:09:59 09 | 11 |  0.0166 |  0.0116
00:10:03 09 | 12 |  0.0189 |  0.0226
00:10:07 09 | 13 |  0.0208 |  0.0218
00:10:10 09 | 14 |  0.0147 |  0.0059
00:10:14 09 | 15 |  0.0158 |  0.0103
00:10:17 09 | 16 |  0.0152 |  0.0178
00:10:21 09 | 17 |  0.0154 |  0.0128
00:10:25 09 | 18 |  0.0192 |  0.0172
00:10:28 09 | 19 |  0.0216 |  0.0297
00:10:32 09 | 20 |  0.0190 |  0.0220
00:10:37 10 | 01 | -0.0383 | -0.0742
00:10:41 10 | 02 | -0.0239 | -0.0390
00:10:45 10 | 03 |  0.0238 |  0.0204
00:10:48 10 | 04 |  0.0198 |  0.0339
00:10:52 10 | 05 | -0.0304 | -0.0322
00:10:55 10 | 06 | -0.0298 | -0.0623
00:10:59 10 | 07 | -0.0490 | -0.0721
00:11:03 10 | 08 | -0.0122 | -0.0267
00:11:06 10 | 09 | -0.0058 | -0.0192
00:11:10 10 | 10 | -0.0216 | -0.0174
00:11:14 10 | 11 | -0.0182 | -0.0363
00:11:17 10 | 12 | -0.0301 | -0.0314
00:11:21 10 | 13 | -0.0158 | -0.0314
00:11:25 10 | 14 | -0.0175 | -0.0284
00:11:28 10 | 15 | -0.0187 | -0.0221
00:11:32 10 | 16 | -0.0207 | -0.0441
00:11:36 10 | 17 |  0.0074 | -0.0023
00:11:39 10 | 18 |  0.0054 |  0.0012
00:11:43 10 | 19 |  0.0041 |  0.0050
00:11:47 10 | 20 |  0.0040 | -0.0033
00:11:51 11 | 01 | -0.0119 | -0.0027
00:11:55 11 | 02 |  0.0309 |  0.0362</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;4640c373-529d-4e37-8a1a-56e04d5950f4&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">00:11:58 11 | 03 |  0.0428 |  0.0488
00:12:02 11 | 04 |  0.0250 |  0.0235
00:12:06 11 | 05 |  0.0270 |  0.0318
00:12:09 11 | 06 |  0.0335 |  0.0354
00:12:13 11 | 07 |  0.0004 | -0.0036
00:12:17 11 | 08 | -0.0034 | -0.0096
00:12:20 11 | 09 | -0.0067 | -0.0123
00:12:24 11 | 10 |  0.0102 |  0.0104
00:12:27 11 | 11 |  0.0214 |  0.0292
00:12:31 11 | 12 |  0.0237 |  0.0220
00:12:35 11 | 13 |  0.0223 |  0.0176
00:12:38 11 | 14 |  0.0265 |  0.0132
00:12:42 11 | 15 |  0.0206 |  0.0175
00:12:45 11 | 16 |  0.0266 |  0.0145
00:12:49 11 | 17 |  0.0335 |  0.0355
00:12:53 11 | 18 |  0.0210 |  0.0107
00:12:56 11 | 19 |  0.0424 |  0.0297
00:12:60 11 | 20 |  0.0197 |  0.0153
00:13:04 12 | 01 |  0.0070 |  0.0164
00:13:08 12 | 02 |  0.0086 |  0.0021
00:13:11 12 | 03 |  0.0222 |  0.0245
00:13:14 12 | 04 |  0.0098 | -0.0140
00:13:18 12 | 05 |  0.0057 | -0.0068
00:13:21 12 | 06 |  0.0075 | -0.0103
00:13:24 12 | 07 |  0.0223 |  0.0186
00:13:28 12 | 08 |  0.0317 |  0.0217
00:13:31 12 | 09 |  0.0053 | -0.0047
00:13:34 12 | 10 |  0.0149 | -0.0066
00:13:38 12 | 11 |  0.0145 |  0.0028
00:13:41 12 | 12 |  0.0054 | -0.0074
00:13:44 12 | 13 |  0.0070 | -0.0210
00:13:47 12 | 14 |  0.0056 | -0.0104
00:13:51 12 | 15 |  0.0042 | -0.0095
00:13:54 12 | 16 |  0.0135 |  0.0147
00:13:57 12 | 17 |  0.0012 | -0.0201
00:14:01 12 | 18 |  0.0097 |  0.0226
00:14:04 12 | 19 | -0.0076 | -0.0315
00:14:08 12 | 20 |  0.0014 |  0.0070
(64, 32) tanh 0 64
00:00:10 01 | 01 |  0.0016 |  0.0012
00:00:17 01 | 02 |  0.0195 |  0.0222
00:00:25 01 | 03 |  0.0151 |  0.0153
00:00:33 01 | 04 |  0.0063 |  0.0289
00:00:41 01 | 05 |  0.0060 |  0.0056
00:00:49 01 | 06 |  0.0064 |  0.0246
00:00:57 01 | 07 |  0.0143 |  0.0093
00:01:05 01 | 08 |  0.0131 |  0.0169
00:01:13 01 | 09 |  0.0139 |  0.0225
00:01:21 01 | 10 |  0.0143 |  0.0039
00:01:29 01 | 11 |  0.0058 | -0.0056
00:01:38 01 | 12 |  0.0165 |  0.0102
00:01:47 01 | 13 |  0.0122 |  0.0136
00:01:55 01 | 14 |  0.0134 |  0.0250
00:02:04 01 | 15 |  0.0273 |  0.0357
00:02:13 01 | 16 |  0.0100 |  0.0103
00:02:21 01 | 17 |  0.0218 |  0.0276
00:02:29 01 | 18 |  0.0152 |  0.0095
00:02:37 01 | 19 |  0.0124 |  0.0087
00:02:45 01 | 20 |  0.0233 |  0.0118
00:02:55 02 | 01 | -0.0212 | -0.0234
00:03:03 02 | 02 |  0.0220 |  0.0289
00:03:11 02 | 03 |  0.0218 |  0.0163
00:03:19 02 | 04 | -0.0200 | -0.0061
00:03:27 02 | 05 |  0.0004 | -0.0040
00:03:35 02 | 06 |  0.0186 |  0.0151
00:03:43 02 | 07 |  0.0016 |  0.0028
00:03:50 02 | 08 |  0.0013 |  0.0209
00:03:58 02 | 09 |  0.0103 |  0.0183
00:04:06 02 | 10 | -0.0213 | -0.0206
00:04:14 02 | 11 | -0.0017 | -0.0019
00:04:22 02 | 12 | -0.0012 | -0.0186
00:04:30 02 | 13 |  0.0088 |  0.0249
00:04:38 02 | 14 | -0.0045 | -0.0006
00:04:46 02 | 15 | -0.0019 |  0.0055
00:04:53 02 | 16 |  0.0006 | -0.0110
00:05:01 02 | 17 | -0.0068 | -0.0273
00:05:09 02 | 18 |  0.0003 |  0.0113
00:05:17 02 | 19 |  0.0125 |  0.0110
00:05:25 02 | 20 |  0.0152 |  0.0074
00:05:34 03 | 01 |  0.0105 |  0.0090
00:05:41 03 | 02 | -0.0078 | -0.0017
00:05:48 03 | 03 |  0.0369 |  0.0320
00:05:55 03 | 04 |  0.0122 | -0.0140
00:06:03 03 | 05 | -0.0170 | -0.0361
00:06:10 03 | 06 | -0.0162 | -0.0109
00:06:17 03 | 07 | -0.0179 | -0.0384
00:06:24 03 | 08 | -0.0128 | -0.0273
00:06:32 03 | 09 | -0.0322 | -0.0294
00:06:39 03 | 10 | -0.0228 | -0.0239
00:06:46 03 | 11 |  0.0021 |  0.0050
00:06:53 03 | 12 | -0.0006 |  0.0157
00:07:01 03 | 13 | -0.0081 | -0.0233
00:07:08 03 | 14 | -0.0309 | -0.0461
00:07:15 03 | 15 | -0.0189 | -0.0211
00:07:23 03 | 16 |  0.0082 | -0.0084
00:07:30 03 | 17 | -0.0219 | -0.0220
00:07:37 03 | 18 | -0.0239 | -0.0306
00:07:44 03 | 19 | -0.0012 |  0.0113
00:07:52 03 | 20 |  0.0020 |  0.0078
00:07:60 04 | 01 |  0.0167 |  0.0409
00:08:07 04 | 02 |  0.0143 |  0.0168
00:08:14 04 | 03 | -0.0161 | -0.0162
00:08:22 04 | 04 |  0.0268 |  0.0165
00:08:29 04 | 05 | -0.0443 | -0.0348
00:08:36 04 | 06 | -0.0420 | -0.0576
00:08:44 04 | 07 | -0.0334 | -0.0187
00:08:51 04 | 08 | -0.0006 | -0.0090
00:08:58 04 | 09 | -0.0453 | -0.0507
00:09:06 04 | 10 |  0.0099 | -0.0126
00:09:13 04 | 11 | -0.0242 | -0.0041
00:09:20 04 | 12 | -0.0070 | -0.0126
00:09:28 04 | 13 |  0.0136 | -0.0037
00:09:35 04 | 14 |  0.0345 |  0.0238
00:09:42 04 | 15 |  0.0229 |  0.0165
00:09:49 04 | 16 | -0.0497 | -0.0411
00:09:57 04 | 17 |  0.0001 | -0.0113
00:10:04 04 | 18 |  0.0110 |  0.0077
00:10:11 04 | 19 | -0.0425 | -0.0279
00:10:19 04 | 20 |  0.0039 |  0.0218
00:10:27 05 | 01 | -0.0174 | -0.0010
00:10:34 05 | 02 |  0.0148 |  0.0222
00:10:41 05 | 03 |  0.0108 |  0.0154
00:10:49 05 | 04 |  0.0323 |  0.0340
00:10:56 05 | 05 |  0.0103 | -0.0038
00:11:03 05 | 06 | -0.0015 | -0.0033
00:11:10 05 | 07 |  0.0168 |  0.0158
00:11:18 05 | 08 |  0.0239 |  0.0274
00:11:25 05 | 09 |  0.0132 | -0.0091
00:11:32 05 | 10 |  0.0137 |  0.0124
00:11:40 05 | 11 |  0.0196 |  0.0192
00:11:47 05 | 12 |  0.0061 | -0.0096
00:11:54 05 | 13 | -0.0058 | -0.0004
00:12:02 05 | 14 | -0.0042 |  0.0126
00:12:09 05 | 15 |  0.0287 |  0.0240
00:12:16 05 | 16 |  0.0166 |  0.0119
00:12:24 05 | 17 |  0.0031 |  0.0016
00:12:31 05 | 18 | -0.0047 |  0.0047
00:12:38 05 | 19 |  0.0122 |  0.0123
00:12:46 05 | 20 |  0.0178 |  0.0098
00:12:54 06 | 01 |  0.0234 |  0.0198
00:13:01 06 | 02 | -0.0069 | -0.0034
00:13:08 06 | 03 |  0.0339 |  0.0437
00:13:16 06 | 04 |  0.0345 |  0.0147
00:13:23 06 | 05 |  0.0192 |  0.0177
00:13:30 06 | 06 |  0.0283 |  0.0247
00:13:38 06 | 07 |  0.0152 |  0.0085
00:13:45 06 | 08 | -0.0017 | -0.0013
00:13:52 06 | 09 |  0.0248 |  0.0344
00:13:60 06 | 10 |  0.0380 |  0.0185
00:14:07 06 | 11 |  0.0239 | -0.0020
00:14:15 06 | 12 |  0.0205 | -0.0045
00:14:22 06 | 13 |  0.0334 |  0.0297
00:14:30 06 | 14 |  0.0373 |  0.0172
00:14:37 06 | 15 |  0.0447 |  0.0549
00:14:44 06 | 16 |  0.0126 | -0.0000
00:14:52 06 | 17 |  0.0325 |  0.0281
00:14:59 06 | 18 |  0.0258 |  0.0103
00:15:07 06 | 19 |  0.0453 |  0.0691
00:15:14 06 | 20 |  0.0398 |  0.0487
00:15:22 07 | 01 |  0.0218 |  0.0335
00:15:29 07 | 02 |  0.0376 |  0.0496
00:15:37 07 | 03 |  0.0005 |  0.0194
00:15:44 07 | 04 | -0.0230 | -0.0202
00:15:51 07 | 05 |  0.0054 | -0.0225
00:15:58 07 | 06 |  0.0213 |  0.0189
00:16:06 07 | 07 | -0.0081 | -0.0031
00:16:13 07 | 08 |  0.0345 |  0.0605
00:16:20 07 | 09 |  0.0014 |  0.0095
00:16:28 07 | 10 | -0.0074 | -0.0022
00:16:35 07 | 11 |  0.0102 |  0.0215
00:16:42 07 | 12 |  0.0054 |  0.0341
00:16:50 07 | 13 |  0.0229 |  0.0540
00:16:57 07 | 14 |  0.0163 |  0.0621
00:17:04 07 | 15 | -0.0198 | -0.0048
00:17:12 07 | 16 |  0.0189 |  0.0077
00:17:19 07 | 17 | -0.0005 | -0.0056
00:17:26 07 | 18 |  0.0299 |  0.0715
00:17:34 07 | 19 |  0.0366 |  0.0574
00:17:41 07 | 20 |  0.0239 |  0.0205
00:17:49 08 | 01 | -0.0010 | -0.0107
00:17:56 08 | 02 |  0.0019 | -0.0171
00:18:04 08 | 03 |  0.0109 |  0.0008
00:18:11 08 | 04 |  0.0159 |  0.0442
00:18:19 08 | 05 | -0.0102 |  0.0067
00:18:26 08 | 06 |  0.0205 |  0.0224
00:18:33 08 | 07 |  0.0112 |  0.0203
00:18:41 08 | 08 |  0.0158 |  0.0242
00:18:48 08 | 09 |  0.0072 |  0.0030
00:18:55 08 | 10 |  0.0210 |  0.0099
00:19:02 08 | 11 |  0.0023 | -0.0208
00:19:10 08 | 12 |  0.0112 |  0.0316
00:19:17 08 | 13 |  0.0046 |  0.0053
00:19:25 08 | 14 |  0.0075 | -0.0063
00:19:32 08 | 15 | -0.0018 |  0.0064
00:19:39 08 | 16 |  0.0060 |  0.0131
00:19:47 08 | 17 |  0.0179 |  0.0367
00:19:54 08 | 18 |  0.0024 | -0.0092
00:20:01 08 | 19 |  0.0208 |  0.0401
00:20:09 08 | 20 | -0.0006 |  0.0411
00:20:17 09 | 01 | -0.0012 | -0.0071
00:20:24 09 | 02 |  0.0038 | -0.0036
00:20:31 09 | 03 |  0.0082 |  0.0027
00:20:39 09 | 04 |  0.0089 |  0.0183
00:20:46 09 | 05 |  0.0201 |  0.0215
00:20:53 09 | 06 |  0.0156 |  0.0180
00:21:01 09 | 07 | -0.0043 | -0.0049
00:21:08 09 | 08 |  0.0065 |  0.0062
00:21:15 09 | 09 |  0.0012 |  0.0041
00:21:23 09 | 10 |  0.0132 |  0.0014
00:21:30 09 | 11 |  0.0043 | -0.0058
00:21:38 09 | 12 | -0.0094 | -0.0182
00:21:45 09 | 13 | -0.0059 | -0.0146
00:21:52 09 | 14 | -0.0026 | -0.0131
00:21:60 09 | 15 |  0.0005 |  0.0004
00:22:07 09 | 16 |  0.0003 | -0.0028
00:22:15 09 | 17 | -0.0010 |  0.0043
00:22:22 09 | 18 | -0.0137 | -0.0131
00:22:29 09 | 19 |  0.0014 |  0.0089
00:22:37 09 | 20 |  0.0033 |  0.0015
00:22:45 10 | 01 | -0.0230 | -0.0269
00:22:52 10 | 02 | -0.0310 | -0.0773
00:22:59 10 | 03 | -0.0068 |  0.0027</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;bbfa2db8-61ec-4686-96cb-e8b730928c3e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">00:23:07 10 | 04 | -0.0260 | -0.0485
00:23:14 10 | 05 |  0.0051 | -0.0052
00:23:21 10 | 06 | -0.0108 | -0.0292
00:23:28 10 | 07 | -0.0013 | -0.0012
00:23:36 10 | 08 | -0.0218 | -0.0377
00:23:43 10 | 09 | -0.0052 | -0.0046
00:23:50 10 | 10 | -0.0053 | -0.0111
00:23:57 10 | 11 | -0.0169 | -0.0131
00:24:05 10 | 12 | -0.0201 | -0.0271
00:24:12 10 | 13 |  0.0050 |  0.0089
00:24:19 10 | 14 | -0.0045 |  0.0041
00:24:26 10 | 15 |  0.0085 |  0.0154
00:24:34 10 | 16 | -0.0027 | -0.0020
00:24:41 10 | 17 | -0.0173 |  0.0048
00:24:48 10 | 18 | -0.0182 |  0.0004
00:24:55 10 | 19 | -0.0078 |  0.0018
00:25:03 10 | 20 | -0.0054 |  0.0090
00:25:11 11 | 01 |  0.0045 |  0.0087
00:25:18 11 | 02 |  0.0319 |  0.0106
00:25:26 11 | 03 |  0.0077 |  0.0156
00:25:33 11 | 04 | -0.0020 | -0.0110
00:25:40 11 | 05 |  0.0203 |  0.0222
00:25:47 11 | 06 |  0.0049 |  0.0032
00:25:55 11 | 07 |  0.0258 |  0.0155
00:26:02 11 | 08 |  0.0159 |  0.0122
00:26:10 11 | 09 |  0.0094 |  0.0003
00:26:17 11 | 10 |  0.0219 |  0.0125
00:26:24 11 | 11 |  0.0274 |  0.0277
00:26:32 11 | 12 |  0.0230 |  0.0202
00:26:39 11 | 13 |  0.0156 |  0.0208
00:26:46 11 | 14 |  0.0057 | -0.0018
00:26:54 11 | 15 |  0.0167 |  0.0158
00:27:01 11 | 16 |  0.0133 |  0.0009
00:27:08 11 | 17 |  0.0063 | -0.0000
00:27:16 11 | 18 |  0.0163 |  0.0135
00:27:23 11 | 19 |  0.0245 |  0.0228
00:27:30 11 | 20 |  0.0247 |  0.0313
00:27:38 12 | 01 | -0.0053 | -0.0099
00:27:46 12 | 02 |  0.0118 |  0.0199
00:27:53 12 | 03 |  0.0059 | -0.0043
00:28:00 12 | 04 |  0.0079 |  0.0168
00:28:07 12 | 05 |  0.0209 |  0.0283
00:28:15 12 | 06 |  0.0080 |  0.0074
00:28:22 12 | 07 |  0.0066 |  0.0126
00:28:29 12 | 08 |  0.0067 |  0.0178
00:28:36 12 | 09 |  0.0203 |  0.0133
00:28:44 12 | 10 |  0.0172 |  0.0312
00:28:51 12 | 11 |  0.0104 |  0.0038
00:28:58 12 | 12 |  0.0076 |  0.0211
00:29:05 12 | 13 |  0.0148 |  0.0060
00:29:13 12 | 14 |  0.0221 |  0.0288
00:29:20 12 | 15 |  0.0045 |  0.0131
00:29:27 12 | 16 |  0.0124 |  0.0129
00:29:35 12 | 17 |  0.0072 |  0.0042
00:29:42 12 | 18 | -0.0004 |  0.0110
00:29:49 12 | 19 |  0.0069 |  0.0069
00:29:56 12 | 20 |  0.0036 |  0.0030
(64, 32) tanh 0 256
00:00:04 01 | 01 |  0.0119 |  0.0158
00:00:07 01 | 02 | -0.0106 | -0.0062
00:00:10 01 | 03 |  0.0185 |  0.0123
00:00:13 01 | 04 |  0.0142 |  0.0022
00:00:16 01 | 05 |  0.0144 |  0.0211
00:00:19 01 | 06 |  0.0092 |  0.0069
00:00:22 01 | 07 | -0.0045 | -0.0064
00:00:25 01 | 08 | -0.0021 | -0.0098
00:00:28 01 | 09 |  0.0004 |  0.0041
00:00:31 01 | 10 |  0.0092 |  0.0085
00:00:34 01 | 11 | -0.0079 | -0.0249
00:00:37 01 | 12 | -0.0018 |  0.0000
00:00:40 01 | 13 |  0.0016 | -0.0055
00:00:43 01 | 14 |  0.0045 |  0.0044
00:00:46 01 | 15 |  0.0105 |  0.0103
00:00:49 01 | 16 |  0.0116 |  0.0073
00:00:52 01 | 17 |  0.0033 | -0.0039
00:00:55 01 | 18 |  0.0088 |  0.0063
00:00:58 01 | 19 |  0.0087 |  0.0022
00:01:01 01 | 20 |  0.0061 | -0.0062
00:01:05 02 | 01 | -0.0168 | -0.0221
00:01:08 02 | 02 |  0.0221 |  0.0133
00:01:11 02 | 03 | -0.0003 |  0.0154
00:01:14 02 | 04 |  0.0037 | -0.0066
00:01:17 02 | 05 |  0.0092 |  0.0125
00:01:20 02 | 06 |  0.0072 |  0.0021
00:01:23 02 | 07 | -0.0040 | -0.0091
00:01:26 02 | 08 | -0.0116 | -0.0137
00:01:29 02 | 09 | -0.0012 |  0.0190
00:01:32 02 | 10 |  0.0075 |  0.0069
00:01:35 02 | 11 |  0.0236 |  0.0152
00:01:38 02 | 12 |  0.0002 | -0.0044
00:01:41 02 | 13 |  0.0191 |  0.0184
00:01:44 02 | 14 |  0.0022 |  0.0125
00:01:47 02 | 15 | -0.0040 | -0.0035
00:01:50 02 | 16 |  0.0126 |  0.0086
00:01:53 02 | 17 | -0.0111 | -0.0034
00:01:56 02 | 18 | -0.0092 | -0.0087
00:01:59 02 | 19 | -0.0122 | -0.0072
00:02:02 02 | 20 | -0.0120 | -0.0059
00:02:05 03 | 01 |  0.0192 |  0.0298
00:02:08 03 | 02 | -0.0244 | -0.0304
00:02:11 03 | 03 | -0.0051 | -0.0137
00:02:14 03 | 04 |  0.0074 |  0.0168
00:02:17 03 | 05 |  0.0226 |  0.0519
00:02:20 03 | 06 | -0.0140 | -0.0215
00:02:24 03 | 07 | -0.0051 | -0.0250
00:02:27 03 | 08 | -0.0150 | -0.0333
00:02:30 03 | 09 | -0.0158 | -0.0580
00:02:33 03 | 10 | -0.0101 | -0.0314
00:02:36 03 | 11 |  0.0057 | -0.0028
00:02:39 03 | 12 |  0.0044 |  0.0052
00:02:42 03 | 13 | -0.0116 | -0.0285
00:02:45 03 | 14 | -0.0050 | -0.0099
00:02:48 03 | 15 |  0.0037 |  0.0018
00:02:51 03 | 16 |  0.0033 |  0.0002
00:02:54 03 | 17 | -0.0015 | -0.0070
00:02:57 03 | 18 |  0.0005 | -0.0136
00:03:00 03 | 19 | -0.0110 | -0.0141
00:03:03 03 | 20 | -0.0025 |  0.0008
00:03:07 04 | 01 | -0.0312 | -0.0253
00:03:10 04 | 02 |  0.0017 |  0.0031
00:03:13 04 | 03 | -0.0412 | -0.0519
00:03:16 04 | 04 | -0.0114 | -0.0135
00:03:19 04 | 05 |  0.0243 |  0.0160
00:03:22 04 | 06 |  0.0225 |  0.0370
00:03:25 04 | 07 |  0.0050 |  0.0030
00:03:28 04 | 08 |  0.0299 |  0.0170
00:03:31 04 | 09 |  0.0260 |  0.0233
00:03:34 04 | 10 |  0.0008 |  0.0020
00:03:37 04 | 11 | -0.0074 | -0.0189
00:03:40 04 | 12 | -0.0108 | -0.0350
00:03:43 04 | 13 | -0.0066 | -0.0127
00:03:46 04 | 14 |  0.0174 |  0.0078
00:03:49 04 | 15 | -0.0110 | -0.0249
00:03:53 04 | 16 | -0.0058 | -0.0274
00:03:56 04 | 17 |  0.0009 |  0.0031
00:03:59 04 | 18 |  0.0056 | -0.0080
00:04:02 04 | 19 |  0.0106 | -0.0052
00:04:05 04 | 20 |  0.0017 | -0.0070
00:04:08 05 | 01 | -0.0152 | -0.0107
00:04:11 05 | 02 |  0.0363 |  0.0252
00:04:15 05 | 03 |  0.0009 |  0.0075
00:04:18 05 | 04 |  0.0092 |  0.0076
00:04:21 05 | 05 |  0.0204 |  0.0167
00:04:24 05 | 06 | -0.0073 | -0.0004
00:04:27 05 | 07 |  0.0207 |  0.0195
00:04:30 05 | 08 |  0.0198 |  0.0193
00:04:33 05 | 09 |  0.0166 |  0.0242
00:04:36 05 | 10 | -0.0113 | -0.0143
00:04:39 05 | 11 |  0.0236 |  0.0184
00:04:42 05 | 12 |  0.0238 |  0.0297
00:04:45 05 | 13 |  0.0251 |  0.0246
00:04:48 05 | 14 |  0.0192 |  0.0180
00:04:51 05 | 15 | -0.0036 | -0.0066
00:04:54 05 | 16 |  0.0254 |  0.0286
00:04:57 05 | 17 |  0.0215 |  0.0158
00:04:60 05 | 18 | -0.0096 | -0.0180
00:05:03 05 | 19 | -0.0133 | -0.0181
00:05:06 05 | 20 | -0.0116 | -0.0140
00:05:10 06 | 01 |  0.0340 |  0.0215
00:05:13 06 | 02 |  0.0300 |  0.0277
00:05:16 06 | 03 |  0.0379 |  0.0344
00:05:19 06 | 04 |  0.0451 |  0.0509
00:05:22 06 | 05 |  0.0325 |  0.0187
00:05:25 06 | 06 |  0.0222 |  0.0288
00:05:28 06 | 07 |  0.0178 |  0.0166
00:05:31 06 | 08 |  0.0253 |  0.0196
00:05:34 06 | 09 |  0.0226 |  0.0106
00:05:37 06 | 10 |  0.0210 |  0.0270
00:05:40 06 | 11 |  0.0241 |  0.0263
00:05:43 06 | 12 |  0.0274 |  0.0282
00:05:46 06 | 13 |  0.0004 |  0.0030
00:05:49 06 | 14 | -0.0006 |  0.0013
00:05:52 06 | 15 |  0.0203 |  0.0078
00:05:55 06 | 16 |  0.0146 |  0.0289
00:05:58 06 | 17 |  0.0193 |  0.0204
00:06:01 06 | 18 | -0.0101 | -0.0112
00:06:04 06 | 19 | -0.0020 | -0.0081
00:06:07 06 | 20 | -0.0048 | -0.0071
00:06:11 07 | 01 |  0.0100 |  0.0229
00:06:14 07 | 02 |  0.0472 |  0.0406
00:06:17 07 | 03 |  0.0019 | -0.0083
00:06:20 07 | 04 |  0.0201 |  0.0087
00:06:23 07 | 05 |  0.0192 |  0.0267
00:06:26 07 | 06 |  0.0058 |  0.0389
00:06:29 07 | 07 |  0.0225 |  0.0269
00:06:32 07 | 08 |  0.0412 |  0.0666
00:06:35 07 | 09 |  0.0184 |  0.0296
00:06:39 07 | 10 |  0.0278 |  0.0377
00:06:42 07 | 11 |  0.0038 |  0.0140
00:06:45 07 | 12 |  0.0106 |  0.0385
00:06:48 07 | 13 |  0.0287 |  0.0484
00:06:51 07 | 14 |  0.0168 |  0.0280
00:06:54 07 | 15 |  0.0068 |  0.0155
00:06:57 07 | 16 |  0.0096 |  0.0431
00:06:60 07 | 17 | -0.0035 |  0.0098
00:07:03 07 | 18 |  0.0036 |  0.0229
00:07:06 07 | 19 | -0.0056 |  0.0080
00:07:09 07 | 20 | -0.0125 |  0.0127
00:07:13 08 | 01 |  0.0085 |  0.0028
00:07:16 08 | 02 | -0.0017 | -0.0027
00:07:19 08 | 03 |  0.0114 |  0.0230
00:07:22 08 | 04 |  0.0213 |  0.0221
00:07:25 08 | 05 |  0.0176 |  0.0044
00:07:28 08 | 06 |  0.0038 |  0.0152
00:07:31 08 | 07 |  0.0099 |  0.0050
00:07:34 08 | 08 |  0.0201 |  0.0130
00:07:37 08 | 09 |  0.0032 |  0.0031
00:07:40 08 | 10 |  0.0008 |  0.0057
00:07:43 08 | 11 | -0.0031 |  0.0122
00:07:46 08 | 12 | -0.0138 | -0.0031
00:07:49 08 | 13 |  0.0015 |  0.0132
00:07:52 08 | 14 |  0.0001 |  0.0048
00:07:55 08 | 15 | -0.0033 | -0.0038
00:07:58 08 | 16 | -0.0026 | -0.0038
00:08:01 08 | 17 |  0.0017 | -0.0089
00:08:04 08 | 18 |  0.0071 | -0.0173
00:08:07 08 | 19 |  0.0016 | -0.0075
00:08:10 08 | 20 | -0.0001 | -0.0138
00:08:14 09 | 01 |  0.0218 |  0.0364
00:08:17 09 | 02 | -0.0007 |  0.0120
00:08:20 09 | 03 | -0.0238 | -0.0339
00:08:23 09 | 04 |  0.0080 |  0.0175</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;f828d2ca-d0bb-4942-a08d-e7a796b7d391&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">00:08:26 09 | 05 |  0.0068 |  0.0192
00:08:29 09 | 06 | -0.0011 |  0.0084
00:08:33 09 | 07 |  0.0163 |  0.0279
00:08:36 09 | 08 | -0.0014 | -0.0102
00:08:39 09 | 09 |  0.0101 |  0.0185
00:08:42 09 | 10 | -0.0021 |  0.0140
00:08:45 09 | 11 |  0.0019 |  0.0125
00:08:48 09 | 12 | -0.0119 | -0.0192
00:08:51 09 | 13 | -0.0090 | -0.0073
00:08:54 09 | 14 |  0.0056 |  0.0006
00:08:57 09 | 15 | -0.0093 | -0.0147
00:08:60 09 | 16 |  0.0165 |  0.0144
00:09:03 09 | 17 |  0.0166 |  0.0083
00:09:06 09 | 18 |  0.0043 | -0.0010
00:09:09 09 | 19 |  0.0010 | -0.0061
00:09:12 09 | 20 |  0.0081 | -0.0019
00:09:16 10 | 01 | -0.0368 | -0.0577
00:09:19 10 | 02 |  0.0046 | -0.0120
00:09:22 10 | 03 | -0.0019 |  0.0081
00:09:25 10 | 04 | -0.0015 | -0.0174
00:09:28 10 | 05 | -0.0214 | -0.0242
00:09:31 10 | 06 |  0.0091 |  0.0272
00:09:34 10 | 07 | -0.0185 | -0.0177
00:09:37 10 | 08 | -0.0085 | -0.0208
00:09:40 10 | 09 | -0.0105 | -0.0062
00:09:43 10 | 10 | -0.0035 | -0.0041
00:09:46 10 | 11 | -0.0252 | -0.0339
00:09:49 10 | 12 | -0.0005 |  0.0056
00:09:52 10 | 13 | -0.0246 | -0.0335
00:09:55 10 | 14 | -0.0176 | -0.0363
00:09:58 10 | 15 | -0.0278 | -0.0337
00:10:01 10 | 16 | -0.0043 | -0.0085
00:10:05 10 | 17 | -0.0096 | -0.0213
00:10:08 10 | 18 | -0.0086 | -0.0124
00:10:11 10 | 19 | -0.0184 | -0.0253
00:10:14 10 | 20 | -0.0213 | -0.0219
00:10:18 11 | 01 |  0.0311 |  0.0324
00:10:21 11 | 02 |  0.0180 |  0.0134
00:10:24 11 | 03 |  0.0044 |  0.0050
00:10:27 11 | 04 |  0.0303 |  0.0300
00:10:30 11 | 05 |  0.0173 |  0.0146
00:10:33 11 | 06 |  0.0035 |  0.0118
00:10:36 11 | 07 |  0.0015 | -0.0037
00:10:39 11 | 08 |  0.0126 |  0.0057
00:10:42 11 | 09 |  0.0014 | -0.0060
00:10:45 11 | 10 |  0.0221 |  0.0084
00:10:48 11 | 11 |  0.0046 |  0.0034
00:10:51 11 | 12 |  0.0236 |  0.0190
00:10:54 11 | 13 |  0.0168 |  0.0039
00:10:57 11 | 14 |  0.0035 | -0.0020
00:11:00 11 | 15 |  0.0162 |  0.0074
00:11:03 11 | 16 |  0.0065 | -0.0211
00:11:06 11 | 17 |  0.0136 |  0.0096
00:11:09 11 | 18 |  0.0165 |  0.0089
00:11:12 11 | 19 |  0.0101 |  0.0045
00:11:15 11 | 20 |  0.0097 |  0.0067
00:11:19 12 | 01 |  0.0181 |  0.0203
00:11:22 12 | 02 |  0.0067 |  0.0113
00:11:25 12 | 03 |  0.0174 |  0.0137
00:11:28 12 | 04 |  0.0110 | -0.0144
00:11:32 12 | 05 |  0.0046 | -0.0095
00:11:35 12 | 06 |  0.0118 |  0.0124
00:11:38 12 | 07 |  0.0270 |  0.0213
00:11:41 12 | 08 |  0.0294 |  0.0247
00:11:44 12 | 09 |  0.0143 |  0.0176
00:11:47 12 | 10 |  0.0326 |  0.0413
00:11:50 12 | 11 |  0.0292 |  0.0312
00:11:53 12 | 12 |  0.0186 |  0.0252
00:11:56 12 | 13 |  0.0180 |  0.0182
00:11:59 12 | 14 |  0.0214 |  0.0108
00:12:02 12 | 15 |  0.0223 |  0.0127
00:12:05 12 | 16 |  0.0257 |  0.0376
00:12:09 12 | 17 |  0.0194 |  0.0314
00:12:12 12 | 18 |  0.0132 |  0.0019
00:12:15 12 | 19 |  0.0196 |  0.0299
00:12:18 12 | 20 |  0.0232 |  0.0353
(16, 8) tanh 0 64
00:00:07 01 | 01 | -0.0133 | -0.0139
00:00:14 01 | 02 |  0.0281 |  0.0298
00:00:21 01 | 03 |  0.0138 |  0.0085
00:00:27 01 | 04 | -0.0009 |  0.0087
00:00:34 01 | 05 |  0.0083 | -0.0031
00:00:40 01 | 06 |  0.0166 |  0.0172
00:00:47 01 | 07 | -0.0018 | -0.0063
00:00:53 01 | 08 |  0.0059 |  0.0050
00:00:60 01 | 09 |  0.0070 | -0.0058
00:01:06 01 | 10 |  0.0099 |  0.0071
00:01:13 01 | 11 |  0.0077 |  0.0007
00:01:19 01 | 12 |  0.0005 | -0.0000
00:01:26 01 | 13 |  0.0070 |  0.0110
00:01:32 01 | 14 |  0.0137 |  0.0102
00:01:39 01 | 15 |  0.0037 | -0.0017
00:01:45 01 | 16 |  0.0127 |  0.0160
00:01:52 01 | 17 |  0.0103 | -0.0064
00:01:59 01 | 18 |  0.0116 | -0.0049
00:02:05 01 | 19 |  0.0228 |  0.0224
00:02:12 01 | 20 |  0.0170 |  0.0218
00:02:19 02 | 01 |  0.0167 | -0.0072
00:02:26 02 | 02 |  0.0098 |  0.0123
00:02:32 02 | 03 |  0.0128 |  0.0215
00:02:39 02 | 04 | -0.0102 |  0.0020
00:02:45 02 | 05 | -0.0026 |  0.0139
00:02:52 02 | 06 | -0.0134 | -0.0038
00:02:58 02 | 07 | -0.0036 | -0.0059
00:03:05 02 | 08 |  0.0018 |  0.0046
00:03:11 02 | 09 | -0.0082 | -0.0021
00:03:18 02 | 10 |  0.0008 | -0.0023
00:03:24 02 | 11 |  0.0019 |  0.0050
00:03:31 02 | 12 |  0.0142 |  0.0166
00:03:37 02 | 13 |  0.0112 |  0.0127
00:03:44 02 | 14 |  0.0036 |  0.0065
00:03:50 02 | 15 | -0.0042 | -0.0141
00:03:57 02 | 16 | -0.0030 | -0.0086
00:04:04 02 | 17 |  0.0030 |  0.0044
00:04:10 02 | 18 |  0.0067 |  0.0109
00:04:17 02 | 19 |  0.0014 | -0.0050
00:04:23 02 | 20 | -0.0077 | -0.0044
00:04:31 03 | 01 |  0.0184 |  0.0258
00:04:37 03 | 02 |  0.0155 |  0.0057
00:04:44 03 | 03 |  0.0048 | -0.0055
00:04:51 03 | 04 |  0.0213 |  0.0223
00:04:57 03 | 05 | -0.0186 | -0.0287
00:05:04 03 | 06 | -0.0076 | -0.0048
00:05:11 03 | 07 |  0.0200 |  0.0238
00:05:17 03 | 08 |  0.0254 |  0.0204
00:05:24 03 | 09 | -0.0196 | -0.0386
00:05:31 03 | 10 |  0.0224 |  0.0118
00:05:37 03 | 11 |  0.0249 |  0.0149
00:05:44 03 | 12 | -0.0255 | -0.0276
00:05:51 03 | 13 | -0.0178 | -0.0257
00:05:57 03 | 14 |  0.0173 |  0.0176
00:06:04 03 | 15 |  0.0147 |  0.0185
00:06:10 03 | 16 |  0.0184 |  0.0201
00:06:17 03 | 17 |  0.0104 |  0.0180
00:06:24 03 | 18 | -0.0127 | -0.0182
00:06:30 03 | 19 |  0.0089 |  0.0131
00:06:37 03 | 20 | -0.0153 | -0.0380
00:06:44 04 | 01 |  0.0204 |  0.0049
00:06:51 04 | 02 | -0.0323 | -0.0415
00:06:57 04 | 03 | -0.0322 | -0.0409
00:07:04 04 | 04 | -0.0393 | -0.0486
00:07:11 04 | 05 |  0.0218 |  0.0300
00:07:17 04 | 06 | -0.0335 | -0.0327
00:07:24 04 | 07 | -0.0372 | -0.0524
00:07:30 04 | 08 | -0.0322 | -0.0484
00:07:37 04 | 09 | -0.0344 | -0.0402
00:07:44 04 | 10 | -0.0296 | -0.0410
00:07:50 04 | 11 |  0.0239 |  0.0129
00:07:57 04 | 12 |  0.0042 | -0.0089
00:08:03 04 | 13 |  0.0151 |  0.0047
00:08:10 04 | 14 | -0.0382 | -0.0496
00:08:16 04 | 15 | -0.0338 | -0.0401
00:08:23 04 | 16 | -0.0267 | -0.0302
00:08:30 04 | 17 | -0.0297 | -0.0162
00:08:36 04 | 18 | -0.0281 | -0.0182
00:08:43 04 | 19 | -0.0012 | -0.0029
00:08:49 04 | 20 | -0.0269 | -0.0201
00:08:57 05 | 01 |  0.0131 |  0.0133
00:09:03 05 | 02 |  0.0188 |  0.0172
00:09:10 05 | 03 |  0.0265 |  0.0266
00:09:17 05 | 04 | -0.0112 | -0.0154
00:09:23 05 | 05 | -0.0026 | -0.0123
00:09:30 05 | 06 |  0.0157 |  0.0292
00:09:36 05 | 07 | -0.0098 | -0.0026
00:09:43 05 | 08 |  0.0175 |  0.0265
00:09:49 05 | 09 |  0.0059 |  0.0031
00:09:56 05 | 10 |  0.0193 |  0.0282
00:10:03 05 | 11 |  0.0232 |  0.0281
00:10:09 05 | 12 |  0.0024 |  0.0085
00:10:16 05 | 13 |  0.0189 |  0.0152
00:10:23 05 | 14 |  0.0236 |  0.0274
00:10:29 05 | 15 | -0.0061 | -0.0104
00:10:36 05 | 16 | -0.0088 | -0.0079
00:10:42 05 | 17 | -0.0076 |  0.0097
00:10:49 05 | 18 | -0.0012 |  0.0022
00:10:56 05 | 19 |  0.0101 |  0.0040
00:11:02 05 | 20 |  0.0076 | -0.0004
00:11:10 06 | 01 |  0.0245 |  0.0106
00:11:16 06 | 02 | -0.0343 | -0.0415
00:11:23 06 | 03 |  0.0226 |  0.0192
00:11:30 06 | 04 |  0.0208 |  0.0253
00:11:36 06 | 05 |  0.0341 |  0.0231
00:11:43 06 | 06 |  0.0272 |  0.0239
00:11:49 06 | 07 |  0.0224 |  0.0235
00:11:56 06 | 08 |  0.0293 |  0.0134
00:12:03 06 | 09 |  0.0121 |  0.0124
00:12:09 06 | 10 |  0.0069 | -0.0010
00:12:16 06 | 11 |  0.0252 |  0.0391
00:12:22 06 | 12 |  0.0255 |  0.0346
00:12:29 06 | 13 |  0.0243 |  0.0262
00:12:36 06 | 14 |  0.0170 | -0.0026
00:12:42 06 | 15 |  0.0216 |  0.0086
00:12:49 06 | 16 |  0.0179 |  0.0251
00:12:55 06 | 17 |  0.0053 |  0.0267
00:13:02 06 | 18 |  0.0043 |  0.0086
00:13:09 06 | 19 |  0.0111 |  0.0154
00:13:15 06 | 20 |  0.0025 |  0.0190
00:13:22 07 | 01 |  0.0006 |  0.0167
00:13:29 07 | 02 | -0.0074 |  0.0081
00:13:35 07 | 03 |  0.0304 |  0.0676
00:13:42 07 | 04 |  0.0170 |  0.0710
00:13:49 07 | 05 | -0.0057 |  0.0148
00:13:55 07 | 06 | -0.0031 |  0.0068
00:14:02 07 | 07 |  0.0138 |  0.0361
00:14:08 07 | 08 |  0.0025 |  0.0206
00:14:15 07 | 09 |  0.0040 |  0.0166
00:14:21 07 | 10 | -0.0104 |  0.0225
00:14:28 07 | 11 | -0.0130 | -0.0023
00:14:34 07 | 12 |  0.0015 |  0.0036
00:14:41 07 | 13 |  0.0248 |  0.0403
00:14:47 07 | 14 |  0.0184 |  0.0272
00:14:54 07 | 15 |  0.0119 |  0.0181
00:15:00 07 | 16 |  0.0245 |  0.0408
00:15:07 07 | 17 |  0.0056 |  0.0280
00:15:13 07 | 18 | -0.0040 |  0.0114
00:15:20 07 | 19 |  0.0129 |  0.0297
00:15:26 07 | 20 |  0.0279 |  0.0467
00:15:34 08 | 01 |  0.0072 |  0.0072
00:15:40 08 | 02 |  0.0047 |  0.0001
00:15:47 08 | 03 |  0.0107 |  0.0223
00:15:53 08 | 04 |  0.0140 |  0.0114
00:15:60 08 | 05 |  0.0214 |  0.0005</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;b92f38ba-596b-4eba-aef1-f7fcb6f8e53f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">00:16:06 08 | 06 |  0.0323 |  0.0397
00:16:13 08 | 07 |  0.0209 |  0.0224
00:16:19 08 | 08 |  0.0130 | -0.0021
00:16:26 08 | 09 | -0.0047 | -0.0255
00:16:32 08 | 10 |  0.0077 |  0.0067
00:16:39 08 | 11 | -0.0078 | -0.0225
00:16:45 08 | 12 | -0.0026 | -0.0003
00:16:52 08 | 13 |  0.0035 |  0.0058
00:16:58 08 | 14 | -0.0108 | -0.0048
00:17:05 08 | 15 | -0.0099 | -0.0024
00:17:12 08 | 16 |  0.0012 |  0.0035
00:17:18 08 | 17 | -0.0019 | -0.0059
00:17:25 08 | 18 | -0.0057 | -0.0217
00:17:31 08 | 19 |  0.0029 |  0.0078
00:17:38 08 | 20 | -0.0074 | -0.0104
00:17:45 09 | 01 |  0.0155 |  0.0164
00:17:52 09 | 02 | -0.0073 | -0.0104
00:17:58 09 | 03 |  0.0132 |  0.0116
00:18:05 09 | 04 | -0.0090 | -0.0044
00:18:11 09 | 05 |  0.0176 |  0.0366
00:18:18 09 | 06 |  0.0127 |  0.0057
00:18:25 09 | 07 | -0.0036 | -0.0134
00:18:31 09 | 08 | -0.0001 | -0.0009
00:18:38 09 | 09 | -0.0028 | -0.0108
00:18:45 09 | 10 |  0.0182 |  0.0138
00:18:51 09 | 11 |  0.0116 |  0.0146
00:18:58 09 | 12 |  0.0172 |  0.0180
00:19:05 09 | 13 |  0.0106 |  0.0116
00:19:11 09 | 14 |  0.0212 |  0.0152
00:19:18 09 | 15 |  0.0145 |  0.0194
00:19:24 09 | 16 |  0.0038 |  0.0138
00:19:31 09 | 17 |  0.0137 |  0.0182
00:19:38 09 | 18 |  0.0084 |  0.0121
00:19:44 09 | 19 |  0.0160 |  0.0181
00:19:51 09 | 20 |  0.0010 |  0.0097
00:19:58 10 | 01 |  0.0292 |  0.0446
00:20:05 10 | 02 |  0.0219 |  0.0272
00:20:11 10 | 03 | -0.0148 | -0.0086
00:20:18 10 | 04 | -0.0015 |  0.0174
00:20:24 10 | 05 | -0.0053 |  0.0038
00:20:31 10 | 06 |  0.0067 |  0.0218
00:20:37 10 | 07 |  0.0083 | -0.0061
00:20:44 10 | 08 |  0.0118 | -0.0005
00:20:50 10 | 09 |  0.0156 |  0.0198
00:20:57 10 | 10 |  0.0146 |  0.0078
00:21:03 10 | 11 | -0.0044 | -0.0144
00:21:10 10 | 12 |  0.0014 | -0.0162
00:21:16 10 | 13 |  0.0153 | -0.0020
00:21:23 10 | 14 |  0.0024 | -0.0013
00:21:29 10 | 15 |  0.0028 | -0.0104
00:21:36 10 | 16 | -0.0037 |  0.0018
00:21:42 10 | 17 | -0.0059 | -0.0052
00:21:49 10 | 18 | -0.0072 |  0.0009
00:21:55 10 | 19 |  0.0102 |  0.0104
00:22:02 10 | 20 |  0.0021 |  0.0031
00:22:09 11 | 01 |  0.0129 |  0.0291
00:22:15 11 | 02 |  0.0126 |  0.0157
00:22:22 11 | 03 |  0.0198 |  0.0243
00:22:28 11 | 04 |  0.0246 |  0.0331
00:22:35 11 | 05 |  0.0070 |  0.0026
00:22:41 11 | 06 |  0.0156 |  0.0056
00:22:48 11 | 07 |  0.0284 |  0.0271
00:22:54 11 | 08 |  0.0121 |  0.0065
00:23:01 11 | 09 |  0.0203 |  0.0206
00:23:07 11 | 10 |  0.0229 |  0.0188
00:23:14 11 | 11 |  0.0206 |  0.0215
00:23:20 11 | 12 |  0.0214 |  0.0179
00:23:27 11 | 13 |  0.0444 |  0.0357
00:23:34 11 | 14 |  0.0400 |  0.0359
00:23:40 11 | 15 |  0.0311 |  0.0340
00:23:46 11 | 16 |  0.0231 |  0.0177
00:23:53 11 | 17 |  0.0172 |  0.0066
00:23:60 11 | 18 |  0.0271 |  0.0326
00:24:06 11 | 19 |  0.0179 |  0.0153
00:24:13 11 | 20 |  0.0263 |  0.0338
00:24:20 12 | 01 |  0.0060 |  0.0141
00:24:26 12 | 02 |  0.0218 |  0.0202
00:24:33 12 | 03 |  0.0108 |  0.0218
00:24:40 12 | 04 |  0.0170 |  0.0101
00:24:46 12 | 05 |  0.0143 |  0.0209
00:24:53 12 | 06 |  0.0326 |  0.0298
00:24:59 12 | 07 |  0.0239 |  0.0307
00:25:06 12 | 08 | -0.0107 | -0.0204
00:25:12 12 | 09 |  0.0073 |  0.0064
00:25:19 12 | 10 |  0.0265 |  0.0395
00:25:25 12 | 11 |  0.0258 |  0.0312
00:25:32 12 | 12 |  0.0112 |  0.0127
00:25:39 12 | 13 |  0.0280 |  0.0264
00:25:45 12 | 14 |  0.0167 |  0.0152
00:25:52 12 | 15 |  0.0113 |  0.0130
00:25:59 12 | 16 | -0.0026 | -0.0127
00:26:05 12 | 17 |  0.0053 |  0.0009
00:26:12 12 | 18 |  0.0004 | -0.0046
00:26:19 12 | 19 |  0.0070 | -0.0059
00:26:25 12 | 20 |  0.0109 |  0.0175
(16, 8) tanh 0 256
00:00:03 01 | 01 |  0.0064 |  0.0080
00:00:06 01 | 02 |  0.0007 |  0.0067
00:00:08 01 | 03 |  0.0070 |  0.0031
00:00:10 01 | 04 |  0.0209 |  0.0285
00:00:13 01 | 05 |  0.0043 | -0.0004
00:00:15 01 | 06 |  0.0192 |  0.0133
00:00:17 01 | 07 |  0.0201 |  0.0178
00:00:20 01 | 08 |  0.0124 |  0.0125
00:00:22 01 | 09 |  0.0124 |  0.0236
00:00:24 01 | 10 |  0.0087 | -0.0054
00:00:27 01 | 11 |  0.0084 | -0.0070
00:00:29 01 | 12 |  0.0110 | -0.0120
00:00:31 01 | 13 |  0.0072 | -0.0236
00:00:34 01 | 14 |  0.0081 | -0.0032
00:00:36 01 | 15 |  0.0108 |  0.0049
00:00:38 01 | 16 |  0.0067 | -0.0021
00:00:41 01 | 17 |  0.0088 |  0.0060
00:00:43 01 | 18 |  0.0141 |  0.0100
00:00:45 01 | 19 |  0.0024 |  0.0048
00:00:48 01 | 20 |  0.0054 |  0.0062
00:00:51 02 | 01 |  0.0156 |  0.0013
00:00:53 02 | 02 |  0.0113 |  0.0143
00:00:56 02 | 03 |  0.0172 |  0.0038
00:00:58 02 | 04 |  0.0069 |  0.0233
00:01:00 02 | 05 | -0.0103 | -0.0061
00:01:02 02 | 06 |  0.0109 |  0.0249
00:01:05 02 | 07 |  0.0048 |  0.0012
00:01:07 02 | 08 |  0.0006 |  0.0025
00:01:09 02 | 09 |  0.0013 |  0.0108
00:01:12 02 | 10 |  0.0098 |  0.0330
00:01:14 02 | 11 |  0.0003 | -0.0034
00:01:16 02 | 12 |  0.0076 |  0.0024
00:01:19 02 | 13 |  0.0092 |  0.0225
00:01:21 02 | 14 |  0.0001 | -0.0061
00:01:23 02 | 15 |  0.0091 |  0.0009
00:01:26 02 | 16 | -0.0008 |  0.0170
00:01:28 02 | 17 |  0.0008 |  0.0023
00:01:30 02 | 18 |  0.0023 |  0.0180
00:01:33 02 | 19 | -0.0010 | -0.0057
00:01:35 02 | 20 | -0.0067 |  0.0001
00:01:38 03 | 01 | -0.0130 | -0.0096
00:01:40 03 | 02 | -0.0107 |  0.0020
00:01:43 03 | 03 |  0.0235 |  0.0221
00:01:45 03 | 04 | -0.0311 | -0.0463
00:01:47 03 | 05 | -0.0258 | -0.0215
00:01:50 03 | 06 |  0.0175 |  0.0293
00:01:52 03 | 07 |  0.0092 |  0.0023
00:01:54 03 | 08 |  0.0306 |  0.0348
00:01:57 03 | 09 | -0.0115 | -0.0122
00:01:59 03 | 10 |  0.0033 | -0.0003
00:02:01 03 | 11 | -0.0009 | -0.0148
00:02:04 03 | 12 |  0.0175 |  0.0160
00:02:06 03 | 13 | -0.0136 | -0.0150
00:02:08 03 | 14 | -0.0036 | -0.0223
00:02:11 03 | 15 |  0.0075 |  0.0007
00:02:13 03 | 16 |  0.0108 |  0.0064
00:02:15 03 | 17 | -0.0059 | -0.0084
00:02:17 03 | 18 | -0.0091 | -0.0246
00:02:20 03 | 19 | -0.0158 | -0.0175
00:02:22 03 | 20 | -0.0210 | -0.0366
00:02:26 04 | 01 | -0.0075 |  0.0003
00:02:28 04 | 02 | -0.0261 |  0.0059
00:02:30 04 | 03 |  0.0271 |  0.0116
00:02:33 04 | 04 |  0.0215 |  0.0134
00:02:35 04 | 05 |  0.0181 |  0.0043
00:02:38 04 | 06 |  0.0313 |  0.0130
00:02:40 04 | 07 | -0.0301 | -0.0318
00:02:42 04 | 08 |  0.0221 |  0.0165
00:02:45 04 | 09 |  0.0316 |  0.0265
00:02:47 04 | 10 | -0.0116 | -0.0114
00:02:49 04 | 11 |  0.0216 |  0.0111
00:02:52 04 | 12 |  0.0149 |  0.0011
00:02:54 04 | 13 |  0.0171 |  0.0157
00:02:57 04 | 14 | -0.0214 | -0.0261
00:02:59 04 | 15 |  0.0302 |  0.0198
00:03:01 04 | 16 | -0.0275 | -0.0332
00:03:04 04 | 17 | -0.0196 | -0.0156
00:03:06 04 | 18 |  0.0271 |  0.0138
00:03:09 04 | 19 |  0.0222 |  0.0103
00:03:11 04 | 20 |  0.0272 |  0.0143
00:03:14 05 | 01 |  0.0000 |  0.0106
00:03:17 05 | 02 | -0.0195 | -0.0052
00:03:19 05 | 03 |  0.0192 |  0.0061
00:03:21 05 | 04 |  0.0066 | -0.0031
00:03:24 05 | 05 |  0.0109 |  0.0081
00:03:26 05 | 06 |  0.0109 |  0.0001
00:03:28 05 | 07 |  0.0140 |  0.0172
00:03:31 05 | 08 |  0.0279 |  0.0084
00:03:33 05 | 09 |  0.0201 |  0.0190
00:03:35 05 | 10 | -0.0038 | -0.0026
00:03:38 05 | 11 |  0.0064 |  0.0201
00:03:40 05 | 12 |  0.0098 |  0.0040
00:03:42 05 | 13 |  0.0030 | -0.0096
00:03:45 05 | 14 |  0.0093 |  0.0050
00:03:47 05 | 15 |  0.0129 |  0.0079
00:03:50 05 | 16 |  0.0009 |  0.0091
00:03:52 05 | 17 |  0.0059 |  0.0181
00:03:54 05 | 18 |  0.0104 |  0.0171
00:03:57 05 | 19 |  0.0192 |  0.0304
00:03:59 05 | 20 |  0.0123 |  0.0201
00:04:02 06 | 01 | -0.0063 | -0.0080
00:04:05 06 | 02 |  0.0074 | -0.0044
00:04:07 06 | 03 |  0.0157 |  0.0257
00:04:09 06 | 04 |  0.0232 |  0.0171
00:04:12 06 | 05 | -0.0154 | -0.0303
00:04:14 06 | 06 | -0.0050 | -0.0084
00:04:16 06 | 07 |  0.0023 | -0.0055
00:04:19 06 | 08 |  0.0150 |  0.0138
00:04:21 06 | 09 |  0.0408 |  0.0304
00:04:24 06 | 10 |  0.0144 |  0.0031
00:04:26 06 | 11 |  0.0332 |  0.0219
00:04:28 06 | 12 | -0.0100 | -0.0052
00:04:31 06 | 13 |  0.0112 |  0.0024
00:04:33 06 | 14 |  0.0296 |  0.0141
00:04:35 06 | 15 |  0.0065 | -0.0003
00:04:38 06 | 16 |  0.0290 |  0.0250
00:04:40 06 | 17 | -0.0056 | -0.0198
00:04:42 06 | 18 |  0.0016 |  0.0002
00:04:45 06 | 19 |  0.0093 | -0.0123
00:04:47 06 | 20 |  0.0307 |  0.0215
00:04:51 07 | 01 |  0.0254 |  0.0198
00:04:53 07 | 02 | -0.0186 | -0.0181
00:04:55 07 | 03 | -0.0327 | -0.0391
00:04:58 07 | 04 | -0.0048 | -0.0083
00:05:00 07 | 05 |  0.0093 |  0.0070
00:05:02 07 | 06 |  0.0037 |  0.0039</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;06656959-66fe-4c0e-87fe-045f98da4c52&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">00:05:05 07 | 07 |  0.0139 |  0.0261
00:05:07 07 | 08 | -0.0135 |  0.0153
00:05:09 07 | 09 |  0.0189 |  0.0215
00:05:12 07 | 10 | -0.0088 |  0.0213
00:05:14 07 | 11 | -0.0057 |  0.0177
00:05:17 07 | 12 | -0.0064 |  0.0064
00:05:19 07 | 13 |  0.0109 |  0.0428
00:05:21 07 | 14 |  0.0206 |  0.0107
00:05:24 07 | 15 | -0.0023 |  0.0296
00:05:26 07 | 16 | -0.0023 |  0.0358
00:05:28 07 | 17 |  0.0380 |  0.0201
00:05:31 07 | 18 |  0.0344 |  0.0242
00:05:33 07 | 19 |  0.0299 |  0.0233
00:05:35 07 | 20 |  0.0285 |  0.0273
00:05:39 08 | 01 |  0.0038 |  0.0056
00:05:41 08 | 02 |  0.0050 | -0.0014
00:05:43 08 | 03 |  0.0176 |  0.0015
00:05:46 08 | 04 |  0.0124 |  0.0056
00:05:48 08 | 05 |  0.0067 | -0.0005
00:05:50 08 | 06 |  0.0074 |  0.0269
00:05:53 08 | 07 |  0.0307 |  0.0287
00:05:55 08 | 08 |  0.0087 |  0.0295
00:05:58 08 | 09 |  0.0126 | -0.0091
00:05:60 08 | 10 |  0.0112 |  0.0176
00:06:02 08 | 11 |  0.0212 |  0.0125
00:06:05 08 | 12 |  0.0116 |  0.0353
00:06:07 08 | 13 |  0.0129 |  0.0124
00:06:10 08 | 14 |  0.0161 |  0.0175
00:06:12 08 | 15 |  0.0105 |  0.0218
00:06:14 08 | 16 |  0.0238 |  0.0248
00:06:17 08 | 17 |  0.0144 |  0.0201
00:06:19 08 | 18 |  0.0289 |  0.0179
00:06:21 08 | 19 |  0.0292 |  0.0313
00:06:24 08 | 20 |  0.0202 |  0.0282
00:06:27 09 | 01 |  0.0020 | -0.0015
00:06:29 09 | 02 | -0.0057 | -0.0046
00:06:32 09 | 03 |  0.0151 |  0.0197
00:06:34 09 | 04 |  0.0033 |  0.0069
00:06:36 09 | 05 |  0.0052 | -0.0033
00:06:39 09 | 06 |  0.0165 |  0.0318
00:06:41 09 | 07 |  0.0048 |  0.0049
00:06:44 09 | 08 |  0.0049 | -0.0030
00:06:46 09 | 09 | -0.0003 | -0.0164
00:06:48 09 | 10 | -0.0125 | -0.0318
00:06:51 09 | 11 |  0.0064 |  0.0025
00:06:53 09 | 12 | -0.0061 | -0.0088
00:06:55 09 | 13 | -0.0051 | -0.0210
00:06:58 09 | 14 | -0.0016 | -0.0111
00:07:00 09 | 15 | -0.0041 | -0.0028
00:07:03 09 | 16 | -0.0082 | -0.0082
00:07:05 09 | 17 | -0.0074 | -0.0103
00:07:07 09 | 18 | -0.0035 |  0.0054
00:07:10 09 | 19 | -0.0044 | -0.0093
00:07:12 09 | 20 | -0.0046 |  0.0029
00:07:15 10 | 01 | -0.0350 | -0.0414
00:07:18 10 | 02 | -0.0377 | -0.0338
00:07:20 10 | 03 | -0.0200 | -0.0220
00:07:23 10 | 04 | -0.0556 | -0.0542
00:07:25 10 | 05 | -0.0257 | -0.0165
00:07:27 10 | 06 | -0.0345 | -0.0365
00:07:30 10 | 07 |  0.0117 | -0.0025
00:07:32 10 | 08 |  0.0102 | -0.0200
00:07:34 10 | 09 | -0.0075 | -0.0033
00:07:37 10 | 10 |  0.0102 | -0.0020
00:07:39 10 | 11 | -0.0108 | -0.0137
00:07:42 10 | 12 |  0.0140 |  0.0144
00:07:44 10 | 13 | -0.0066 | -0.0134
00:07:46 10 | 14 | -0.0107 | -0.0181
00:07:49 10 | 15 |  0.0136 |  0.0082
00:07:51 10 | 16 | -0.0032 | -0.0074
00:07:53 10 | 17 | -0.0022 | -0.0011
00:07:56 10 | 18 | -0.0004 | -0.0097
00:07:58 10 | 19 | -0.0169 | -0.0185
00:08:00 10 | 20 | -0.0053 | -0.0139
00:08:04 11 | 01 |  0.0010 |  0.0114
00:08:06 11 | 02 | -0.0202 | -0.0204
00:08:08 11 | 03 |  0.0158 |  0.0066
00:08:11 11 | 04 | -0.0004 | -0.0030
00:08:13 11 | 05 |  0.0213 |  0.0183
00:08:15 11 | 06 |  0.0186 |  0.0015
00:08:18 11 | 07 |  0.0222 |  0.0199
00:08:20 11 | 08 |  0.0121 |  0.0034
00:08:22 11 | 09 |  0.0265 |  0.0304
00:08:25 11 | 10 |  0.0244 |  0.0308
00:08:27 11 | 11 |  0.0144 |  0.0202
00:08:29 11 | 12 |  0.0177 | -0.0032
00:08:32 11 | 13 |  0.0187 |  0.0220
00:08:34 11 | 14 |  0.0095 |  0.0005
00:08:36 11 | 15 |  0.0066 | -0.0052
00:08:39 11 | 16 |  0.0080 |  0.0040
00:08:41 11 | 17 |  0.0104 |  0.0093
00:08:44 11 | 18 |  0.0013 | -0.0101
00:08:46 11 | 19 |  0.0036 |  0.0032
00:08:48 11 | 20 |  0.0051 |  0.0027
00:08:52 12 | 01 |  0.0062 |  0.0134
00:08:54 12 | 02 |  0.0155 |  0.0081
00:08:56 12 | 03 |  0.0164 |  0.0088
00:08:59 12 | 04 |  0.0247 |  0.0300
00:09:01 12 | 05 |  0.0003 |  0.0207
00:09:03 12 | 06 |  0.0169 |  0.0099
00:09:06 12 | 07 |  0.0235 |  0.0096
00:09:08 12 | 08 |  0.0300 |  0.0314
00:09:10 12 | 09 |  0.0067 | -0.0033
00:09:13 12 | 10 |  0.0382 |  0.0360
00:09:15 12 | 11 |  0.0215 |  0.0248
00:09:17 12 | 12 |  0.0156 |  0.0152
00:09:20 12 | 13 | -0.0022 | -0.0163
00:09:22 12 | 14 |  0.0155 |  0.0057
00:09:24 12 | 15 |  0.0151 |  0.0256
00:09:27 12 | 16 |  0.0113 |  0.0130
00:09:29 12 | 17 |  0.0065 |  0.0189
00:09:32 12 | 18 |  0.0278 |  0.0363
00:09:34 12 | 19 |  0.0287 |  0.0131
00:09:36 12 | 20 |  0.0248 |  0.0428
(32, 32) tanh 0.2 64
00:00:08 01 | 01 |  0.0088 |  0.0242
00:00:15 01 | 02 | -0.0146 | -0.0192
00:00:22 01 | 03 |  0.0100 |  0.0075
00:00:28 01 | 04 |  0.0134 |  0.0190
00:00:35 01 | 05 |  0.0135 |  0.0255
00:00:42 01 | 06 |  0.0154 |  0.0211
00:00:49 01 | 07 |  0.0165 |  0.0280
00:00:56 01 | 08 |  0.0193 |  0.0450
00:01:03 01 | 09 |  0.0085 |  0.0289
00:01:10 01 | 10 |  0.0114 |  0.0130
00:01:17 01 | 11 |  0.0112 |  0.0080
00:01:24 01 | 12 |  0.0101 |  0.0060
00:01:31 01 | 13 |  0.0307 |  0.0144
00:01:38 01 | 14 |  0.0157 |  0.0204
00:01:45 01 | 15 |  0.0134 |  0.0266
00:01:52 01 | 16 |  0.0180 |  0.0198
00:01:59 01 | 17 |  0.0218 |  0.0253
00:02:06 01 | 18 |  0.0158 |  0.0119
00:02:13 01 | 19 |  0.0323 |  0.0350
00:02:20 01 | 20 |  0.0214 |  0.0197
00:02:28 02 | 01 | -0.0145 | -0.0116
00:02:35 02 | 02 |  0.0102 |  0.0195
00:02:42 02 | 03 |  0.0056 |  0.0158
00:02:49 02 | 04 |  0.0072 |  0.0049
00:02:56 02 | 05 | -0.0274 | -0.0170
00:03:03 02 | 06 | -0.0121 |  0.0052
00:03:10 02 | 07 | -0.0011 |  0.0040
00:03:17 02 | 08 | -0.0269 | -0.0107
00:03:23 02 | 09 | -0.0040 |  0.0065
00:03:30 02 | 10 |  0.0181 |  0.0270
00:03:37 02 | 11 | -0.0186 | -0.0263
00:03:44 02 | 12 | -0.0211 | -0.0073
00:03:51 02 | 13 | -0.0074 | -0.0042
00:03:58 02 | 14 | -0.0103 | -0.0136
00:04:05 02 | 15 |  0.0163 |  0.0072
00:04:12 02 | 16 |  0.0144 |  0.0074
00:04:19 02 | 17 |  0.0191 |  0.0565
00:04:26 02 | 18 | -0.0043 |  0.0082
00:04:33 02 | 19 |  0.0090 |  0.0066
00:04:40 02 | 20 | -0.0108 | -0.0070
00:04:48 03 | 01 |  0.0058 |  0.0249
00:04:55 03 | 02 |  0.0106 | -0.0177
00:05:02 03 | 03 |  0.0118 |  0.0032
00:05:09 03 | 04 | -0.0146 | -0.0013
00:05:16 03 | 05 |  0.0166 |  0.0323
00:05:23 03 | 06 |  0.0017 | -0.0008
00:05:30 03 | 07 | -0.0045 | -0.0144
00:05:37 03 | 08 |  0.0068 |  0.0054
00:05:44 03 | 09 | -0.0065 |  0.0031
00:05:51 03 | 10 |  0.0134 |  0.0048
00:05:58 03 | 11 |  0.0159 |  0.0162
00:06:05 03 | 12 | -0.0020 | -0.0018
00:06:12 03 | 13 | -0.0047 | -0.0279
00:06:19 03 | 14 |  0.0021 | -0.0125
00:06:26 03 | 15 | -0.0217 | -0.0356
00:06:32 03 | 16 | -0.0145 | -0.0100
00:06:39 03 | 17 | -0.0254 | -0.0440
00:06:46 03 | 18 | -0.0277 | -0.0219
00:06:53 03 | 19 | -0.0131 | -0.0039
00:07:00 03 | 20 | -0.0182 | -0.0380
00:07:08 04 | 01 |  0.0278 |  0.0342
00:07:15 04 | 02 |  0.0246 |  0.0217
00:07:22 04 | 03 | -0.0141 | -0.0144
00:07:29 04 | 04 |  0.0276 |  0.0345
00:07:36 04 | 05 |  0.0068 |  0.0139
00:07:43 04 | 06 |  0.0084 |  0.0133
00:07:50 04 | 07 | -0.0345 | -0.0403
00:07:57 04 | 08 |  0.0038 |  0.0117
00:08:04 04 | 09 | -0.0319 | -0.0319
00:08:11 04 | 10 | -0.0313 | -0.0176
00:08:18 04 | 11 | -0.0166 |  0.0134
00:08:25 04 | 12 | -0.0480 | -0.0520
00:08:32 04 | 13 | -0.0316 | -0.0414
00:08:39 04 | 14 |  0.0053 | -0.0085
00:08:46 04 | 15 | -0.0065 |  0.0080
00:08:54 04 | 16 |  0.0107 |  0.0329
00:09:01 04 | 17 |  0.0286 |  0.0256
00:09:08 04 | 18 |  0.0004 | -0.0124
00:09:15 04 | 19 | -0.0218 | -0.0384
00:09:22 04 | 20 | -0.0015 | -0.0091
00:09:30 05 | 01 |  0.0048 | -0.0097
00:09:37 05 | 02 |  0.0002 |  0.0089
00:09:44 05 | 03 |  0.0090 |  0.0123
00:09:50 05 | 04 | -0.0114 | -0.0148
00:09:57 05 | 05 | -0.0119 | -0.0114
00:10:04 05 | 06 | -0.0120 | -0.0096
00:10:11 05 | 07 | -0.0147 | -0.0158
00:10:18 05 | 08 | -0.0052 | -0.0102
00:10:25 05 | 09 | -0.0080 | -0.0148
00:10:32 05 | 10 |  0.0023 | -0.0125
00:10:39 05 | 11 | -0.0013 | -0.0260
00:10:46 05 | 12 |  0.0141 |  0.0189
00:10:53 05 | 13 | -0.0199 | -0.0171
00:11:00 05 | 14 | -0.0047 | -0.0211
00:11:07 05 | 15 | -0.0069 | -0.0166
00:11:14 05 | 16 | -0.0118 | -0.0228
00:11:21 05 | 17 | -0.0180 | -0.0375
00:11:28 05 | 18 |  0.0035 |  0.0077
00:11:35 05 | 19 | -0.0052 | -0.0296
00:11:42 05 | 20 | -0.0087 | -0.0016
00:11:50 06 | 01 |  0.0141 | -0.0033
00:11:57 06 | 02 |  0.0207 | -0.0085
00:12:04 06 | 03 |  0.0391 |  0.0403
00:12:11 06 | 04 |  0.0351 |  0.0527
00:12:18 06 | 05 |  0.0283 |  0.0246
00:12:25 06 | 06 |  0.0519 |  0.0457
00:12:32 06 | 07 |  0.0181 |  0.0073</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;c4d21438-6f39-4383-b8d2-a07daf9b04b5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">00:12:39 06 | 08 |  0.0306 |  0.0128
00:12:46 06 | 09 |  0.0242 |  0.0160
00:12:53 06 | 10 |  0.0232 |  0.0098
00:13:00 06 | 11 |  0.0421 |  0.0356
00:13:07 06 | 12 |  0.0321 |  0.0241
00:13:14 06 | 13 |  0.0326 |  0.0648
00:13:21 06 | 14 |  0.0052 | -0.0148
00:13:28 06 | 15 |  0.0338 |  0.0423
00:13:35 06 | 16 |  0.0339 |  0.0495
00:13:42 06 | 17 |  0.0381 |  0.0391
00:13:49 06 | 18 |  0.0333 |  0.0510
00:13:56 06 | 19 |  0.0448 |  0.0470
00:14:03 06 | 20 |  0.0310 |  0.0433
00:14:11 07 | 01 |  0.0193 |  0.0215
00:14:18 07 | 02 |  0.0130 |  0.0052
00:14:25 07 | 03 |  0.0205 |  0.0348
00:14:32 07 | 04 |  0.0257 |  0.0196
00:14:39 07 | 05 |  0.0223 |  0.0414
00:14:46 07 | 06 |  0.0153 |  0.0368
00:14:53 07 | 07 |  0.0171 |  0.0026
00:14:60 07 | 08 |  0.0074 |  0.0163
00:15:07 07 | 09 |  0.0228 |  0.0166
00:15:14 07 | 10 |  0.0352 |  0.0240
00:15:21 07 | 11 |  0.0274 |  0.0228
00:15:28 07 | 12 |  0.0152 |  0.0066
00:15:35 07 | 13 |  0.0438 |  0.0468
00:15:42 07 | 14 |  0.0268 |  0.0178
00:15:49 07 | 15 | -0.0110 | -0.0142
00:15:56 07 | 16 |  0.0446 |  0.0447
00:16:03 07 | 17 | -0.0025 |  0.0017
00:16:10 07 | 18 |  0.0019 |  0.0247
00:16:17 07 | 19 | -0.0082 | -0.0017
00:16:24 07 | 20 |  0.0154 |  0.0303
00:16:32 08 | 01 |  0.0267 |  0.0087
00:16:39 08 | 02 | -0.0112 | -0.0057
00:16:46 08 | 03 |  0.0343 |  0.0418
00:16:53 08 | 04 |  0.0255 |  0.0083
00:16:60 08 | 05 |  0.0126 |  0.0169
00:17:07 08 | 06 |  0.0078 |  0.0322
00:17:14 08 | 07 |  0.0026 |  0.0118
00:17:21 08 | 08 |  0.0280 |  0.0524
00:17:27 08 | 09 |  0.0060 | -0.0035
00:17:34 08 | 10 |  0.0025 |  0.0099
00:17:41 08 | 11 | -0.0032 |  0.0119
00:17:48 08 | 12 |  0.0089 |  0.0171
00:17:55 08 | 13 |  0.0377 |  0.0420
00:18:02 08 | 14 |  0.0006 |  0.0229
00:18:09 08 | 15 |  0.0292 |  0.0302
00:18:16 08 | 16 |  0.0011 | -0.0103
00:18:23 08 | 17 |  0.0062 |  0.0110
00:18:30 08 | 18 | -0.0044 | -0.0113
00:18:37 08 | 19 |  0.0077 |  0.0012
00:18:44 08 | 20 |  0.0143 |  0.0161
00:18:52 09 | 01 | -0.0254 | -0.0141
00:18:59 09 | 02 |  0.0062 |  0.0145
00:19:06 09 | 03 |  0.0051 |  0.0235
00:19:13 09 | 04 |  0.0028 |  0.0125
00:19:20 09 | 05 |  0.0091 |  0.0151
00:19:27 09 | 06 |  0.0148 |  0.0125
00:19:33 09 | 07 |  0.0255 |  0.0317
00:19:40 09 | 08 | -0.0047 | -0.0008
00:19:47 09 | 09 |  0.0158 |  0.0067
00:19:54 09 | 10 |  0.0157 |  0.0342
00:20:01 09 | 11 |  0.0063 |  0.0010
00:20:08 09 | 12 |  0.0092 |  0.0165
00:20:15 09 | 13 |  0.0171 |  0.0169
00:20:22 09 | 14 |  0.0112 |  0.0060
00:20:29 09 | 15 |  0.0049 | -0.0103
00:20:36 09 | 16 |  0.0044 | -0.0051
00:20:43 09 | 17 |  0.0148 |  0.0039
00:20:50 09 | 18 |  0.0146 |  0.0152
00:20:57 09 | 19 |  0.0094 |  0.0053
00:21:04 09 | 20 |  0.0207 |  0.0066
00:21:12 10 | 01 | -0.0199 | -0.0142
00:21:19 10 | 02 | -0.0225 | -0.0097
00:21:26 10 | 03 |  0.0106 |  0.0048
00:21:33 10 | 04 | -0.0083 | -0.0121
00:21:40 10 | 05 | -0.0160 | -0.0317
00:21:47 10 | 06 | -0.0020 |  0.0063
00:21:54 10 | 07 | -0.0100 | -0.0162
00:22:01 10 | 08 | -0.0039 | -0.0070
00:22:08 10 | 09 | -0.0334 | -0.0381
00:22:14 10 | 10 | -0.0301 | -0.0319
00:22:21 10 | 11 |  0.0081 |  0.0177
00:22:28 10 | 12 | -0.0172 | -0.0105
00:22:35 10 | 13 | -0.0149 | -0.0115
00:22:42 10 | 14 | -0.0102 | -0.0162
00:22:49 10 | 15 | -0.0156 | -0.0143
00:22:56 10 | 16 | -0.0025 | -0.0177
00:23:03 10 | 17 | -0.0040 | -0.0046
00:23:10 10 | 18 | -0.0185 | -0.0150
00:23:17 10 | 19 | -0.0104 | -0.0235
00:23:24 10 | 20 | -0.0049 | -0.0053
00:23:32 11 | 01 |  0.0300 |  0.0177
00:23:39 11 | 02 |  0.0067 |  0.0100
00:23:46 11 | 03 |  0.0153 |  0.0197
00:23:53 11 | 04 |  0.0194 |  0.0257
00:23:60 11 | 05 |  0.0185 |  0.0188
00:24:07 11 | 06 |  0.0271 |  0.0172
00:24:14 11 | 07 |  0.0288 |  0.0274
00:24:21 11 | 08 |  0.0323 |  0.0241
00:24:28 11 | 09 |  0.0036 | -0.0044
00:24:35 11 | 10 |  0.0221 |  0.0290
00:24:42 11 | 11 |  0.0189 |  0.0317
00:24:49 11 | 12 |  0.0080 |  0.0185
00:24:56 11 | 13 |  0.0041 |  0.0081
00:25:03 11 | 14 |  0.0100 |  0.0156
00:25:10 11 | 15 |  0.0256 |  0.0290
00:25:17 11 | 16 |  0.0101 |  0.0224
00:25:23 11 | 17 |  0.0160 |  0.0144
00:25:30 11 | 18 |  0.0053 |  0.0120
00:25:37 11 | 19 |  0.0054 |  0.0141
00:25:44 11 | 20 |  0.0105 |  0.0102
00:25:52 12 | 01 |  0.0078 |  0.0050
00:25:59 12 | 02 |  0.0046 |  0.0138
00:26:06 12 | 03 |  0.0034 |  0.0170
00:26:13 12 | 04 | -0.0004 |  0.0087
00:26:20 12 | 05 |  0.0127 |  0.0197
00:26:27 12 | 06 |  0.0037 |  0.0018
00:26:34 12 | 07 |  0.0105 |  0.0001
00:26:41 12 | 08 |  0.0077 |  0.0021
00:26:48 12 | 09 |  0.0142 |  0.0036
00:26:55 12 | 10 |  0.0168 |  0.0253
00:27:02 12 | 11 |  0.0063 | -0.0036
00:27:09 12 | 12 |  0.0111 |  0.0193
00:27:16 12 | 13 |  0.0147 |  0.0120
00:27:23 12 | 14 |  0.0230 |  0.0113
00:27:30 12 | 15 |  0.0062 |  0.0037
00:27:37 12 | 16 |  0.0047 |  0.0106
00:27:44 12 | 17 | -0.0039 |  0.0073
00:27:51 12 | 18 |  0.0101 |  0.0082
00:27:58 12 | 19 |  0.0101 |  0.0256
00:28:05 12 | 20 |  0.0106 |  0.0261
(32, 32) tanh 0.2 256
00:00:04 01 | 01 | -0.0017 |  0.0022
00:00:07 01 | 02 | -0.0034 |  0.0039
00:00:09 01 | 03 |  0.0047 |  0.0032
00:00:12 01 | 04 |  0.0180 |  0.0126
00:00:15 01 | 05 | -0.0012 | -0.0038
00:00:18 01 | 06 |  0.0070 | -0.0024
00:00:20 01 | 07 |  0.0036 | -0.0077
00:00:23 01 | 08 | -0.0102 | -0.0054
00:00:26 01 | 09 |  0.0125 |  0.0067
00:00:29 01 | 10 |  0.0075 |  0.0032
00:00:31 01 | 11 |  0.0136 |  0.0128
00:00:34 01 | 12 | -0.0060 | -0.0087
00:00:37 01 | 13 | -0.0008 |  0.0070
00:00:40 01 | 14 |  0.0046 |  0.0001
00:00:42 01 | 15 |  0.0025 |  0.0041
00:00:45 01 | 16 | -0.0087 | -0.0096
00:00:48 01 | 17 |  0.0034 |  0.0151
00:00:51 01 | 18 |  0.0146 |  0.0182
00:00:53 01 | 19 |  0.0132 |  0.0178
00:00:56 01 | 20 |  0.0164 |  0.0190
00:00:60 02 | 01 |  0.0178 |  0.0358
00:01:03 02 | 02 |  0.0068 |  0.0054
00:01:05 02 | 03 | -0.0031 |  0.0072
00:01:08 02 | 04 | -0.0083 | -0.0158
00:01:11 02 | 05 | -0.0020 | -0.0144
00:01:14 02 | 06 | -0.0016 | -0.0006
00:01:17 02 | 07 | -0.0058 | -0.0107
00:01:19 02 | 08 |  0.0150 |  0.0321
00:01:22 02 | 09 | -0.0102 |  0.0093
00:01:25 02 | 10 | -0.0102 | -0.0188
00:01:28 02 | 11 | -0.0073 |  0.0022
00:01:30 02 | 12 | -0.0077 | -0.0068
00:01:33 02 | 13 | -0.0090 | -0.0042
00:01:36 02 | 14 | -0.0084 |  0.0044
00:01:39 02 | 15 |  0.0101 |  0.0151
00:01:42 02 | 16 | -0.0153 | -0.0120
00:01:44 02 | 17 |  0.0204 |  0.0235
00:01:47 02 | 18 |  0.0041 |  0.0152
00:01:50 02 | 19 |  0.0057 |  0.0062
00:01:53 02 | 20 | -0.0053 | -0.0102
00:01:56 03 | 01 | -0.0188 | -0.0227
00:01:59 03 | 02 | -0.0146 | -0.0140
00:02:02 03 | 03 | -0.0095 | -0.0036
00:02:05 03 | 04 | -0.0041 | -0.0009
00:02:07 03 | 05 |  0.0111 |  0.0053
00:02:10 03 | 06 |  0.0292 |  0.0218
00:02:13 03 | 07 |  0.0202 |  0.0024
00:02:16 03 | 08 |  0.0164 |  0.0225
00:02:19 03 | 09 |  0.0042 |  0.0006
00:02:22 03 | 10 |  0.0070 |  0.0035
00:02:25 03 | 11 | -0.0107 | -0.0246
00:02:27 03 | 12 |  0.0050 | -0.0102
00:02:30 03 | 13 |  0.0086 |  0.0129
00:02:33 03 | 14 |  0.0168 |  0.0351
00:02:36 03 | 15 | -0.0211 | -0.0349
00:02:39 03 | 16 | -0.0028 | -0.0014
00:02:41 03 | 17 | -0.0271 | -0.0445
00:02:44 03 | 18 | -0.0181 | -0.0467
00:02:47 03 | 19 |  0.0042 |  0.0099
00:02:50 03 | 20 |  0.0004 |  0.0008
00:02:53 04 | 01 |  0.0134 | -0.0064
00:02:56 04 | 02 | -0.0220 | -0.0127
00:02:59 04 | 03 |  0.0072 |  0.0138
00:03:02 04 | 04 | -0.0156 | -0.0090
00:03:05 04 | 05 |  0.0263 |  0.0254
00:03:07 04 | 06 | -0.0001 |  0.0013
00:03:10 04 | 07 |  0.0200 |  0.0147
00:03:13 04 | 08 |  0.0138 | -0.0251
00:03:16 04 | 09 |  0.0063 | -0.0042
00:03:18 04 | 10 | -0.0355 | -0.0518
00:03:21 04 | 11 |  0.0258 |  0.0119
00:03:24 04 | 12 | -0.0502 | -0.0444
00:03:27 04 | 13 | -0.0055 | -0.0030
00:03:30 04 | 14 | -0.0164 | -0.0140
00:03:32 04 | 15 | -0.0328 | -0.0275
00:03:35 04 | 16 | -0.0061 |  0.0080
00:03:38 04 | 17 | -0.0072 | -0.0004
00:03:41 04 | 18 | -0.0094 |  0.0031
00:03:43 04 | 19 | -0.0041 | -0.0045
00:03:46 04 | 20 |  0.0062 |  0.0027
00:03:50 05 | 01 |  0.0200 |  0.0197
00:03:53 05 | 02 | -0.0013 | -0.0090
00:03:55 05 | 03 | -0.0191 | -0.0098
00:03:58 05 | 04 |  0.0041 | -0.0113
00:04:01 05 | 05 | -0.0110 | -0.0107
00:04:04 05 | 06 |  0.0151 |  0.0144
00:04:06 05 | 07 | -0.0009 |  0.0069
00:04:09 05 | 08 |  0.0079 |  0.0008</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;207e77db-5ead-42b4-9c7f-c9d6785ed71b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">00:04:12 05 | 09 |  0.0285 |  0.0348
00:04:15 05 | 10 |  0.0126 | -0.0030
00:04:18 05 | 11 |  0.0230 |  0.0090
00:04:20 05 | 12 |  0.0128 |  0.0095
00:04:23 05 | 13 |  0.0080 |  0.0059
00:04:26 05 | 14 |  0.0072 |  0.0032
00:04:29 05 | 15 |  0.0085 |  0.0105
00:04:32 05 | 16 |  0.0124 |  0.0203
00:04:34 05 | 17 |  0.0106 |  0.0046
00:04:37 05 | 18 |  0.0018 |  0.0092
00:04:40 05 | 19 |  0.0041 |  0.0069
00:04:43 05 | 20 |  0.0010 | -0.0084
00:04:47 06 | 01 |  0.0032 |  0.0063
00:04:49 06 | 02 |  0.0283 |  0.0247
00:04:52 06 | 03 |  0.0080 |  0.0003
00:04:55 06 | 04 |  0.0052 |  0.0272
00:04:58 06 | 05 |  0.0260 |  0.0344
00:05:01 06 | 06 |  0.0073 | -0.0127
00:05:03 06 | 07 |  0.0348 |  0.0079
00:05:06 06 | 08 |  0.0013 |  0.0107
00:05:09 06 | 09 |  0.0258 |  0.0255
00:05:12 06 | 10 |  0.0326 |  0.0293
00:05:15 06 | 11 |  0.0359 |  0.0487
00:05:17 06 | 12 |  0.0352 |  0.0361
00:05:20 06 | 13 |  0.0316 |  0.0245
00:05:23 06 | 14 |  0.0320 |  0.0514
00:05:26 06 | 15 |  0.0118 |  0.0109
00:05:29 06 | 16 |  0.0365 |  0.0373
00:05:31 06 | 17 |  0.0278 |  0.0356
00:05:34 06 | 18 |  0.0378 |  0.0355
00:05:37 06 | 19 |  0.0319 |  0.0363
00:05:40 06 | 20 |  0.0306 |  0.0530
00:05:44 07 | 01 | -0.0003 |  0.0184
00:05:46 07 | 02 |  0.0263 |  0.0245
00:05:49 07 | 03 |  0.0280 |  0.0333
00:05:52 07 | 04 |  0.0197 |  0.0200
00:05:55 07 | 05 | -0.0178 | -0.0088
00:05:57 07 | 06 |  0.0192 |  0.0229
00:06:00 07 | 07 |  0.0220 |  0.0304
00:06:03 07 | 08 |  0.0310 |  0.0504
00:06:06 07 | 09 |  0.0124 |  0.0398
00:06:09 07 | 10 |  0.0212 |  0.0086
00:06:11 07 | 11 |  0.0261 |  0.0273
00:06:14 07 | 12 |  0.0415 |  0.0353
00:06:17 07 | 13 |  0.0272 |  0.0335
00:06:20 07 | 14 |  0.0068 |  0.0346
00:06:22 07 | 15 |  0.0126 |  0.0042
00:06:25 07 | 16 |  0.0275 |  0.0362
00:06:28 07 | 17 |  0.0195 |  0.0198
00:06:31 07 | 18 |  0.0347 |  0.0454
00:06:33 07 | 19 |  0.0235 |  0.0403
00:06:36 07 | 20 |  0.0295 |  0.0215
00:06:40 08 | 01 |  0.0023 |  0.0280
00:06:43 08 | 02 | -0.0003 |  0.0173
00:06:45 08 | 03 |  0.0024 |  0.0094
00:06:48 08 | 04 |  0.0150 |  0.0092
00:06:51 08 | 05 |  0.0016 |  0.0251
00:06:54 08 | 06 |  0.0035 | -0.0007
00:06:57 08 | 07 |  0.0153 |  0.0044
00:06:59 08 | 08 |  0.0091 |  0.0218
00:07:02 08 | 09 |  0.0092 |  0.0010
00:07:05 08 | 10 |  0.0045 |  0.0233
00:07:08 08 | 11 |  0.0033 |  0.0040
00:07:10 08 | 12 |  0.0247 |  0.0210
00:07:13 08 | 13 |  0.0084 | -0.0071
00:07:16 08 | 14 |  0.0103 |  0.0114
00:07:19 08 | 15 |  0.0149 |  0.0131
00:07:21 08 | 16 |  0.0102 |  0.0236
00:07:24 08 | 17 |  0.0201 |  0.0323
00:07:27 08 | 18 |  0.0046 |  0.0046
00:07:30 08 | 19 |  0.0129 | -0.0069
00:07:33 08 | 20 |  0.0137 |  0.0039
00:07:36 09 | 01 | -0.0186 | -0.0329
00:07:39 09 | 02 | -0.0142 |  0.0079
00:07:42 09 | 03 |  0.0092 |  0.0028
00:07:45 09 | 04 | -0.0004 | -0.0071
00:07:48 09 | 05 |  0.0249 |  0.0343
00:07:51 09 | 06 |  0.0243 |  0.0206
00:07:53 09 | 07 |  0.0114 |  0.0072
00:07:56 09 | 08 | -0.0004 | -0.0076
00:07:59 09 | 09 |  0.0157 |  0.0183
00:08:02 09 | 10 | -0.0008 | -0.0067
00:08:04 09 | 11 |  0.0148 |  0.0100
00:08:07 09 | 12 | -0.0031 | -0.0131
00:08:10 09 | 13 |  0.0101 |  0.0034
00:08:13 09 | 14 |  0.0195 |  0.0238
00:08:15 09 | 15 |  0.0002 | -0.0092
00:08:18 09 | 16 |  0.0204 |  0.0252
00:08:21 09 | 17 |  0.0140 |  0.0142
00:08:24 09 | 18 |  0.0226 |  0.0181
00:08:27 09 | 19 |  0.0139 |  0.0020
00:08:29 09 | 20 |  0.0107 |  0.0005
00:08:33 10 | 01 | -0.0022 |  0.0031
00:08:36 10 | 02 |  0.0003 | -0.0112
00:08:39 10 | 03 |  0.0178 |  0.0088
00:08:41 10 | 04 | -0.0053 | -0.0073
00:08:44 10 | 05 | -0.0366 | -0.0680
00:08:47 10 | 06 | -0.0323 | -0.0331
00:08:50 10 | 07 | -0.0006 |  0.0072
00:08:53 10 | 08 | -0.0233 | -0.0257
00:08:55 10 | 09 |  0.0201 |  0.0261
00:08:58 10 | 10 | -0.0256 | -0.0167
00:09:01 10 | 11 | -0.0232 | -0.0338
00:09:04 10 | 12 |  0.0156 |  0.0139
00:09:07 10 | 13 | -0.0182 | -0.0100
00:09:09 10 | 14 | -0.0428 | -0.0388
00:09:12 10 | 15 | -0.0364 | -0.0420
00:09:15 10 | 16 | -0.0272 | -0.0214
00:09:18 10 | 17 | -0.0207 | -0.0057
00:09:21 10 | 18 | -0.0455 | -0.0651
00:09:23 10 | 19 | -0.0122 | -0.0127
00:09:26 10 | 20 |  0.0038 |  0.0046
00:09:30 11 | 01 |  0.0101 |  0.0161
00:09:33 11 | 02 | -0.0027 |  0.0000
00:09:35 11 | 03 |  0.0221 |  0.0291
00:09:38 11 | 04 |  0.0277 |  0.0288
00:09:41 11 | 05 |  0.0109 |  0.0100
00:09:44 11 | 06 |  0.0092 |  0.0212
00:09:46 11 | 07 |  0.0114 |  0.0074
00:09:49 11 | 08 |  0.0163 |  0.0204
00:09:52 11 | 09 |  0.0198 |  0.0185
00:09:55 11 | 10 | -0.0028 | -0.0002
00:09:57 11 | 11 |  0.0319 |  0.0247
00:10:00 11 | 12 |  0.0320 |  0.0232
00:10:03 11 | 13 |  0.0246 |  0.0139
00:10:06 11 | 14 |  0.0230 |  0.0296
00:10:09 11 | 15 |  0.0310 |  0.0222
00:10:11 11 | 16 |  0.0321 |  0.0277
00:10:14 11 | 17 |  0.0253 |  0.0146
00:10:17 11 | 18 |  0.0126 |  0.0056
00:10:20 11 | 19 |  0.0035 | -0.0064
00:10:23 11 | 20 | -0.0016 | -0.0071
00:10:26 12 | 01 | -0.0040 | -0.0234
00:10:29 12 | 02 |  0.0082 |  0.0265
00:10:32 12 | 03 |  0.0048 |  0.0124
00:10:34 12 | 04 |  0.0222 |  0.0273
00:10:37 12 | 05 |  0.0371 |  0.0195
00:10:40 12 | 06 |  0.0332 |  0.0144
00:10:43 12 | 07 |  0.0209 |  0.0026
00:10:46 12 | 08 |  0.0187 |  0.0164
00:10:49 12 | 09 |  0.0209 |  0.0073
00:10:51 12 | 10 |  0.0091 | -0.0004
00:10:54 12 | 11 |  0.0054 | -0.0105
00:10:57 12 | 12 | -0.0052 | -0.0070
00:10:60 12 | 13 |  0.0029 |  0.0085
00:11:02 12 | 14 | -0.0067 | -0.0129
00:11:05 12 | 15 |  0.0144 |  0.0382
00:11:08 12 | 16 |  0.0024 | -0.0046
00:11:11 12 | 17 |  0.0058 |  0.0220
00:11:14 12 | 18 | -0.0059 | -0.0118
00:11:17 12 | 19 |  0.0023 | -0.0171
00:11:19 12 | 20 |  0.0053 |  0.0131
(32, 16) tanh 0.2 64
00:00:08 01 | 01 | -0.0083 |  0.0022
00:00:14 01 | 02 | -0.0200 | -0.0344
00:00:21 01 | 03 |  0.0081 |  0.0054
00:00:28 01 | 04 | -0.0092 | -0.0048
00:00:34 01 | 05 |  0.0278 |  0.0230
00:00:41 01 | 06 |  0.0194 |  0.0217
00:00:48 01 | 07 |  0.0123 |  0.0169
00:00:54 01 | 08 |  0.0224 |  0.0204
00:01:01 01 | 09 |  0.0114 |  0.0101
00:01:08 01 | 10 |  0.0153 |  0.0186
00:01:14 01 | 11 |  0.0197 |  0.0155
00:01:21 01 | 12 |  0.0220 |  0.0228
00:01:27 01 | 13 |  0.0115 |  0.0064
00:01:34 01 | 14 |  0.0020 | -0.0000
00:01:41 01 | 15 |  0.0099 |  0.0066
00:01:47 01 | 16 |  0.0062 |  0.0154
00:01:54 01 | 17 |  0.0075 |  0.0038
00:02:01 01 | 18 |  0.0050 | -0.0114
00:02:07 01 | 19 |  0.0024 | -0.0110
00:02:14 01 | 20 | -0.0020 | -0.0040
00:02:21 02 | 01 | -0.0101 | -0.0180
00:02:28 02 | 02 | -0.0101 | -0.0295
00:02:35 02 | 03 |  0.0019 |  0.0121
00:02:41 02 | 04 |  0.0125 |  0.0339
00:02:48 02 | 05 | -0.0013 | -0.0042
00:02:55 02 | 06 |  0.0055 |  0.0004
00:03:01 02 | 07 |  0.0034 |  0.0191
00:03:08 02 | 08 |  0.0162 |  0.0210
00:03:14 02 | 09 |  0.0096 |  0.0050
00:03:21 02 | 10 |  0.0048 |  0.0064
00:03:28 02 | 11 |  0.0075 |  0.0054
00:03:34 02 | 12 |  0.0134 |  0.0187
00:03:41 02 | 13 |  0.0190 |  0.0317
00:03:47 02 | 14 |  0.0076 |  0.0129
00:03:54 02 | 15 | -0.0111 | -0.0117
00:04:01 02 | 16 |  0.0212 |  0.0239
00:04:07 02 | 17 |  0.0068 |  0.0111
00:04:14 02 | 18 |  0.0018 | -0.0037
00:04:21 02 | 19 |  0.0064 |  0.0041
00:04:27 02 | 20 | -0.0004 |  0.0011
00:04:35 03 | 01 | -0.0124 | -0.0238
00:04:41 03 | 02 |  0.0049 |  0.0084
00:04:48 03 | 03 |  0.0089 |  0.0277
00:04:55 03 | 04 |  0.0002 |  0.0192
00:05:01 03 | 05 | -0.0094 | -0.0187
00:05:08 03 | 06 | -0.0334 | -0.0169
00:05:15 03 | 07 | -0.0075 | -0.0045
00:05:21 03 | 08 | -0.0241 | -0.0252
00:05:28 03 | 09 | -0.0041 | -0.0031
00:05:35 03 | 10 | -0.0140 | -0.0373
00:05:41 03 | 11 | -0.0174 | -0.0340
00:05:48 03 | 12 | -0.0161 | -0.0247
00:05:55 03 | 13 |  0.0043 |  0.0128
00:06:01 03 | 14 |  0.0014 |  0.0187
00:06:08 03 | 15 | -0.0146 | -0.0276
00:06:15 03 | 16 | -0.0239 | -0.0349
00:06:21 03 | 17 | -0.0111 | -0.0258
00:06:28 03 | 18 |  0.0149 |  0.0200
00:06:35 03 | 19 | -0.0054 | -0.0154
00:06:41 03 | 20 |  0.0046 | -0.0046
00:06:49 04 | 01 | -0.0143 | -0.0124
00:06:56 04 | 02 |  0.0364 |  0.0324
00:07:03 04 | 03 | -0.0080 | -0.0135
00:07:09 04 | 04 | -0.0016 | -0.0096
00:07:16 04 | 05 | -0.0298 | -0.0426
00:07:23 04 | 06 |  0.0030 | -0.0029
00:07:30 04 | 07 | -0.0145 | -0.0079
00:07:36 04 | 08 | -0.0107 | -0.0048
00:07:43 04 | 09 |  0.0042 | -0.0171</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;19bdd9e4-212d-4840-92fd-8f4c07e8d3b9&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">00:07:50 04 | 10 | -0.0240 | -0.0470
00:07:57 04 | 11 |  0.0058 | -0.0138
00:08:03 04 | 12 |  0.0134 | -0.0145
00:08:10 04 | 13 |  0.0087 | -0.0101
00:08:17 04 | 14 |  0.0077 |  0.0117
00:08:23 04 | 15 | -0.0015 |  0.0001
00:08:30 04 | 16 | -0.0186 | -0.0236
00:08:37 04 | 17 | -0.0026 | -0.0001
00:08:43 04 | 18 |  0.0024 |  0.0066
00:08:50 04 | 19 |  0.0114 |  0.0032
00:08:57 04 | 20 | -0.0197 | -0.0252
00:09:04 05 | 01 | -0.0098 |  0.0072
00:09:11 05 | 02 | -0.0040 |  0.0010
00:09:18 05 | 03 |  0.0217 |  0.0270
00:09:24 05 | 04 |  0.0075 |  0.0141
00:09:31 05 | 05 |  0.0222 |  0.0148
00:09:38 05 | 06 |  0.0161 |  0.0113
00:09:45 05 | 07 |  0.0065 |  0.0092
00:09:51 05 | 08 | -0.0083 | -0.0069
00:09:58 05 | 09 |  0.0233 |  0.0212
00:10:05 05 | 10 | -0.0091 | -0.0292
00:10:11 05 | 11 | -0.0000 | -0.0055
00:10:18 05 | 12 | -0.0113 | -0.0053
00:10:25 05 | 13 |  0.0046 |  0.0157
00:10:31 05 | 14 | -0.0011 |  0.0087
00:10:38 05 | 15 |  0.0102 |  0.0195
00:10:45 05 | 16 | -0.0038 |  0.0188
00:10:52 05 | 17 |  0.0076 | -0.0007
00:10:58 05 | 18 |  0.0039 |  0.0021
00:11:05 05 | 19 |  0.0134 |  0.0119
00:11:12 05 | 20 |  0.0066 | -0.0040
00:11:19 06 | 01 |  0.0210 |  0.0132
00:11:26 06 | 02 | -0.0009 |  0.0146
00:11:32 06 | 03 |  0.0311 |  0.0152
00:11:39 06 | 04 |  0.0344 |  0.0321
00:11:46 06 | 05 |  0.0298 |  0.0307
00:11:53 06 | 06 |  0.0413 |  0.0377
00:11:59 06 | 07 |  0.0380 |  0.0194
00:12:06 06 | 08 |  0.0210 |  0.0159
00:12:13 06 | 09 |  0.0181 |  0.0321
00:12:19 06 | 10 |  0.0455 |  0.0483
00:12:26 06 | 11 |  0.0312 |  0.0269
00:12:33 06 | 12 |  0.0313 |  0.0244
00:12:40 06 | 13 |  0.0312 |  0.0226
00:12:47 06 | 14 |  0.0417 |  0.0113
00:12:54 06 | 15 |  0.0191 |  0.0007
00:13:01 06 | 16 |  0.0345 |  0.0033
00:13:09 06 | 17 |  0.0311 |  0.0277
00:13:15 06 | 18 |  0.0347 |  0.0224
00:13:22 06 | 19 |  0.0444 |  0.0567
00:13:29 06 | 20 |  0.0154 |  0.0196
00:13:36 07 | 01 | -0.0175 | -0.0210
00:13:43 07 | 02 |  0.0326 |  0.0434
00:13:49 07 | 03 |  0.0040 |  0.0113
00:13:56 07 | 04 |  0.0228 |  0.0281
00:14:03 07 | 05 |  0.0359 |  0.0508
00:14:10 07 | 06 |  0.0337 |  0.0356
00:14:16 07 | 07 |  0.0367 |  0.0608
00:14:23 07 | 08 |  0.0240 |  0.0615
00:14:30 07 | 09 |  0.0376 |  0.0479
00:14:37 07 | 10 | -0.0057 |  0.0212
00:14:43 07 | 11 |  0.0309 |  0.0498
00:14:50 07 | 12 |  0.0181 |  0.0287
00:14:57 07 | 13 |  0.0142 |  0.0012
00:15:04 07 | 14 |  0.0161 |  0.0369
00:15:11 07 | 15 | -0.0070 |  0.0144
00:15:17 07 | 16 |  0.0218 |  0.0298
00:15:24 07 | 17 |  0.0102 |  0.0395
00:15:31 07 | 18 |  0.0220 |  0.0409
00:15:38 07 | 19 | -0.0172 |  0.0050
00:15:44 07 | 20 |  0.0189 |  0.0264
00:15:52 08 | 01 |  0.0162 |  0.0263
00:15:59 08 | 02 |  0.0126 |  0.0092
00:16:05 08 | 03 |  0.0103 |  0.0089
00:16:12 08 | 04 |  0.0221 |  0.0245
00:16:19 08 | 05 |  0.0108 |  0.0258
00:16:25 08 | 06 | -0.0102 | -0.0110
00:16:32 08 | 07 |  0.0301 |  0.0323
00:16:39 08 | 08 |  0.0043 |  0.0123
00:16:45 08 | 09 |  0.0161 |  0.0196
00:16:52 08 | 10 | -0.0054 |  0.0136
00:16:59 08 | 11 | -0.0058 |  0.0291
00:17:05 08 | 12 |  0.0111 |  0.0097
00:17:12 08 | 13 | -0.0004 |  0.0199
00:17:19 08 | 14 |  0.0058 |  0.0156
00:17:25 08 | 15 |  0.0110 |  0.0369
00:17:32 08 | 16 |  0.0154 |  0.0187
00:17:39 08 | 17 |  0.0047 |  0.0292
00:17:45 08 | 18 |  0.0099 |  0.0113
00:17:52 08 | 19 |  0.0027 |  0.0067
00:17:59 08 | 20 |  0.0138 | -0.0028
00:18:06 09 | 01 |  0.0037 |  0.0056
00:18:13 09 | 02 |  0.0134 |  0.0063
00:18:20 09 | 03 |  0.0345 |  0.0255
00:18:27 09 | 04 |  0.0171 |  0.0088
00:18:33 09 | 05 |  0.0068 |  0.0087
00:18:40 09 | 06 | -0.0096 | -0.0181
00:18:47 09 | 07 |  0.0093 |  0.0013
00:18:53 09 | 08 |  0.0217 |  0.0145
00:19:00 09 | 09 |  0.0103 | -0.0041
00:19:07 09 | 10 |  0.0129 |  0.0128
00:19:14 09 | 11 |  0.0177 |  0.0098
00:19:20 09 | 12 |  0.0026 | -0.0224
00:19:27 09 | 13 |  0.0122 |  0.0009
00:19:34 09 | 14 |  0.0127 |  0.0230
00:19:41 09 | 15 |  0.0155 |  0.0038
00:19:47 09 | 16 |  0.0094 |  0.0091
00:19:54 09 | 17 |  0.0187 |  0.0210
00:20:01 09 | 18 |  0.0146 |  0.0047
00:20:07 09 | 19 |  0.0104 | -0.0017
00:20:14 09 | 20 |  0.0001 | -0.0201
00:20:21 10 | 01 | -0.0277 | -0.0189
00:20:28 10 | 02 | -0.0198 | -0.0123
00:20:35 10 | 03 |  0.0437 |  0.0443
00:20:42 10 | 04 |  0.0295 |  0.0365
00:20:48 10 | 05 |  0.0231 |  0.0212
00:20:55 10 | 06 | -0.0124 |  0.0008
00:21:01 10 | 07 | -0.0362 | -0.0167
00:21:08 10 | 08 | -0.0020 |  0.0049
00:21:15 10 | 09 | -0.0087 | -0.0077
00:21:22 10 | 10 |  0.0112 |  0.0184
00:21:28 10 | 11 |  0.0071 |  0.0131
00:21:35 10 | 12 | -0.0001 | -0.0173
00:21:42 10 | 13 |  0.0069 |  0.0136
00:21:48 10 | 14 | -0.0092 | -0.0155
00:21:55 10 | 15 | -0.0264 | -0.0352
00:22:02 10 | 16 | -0.0025 | -0.0008
00:22:08 10 | 17 |  0.0257 |  0.0395
00:22:15 10 | 18 |  0.0139 |  0.0039
00:22:22 10 | 19 |  0.0042 |  0.0062
00:22:28 10 | 20 |  0.0023 | -0.0038
00:22:36 11 | 01 | -0.0003 | -0.0002
00:22:43 11 | 02 |  0.0229 |  0.0195
00:22:50 11 | 03 |  0.0268 |  0.0285
00:22:56 11 | 04 |  0.0067 |  0.0051
00:23:03 11 | 05 |  0.0262 |  0.0254
00:23:10 11 | 06 |  0.0109 | -0.0021
00:23:16 11 | 07 |  0.0140 |  0.0263
00:23:23 11 | 08 |  0.0086 |  0.0020
00:23:30 11 | 09 |  0.0279 |  0.0322
00:23:37 11 | 10 |  0.0180 |  0.0119
00:23:43 11 | 11 |  0.0140 |  0.0121
00:23:50 11 | 12 |  0.0168 |  0.0270
00:23:57 11 | 13 |  0.0094 |  0.0159
00:24:04 11 | 14 |  0.0091 |  0.0046
00:24:10 11 | 15 |  0.0082 | -0.0014
00:24:17 11 | 16 |  0.0117 | -0.0047
00:24:24 11 | 17 |  0.0147 |  0.0129
00:24:30 11 | 18 |  0.0189 |  0.0123
00:24:37 11 | 19 | -0.0104 | -0.0136
00:24:44 11 | 20 | -0.0021 |  0.0015
00:24:51 12 | 01 |  0.0106 |  0.0406
00:24:58 12 | 02 |  0.0290 |  0.0146
00:25:05 12 | 03 |  0.0227 |  0.0061
00:25:11 12 | 04 |  0.0177 |  0.0243
00:25:18 12 | 05 |  0.0077 | -0.0108
00:25:25 12 | 06 |  0.0143 | -0.0011
00:25:31 12 | 07 |  0.0094 | -0.0100
00:25:38 12 | 08 |  0.0214 |  0.0384
00:25:45 12 | 09 |  0.0023 |  0.0129
00:25:51 12 | 10 | -0.0032 | -0.0000
00:25:58 12 | 11 | -0.0057 | -0.0001
00:26:05 12 | 12 | -0.0180 | -0.0403
00:26:11 12 | 13 | -0.0156 | -0.0297
00:26:18 12 | 14 | -0.0067 |  0.0135
00:26:25 12 | 15 | -0.0210 | -0.0119
00:26:31 12 | 16 | -0.0092 | -0.0009
00:26:38 12 | 17 | -0.0192 | -0.0172
00:26:45 12 | 18 | -0.0071 | -0.0043
00:26:51 12 | 19 | -0.0135 | -0.0009
00:26:58 12 | 20 | -0.0176 | -0.0085
(32, 16) tanh 0.2 256
00:00:04 01 | 01 |  0.0048 |  0.0108
00:00:06 01 | 02 |  0.0214 |  0.0171
00:00:09 01 | 03 |  0.0062 | -0.0020
00:00:11 01 | 04 |  0.0132 |  0.0179
00:00:14 01 | 05 | -0.0023 | -0.0012
00:00:16 01 | 06 |  0.0196 |  0.0211
00:00:19 01 | 07 |  0.0193 |  0.0416
00:00:21 01 | 08 |  0.0144 |  0.0202
00:00:24 01 | 09 |  0.0138 |  0.0133
00:00:26 01 | 10 |  0.0079 |  0.0104
00:00:29 01 | 11 |  0.0238 |  0.0277
00:00:31 01 | 12 |  0.0164 |  0.0288
00:00:34 01 | 13 |  0.0139 |  0.0148
00:00:36 01 | 14 | -0.0080 |  0.0001
00:00:39 01 | 15 |  0.0029 |  0.0062
00:00:41 01 | 16 |  0.0128 |  0.0286
00:00:44 01 | 17 |  0.0058 |  0.0107
00:00:46 01 | 18 |  0.0031 |  0.0054
00:00:49 01 | 19 |  0.0045 |  0.0055
00:00:51 01 | 20 |  0.0070 |  0.0203
00:00:54 02 | 01 |  0.0011 | -0.0059
00:00:57 02 | 02 | -0.0068 | -0.0061
00:00:59 02 | 03 | -0.0133 | -0.0107
00:01:02 02 | 04 | -0.0225 | -0.0130
00:01:04 02 | 05 |  0.0027 |  0.0195
00:01:07 02 | 06 |  0.0013 | -0.0006
00:01:09 02 | 07 |  0.0131 |  0.0120
00:01:12 02 | 08 | -0.0157 | -0.0194
00:01:14 02 | 09 |  0.0079 |  0.0089
00:01:17 02 | 10 | -0.0056 | -0.0018
00:01:20 02 | 11 | -0.0101 | -0.0117
00:01:22 02 | 12 |  0.0001 | -0.0007
00:01:25 02 | 13 |  0.0015 | -0.0145
00:01:27 02 | 14 | -0.0050 |  0.0128
00:01:30 02 | 15 |  0.0036 | -0.0139
00:01:32 02 | 16 |  0.0099 |  0.0080
00:01:35 02 | 17 |  0.0094 | -0.0029
00:01:37 02 | 18 | -0.0011 | -0.0128
00:01:40 02 | 19 |  0.0115 |  0.0147
00:01:42 02 | 20 | -0.0067 | -0.0162
00:01:46 03 | 01 | -0.0237 | -0.0184
00:01:48 03 | 02 | -0.0174 | -0.0111
00:01:51 03 | 03 | -0.0143 | -0.0217
00:01:53 03 | 04 | -0.0084 | -0.0207
00:01:56 03 | 05 |  0.0197 |  0.0258
00:01:58 03 | 06 | -0.0126 | -0.0022
00:02:01 03 | 07 | -0.0140 | -0.0251
00:02:03 03 | 08 | -0.0188 | -0.0408
00:02:06 03 | 09 |  0.0017 | -0.0189
00:02:08 03 | 10 |  0.0156 |  0.0038</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;a89d1ea4-7939-445f-aad1-15bce57015a7&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">00:02:11 03 | 11 |  0.0093 | -0.0039
00:02:13 03 | 12 | -0.0006 | -0.0158
00:02:16 03 | 13 | -0.0067 | -0.0148
00:02:18 03 | 14 | -0.0032 | -0.0116
00:02:21 03 | 15 |  0.0043 |  0.0156
00:02:23 03 | 16 | -0.0052 | -0.0090
00:02:26 03 | 17 |  0.0153 |  0.0139
00:02:28 03 | 18 |  0.0007 | -0.0051
00:02:31 03 | 19 |  0.0173 |  0.0033
00:02:33 03 | 20 |  0.0120 | -0.0080
00:02:37 04 | 01 |  0.0081 |  0.0213
00:02:39 04 | 02 |  0.0026 | -0.0070
00:02:42 04 | 03 |  0.0079 |  0.0085
00:02:44 04 | 04 | -0.0045 | -0.0009
00:02:47 04 | 05 |  0.0247 |  0.0284
00:02:49 04 | 06 |  0.0471 |  0.0355
00:02:52 04 | 07 |  0.0151 |  0.0278
00:02:54 04 | 08 |  0.0080 |  0.0039
00:02:57 04 | 09 |  0.0221 | -0.0048
00:02:59 04 | 10 |  0.0047 | -0.0091
00:03:02 04 | 11 |  0.0265 |  0.0024
00:03:04 04 | 12 |  0.0125 |  0.0012
00:03:07 04 | 13 | -0.0102 |  0.0007
00:03:09 04 | 14 |  0.0001 |  0.0028
00:03:12 04 | 15 | -0.0096 | -0.0186
00:03:15 04 | 16 |  0.0192 |  0.0127
00:03:17 04 | 17 |  0.0195 |  0.0184
00:03:20 04 | 18 | -0.0043 | -0.0217
00:03:22 04 | 19 |  0.0088 | -0.0144
00:03:25 04 | 20 |  0.0063 | -0.0050
00:03:28 05 | 01 |  0.0001 |  0.0053
00:03:31 05 | 02 | -0.0227 | -0.0162
00:03:33 05 | 03 | -0.0019 | -0.0056
00:03:36 05 | 04 |  0.0199 |  0.0254
00:03:38 05 | 05 |  0.0097 |  0.0012
00:03:41 05 | 06 |  0.0118 |  0.0026
00:03:43 05 | 07 |  0.0104 |  0.0097
00:03:46 05 | 08 | -0.0083 | -0.0120
00:03:48 05 | 09 |  0.0090 |  0.0140
00:03:51 05 | 10 | -0.0172 | -0.0177
00:03:53 05 | 11 | -0.0137 | -0.0092
00:03:56 05 | 12 | -0.0143 | -0.0109
00:03:58 05 | 13 | -0.0110 | -0.0102
00:04:01 05 | 14 | -0.0036 |  0.0053
00:04:03 05 | 15 |  0.0005 |  0.0096
00:04:06 05 | 16 |  0.0056 | -0.0086
00:04:08 05 | 17 | -0.0069 | -0.0168
00:04:11 05 | 18 |  0.0038 |  0.0023
00:04:13 05 | 19 |  0.0026 | -0.0143
00:04:16 05 | 20 | -0.0050 | -0.0202
00:04:20 06 | 01 |  0.0226 |  0.0223
00:04:22 06 | 02 | -0.0019 |  0.0042
00:04:25 06 | 03 | -0.0127 | -0.0056
00:04:27 06 | 04 |  0.0233 |  0.0244
00:04:30 06 | 05 |  0.0224 |  0.0095
00:04:32 06 | 06 |  0.0157 |  0.0323
00:04:35 06 | 07 |  0.0196 |  0.0134
00:04:37 06 | 08 |  0.0356 |  0.0096
00:04:40 06 | 09 | -0.0019 | -0.0004
00:04:42 06 | 10 |  0.0051 |  0.0231
00:04:45 06 | 11 |  0.0016 | -0.0150
00:04:47 06 | 12 |  0.0024 | -0.0102
00:04:50 06 | 13 |  0.0423 |  0.0416
00:04:53 06 | 14 |  0.0221 |  0.0180
00:04:55 06 | 15 |  0.0144 |  0.0061
00:04:58 06 | 16 | -0.0021 | -0.0063
00:05:00 06 | 17 | -0.0024 | -0.0072
00:05:03 06 | 18 |  0.0084 | -0.0078
00:05:05 06 | 19 |  0.0066 |  0.0285
00:05:08 06 | 20 |  0.0076 |  0.0182
00:05:11 07 | 01 |  0.0123 |  0.0204
00:05:14 07 | 02 |  0.0119 |  0.0517
00:05:16 07 | 03 |  0.0350 |  0.0562
00:05:19 07 | 04 |  0.0225 |  0.0460
00:05:21 07 | 05 |  0.0142 |  0.0368
00:05:24 07 | 06 |  0.0376 |  0.0449
00:05:26 07 | 07 |  0.0114 |  0.0155
00:05:29 07 | 08 |  0.0222 |  0.0197
00:05:31 07 | 09 |  0.0373 |  0.0540
00:05:34 07 | 10 |  0.0255 |  0.0461
00:05:36 07 | 11 |  0.0373 |  0.0620
00:05:39 07 | 12 |  0.0234 |  0.0207
00:05:41 07 | 13 |  0.0231 |  0.0298
00:05:44 07 | 14 |  0.0207 |  0.0280
00:05:46 07 | 15 |  0.0205 |  0.0331
00:05:49 07 | 16 |  0.0372 |  0.0466
00:05:51 07 | 17 |  0.0120 |  0.0335
00:05:54 07 | 18 |  0.0154 |  0.0438
00:05:56 07 | 19 |  0.0020 |  0.0229
00:05:59 07 | 20 |  0.0325 |  0.0380
00:06:02 08 | 01 |  0.0144 |  0.0190
00:06:05 08 | 02 | -0.0075 |  0.0072
00:06:07 08 | 03 |  0.0127 | -0.0042
00:06:10 08 | 04 |  0.0062 |  0.0132
00:06:12 08 | 05 |  0.0336 |  0.0337
00:06:15 08 | 06 |  0.0015 | -0.0008
00:06:17 08 | 07 |  0.0114 |  0.0049
00:06:20 08 | 08 |  0.0104 |  0.0086
00:06:22 08 | 09 |  0.0228 |  0.0005
00:06:25 08 | 10 |  0.0137 | -0.0031
00:06:27 08 | 11 |  0.0315 |  0.0220
00:06:30 08 | 12 |  0.0280 |  0.0209
00:06:32 08 | 13 |  0.0187 |  0.0011
00:06:35 08 | 14 |  0.0174 |  0.0094
00:06:37 08 | 15 |  0.0251 |  0.0283
00:06:40 08 | 16 |  0.0189 |  0.0194
00:06:42 08 | 17 |  0.0219 |  0.0310
00:06:45 08 | 18 |  0.0315 |  0.0319
00:06:48 08 | 19 |  0.0227 |  0.0098
00:06:50 08 | 20 |  0.0293 |  0.0223
00:06:53 09 | 01 | -0.0186 | -0.0165
00:06:56 09 | 02 | -0.0044 | -0.0210
00:06:58 09 | 03 | -0.0137 | -0.0095
00:07:01 09 | 04 |  0.0100 |  0.0175
00:07:03 09 | 05 |  0.0060 |  0.0228
00:07:06 09 | 06 |  0.0112 |  0.0008
00:07:09 09 | 07 | -0.0041 | -0.0021
00:07:11 09 | 08 |  0.0040 |  0.0165
00:07:14 09 | 09 |  0.0301 |  0.0229
00:07:16 09 | 10 |  0.0307 |  0.0277
00:07:19 09 | 11 |  0.0098 |  0.0122
00:07:21 09 | 12 |  0.0284 |  0.0115
00:07:24 09 | 13 |  0.0249 |  0.0350
00:07:26 09 | 14 |  0.0104 |  0.0153
00:07:29 09 | 15 |  0.0054 |  0.0001
00:07:31 09 | 16 |  0.0140 |  0.0113
00:07:34 09 | 17 |  0.0159 |  0.0245
00:07:37 09 | 18 |  0.0267 |  0.0226
00:07:39 09 | 19 |  0.0178 |  0.0204
00:07:42 09 | 20 |  0.0137 |  0.0254
00:07:45 10 | 01 |  0.0179 |  0.0125
00:07:48 10 | 02 |  0.0295 |  0.0070
00:07:50 10 | 03 | -0.0049 | -0.0239
00:07:53 10 | 04 |  0.0057 |  0.0094
00:07:55 10 | 05 |  0.0356 |  0.0335
00:07:58 10 | 06 | -0.0097 | -0.0063
00:08:00 10 | 07 | -0.0026 | -0.0081
00:08:03 10 | 08 | -0.0005 | -0.0207
00:08:05 10 | 09 | -0.0342 | -0.0319
00:08:08 10 | 10 |  0.0405 |  0.0425
00:08:10 10 | 11 | -0.0239 | -0.0160
00:08:13 10 | 12 | -0.0194 | -0.0292
00:08:15 10 | 13 | -0.0078 | -0.0119
00:08:18 10 | 14 | -0.0346 | -0.0316
00:08:21 10 | 15 | -0.0469 | -0.0385
00:08:23 10 | 16 | -0.0410 | -0.0233
00:08:26 10 | 17 | -0.0053 | -0.0095
00:08:28 10 | 18 | -0.0277 | -0.0221
00:08:31 10 | 19 | -0.0049 | -0.0003
00:08:33 10 | 20 | -0.0310 | -0.0203
00:08:36 11 | 01 |  0.0165 |  0.0154
00:08:39 11 | 02 |  0.0275 |  0.0225
00:08:41 11 | 03 |  0.0460 |  0.0543
00:08:44 11 | 04 |  0.0232 |  0.0301
00:08:46 11 | 05 |  0.0356 |  0.0293
00:08:49 11 | 06 |  0.0336 |  0.0323
00:08:51 11 | 07 |  0.0304 |  0.0201
00:08:54 11 | 08 | -0.0016 | -0.0003
00:08:56 11 | 09 |  0.0303 |  0.0259
00:08:59 11 | 10 |  0.0193 | -0.0061
00:09:01 11 | 11 |  0.0273 |  0.0216
00:09:04 11 | 12 | -0.0031 | -0.0098
00:09:06 11 | 13 |  0.0049 |  0.0017
00:09:09 11 | 14 | -0.0083 | -0.0133
00:09:11 11 | 15 | -0.0099 | -0.0213
00:09:14 11 | 16 |  0.0006 | -0.0011
00:09:16 11 | 17 | -0.0022 | -0.0056
00:09:19 11 | 18 | -0.0001 | -0.0089
00:09:21 11 | 19 | -0.0086 | -0.0120
00:09:24 11 | 20 |  0.0092 |  0.0063
00:09:27 12 | 01 |  0.0025 | -0.0104
00:09:30 12 | 02 |  0.0041 |  0.0057
00:09:32 12 | 03 |  0.0171 |  0.0068
00:09:35 12 | 04 |  0.0038 | -0.0133
00:09:37 12 | 05 |  0.0055 |  0.0034
00:09:40 12 | 06 |  0.0014 | -0.0007
00:09:42 12 | 07 |  0.0104 |  0.0216
00:09:45 12 | 08 |  0.0149 |  0.0168
00:09:48 12 | 09 |  0.0102 |  0.0140
00:09:50 12 | 10 |  0.0282 |  0.0405
00:09:53 12 | 11 |  0.0213 |  0.0407
00:09:55 12 | 12 |  0.0029 |  0.0013
00:09:58 12 | 13 |  0.0188 |  0.0061
00:10:00 12 | 14 |  0.0167 |  0.0145
00:10:03 12 | 15 |  0.0078 |  0.0151
00:10:05 12 | 16 |  0.0017 | -0.0114
00:10:08 12 | 17 |  0.0098 |  0.0099
00:10:10 12 | 18 |  0.0053 |  0.0065
00:10:13 12 | 19 |  0.0195 |  0.0075
00:10:15 12 | 20 |  0.0061 | -0.0073
(16, 8) tanh 0.2 64
00:00:07 01 | 01 |  0.0075 | -0.0055
00:00:14 01 | 02 |  0.0144 |  0.0130
00:00:20 01 | 03 |  0.0084 |  0.0153
00:00:27 01 | 04 |  0.0121 |  0.0119
00:00:33 01 | 05 |  0.0137 |  0.0131
00:00:39 01 | 06 |  0.0183 |  0.0133
00:00:46 01 | 07 |  0.0262 |  0.0443
00:00:52 01 | 08 |  0.0291 |  0.0367
00:00:59 01 | 09 |  0.0168 |  0.0064
00:01:05 01 | 10 |  0.0084 |  0.0174
00:01:12 01 | 11 |  0.0185 |  0.0365
00:01:18 01 | 12 |  0.0185 |  0.0300
00:01:25 01 | 13 |  0.0079 |  0.0023
00:01:31 01 | 14 |  0.0064 | -0.0020
00:01:38 01 | 15 |  0.0146 |  0.0292
00:01:44 01 | 16 |  0.0022 | -0.0160
00:01:50 01 | 17 |  0.0105 |  0.0037
00:01:55 01 | 18 | -0.0040 | -0.0180
00:02:01 01 | 19 |  0.0039 |  0.0071
00:02:06 01 | 20 |  0.0151 |  0.0202
00:02:13 02 | 01 |  0.0052 | -0.0240
00:02:18 02 | 02 |  0.0135 |  0.0197
00:02:24 02 | 03 |  0.0008 |  0.0004
00:02:30 02 | 04 |  0.0098 |  0.0210
00:02:35 02 | 05 |  0.0005 |  0.0101
00:02:41 02 | 06 |  0.0174 |  0.0231
00:02:46 02 | 07 |  0.0064 |  0.0023
00:02:52 02 | 08 | -0.0012 |  0.0102
00:02:58 02 | 09 |  0.0099 |  0.0029
00:03:03 02 | 10 |  0.0045 | -0.0087
00:03:09 02 | 11 |  0.0014 |  0.0074</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;e45533b1-8aa5-4d3a-9023-a4f3bbb186bf&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">00:03:14 02 | 12 |  0.0077 |  0.0166
00:03:20 02 | 13 |  0.0082 |  0.0116
00:03:26 02 | 14 |  0.0043 |  0.0123
00:03:31 02 | 15 | -0.0081 | -0.0062
00:03:37 02 | 16 | -0.0009 | -0.0051
00:03:42 02 | 17 |  0.0050 |  0.0076
00:03:48 02 | 18 | -0.0105 | -0.0121
00:03:54 02 | 19 |  0.0005 | -0.0066
00:03:59 02 | 20 | -0.0038 |  0.0041
00:04:05 03 | 01 | -0.0332 | -0.0385
00:04:11 03 | 02 | -0.0022 | -0.0078
00:04:16 03 | 03 |  0.0257 |  0.0148
00:04:22 03 | 04 |  0.0001 |  0.0197
00:04:28 03 | 05 | -0.0216 | -0.0222
00:04:33 03 | 06 | -0.0101 | -0.0002
00:04:39 03 | 07 | -0.0058 |  0.0128
00:04:44 03 | 08 | -0.0039 | -0.0069
00:04:50 03 | 09 | -0.0115 | -0.0057
00:04:55 03 | 10 |  0.0047 | -0.0007
00:05:01 03 | 11 | -0.0016 | -0.0172
00:05:06 03 | 12 |  0.0151 |  0.0122
00:05:12 03 | 13 |  0.0134 |  0.0175
00:05:17 03 | 14 |  0.0089 |  0.0045
00:05:23 03 | 15 |  0.0037 | -0.0014
00:05:28 03 | 16 |  0.0048 | -0.0074
00:05:34 03 | 17 | -0.0002 |  0.0017
00:05:40 03 | 18 | -0.0010 | -0.0007
00:05:45 03 | 19 | -0.0220 | -0.0221
00:05:51 03 | 20 | -0.0117 | -0.0252
00:05:57 04 | 01 | -0.0114 | -0.0124
00:06:02 04 | 02 | -0.0133 | -0.0090
00:06:08 04 | 03 |  0.0300 |  0.0037
00:06:13 04 | 04 | -0.0054 | -0.0108
00:06:19 04 | 05 | -0.0241 | -0.0277
00:06:25 04 | 06 |  0.0250 |  0.0199
00:06:30 04 | 07 |  0.0117 | -0.0010
00:06:36 04 | 08 |  0.0176 |  0.0253
00:06:41 04 | 09 |  0.0225 |  0.0156
00:06:47 04 | 10 |  0.0246 |  0.0180
00:06:52 04 | 11 |  0.0190 |  0.0002
00:06:58 04 | 12 |  0.0385 |  0.0323
00:07:03 04 | 13 |  0.0140 |  0.0160
00:07:09 04 | 14 |  0.0322 |  0.0162
00:07:14 04 | 15 |  0.0381 |  0.0255
00:07:20 04 | 16 |  0.0317 |  0.0258
00:07:26 04 | 17 |  0.0013 | -0.0184
00:07:31 04 | 18 |  0.0202 |  0.0090
00:07:37 04 | 19 |  0.0258 |  0.0212
00:07:43 04 | 20 |  0.0165 |  0.0063
00:07:49 05 | 01 |  0.0090 |  0.0047
00:07:55 05 | 02 | -0.0116 | -0.0160
00:08:00 05 | 03 |  0.0105 |  0.0197
00:08:06 05 | 04 | -0.0248 | -0.0242
00:08:11 05 | 05 |  0.0113 |  0.0087
00:08:17 05 | 06 |  0.0355 |  0.0329
00:08:22 05 | 07 | -0.0045 |  0.0063
00:08:28 05 | 08 |  0.0075 |  0.0195
00:08:34 05 | 09 |  0.0031 |  0.0127
00:08:39 05 | 10 |  0.0240 |  0.0359
00:08:45 05 | 11 |  0.0038 |  0.0194
00:08:50 05 | 12 |  0.0020 |  0.0053
00:08:56 05 | 13 |  0.0012 |  0.0177
00:09:01 05 | 14 |  0.0055 |  0.0154
00:09:07 05 | 15 |  0.0028 | -0.0002
00:09:13 05 | 16 |  0.0054 |  0.0077
00:09:18 05 | 17 |  0.0096 |  0.0269
00:09:24 05 | 18 |  0.0001 |  0.0103
00:09:29 05 | 19 |  0.0051 |  0.0043
00:09:35 05 | 20 | -0.0009 |  0.0120
00:09:41 06 | 01 |  0.0364 |  0.0436
00:09:47 06 | 02 |  0.0043 |  0.0121
00:09:52 06 | 03 |  0.0199 | -0.0036
00:09:58 06 | 04 |  0.0429 |  0.0303
00:10:04 06 | 05 |  0.0236 |  0.0258
00:10:09 06 | 06 |  0.0211 |  0.0111
00:10:15 06 | 07 | -0.0138 | -0.0199
00:10:20 06 | 08 |  0.0236 |  0.0481
00:10:26 06 | 09 |  0.0173 |  0.0245
00:10:32 06 | 10 |  0.0293 |  0.0246
00:10:37 06 | 11 |  0.0129 | -0.0088
00:10:43 06 | 12 |  0.0281 |  0.0288
00:10:48 06 | 13 |  0.0240 |  0.0241
00:10:54 06 | 14 |  0.0108 |  0.0160
00:10:60 06 | 15 |  0.0184 |  0.0167
00:11:05 06 | 16 |  0.0368 |  0.0455
00:11:11 06 | 17 |  0.0115 |  0.0209
00:11:16 06 | 18 |  0.0291 |  0.0363
00:11:22 06 | 19 |  0.0195 |  0.0217
00:11:28 06 | 20 |  0.0365 |  0.0350
00:11:34 07 | 01 |  0.0004 |  0.0004
00:11:39 07 | 02 |  0.0220 |  0.0306
00:11:45 07 | 03 |  0.0154 |  0.0286
00:11:51 07 | 04 |  0.0098 |  0.0152
00:11:56 07 | 05 | -0.0040 | -0.0007
00:12:02 07 | 06 |  0.0065 |  0.0207
00:12:07 07 | 07 |  0.0289 |  0.0507
00:12:13 07 | 08 |  0.0174 |  0.0544
00:12:19 07 | 09 |  0.0124 |  0.0262
00:12:24 07 | 10 |  0.0250 |  0.0447
00:12:30 07 | 11 |  0.0230 |  0.0487
00:12:35 07 | 12 |  0.0357 |  0.0445
00:12:41 07 | 13 |  0.0230 |  0.0256
00:12:47 07 | 14 |  0.0246 |  0.0335
00:12:52 07 | 15 |  0.0325 |  0.0236
00:12:58 07 | 16 |  0.0168 |  0.0113
00:13:04 07 | 17 |  0.0170 |  0.0395
00:13:09 07 | 18 |  0.0248 |  0.0332
00:13:15 07 | 19 |  0.0227 |  0.0326
00:13:20 07 | 20 |  0.0125 |  0.0243
00:13:27 08 | 01 | -0.0124 | -0.0292
00:13:32 08 | 02 |  0.0047 |  0.0050
00:13:38 08 | 03 |  0.0122 |  0.0025
00:13:43 08 | 04 |  0.0288 |  0.0217
00:13:49 08 | 05 |  0.0315 |  0.0148
00:13:54 08 | 06 |  0.0195 |  0.0282
00:13:60 08 | 07 |  0.0216 |  0.0246
00:14:05 08 | 08 |  0.0245 |  0.0188
00:14:11 08 | 09 |  0.0292 |  0.0337
00:14:17 08 | 10 |  0.0202 |  0.0149
00:14:22 08 | 11 |  0.0131 |  0.0078
00:14:28 08 | 12 |  0.0107 | -0.0070
00:14:34 08 | 13 |  0.0138 |  0.0087
00:14:39 08 | 14 |  0.0131 |  0.0040
00:14:45 08 | 15 |  0.0078 |  0.0078
00:14:51 08 | 16 |  0.0156 |  0.0116
00:14:56 08 | 17 |  0.0136 |  0.0171
00:15:02 08 | 18 |  0.0229 |  0.0238
00:15:07 08 | 19 |  0.0106 |  0.0260
00:15:13 08 | 20 |  0.0140 |  0.0199
00:15:19 09 | 01 |  0.0025 | -0.0001
00:15:25 09 | 02 |  0.0125 | -0.0085
00:15:31 09 | 03 |  0.0119 |  0.0274
00:15:36 09 | 04 | -0.0000 |  0.0048
00:15:42 09 | 05 |  0.0198 |  0.0090
00:15:47 09 | 06 |  0.0159 |  0.0032
00:15:53 09 | 07 |  0.0101 | -0.0030
00:15:59 09 | 08 |  0.0229 |  0.0125
00:16:04 09 | 09 |  0.0158 |  0.0171
00:16:10 09 | 10 |  0.0228 |  0.0258
00:16:16 09 | 11 |  0.0211 |  0.0211
00:16:21 09 | 12 |  0.0178 |  0.0257
00:16:27 09 | 13 |  0.0106 | -0.0065
00:16:33 09 | 14 |  0.0124 | -0.0001
00:16:38 09 | 15 |  0.0246 |  0.0161
00:16:44 09 | 16 |  0.0242 | -0.0064
00:16:49 09 | 17 |  0.0158 |  0.0157
00:16:55 09 | 18 |  0.0191 |  0.0169
00:17:01 09 | 19 |  0.0172 | -0.0009
00:17:06 09 | 20 |  0.0095 | -0.0110
00:17:13 10 | 01 | -0.0309 | -0.0441
00:17:18 10 | 02 | -0.0062 |  0.0136
00:17:24 10 | 03 | -0.0082 |  0.0037
00:17:29 10 | 04 |  0.0360 |  0.0319
00:17:35 10 | 05 |  0.0199 |  0.0198
00:17:41 10 | 06 |  0.0305 |  0.0368
00:17:46 10 | 07 |  0.0376 |  0.0314
00:17:52 10 | 08 |  0.0235 |  0.0201
00:17:57 10 | 09 |  0.0034 |  0.0024
00:18:03 10 | 10 | -0.0162 | -0.0018
00:18:09 10 | 11 | -0.0086 | -0.0039
00:18:14 10 | 12 | -0.0084 |  0.0026
00:18:20 10 | 13 | -0.0040 |  0.0004
00:18:26 10 | 14 | -0.0158 | -0.0140
00:18:31 10 | 15 |  0.0335 |  0.0436
00:18:37 10 | 16 | -0.0070 |  0.0066
00:18:42 10 | 17 | -0.0133 | -0.0049
00:18:48 10 | 18 | -0.0098 |  0.0026
00:18:54 10 | 19 | -0.0080 | -0.0062
00:18:59 10 | 20 | -0.0103 | -0.0028
00:19:06 11 | 01 |  0.0348 |  0.0311
00:19:11 11 | 02 |  0.0331 |  0.0476
00:19:17 11 | 03 |  0.0212 |  0.0107
00:19:22 11 | 04 |  0.0002 | -0.0165
00:19:28 11 | 05 |  0.0208 |  0.0137
00:19:34 11 | 06 |  0.0170 |  0.0208
00:19:39 11 | 07 |  0.0251 |  0.0264
00:19:45 11 | 08 |  0.0131 |  0.0270
00:19:50 11 | 09 |  0.0380 |  0.0318
00:19:56 11 | 10 |  0.0038 |  0.0008
00:20:01 11 | 11 |  0.0210 |  0.0109
00:20:07 11 | 12 |  0.0117 |  0.0030
00:20:13 11 | 13 |  0.0123 |  0.0058
00:20:18 11 | 14 |  0.0050 |  0.0051
00:20:24 11 | 15 |  0.0102 |  0.0019
00:20:29 11 | 16 |  0.0172 |  0.0128
00:20:35 11 | 17 |  0.0047 |  0.0101
00:20:41 11 | 18 |  0.0307 |  0.0286
00:20:46 11 | 19 |  0.0061 |  0.0109
00:20:52 11 | 20 |  0.0177 |  0.0303
00:20:58 12 | 01 |  0.0147 |  0.0114
00:21:04 12 | 02 |  0.0089 |  0.0069
00:21:09 12 | 03 | -0.0055 | -0.0151
00:21:15 12 | 04 | -0.0077 | -0.0245
00:21:21 12 | 05 | -0.0053 |  0.0038
00:21:26 12 | 06 | -0.0158 | -0.0171
00:21:32 12 | 07 | -0.0100 | -0.0095
00:21:38 12 | 08 | -0.0192 | -0.0155
00:21:43 12 | 09 | -0.0272 | -0.0365
00:21:49 12 | 10 | -0.0222 | -0.0352
00:21:55 12 | 11 | -0.0262 | -0.0266
00:22:00 12 | 12 | -0.0312 | -0.0372
00:22:06 12 | 13 | -0.0262 | -0.0490
00:22:12 12 | 14 | -0.0253 | -0.0341
00:22:17 12 | 15 | -0.0241 | -0.0266
00:22:23 12 | 16 | -0.0295 | -0.0210
00:22:29 12 | 17 | -0.0314 | -0.0351
00:22:34 12 | 18 | -0.0261 | -0.0154
00:22:40 12 | 19 | -0.0248 | -0.0373
00:22:46 12 | 20 | -0.0242 | -0.0254
(16, 8) tanh 0.2 256
00:00:03 01 | 01 | -0.0038 | -0.0136
00:00:05 01 | 02 |  0.0066 | -0.0154
00:00:07 01 | 03 |  0.0202 |  0.0182
00:00:09 01 | 04 |  0.0166 |  0.0065
00:00:11 01 | 05 |  0.0121 |  0.0040
00:00:13 01 | 06 |  0.0215 |  0.0195
00:00:15 01 | 07 |  0.0144 |  0.0137
00:00:17 01 | 08 |  0.0221 |  0.0366
00:00:19 01 | 09 |  0.0277 |  0.0350
00:00:21 01 | 10 |  0.0181 |  0.0103
00:00:23 01 | 11 |  0.0223 |  0.0228
00:00:25 01 | 12 |  0.0196 |  0.0119</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;06851eb3-a79b-4382-bea7-cfbb9a445cf5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">00:00:27 01 | 13 |  0.0106 |  0.0143
00:00:29 01 | 14 | -0.0006 | -0.0234
00:00:31 01 | 15 |  0.0044 | -0.0009
00:00:33 01 | 16 |  0.0029 |  0.0108
00:00:35 01 | 17 |  0.0029 |  0.0011
00:00:37 01 | 18 |  0.0016 |  0.0159
00:00:39 01 | 19 |  0.0097 |  0.0076
00:00:41 01 | 20 |  0.0032 |  0.0050
00:00:44 02 | 01 |  0.0057 |  0.0121
00:00:46 02 | 02 | -0.0026 |  0.0178
00:00:48 02 | 03 |  0.0098 |  0.0019
00:00:50 02 | 04 |  0.0095 |  0.0184
00:00:52 02 | 05 |  0.0171 |  0.0277
00:00:54 02 | 06 |  0.0106 |  0.0216
00:00:56 02 | 07 |  0.0171 |  0.0277
00:00:58 02 | 08 |  0.0005 |  0.0056
00:00:60 02 | 09 |  0.0043 |  0.0154
00:01:02 02 | 10 | -0.0104 | -0.0164
00:01:04 02 | 11 | -0.0198 | -0.0228
00:01:06 02 | 12 | -0.0037 | -0.0298
00:01:08 02 | 13 | -0.0076 | -0.0115
00:01:10 02 | 14 |  0.0001 |  0.0012
00:01:12 02 | 15 | -0.0141 | -0.0062
00:01:14 02 | 16 | -0.0036 | -0.0122
00:01:16 02 | 17 | -0.0100 | -0.0172
00:01:18 02 | 18 | -0.0096 | -0.0252
00:01:20 02 | 19 | -0.0057 | -0.0004
00:01:22 02 | 20 | -0.0124 | -0.0217
00:01:24 03 | 01 | -0.0054 | -0.0066
00:01:26 03 | 02 |  0.0198 |  0.0429
00:01:28 03 | 03 |  0.0009 |  0.0040
00:01:30 03 | 04 | -0.0037 | -0.0097
00:01:32 03 | 05 | -0.0060 |  0.0005
00:01:34 03 | 06 | -0.0008 | -0.0183
00:01:36 03 | 07 |  0.0314 |  0.0464
00:01:38 03 | 08 |  0.0156 |  0.0230
00:01:40 03 | 09 |  0.0354 |  0.0477
00:01:42 03 | 10 |  0.0214 |  0.0024
00:01:44 03 | 11 | -0.0083 | -0.0136
00:01:46 03 | 12 |  0.0129 |  0.0209
00:01:48 03 | 13 | -0.0079 | -0.0156
00:01:50 03 | 14 |  0.0020 |  0.0162
00:01:52 03 | 15 | -0.0022 | -0.0073
00:01:54 03 | 16 |  0.0106 |  0.0309
00:01:56 03 | 17 | -0.0064 |  0.0016
00:01:58 03 | 18 | -0.0073 | -0.0036
00:02:00 03 | 19 | -0.0129 |  0.0057
00:02:02 03 | 20 | -0.0086 | -0.0202
00:02:05 04 | 01 |  0.0024 |  0.0131
00:02:07 04 | 02 |  0.0337 |  0.0280
00:02:09 04 | 03 | -0.0038 |  0.0018
00:02:11 04 | 04 | -0.0269 | -0.0326
00:02:14 04 | 05 |  0.0038 | -0.0314
00:02:16 04 | 06 | -0.0041 | -0.0058
00:02:18 04 | 07 | -0.0171 | -0.0358
00:02:20 04 | 08 | -0.0235 | -0.0469
00:02:22 04 | 09 | -0.0001 | -0.0230
00:02:24 04 | 10 |  0.0151 |  0.0094
00:02:26 04 | 11 | -0.0035 | -0.0068
00:02:28 04 | 12 |  0.0210 |  0.0017
00:02:30 04 | 13 | -0.0115 | -0.0307
00:02:32 04 | 14 |  0.0060 | -0.0009
00:02:34 04 | 15 |  0.0085 |  0.0067
00:02:36 04 | 16 |  0.0211 |  0.0244
00:02:38 04 | 17 |  0.0083 | -0.0116
00:02:40 04 | 18 | -0.0006 |  0.0086
00:02:42 04 | 19 |  0.0164 |  0.0058
00:02:44 04 | 20 |  0.0277 |  0.0296
00:02:47 05 | 01 | -0.0264 | -0.0251
00:02:49 05 | 02 | -0.0003 | -0.0025
00:02:51 05 | 03 | -0.0067 | -0.0376
00:02:53 05 | 04 |  0.0109 |  0.0028
00:02:55 05 | 05 |  0.0174 |  0.0210
00:02:57 05 | 06 |  0.0025 | -0.0032
00:02:59 05 | 07 | -0.0070 |  0.0057
00:03:01 05 | 08 |  0.0160 |  0.0047
00:03:03 05 | 09 |  0.0206 |  0.0109
00:03:05 05 | 10 |  0.0269 |  0.0248
00:03:07 05 | 11 |  0.0293 |  0.0377
00:03:09 05 | 12 |  0.0164 |  0.0224
00:03:11 05 | 13 |  0.0207 |  0.0260
00:03:13 05 | 14 |  0.0119 |  0.0181
00:03:15 05 | 15 |  0.0156 |  0.0286
00:03:17 05 | 16 |  0.0158 |  0.0246
00:03:19 05 | 17 |  0.0130 |  0.0266
00:03:21 05 | 18 |  0.0034 | -0.0002
00:03:23 05 | 19 | -0.0026 |  0.0071
00:03:25 05 | 20 | -0.0034 |  0.0004
00:03:28 06 | 01 |  0.0208 |  0.0125
00:03:30 06 | 02 | -0.0182 | -0.0363
00:03:32 06 | 03 |  0.0270 |  0.0207
00:03:34 06 | 04 |  0.0255 |  0.0368
00:03:36 06 | 05 |  0.0359 |  0.0223
00:03:38 06 | 06 |  0.0286 |  0.0301
00:03:40 06 | 07 |  0.0410 |  0.0436
00:03:42 06 | 08 |  0.0406 |  0.0380
00:03:44 06 | 09 |  0.0309 |  0.0089
00:03:46 06 | 10 |  0.0367 |  0.0382
00:03:48 06 | 11 |  0.0299 |  0.0190
00:03:50 06 | 12 |  0.0161 | -0.0059
00:03:52 06 | 13 |  0.0355 |  0.0072
00:03:54 06 | 14 |  0.0290 |  0.0199
00:03:56 06 | 15 |  0.0138 |  0.0060
00:03:58 06 | 16 |  0.0215 |  0.0010
00:04:00 06 | 17 |  0.0305 |  0.0080
00:04:02 06 | 18 |  0.0037 |  0.0049
00:04:04 06 | 19 |  0.0299 |  0.0346
00:04:06 06 | 20 |  0.0264 |  0.0286
00:04:09 07 | 01 |  0.0111 |  0.0033
00:04:11 07 | 02 | -0.0137 | -0.0202
00:04:13 07 | 03 |  0.0241 |  0.0286
00:04:15 07 | 04 | -0.0020 |  0.0040
00:04:17 07 | 05 |  0.0009 |  0.0010
00:04:19 07 | 06 |  0.0198 |  0.0545
00:04:21 07 | 07 | -0.0043 |  0.0114
00:04:23 07 | 08 | -0.0205 |  0.0080
00:04:25 07 | 09 |  0.0220 |  0.0207
00:04:27 07 | 10 |  0.0117 |  0.0093
00:04:29 07 | 11 |  0.0164 |  0.0126
00:04:31 07 | 12 |  0.0238 |  0.0428
00:04:33 07 | 13 |  0.0234 |  0.0581
00:04:35 07 | 14 |  0.0277 |  0.0388
00:04:37 07 | 15 |  0.0279 |  0.0372
00:04:39 07 | 16 |  0.0221 |  0.0324
00:04:41 07 | 17 |  0.0304 |  0.0448
00:04:43 07 | 18 |  0.0429 |  0.0589
00:04:45 07 | 19 |  0.0332 |  0.0276
00:04:47 07 | 20 |  0.0366 |  0.0129
00:04:50 08 | 01 |  0.0112 |  0.0127
00:04:52 08 | 02 | -0.0101 |  0.0020
00:04:54 08 | 03 | -0.0079 |  0.0115
00:04:56 08 | 04 |  0.0125 |  0.0114
00:04:58 08 | 05 |  0.0039 | -0.0004
00:05:00 08 | 06 |  0.0150 |  0.0152
00:05:02 08 | 07 |  0.0142 |  0.0246
00:05:04 08 | 08 |  0.0255 |  0.0163
00:05:06 08 | 09 |  0.0224 |  0.0020
00:05:08 08 | 10 |  0.0202 |  0.0282
00:05:10 08 | 11 |  0.0124 |  0.0093
00:05:12 08 | 12 |  0.0154 |  0.0063
00:05:14 08 | 13 |  0.0212 |  0.0148
00:05:16 08 | 14 |  0.0160 |  0.0202
00:05:19 08 | 15 |  0.0127 |  0.0129
00:05:21 08 | 16 |  0.0216 | -0.0054
00:05:23 08 | 17 |  0.0206 |  0.0099
00:05:25 08 | 18 |  0.0166 |  0.0034
00:05:27 08 | 19 |  0.0303 |  0.0099
00:05:29 08 | 20 |  0.0289 |  0.0262
00:05:31 09 | 01 |  0.0166 | -0.0002
00:05:33 09 | 02 | -0.0161 | -0.0257
00:05:35 09 | 03 | -0.0355 | -0.0169
00:05:37 09 | 04 |  0.0069 | -0.0126
00:05:39 09 | 05 |  0.0229 |  0.0140
00:05:41 09 | 06 |  0.0248 |  0.0210
00:05:43 09 | 07 |  0.0150 |  0.0048
00:05:46 09 | 08 |  0.0156 |  0.0007
00:05:48 09 | 09 |  0.0055 |  0.0139
00:05:50 09 | 10 |  0.0177 |  0.0279
00:05:52 09 | 11 |  0.0143 |  0.0044
00:05:54 09 | 12 |  0.0182 |  0.0301
00:05:56 09 | 13 |  0.0069 | -0.0088
00:05:58 09 | 14 |  0.0276 |  0.0206
00:05:60 09 | 15 |  0.0120 |  0.0133
00:06:02 09 | 16 |  0.0237 |  0.0424
00:06:04 09 | 17 |  0.0186 | -0.0037
00:06:06 09 | 18 |  0.0244 |  0.0208
00:06:08 09 | 19 |  0.0113 |  0.0029
00:06:10 09 | 20 |  0.0100 |  0.0054
00:06:13 10 | 01 |  0.0076 |  0.0083
00:06:15 10 | 02 | -0.0031 | -0.0029
00:06:17 10 | 03 | -0.0082 | -0.0072
00:06:19 10 | 04 |  0.0004 | -0.0024
00:06:21 10 | 05 | -0.0193 | -0.0121
00:06:23 10 | 06 | -0.0002 | -0.0082
00:06:25 10 | 07 | -0.0232 | -0.0053
00:06:27 10 | 08 | -0.0278 | -0.0121
00:06:29 10 | 09 | -0.0304 | -0.0270
00:06:31 10 | 10 | -0.0019 | -0.0116
00:06:33 10 | 11 | -0.0254 |  0.0106
00:06:35 10 | 12 |  0.0205 |  0.0221
00:06:37 10 | 13 | -0.0209 | -0.0110
00:06:39 10 | 14 | -0.0072 |  0.0095
00:06:41 10 | 15 | -0.0038 |  0.0060
00:06:43 10 | 16 |  0.0199 |  0.0333
00:06:45 10 | 17 |  0.0053 |  0.0150
00:06:47 10 | 18 | -0.0300 | -0.0293
00:06:49 10 | 19 | -0.0053 |  0.0019
00:06:51 10 | 20 |  0.0002 |  0.0043
00:06:53 11 | 01 |  0.0122 |  0.0059
00:06:56 11 | 02 |  0.0121 |  0.0146
00:06:58 11 | 03 |  0.0000 | -0.0078
00:06:60 11 | 04 |  0.0223 |  0.0206
00:07:02 11 | 05 |  0.0159 |  0.0220
00:07:04 11 | 06 | -0.0117 |  0.0057
00:07:06 11 | 07 |  0.0003 | -0.0026
00:07:08 11 | 08 |  0.0270 |  0.0138
00:07:10 11 | 09 |  0.0344 |  0.0427
00:07:12 11 | 10 |  0.0097 | -0.0021
00:07:14 11 | 11 |  0.0172 |  0.0030
00:07:16 11 | 12 |  0.0109 |  0.0101
00:07:18 11 | 13 |  0.0191 |  0.0196
00:07:20 11 | 14 |  0.0021 | -0.0112
00:07:22 11 | 15 | -0.0072 | -0.0235
00:07:24 11 | 16 |  0.0093 | -0.0112
00:07:26 11 | 17 |  0.0152 |  0.0143
00:07:28 11 | 18 | -0.0005 | -0.0221
00:07:30 11 | 19 |  0.0117 |  0.0087
00:07:32 11 | 20 |  0.0195 |  0.0053
00:07:35 12 | 01 |  0.0070 |  0.0189
00:07:37 12 | 02 |  0.0190 |  0.0133
00:07:39 12 | 03 |  0.0154 |  0.0060
00:07:41 12 | 04 |  0.0240 |  0.0221
00:07:43 12 | 05 |  0.0021 |  0.0061
00:07:45 12 | 06 |  0.0035 |  0.0052
00:07:47 12 | 07 |  0.0010 | -0.0213
00:07:49 12 | 08 |  0.0035 |  0.0187
00:07:51 12 | 09 | -0.0009 |  0.0013
00:07:53 12 | 10 |  0.0011 |  0.0206
00:07:55 12 | 11 |  0.0072 |  0.0054
00:07:57 12 | 12 |  0.0061 |  0.0126
00:07:59 12 | 13 | -0.0043 | -0.0075
00:08:01 12 | 14 |  0.0154 |  0.0139</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;7ebcb085-6063-44d1-a372-3d8a38934a6e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">00:08:03 12 | 15 |  0.0112 | -0.0010
00:08:05 12 | 16 |  0.0089 |  0.0110
00:08:08 12 | 17 |  0.0173 |  0.0272
00:08:10 12 | 18 |  0.0271 |  0.0185
00:08:12 12 | 19 |  0.0244 |  0.0297
00:08:14 12 | 20 |  0.0241 |  0.0211
(16, 8) tanh 0.1 64
00:00:06 01 | 01 |  0.0032 | -0.0136
00:00:12 01 | 02 | -0.0071 |  0.0016
00:00:18 01 | 03 |  0.0028 |  0.0148
00:00:23 01 | 04 | -0.0044 |  0.0060
00:00:29 01 | 05 |  0.0082 |  0.0061
00:00:34 01 | 06 |  0.0022 | -0.0044
00:00:40 01 | 07 |  0.0149 |  0.0147
00:00:46 01 | 08 |  0.0145 |  0.0143
00:00:51 01 | 09 |  0.0079 | -0.0023
00:00:57 01 | 10 |  0.0086 |  0.0041
00:01:02 01 | 11 | -0.0040 | -0.0078
00:01:08 01 | 12 |  0.0008 | -0.0204
00:01:14 01 | 13 |  0.0030 | -0.0012
00:01:19 01 | 14 |  0.0078 |  0.0085
00:01:25 01 | 15 |  0.0106 |  0.0050
00:01:30 01 | 16 |  0.0102 | -0.0025
00:01:36 01 | 17 |  0.0002 | -0.0017
00:01:42 01 | 18 | -0.0006 | -0.0040
00:01:47 01 | 19 |  0.0035 | -0.0053
00:01:53 01 | 20 |  0.0009 |  0.0196
00:01:59 02 | 01 |  0.0125 |  0.0146
00:02:05 02 | 02 |  0.0120 |  0.0095
00:02:10 02 | 03 |  0.0132 |  0.0003
00:02:16 02 | 04 | -0.0006 | -0.0015
00:02:21 02 | 05 |  0.0091 |  0.0024
00:02:27 02 | 06 |  0.0100 |  0.0194
00:02:32 02 | 07 | -0.0091 | -0.0118
00:02:38 02 | 08 | -0.0111 |  0.0009
00:02:44 02 | 09 | -0.0050 |  0.0025
00:02:49 02 | 10 |  0.0101 |  0.0073
00:02:55 02 | 11 | -0.0011 |  0.0053
00:03:00 02 | 12 |  0.0022 | -0.0049
00:03:06 02 | 13 | -0.0012 |  0.0097
00:03:11 02 | 14 | -0.0041 |  0.0044
00:03:17 02 | 15 | -0.0039 |  0.0010
00:03:22 02 | 16 |  0.0209 |  0.0289
00:03:28 02 | 17 | -0.0067 | -0.0012
00:03:34 02 | 18 | -0.0075 | -0.0118
00:03:39 02 | 19 | -0.0066 |  0.0120
00:03:45 02 | 20 |  0.0095 | -0.0054
00:03:51 03 | 01 | -0.0032 | -0.0016
00:03:57 03 | 02 | -0.0159 | -0.0205
00:04:02 03 | 03 | -0.0109 | -0.0190
00:04:08 03 | 04 |  0.0306 |  0.0398
00:04:13 03 | 05 |  0.0221 |  0.0227
00:04:19 03 | 06 |  0.0116 |  0.0265
00:04:24 03 | 07 | -0.0061 | -0.0127
00:04:30 03 | 08 | -0.0063 | -0.0054
00:04:35 03 | 09 |  0.0118 | -0.0025
00:04:41 03 | 10 | -0.0051 | -0.0205
00:04:47 03 | 11 | -0.0088 | -0.0188
00:04:53 03 | 12 | -0.0111 | -0.0212
00:04:58 03 | 13 | -0.0140 | -0.0379
00:05:04 03 | 14 | -0.0213 | -0.0248
00:05:10 03 | 15 |  0.0009 |  0.0031
00:05:15 03 | 16 |  0.0018 |  0.0113
00:05:21 03 | 17 | -0.0005 | -0.0065
00:05:26 03 | 18 | -0.0057 | -0.0147
00:05:32 03 | 19 | -0.0161 | -0.0156
00:05:38 03 | 20 | -0.0236 | -0.0268
00:05:44 04 | 01 | -0.0131 | -0.0232
00:05:50 04 | 02 |  0.0060 |  0.0012
00:05:55 04 | 03 |  0.0138 |  0.0164
00:06:01 04 | 04 |  0.0015 | -0.0174
00:06:07 04 | 05 |  0.0195 |  0.0275
00:06:12 04 | 06 | -0.0030 | -0.0106
00:06:18 04 | 07 |  0.0002 | -0.0168
00:06:23 04 | 08 |  0.0016 | -0.0124
00:06:29 04 | 09 | -0.0074 | -0.0010
00:06:35 04 | 10 | -0.0398 | -0.0490
00:06:40 04 | 11 |  0.0144 |  0.0124
00:06:46 04 | 12 |  0.0030 | -0.0088
00:06:52 04 | 13 | -0.0148 | -0.0225
00:06:57 04 | 14 |  0.0222 |  0.0097
00:07:03 04 | 15 |  0.0048 | -0.0032
00:07:08 04 | 16 |  0.0129 |  0.0350
00:07:14 04 | 17 |  0.0009 | -0.0001
00:07:20 04 | 18 |  0.0289 |  0.0382
00:07:25 04 | 19 |  0.0211 |  0.0289
00:07:31 04 | 20 |  0.0057 |  0.0142
00:07:37 05 | 01 |  0.0034 |  0.0021
00:07:43 05 | 02 | -0.0027 |  0.0058
00:07:48 05 | 03 | -0.0076 | -0.0079
00:07:54 05 | 04 | -0.0011 | -0.0130
00:07:59 05 | 05 |  0.0179 |  0.0105
00:08:05 05 | 06 |  0.0137 |  0.0154
00:08:11 05 | 07 |  0.0035 | -0.0015
00:08:16 05 | 08 |  0.0036 | -0.0065
00:08:22 05 | 09 | -0.0056 | -0.0097
00:08:27 05 | 10 |  0.0132 |  0.0264
00:08:33 05 | 11 |  0.0270 |  0.0211
00:08:38 05 | 12 |  0.0304 |  0.0325
00:08:44 05 | 13 |  0.0170 |  0.0115
00:08:50 05 | 14 |  0.0106 |  0.0182
00:08:55 05 | 15 |  0.0162 |  0.0267
00:09:01 05 | 16 |  0.0074 |  0.0144
00:09:07 05 | 17 |  0.0058 |  0.0124
00:09:12 05 | 18 |  0.0197 |  0.0172
00:09:18 05 | 19 |  0.0138 |  0.0237
00:09:23 05 | 20 |  0.0156 |  0.0164
00:09:30 06 | 01 | -0.0031 | -0.0131
00:09:35 06 | 02 |  0.0317 |  0.0514
00:09:41 06 | 03 |  0.0036 |  0.0070
00:09:46 06 | 04 |  0.0485 |  0.0413
00:09:52 06 | 05 |  0.0302 |  0.0286
00:09:57 06 | 06 |  0.0287 |  0.0168
00:10:03 06 | 07 |  0.0320 |  0.0421
00:10:09 06 | 08 |  0.0330 |  0.0236
00:10:14 06 | 09 |  0.0255 |  0.0077
00:10:20 06 | 10 |  0.0223 |  0.0054
00:10:25 06 | 11 |  0.0319 |  0.0376
00:10:31 06 | 12 |  0.0197 |  0.0028
00:10:37 06 | 13 |  0.0129 |  0.0049
00:10:42 06 | 14 |  0.0238 |  0.0020
00:10:48 06 | 15 |  0.0177 |  0.0222
00:10:53 06 | 16 |  0.0188 | -0.0014
00:10:59 06 | 17 |  0.0169 |  0.0090
00:11:04 06 | 18 |  0.0114 |  0.0161
00:11:10 06 | 19 |  0.0181 | -0.0006
00:11:16 06 | 20 |  0.0170 | -0.0024
00:11:22 07 | 01 |  0.0116 |  0.0228
00:11:27 07 | 02 |  0.0161 |  0.0321
00:11:33 07 | 03 |  0.0169 |  0.0234
00:11:39 07 | 04 | -0.0042 | -0.0057
00:11:44 07 | 05 |  0.0045 |  0.0354
00:11:50 07 | 06 |  0.0136 |  0.0129
00:11:55 07 | 07 |  0.0175 |  0.0437
00:12:01 07 | 08 |  0.0133 |  0.0330
00:12:06 07 | 09 | -0.0044 |  0.0196
00:12:12 07 | 10 |  0.0197 |  0.0447
00:12:18 07 | 11 |  0.0061 |  0.0238
00:12:24 07 | 12 |  0.0223 |  0.0404
00:12:29 07 | 13 |  0.0129 |  0.0269
00:12:35 07 | 14 |  0.0204 |  0.0120
00:12:40 07 | 15 | -0.0055 |  0.0066
00:12:46 07 | 16 |  0.0004 |  0.0068
00:12:52 07 | 17 |  0.0205 |  0.0197
00:12:57 07 | 18 | -0.0008 |  0.0086
00:13:03 07 | 19 |  0.0260 |  0.0440
00:13:09 07 | 20 |  0.0121 |  0.0216
00:13:15 08 | 01 |  0.0259 |  0.0216
00:13:21 08 | 02 |  0.0239 |  0.0210
00:13:26 08 | 03 |  0.0215 |  0.0073
00:13:32 08 | 04 |  0.0044 |  0.0117
00:13:37 08 | 05 |  0.0101 |  0.0031
00:13:43 08 | 06 |  0.0071 | -0.0037
00:13:49 08 | 07 |  0.0116 |  0.0143
00:13:54 08 | 08 |  0.0163 |  0.0025
00:13:60 08 | 09 |  0.0073 |  0.0178
00:14:05 08 | 10 |  0.0029 |  0.0190
00:14:11 08 | 11 |  0.0111 |  0.0252
00:14:17 08 | 12 |  0.0142 |  0.0114
00:14:22 08 | 13 |  0.0131 |  0.0487
00:14:28 08 | 14 |  0.0075 |  0.0291
00:14:33 08 | 15 |  0.0087 |  0.0290
00:14:39 08 | 16 |  0.0090 |  0.0214
00:14:45 08 | 17 |  0.0068 |  0.0239
00:14:50 08 | 18 |  0.0027 |  0.0216
00:14:56 08 | 19 |  0.0087 |  0.0044
00:15:02 08 | 20 |  0.0015 |  0.0093
00:15:08 09 | 01 |  0.0015 | -0.0069
00:15:13 09 | 02 |  0.0098 |  0.0125
00:15:19 09 | 03 |  0.0130 |  0.0072
00:15:25 09 | 04 |  0.0118 |  0.0148
00:15:30 09 | 05 |  0.0117 |  0.0113
00:15:36 09 | 06 |  0.0124 |  0.0015
00:15:41 09 | 07 |  0.0030 |  0.0016
00:15:47 09 | 08 | -0.0012 |  0.0031
00:15:52 09 | 09 | -0.0086 | -0.0095
00:15:58 09 | 10 | -0.0040 | -0.0043
00:16:03 09 | 11 | -0.0072 | -0.0011
00:16:09 09 | 12 | -0.0095 | -0.0090
00:16:14 09 | 13 | -0.0240 | -0.0124
00:16:20 09 | 14 | -0.0399 | -0.0582
00:16:26 09 | 15 | -0.0287 | -0.0287
00:16:31 09 | 16 | -0.0255 | -0.0305
00:16:37 09 | 17 | -0.0265 | -0.0140
00:16:42 09 | 18 | -0.0145 | -0.0237
00:16:48 09 | 19 | -0.0234 | -0.0277
00:16:53 09 | 20 | -0.0197 | -0.0325
00:16:60 10 | 01 |  0.0444 |  0.0346
00:17:05 10 | 02 | -0.0182 | -0.0076
00:17:11 10 | 03 |  0.0233 |  0.0183
00:17:16 10 | 04 |  0.0159 |  0.0028
00:17:22 10 | 05 |  0.0151 | -0.0038
00:17:28 10 | 06 |  0.0186 |  0.0110
00:17:33 10 | 07 |  0.0099 | -0.0041
00:17:39 10 | 08 | -0.0076 |  0.0010
00:17:44 10 | 09 | -0.0007 |  0.0049
00:17:50 10 | 10 | -0.0048 |  0.0089
00:17:55 10 | 11 |  0.0015 | -0.0039
00:18:01 10 | 12 | -0.0179 | -0.0095
00:18:07 10 | 13 | -0.0273 | -0.0293
00:18:13 10 | 14 | -0.0090 | -0.0212
00:18:18 10 | 15 |  0.0089 |  0.0233
00:18:24 10 | 16 | -0.0274 | -0.0335
00:18:29 10 | 17 | -0.0203 | -0.0145
00:18:35 10 | 18 | -0.0214 | -0.0230
00:18:40 10 | 19 | -0.0278 | -0.0163
00:18:46 10 | 20 | -0.0208 | -0.0082
00:18:52 11 | 01 | -0.0060 |  0.0053
00:18:58 11 | 02 |  0.0365 |  0.0362
00:19:04 11 | 03 |  0.0362 |  0.0313
00:19:09 11 | 04 |  0.0324 |  0.0277
00:19:15 11 | 05 |  0.0156 |  0.0253
00:19:21 11 | 06 |  0.0070 |  0.0117
00:19:27 11 | 07 |  0.0189 |  0.0225
00:19:32 11 | 08 |  0.0293 |  0.0342
00:19:38 11 | 09 |  0.0066 |  0.0115
00:19:44 11 | 10 |  0.0228 |  0.0146
00:19:50 11 | 11 | -0.0092 | -0.0090
00:19:55 11 | 12 |  0.0060 | -0.0126
00:20:01 11 | 13 |  0.0023 |  0.0045
00:20:07 11 | 14 |  0.0335 |  0.0225
00:20:12 11 | 15 | -0.0005 | -0.0051</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;a01c5794-c9bb-4815-974e-a6b0780e986b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">00:20:18 11 | 16 |  0.0209 |  0.0096
00:20:24 11 | 17 |  0.0341 |  0.0222
00:20:29 11 | 18 |  0.0125 |  0.0110
00:20:35 11 | 19 |  0.0170 |  0.0230
00:20:41 11 | 20 |  0.0087 |  0.0100
00:20:47 12 | 01 |  0.0195 |  0.0341
00:20:52 12 | 02 |  0.0139 |  0.0212
00:20:58 12 | 03 |  0.0158 |  0.0139
00:21:04 12 | 04 |  0.0133 |  0.0211
00:21:09 12 | 05 |  0.0123 |  0.0038
00:21:15 12 | 06 |  0.0045 | -0.0110
00:21:20 12 | 07 | -0.0040 | -0.0165
00:21:26 12 | 08 |  0.0199 |  0.0031
00:21:32 12 | 09 |  0.0025 | -0.0069
00:21:37 12 | 10 | -0.0133 | -0.0084
00:21:43 12 | 11 | -0.0114 | -0.0160
00:21:49 12 | 12 | -0.0039 |  0.0104
00:21:54 12 | 13 |  0.0052 |  0.0231
00:21:60 12 | 14 |  0.0066 |  0.0189
00:22:06 12 | 15 |  0.0109 |  0.0083
00:22:11 12 | 16 |  0.0003 |  0.0095
00:22:17 12 | 17 |  0.0023 |  0.0239
00:22:22 12 | 18 |  0.0011 | -0.0082
00:22:28 12 | 19 |  0.0034 | -0.0082
00:22:34 12 | 20 | -0.0001 | -0.0043
(16, 8) tanh 0.1 256
00:00:03 01 | 01 |  0.0031 |  0.0024
00:00:05 01 | 02 | -0.0037 | -0.0037
00:00:07 01 | 03 |  0.0123 |  0.0006
00:00:09 01 | 04 |  0.0205 |  0.0166
00:00:11 01 | 05 |  0.0138 |  0.0292
00:00:13 01 | 06 |  0.0007 |  0.0009
00:00:15 01 | 07 | -0.0039 |  0.0056
00:00:17 01 | 08 |  0.0016 |  0.0006
00:00:19 01 | 09 |  0.0074 |  0.0129
00:00:21 01 | 10 |  0.0095 |  0.0346
00:00:23 01 | 11 |  0.0112 |  0.0137
00:00:25 01 | 12 |  0.0036 | -0.0036
00:00:27 01 | 13 |  0.0025 |  0.0076
00:00:29 01 | 14 |  0.0222 |  0.0017
00:00:31 01 | 15 |  0.0121 |  0.0058
00:00:33 01 | 16 |  0.0089 |  0.0299
00:00:35 01 | 17 |  0.0127 |  0.0156
00:00:37 01 | 18 |  0.0021 | -0.0037
00:00:39 01 | 19 | -0.0004 | -0.0005
00:00:41 01 | 20 |  0.0112 |  0.0244
00:00:44 02 | 01 | -0.0119 | -0.0102
00:00:46 02 | 02 | -0.0105 | -0.0019
00:00:48 02 | 03 | -0.0189 | -0.0144
00:00:50 02 | 04 |  0.0159 |  0.0008
00:00:52 02 | 05 |  0.0105 |  0.0153
00:00:54 02 | 06 |  0.0046 |  0.0096
00:00:56 02 | 07 |  0.0086 |  0.0129
00:00:58 02 | 08 | -0.0025 |  0.0115
00:00:60 02 | 09 | -0.0127 | -0.0028
00:01:02 02 | 10 |  0.0081 |  0.0182
00:01:04 02 | 11 | -0.0007 | -0.0086
00:01:06 02 | 12 |  0.0072 |  0.0155
00:01:08 02 | 13 |  0.0168 |  0.0253
00:01:10 02 | 14 |  0.0187 |  0.0261
00:01:12 02 | 15 |  0.0103 |  0.0195
00:01:14 02 | 16 |  0.0247 |  0.0397
00:01:16 02 | 17 |  0.0191 |  0.0338
00:01:18 02 | 18 |  0.0148 |  0.0196
00:01:20 02 | 19 |  0.0036 |  0.0073
00:01:22 02 | 20 | -0.0016 |  0.0073
00:01:25 03 | 01 | -0.0120 | -0.0099
00:01:27 03 | 02 | -0.0087 | -0.0198
00:01:29 03 | 03 | -0.0041 | -0.0151
00:01:31 03 | 04 |  0.0166 |  0.0201
00:01:33 03 | 05 |  0.0100 |  0.0130
00:01:35 03 | 06 |  0.0290 |  0.0297
00:01:37 03 | 07 |  0.0081 |  0.0098
00:01:39 03 | 08 | -0.0084 |  0.0011
00:01:41 03 | 09 | -0.0068 |  0.0129
00:01:43 03 | 10 | -0.0249 | -0.0426
00:01:45 03 | 11 | -0.0122 | -0.0468
00:01:47 03 | 12 | -0.0063 | -0.0057
00:01:49 03 | 13 | -0.0138 | -0.0242
00:01:51 03 | 14 | -0.0066 | -0.0134
00:01:53 03 | 15 | -0.0062 | -0.0070
00:01:55 03 | 16 | -0.0132 | -0.0313
00:01:57 03 | 17 | -0.0053 | -0.0080
00:01:59 03 | 18 | -0.0023 | -0.0277
00:02:01 03 | 19 | -0.0174 | -0.0317
00:02:03 03 | 20 | -0.0159 | -0.0414
00:02:06 04 | 01 |  0.0191 |  0.0192
00:02:08 04 | 02 |  0.0079 |  0.0052
00:02:10 04 | 03 | -0.0146 | -0.0080
00:02:12 04 | 04 |  0.0256 |  0.0106
00:02:14 04 | 05 |  0.0260 |  0.0146
00:02:16 04 | 06 | -0.0054 | -0.0139
00:02:18 04 | 07 |  0.0385 |  0.0247
00:02:20 04 | 08 | -0.0141 | -0.0254
00:02:22 04 | 09 |  0.0192 |  0.0020
00:02:24 04 | 10 | -0.0022 |  0.0000
00:02:26 04 | 11 |  0.0058 |  0.0040
00:02:28 04 | 12 |  0.0145 |  0.0077
00:02:30 04 | 13 |  0.0128 |  0.0116
00:02:32 04 | 14 |  0.0034 | -0.0283
00:02:34 04 | 15 |  0.0158 | -0.0029
00:02:36 04 | 16 |  0.0022 |  0.0013
00:02:38 04 | 17 |  0.0018 |  0.0031
00:02:40 04 | 18 |  0.0007 | -0.0004
00:02:42 04 | 19 | -0.0058 | -0.0183
00:02:45 04 | 20 | -0.0287 | -0.0443
00:02:47 05 | 01 |  0.0206 |  0.0236
00:02:49 05 | 02 |  0.0006 | -0.0052
00:02:51 05 | 03 |  0.0082 |  0.0062
00:02:53 05 | 04 |  0.0009 | -0.0119
00:02:55 05 | 05 |  0.0032 |  0.0044
00:02:57 05 | 06 |  0.0198 |  0.0082
00:02:59 05 | 07 |  0.0079 |  0.0022
00:03:01 05 | 08 |  0.0243 |  0.0042
00:03:03 05 | 09 |  0.0141 | -0.0016
00:03:05 05 | 10 |  0.0189 |  0.0182
00:03:07 05 | 11 | -0.0014 | -0.0147
00:03:09 05 | 12 | -0.0008 |  0.0071
00:03:11 05 | 13 | -0.0090 | -0.0005
00:03:13 05 | 14 |  0.0034 |  0.0009
00:03:16 05 | 15 |  0.0024 | -0.0114
00:03:18 05 | 16 |  0.0123 |  0.0273
00:03:20 05 | 17 |  0.0150 |  0.0251
00:03:22 05 | 18 |  0.0129 |  0.0137
00:03:24 05 | 19 |  0.0103 |  0.0112
00:03:26 05 | 20 |  0.0077 |  0.0151
00:03:28 06 | 01 | -0.0106 | -0.0143
00:03:30 06 | 02 |  0.0053 |  0.0152
00:03:32 06 | 03 |  0.0191 |  0.0091
00:03:34 06 | 04 |  0.0294 |  0.0292
00:03:36 06 | 05 |  0.0299 |  0.0315
00:03:38 06 | 06 |  0.0402 |  0.0535
00:03:40 06 | 07 |  0.0426 |  0.0541
00:03:42 06 | 08 |  0.0438 |  0.0575
00:03:44 06 | 09 |  0.0338 |  0.0382
00:03:46 06 | 10 |  0.0326 |  0.0363
00:03:48 06 | 11 |  0.0257 |  0.0227
00:03:51 06 | 12 |  0.0330 |  0.0444
00:03:53 06 | 13 |  0.0252 |  0.0217
00:03:55 06 | 14 |  0.0220 |  0.0298
00:03:57 06 | 15 |  0.0254 |  0.0249
00:03:59 06 | 16 |  0.0266 |  0.0247
00:04:01 06 | 17 |  0.0278 |  0.0242
00:04:03 06 | 18 |  0.0274 |  0.0206
00:04:05 06 | 19 |  0.0204 |  0.0158
00:04:07 06 | 20 |  0.0247 |  0.0250
00:04:09 07 | 01 |  0.0108 |  0.0223
00:04:11 07 | 02 |  0.0013 | -0.0024
00:04:13 07 | 03 |  0.0251 |  0.0363
00:04:15 07 | 04 |  0.0367 |  0.0528
00:04:18 07 | 05 |  0.0039 |  0.0284
00:04:20 07 | 06 |  0.0286 |  0.0278
00:04:22 07 | 07 |  0.0295 |  0.0227
00:04:24 07 | 08 |  0.0306 |  0.0313
00:04:26 07 | 09 |  0.0172 |  0.0388
00:04:28 07 | 10 |  0.0150 |  0.0274
00:04:30 07 | 11 |  0.0069 |  0.0462
00:04:32 07 | 12 |  0.0094 |  0.0353
00:04:34 07 | 13 |  0.0359 |  0.0672
00:04:36 07 | 14 |  0.0173 |  0.0473
00:04:38 07 | 15 |  0.0210 |  0.0402
00:04:40 07 | 16 |  0.0354 |  0.0645
00:04:42 07 | 17 |  0.0277 |  0.0294
00:04:44 07 | 18 |  0.0197 |  0.0419
00:04:46 07 | 19 |  0.0194 |  0.0284
00:04:48 07 | 20 |  0.0146 |  0.0301
00:04:51 08 | 01 |  0.0062 |  0.0118
00:04:53 08 | 02 | -0.0049 |  0.0105
00:04:55 08 | 03 |  0.0152 |  0.0140
00:04:57 08 | 04 | -0.0082 |  0.0250
00:04:59 08 | 05 | -0.0111 | -0.0086
00:05:01 08 | 06 |  0.0034 |  0.0069
00:05:03 08 | 07 |  0.0037 |  0.0096
00:05:05 08 | 08 |  0.0059 | -0.0020
00:05:07 08 | 09 | -0.0020 | -0.0055
00:05:09 08 | 10 |  0.0165 |  0.0102
00:05:12 08 | 11 |  0.0304 |  0.0296
00:05:14 08 | 12 |  0.0285 |  0.0305
00:05:16 08 | 13 |  0.0125 | -0.0071
00:05:18 08 | 14 |  0.0208 | -0.0003
00:05:20 08 | 15 |  0.0093 | -0.0093
00:05:22 08 | 16 |  0.0269 |  0.0314
00:05:24 08 | 17 |  0.0161 |  0.0137
00:05:26 08 | 18 |  0.0169 |  0.0345
00:05:28 08 | 19 |  0.0147 |  0.0201
00:05:30 08 | 20 |  0.0243 |  0.0465
00:05:33 09 | 01 |  0.0012 | -0.0093
00:05:35 09 | 02 | -0.0106 | -0.0259
00:05:37 09 | 03 |  0.0196 |  0.0321
00:05:39 09 | 04 |  0.0321 |  0.0375
00:05:41 09 | 05 |  0.0082 |  0.0130
00:05:43 09 | 06 |  0.0046 |  0.0088
00:05:45 09 | 07 |  0.0023 |  0.0209
00:05:47 09 | 08 | -0.0057 |  0.0074
00:05:49 09 | 09 |  0.0109 |  0.0101
00:05:51 09 | 10 | -0.0197 | -0.0072
00:05:53 09 | 11 | -0.0017 | -0.0053
00:05:55 09 | 12 | -0.0054 | -0.0191
00:05:57 09 | 13 |  0.0060 |  0.0081
00:05:59 09 | 14 | -0.0005 | -0.0086
00:06:01 09 | 15 |  0.0061 |  0.0034
00:06:03 09 | 16 |  0.0017 |  0.0038
00:06:05 09 | 17 | -0.0083 | -0.0266
00:06:07 09 | 18 | -0.0077 | -0.0237
00:06:09 09 | 19 | -0.0134 | -0.0201
00:06:11 09 | 20 | -0.0028 | -0.0194
00:06:14 10 | 01 | -0.0205 | -0.0241
00:06:16 10 | 02 | -0.0373 | -0.0765
00:06:18 10 | 03 | -0.0329 | -0.0363
00:06:20 10 | 04 | -0.0038 | -0.0294
00:06:22 10 | 05 | -0.0315 | -0.0258
00:06:24 10 | 06 | -0.0270 | -0.0355
00:06:26 10 | 07 | -0.0214 | -0.0278
00:06:28 10 | 08 | -0.0049 | -0.0156
00:06:30 10 | 09 | -0.0094 | -0.0068
00:06:32 10 | 10 |  0.0067 |  0.0114
00:06:34 10 | 11 | -0.0192 | -0.0080
00:06:36 10 | 12 | -0.0114 | -0.0001
00:06:38 10 | 13 |  0.0047 | -0.0188
00:06:40 10 | 14 |  0.0247 |  0.0190
00:06:42 10 | 15 |  0.0206 |  0.0171
00:06:44 10 | 16 |  0.0015 | -0.0107</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;3314a0ca-6b26-4a58-a8a2-09d3793ca764&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">00:06:46 10 | 17 | -0.0130 | -0.0095
00:06:48 10 | 18 |  0.0294 |  0.0186
00:06:50 10 | 19 |  0.0139 |  0.0069
00:06:52 10 | 20 |  0.0133 |  0.0238
00:06:55 11 | 01 |  0.0294 |  0.0317
00:06:57 11 | 02 |  0.0249 |  0.0241
00:06:59 11 | 03 |  0.0405 |  0.0454
00:07:02 11 | 04 |  0.0260 |  0.0030
00:07:04 11 | 05 |  0.0011 |  0.0093
00:07:06 11 | 06 | -0.0019 | -0.0010
00:07:08 11 | 07 |  0.0275 |  0.0164
00:07:10 11 | 08 | -0.0011 | -0.0087
00:07:12 11 | 09 |  0.0133 |  0.0028
00:07:14 11 | 10 |  0.0190 |  0.0106
00:07:16 11 | 11 |  0.0223 |  0.0272
00:07:18 11 | 12 |  0.0320 |  0.0258
00:07:20 11 | 13 |  0.0265 |  0.0212
00:07:22 11 | 14 |  0.0120 |  0.0056
00:07:24 11 | 15 |  0.0312 |  0.0266
00:07:26 11 | 16 |  0.0260 |  0.0326
00:07:28 11 | 17 |  0.0006 |  0.0029
00:07:30 11 | 18 |  0.0217 |  0.0247
00:07:32 11 | 19 |  0.0173 |  0.0183
00:07:34 11 | 20 |  0.0113 |  0.0123
00:07:37 12 | 01 | -0.0056 |  0.0018
00:07:39 12 | 02 |  0.0091 |  0.0210
00:07:41 12 | 03 |  0.0227 |  0.0087
00:07:43 12 | 04 |  0.0125 |  0.0122
00:07:45 12 | 05 |  0.0283 |  0.0336
00:07:47 12 | 06 |  0.0063 | -0.0029
00:07:49 12 | 07 |  0.0309 |  0.0422
00:07:51 12 | 08 |  0.0286 |  0.0299
00:07:53 12 | 09 |  0.0111 |  0.0240
00:07:55 12 | 10 |  0.0043 |  0.0225
00:07:57 12 | 11 |  0.0039 |  0.0019
00:07:59 12 | 12 |  0.0099 |  0.0460
00:08:01 12 | 13 |  0.0112 |  0.0326
00:08:03 12 | 14 |  0.0171 |  0.0326
00:08:05 12 | 15 |  0.0144 |  0.0133
00:08:07 12 | 16 |  0.0117 |  0.0269
00:08:09 12 | 17 |  0.0113 | -0.0025
00:08:12 12 | 18 |  0.0134 |  0.0119
00:08:14 12 | 19 |  0.0094 |  0.0130
00:08:16 12 | 20 |  0.0132 |  0.0146
(64, 32) tanh 0.2 64
00:00:07 01 | 01 |  0.0029 |  0.0107
00:00:13 01 | 02 |  0.0136 |  0.0103
00:00:19 01 | 03 |  0.0067 |  0.0056
00:00:25 01 | 04 |  0.0168 |  0.0097
00:00:31 01 | 05 |  0.0005 | -0.0013
00:00:38 01 | 06 |  0.0129 |  0.0159
00:00:44 01 | 07 |  0.0033 |  0.0098
00:00:50 01 | 08 |  0.0223 |  0.0235
00:00:56 01 | 09 |  0.0125 |  0.0293
00:01:02 01 | 10 |  0.0314 |  0.0452
00:01:08 01 | 11 |  0.0071 |  0.0082
00:01:14 01 | 12 |  0.0070 | -0.0052
00:01:20 01 | 13 |  0.0350 |  0.0402
00:01:27 01 | 14 |  0.0050 |  0.0118
00:01:33 01 | 15 |  0.0122 |  0.0183
00:01:39 01 | 16 |  0.0073 |  0.0049
00:01:45 01 | 17 |  0.0093 |  0.0046
00:01:51 01 | 18 |  0.0104 |  0.0190
00:01:57 01 | 19 |  0.0175 |  0.0255
00:02:04 01 | 20 |  0.0022 | -0.0064
00:02:10 02 | 01 |  0.0260 |  0.0258
00:02:17 02 | 02 |  0.0147 |  0.0103
00:02:23 02 | 03 |  0.0003 | -0.0104
00:02:29 02 | 04 |  0.0163 |  0.0314
00:02:35 02 | 05 |  0.0051 |  0.0122
00:02:41 02 | 06 |  0.0031 | -0.0107
00:02:47 02 | 07 |  0.0138 |  0.0022
00:02:53 02 | 08 |  0.0307 |  0.0542
00:02:59 02 | 09 |  0.0039 |  0.0044
00:03:05 02 | 10 |  0.0197 | -0.0027
00:03:11 02 | 11 |  0.0028 | -0.0036
00:03:18 02 | 12 |  0.0124 |  0.0138
00:03:24 02 | 13 | -0.0080 | -0.0058
00:03:30 02 | 14 | -0.0041 | -0.0105
00:03:36 02 | 15 | -0.0210 | -0.0220
00:03:42 02 | 16 |  0.0002 | -0.0108
00:03:48 02 | 17 | -0.0028 | -0.0072
00:03:55 02 | 18 | -0.0099 |  0.0037
00:04:01 02 | 19 | -0.0068 | -0.0036
00:04:07 02 | 20 |  0.0039 |  0.0039
00:04:14 03 | 01 |  0.0070 | -0.0092
00:04:20 03 | 02 | -0.0141 | -0.0131
00:04:27 03 | 03 |  0.0134 |  0.0029
00:04:33 03 | 04 | -0.0045 | -0.0313
00:04:39 03 | 05 | -0.0039 | -0.0095
00:04:45 03 | 06 | -0.0278 | -0.0373
00:04:52 03 | 07 |  0.0095 |  0.0148
00:04:58 03 | 08 |  0.0255 |  0.0342
00:05:04 03 | 09 |  0.0147 |  0.0118
00:05:11 03 | 10 | -0.0078 | -0.0222
00:05:17 03 | 11 |  0.0171 |  0.0234
00:05:23 03 | 12 |  0.0071 | -0.0006
00:05:29 03 | 13 |  0.0205 |  0.0139
00:05:35 03 | 14 | -0.0134 | -0.0245
00:05:42 03 | 15 | -0.0248 | -0.0278
00:05:48 03 | 16 |  0.0178 |  0.0151
00:05:54 03 | 17 | -0.0136 | -0.0342
00:06:00 03 | 18 |  0.0018 | -0.0042
00:06:07 03 | 19 | -0.0011 | -0.0046
00:06:13 03 | 20 |  0.0083 |  0.0195
00:06:20 04 | 01 | -0.0580 | -0.0572
00:06:26 04 | 02 |  0.0218 |  0.0151
00:06:32 04 | 03 | -0.0280 | -0.0138
00:06:39 04 | 04 |  0.0141 |  0.0030
00:06:45 04 | 05 | -0.0139 |  0.0000
00:06:51 04 | 06 | -0.0069 | -0.0163
00:06:57 04 | 07 |  0.0079 |  0.0025
00:07:04 04 | 08 |  0.0174 | -0.0221
00:07:10 04 | 09 |  0.0343 |  0.0217
00:07:16 04 | 10 |  0.0171 |  0.0087
00:07:22 04 | 11 |  0.0368 |  0.0218
00:07:29 04 | 12 |  0.0321 |  0.0238
00:07:35 04 | 13 |  0.0197 |  0.0234
00:07:41 04 | 14 | -0.0017 |  0.0099
00:07:47 04 | 15 |  0.0333 |  0.0440
00:07:53 04 | 16 |  0.0051 |  0.0008
00:07:60 04 | 17 |  0.0265 |  0.0329
00:08:06 04 | 18 |  0.0389 |  0.0234
00:08:12 04 | 19 |  0.0083 | -0.0059
00:08:18 04 | 20 |  0.0302 |  0.0238
00:08:25 05 | 01 |  0.0327 |  0.0259
00:08:31 05 | 02 |  0.0121 | -0.0020
00:08:37 05 | 03 |  0.0104 |  0.0108
00:08:43 05 | 04 |  0.0220 |  0.0357
00:08:50 05 | 05 |  0.0134 | -0.0035
00:08:56 05 | 06 |  0.0133 |  0.0167
00:09:02 05 | 07 |  0.0137 | -0.0036
00:09:08 05 | 08 | -0.0162 | -0.0069
00:09:14 05 | 09 |  0.0104 |  0.0112
00:09:20 05 | 10 | -0.0096 | -0.0058
00:09:27 05 | 11 | -0.0215 | -0.0358
00:09:33 05 | 12 | -0.0053 | -0.0155
00:09:39 05 | 13 | -0.0085 |  0.0083
00:09:45 05 | 14 |  0.0045 |  0.0155
00:09:51 05 | 15 |  0.0082 |  0.0098
00:09:57 05 | 16 |  0.0100 |  0.0019
00:10:04 05 | 17 |  0.0010 | -0.0105
00:10:10 05 | 18 | -0.0073 |  0.0076
00:10:16 05 | 19 |  0.0263 |  0.0205
00:10:22 05 | 20 |  0.0065 |  0.0237
00:10:29 06 | 01 | -0.0048 | -0.0151
00:10:35 06 | 02 |  0.0151 |  0.0145
00:10:41 06 | 03 | -0.0120 | -0.0250
00:10:47 06 | 04 |  0.0156 |  0.0127
00:10:54 06 | 05 |  0.0098 | -0.0048
00:10:60 06 | 06 |  0.0062 | -0.0053
00:11:06 06 | 07 |  0.0229 |  0.0037
00:11:12 06 | 08 |  0.0324 |  0.0283
00:11:18 06 | 09 |  0.0164 |  0.0104
00:11:24 06 | 10 |  0.0168 | -0.0200
00:11:31 06 | 11 |  0.0394 |  0.0328
00:11:37 06 | 12 |  0.0107 | -0.0021
00:11:43 06 | 13 |  0.0226 |  0.0234
00:11:49 06 | 14 |  0.0301 |  0.0045
00:11:55 06 | 15 |  0.0253 |  0.0102
00:12:01 06 | 16 |  0.0328 |  0.0164
00:12:08 06 | 17 |  0.0118 |  0.0123
00:12:14 06 | 18 |  0.0221 |  0.0090
00:12:20 06 | 19 |  0.0031 | -0.0036
00:12:26 06 | 20 |  0.0241 |  0.0054
00:12:33 07 | 01 |  0.0401 |  0.0650
00:12:39 07 | 02 | -0.0518 | -0.0586
00:12:45 07 | 03 | -0.0066 |  0.0041
00:12:51 07 | 04 |  0.0141 |  0.0384
00:12:58 07 | 05 |  0.0276 |  0.0436
00:13:04 07 | 06 |  0.0262 |  0.0453
00:13:10 07 | 07 |  0.0110 |  0.0348
00:13:16 07 | 08 |  0.0253 |  0.0333
00:13:22 07 | 09 |  0.0372 |  0.0242
00:13:29 07 | 10 | -0.0081 |  0.0028
00:13:35 07 | 11 |  0.0194 | -0.0070
00:13:41 07 | 12 |  0.0157 |  0.0090
00:13:47 07 | 13 |  0.0296 |  0.0337
00:13:54 07 | 14 | -0.0225 | -0.0108
00:13:60 07 | 15 |  0.0342 |  0.0351
00:14:06 07 | 16 |  0.0272 |  0.0181
00:14:12 07 | 17 |  0.0176 |  0.0165
00:14:18 07 | 18 |  0.0028 |  0.0252
00:14:25 07 | 19 |  0.0331 |  0.0508
00:14:31 07 | 20 |  0.0299 |  0.0332
00:14:38 08 | 01 |  0.0157 |  0.0119
00:14:44 08 | 02 |  0.0177 |  0.0111
00:14:50 08 | 03 | -0.0022 | -0.0134
00:14:56 08 | 04 | -0.0011 |  0.0191
00:15:03 08 | 05 |  0.0374 |  0.0343
00:15:09 08 | 06 |  0.0149 |  0.0402
00:15:15 08 | 07 |  0.0180 |  0.0183
00:15:21 08 | 08 | -0.0070 |  0.0087
00:15:27 08 | 09 |  0.0190 |  0.0202
00:15:34 08 | 10 |  0.0130 |  0.0200
00:15:40 08 | 11 |  0.0043 | -0.0160
00:15:46 08 | 12 |  0.0051 |  0.0051
00:15:52 08 | 13 | -0.0046 |  0.0098
00:15:58 08 | 14 |  0.0066 |  0.0055
00:16:05 08 | 15 |  0.0145 |  0.0020
00:16:11 08 | 16 |  0.0153 |  0.0138
00:16:17 08 | 17 |  0.0120 |  0.0001
00:16:23 08 | 18 | -0.0154 | -0.0259
00:16:29 08 | 19 |  0.0270 |  0.0148
00:16:36 08 | 20 | -0.0088 | -0.0180
00:16:43 09 | 01 |  0.0221 |  0.0209
00:16:49 09 | 02 |  0.0103 |  0.0066
00:16:55 09 | 03 |  0.0004 | -0.0083
00:17:01 09 | 04 |  0.0129 |  0.0199
00:17:07 09 | 05 | -0.0149 | -0.0151
00:17:13 09 | 06 |  0.0148 |  0.0145
00:17:19 09 | 07 |  0.0152 |  0.0203
00:17:26 09 | 08 |  0.0082 |  0.0043
00:17:32 09 | 09 | -0.0062 | -0.0072
00:17:38 09 | 10 |  0.0187 |  0.0115
00:17:44 09 | 11 | -0.0006 |  0.0025
00:17:50 09 | 12 |  0.0143 |  0.0092
00:17:57 09 | 13 | -0.0013 | -0.0047
00:18:03 09 | 14 |  0.0006 |  0.0069
00:18:09 09 | 15 |  0.0155 |  0.0132
00:18:15 09 | 16 |  0.0158 |  0.0181
00:18:21 09 | 17 |  0.0029 | -0.0016</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;3ebc520a-a08a-4c77-96c7-aee760e2d780&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">00:18:28 09 | 18 | -0.0007 | -0.0240
00:18:34 09 | 19 | -0.0046 | -0.0158
00:18:40 09 | 20 |  0.0084 |  0.0004
00:18:47 10 | 01 | -0.0120 | -0.0170
00:18:53 10 | 02 | -0.0247 | -0.0025
00:18:59 10 | 03 |  0.0100 |  0.0121
00:19:05 10 | 04 |  0.0060 |  0.0052
00:19:12 10 | 05 | -0.0367 | -0.0358
00:19:18 10 | 06 |  0.0242 |  0.0232
00:19:24 10 | 07 | -0.0143 | -0.0311
00:19:30 10 | 08 |  0.0051 | -0.0076
00:19:37 10 | 09 | -0.0184 | -0.0152
00:19:43 10 | 10 | -0.0135 | -0.0157
00:19:49 10 | 11 |  0.0080 |  0.0147
00:19:55 10 | 12 |  0.0067 | -0.0078
00:20:02 10 | 13 |  0.0305 |  0.0483
00:20:08 10 | 14 | -0.0404 | -0.0383
00:20:14 10 | 15 | -0.0136 | -0.0115
00:20:20 10 | 16 | -0.0131 | -0.0179
00:20:27 10 | 17 | -0.0067 | -0.0068
00:20:33 10 | 18 | -0.0120 | -0.0003
00:20:39 10 | 19 | -0.0408 | -0.0218
00:20:45 10 | 20 | -0.0381 | -0.0366
00:20:52 11 | 01 |  0.0232 |  0.0148
00:20:58 11 | 02 | -0.0141 | -0.0072
00:21:04 11 | 03 |  0.0187 |  0.0201
00:21:10 11 | 04 | -0.0096 | -0.0218
00:21:16 11 | 05 |  0.0093 |  0.0028
00:21:22 11 | 06 |  0.0252 |  0.0275
00:21:29 11 | 07 | -0.0022 | -0.0131
00:21:35 11 | 08 |  0.0158 |  0.0189
00:21:41 11 | 09 | -0.0110 | -0.0156
00:21:47 11 | 10 | -0.0078 | -0.0106
00:21:54 11 | 11 | -0.0143 | -0.0219
00:21:60 11 | 12 | -0.0082 | -0.0062
00:22:06 11 | 13 | -0.0111 | -0.0119
00:22:12 11 | 14 | -0.0024 | -0.0050
00:22:19 11 | 15 |  0.0019 | -0.0036
00:22:25 11 | 16 | -0.0003 |  0.0039
00:22:31 11 | 17 | -0.0095 | -0.0125
00:22:37 11 | 18 | -0.0219 | -0.0264
00:22:43 11 | 19 |  0.0048 |  0.0008
00:22:50 11 | 20 | -0.0093 | -0.0033
00:22:57 12 | 01 |  0.0132 |  0.0336
00:23:03 12 | 02 |  0.0068 |  0.0283
00:23:09 12 | 03 | -0.0061 | -0.0102
00:23:15 12 | 04 |  0.0100 | -0.0004
00:23:22 12 | 05 |  0.0136 |  0.0351
00:23:28 12 | 06 |  0.0175 |  0.0206
00:23:34 12 | 07 |  0.0169 |  0.0006
00:23:40 12 | 08 |  0.0046 | -0.0078
00:23:47 12 | 09 | -0.0009 | -0.0282
00:23:53 12 | 10 |  0.0110 |  0.0046
00:23:59 12 | 11 |  0.0094 |  0.0199
00:24:06 12 | 12 |  0.0016 |  0.0047
00:24:12 12 | 13 |  0.0057 |  0.0050
00:24:18 12 | 14 |  0.0153 |  0.0147
00:24:24 12 | 15 |  0.0135 |  0.0051
00:24:31 12 | 16 |  0.0103 |  0.0057
00:24:37 12 | 17 |  0.0062 |  0.0198
00:24:43 12 | 18 |  0.0112 |  0.0268
00:24:49 12 | 19 |  0.0072 |  0.0094
00:24:56 12 | 20 |  0.0101 |  0.0158
(64, 32) tanh 0.2 256
00:00:03 01 | 01 | -0.0109 | -0.0093
00:00:06 01 | 02 | -0.0113 | -0.0081
00:00:09 01 | 03 |  0.0190 |  0.0211
00:00:11 01 | 04 |  0.0296 |  0.0248
00:00:14 01 | 05 |  0.0119 |  0.0190
00:00:16 01 | 06 |  0.0138 |  0.0203
00:00:19 01 | 07 | -0.0001 | -0.0211
00:00:22 01 | 08 |  0.0158 |  0.0223
00:00:24 01 | 09 |  0.0024 | -0.0011
00:00:27 01 | 10 |  0.0176 |  0.0086
00:00:29 01 | 11 |  0.0148 |  0.0231
00:00:32 01 | 12 |  0.0225 |  0.0198
00:00:35 01 | 13 |  0.0194 |  0.0195
00:00:37 01 | 14 |  0.0209 |  0.0197
00:00:40 01 | 15 |  0.0210 |  0.0199
00:00:43 01 | 16 |  0.0167 |  0.0190
00:00:45 01 | 17 |  0.0070 | -0.0039
00:00:48 01 | 18 |  0.0106 |  0.0131
00:00:50 01 | 19 |  0.0120 |  0.0050
00:00:53 01 | 20 |  0.0092 |  0.0116
00:00:56 02 | 01 | -0.0182 | -0.0104
00:00:59 02 | 02 | -0.0039 |  0.0033
00:01:01 02 | 03 |  0.0216 |  0.0308
00:01:04 02 | 04 | -0.0225 | -0.0139
00:01:07 02 | 05 |  0.0024 |  0.0090
00:01:09 02 | 06 |  0.0091 | -0.0019
00:01:12 02 | 07 |  0.0293 |  0.0502
00:01:15 02 | 08 | -0.0146 | -0.0163
00:01:17 02 | 09 | -0.0056 |  0.0025
00:01:20 02 | 10 | -0.0067 | -0.0037
00:01:22 02 | 11 | -0.0132 | -0.0200
00:01:25 02 | 12 |  0.0172 |  0.0278
00:01:28 02 | 13 | -0.0003 |  0.0034
00:01:30 02 | 14 |  0.0159 |  0.0266
00:01:33 02 | 15 |  0.0016 |  0.0068
00:01:35 02 | 16 |  0.0122 |  0.0147
00:01:38 02 | 17 |  0.0195 |  0.0238
00:01:41 02 | 18 | -0.0023 | -0.0190
00:01:43 02 | 19 |  0.0097 | -0.0104
00:01:46 02 | 20 |  0.0148 |  0.0027
00:01:49 03 | 01 | -0.0087 | -0.0088
00:01:52 03 | 02 | -0.0088 |  0.0013
00:01:55 03 | 03 |  0.0193 |  0.0127
00:01:57 03 | 04 |  0.0066 |  0.0106
00:01:60 03 | 05 |  0.0120 |  0.0053
00:02:02 03 | 06 |  0.0189 |  0.0378
00:02:05 03 | 07 | -0.0175 | -0.0275
00:02:08 03 | 08 | -0.0112 | -0.0277
00:02:11 03 | 09 |  0.0059 | -0.0063
00:02:13 03 | 10 | -0.0149 | -0.0353
00:02:16 03 | 11 |  0.0188 |  0.0255
00:02:19 03 | 12 |  0.0144 | -0.0106
00:02:21 03 | 13 |  0.0055 | -0.0145
00:02:24 03 | 14 |  0.0037 | -0.0028
00:02:27 03 | 15 |  0.0025 | -0.0155
00:02:29 03 | 16 |  0.0134 |  0.0253
00:02:32 03 | 17 | -0.0229 | -0.0491
00:02:35 03 | 18 |  0.0112 | -0.0028
00:02:37 03 | 19 | -0.0017 | -0.0268
00:02:40 03 | 20 | -0.0055 | -0.0233
00:02:43 04 | 01 | -0.0146 | -0.0087
00:02:46 04 | 02 | -0.0178 | -0.0234
00:02:49 04 | 03 |  0.0214 |  0.0252
00:02:51 04 | 04 | -0.0223 | -0.0162
00:02:54 04 | 05 | -0.0302 | -0.0292
00:02:56 04 | 06 | -0.0172 | -0.0104
00:02:59 04 | 07 | -0.0096 | -0.0058
00:03:02 04 | 08 |  0.0093 |  0.0126
00:03:04 04 | 09 |  0.0015 |  0.0025
00:03:07 04 | 10 | -0.0186 | -0.0163
00:03:10 04 | 11 | -0.0104 | -0.0179
00:03:12 04 | 12 | -0.0076 | -0.0061
00:03:15 04 | 13 | -0.0156 | -0.0127
00:03:18 04 | 14 | -0.0061 | -0.0201
00:03:20 04 | 15 | -0.0103 | -0.0208
00:03:23 04 | 16 |  0.0101 |  0.0005
00:03:26 04 | 17 |  0.0056 |  0.0088
00:03:29 04 | 18 | -0.0226 | -0.0445
00:03:31 04 | 19 |  0.0033 | -0.0042
00:03:34 04 | 20 |  0.0063 |  0.0067
00:03:37 05 | 01 | -0.0090 | -0.0077
00:03:40 05 | 02 |  0.0057 |  0.0027
00:03:43 05 | 03 |  0.0076 |  0.0022
00:03:45 05 | 04 |  0.0070 | -0.0052
00:03:48 05 | 05 |  0.0000 | -0.0222
00:03:50 05 | 06 |  0.0178 |  0.0456
00:03:53 05 | 07 |  0.0118 |  0.0105
00:03:56 05 | 08 |  0.0226 |  0.0178
00:03:58 05 | 09 |  0.0165 |  0.0099
00:04:01 05 | 10 |  0.0109 |  0.0145
00:04:04 05 | 11 |  0.0055 | -0.0042
00:04:06 05 | 12 | -0.0041 |  0.0029
00:04:09 05 | 13 | -0.0052 | -0.0086
00:04:12 05 | 14 | -0.0039 |  0.0115
00:04:14 05 | 15 | -0.0114 | -0.0116
00:04:17 05 | 16 | -0.0103 | -0.0166
00:04:20 05 | 17 | -0.0067 | -0.0114
00:04:22 05 | 18 | -0.0078 | -0.0061
00:04:25 05 | 19 | -0.0036 |  0.0072
00:04:28 05 | 20 | -0.0247 | -0.0128
00:04:31 06 | 01 |  0.0010 |  0.0053
00:04:34 06 | 02 |  0.0451 |  0.0605
00:04:36 06 | 03 |  0.0064 |  0.0028
00:04:39 06 | 04 |  0.0400 |  0.0210
00:04:41 06 | 05 |  0.0366 |  0.0391
00:04:44 06 | 06 |  0.0264 |  0.0268
00:04:47 06 | 07 |  0.0366 |  0.0228
00:04:49 06 | 08 |  0.0281 |  0.0156
00:04:52 06 | 09 |  0.0244 |  0.0247
00:04:55 06 | 10 |  0.0278 |  0.0240
00:04:57 06 | 11 |  0.0266 |  0.0063
00:05:00 06 | 12 |  0.0329 |  0.0116
00:05:03 06 | 13 |  0.0523 |  0.0230
00:05:05 06 | 14 |  0.0503 |  0.0399
00:05:08 06 | 15 |  0.0304 |  0.0436
00:05:11 06 | 16 |  0.0461 |  0.0235
00:05:13 06 | 17 |  0.0384 |  0.0196
00:05:16 06 | 18 |  0.0441 |  0.0333
00:05:19 06 | 19 |  0.0398 |  0.0254
00:05:21 06 | 20 |  0.0440 |  0.0213
00:05:25 07 | 01 |  0.0121 |  0.0231
00:05:27 07 | 02 |  0.0262 |  0.0137
00:05:30 07 | 03 |  0.0096 |  0.0277
00:05:32 07 | 04 |  0.0369 |  0.0335
00:05:35 07 | 05 |  0.0246 |  0.0208
00:05:38 07 | 06 |  0.0217 |  0.0262
00:05:40 07 | 07 |  0.0159 |  0.0258
00:05:43 07 | 08 |  0.0141 |  0.0216
00:05:46 07 | 09 |  0.0306 |  0.0308
00:05:48 07 | 10 |  0.0296 |  0.0285
00:05:51 07 | 11 |  0.0373 |  0.0336
00:05:53 07 | 12 |  0.0342 |  0.0400
00:05:56 07 | 13 |  0.0315 |  0.0268
00:05:59 07 | 14 |  0.0206 |  0.0309
00:06:02 07 | 15 |  0.0343 |  0.0295
00:06:04 07 | 16 |  0.0243 |  0.0243
00:06:07 07 | 17 |  0.0132 |  0.0234
00:06:10 07 | 18 |  0.0107 |  0.0140
00:06:12 07 | 19 |  0.0178 |  0.0321
00:06:15 07 | 20 |  0.0156 |  0.0190
00:06:18 08 | 01 |  0.0162 |  0.0257
00:06:21 08 | 02 |  0.0037 |  0.0218
00:06:24 08 | 03 | -0.0117 | -0.0066
00:06:26 08 | 04 | -0.0020 |  0.0083
00:06:29 08 | 05 |  0.0097 |  0.0179
00:06:31 08 | 06 |  0.0286 |  0.0324
00:06:34 08 | 07 |  0.0109 |  0.0345
00:06:37 08 | 08 |  0.0264 |  0.0188
00:06:39 08 | 09 |  0.0207 |  0.0268
00:06:42 08 | 10 |  0.0168 |  0.0186
00:06:45 08 | 11 |  0.0224 |  0.0213
00:06:47 08 | 12 |  0.0116 |  0.0182
00:06:50 08 | 13 |  0.0175 | -0.0053
00:06:52 08 | 14 |  0.0268 |  0.0325
00:06:55 08 | 15 |  0.0228 |  0.0248
00:06:58 08 | 16 |  0.0185 |  0.0254
00:07:00 08 | 17 |  0.0322 |  0.0358
00:07:03 08 | 18 |  0.0271 | -0.0039</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;d2337c3c-8e53-4f18-9d42-223172a8fd62&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">00:07:06 08 | 19 |  0.0115 |  0.0026
00:07:08 08 | 20 |  0.0271 |  0.0139
00:07:12 09 | 01 |  0.0038 | -0.0098
00:07:14 09 | 02 |  0.0056 | -0.0058
00:07:17 09 | 03 |  0.0107 | -0.0015
00:07:20 09 | 04 | -0.0047 | -0.0196
00:07:22 09 | 05 | -0.0107 | -0.0124
00:07:25 09 | 06 | -0.0099 | -0.0066
00:07:27 09 | 07 | -0.0099 | -0.0142
00:07:30 09 | 08 | -0.0120 | -0.0225
00:07:33 09 | 09 | -0.0114 | -0.0292
00:07:35 09 | 10 |  0.0043 |  0.0143
00:07:38 09 | 11 |  0.0049 |  0.0152
00:07:40 09 | 12 | -0.0050 | -0.0018
00:07:43 09 | 13 | -0.0062 | -0.0181
00:07:46 09 | 14 | -0.0187 | -0.0308
00:07:48 09 | 15 | -0.0143 | -0.0207
00:07:51 09 | 16 | -0.0065 | -0.0037
00:07:53 09 | 17 | -0.0019 |  0.0072
00:07:56 09 | 18 | -0.0032 | -0.0092
00:07:59 09 | 19 | -0.0098 | -0.0018
00:08:01 09 | 20 | -0.0003 | -0.0046
00:08:05 10 | 01 | -0.0279 | -0.0386
00:08:07 10 | 02 |  0.0064 | -0.0155
00:08:10 10 | 03 |  0.0394 |  0.0448
00:08:13 10 | 04 |  0.0105 |  0.0100
00:08:15 10 | 05 |  0.0076 |  0.0021
00:08:18 10 | 06 | -0.0282 | -0.0353
00:08:21 10 | 07 |  0.0355 |  0.0315
00:08:23 10 | 08 | -0.0130 | -0.0012
00:08:26 10 | 09 | -0.0242 | -0.0202
00:08:28 10 | 10 |  0.0015 | -0.0026
00:08:31 10 | 11 | -0.0196 | -0.0049
00:08:34 10 | 12 | -0.0345 | -0.0545
00:08:36 10 | 13 | -0.0227 | -0.0250
00:08:39 10 | 14 | -0.0192 | -0.0175
00:08:42 10 | 15 | -0.0117 | -0.0190
00:08:44 10 | 16 | -0.0049 |  0.0017
00:08:47 10 | 17 | -0.0406 | -0.0358
00:08:49 10 | 18 | -0.0085 | -0.0217
00:08:52 10 | 19 | -0.0362 | -0.0324
00:08:55 10 | 20 | -0.0291 | -0.0335
00:08:59 11 | 01 |  0.0249 |  0.0177
00:09:01 11 | 02 | -0.0095 | -0.0070
00:09:04 11 | 03 |  0.0450 |  0.0484
00:09:07 11 | 04 |  0.0357 |  0.0284
00:09:09 11 | 05 | -0.0037 |  0.0010
00:09:12 11 | 06 |  0.0474 |  0.0204
00:09:14 11 | 07 |  0.0095 | -0.0013
00:09:17 11 | 08 |  0.0036 |  0.0080
00:09:20 11 | 09 |  0.0102 |  0.0100
00:09:22 11 | 10 |  0.0216 |  0.0080
00:09:25 11 | 11 |  0.0134 |  0.0166
00:09:28 11 | 12 |  0.0392 |  0.0352
00:09:30 11 | 13 |  0.0054 |  0.0051
00:09:33 11 | 14 |  0.0235 |  0.0183
00:09:35 11 | 15 |  0.0282 |  0.0281
00:09:38 11 | 16 |  0.0222 |  0.0301
00:09:41 11 | 17 |  0.0070 |  0.0146
00:09:43 11 | 18 |  0.0138 |  0.0111
00:09:47 11 | 19 |  0.0236 |  0.0282
00:09:49 11 | 20 |  0.0165 |  0.0122
00:09:53 12 | 01 |  0.0279 |  0.0220
00:09:55 12 | 02 |  0.0269 |  0.0272
00:09:58 12 | 03 |  0.0285 |  0.0193
00:10:01 12 | 04 |  0.0196 |  0.0353
00:10:03 12 | 05 |  0.0249 |  0.0203
00:10:06 12 | 06 |  0.0047 |  0.0166
00:10:09 12 | 07 |  0.0178 |  0.0274
00:10:11 12 | 08 |  0.0085 |  0.0025
00:10:14 12 | 09 |  0.0068 | -0.0233
00:10:17 12 | 10 |  0.0061 |  0.0069
00:10:19 12 | 11 |  0.0019 |  0.0202
00:10:22 12 | 12 |  0.0083 | -0.0045
00:10:24 12 | 13 |  0.0105 | -0.0001
00:10:27 12 | 14 |  0.0114 |  0.0063
00:10:30 12 | 15 |  0.0057 |  0.0129
00:10:32 12 | 16 |  0.0083 |  0.0173
00:10:35 12 | 17 |  0.0134 |  0.0143
00:10:38 12 | 18 |  0.0100 |  0.0099
00:10:40 12 | 19 |  0.0087 |  0.0248
00:10:43 12 | 20 |  0.0120 |  0.0108
(32, 16) tanh 0.1 64
00:00:07 01 | 01 |  0.0205 |  0.0150
00:00:12 01 | 02 |  0.0038 |  0.0095
00:00:18 01 | 03 |  0.0003 |  0.0011
00:00:24 01 | 04 |  0.0135 |  0.0185
00:00:29 01 | 05 |  0.0098 |  0.0127
00:00:35 01 | 06 |  0.0085 |  0.0213
00:00:41 01 | 07 |  0.0097 |  0.0201
00:00:47 01 | 08 |  0.0158 |  0.0230
00:00:52 01 | 09 |  0.0041 |  0.0009
00:00:58 01 | 10 |  0.0087 |  0.0159
00:01:04 01 | 11 |  0.0073 |  0.0162
00:01:10 01 | 12 |  0.0156 |  0.0210
00:01:15 01 | 13 |  0.0112 |  0.0101
00:01:21 01 | 14 |  0.0151 |  0.0340
00:01:27 01 | 15 |  0.0214 |  0.0225
00:01:33 01 | 16 |  0.0149 |  0.0193
00:01:38 01 | 17 |  0.0221 |  0.0246
00:01:44 01 | 18 |  0.0171 |  0.0137
00:01:50 01 | 19 |  0.0261 |  0.0276
00:01:56 01 | 20 |  0.0155 |  0.0017
00:02:02 02 | 01 |  0.0028 |  0.0072
00:02:08 02 | 02 | -0.0037 |  0.0029
00:02:14 02 | 03 |  0.0106 |  0.0314
00:02:19 02 | 04 |  0.0065 | -0.0017
00:02:25 02 | 05 | -0.0122 | -0.0035
00:02:31 02 | 06 |  0.0065 |  0.0188
00:02:37 02 | 07 |  0.0098 |  0.0113
00:02:42 02 | 08 |  0.0097 |  0.0090
00:02:48 02 | 09 |  0.0233 |  0.0277
00:02:54 02 | 10 |  0.0123 |  0.0162
00:02:59 02 | 11 |  0.0181 |  0.0237
00:03:05 02 | 12 |  0.0087 |  0.0332
00:03:11 02 | 13 |  0.0003 |  0.0100
00:03:17 02 | 14 |  0.0012 |  0.0222
00:03:23 02 | 15 | -0.0071 | -0.0051
00:03:28 02 | 16 | -0.0120 | -0.0026
00:03:34 02 | 17 |  0.0034 |  0.0249
00:03:40 02 | 18 | -0.0135 | -0.0006
00:03:46 02 | 19 | -0.0080 | -0.0007
00:03:51 02 | 20 | -0.0051 |  0.0017
00:03:58 03 | 01 |  0.0038 |  0.0017
00:04:04 03 | 02 | -0.0093 | -0.0132
00:04:09 03 | 03 | -0.0111 | -0.0368
00:04:15 03 | 04 | -0.0009 | -0.0191
00:04:21 03 | 05 | -0.0091 | -0.0202
00:04:26 03 | 06 | -0.0025 | -0.0119
00:04:32 03 | 07 | -0.0244 | -0.0359
00:04:38 03 | 08 |  0.0009 | -0.0038
00:04:44 03 | 09 | -0.0143 | -0.0366
00:04:49 03 | 10 | -0.0059 | -0.0219
00:04:55 03 | 11 | -0.0113 | -0.0299
00:05:01 03 | 12 |  0.0027 |  0.0096
00:05:07 03 | 13 | -0.0161 | -0.0104
00:05:12 03 | 14 | -0.0111 |  0.0001
00:05:18 03 | 15 | -0.0169 | -0.0158
00:05:24 03 | 16 | -0.0092 | -0.0134
00:05:30 03 | 17 | -0.0067 | -0.0134
00:05:35 03 | 18 |  0.0097 |  0.0088
00:05:41 03 | 19 | -0.0027 |  0.0021
00:05:47 03 | 20 | -0.0122 | -0.0115
00:05:54 04 | 01 | -0.0063 | -0.0045
00:05:59 04 | 02 | -0.0404 | -0.0436
00:06:05 04 | 03 | -0.0155 | -0.0307
00:06:11 04 | 04 |  0.0349 |  0.0219
00:06:17 04 | 05 |  0.0125 |  0.0047
00:06:23 04 | 06 | -0.0095 | -0.0088
00:06:29 04 | 07 | -0.0488 | -0.0466
00:06:35 04 | 08 | -0.0377 | -0.0525
00:06:40 04 | 09 | -0.0217 | -0.0186
00:06:46 04 | 10 | -0.0360 | -0.0401
00:06:52 04 | 11 | -0.0503 | -0.0445
00:06:58 04 | 12 | -0.0284 | -0.0183
00:07:04 04 | 13 | -0.0266 | -0.0343
00:07:09 04 | 14 | -0.0130 | -0.0012
00:07:15 04 | 15 | -0.0310 | -0.0185
00:07:21 04 | 16 | -0.0323 | -0.0299
00:07:27 04 | 17 | -0.0348 | -0.0305
00:07:33 04 | 18 | -0.0110 | -0.0266
00:07:38 04 | 19 | -0.0199 | -0.0169
00:07:44 04 | 20 |  0.0041 | -0.0033
00:07:51 05 | 01 |  0.0136 |  0.0221
00:07:56 05 | 02 |  0.0105 |  0.0084
00:08:02 05 | 03 |  0.0079 | -0.0019
00:08:08 05 | 04 |  0.0126 | -0.0104
00:08:14 05 | 05 |  0.0142 |  0.0009
00:08:20 05 | 06 | -0.0062 | -0.0031
00:08:25 05 | 07 |  0.0094 |  0.0081
00:08:31 05 | 08 | -0.0073 | -0.0217
00:08:37 05 | 09 |  0.0115 |  0.0019
00:08:43 05 | 10 |  0.0060 | -0.0011
00:08:48 05 | 11 |  0.0024 | -0.0029
00:08:54 05 | 12 |  0.0000 | -0.0201
00:08:60 05 | 13 | -0.0028 | -0.0212
00:09:06 05 | 14 |  0.0046 | -0.0097
00:09:11 05 | 15 |  0.0128 | -0.0012
00:09:17 05 | 16 |  0.0040 |  0.0056
00:09:23 05 | 17 |  0.0062 |  0.0029
00:09:29 05 | 18 |  0.0183 |  0.0097
00:09:34 05 | 19 |  0.0013 | -0.0036
00:09:40 05 | 20 |  0.0226 |  0.0149
00:09:47 06 | 01 | -0.0058 | -0.0168
00:09:52 06 | 02 |  0.0263 |  0.0228
00:09:58 06 | 03 |  0.0292 |  0.0116
00:10:04 06 | 04 |  0.0142 |  0.0244
00:10:10 06 | 05 |  0.0099 |  0.0229
00:10:16 06 | 06 |  0.0055 |  0.0122
00:10:21 06 | 07 |  0.0286 |  0.0195
00:10:27 06 | 08 |  0.0114 |  0.0060
00:10:33 06 | 09 |  0.0029 | -0.0139
00:10:39 06 | 10 |  0.0104 | -0.0137
00:10:44 06 | 11 | -0.0082 | -0.0030
00:10:50 06 | 12 |  0.0060 |  0.0124
00:10:56 06 | 13 |  0.0088 | -0.0091
00:11:02 06 | 14 |  0.0083 |  0.0034
00:11:07 06 | 15 | -0.0029 | -0.0140
00:11:13 06 | 16 | -0.0018 | -0.0192
00:11:19 06 | 17 |  0.0084 |  0.0058
00:11:25 06 | 18 |  0.0185 |  0.0103
00:11:31 06 | 19 |  0.0020 |  0.0056
00:11:36 06 | 20 | -0.0079 | -0.0094
00:11:43 07 | 01 |  0.0309 |  0.0187
00:11:49 07 | 02 | -0.0079 |  0.0091
00:11:54 07 | 03 |  0.0151 |  0.0175
00:11:60 07 | 04 |  0.0382 |  0.0305
00:12:06 07 | 05 | -0.0030 | -0.0087
00:12:11 07 | 06 |  0.0274 |  0.0399
00:12:17 07 | 07 |  0.0113 |  0.0322
00:12:23 07 | 08 |  0.0092 |  0.0121
00:12:29 07 | 09 |  0.0155 |  0.0118
00:12:34 07 | 10 |  0.0211 |  0.0120
00:12:40 07 | 11 |  0.0180 |  0.0216
00:12:46 07 | 12 | -0.0032 |  0.0261
00:12:52 07 | 13 |  0.0013 |  0.0016
00:12:57 07 | 14 | -0.0214 | -0.0009
00:13:03 07 | 15 | -0.0130 | -0.0114
00:13:09 07 | 16 | -0.0054 | -0.0103
00:13:15 07 | 17 | -0.0060 | -0.0092
00:13:20 07 | 18 | -0.0146 | -0.0118
00:13:26 07 | 19 | -0.0097 |  0.0045</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;82fe4cde-77a3-4127-9fe5-6e6d3382c274&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">00:13:32 07 | 20 | -0.0171 | -0.0171
00:13:38 08 | 01 | -0.0027 |  0.0151
00:13:44 08 | 02 |  0.0218 |  0.0255
00:13:50 08 | 03 |  0.0325 |  0.0279
00:13:56 08 | 04 |  0.0246 |  0.0154
00:14:02 08 | 05 |  0.0282 |  0.0265
00:14:07 08 | 06 |  0.0186 |  0.0338
00:14:13 08 | 07 |  0.0129 |  0.0004
00:14:19 08 | 08 |  0.0241 |  0.0356
00:14:25 08 | 09 |  0.0231 |  0.0475
00:14:31 08 | 10 |  0.0183 |  0.0291
00:14:37 08 | 11 |  0.0243 |  0.0438
00:14:42 08 | 12 |  0.0246 |  0.0498
00:14:48 08 | 13 |  0.0168 |  0.0222
00:14:54 08 | 14 |  0.0174 |  0.0224
00:14:60 08 | 15 |  0.0156 |  0.0309
00:15:06 08 | 16 |  0.0105 |  0.0057
00:15:11 08 | 17 |  0.0038 |  0.0205
00:15:17 08 | 18 | -0.0015 |  0.0167
00:15:23 08 | 19 |  0.0095 |  0.0160
00:15:29 08 | 20 |  0.0008 |  0.0068
00:15:35 09 | 01 |  0.0093 |  0.0070
00:15:41 09 | 02 |  0.0017 |  0.0025
00:15:47 09 | 03 | -0.0162 | -0.0234
00:15:53 09 | 04 |  0.0182 |  0.0150
00:15:59 09 | 05 |  0.0036 | -0.0064
00:16:04 09 | 06 |  0.0140 | -0.0002
00:16:10 09 | 07 |  0.0041 | -0.0102
00:16:16 09 | 08 |  0.0073 | -0.0002
00:16:22 09 | 09 |  0.0130 |  0.0044
00:16:27 09 | 10 |  0.0175 |  0.0019
00:16:33 09 | 11 |  0.0052 | -0.0031
00:16:39 09 | 12 |  0.0078 | -0.0011
00:16:45 09 | 13 |  0.0116 | -0.0042
00:16:51 09 | 14 |  0.0203 |  0.0081
00:16:56 09 | 15 |  0.0289 |  0.0275
00:17:02 09 | 16 |  0.0111 | -0.0086
00:17:08 09 | 17 |  0.0265 |  0.0130
00:17:14 09 | 18 |  0.0119 |  0.0050
00:17:20 09 | 19 |  0.0214 |  0.0069
00:17:25 09 | 20 |  0.0170 |  0.0083
00:17:32 10 | 01 | -0.0302 | -0.0195
00:17:38 10 | 02 |  0.0541 |  0.0740
00:17:43 10 | 03 |  0.0107 |  0.0233
00:17:49 10 | 04 | -0.0119 | -0.0012
00:17:55 10 | 05 | -0.0128 | -0.0040
00:18:01 10 | 06 | -0.0203 | -0.0069
00:18:07 10 | 07 | -0.0300 | -0.0136
00:18:13 10 | 08 | -0.0009 | -0.0056
00:18:18 10 | 09 | -0.0223 | -0.0165
00:18:24 10 | 10 | -0.0347 | -0.0203
00:18:30 10 | 11 | -0.0256 | -0.0178
00:18:36 10 | 12 | -0.0319 | -0.0371
00:18:41 10 | 13 | -0.0474 | -0.0301
00:18:47 10 | 14 |  0.0019 |  0.0143
00:18:53 10 | 15 | -0.0056 | -0.0199
00:18:59 10 | 16 | -0.0355 | -0.0608
00:19:05 10 | 17 | -0.0112 |  0.0039
00:19:10 10 | 18 | -0.0418 | -0.0442
00:19:16 10 | 19 | -0.0227 | -0.0123
00:19:22 10 | 20 | -0.0134 | -0.0154
00:19:28 11 | 01 |  0.0345 |  0.0298
00:19:34 11 | 02 | -0.0076 | -0.0066
00:19:40 11 | 03 | -0.0031 | -0.0049
00:19:46 11 | 04 |  0.0088 |  0.0027
00:19:52 11 | 05 |  0.0202 |  0.0198
00:19:57 11 | 06 |  0.0044 |  0.0110
00:20:03 11 | 07 |  0.0170 |  0.0162
00:20:09 11 | 08 |  0.0145 |  0.0149
00:20:15 11 | 09 |  0.0063 | -0.0019
00:20:20 11 | 10 | -0.0097 | -0.0042
00:20:26 11 | 11 |  0.0005 |  0.0109
00:20:32 11 | 12 |  0.0200 |  0.0202
00:20:38 11 | 13 |  0.0197 |  0.0194
00:20:43 11 | 14 |  0.0200 |  0.0127
00:20:49 11 | 15 |  0.0229 |  0.0202
00:20:55 11 | 16 |  0.0116 |  0.0230
00:21:01 11 | 17 |  0.0101 |  0.0095
00:21:07 11 | 18 |  0.0187 |  0.0144
00:21:12 11 | 19 |  0.0031 | -0.0071
00:21:18 11 | 20 | -0.0011 | -0.0047
00:21:25 12 | 01 |  0.0288 |  0.0355
00:21:30 12 | 02 |  0.0238 |  0.0157
00:21:36 12 | 03 |  0.0220 |  0.0163
00:21:42 12 | 04 |  0.0141 |  0.0109
00:21:48 12 | 05 | -0.0000 |  0.0219
00:21:53 12 | 06 | -0.0051 | -0.0055
00:21:59 12 | 07 |  0.0069 |  0.0039
00:22:05 12 | 08 |  0.0089 |  0.0063
00:22:11 12 | 09 |  0.0239 |  0.0207
00:22:17 12 | 10 |  0.0109 |  0.0025
00:22:23 12 | 11 |  0.0176 |  0.0234
00:22:28 12 | 12 |  0.0107 |  0.0240
00:22:34 12 | 13 |  0.0178 |  0.0233
00:22:40 12 | 14 | -0.0008 |  0.0099
00:22:46 12 | 15 |  0.0037 |  0.0242
00:22:52 12 | 16 |  0.0072 |  0.0390
00:22:58 12 | 17 | -0.0017 | -0.0010
00:23:03 12 | 18 |  0.0025 |  0.0229
00:23:09 12 | 19 |  0.0009 |  0.0027
00:23:15 12 | 20 |  0.0006 |  0.0051
(32, 16) tanh 0.1 256
00:00:03 01 | 01 |  0.0012 |  0.0105
00:00:05 01 | 02 |  0.0090 |  0.0127
00:00:07 01 | 03 |  0.0176 |  0.0166
00:00:10 01 | 04 |  0.0111 |  0.0223
00:00:12 01 | 05 |  0.0069 |  0.0120
00:00:14 01 | 06 |  0.0046 |  0.0026
00:00:16 01 | 07 |  0.0229 |  0.0235
00:00:18 01 | 08 | -0.0028 |  0.0060
00:00:20 01 | 09 |  0.0252 |  0.0270
00:00:23 01 | 10 |  0.0115 |  0.0251
00:00:25 01 | 11 |  0.0232 |  0.0208
00:00:27 01 | 12 |  0.0122 |  0.0080
00:00:29 01 | 13 |  0.0077 |  0.0035
00:00:31 01 | 14 |  0.0093 |  0.0082
00:00:34 01 | 15 |  0.0138 |  0.0153
00:00:36 01 | 16 |  0.0103 |  0.0279
00:00:38 01 | 17 |  0.0055 |  0.0097
00:00:40 01 | 18 |  0.0119 |  0.0109
00:00:42 01 | 19 |  0.0138 |  0.0221
00:00:44 01 | 20 |  0.0063 | -0.0047
00:00:47 02 | 01 |  0.0047 |  0.0042
00:00:50 02 | 02 | -0.0048 | -0.0023
00:00:52 02 | 03 |  0.0242 |  0.0477
00:00:54 02 | 04 |  0.0139 |  0.0136
00:00:56 02 | 05 | -0.0151 | -0.0049
00:00:58 02 | 06 |  0.0001 | -0.0168
00:01:00 02 | 07 |  0.0097 |  0.0169
00:01:02 02 | 08 |  0.0126 |  0.0232
00:01:04 02 | 09 |  0.0135 |  0.0180
00:01:07 02 | 10 |  0.0099 |  0.0255
00:01:09 02 | 11 |  0.0260 |  0.0267
00:01:11 02 | 12 |  0.0165 |  0.0243
00:01:13 02 | 13 |  0.0142 |  0.0066
00:01:15 02 | 14 |  0.0186 |  0.0304
00:01:17 02 | 15 |  0.0091 |  0.0103
00:01:19 02 | 16 |  0.0183 |  0.0200
00:01:22 02 | 17 |  0.0039 |  0.0168
00:01:24 02 | 18 |  0.0100 |  0.0135
00:01:26 02 | 19 |  0.0149 |  0.0259
00:01:28 02 | 20 |  0.0123 |  0.0059
00:01:31 03 | 01 | -0.0009 | -0.0155
00:01:33 03 | 02 |  0.0096 |  0.0236
00:01:35 03 | 03 | -0.0206 | -0.0254
00:01:37 03 | 04 | -0.0290 | -0.0394
00:01:40 03 | 05 |  0.0267 |  0.0202
00:01:42 03 | 06 | -0.0042 | -0.0257
00:01:44 03 | 07 |  0.0025 |  0.0044
00:01:46 03 | 08 | -0.0046 | -0.0043
00:01:48 03 | 09 | -0.0049 | -0.0214
00:01:50 03 | 10 | -0.0046 | -0.0123
00:01:52 03 | 11 |  0.0079 | -0.0196
00:01:55 03 | 12 |  0.0061 |  0.0029
00:01:57 03 | 13 |  0.0153 |  0.0100
00:01:59 03 | 14 |  0.0047 | -0.0088
00:02:01 03 | 15 | -0.0188 | -0.0285
00:02:03 03 | 16 | -0.0053 | -0.0004
00:02:06 03 | 17 |  0.0003 | -0.0193
00:02:08 03 | 18 | -0.0173 | -0.0278
00:02:10 03 | 19 | -0.0099 | -0.0082
00:02:12 03 | 20 |  0.0037 |  0.0128
00:02:15 04 | 01 | -0.0174 | -0.0249
00:02:17 04 | 02 |  0.0197 |  0.0223
00:02:19 04 | 03 | -0.0285 | -0.0353
00:02:21 04 | 04 | -0.0195 | -0.0041
00:02:24 04 | 05 |  0.0099 | -0.0156
00:02:26 04 | 06 | -0.0230 | -0.0210
00:02:28 04 | 07 | -0.0102 | -0.0127
00:02:30 04 | 08 | -0.0027 | -0.0022
00:02:32 04 | 09 |  0.0260 |  0.0099
00:02:34 04 | 10 |  0.0270 |  0.0464
00:02:37 04 | 11 | -0.0120 | -0.0065
00:02:39 04 | 12 |  0.0308 |  0.0223
00:02:41 04 | 13 | -0.0023 | -0.0119
00:02:43 04 | 14 |  0.0040 | -0.0059
00:02:45 04 | 15 | -0.0285 | -0.0424
00:02:47 04 | 16 | -0.0103 | -0.0331
00:02:50 04 | 17 |  0.0018 | -0.0058
00:02:52 04 | 18 | -0.0111 | -0.0264
00:02:54 04 | 19 | -0.0058 | -0.0154
00:02:56 04 | 20 | -0.0179 | -0.0187
00:02:59 05 | 01 |  0.0163 |  0.0059
00:03:02 05 | 02 |  0.0016 |  0.0156
00:03:04 05 | 03 |  0.0152 |  0.0162
00:03:06 05 | 04 |  0.0168 |  0.0243
00:03:08 05 | 05 |  0.0113 |  0.0028
00:03:10 05 | 06 |  0.0065 |  0.0148
00:03:13 05 | 07 |  0.0071 | -0.0013
00:03:15 05 | 08 |  0.0222 |  0.0302
00:03:17 05 | 09 |  0.0183 |  0.0190
00:03:19 05 | 10 |  0.0178 |  0.0143
00:03:21 05 | 11 |  0.0112 | -0.0175
00:03:23 05 | 12 |  0.0066 |  0.0034
00:03:26 05 | 13 |  0.0272 |  0.0219
00:03:28 05 | 14 |  0.0239 |  0.0159
00:03:30 05 | 15 |  0.0223 |  0.0100
00:03:32 05 | 16 |  0.0143 |  0.0050
00:03:34 05 | 17 | -0.0018 |  0.0078
00:03:37 05 | 18 |  0.0047 |  0.0129
00:03:39 05 | 19 |  0.0093 |  0.0256
00:03:41 05 | 20 |  0.0076 |  0.0188
00:03:44 06 | 01 | -0.0010 |  0.0072
00:03:46 06 | 02 |  0.0046 | -0.0003
00:03:48 06 | 03 |  0.0186 |  0.0252
00:03:50 06 | 04 |  0.0300 |  0.0478
00:03:53 06 | 05 |  0.0289 |  0.0021
00:03:55 06 | 06 |  0.0256 |  0.0217
00:03:57 06 | 07 |  0.0314 |  0.0299
00:03:59 06 | 08 |  0.0235 |  0.0294
00:04:01 06 | 09 |  0.0264 |  0.0015
00:04:03 06 | 10 |  0.0063 | -0.0003
00:04:06 06 | 11 |  0.0240 |  0.0132
00:04:08 06 | 12 |  0.0104 |  0.0038
00:04:10 06 | 13 |  0.0102 |  0.0127
00:04:12 06 | 14 |  0.0190 |  0.0269
00:04:14 06 | 15 |  0.0241 |  0.0263
00:04:16 06 | 16 |  0.0291 |  0.0448
00:04:19 06 | 17 |  0.0261 |  0.0353
00:04:21 06 | 18 |  0.0224 |  0.0351
00:04:23 06 | 19 |  0.0148 |  0.0096
00:04:25 06 | 20 |  0.0327 |  0.0454</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;780caf83-4592-4819-85ab-b3e85ca361aa&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">00:04:28 07 | 01 |  0.0453 |  0.0468
00:04:30 07 | 02 |  0.0044 |  0.0039
00:04:32 07 | 03 | -0.0029 |  0.0289
00:04:35 07 | 04 |  0.0053 | -0.0091
00:04:37 07 | 05 |  0.0131 |  0.0135
00:04:39 07 | 06 |  0.0074 |  0.0621
00:04:41 07 | 07 |  0.0208 |  0.0499
00:04:43 07 | 08 |  0.0260 |  0.0616
00:04:45 07 | 09 |  0.0350 |  0.0525
00:04:48 07 | 10 |  0.0149 |  0.0368
00:04:50 07 | 11 |  0.0435 |  0.0393
00:04:52 07 | 12 |  0.0426 |  0.0539
00:04:54 07 | 13 |  0.0160 |  0.0248
00:04:56 07 | 14 |  0.0086 |  0.0115
00:04:58 07 | 15 |  0.0140 |  0.0161
00:05:00 07 | 16 |  0.0158 |  0.0446
00:05:03 07 | 17 |  0.0156 |  0.0111
00:05:05 07 | 18 |  0.0025 |  0.0178
00:05:07 07 | 19 |  0.0176 |  0.0269
00:05:09 07 | 20 |  0.0142 |  0.0129
00:05:12 08 | 01 |  0.0177 |  0.0145
00:05:14 08 | 02 |  0.0145 |  0.0234
00:05:16 08 | 03 |  0.0036 | -0.0166
00:05:19 08 | 04 |  0.0277 |  0.0189
00:05:21 08 | 05 |  0.0022 |  0.0006
00:05:23 08 | 06 |  0.0264 |  0.0298
00:05:25 08 | 07 |  0.0247 |  0.0118
00:05:27 08 | 08 |  0.0304 |  0.0165
00:05:29 08 | 09 |  0.0323 |  0.0260
00:05:32 08 | 10 |  0.0148 |  0.0135
00:05:34 08 | 11 |  0.0076 |  0.0154
00:05:36 08 | 12 |  0.0274 |  0.0259
00:05:38 08 | 13 |  0.0108 |  0.0200
00:05:40 08 | 14 |  0.0151 |  0.0283
00:05:43 08 | 15 |  0.0115 |  0.0121
00:05:45 08 | 16 |  0.0241 |  0.0323
00:05:47 08 | 17 |  0.0098 |  0.0151
00:05:49 08 | 18 |  0.0206 |  0.0323
00:05:51 08 | 19 |  0.0278 |  0.0276
00:05:53 08 | 20 |  0.0225 |  0.0242
00:05:57 09 | 01 |  0.0041 |  0.0032
00:05:59 09 | 02 |  0.0019 | -0.0052
00:06:01 09 | 03 | -0.0010 |  0.0003
00:06:03 09 | 04 |  0.0026 |  0.0120
00:06:06 09 | 05 |  0.0245 |  0.0237
00:06:08 09 | 06 |  0.0190 |  0.0190
00:06:10 09 | 07 |  0.0031 | -0.0208
00:06:12 09 | 08 |  0.0088 |  0.0163
00:06:14 09 | 09 |  0.0065 | -0.0075
00:06:17 09 | 10 | -0.0073 | -0.0171
00:06:19 09 | 11 |  0.0068 |  0.0187
00:06:21 09 | 12 | -0.0063 |  0.0080
00:06:23 09 | 13 |  0.0006 |  0.0201
00:06:26 09 | 14 |  0.0011 | -0.0092
00:06:28 09 | 15 | -0.0008 | -0.0065
00:06:30 09 | 16 |  0.0066 |  0.0118
00:06:32 09 | 17 | -0.0141 | -0.0108
00:06:34 09 | 18 |  0.0046 |  0.0220
00:06:37 09 | 19 | -0.0062 | -0.0117
00:06:39 09 | 20 |  0.0008 |  0.0099
00:06:42 10 | 01 |  0.0336 |  0.0497
00:06:44 10 | 02 |  0.0164 |  0.0119
00:06:46 10 | 03 | -0.0313 | -0.0351
00:06:48 10 | 04 | -0.0147 | -0.0142
00:06:51 10 | 05 |  0.0290 |  0.0334
00:06:53 10 | 06 | -0.0023 | -0.0099
00:06:55 10 | 07 |  0.0053 |  0.0109
00:06:57 10 | 08 | -0.0195 | -0.0258
00:06:59 10 | 09 |  0.0040 |  0.0039
00:07:02 10 | 10 | -0.0011 | -0.0228
00:07:04 10 | 11 |  0.0224 |  0.0182
00:07:06 10 | 12 |  0.0213 |  0.0267
00:07:08 10 | 13 |  0.0161 |  0.0149
00:07:10 10 | 14 | -0.0036 | -0.0004
00:07:13 10 | 15 |  0.0040 | -0.0021
00:07:15 10 | 16 |  0.0039 |  0.0060
00:07:17 10 | 17 |  0.0126 | -0.0142
00:07:19 10 | 18 | -0.0033 |  0.0018
00:07:21 10 | 19 |  0.0276 |  0.0281
00:07:24 10 | 20 |  0.0084 |  0.0168
00:07:27 11 | 01 |  0.0121 |  0.0077
00:07:29 11 | 02 |  0.0090 |  0.0159
00:07:31 11 | 03 |  0.0078 |  0.0118
00:07:33 11 | 04 | -0.0076 | -0.0137
00:07:35 11 | 05 |  0.0046 | -0.0016
00:07:37 11 | 06 | -0.0147 | -0.0028
00:07:40 11 | 07 |  0.0278 |  0.0315
00:07:42 11 | 08 |  0.0096 |  0.0062
00:07:44 11 | 09 |  0.0069 |  0.0002
00:07:46 11 | 10 |  0.0041 | -0.0108
00:07:48 11 | 11 |  0.0369 |  0.0352
00:07:50 11 | 12 |  0.0140 |  0.0151
00:07:53 11 | 13 | -0.0048 | -0.0082
00:07:55 11 | 14 |  0.0174 |  0.0169
00:07:57 11 | 15 |  0.0176 |  0.0203
00:07:59 11 | 16 |  0.0222 |  0.0303
00:08:01 11 | 17 |  0.0153 |  0.0143
00:08:04 11 | 18 |  0.0179 |  0.0136
00:08:06 11 | 19 |  0.0085 |  0.0092
00:08:08 11 | 20 |  0.0193 |  0.0070
00:08:11 12 | 01 |  0.0012 | -0.0148
00:08:13 12 | 02 |  0.0143 |  0.0368
00:08:15 12 | 03 |  0.0093 |  0.0008
00:08:17 12 | 04 |  0.0067 |  0.0105
00:08:19 12 | 05 | -0.0073 | -0.0103
00:08:22 12 | 06 |  0.0001 |  0.0142
00:08:24 12 | 07 |  0.0021 | -0.0061
00:08:26 12 | 08 | -0.0073 | -0.0177
00:08:28 12 | 09 | -0.0054 | -0.0166
00:08:30 12 | 10 | -0.0017 | -0.0072
00:08:33 12 | 11 |  0.0102 |  0.0089
00:08:35 12 | 12 |  0.0016 | -0.0159
00:08:37 12 | 13 |  0.0061 |  0.0030
00:08:39 12 | 14 |  0.0079 |  0.0004
00:08:41 12 | 15 |  0.0135 |  0.0123
00:08:43 12 | 16 |  0.0189 |  0.0209
00:08:46 12 | 17 |  0.0216 |  0.0284
00:08:48 12 | 18 |  0.0159 |  0.0299
00:08:50 12 | 19 |  0.0141 |  0.0266
00:08:52 12 | 20 |  0.0042 | -0.0117
(32, 32) tanh 0.1 64
00:00:07 01 | 01 | -0.0023 | -0.0012
00:00:13 01 | 02 | -0.0057 | -0.0149
00:00:18 01 | 03 |  0.0155 |  0.0076
00:00:24 01 | 04 |  0.0243 |  0.0195
00:00:30 01 | 05 |  0.0133 |  0.0193
00:00:37 01 | 06 |  0.0110 |  0.0185
00:00:43 01 | 07 |  0.0101 |  0.0296
00:00:49 01 | 08 |  0.0076 |  0.0153
00:00:55 01 | 09 | -0.0088 |  0.0103
00:01:00 01 | 10 |  0.0099 |  0.0236
00:01:06 01 | 11 |  0.0014 | -0.0063
00:01:12 01 | 12 |  0.0098 |  0.0029
00:01:18 01 | 13 |  0.0050 |  0.0111
00:01:24 01 | 14 | -0.0032 |  0.0056
00:01:30 01 | 15 |  0.0013 | -0.0013
00:01:36 01 | 16 |  0.0111 |  0.0024
00:01:42 01 | 17 |  0.0046 |  0.0101
00:01:48 01 | 18 |  0.0096 |  0.0077
00:01:54 01 | 19 |  0.0144 |  0.0315
00:02:00 01 | 20 |  0.0050 |  0.0044
00:02:07 02 | 01 |  0.0188 |  0.0066
00:02:13 02 | 02 |  0.0070 |  0.0205
00:02:19 02 | 03 |  0.0095 |  0.0081
00:02:25 02 | 04 |  0.0153 |  0.0057
00:02:31 02 | 05 | -0.0065 | -0.0134
00:02:37 02 | 06 | -0.0235 | -0.0256
00:02:43 02 | 07 | -0.0105 |  0.0094
00:02:49 02 | 08 | -0.0055 | -0.0106
00:02:55 02 | 09 |  0.0005 | -0.0157
00:03:01 02 | 10 |  0.0067 | -0.0019
00:03:07 02 | 11 | -0.0004 | -0.0095
00:03:13 02 | 12 |  0.0040 | -0.0143
00:03:19 02 | 13 | -0.0198 | -0.0217
00:03:25 02 | 14 |  0.0192 |  0.0186
00:03:31 02 | 15 | -0.0134 | -0.0013
00:03:37 02 | 16 | -0.0096 |  0.0081
00:03:43 02 | 17 |  0.0275 |  0.0228
00:03:49 02 | 18 |  0.0138 |  0.0437
00:03:55 02 | 19 | -0.0090 | -0.0064
00:04:01 02 | 20 |  0.0053 |  0.0131
00:04:07 03 | 01 | -0.0275 | -0.0442
00:04:13 03 | 02 | -0.0094 |  0.0081
00:04:19 03 | 03 | -0.0041 | -0.0056
00:04:25 03 | 04 | -0.0213 | -0.0211
00:04:31 03 | 05 |  0.0045 |  0.0076
00:04:37 03 | 06 |  0.0123 |  0.0196
00:04:43 03 | 07 |  0.0149 |  0.0064
00:04:49 03 | 08 | -0.0200 | -0.0300
00:04:55 03 | 09 | -0.0215 | -0.0320
00:05:01 03 | 10 | -0.0084 | -0.0209
00:05:07 03 | 11 |  0.0056 |  0.0258
00:05:13 03 | 12 |  0.0041 |  0.0351
00:05:19 03 | 13 |  0.0030 | -0.0125
00:05:25 03 | 14 |  0.0008 | -0.0143
00:05:31 03 | 15 | -0.0197 | -0.0248
00:05:37 03 | 16 | -0.0111 | -0.0193
00:05:43 03 | 17 | -0.0103 | -0.0153
00:05:49 03 | 18 |  0.0307 |  0.0382
00:05:55 03 | 19 | -0.0270 | -0.0380
00:06:01 03 | 20 |  0.0050 | -0.0013
00:06:07 04 | 01 |  0.0065 |  0.0059
00:06:13 04 | 02 | -0.0153 | -0.0143
00:06:19 04 | 03 |  0.0194 | -0.0020
00:06:25 04 | 04 | -0.0126 |  0.0041
00:06:31 04 | 05 | -0.0176 | -0.0138
00:06:37 04 | 06 | -0.0403 | -0.0665
00:06:43 04 | 07 | -0.0069 | -0.0052
00:06:49 04 | 08 | -0.0453 | -0.0431
00:06:55 04 | 09 |  0.0329 |  0.0556
00:07:01 04 | 10 |  0.0233 |  0.0078
00:07:07 04 | 11 |  0.0440 |  0.0626
00:07:13 04 | 12 |  0.0185 |  0.0278
00:07:20 04 | 13 |  0.0297 |  0.0323
00:07:26 04 | 14 |  0.0076 | -0.0068
00:07:32 04 | 15 | -0.0016 | -0.0016
00:07:38 04 | 16 | -0.0359 | -0.0432
00:07:44 04 | 17 | -0.0356 | -0.0161
00:07:50 04 | 18 |  0.0198 | -0.0115
00:07:56 04 | 19 |  0.0231 |  0.0329
00:08:02 04 | 20 |  0.0045 | -0.0041
00:08:08 05 | 01 |  0.0074 | -0.0017
00:08:14 05 | 02 |  0.0061 |  0.0064
00:08:20 05 | 03 |  0.0256 |  0.0269
00:08:26 05 | 04 |  0.0248 |  0.0290
00:08:32 05 | 05 |  0.0138 |  0.0240
00:08:38 05 | 06 |  0.0161 |  0.0134
00:08:44 05 | 07 |  0.0371 |  0.0085
00:08:50 05 | 08 |  0.0219 |  0.0030
00:08:56 05 | 09 |  0.0173 |  0.0270
00:09:02 05 | 10 | -0.0112 | -0.0298
00:09:08 05 | 11 | -0.0083 | -0.0021
00:09:14 05 | 12 |  0.0164 |  0.0268
00:09:20 05 | 13 |  0.0004 |  0.0020
00:09:26 05 | 14 |  0.0112 | -0.0023
00:09:32 05 | 15 | -0.0002 | -0.0085
00:09:38 05 | 16 |  0.0104 |  0.0085
00:09:44 05 | 17 |  0.0157 | -0.0072
00:09:50 05 | 18 |  0.0287 |  0.0315
00:09:56 05 | 19 |  0.0067 | -0.0125
00:10:02 05 | 20 | -0.0144 | -0.0270
00:10:09 06 | 01 |  0.0034 |  0.0127</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;dbdc52cc-72af-4a56-a5a8-5e13ca07d1c3&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">00:10:15 06 | 02 |  0.0032 |  0.0071
00:10:21 06 | 03 |  0.0125 |  0.0284
00:10:27 06 | 04 |  0.0032 |  0.0051
00:10:33 06 | 05 |  0.0302 | -0.0030
00:10:39 06 | 06 |  0.0286 |  0.0150
00:10:45 06 | 07 |  0.0230 |  0.0275
00:10:51 06 | 08 |  0.0198 |  0.0006
00:10:57 06 | 09 | -0.0108 | -0.0206
00:11:04 06 | 10 |  0.0381 |  0.0151
00:11:10 06 | 11 |  0.0064 | -0.0021
00:11:16 06 | 12 |  0.0129 | -0.0087
00:11:22 06 | 13 |  0.0047 | -0.0059
00:11:28 06 | 14 |  0.0207 |  0.0213
00:11:34 06 | 15 |  0.0287 |  0.0332
00:11:40 06 | 16 |  0.0126 |  0.0045
00:11:46 06 | 17 |  0.0343 |  0.0306
00:11:52 06 | 18 |  0.0244 |  0.0002
00:11:58 06 | 19 |  0.0236 | -0.0031
00:12:04 06 | 20 | -0.0096 | -0.0282
00:12:10 07 | 01 |  0.0109 |  0.0148
00:12:16 07 | 02 | -0.0013 |  0.0129
00:12:22 07 | 03 |  0.0344 |  0.0282
00:12:28 07 | 04 |  0.0037 |  0.0279
00:12:34 07 | 05 |  0.0112 |  0.0149
00:12:40 07 | 06 |  0.0267 |  0.0502
00:12:46 07 | 07 |  0.0177 |  0.0178
00:12:52 07 | 08 |  0.0077 |  0.0085
00:12:58 07 | 09 |  0.0157 |  0.0303
00:13:04 07 | 10 |  0.0032 |  0.0102
00:13:10 07 | 11 |  0.0158 |  0.0208
00:13:16 07 | 12 |  0.0310 |  0.0535
00:13:22 07 | 13 |  0.0209 |  0.0457
00:13:28 07 | 14 |  0.0056 |  0.0251
00:13:34 07 | 15 | -0.0051 |  0.0036
00:13:40 07 | 16 |  0.0046 |  0.0113
00:13:46 07 | 17 | -0.0041 | -0.0185
00:13:52 07 | 18 |  0.0140 |  0.0142
00:13:58 07 | 19 |  0.0341 |  0.0543
00:14:04 07 | 20 |  0.0161 |  0.0189
00:14:11 08 | 01 |  0.0041 | -0.0169
00:14:17 08 | 02 | -0.0227 | -0.0077
00:14:23 08 | 03 |  0.0057 | -0.0096
00:14:29 08 | 04 |  0.0265 |  0.0359
00:14:35 08 | 05 |  0.0457 |  0.0530
00:14:41 08 | 06 |  0.0248 |  0.0293
00:14:47 08 | 07 |  0.0361 |  0.0286
00:14:53 08 | 08 |  0.0286 |  0.0334
00:14:59 08 | 09 |  0.0253 |  0.0349
00:15:05 08 | 10 |  0.0157 |  0.0140
00:15:11 08 | 11 |  0.0280 |  0.0252
00:15:17 08 | 12 |  0.0097 |  0.0176
00:15:23 08 | 13 |  0.0245 |  0.0097
00:15:29 08 | 14 |  0.0124 |  0.0398
00:15:35 08 | 15 |  0.0126 |  0.0172
00:15:41 08 | 16 |  0.0097 |  0.0092
00:15:47 08 | 17 |  0.0053 |  0.0207
00:15:53 08 | 18 |  0.0085 |  0.0021
00:15:59 08 | 19 |  0.0136 |  0.0109
00:16:05 08 | 20 |  0.0153 |  0.0037
00:16:11 09 | 01 |  0.0066 | -0.0088
00:16:17 09 | 02 |  0.0095 |  0.0022
00:16:23 09 | 03 |  0.0001 |  0.0168
00:16:29 09 | 04 |  0.0152 |  0.0031
00:16:35 09 | 05 |  0.0190 |  0.0140
00:16:41 09 | 06 |  0.0095 | -0.0009
00:16:47 09 | 07 |  0.0211 |  0.0219
00:16:53 09 | 08 |  0.0101 |  0.0102
00:16:59 09 | 09 |  0.0155 |  0.0111
00:17:05 09 | 10 |  0.0125 |  0.0194
00:17:11 09 | 11 |  0.0038 | -0.0066
00:17:17 09 | 12 |  0.0184 |  0.0117
00:17:23 09 | 13 |  0.0218 |  0.0252
00:17:29 09 | 14 |  0.0064 |  0.0058
00:17:35 09 | 15 |  0.0084 |  0.0235
00:17:40 09 | 16 | -0.0027 |  0.0016
00:17:46 09 | 17 |  0.0055 |  0.0030
00:17:52 09 | 18 |  0.0055 |  0.0041
00:17:58 09 | 19 |  0.0077 |  0.0200
00:18:04 09 | 20 |  0.0085 |  0.0226
00:18:12 10 | 01 |  0.0048 |  0.0025
00:18:18 10 | 02 |  0.0447 |  0.0664
00:18:24 10 | 03 | -0.0163 |  0.0010
00:18:30 10 | 04 | -0.0052 |  0.0218
00:18:36 10 | 05 |  0.0302 |  0.0314
00:18:42 10 | 06 | -0.0105 |  0.0087
00:18:48 10 | 07 |  0.0307 |  0.0231
00:18:54 10 | 08 | -0.0004 |  0.0028
00:18:60 10 | 09 |  0.0303 |  0.0422
00:19:06 10 | 10 | -0.0104 |  0.0017
00:19:12 10 | 11 | -0.0030 |  0.0172
00:19:18 10 | 12 |  0.0187 |  0.0315
00:19:24 10 | 13 |  0.0095 |  0.0202
00:19:30 10 | 14 | -0.0138 | -0.0139
00:19:36 10 | 15 | -0.0282 | -0.0168
00:19:42 10 | 16 | -0.0104 |  0.0012
00:19:48 10 | 17 | -0.0159 | -0.0035
00:19:54 10 | 18 | -0.0286 | -0.0296
00:19:60 10 | 19 | -0.0299 | -0.0159
00:20:06 10 | 20 | -0.0217 | -0.0263
00:20:13 11 | 01 |  0.0237 |  0.0097
00:20:19 11 | 02 |  0.0106 | -0.0017
00:20:25 11 | 03 | -0.0066 | -0.0182
00:20:31 11 | 04 |  0.0252 |  0.0202
00:20:37 11 | 05 |  0.0336 |  0.0187
00:20:43 11 | 06 |  0.0170 |  0.0134
00:20:49 11 | 07 |  0.0162 |  0.0115
00:20:55 11 | 08 |  0.0260 |  0.0218
00:21:01 11 | 09 |  0.0230 |  0.0212
00:21:07 11 | 10 |  0.0258 |  0.0180
00:21:13 11 | 11 |  0.0254 |  0.0226
00:21:19 11 | 12 |  0.0390 |  0.0469
00:21:25 11 | 13 |  0.0137 |  0.0049
00:21:31 11 | 14 |  0.0198 |  0.0216
00:21:37 11 | 15 |  0.0156 |  0.0176
00:21:43 11 | 16 |  0.0281 |  0.0241
00:21:49 11 | 17 |  0.0169 |  0.0087
00:21:55 11 | 18 |  0.0100 |  0.0046
00:22:01 11 | 19 |  0.0173 |  0.0070
00:22:07 11 | 20 |  0.0123 |  0.0028
00:22:14 12 | 01 |  0.0145 |  0.0261
00:22:20 12 | 02 |  0.0200 |  0.0221
00:22:26 12 | 03 |  0.0111 |  0.0115
00:22:32 12 | 04 |  0.0035 | -0.0107
00:22:38 12 | 05 | -0.0036 |  0.0008
00:22:43 12 | 06 |  0.0001 | -0.0036
00:22:49 12 | 07 |  0.0059 | -0.0065
00:22:55 12 | 08 |  0.0056 | -0.0051
00:23:01 12 | 09 |  0.0019 | -0.0139
00:23:07 12 | 10 |  0.0067 | -0.0000
00:23:13 12 | 11 | -0.0109 | -0.0062
00:23:19 12 | 12 | -0.0032 | -0.0083
00:23:25 12 | 13 |  0.0004 |  0.0029
00:23:31 12 | 14 | -0.0121 | -0.0012
00:23:37 12 | 15 | -0.0030 | -0.0005
00:23:43 12 | 16 | -0.0135 | -0.0065
00:23:49 12 | 17 | -0.0103 | -0.0023
00:23:55 12 | 18 | -0.0107 |  0.0110
00:24:01 12 | 19 | -0.0075 |  0.0058
00:24:07 12 | 20 | -0.0018 | -0.0006
(32, 32) tanh 0.1 256
00:00:03 01 | 01 |  0.0003 |  0.0034
00:00:06 01 | 02 |  0.0049 | -0.0008
00:00:08 01 | 03 | -0.0058 | -0.0254
00:00:10 01 | 04 |  0.0062 |  0.0141
00:00:13 01 | 05 |  0.0118 |  0.0281
00:00:15 01 | 06 |  0.0026 | -0.0115
00:00:18 01 | 07 |  0.0040 |  0.0041
00:00:20 01 | 08 |  0.0035 |  0.0177
00:00:22 01 | 09 |  0.0127 |  0.0072
00:00:25 01 | 10 |  0.0268 |  0.0345
00:00:27 01 | 11 |  0.0144 | -0.0018
00:00:29 01 | 12 |  0.0157 |  0.0295
00:00:32 01 | 13 |  0.0155 |  0.0229
00:00:34 01 | 14 |  0.0202 |  0.0160
00:00:37 01 | 15 |  0.0161 |  0.0144
00:00:39 01 | 16 |  0.0225 |  0.0246
00:00:41 01 | 17 |  0.0189 |  0.0000
00:00:44 01 | 18 |  0.0280 |  0.0164
00:00:46 01 | 19 |  0.0305 |  0.0245
00:00:49 01 | 20 |  0.0179 |  0.0100
00:00:52 02 | 01 |  0.0054 |  0.0043
00:00:54 02 | 02 | -0.0134 | -0.0167
00:00:56 02 | 03 |  0.0193 |  0.0319
00:00:59 02 | 04 | -0.0077 | -0.0056
00:01:01 02 | 05 |  0.0077 |  0.0204
00:01:03 02 | 06 |  0.0069 |  0.0329
00:01:06 02 | 07 |  0.0011 |  0.0019
00:01:08 02 | 08 | -0.0022 |  0.0123
00:01:11 02 | 09 | -0.0059 | -0.0013
00:01:13 02 | 10 |  0.0117 |  0.0129
00:01:15 02 | 11 |  0.0054 |  0.0026
00:01:18 02 | 12 |  0.0022 | -0.0028
00:01:20 02 | 13 |  0.0078 |  0.0111
00:01:22 02 | 14 | -0.0080 | -0.0180
00:01:25 02 | 15 | -0.0070 |  0.0084
00:01:28 02 | 16 | -0.0090 | -0.0041
00:01:30 02 | 17 |  0.0034 |  0.0033
00:01:32 02 | 18 |  0.0028 |  0.0152
00:01:35 02 | 19 |  0.0148 |  0.0032
00:01:37 02 | 20 |  0.0126 |  0.0056
00:01:40 03 | 01 |  0.0086 |  0.0015
00:01:43 03 | 02 |  0.0135 |  0.0062
00:01:45 03 | 03 |  0.0181 |  0.0191
00:01:48 03 | 04 |  0.0026 |  0.0189
00:01:50 03 | 05 |  0.0012 | -0.0084
00:01:52 03 | 06 |  0.0022 |  0.0114
00:01:55 03 | 07 |  0.0108 |  0.0331
00:01:57 03 | 08 | -0.0113 | -0.0443
00:01:60 03 | 09 |  0.0042 | -0.0108
00:02:02 03 | 10 |  0.0128 | -0.0038
00:02:05 03 | 11 | -0.0192 | -0.0224
00:02:07 03 | 12 |  0.0082 |  0.0073
00:02:09 03 | 13 | -0.0028 |  0.0097
00:02:12 03 | 14 |  0.0092 |  0.0088
00:02:14 03 | 15 | -0.0075 | -0.0056
00:02:17 03 | 16 | -0.0052 | -0.0145
00:02:19 03 | 17 | -0.0067 | -0.0112
00:02:22 03 | 18 | -0.0051 | -0.0067
00:02:24 03 | 19 |  0.0103 |  0.0030
00:02:26 03 | 20 | -0.0070 | -0.0219
00:02:30 04 | 01 |  0.0368 |  0.0134
00:02:32 04 | 02 |  0.0018 |  0.0150
00:02:34 04 | 03 |  0.0325 |  0.0353
00:02:37 04 | 04 | -0.0162 | -0.0105
00:02:39 04 | 05 | -0.0339 | -0.0436
00:02:42 04 | 06 |  0.0057 | -0.0152
00:02:44 04 | 07 | -0.0050 | -0.0042
00:02:46 04 | 08 |  0.0121 | -0.0013
00:02:49 04 | 09 |  0.0333 |  0.0224
00:02:51 04 | 10 |  0.0125 | -0.0001
00:02:54 04 | 11 |  0.0074 |  0.0163
00:02:56 04 | 12 |  0.0362 |  0.0110
00:02:59 04 | 13 |  0.0168 |  0.0125
00:03:01 04 | 14 |  0.0167 |  0.0117
00:03:03 04 | 15 | -0.0016 | -0.0081
00:03:06 04 | 16 |  0.0207 |  0.0064
00:03:08 04 | 17 |  0.0311 |  0.0186
00:03:11 04 | 18 |  0.0191 |  0.0193
00:03:13 04 | 19 |  0.0276 |  0.0134
00:03:16 04 | 20 |  0.0196 |  0.0105
00:03:19 05 | 01 |  0.0087 |  0.0136
00:03:21 05 | 02 |  0.0154 |  0.0023</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;908b3106-bb9b-414e-b490-2a19dd274de6&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">00:03:24 05 | 03 | -0.0068 | -0.0102
00:03:26 05 | 04 | -0.0057 | -0.0096
00:03:28 05 | 05 |  0.0217 |  0.0020
00:03:31 05 | 06 |  0.0140 |  0.0141
00:03:33 05 | 07 |  0.0139 |  0.0264
00:03:36 05 | 08 |  0.0231 |  0.0337
00:03:38 05 | 09 | -0.0134 | -0.0025
00:03:41 05 | 10 | -0.0017 |  0.0061
00:03:43 05 | 11 | -0.0133 | -0.0072
00:03:45 05 | 12 | -0.0059 | -0.0047
00:03:48 05 | 13 | -0.0049 |  0.0024
00:03:50 05 | 14 | -0.0059 |  0.0104
00:03:53 05 | 15 | -0.0088 | -0.0259
00:03:55 05 | 16 | -0.0037 |  0.0072
00:03:57 05 | 17 | -0.0113 | -0.0055
00:03:60 05 | 18 | -0.0117 | -0.0081
00:04:02 05 | 19 | -0.0184 | -0.0137
00:04:05 05 | 20 | -0.0210 | -0.0096
00:04:08 06 | 01 | -0.0064 |  0.0149
00:04:10 06 | 02 |  0.0063 |  0.0118
00:04:13 06 | 03 |  0.0493 |  0.0270
00:04:15 06 | 04 |  0.0471 |  0.0395
00:04:17 06 | 05 | -0.0000 |  0.0073
00:04:20 06 | 06 |  0.0285 |  0.0199
00:04:22 06 | 07 |  0.0347 |  0.0229
00:04:25 06 | 08 |  0.0153 |  0.0094
00:04:27 06 | 09 |  0.0265 |  0.0354
00:04:29 06 | 10 |  0.0210 |  0.0284
00:04:32 06 | 11 |  0.0480 |  0.0124
00:04:34 06 | 12 |  0.0144 |  0.0130
00:04:36 06 | 13 |  0.0056 |  0.0043
00:04:39 06 | 14 |  0.0130 |  0.0045
00:04:41 06 | 15 |  0.0131 |  0.0186
00:04:44 06 | 16 |  0.0101 |  0.0161
00:04:46 06 | 17 |  0.0257 |  0.0225
00:04:48 06 | 18 |  0.0334 |  0.0420
00:04:51 06 | 19 |  0.0234 |  0.0018
00:04:53 06 | 20 |  0.0231 |  0.0190
00:04:57 07 | 01 |  0.0157 |  0.0243
00:04:59 07 | 02 |  0.0171 |  0.0507
00:05:02 07 | 03 | -0.0002 | -0.0030
00:05:04 07 | 04 | -0.0075 |  0.0034
00:05:06 07 | 05 |  0.0412 |  0.0496
00:05:09 07 | 06 |  0.0175 |  0.0192
00:05:11 07 | 07 |  0.0167 |  0.0231
00:05:14 07 | 08 |  0.0101 |  0.0360
00:05:16 07 | 09 |  0.0010 |  0.0055
00:05:18 07 | 10 | -0.0077 |  0.0155
00:05:21 07 | 11 | -0.0012 |  0.0086
00:05:23 07 | 12 | -0.0090 | -0.0004
00:05:26 07 | 13 |  0.0113 |  0.0288
00:05:28 07 | 14 | -0.0007 |  0.0133
00:05:31 07 | 15 | -0.0090 |  0.0050
00:05:33 07 | 16 |  0.0278 |  0.0337
00:05:35 07 | 17 |  0.0295 |  0.0296
00:05:38 07 | 18 |  0.0318 |  0.0406
00:05:40 07 | 19 |  0.0287 |  0.0203
00:05:43 07 | 20 |  0.0321 |  0.0428
00:05:46 08 | 01 | -0.0016 |  0.0195
00:05:48 08 | 02 |  0.0005 |  0.0103
00:05:51 08 | 03 |  0.0147 |  0.0008
00:05:53 08 | 04 |  0.0277 |  0.0250
00:05:56 08 | 05 | -0.0053 |  0.0316
00:05:58 08 | 06 |  0.0258 |  0.0165
00:06:00 08 | 07 |  0.0121 | -0.0137
00:06:03 08 | 08 |  0.0225 |  0.0317
00:06:05 08 | 09 |  0.0297 |  0.0417
00:06:08 08 | 10 |  0.0193 | -0.0122
00:06:10 08 | 11 |  0.0333 |  0.0290
00:06:13 08 | 12 |  0.0175 |  0.0187
00:06:15 08 | 13 |  0.0177 |  0.0271
00:06:17 08 | 14 |  0.0185 |  0.0329
00:06:20 08 | 15 |  0.0178 |  0.0112
00:06:22 08 | 16 |  0.0141 | -0.0077
00:06:25 08 | 17 |  0.0246 |  0.0125
00:06:27 08 | 18 |  0.0213 |  0.0194
00:06:30 08 | 19 |  0.0218 |  0.0199
00:06:32 08 | 20 |  0.0256 |  0.0245
00:06:35 09 | 01 |  0.0149 |  0.0108
00:06:38 09 | 02 |  0.0344 |  0.0409
00:06:40 09 | 03 |  0.0021 |  0.0027
00:06:42 09 | 04 |  0.0099 |  0.0026
00:06:45 09 | 05 | -0.0020 | -0.0075
00:06:47 09 | 06 |  0.0104 | -0.0006
00:06:50 09 | 07 |  0.0136 | -0.0048
00:06:52 09 | 08 |  0.0070 | -0.0047
00:06:54 09 | 09 |  0.0186 |  0.0102
00:06:57 09 | 10 |  0.0066 | -0.0017
00:06:59 09 | 11 |  0.0280 |  0.0411
00:07:02 09 | 12 |  0.0097 | -0.0056
00:07:04 09 | 13 |  0.0164 |  0.0057
00:07:06 09 | 14 |  0.0055 |  0.0098
00:07:09 09 | 15 |  0.0074 | -0.0093
00:07:11 09 | 16 |  0.0121 | -0.0018
00:07:14 09 | 17 |  0.0166 |  0.0033
00:07:16 09 | 18 |  0.0197 |  0.0081
00:07:18 09 | 19 |  0.0005 | -0.0059
00:07:21 09 | 20 |  0.0110 | -0.0019
00:07:24 10 | 01 | -0.0232 | -0.0017
00:07:26 10 | 02 |  0.0464 |  0.0377
00:07:29 10 | 03 | -0.0011 |  0.0062
00:07:31 10 | 04 |  0.0155 |  0.0147
00:07:34 10 | 05 | -0.0305 | -0.0334
00:07:36 10 | 06 |  0.0054 |  0.0151
00:07:39 10 | 07 |  0.0021 |  0.0158
00:07:41 10 | 08 | -0.0139 | -0.0097
00:07:43 10 | 09 | -0.0155 | -0.0311
00:07:46 10 | 10 | -0.0079 | -0.0119
00:07:48 10 | 11 | -0.0260 | -0.0226
00:07:51 10 | 12 | -0.0312 | -0.0071
00:07:53 10 | 13 | -0.0173 | -0.0062
00:07:56 10 | 14 | -0.0055 | -0.0015
00:07:58 10 | 15 | -0.0385 | -0.0177
00:08:00 10 | 16 | -0.0161 |  0.0120
00:08:03 10 | 17 | -0.0231 | -0.0123
00:08:05 10 | 18 | -0.0217 | -0.0173
00:08:08 10 | 19 | -0.0233 | -0.0345
00:08:10 10 | 20 | -0.0040 |  0.0064
00:08:13 11 | 01 |  0.0191 |  0.0120
00:08:16 11 | 02 |  0.0255 |  0.0242
00:08:18 11 | 03 |  0.0281 |  0.0281
00:08:20 11 | 04 |  0.0297 |  0.0344
00:08:23 11 | 05 |  0.0268 |  0.0172
00:08:25 11 | 06 |  0.0316 |  0.0258
00:08:28 11 | 07 |  0.0025 |  0.0017
00:08:30 11 | 08 |  0.0316 |  0.0301
00:08:33 11 | 09 |  0.0302 |  0.0306
00:08:35 11 | 10 | -0.0018 | -0.0172
00:08:37 11 | 11 | -0.0046 | -0.0146
00:08:40 11 | 12 | -0.0149 | -0.0199
00:08:42 11 | 13 |  0.0042 |  0.0050
00:08:45 11 | 14 | -0.0096 | -0.0132
00:08:47 11 | 15 |  0.0200 |  0.0194
00:08:49 11 | 16 |  0.0054 |  0.0114
00:08:52 11 | 17 |  0.0014 |  0.0115
00:08:54 11 | 18 |  0.0075 |  0.0132
00:08:57 11 | 19 |  0.0037 | -0.0040
00:08:59 11 | 20 |  0.0127 |  0.0095
00:09:03 12 | 01 |  0.0222 |  0.0286
00:09:05 12 | 02 |  0.0065 | -0.0045
00:09:07 12 | 03 |  0.0191 |  0.0314
00:09:10 12 | 04 |  0.0193 |  0.0227
00:09:12 12 | 05 |  0.0162 |  0.0211
00:09:15 12 | 06 |  0.0170 |  0.0055
00:09:17 12 | 07 |  0.0123 |  0.0165
00:09:20 12 | 08 |  0.0178 |  0.0343
00:09:22 12 | 09 |  0.0246 |  0.0391
00:09:24 12 | 10 |  0.0116 |  0.0176
00:09:27 12 | 11 |  0.0269 |  0.0245
00:09:29 12 | 12 |  0.0141 |  0.0138
00:09:32 12 | 13 |  0.0118 |  0.0325
00:09:34 12 | 14 |  0.0110 |  0.0268
00:09:37 12 | 15 |  0.0243 |  0.0411
00:09:39 12 | 16 |  0.0222 |  0.0115
00:09:41 12 | 17 |  0.0182 |  0.0250
00:09:44 12 | 18 |  0.0245 |  0.0329
00:09:46 12 | 19 |  0.0226 |  0.0094
00:09:49 12 | 20 |  0.0229 |  0.0293
(32, 16) tanh 0 64
00:00:07 01 | 01 | -0.0044 |  0.0151
00:00:12 01 | 02 |  0.0249 |  0.0296
00:00:18 01 | 03 |  0.0034 | -0.0118
00:00:24 01 | 04 |  0.0185 |  0.0101
00:00:30 01 | 05 |  0.0258 |  0.0158
00:00:35 01 | 06 |  0.0088 | -0.0166
00:00:41 01 | 07 |  0.0200 |  0.0148
00:00:47 01 | 08 |  0.0142 |  0.0167
00:00:52 01 | 09 |  0.0209 |  0.0254
00:00:58 01 | 10 |  0.0022 |  0.0118
00:01:04 01 | 11 |  0.0039 |  0.0055
00:01:10 01 | 12 |  0.0133 | -0.0061
00:01:15 01 | 13 | -0.0162 | -0.0198
00:01:21 01 | 14 |  0.0089 |  0.0156
00:01:27 01 | 15 |  0.0011 |  0.0023
00:01:32 01 | 16 | -0.0001 |  0.0032
00:01:38 01 | 17 | -0.0080 | -0.0169
00:01:44 01 | 18 |  0.0025 |  0.0015
00:01:50 01 | 19 | -0.0069 | -0.0026
00:01:55 01 | 20 | -0.0045 | -0.0129
00:02:02 02 | 01 | -0.0046 | -0.0187
00:02:08 02 | 02 |  0.0101 |  0.0240
00:02:13 02 | 03 |  0.0131 |  0.0124
00:02:19 02 | 04 |  0.0125 |  0.0293
00:02:25 02 | 05 |  0.0095 |  0.0154
00:02:30 02 | 06 | -0.0173 | -0.0170
00:02:36 02 | 07 |  0.0114 |  0.0094
00:02:42 02 | 08 |  0.0086 |  0.0214
00:02:48 02 | 09 |  0.0191 |  0.0273
00:02:53 02 | 10 |  0.0046 |  0.0250
00:02:59 02 | 11 |  0.0177 |  0.0206
00:03:05 02 | 12 |  0.0019 |  0.0044
00:03:11 02 | 13 |  0.0125 |  0.0229
00:03:16 02 | 14 |  0.0078 |  0.0392
00:03:22 02 | 15 |  0.0247 |  0.0332
00:03:28 02 | 16 |  0.0130 |  0.0244
00:03:34 02 | 17 |  0.0097 | -0.0049
00:03:39 02 | 18 |  0.0012 | -0.0011
00:03:45 02 | 19 |  0.0171 |  0.0008
00:03:51 02 | 20 | -0.0110 |  0.0004
00:03:57 03 | 01 |  0.0382 |  0.0364
00:04:03 03 | 02 | -0.0109 | -0.0179
00:04:09 03 | 03 |  0.0061 |  0.0082
00:04:14 03 | 04 | -0.0066 | -0.0114
00:04:20 03 | 05 | -0.0193 | -0.0451
00:04:26 03 | 06 |  0.0002 | -0.0208
00:04:32 03 | 07 |  0.0057 | -0.0143
00:04:37 03 | 08 | -0.0104 | -0.0261
00:04:43 03 | 09 | -0.0000 |  0.0007
00:04:49 03 | 10 | -0.0163 | -0.0149
00:04:55 03 | 11 | -0.0060 | -0.0334
00:05:00 03 | 12 | -0.0094 | -0.0299
00:05:06 03 | 13 |  0.0001 |  0.0045
00:05:12 03 | 14 | -0.0060 | -0.0183
00:05:17 03 | 15 | -0.0009 | -0.0077
00:05:23 03 | 16 | -0.0042 | -0.0073
00:05:29 03 | 17 | -0.0171 | -0.0195
00:05:34 03 | 18 | -0.0038 | -0.0122
00:05:40 03 | 19 | -0.0086 | -0.0069
00:05:46 03 | 20 |  0.0000 |  0.0012
00:05:52 04 | 01 |  0.0029 |  0.0088
00:05:58 04 | 02 |  0.0043 |  0.0113
00:06:04 04 | 03 | -0.0252 | -0.0217</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;4f60283b-b27d-45d4-9679-019d10130a04&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">00:06:09 04 | 04 |  0.0070 | -0.0061
00:06:15 04 | 05 |  0.0107 |  0.0248
00:06:21 04 | 06 |  0.0204 |  0.0019
00:06:27 04 | 07 |  0.0023 | -0.0193
00:06:32 04 | 08 | -0.0217 | -0.0418
00:06:38 04 | 09 | -0.0098 | -0.0075
00:06:44 04 | 10 |  0.0100 |  0.0007
00:06:50 04 | 11 | -0.0029 | -0.0117
00:06:56 04 | 12 |  0.0014 | -0.0000
00:07:02 04 | 13 | -0.0009 | -0.0196
00:07:07 04 | 14 | -0.0108 | -0.0058
00:07:13 04 | 15 | -0.0174 | -0.0199
00:07:19 04 | 16 | -0.0220 | -0.0339
00:07:25 04 | 17 |  0.0031 |  0.0031
00:07:30 04 | 18 | -0.0216 | -0.0219
00:07:36 04 | 19 | -0.0033 |  0.0078
00:07:42 04 | 20 | -0.0033 |  0.0071
00:07:48 05 | 01 |  0.0108 |  0.0168
00:07:54 05 | 02 |  0.0139 |  0.0052
00:07:60 05 | 03 |  0.0217 |  0.0330
00:08:06 05 | 04 |  0.0164 |  0.0138
00:08:11 05 | 05 |  0.0065 | -0.0062
00:08:17 05 | 06 |  0.0171 |  0.0027
00:08:23 05 | 07 | -0.0043 | -0.0031
00:08:29 05 | 08 | -0.0135 | -0.0227
00:08:35 05 | 09 | -0.0001 | -0.0086
00:08:40 05 | 10 |  0.0045 |  0.0117
00:08:46 05 | 11 |  0.0061 |  0.0083
00:08:52 05 | 12 |  0.0295 |  0.0309
00:08:58 05 | 13 |  0.0185 |  0.0213
00:09:03 05 | 14 |  0.0266 |  0.0275
00:09:09 05 | 15 |  0.0107 |  0.0016
00:09:15 05 | 16 |  0.0013 | -0.0099
00:09:21 05 | 17 |  0.0149 | -0.0042
00:09:26 05 | 18 |  0.0100 |  0.0007
00:09:32 05 | 19 |  0.0237 | -0.0019
00:09:38 05 | 20 | -0.0012 | -0.0057
00:09:44 06 | 01 |  0.0048 |  0.0099
00:09:50 06 | 02 |  0.0427 |  0.0391
00:09:56 06 | 03 |  0.0093 |  0.0258
00:10:01 06 | 04 |  0.0093 |  0.0120
00:10:07 06 | 05 |  0.0333 |  0.0102
00:10:13 06 | 06 |  0.0081 | -0.0149
00:10:19 06 | 07 |  0.0376 |  0.0321
00:10:24 06 | 08 |  0.0281 |  0.0263
00:10:30 06 | 09 |  0.0121 |  0.0063
00:10:36 06 | 10 |  0.0507 |  0.0492
00:10:42 06 | 11 |  0.0311 |  0.0279
00:10:47 06 | 12 |  0.0286 |  0.0128
00:10:53 06 | 13 |  0.0339 |  0.0236
00:10:59 06 | 14 |  0.0400 |  0.0249
00:11:04 06 | 15 |  0.0193 |  0.0183
00:11:10 06 | 16 |  0.0286 |  0.0387
00:11:16 06 | 17 |  0.0283 |  0.0366
00:11:22 06 | 18 |  0.0291 |  0.0131
00:11:27 06 | 19 |  0.0328 |  0.0483
00:11:33 06 | 20 |  0.0206 |  0.0204
00:11:40 07 | 01 |  0.0201 |  0.0033
00:11:45 07 | 02 | -0.0032 |  0.0265
00:11:51 07 | 03 |  0.0107 |  0.0423
00:11:57 07 | 04 |  0.0320 |  0.0677
00:12:03 07 | 05 | -0.0040 |  0.0166
00:12:08 07 | 06 |  0.0258 |  0.0451
00:12:14 07 | 07 |  0.0055 |  0.0457
00:12:20 07 | 08 | -0.0054 |  0.0308
00:12:26 07 | 09 |  0.0158 |  0.0360
00:12:32 07 | 10 | -0.0034 |  0.0220
00:12:37 07 | 11 |  0.0045 |  0.0321
00:12:43 07 | 12 |  0.0248 |  0.0182
00:12:49 07 | 13 | -0.0050 |  0.0074
00:12:55 07 | 14 |  0.0003 |  0.0196
00:13:00 07 | 15 |  0.0103 |  0.0303
00:13:06 07 | 16 |  0.0162 |  0.0380
00:13:12 07 | 17 | -0.0175 |  0.0185
00:13:18 07 | 18 |  0.0148 |  0.0222
00:13:24 07 | 19 |  0.0131 |  0.0303
00:13:29 07 | 20 |  0.0109 |  0.0248
00:13:36 08 | 01 |  0.0225 |  0.0016
00:13:41 08 | 02 | -0.0060 | -0.0142
00:13:47 08 | 03 |  0.0222 |  0.0168
00:13:53 08 | 04 |  0.0022 | -0.0061
00:13:59 08 | 05 |  0.0079 |  0.0123
00:14:04 08 | 06 |  0.0093 |  0.0074
00:14:10 08 | 07 |  0.0114 |  0.0064
00:14:16 08 | 08 |  0.0094 |  0.0128
00:14:22 08 | 09 |  0.0141 |  0.0034
00:14:27 08 | 10 |  0.0183 |  0.0286
00:14:33 08 | 11 |  0.0158 |  0.0214
00:14:39 08 | 12 |  0.0219 |  0.0272
00:14:45 08 | 13 |  0.0258 |  0.0248
00:14:50 08 | 14 |  0.0209 |  0.0075
00:14:56 08 | 15 |  0.0204 |  0.0165
00:15:02 08 | 16 |  0.0347 |  0.0344
00:15:07 08 | 17 |  0.0226 |  0.0362
00:15:13 08 | 18 |  0.0200 |  0.0188
00:15:19 08 | 19 |  0.0248 |  0.0260
00:15:25 08 | 20 |  0.0179 |  0.0268
00:15:32 09 | 01 |  0.0093 | -0.0041
00:15:37 09 | 02 | -0.0073 | -0.0028
00:15:43 09 | 03 |  0.0074 |  0.0123
00:15:49 09 | 04 | -0.0019 | -0.0091
00:15:55 09 | 05 |  0.0060 |  0.0018
00:16:01 09 | 06 | -0.0087 | -0.0201
00:16:07 09 | 07 | -0.0040 | -0.0162
00:16:13 09 | 08 | -0.0073 | -0.0098
00:16:18 09 | 09 | -0.0068 |  0.0097
00:16:24 09 | 10 | -0.0071 | -0.0108
00:16:30 09 | 11 |  0.0052 |  0.0107
00:16:36 09 | 12 | -0.0196 | -0.0205
00:16:42 09 | 13 | -0.0127 | -0.0190
00:16:48 09 | 14 | -0.0163 | -0.0315
00:16:53 09 | 15 | -0.0152 | -0.0362
00:16:59 09 | 16 | -0.0179 | -0.0276
00:17:05 09 | 17 | -0.0049 | -0.0118
00:17:11 09 | 18 | -0.0261 | -0.0267
00:17:17 09 | 19 | -0.0290 | -0.0206
00:17:22 09 | 20 | -0.0117 |  0.0055
00:17:29 10 | 01 |  0.0152 |  0.0200
00:17:35 10 | 02 | -0.0338 | -0.0560
00:17:41 10 | 03 | -0.0160 | -0.0190
00:17:46 10 | 04 |  0.0150 |  0.0211
00:17:52 10 | 05 | -0.0059 | -0.0104
00:17:58 10 | 06 |  0.0206 |  0.0048
00:18:04 10 | 07 |  0.0176 |  0.0052
00:18:09 10 | 08 |  0.0166 |  0.0066
00:18:15 10 | 09 |  0.0171 |  0.0254
00:18:21 10 | 10 | -0.0013 | -0.0047
00:18:27 10 | 11 | -0.0024 | -0.0012
00:18:32 10 | 12 | -0.0161 | -0.0349
00:18:38 10 | 13 | -0.0148 | -0.0054
00:18:44 10 | 14 | -0.0099 | -0.0026
00:18:50 10 | 15 | -0.0033 | -0.0035
00:18:55 10 | 16 | -0.0299 | -0.0435
00:19:01 10 | 17 | -0.0216 | -0.0328
00:19:07 10 | 18 | -0.0387 | -0.0560
00:19:13 10 | 19 | -0.0142 | -0.0274
00:19:19 10 | 20 | -0.0309 | -0.0314
00:19:25 11 | 01 | -0.0161 | -0.0164
00:19:31 11 | 02 | -0.0012 | -0.0084
00:19:37 11 | 03 |  0.0255 |  0.0136
00:19:43 11 | 04 |  0.0336 |  0.0213
00:19:49 11 | 05 |  0.0136 |  0.0139
00:19:54 11 | 06 |  0.0303 |  0.0383
00:20:00 11 | 07 |  0.0173 |  0.0115
00:20:06 11 | 08 |  0.0141 |  0.0058
00:20:12 11 | 09 |  0.0056 |  0.0093
00:20:17 11 | 10 | -0.0048 | -0.0105
00:20:23 11 | 11 |  0.0129 |  0.0010
00:20:29 11 | 12 |  0.0010 |  0.0046
00:20:35 11 | 13 |  0.0117 |  0.0137
00:20:40 11 | 14 |  0.0142 |  0.0280
00:20:46 11 | 15 |  0.0232 |  0.0336
00:20:52 11 | 16 |  0.0203 |  0.0120
00:20:58 11 | 17 |  0.0323 |  0.0214
00:21:04 11 | 18 |  0.0302 |  0.0243
00:21:09 11 | 19 |  0.0154 |  0.0121
00:21:15 11 | 20 |  0.0310 |  0.0282
00:21:22 12 | 01 |  0.0204 |  0.0097
00:21:27 12 | 02 |  0.0046 |  0.0069
00:21:33 12 | 03 |  0.0005 |  0.0108
00:21:39 12 | 04 |  0.0047 |  0.0013
00:21:45 12 | 05 |  0.0239 |  0.0241
00:21:51 12 | 06 |  0.0135 |  0.0255
00:21:56 12 | 07 |  0.0128 |  0.0050
00:22:02 12 | 08 |  0.0054 |  0.0115
00:22:08 12 | 09 |  0.0141 |  0.0205
00:22:14 12 | 10 | -0.0025 |  0.0258
00:22:20 12 | 11 |  0.0037 |  0.0153
00:22:25 12 | 12 | -0.0046 |  0.0077
00:22:31 12 | 13 |  0.0015 |  0.0086
00:22:37 12 | 14 |  0.0075 |  0.0135
00:22:43 12 | 15 |  0.0143 |  0.0192
00:22:48 12 | 16 |  0.0092 |  0.0177
00:22:54 12 | 17 | -0.0008 |  0.0087
00:22:60 12 | 18 |  0.0095 |  0.0280
00:23:06 12 | 19 |  0.0047 |  0.0059
00:23:11 12 | 20 |  0.0076 |  0.0134
(32, 16) tanh 0 256
00:00:03 01 | 01 |  0.0065 |  0.0054
00:00:05 01 | 02 |  0.0003 |  0.0026
00:00:07 01 | 03 |  0.0065 |  0.0048
00:00:10 01 | 04 |  0.0097 |  0.0050
00:00:12 01 | 05 |  0.0062 |  0.0029
00:00:14 01 | 06 |  0.0210 |  0.0187
00:00:16 01 | 07 |  0.0112 |  0.0070
00:00:18 01 | 08 |  0.0069 | -0.0174
00:00:20 01 | 09 |  0.0105 |  0.0010
00:00:22 01 | 10 |  0.0076 | -0.0178
00:00:25 01 | 11 |  0.0108 |  0.0048
00:00:27 01 | 12 |  0.0188 |  0.0072
00:00:29 01 | 13 |  0.0123 |  0.0056
00:00:31 01 | 14 |  0.0193 |  0.0226
00:00:34 01 | 15 |  0.0091 | -0.0033
00:00:36 01 | 16 |  0.0103 |  0.0042
00:00:38 01 | 17 |  0.0113 | -0.0129
00:00:40 01 | 18 |  0.0028 | -0.0040
00:00:42 01 | 19 |  0.0069 | -0.0005
00:00:45 01 | 20 |  0.0128 |  0.0028
00:00:48 02 | 01 |  0.0083 | -0.0045
00:00:50 02 | 02 |  0.0112 |  0.0159
00:00:52 02 | 03 | -0.0257 | -0.0194
00:00:55 02 | 04 |  0.0168 |  0.0007
00:00:57 02 | 05 | -0.0247 | -0.0209
00:00:59 02 | 06 |  0.0221 |  0.0214
00:01:01 02 | 07 | -0.0008 |  0.0083
00:01:03 02 | 08 |  0.0115 |  0.0138
00:01:06 02 | 09 |  0.0141 |  0.0011
00:01:08 02 | 10 | -0.0174 | -0.0168
00:01:10 02 | 11 | -0.0012 | -0.0039
00:01:12 02 | 12 |  0.0111 |  0.0119
00:01:14 02 | 13 |  0.0129 |  0.0040
00:01:16 02 | 14 |  0.0246 |  0.0210
00:01:19 02 | 15 |  0.0022 |  0.0078
00:01:21 02 | 16 |  0.0115 |  0.0080
00:01:23 02 | 17 |  0.0054 |  0.0030
00:01:25 02 | 18 |  0.0091 |  0.0268
00:01:27 02 | 19 |  0.0048 |  0.0063
00:01:29 02 | 20 |  0.0164 |  0.0343
00:01:32 03 | 01 | -0.0129 | -0.0402
00:01:35 03 | 02 |  0.0282 |  0.0190
00:01:37 03 | 03 | -0.0310 | -0.0329
00:01:39 03 | 04 | -0.0125 | -0.0104</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;d846c9da-cc50-4c38-be5a-af5776065247&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">00:01:41 03 | 05 | -0.0131 | -0.0277
00:01:43 03 | 06 |  0.0013 |  0.0070
00:01:45 03 | 07 |  0.0031 | -0.0105
00:01:48 03 | 08 | -0.0074 | -0.0317
00:01:50 03 | 09 | -0.0132 | -0.0282
00:01:52 03 | 10 |  0.0214 |  0.0028
00:01:54 03 | 11 |  0.0100 | -0.0143
00:01:56 03 | 12 |  0.0071 | -0.0010
00:01:58 03 | 13 |  0.0095 | -0.0003
00:02:01 03 | 14 |  0.0232 | -0.0008
00:02:03 03 | 15 |  0.0173 |  0.0076
00:02:05 03 | 16 |  0.0104 |  0.0079
00:02:07 03 | 17 |  0.0158 |  0.0099
00:02:09 03 | 18 |  0.0079 | -0.0137
00:02:11 03 | 19 |  0.0188 |  0.0224
00:02:14 03 | 20 |  0.0058 | -0.0059
00:02:17 04 | 01 | -0.0353 | -0.0357
00:02:19 04 | 02 | -0.0133 | -0.0431
00:02:21 04 | 03 | -0.0319 | -0.0311
00:02:23 04 | 04 |  0.0240 |  0.0281
00:02:25 04 | 05 | -0.0021 | -0.0103
00:02:28 04 | 06 |  0.0317 |  0.0100
00:02:30 04 | 07 |  0.0156 |  0.0127
00:02:32 04 | 08 |  0.0158 |  0.0134
00:02:34 04 | 09 |  0.0172 |  0.0064
00:02:36 04 | 10 |  0.0259 |  0.0211
00:02:38 04 | 11 |  0.0279 |  0.0222
00:02:41 04 | 12 | -0.0033 | -0.0127
00:02:43 04 | 13 |  0.0185 |  0.0101
00:02:45 04 | 14 |  0.0040 | -0.0213
00:02:47 04 | 15 |  0.0218 |  0.0255
00:02:49 04 | 16 |  0.0051 |  0.0002
00:02:52 04 | 17 | -0.0166 | -0.0166
00:02:54 04 | 18 | -0.0135 | -0.0000
00:02:56 04 | 19 | -0.0329 | -0.0113
00:02:58 04 | 20 |  0.0039 |  0.0197
00:03:01 05 | 01 |  0.0174 |  0.0100
00:03:03 05 | 02 | -0.0193 | -0.0045
00:03:05 05 | 03 | -0.0200 | -0.0242
00:03:08 05 | 04 | -0.0103 | -0.0178
00:03:10 05 | 05 |  0.0106 | -0.0077
00:03:12 05 | 06 |  0.0139 | -0.0004
00:03:14 05 | 07 |  0.0004 | -0.0003
00:03:17 05 | 08 |  0.0038 |  0.0054
00:03:19 05 | 09 |  0.0238 |  0.0283
00:03:21 05 | 10 |  0.0047 | -0.0066
00:03:23 05 | 11 |  0.0058 |  0.0136
00:03:25 05 | 12 |  0.0086 |  0.0045
00:03:27 05 | 13 | -0.0021 | -0.0103
00:03:29 05 | 14 |  0.0178 |  0.0116
00:03:32 05 | 15 |  0.0181 |  0.0060
00:03:34 05 | 16 |  0.0264 |  0.0176
00:03:36 05 | 17 |  0.0191 |  0.0152
00:03:38 05 | 18 |  0.0227 |  0.0240
00:03:40 05 | 19 |  0.0153 |  0.0065
00:03:42 05 | 20 |  0.0119 |  0.0123
00:03:45 06 | 01 |  0.0005 | -0.0013
00:03:48 06 | 02 | -0.0105 | -0.0045
00:03:50 06 | 03 |  0.0110 | -0.0014
00:03:52 06 | 04 |  0.0220 |  0.0271
00:03:54 06 | 05 |  0.0346 |  0.0244
00:03:56 06 | 06 |  0.0192 |  0.0194
00:03:59 06 | 07 |  0.0249 |  0.0390
00:04:01 06 | 08 |  0.0370 |  0.0326
00:04:03 06 | 09 |  0.0303 |  0.0135
00:04:06 06 | 10 |  0.0199 |  0.0095
00:04:08 06 | 11 | -0.0061 | -0.0105
00:04:10 06 | 12 |  0.0311 |  0.0291
00:04:12 06 | 13 |  0.0334 |  0.0244
00:04:14 06 | 14 | -0.0113 | -0.0108
00:04:17 06 | 15 |  0.0053 |  0.0115
00:04:19 06 | 16 |  0.0248 |  0.0220
00:04:21 06 | 17 |  0.0167 |  0.0035
00:04:23 06 | 18 |  0.0412 |  0.0229
00:04:26 06 | 19 | -0.0025 | -0.0114
00:04:28 06 | 20 | -0.0116 | -0.0185
00:04:31 07 | 01 |  0.0061 |  0.0110
00:04:33 07 | 02 |  0.0174 |  0.0162
00:04:35 07 | 03 |  0.0039 |  0.0135
00:04:37 07 | 04 |  0.0129 |  0.0089
00:04:40 07 | 05 |  0.0111 |  0.0309
00:04:42 07 | 06 |  0.0131 |  0.0317
00:04:44 07 | 07 |  0.0475 |  0.0360
00:04:46 07 | 08 |  0.0473 |  0.0397
00:04:48 07 | 09 |  0.0210 |  0.0627
00:04:50 07 | 10 |  0.0103 |  0.0081
00:04:53 07 | 11 |  0.0017 |  0.0130
00:04:55 07 | 12 | -0.0155 | -0.0254
00:04:57 07 | 13 |  0.0087 |  0.0336
00:04:59 07 | 14 |  0.0107 |  0.0253
00:05:01 07 | 15 |  0.0174 |  0.0456
00:05:04 07 | 16 |  0.0226 |  0.0169
00:05:06 07 | 17 |  0.0075 |  0.0182
00:05:08 07 | 18 |  0.0153 |  0.0233
00:05:10 07 | 19 |  0.0231 |  0.0205
00:05:13 07 | 20 |  0.0081 |  0.0174
00:05:16 08 | 01 | -0.0138 | -0.0005
00:05:18 08 | 02 | -0.0068 |  0.0015
00:05:20 08 | 03 |  0.0206 |  0.0195
00:05:22 08 | 04 |  0.0356 |  0.0332
00:05:25 08 | 05 |  0.0471 |  0.0431
00:05:27 08 | 06 |  0.0267 |  0.0205
00:05:29 08 | 07 |  0.0109 |  0.0417
00:05:31 08 | 08 |  0.0263 |  0.0466
00:05:33 08 | 09 |  0.0314 |  0.0102
00:05:36 08 | 10 |  0.0240 |  0.0020
00:05:38 08 | 11 |  0.0137 |  0.0488
00:05:40 08 | 12 |  0.0151 |  0.0087
00:05:42 08 | 13 |  0.0062 | -0.0116
00:05:44 08 | 14 |  0.0128 |  0.0079
00:05:47 08 | 15 |  0.0176 |  0.0200
00:05:49 08 | 16 |  0.0143 |  0.0152
00:05:51 08 | 17 |  0.0016 |  0.0051
00:05:53 08 | 18 |  0.0022 | -0.0037
00:05:55 08 | 19 |  0.0061 |  0.0090
00:05:58 08 | 20 |  0.0135 |  0.0118
00:06:01 09 | 01 |  0.0040 |  0.0041
00:06:03 09 | 02 |  0.0036 |  0.0074
00:06:05 09 | 03 |  0.0206 |  0.0236
00:06:07 09 | 04 |  0.0062 | -0.0020
00:06:09 09 | 05 | -0.0054 | -0.0076
00:06:11 09 | 06 |  0.0081 |  0.0127
00:06:14 09 | 07 | -0.0037 | -0.0050
00:06:16 09 | 08 |  0.0047 |  0.0024
00:06:18 09 | 09 |  0.0074 |  0.0107
00:06:20 09 | 10 |  0.0219 |  0.0196
00:06:22 09 | 11 |  0.0228 |  0.0261
00:06:25 09 | 12 |  0.0153 | -0.0051
00:06:27 09 | 13 | -0.0087 |  0.0007
00:06:29 09 | 14 |  0.0081 |  0.0020
00:06:31 09 | 15 |  0.0161 |  0.0231
00:06:33 09 | 16 |  0.0092 |  0.0074
00:06:35 09 | 17 |  0.0062 |  0.0079
00:06:38 09 | 18 |  0.0041 |  0.0043
00:06:40 09 | 19 |  0.0067 |  0.0004
00:06:42 09 | 20 |  0.0137 |  0.0100
00:06:45 10 | 01 | -0.0334 | -0.0243
00:06:47 10 | 02 |  0.0194 |  0.0276
00:06:49 10 | 03 |  0.0095 |  0.0006
00:06:51 10 | 04 | -0.0041 | -0.0241
00:06:54 10 | 05 |  0.0248 |  0.0152
00:06:56 10 | 06 | -0.0055 |  0.0047
00:06:58 10 | 07 |  0.0137 |  0.0194
00:07:00 10 | 08 |  0.0003 | -0.0086
00:07:02 10 | 09 | -0.0008 |  0.0190
00:07:04 10 | 10 |  0.0037 |  0.0173
00:07:07 10 | 11 |  0.0239 |  0.0259
00:07:09 10 | 12 |  0.0338 |  0.0270
00:07:11 10 | 13 |  0.0236 |  0.0243
00:07:13 10 | 14 |  0.0287 |  0.0207
00:07:15 10 | 15 |  0.0051 |  0.0063
00:07:17 10 | 16 |  0.0161 |  0.0083
00:07:20 10 | 17 | -0.0071 | -0.0157
00:07:22 10 | 18 |  0.0166 |  0.0257
00:07:24 10 | 19 | -0.0027 | -0.0091
00:07:26 10 | 20 |  0.0104 | -0.0006
00:07:30 11 | 01 | -0.0070 | -0.0096
00:07:32 11 | 02 |  0.0168 |  0.0177
00:07:34 11 | 03 | -0.0016 | -0.0087
00:07:36 11 | 04 |  0.0159 |  0.0196
00:07:38 11 | 05 |  0.0324 |  0.0391
00:07:41 11 | 06 |  0.0134 |  0.0140
00:07:43 11 | 07 |  0.0115 |  0.0131
00:07:45 11 | 08 |  0.0371 |  0.0356
00:07:47 11 | 09 |  0.0251 |  0.0271
00:07:49 11 | 10 |  0.0226 |  0.0161
00:07:52 11 | 11 |  0.0245 |  0.0104
00:07:54 11 | 12 |  0.0283 |  0.0286
00:07:56 11 | 13 |  0.0386 |  0.0297
00:07:58 11 | 14 |  0.0484 |  0.0496
00:08:00 11 | 15 |  0.0353 |  0.0310
00:08:02 11 | 16 |  0.0235 |  0.0221
00:08:05 11 | 17 |  0.0421 |  0.0405
00:08:07 11 | 18 |  0.0284 |  0.0288
00:08:09 11 | 19 |  0.0507 |  0.0487
00:08:11 11 | 20 |  0.0288 |  0.0258
00:08:14 12 | 01 | -0.0012 |  0.0064
00:08:16 12 | 02 | -0.0007 |  0.0053
00:08:19 12 | 03 |  0.0228 |  0.0323
00:08:21 12 | 04 |  0.0159 |  0.0157
00:08:23 12 | 05 |  0.0036 |  0.0006
00:08:25 12 | 06 |  0.0128 |  0.0156
00:08:27 12 | 07 |  0.0196 |  0.0248
00:08:30 12 | 08 |  0.0219 |  0.0271
00:08:32 12 | 09 |  0.0160 |  0.0170
00:08:34 12 | 10 |  0.0151 |  0.0096
00:08:36 12 | 11 |  0.0157 |  0.0084
00:08:38 12 | 12 |  0.0110 | -0.0009
00:08:41 12 | 13 |  0.0103 | -0.0064
00:08:43 12 | 14 |  0.0007 | -0.0168
00:08:45 12 | 15 |  0.0160 |  0.0065
00:08:47 12 | 16 |  0.0100 |  0.0009
00:08:49 12 | 17 | -0.0023 | -0.0056
00:08:51 12 | 18 |  0.0094 |  0.0013
00:08:54 12 | 19 | -0.0001 | -0.0059
00:08:56 12 | 20 |  0.0073 |  0.0062
(32, 32) tanh 0 64
00:00:07 01 | 01 | -0.0023 | -0.0138
00:00:13 01 | 02 |  0.0003 |  0.0059
00:00:19 01 | 03 |  0.0092 |  0.0141
00:00:25 01 | 04 |  0.0023 | -0.0016
00:00:31 01 | 05 |  0.0136 |  0.0106
00:00:37 01 | 06 |  0.0112 |  0.0023
00:00:42 01 | 07 |  0.0175 |  0.0081
00:00:48 01 | 08 |  0.0103 |  0.0028
00:00:54 01 | 09 |  0.0051 |  0.0002
00:01:00 01 | 10 |  0.0112 |  0.0032
00:01:06 01 | 11 |  0.0089 |  0.0045
00:01:12 01 | 12 |  0.0049 | -0.0071
00:01:18 01 | 13 | -0.0040 |  0.0177
00:01:24 01 | 14 |  0.0246 |  0.0147
00:01:30 01 | 15 |  0.0037 | -0.0047
00:01:36 01 | 16 |  0.0153 |  0.0265
00:01:42 01 | 17 |  0.0114 |  0.0101
00:01:48 01 | 18 |  0.0204 |  0.0189
00:01:54 01 | 19 |  0.0230 |  0.0304
00:01:60 01 | 20 |  0.0144 |  0.0101
00:02:06 02 | 01 |  0.0090 | -0.0207
00:02:12 02 | 02 |  0.0222 |  0.0238
00:02:18 02 | 03 | -0.0004 | -0.0096
00:02:24 02 | 04 | -0.0029 | -0.0077
00:02:30 02 | 05 |  0.0041 |  0.0154</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;5c3c4e48-d406-4456-8375-fdf8e20909a8&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">00:02:36 02 | 06 | -0.0062 | -0.0167
00:02:42 02 | 07 | -0.0194 | -0.0181
00:02:48 02 | 08 | -0.0152 | -0.0089
00:02:54 02 | 09 | -0.0065 | -0.0180
00:02:59 02 | 10 |  0.0106 |  0.0397
00:03:05 02 | 11 |  0.0212 |  0.0222
00:03:11 02 | 12 |  0.0081 |  0.0153
00:03:17 02 | 13 |  0.0193 |  0.0122
00:03:23 02 | 14 |  0.0144 |  0.0036
00:03:29 02 | 15 | -0.0005 | -0.0105
00:03:35 02 | 16 |  0.0034 |  0.0142
00:03:41 02 | 17 | -0.0177 | -0.0150
00:03:47 02 | 18 |  0.0096 |  0.0172
00:03:53 02 | 19 | -0.0067 | -0.0009
00:03:59 02 | 20 | -0.0010 |  0.0121
00:04:05 03 | 01 | -0.0117 | -0.0070
00:04:11 03 | 02 | -0.0014 | -0.0172
00:04:17 03 | 03 |  0.0023 | -0.0045
00:04:23 03 | 04 | -0.0042 |  0.0214
00:04:29 03 | 05 |  0.0256 |  0.0374
00:04:35 03 | 06 | -0.0171 | -0.0270
00:04:41 03 | 07 | -0.0006 | -0.0237
00:04:47 03 | 08 | -0.0018 | -0.0004
00:04:53 03 | 09 |  0.0044 | -0.0164
00:04:59 03 | 10 | -0.0268 | -0.0442
00:05:05 03 | 11 |  0.0264 |  0.0292
00:05:11 03 | 12 |  0.0107 |  0.0194
00:05:17 03 | 13 | -0.0029 |  0.0219
00:05:23 03 | 14 | -0.0082 | -0.0186
00:05:29 03 | 15 |  0.0067 |  0.0179
00:05:36 03 | 16 |  0.0184 | -0.0000
00:05:42 03 | 17 |  0.0090 |  0.0127
00:05:48 03 | 18 |  0.0133 |  0.0115
00:05:54 03 | 19 |  0.0308 |  0.0463
00:05:60 03 | 20 |  0.0172 |  0.0071
00:06:07 04 | 01 | -0.0334 | -0.0139
00:06:13 04 | 02 |  0.0143 |  0.0024
00:06:19 04 | 03 | -0.0033 | -0.0007
00:06:25 04 | 04 | -0.0308 | -0.0307
00:06:31 04 | 05 |  0.0098 | -0.0051
00:06:37 04 | 06 |  0.0105 | -0.0033
00:06:43 04 | 07 | -0.0046 | -0.0139
00:06:49 04 | 08 |  0.0265 |  0.0293
00:06:55 04 | 09 |  0.0072 | -0.0029
00:07:01 04 | 10 |  0.0326 |  0.0220
00:07:07 04 | 11 | -0.0312 | -0.0368
00:07:13 04 | 12 |  0.0038 | -0.0021
00:07:19 04 | 13 | -0.0017 | -0.0176
00:07:25 04 | 14 | -0.0067 | -0.0240
00:07:31 04 | 15 | -0.0177 | -0.0362
00:07:37 04 | 16 |  0.0184 |  0.0065
00:07:44 04 | 17 |  0.0059 | -0.0213
00:07:50 04 | 18 |  0.0050 |  0.0007
00:07:56 04 | 19 |  0.0346 |  0.0196
00:08:02 04 | 20 |  0.0283 |  0.0252
00:08:08 05 | 01 |  0.0101 |  0.0151
00:08:14 05 | 02 | -0.0053 | -0.0140
00:08:20 05 | 03 | -0.0341 | -0.0268
00:08:26 05 | 04 |  0.0240 |  0.0079
00:08:32 05 | 05 |  0.0195 |  0.0048
00:08:38 05 | 06 |  0.0057 |  0.0265
00:08:44 05 | 07 |  0.0095 |  0.0210
00:08:50 05 | 08 |  0.0484 |  0.0378
00:08:56 05 | 09 | -0.0103 | -0.0173
00:09:03 05 | 10 |  0.0167 |  0.0147
00:09:08 05 | 11 |  0.0147 |  0.0249
00:09:15 05 | 12 |  0.0148 |  0.0194
00:09:21 05 | 13 |  0.0242 |  0.0316
00:09:27 05 | 14 |  0.0017 | -0.0042
00:09:33 05 | 15 |  0.0352 |  0.0422
00:09:39 05 | 16 | -0.0132 | -0.0078
00:09:45 05 | 17 |  0.0008 |  0.0088
00:09:51 05 | 18 |  0.0413 |  0.0349
00:09:57 05 | 19 |  0.0243 |  0.0231
00:10:03 05 | 20 |  0.0101 |  0.0161
00:10:09 06 | 01 | -0.0287 | -0.0255
00:10:15 06 | 02 |  0.0028 | -0.0073
00:10:21 06 | 03 |  0.0332 |  0.0269
00:10:27 06 | 04 |  0.0000 |  0.0168
00:10:33 06 | 05 | -0.0033 |  0.0047
00:10:39 06 | 06 |  0.0321 |  0.0372
00:10:45 06 | 07 |  0.0111 |  0.0248
00:10:51 06 | 08 |  0.0341 |  0.0374
00:10:57 06 | 09 |  0.0257 |  0.0215
00:11:03 06 | 10 |  0.0272 |  0.0311
00:11:09 06 | 11 |  0.0233 |  0.0338
00:11:15 06 | 12 |  0.0075 | -0.0114
00:11:21 06 | 13 |  0.0293 |  0.0241
00:11:27 06 | 14 |  0.0217 |  0.0227
00:11:33 06 | 15 |  0.0091 |  0.0078
00:11:39 06 | 16 |  0.0204 |  0.0136
00:11:45 06 | 17 |  0.0354 |  0.0506
00:11:51 06 | 18 |  0.0209 |  0.0091
00:11:57 06 | 19 |  0.0094 | -0.0023
00:12:03 06 | 20 |  0.0259 |  0.0349
00:12:09 07 | 01 |  0.0267 |  0.0267
00:12:15 07 | 02 |  0.0052 |  0.0103
00:12:21 07 | 03 |  0.0154 |  0.0333
00:12:27 07 | 04 |  0.0394 |  0.0771
00:12:33 07 | 05 |  0.0259 |  0.0508
00:12:39 07 | 06 |  0.0304 |  0.0593
00:12:45 07 | 07 |  0.0092 |  0.0626
00:12:51 07 | 08 |  0.0325 |  0.0567
00:12:57 07 | 09 |  0.0312 |  0.0792
00:13:03 07 | 10 | -0.0013 |  0.0154
00:13:08 07 | 11 |  0.0174 |  0.0516
00:13:14 07 | 12 | -0.0171 |  0.0243
00:13:20 07 | 13 |  0.0163 |  0.0488
00:13:26 07 | 14 | -0.0032 |  0.0023
00:13:32 07 | 15 |  0.0226 |  0.0522
00:13:38 07 | 16 |  0.0066 |  0.0002
00:13:44 07 | 17 |  0.0339 |  0.0258
00:13:50 07 | 18 |  0.0150 |  0.0196
00:13:56 07 | 19 |  0.0279 |  0.0409
00:14:02 07 | 20 |  0.0182 |  0.0284
00:14:09 08 | 01 | -0.0033 |  0.0028
00:14:15 08 | 02 | -0.0073 | -0.0224
00:14:21 08 | 03 | -0.0156 | -0.0213
00:14:26 08 | 04 | -0.0098 |  0.0110
00:14:32 08 | 05 |  0.0191 |  0.0182
00:14:38 08 | 06 | -0.0017 | -0.0283
00:14:44 08 | 07 |  0.0045 |  0.0276
00:14:51 08 | 08 |  0.0012 |  0.0108
00:14:58 08 | 09 |  0.0134 |  0.0178
00:15:04 08 | 10 |  0.0189 |  0.0240
00:15:11 08 | 11 |  0.0256 |  0.0178
00:15:18 08 | 12 | -0.0183 | -0.0060
00:15:24 08 | 13 |  0.0013 |  0.0225
00:15:31 08 | 14 |  0.0032 |  0.0148
00:15:37 08 | 15 |  0.0009 |  0.0057
00:15:43 08 | 16 | -0.0031 |  0.0153
00:15:49 08 | 17 |  0.0191 |  0.0375
00:15:55 08 | 18 | -0.0004 |  0.0237
00:16:02 08 | 19 | -0.0027 |  0.0071
00:16:08 08 | 20 |  0.0320 |  0.0260
00:16:14 09 | 01 | -0.0228 | -0.0363
00:16:21 09 | 02 |  0.0112 | -0.0152
00:16:27 09 | 03 |  0.0044 | -0.0023
00:16:33 09 | 04 |  0.0040 | -0.0014
00:16:39 09 | 05 |  0.0009 | -0.0028
00:16:45 09 | 06 | -0.0109 | -0.0222
00:16:51 09 | 07 | -0.0211 | -0.0202
00:16:57 09 | 08 | -0.0207 | -0.0156
00:17:03 09 | 09 | -0.0115 | -0.0026
00:17:09 09 | 10 | -0.0005 |  0.0005
00:17:15 09 | 11 | -0.0114 | -0.0139
00:17:21 09 | 12 |  0.0102 |  0.0341
00:17:27 09 | 13 |  0.0129 |  0.0208
00:17:33 09 | 14 | -0.0083 |  0.0167
00:17:39 09 | 15 |  0.0040 | -0.0080
00:17:45 09 | 16 | -0.0006 |  0.0208
00:17:51 09 | 17 | -0.0055 |  0.0244
00:17:57 09 | 18 |  0.0033 |  0.0237
00:18:03 09 | 19 | -0.0093 |  0.0019
00:18:09 09 | 20 | -0.0052 |  0.0115
00:18:15 10 | 01 | -0.0161 | -0.0264
00:18:21 10 | 02 | -0.0229 | -0.0385
00:18:27 10 | 03 |  0.0154 |  0.0293
00:18:33 10 | 04 | -0.0052 |  0.0021
00:18:39 10 | 05 | -0.0033 |  0.0110
00:18:45 10 | 06 | -0.0073 | -0.0013
00:18:51 10 | 07 | -0.0112 | -0.0133
00:18:57 10 | 08 |  0.0233 |  0.0019
00:19:03 10 | 09 |  0.0229 |  0.0234
00:19:09 10 | 10 | -0.0091 |  0.0021
00:19:15 10 | 11 | -0.0097 | -0.0113
00:19:21 10 | 12 |  0.0119 |  0.0273
00:19:27 10 | 13 | -0.0044 | -0.0134
00:19:33 10 | 14 | -0.0011 | -0.0183
00:19:39 10 | 15 | -0.0018 |  0.0125
00:19:45 10 | 16 | -0.0096 | -0.0047
00:19:52 10 | 17 |  0.0057 | -0.0134
00:19:58 10 | 18 | -0.0098 |  0.0005
00:20:04 10 | 19 |  0.0040 |  0.0055
00:20:10 10 | 20 | -0.0169 | -0.0193
00:20:17 11 | 01 | -0.0014 |  0.0030
00:20:23 11 | 02 |  0.0296 |  0.0353
00:20:29 11 | 03 | -0.0034 | -0.0041
00:20:35 11 | 04 |  0.0125 |  0.0126
00:20:41 11 | 05 |  0.0308 |  0.0263
00:20:47 11 | 06 |  0.0152 |  0.0151
00:20:53 11 | 07 |  0.0362 |  0.0390
00:20:59 11 | 08 |  0.0169 |  0.0162
00:21:05 11 | 09 |  0.0243 |  0.0143
00:21:11 11 | 10 |  0.0327 |  0.0355
00:21:17 11 | 11 |  0.0274 |  0.0330
00:21:23 11 | 12 |  0.0392 |  0.0305
00:21:29 11 | 13 |  0.0360 |  0.0247
00:21:35 11 | 14 |  0.0411 |  0.0376
00:21:41 11 | 15 |  0.0421 |  0.0466
00:21:47 11 | 16 |  0.0258 |  0.0184
00:21:53 11 | 17 |  0.0193 |  0.0200
00:21:59 11 | 18 |  0.0202 |  0.0088
00:22:05 11 | 19 |  0.0132 |  0.0149
00:22:11 11 | 20 |  0.0334 |  0.0155
00:22:18 12 | 01 |  0.0105 |  0.0194
00:22:24 12 | 02 |  0.0152 | -0.0047
00:22:30 12 | 03 | -0.0019 |  0.0172
00:22:35 12 | 04 |  0.0073 | -0.0032
00:22:41 12 | 05 |  0.0061 | -0.0114
00:22:47 12 | 06 |  0.0030 |  0.0067
00:22:53 12 | 07 |  0.0139 | -0.0075
00:22:59 12 | 08 |  0.0088 |  0.0095
00:23:05 12 | 09 |  0.0046 | -0.0136
00:23:11 12 | 10 | -0.0078 | -0.0210
00:23:17 12 | 11 |  0.0039 | -0.0171
00:23:23 12 | 12 | -0.0083 | -0.0181
00:23:29 12 | 13 | -0.0067 | -0.0116
00:23:35 12 | 14 | -0.0111 | -0.0215
00:23:41 12 | 15 | -0.0053 | -0.0199
00:23:47 12 | 16 | -0.0099 | -0.0211
00:23:53 12 | 17 | -0.0072 | -0.0216
00:23:59 12 | 18 | -0.0134 | -0.0233
00:24:05 12 | 19 | -0.0006 | -0.0063
00:24:11 12 | 20 |  0.0001 | -0.0167
(32, 32) tanh 0 256
00:00:03 01 | 01 | -0.0102 | -0.0139
00:00:06 01 | 02 |  0.0186 |  0.0107
00:00:08 01 | 03 |  0.0283 |  0.0531
00:00:11 01 | 04 | -0.0022 |  0.0015
00:00:13 01 | 05 |  0.0160 |  0.0201
00:00:16 01 | 06 |  0.0140 |  0.0215</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;fb5e0f67-6a0d-4f81-87ea-c82147dde3f0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">00:00:18 01 | 07 |  0.0137 |  0.0305
00:00:21 01 | 08 |  0.0111 |  0.0028
00:00:23 01 | 09 |  0.0115 |  0.0106
00:00:25 01 | 10 |  0.0101 |  0.0103
00:00:28 01 | 11 |  0.0075 |  0.0044
00:00:30 01 | 12 |  0.0021 |  0.0034
00:00:32 01 | 13 |  0.0124 | -0.0015
00:00:35 01 | 14 |  0.0171 |  0.0115
00:00:37 01 | 15 |  0.0168 |  0.0117
00:00:40 01 | 16 |  0.0094 |  0.0082
00:00:42 01 | 17 |  0.0104 |  0.0048
00:00:45 01 | 18 |  0.0094 |  0.0011
00:00:47 01 | 19 |  0.0051 |  0.0091
00:00:49 01 | 20 | -0.0021 |  0.0068
00:00:52 02 | 01 |  0.0242 |  0.0238
00:00:55 02 | 02 |  0.0233 |  0.0281
00:00:57 02 | 03 |  0.0170 |  0.0232
00:00:60 02 | 04 |  0.0042 |  0.0118
00:01:02 02 | 05 |  0.0261 |  0.0176
00:01:04 02 | 06 | -0.0113 |  0.0045
00:01:07 02 | 07 |  0.0018 | -0.0062
00:01:09 02 | 08 |  0.0060 |  0.0100
00:01:12 02 | 09 |  0.0041 |  0.0021
00:01:14 02 | 10 |  0.0097 |  0.0186
00:01:16 02 | 11 |  0.0096 |  0.0071
00:01:19 02 | 12 |  0.0037 | -0.0055
00:01:21 02 | 13 |  0.0117 |  0.0188
00:01:23 02 | 14 |  0.0085 |  0.0198
00:01:26 02 | 15 |  0.0084 |  0.0141
00:01:28 02 | 16 |  0.0053 |  0.0162
00:01:31 02 | 17 |  0.0142 |  0.0135
00:01:33 02 | 18 |  0.0012 |  0.0007
00:01:35 02 | 19 |  0.0050 |  0.0011
00:01:38 02 | 20 | -0.0077 |  0.0030
00:01:41 03 | 01 | -0.0163 | -0.0229
00:01:43 03 | 02 |  0.0056 |  0.0071
00:01:46 03 | 03 |  0.0020 |  0.0158
00:01:48 03 | 04 |  0.0257 |  0.0393
00:01:51 03 | 05 | -0.0174 | -0.0249
00:01:53 03 | 06 | -0.0079 | -0.0122
00:01:55 03 | 07 | -0.0291 | -0.0372
00:01:58 03 | 08 | -0.0032 | -0.0036
00:02:00 03 | 09 | -0.0051 | -0.0146
00:02:03 03 | 10 | -0.0077 | -0.0082
00:02:05 03 | 11 | -0.0247 | -0.0497
00:02:07 03 | 12 | -0.0205 | -0.0311
00:02:10 03 | 13 | -0.0217 | -0.0436
00:02:12 03 | 14 | -0.0013 | -0.0245
00:02:15 03 | 15 |  0.0100 | -0.0085
00:02:17 03 | 16 |  0.0068 | -0.0006
00:02:19 03 | 17 |  0.0026 | -0.0116
00:02:22 03 | 18 | -0.0054 |  0.0003
00:02:24 03 | 19 |  0.0070 | -0.0115
00:02:26 03 | 20 | -0.0016 | -0.0110
00:02:30 04 | 01 | -0.0152 | -0.0056
00:02:32 04 | 02 |  0.0226 |  0.0162
00:02:34 04 | 03 | -0.0118 |  0.0097
00:02:37 04 | 04 | -0.0289 | -0.0417
00:02:39 04 | 05 |  0.0123 |  0.0110
00:02:42 04 | 06 | -0.0007 | -0.0228
00:02:44 04 | 07 |  0.0082 | -0.0054
00:02:46 04 | 08 |  0.0075 | -0.0093
00:02:49 04 | 09 | -0.0024 | -0.0160
00:02:51 04 | 10 | -0.0117 | -0.0331
00:02:54 04 | 11 | -0.0083 | -0.0162
00:02:56 04 | 12 |  0.0078 | -0.0241
00:02:58 04 | 13 | -0.0134 | -0.0170
00:03:01 04 | 14 | -0.0174 | -0.0020
00:03:03 04 | 15 | -0.0028 | -0.0058
00:03:06 04 | 16 |  0.0142 |  0.0137
00:03:08 04 | 17 |  0.0063 | -0.0190
00:03:10 04 | 18 | -0.0089 | -0.0052
00:03:13 04 | 19 | -0.0117 | -0.0049
00:03:15 04 | 20 |  0.0195 |  0.0099
00:03:18 05 | 01 | -0.0176 | -0.0065
00:03:21 05 | 02 |  0.0275 |  0.0401
00:03:23 05 | 03 |  0.0006 |  0.0167
00:03:25 05 | 04 | -0.0015 | -0.0029
00:03:28 05 | 05 |  0.0016 | -0.0228
00:03:30 05 | 06 |  0.0126 | -0.0137
00:03:32 05 | 07 |  0.0193 |  0.0197
00:03:35 05 | 08 |  0.0239 |  0.0171
00:03:37 05 | 09 |  0.0206 |  0.0192
00:03:40 05 | 10 |  0.0087 | -0.0165
00:03:42 05 | 11 |  0.0211 |  0.0196
00:03:44 05 | 12 | -0.0015 |  0.0116
00:03:47 05 | 13 | -0.0034 | -0.0097
00:03:49 05 | 14 |  0.0110 |  0.0073
00:03:52 05 | 15 |  0.0117 | -0.0078
00:03:54 05 | 16 |  0.0168 |  0.0228
00:03:56 05 | 17 | -0.0152 | -0.0069
00:03:59 05 | 18 |  0.0064 |  0.0018
00:04:01 05 | 19 | -0.0079 | -0.0011
00:04:04 05 | 20 |  0.0037 |  0.0180
00:04:07 06 | 01 | -0.0003 | -0.0042
00:04:10 06 | 02 |  0.0100 | -0.0070
00:04:12 06 | 03 |  0.0292 |  0.0109
00:04:15 06 | 04 |  0.0108 |  0.0161
00:04:17 06 | 05 |  0.0381 |  0.0267
00:04:20 06 | 06 |  0.0299 |  0.0195
00:04:22 06 | 07 |  0.0379 |  0.0434
00:04:24 06 | 08 |  0.0212 |  0.0126
00:04:27 06 | 09 |  0.0451 |  0.0352
00:04:29 06 | 10 |  0.0268 |  0.0333
00:04:32 06 | 11 |  0.0325 |  0.0332
00:04:34 06 | 12 |  0.0277 |  0.0122
00:04:37 06 | 13 |  0.0435 |  0.0184
00:04:39 06 | 14 |  0.0433 |  0.0273
00:04:41 06 | 15 |  0.0246 |  0.0100
00:04:44 06 | 16 |  0.0142 |  0.0042
00:04:46 06 | 17 |  0.0293 |  0.0066
00:04:49 06 | 18 |  0.0160 |  0.0155
00:04:51 06 | 19 |  0.0180 |  0.0052
00:04:53 06 | 20 |  0.0210 |  0.0180
00:04:57 07 | 01 | -0.0167 | -0.0028
00:04:59 07 | 02 |  0.0523 |  0.0424
00:05:01 07 | 03 | -0.0098 |  0.0001
00:05:04 07 | 04 |  0.0187 |  0.0319
00:05:06 07 | 05 | -0.0038 |  0.0125
00:05:09 07 | 06 |  0.0106 |  0.0128
00:05:11 07 | 07 |  0.0336 |  0.0516
00:05:13 07 | 08 |  0.0143 |  0.0186
00:05:16 07 | 09 | -0.0031 |  0.0203
00:05:18 07 | 10 | -0.0070 |  0.0104
00:05:21 07 | 11 |  0.0150 |  0.0250
00:05:23 07 | 12 |  0.0194 |  0.0053
00:05:25 07 | 13 |  0.0061 | -0.0206
00:05:28 07 | 14 | -0.0053 | -0.0192
00:05:30 07 | 15 |  0.0034 |  0.0109
00:05:33 07 | 16 |  0.0092 |  0.0219
00:05:35 07 | 17 |  0.0028 |  0.0122
00:05:37 07 | 18 | -0.0051 | -0.0277
00:05:40 07 | 19 | -0.0061 | -0.0051
00:05:42 07 | 20 | -0.0221 | -0.0308
00:05:45 08 | 01 |  0.0115 |  0.0182
00:05:48 08 | 02 |  0.0118 | -0.0033
00:05:50 08 | 03 |  0.0132 |  0.0171
00:05:52 08 | 04 | -0.0075 | -0.0119
00:05:55 08 | 05 |  0.0139 |  0.0069
00:05:57 08 | 06 |  0.0004 |  0.0049
00:05:59 08 | 07 |  0.0189 |  0.0241
00:06:02 08 | 08 |  0.0214 |  0.0077
00:06:04 08 | 09 | -0.0008 |  0.0107
00:06:07 08 | 10 | -0.0049 | -0.0017
00:06:09 08 | 11 |  0.0048 |  0.0217
00:06:11 08 | 12 |  0.0109 | -0.0014
00:06:14 08 | 13 |  0.0059 |  0.0211
00:06:16 08 | 14 | -0.0021 | -0.0148
00:06:19 08 | 15 |  0.0073 |  0.0110
00:06:21 08 | 16 |  0.0120 | -0.0035
00:06:23 08 | 17 |  0.0046 |  0.0083
00:06:26 08 | 18 |  0.0219 |  0.0105
00:06:28 08 | 19 |  0.0136 |  0.0310
00:06:31 08 | 20 |  0.0222 |  0.0354
00:06:34 09 | 01 | -0.0044 |  0.0053
00:06:36 09 | 02 |  0.0130 |  0.0052
00:06:39 09 | 03 |  0.0140 |  0.0044
00:06:41 09 | 04 | -0.0022 | -0.0083
00:06:44 09 | 05 |  0.0111 |  0.0155
00:06:46 09 | 06 |  0.0012 |  0.0028
00:06:48 09 | 07 |  0.0059 |  0.0080
00:06:51 09 | 08 | -0.0109 | -0.0172
00:06:53 09 | 09 | -0.0013 | -0.0090
00:06:56 09 | 10 |  0.0156 |  0.0249
00:06:58 09 | 11 |  0.0130 |  0.0193
00:07:00 09 | 12 |  0.0097 |  0.0064
00:07:03 09 | 13 |  0.0088 |  0.0252
00:07:05 09 | 14 |  0.0079 |  0.0114
00:07:07 09 | 15 |  0.0046 | -0.0046
00:07:10 09 | 16 |  0.0086 |  0.0115
00:07:12 09 | 17 |  0.0076 | -0.0130
00:07:15 09 | 18 | -0.0022 | -0.0224
00:07:17 09 | 19 |  0.0056 |  0.0077
00:07:20 09 | 20 |  0.0065 | -0.0040
00:07:23 10 | 01 | -0.0391 | -0.0545
00:07:25 10 | 02 |  0.0447 |  0.0487
00:07:28 10 | 03 |  0.0178 |  0.0177
00:07:30 10 | 04 |  0.0007 |  0.0198
00:07:32 10 | 05 | -0.0058 |  0.0076
00:07:35 10 | 06 | -0.0236 | -0.0132
00:07:37 10 | 07 | -0.0146 |  0.0126
00:07:40 10 | 08 | -0.0108 | -0.0214
00:07:42 10 | 09 | -0.0089 | -0.0087
00:07:45 10 | 10 | -0.0055 | -0.0125
00:07:47 10 | 11 | -0.0253 | -0.0222
00:07:49 10 | 12 | -0.0264 | -0.0329
00:07:52 10 | 13 | -0.0177 | -0.0297
00:07:54 10 | 14 | -0.0157 | -0.0101
00:07:56 10 | 15 | -0.0039 | -0.0110
00:07:59 10 | 16 |  0.0019 | -0.0047
00:08:01 10 | 17 |  0.0028 |  0.0060
00:08:04 10 | 18 |  0.0062 | -0.0258
00:08:06 10 | 19 | -0.0166 | -0.0021
00:08:08 10 | 20 | -0.0038 | -0.0053
00:08:12 11 | 01 |  0.0153 |  0.0116
00:08:14 11 | 02 |  0.0248 |  0.0413
00:08:17 11 | 03 |  0.0279 |  0.0308
00:08:19 11 | 04 |  0.0166 |  0.0121
00:08:22 11 | 05 |  0.0241 |  0.0256
00:08:24 11 | 06 |  0.0203 |  0.0262
00:08:26 11 | 07 |  0.0092 |  0.0114
00:08:29 11 | 08 |  0.0189 |  0.0245
00:08:31 11 | 09 |  0.0152 |  0.0048
00:08:34 11 | 10 |  0.0200 |  0.0208
00:08:36 11 | 11 |  0.0215 |  0.0200
00:08:38 11 | 12 |  0.0259 |  0.0247
00:08:41 11 | 13 |  0.0263 |  0.0329
00:08:43 11 | 14 |  0.0368 |  0.0379
00:08:46 11 | 15 |  0.0297 |  0.0327
00:08:48 11 | 16 |  0.0361 |  0.0294
00:08:51 11 | 17 |  0.0270 |  0.0237
00:08:53 11 | 18 |  0.0299 |  0.0347
00:08:55 11 | 19 |  0.0281 |  0.0325
00:08:58 11 | 20 |  0.0354 |  0.0332
00:09:01 12 | 01 |  0.0088 | -0.0081
00:09:04 12 | 02 |  0.0188 |  0.0099
00:09:06 12 | 03 |  0.0018 | -0.0031
00:09:08 12 | 04 | -0.0013 | -0.0201
00:09:11 12 | 05 |  0.0034 | -0.0094
00:09:13 12 | 06 |  0.0084 | -0.0028
00:09:15 12 | 07 |  0.0128 |  0.0050
00:09:18 12 | 08 |  0.0039 | -0.0042</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;29114711-da20-464a-8b4f-462962f942b9&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">00:09:20 12 | 09 |  0.0287 |  0.0243
00:09:23 12 | 10 |  0.0148 |  0.0192
00:09:25 12 | 11 |  0.0071 |  0.0016
00:09:27 12 | 12 |  0.0144 |  0.0122
00:09:30 12 | 13 |  0.0127 |  0.0070
00:09:32 12 | 14 |  0.0063 |  0.0105
00:09:35 12 | 15 |  0.0123 |  0.0046
00:09:37 12 | 16 |  0.0040 | -0.0115
00:09:39 12 | 17 |  0.0109 | -0.0022
00:09:42 12 | 18 |  0.0189 |  0.0066
00:09:44 12 | 19 |  0.0207 |  0.0036
00:09:46 12 | 20 |  0.0159 |  0.0007</code></pre></div><p>The purpose here is to run the full training-and-evaluation loop for a whole grid of neural network settings and keep track of how well each version predicts next-day returns. It starts by creating an empty container for the cross-validation results and a standard scaler object that will be reused to normalize the features within each fold. From there, it steps through every combination of network shape, activation, and dropout setting, and then tries each of those again with two different batch sizes. The print statement at the top of each pass is what produces the first line of output showing the current architecture and training settings, such as the layer sizes, activation, dropout rate, and batch size.</p><p>For every configuration, a checkpoint folder is assembled from those settings so that the trained weights for that exact run can be saved in an organized way. If that folder does not already exist, it is created before training begins. The timer is then started so the cell can report how long the process has been running as it moves through folds and epochs.</p><p>The next nested loop walks through the time-series cross-validation splits. For each fold, the training and validation rows are pulled out of the feature matrix and target series. The features are scaled using only the training data, which is important because the validation period should not influence the scaling parameters. The same scaler is then applied to the validation set so both splits are on the same scale. After that, two small data frames are prepared: one to hold the actual validation returns alongside the model&#8217;s predictions, and another to store the daily Spearman rank correlations for each epoch. That rank correlation is the key metric here because the goal is not just to predict exact return values, but to get the cross-sectional ordering of assets right on each date.</p><p>A fresh model matching the current hyperparameters is built for the fold, and then training proceeds one epoch at a time for 20 epochs. Training one epoch at a time gives the notebook a chance to inspect performance after every pass through the data instead of only at the end. After each epoch, the model weights are saved to the checkpoint directory, predictions are generated on the validation fold, and the daily correlations between predicted and actual returns are calculated. The printed lines that follow are the live progress report from this process. Each line shows elapsed time, fold number, epoch number, the mean daily IC for that epoch, and the median daily IC. That is why the output is a long sequence of similar-looking lines: the code is repeatedly evaluating the model after every epoch across many folds and parameter combinations, so the notebook is surfacing a running summary of model quality as it learns.</p><p>After all epochs in a fold are finished, the daily correlation results for that fold are tagged with the model settings and stored in the results list. Once all folds for a configuration are complete, the accumulated scores are written out to an HDF5 file. The saved output is therefore the live trace of a large grid search, and the mix of positive and negative values reflects that some combinations of hyperparameters produce slightly better rank-order predictions than others, while many are only weakly predictive or unstable across folds.</p><h3>Assess predictive performance</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;97c8d0c2-9ae7-4579-a455-7f0b653734b2&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">params = ['dense_layers', 'dropout', 'batch_size']</code></pre></div><p>This cell simply defines which model settings will be treated as the main tuning variables in the later analysis. By putting dense layer structure, dropout rate, and batch size into a single list, it creates a compact way to refer to the architectural and training choices that were explored during model search. Nothing is calculated or displayed yet, but this small assignment is important because it gives the rest of the notebook a consistent set of parameter names to work with when results are grouped, summarized, or compared across experiments.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;3508867b-2162-46c6-8c83-52f6e93d0550&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">ic = pd.read_hdf(results_path / 'scores.h5', 'ic_by_day').drop('activation', axis=1)
ic.info()</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;ef62c328-70cc-408a-8140-5a0d1c947445&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">&lt;class 'pandas.core.frame.DataFrame'&gt;
DatetimeIndex: 18144 entries, 2017-08-30 to 2015-03-02
Data columns (total 24 columns):
 #   Column        Non-Null Count  Dtype  
---  ------        --------------  -----  
 0   0             18144 non-null  float64
 1   1             18144 non-null  float64
 2   2             18144 non-null  float64
 3   3             18144 non-null  float64
 4   4             18144 non-null  float64
 5   5             18144 non-null  float64
 6   6             18144 non-null  float64
 7   7             18144 non-null  float64
 8   8             18144 non-null  float64
 9   9             18144 non-null  float64
 10  10            18144 non-null  float64
 11  11            18144 non-null  float64
 12  12            18144 non-null  float64
 13  13            18144 non-null  float64
 14  14            18144 non-null  float64
 15  15            18144 non-null  float64
 16  16            18144 non-null  float64
 17  17            18144 non-null  float64
 18  18            18144 non-null  float64
 19  19            18144 non-null  float64
 20  dense_layers  18144 non-null  object 
 21  dropout       18144 non-null  float64
 22  batch_size    18144 non-null  int64  
 23  fold          18144 non-null  int64  
dtypes: float64(21), int64(2), object(1)
memory usage: 3.5+ MB</code></pre></div><p>The goal here is to reload the saved daily information-coefficient results from disk and inspect their structure before doing any further analysis. The file being read contains the model evaluation history, organized one row per day of validation performance, and the column labeled activation is removed right away because it is no longer needed for the next steps. That leaves a table focused on the remaining model settings and the daily IC values themselves.</p><p>The information summary shows that the result is a pandas DataFrame indexed by dates, with 18,144 rows in total. The dates run from 2015-03-02 up to 2017-08-30, which fits the rolling backtest-style evaluation used earlier. There are 24 columns after dropping activation. Most of them are numeric columns named 0 through 19, which likely hold the IC values for different epochs or repeated runs captured for each day. Alongside those are the descriptive fields dense<em>layers, dropout, batch</em>size, and fold, which identify the model configuration and cross-validation split associated with each record. The memory report and non-null counts confirm that the table is complete, with no missing values in any of the displayed columns.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;db38dfb9-2e46-4d7e-b761-69699107a456&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">ic.groupby(params).size()</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;999cccaf-b9c1-4649-a7b1-a120c25c122a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">dense_layers  dropout  batch_size
(16, 8)       0.0      64            756
                       256           756
              0.1      64            756
                       256           756
              0.2      64            756
                       256           756
(32, 16)      0.0      64            756
                       256           756
              0.1      64            756
                       256           756
              0.2      64            756
                       256           756
(32, 32)      0.0      64            756
                       256           756
              0.1      64            756
                       256           756
              0.2      64            756
                       256           756
(64, 32)      0.0      64            756
                       256           756
              0.1      64            756
                       256           756
              0.2      64            756
                       256           756
dtype: int64</code></pre></div><p>The purpose here is to check how many observations were collected for each combination of model settings. The data being summarized, called ic, contains performance measurements for many training runs, and params identifies the hyperparameters attached to each run, such as the layer sizes, dropout rate, and batch size. Grouping by those parameter columns and counting the size of each group gives a quick way to confirm whether every configuration was evaluated the same number of times.</p><p>The output shows a tidy multi-level index made up of dense<em>layers, dropout, and batch</em>size, with a count beside each combination. Every row has the same value, 756, which means each architecture and training setup appears exactly 756 times in the results. That uniform count is what you would expect if the experiment was run consistently across the same number of cross-validation folds, epochs, and saved prediction records for every parameter choice. Seeing the same total repeated for all combinations is a useful sanity check: it suggests the collection of IC values is balanced and that no configuration is missing runs or has extra entries.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;0cfb8cc4-5218-47be-88d2-c0d7b445e7f2&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">ic_long = pd.melt(ic, id_vars=params + ['fold'], var_name='epoch', value_name='ic')
ic_long.info()</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;d695abb6-f5c8-4d85-83c9-fcf3f305b094&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">&lt;class 'pandas.core.frame.DataFrame'&gt;
RangeIndex: 362880 entries, 0 to 362879
Data columns (total 6 columns):
 #   Column        Non-Null Count   Dtype  
---  ------        --------------   -----  
 0   dense_layers  362880 non-null  object 
 1   dropout       362880 non-null  float64
 2   batch_size    362880 non-null  int64  
 3   fold          362880 non-null  int64  
 4   epoch         362880 non-null  object 
 5   ic            362880 non-null  float64
dtypes: float64(2), int64(2), object(2)
memory usage: 16.6+ MB</code></pre></div><p>The goal here is to reshape the performance results into a more analysis-friendly format. Up to this point, the scores are stored in a wide table, where each epoch has its own separate column. That arrangement is convenient for collecting results, but not ideal for summarizing, plotting, or fitting statistical models, because the epoch values are spread across many columns instead of being treated as one variable.</p><p>The first step converts that wide table into a long format by keeping the model-setting columns and the fold identifier fixed, while gathering all of the epoch columns into a single column named epoch. The corresponding values from those epoch columns are stacked into a new column named ic, which holds the information coefficient for each model, fold, and epoch combination. Behind the scenes, this creates one row per result observation rather than one row per experiment with many epoch fields, which makes later grouping and comparisons much easier.</p><p>The information output confirms exactly that transformation. The resulting table has 362,880 rows and 6 columns: the configuration descriptors dense<em>layers, dropout, and batch</em>size, the fold number, the epoch label, and the ic value itself. The non-null counts show that every row is complete, which means the reshaping preserved all results without introducing missing values. The data types also make sense for the new structure: the layer configuration and epoch label are stored as objects, while dropout and ic are floating-point numbers and batch_size and fold are integers. The memory usage is larger than a tiny summary table, but still manageable for downstream analysis, and the long layout is what enables the notebook to compare performance across epochs and model settings cleanly.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;a1969650-6a4f-4119-8c8e-35f8ee38bffc&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">ic_long = ic_long.groupby(params+ ['epoch', 'fold']).ic.mean().to_frame('ic').reset_index()</code></pre></div><p>The goal here is to collapse the detailed per-day results into a simpler summary that can be analyzed more easily. The grouped data already contains many individual information-coefficient values for each model configuration, epoch, and fold, so the first step is to combine all rows that share the same parameter settings and training stage. By grouping on the model parameters together with the epoch and fold, the calculation takes the mean of the IC values within each group. That produces one representative IC score for every unique combination of hyperparameters, epoch, and cross-validation fold.</p><p>After the averaging is done, the result is turned back into a regular table with a single IC column. The temporary grouped structure is converted into a DataFrame, and the index is reset so that the grouping variables become normal columns again instead of being hidden in the index. The final object is therefore a cleaner, flattened summary table where each row corresponds to one model configuration at one epoch on one fold, paired with its average information coefficient. Because the cell only reshapes and summarizes existing data, there is no displayed output here; it simply prepares the dataset for later comparison, plotting, or statistical analysis.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;a267588e-2488-4e6e-b5ef-7d70b5a616ec&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">g = sns.relplot(x='epoch', y='ic', col='dense_layers', row='dropout', 
                data=ic_long[ic_long.dropout&gt;0], kind='line')
g.map(plt.axhline, y=0, ls='--', c='k', lw=1)
g.savefig(results_path / 'ic_lineplot', dpi=300);</code></pre></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!tbZv!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa168d59f-224e-48f8-a03b-46dda4479bb7_1432x712.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!tbZv!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa168d59f-224e-48f8-a03b-46dda4479bb7_1432x712.png 424w, https://substackcdn.com/image/fetch/$s_!tbZv!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa168d59f-224e-48f8-a03b-46dda4479bb7_1432x712.png 848w, https://substackcdn.com/image/fetch/$s_!tbZv!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa168d59f-224e-48f8-a03b-46dda4479bb7_1432x712.png 1272w, https://substackcdn.com/image/fetch/$s_!tbZv!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa168d59f-224e-48f8-a03b-46dda4479bb7_1432x712.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!tbZv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa168d59f-224e-48f8-a03b-46dda4479bb7_1432x712.png" width="1432" height="712" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a168d59f-224e-48f8-a03b-46dda4479bb7_1432x712.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:712,&quot;width&quot;:1432,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:173269,&quot;alt&quot;:&quot;Output image&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/202847809?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa168d59f-224e-48f8-a03b-46dda4479bb7_1432x712.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Output image" title="Output image" srcset="https://substackcdn.com/image/fetch/$s_!tbZv!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa168d59f-224e-48f8-a03b-46dda4479bb7_1432x712.png 424w, https://substackcdn.com/image/fetch/$s_!tbZv!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa168d59f-224e-48f8-a03b-46dda4479bb7_1432x712.png 848w, https://substackcdn.com/image/fetch/$s_!tbZv!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa168d59f-224e-48f8-a03b-46dda4479bb7_1432x712.png 1272w, https://substackcdn.com/image/fetch/$s_!tbZv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa168d59f-224e-48f8-a03b-46dda4479bb7_1432x712.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The purpose here is to visualize how model performance changes over training time for the different neural network settings that were tested. It takes the long-form results table, keeps only the runs with nonzero dropout, and then draws a line plot of IC against epoch. The figure is split into a grid so that each column corresponds to one dense-layer architecture and each row corresponds to one dropout level. That makes it easy to compare how the same training pattern behaves under different model sizes and regularization strengths.</p>
      <p>
          <a href="https://onepagecode.substack.com/p/deep-learning-for-quant-trading-training">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Linear Regression for Quant Trading: Building and Evaluating Predictive Models]]></title><description><![CDATA[An introduction to formulating, training, and diagnosing linear regression models using Python]]></description><link>https://onepagecode.substack.com/p/linear-regression-for-quant-trading</link><guid isPermaLink="false">https://onepagecode.substack.com/p/linear-regression-for-quant-trading</guid><dc:creator><![CDATA[Onepagecode]]></dc:creator><pubDate>Tue, 23 Jun 2026 20:01:02 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!IYHq!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f0c0f90-ae63-47d5-805c-3f61e911dbc3_1000x424.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h4>Download the source using the URL at the end of this article!</h4><h2>Introduction to Linear Regression</h2><p>Linear regression is a way to connect a continuous output, also called the response or dependent variable, with one or more input variables, often called predictors, features, or independent variables. It is built on the idea that the relationship has a linear form.</p><ul><li><p>If the other features are held fixed, the effect of any one feature on the response follows a straight-line pattern.</p></li><li><p>The slope for that feature stays the same regardless of the values taken by the remaining variables.</p></li><li><p>Each feature contributes its own effect to the response, and those effects are combined additively, although interaction terms can also be introduced when needed.</p></li></ul><p>Put differently, linear regression treats the response as something that can be described or estimated from a linear combination of the features, with the remaining difference attributed to random variation away from that pattern.</p><h2>Imports and settings</h2><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;800f4e50-5d4c-48d6-aff8-0b9485f62454&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">import warnings
warnings.filterwarnings('ignore')</code></pre></div><p>This step simply turns off warning messages for the rest of the session. Python libraries often emit warnings when something is deprecated, potentially unstable, or not quite ideal, and those messages can clutter the notebook without stopping the code from running. By setting the warning filter to ignore, the notebook keeps the display cleaner so the focus stays on the regression examples and results that come later. Since there is no saved output, nothing is printed here; the effect is purely behind the scenes, changing how warnings will be handled when later cells are executed.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;290865b5-386e-4d4e-bc35-c2a2265763c4&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">%matplotlib inline

import numpy as np
import pandas as pd

import matplotlib.pyplot as plt
import seaborn as sns

import statsmodels.api as sm
from sklearn.linear_model import SGDRegressor
from sklearn.preprocessing import StandardScaler</code></pre></div><p>This cell sets up the main tools needed for the rest of the notebook. The first line tells Jupyter to show plots directly inside the notebook rather than opening them in a separate window, which is why later visualizations will appear inline beneath the code that creates them. After that, the common data analysis libraries are loaded: one for numerical arrays, one for tabular data, and plotting libraries for making charts. The statistical modeling package is imported for ordinary least squares regression, and two scikit-learn classes are brought in as well, one for fitting a regression model with stochastic gradient descent and one for standardizing features before that model is trained.</p><p>There is no saved output from this cell because importing libraries and setting the plotting mode do not produce a visible result on their own. The effect is preparatory: it quietly configures the notebook environment so that later cells can generate data, fit regression models, and display figures without having to repeat these setup steps.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;b18e43f0-2d76-4fcb-b88c-cc55de6ecd08&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">sns.set_style('whitegrid')
pd.options.display.float_format = '{:,.2f}'.format</code></pre></div><p>The first line changes Seaborn&#8217;s plotting theme so that any charts drawn later will use a white background with light grid lines. That makes the plots easier to read, especially for regression examples where seeing the position of points, fitted lines, and residuals matters. The second line changes how pandas displays numbers in tables and summaries, telling it to show values with two decimal places and commas where appropriate. That affects the appearance of printed DataFrames and any tabular output that follows, giving the results a cleaner, more consistent look. Since this cell only adjusts display settings, it does not produce any visible output by itself; instead, it quietly prepares the notebook so later plots and tables are presented in a more polished format.</p><h3>Basic Linear Regression</h3><h4>Create random sample data</h4><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;a2ab0f3b-9b7c-40b2-9390-0bc9d8d0445b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">x = np.linspace(-5, 50, 100)
y = 50 + 2 * x + np.random.normal(0, 20, size=len(x))
data = pd.DataFrame({'X': x, 'Y': y})
ax = data.plot.scatter(x='X', y='Y', figsize=(14, 6))
sns.despine()
plt.tight_layout()</code></pre></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!IYHq!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f0c0f90-ae63-47d5-805c-3f61e911dbc3_1000x424.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!IYHq!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f0c0f90-ae63-47d5-805c-3f61e911dbc3_1000x424.png 424w, https://substackcdn.com/image/fetch/$s_!IYHq!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f0c0f90-ae63-47d5-805c-3f61e911dbc3_1000x424.png 848w, https://substackcdn.com/image/fetch/$s_!IYHq!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f0c0f90-ae63-47d5-805c-3f61e911dbc3_1000x424.png 1272w, https://substackcdn.com/image/fetch/$s_!IYHq!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f0c0f90-ae63-47d5-805c-3f61e911dbc3_1000x424.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!IYHq!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f0c0f90-ae63-47d5-805c-3f61e911dbc3_1000x424.png" width="1000" height="424" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5f0c0f90-ae63-47d5-805c-3f61e911dbc3_1000x424.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:424,&quot;width&quot;:1000,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:13200,&quot;alt&quot;:&quot;Output image&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/202843043?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f0c0f90-ae63-47d5-805c-3f61e911dbc3_1000x424.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Output image" title="Output image" srcset="https://substackcdn.com/image/fetch/$s_!IYHq!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f0c0f90-ae63-47d5-805c-3f61e911dbc3_1000x424.png 424w, https://substackcdn.com/image/fetch/$s_!IYHq!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f0c0f90-ae63-47d5-805c-3f61e911dbc3_1000x424.png 848w, https://substackcdn.com/image/fetch/$s_!IYHq!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f0c0f90-ae63-47d5-805c-3f61e911dbc3_1000x424.png 1272w, https://substackcdn.com/image/fetch/$s_!IYHq!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f0c0f90-ae63-47d5-805c-3f61e911dbc3_1000x424.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The goal here is to create a simple synthetic dataset for a regression example and immediately visualize it. The first line builds 100 evenly spaced x-values from -5 to 50, giving a smooth range of predictor values to work with. Next, the y-values are generated from a straight-line relationship with an intercept of 50 and a slope of 2, then random normal noise is added. That noise is important because it makes the data look like real observations instead of a perfectly clean line; the points will still follow an upward trend, but they will not all sit exactly on it.</p><p>Those two arrays are then packaged into a pandas DataFrame with columns named X and Y, which makes the data easier to plot and later reuse in model fitting. The scatter plot call draws Y against X, and the figure is made fairly wide so the trend is easy to see. The saved output shows the result of that plotting step: a cloud of points rising from left to right, with noticeable spread around the underlying line. That shape matches the way the y-values were constructed, because each point is based on a linear relationship with random variation added on top. The final two lines clean up the plot appearance by removing the top and right borders and tightening the layout so the chart fits neatly in the figure area.</p><p>Our basic one-predictor linear model is written as a response variable explained by a constant term, one feature, and an error term.</p><p>The error term represents the mismatch between the observed data and a perfectly straight-line relationship. Once the model is fitted to real observations, those mismatches are referred to as <strong>residuals</strong>.</p><h4>Fit a simple regression model using statsmodels</h4><p>The top section of the summary reports basic details about the data and model, including the estimation approach, how many observations and parameters are included, and the note that the reported standard errors are not adjusted for heteroskedasticity.</p><p>The center section lists the fitted coefficients, and these align closely with the way the synthetic data were created. As shown earlier, the same coefficient estimates in the middle of the summary can also be recovered using the OLS expression derived before.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;ebef36f9-6ab9-4a79-987e-49e1f6cb6a8f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">X = sm.add_constant(data['X'])
model = sm.OLS(data['Y'], X).fit()
print(model.summary())</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;25b5469b-e9e0-4026-b5a0-58020ac66199&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">                            OLS Regression Results                            
==============================================================================
Dep. Variable:                      Y   R-squared:                       0.739
Model:                            OLS   Adj. R-squared:                  0.736
Method:                 Least Squares   F-statistic:                     277.6
Date:                Thu, 15 Apr 2021   Prob (F-statistic):           2.39e-30
Time:                        14:54:56   Log-Likelihood:                -436.34
No. Observations:                 100   AIC:                             876.7
Df Residuals:                      98   BIC:                             881.9
Df Model:                           1                                         
Covariance Type:            nonrobust                                         
==============================================================================
                 coef    std err          t      P&gt;|t|      [0.025      0.975]
------------------------------------------------------------------------------
const         49.5401      3.307     14.980      0.000      42.977      56.103
X              1.9942      0.120     16.661      0.000       1.757       2.232
==============================================================================
Omnibus:                        0.389   Durbin-Watson:                   1.721
Prob(Omnibus):                  0.823   Jarque-Bera (JB):                0.103
Skew:                          -0.044   Prob(JB):                        0.950
Kurtosis:                       3.130   Cond. No.                         47.6
==============================================================================

Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.</code></pre></div><p>The goal here is to fit a straight-line regression model to the existing data and then print the full statistical summary of that fit. The first step adds a constant column to the predictor values, which is how statsmodels includes an intercept term in the model. Without that extra constant, the regression would be forced to pass through the origin, which is not what we want for this example.</p><p>Next, the ordinary least squares model is built using the response values in Y and the predictor matrix that now contains both the intercept column and X. Fitting the model means statsmodels finds the line that minimizes the sum of squared residuals, so it chooses the intercept and slope that make the vertical prediction errors as small as possible overall.</p><p>The final line prints the model summary, and the saved output shows exactly the standard OLS report that statsmodels produces. The coefficient for const is about 49.54, and the coefficient for X is about 1.99, which matches the synthetic relationship used to generate the data very closely. That makes sense because the data were created from a line with an intercept near 50 and a slope near 2, plus some random noise. The R-squared value of 0.739 tells us that the fitted line explains a large share of the variation in Y, though not all of it, since the noise still adds scatter around the trend.</p><p>The other parts of the summary help assess how reliable the fit is. The small p-values for both coefficients indicate that the intercept and slope are statistically significant in the model. The t statistics and confidence intervals give another view of the same idea by showing that the estimated slope is clearly positive and far from zero. The diagnostics at the bottom, such as Omnibus, Jarque-Bera, Durbin-Watson, and the condition number, are there to check assumptions and possible issues in the residuals. Here they suggest nothing alarming, which is expected for a simple synthetic dataset built to follow a mostly linear pattern.</p><h4>Check the calculation</h4><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;7da11f89-8a83-40c8-aea7-f63ecc0eedaf&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">beta = np.linalg.inv(X.T.dot(X)).dot(X.T.dot(y))
pd.Series(beta, index=X.columns)</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;2e242331-5963-48ab-86d3-c427cf8e5f37&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">const   49.54
X        1.99
dtype: float64</code></pre></div><p>The goal here is to calculate the regression coefficients directly from the ordinary least squares formula, rather than relying on a fitting routine. The expression first builds the matrix product of the predictors with themselves, takes its inverse, and then multiplies by the predictors and the response vector. That is the closed-form solution for linear regression, and it gives the coefficient values that minimize the sum of squared residuals.</p><p>The result is then wrapped in a Pandas Series so the numbers are easier to read, with the coefficient names taken from the columns of the design matrix. The saved output shows two estimated values: one for the intercept and one for the predictor X. The intercept is about 49.54 and the slope is about 1.99, which is very close to the data-generating pattern used earlier. That close match is exactly what we would expect if the synthetic data were built from a line with an intercept near 50 and a slope near 2, with only a little random noise added.</p><h4>Show the fitted model and its residuals</h4><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;6496db6e-c70e-4584-9cc6-4c8cdfe9e549&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">data['y-hat'] = model.predict()
data['residuals'] = model.resid
ax = data.plot.scatter(x='X', y='Y', c='darkgrey', figsize=(14,6))
data.plot.line(x='X', y='y-hat', ax=ax);
for _, row in data.iterrows():
    plt.plot((row.X, row.X), (row.Y, row['y-hat']), 'k-')    
sns.despine()
plt.tight_layout();</code></pre></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!FlPi!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a84167c-03ef-42c6-9dd2-34921c2b9eb0_1000x424.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!FlPi!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a84167c-03ef-42c6-9dd2-34921c2b9eb0_1000x424.png 424w, https://substackcdn.com/image/fetch/$s_!FlPi!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a84167c-03ef-42c6-9dd2-34921c2b9eb0_1000x424.png 848w, https://substackcdn.com/image/fetch/$s_!FlPi!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a84167c-03ef-42c6-9dd2-34921c2b9eb0_1000x424.png 1272w, https://substackcdn.com/image/fetch/$s_!FlPi!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a84167c-03ef-42c6-9dd2-34921c2b9eb0_1000x424.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!FlPi!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a84167c-03ef-42c6-9dd2-34921c2b9eb0_1000x424.png" width="1000" height="424" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5a84167c-03ef-42c6-9dd2-34921c2b9eb0_1000x424.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:424,&quot;width&quot;:1000,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:24016,&quot;alt&quot;:&quot;Output image&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/202843043?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a84167c-03ef-42c6-9dd2-34921c2b9eb0_1000x424.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Output image" title="Output image" srcset="https://substackcdn.com/image/fetch/$s_!FlPi!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a84167c-03ef-42c6-9dd2-34921c2b9eb0_1000x424.png 424w, https://substackcdn.com/image/fetch/$s_!FlPi!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a84167c-03ef-42c6-9dd2-34921c2b9eb0_1000x424.png 848w, https://substackcdn.com/image/fetch/$s_!FlPi!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a84167c-03ef-42c6-9dd2-34921c2b9eb0_1000x424.png 1272w, https://substackcdn.com/image/fetch/$s_!FlPi!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a84167c-03ef-42c6-9dd2-34921c2b9eb0_1000x424.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The goal here is to take the fitted linear regression model and turn it into a visual comparison between the observed data and the model&#8217;s predictions. First, the predicted values are stored in a new column called y-hat, and the model&#8217;s residuals are stored in another column. That makes it easy to inspect both the fitted response and the prediction errors alongside the original data.</p><p>Next, the data are plotted as a scatter plot with X on the horizontal axis and the observed Y values on the vertical axis. The points are drawn in dark grey so they serve as the raw observations. On top of that, the predicted values are plotted as a line using the y-hat column, which creates the blue fitted regression line seen in the output. Because the model is linear, the fitted values form a straight line that rises as X increases.</p><p>The loop that follows draws a vertical black segment for each observation, connecting the actual Y value to the predicted y-hat value at the same X position. These segments are the residuals visualized directly: the longer the line, the larger the prediction error for that point. In the saved figure, you can see some points sitting above the line and some below it, which is exactly what we expect from a regression fit with noise. The residual lines show that the model captures the overall upward trend, but individual observations still vary around that trend.</p><p>The final plotting calls clean up the appearance by removing the extra chart spines and tightening the layout so the figure looks neat and compact. The result is the displayed plot: a scatter of the observed data, the fitted regression line running through it, and residual segments showing how each point differs from the model&#8217;s prediction.</p><h3>Multiple Linear Regression</h3><p>When there are two predictors that are not dependent on one another, the regression equation expands to include both of them. In words, the response equals an intercept term, plus one coefficient multiplying the first variable, plus another coefficient multiplying the second variable, with an error term added at the end.</p><h4>Create a fresh random dataset</h4><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;2321448c-1a80-4ceb-87f5-dd974977b506&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">## Create data
size = 25
X_1, X_2 = np.meshgrid(np.linspace(-50, 50, size), np.linspace(-50, 50, size), indexing='ij')
data = pd.DataFrame({'X_1': X_1.ravel(), 'X_2': X_2.ravel()})
data['Y'] = 50 + data.X_1 + 3 * data.X_2 + np.random.normal(0, 50, size=size**2)

## Plot
three_dee = plt.figure(figsize=(15, 5)).gca(projection='3d')
three_dee.scatter(data.X_1, data.X_2, data.Y, c='g')
sns.despine()
plt.tight_layout();</code></pre></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!lofV!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8814db6c-a173-4000-8db7-98e865fcb351_365x352.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!lofV!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8814db6c-a173-4000-8db7-98e865fcb351_365x352.png 424w, https://substackcdn.com/image/fetch/$s_!lofV!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8814db6c-a173-4000-8db7-98e865fcb351_365x352.png 848w, https://substackcdn.com/image/fetch/$s_!lofV!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8814db6c-a173-4000-8db7-98e865fcb351_365x352.png 1272w, https://substackcdn.com/image/fetch/$s_!lofV!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8814db6c-a173-4000-8db7-98e865fcb351_365x352.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!lofV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8814db6c-a173-4000-8db7-98e865fcb351_365x352.png" width="365" height="352" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8814db6c-a173-4000-8db7-98e865fcb351_365x352.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:352,&quot;width&quot;:365,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:85223,&quot;alt&quot;:&quot;Output image&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/202843043?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8814db6c-a173-4000-8db7-98e865fcb351_365x352.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Output image" title="Output image" srcset="https://substackcdn.com/image/fetch/$s_!lofV!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8814db6c-a173-4000-8db7-98e865fcb351_365x352.png 424w, https://substackcdn.com/image/fetch/$s_!lofV!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8814db6c-a173-4000-8db7-98e865fcb351_365x352.png 848w, https://substackcdn.com/image/fetch/$s_!lofV!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8814db6c-a173-4000-8db7-98e865fcb351_365x352.png 1272w, https://substackcdn.com/image/fetch/$s_!lofV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8814db6c-a173-4000-8db7-98e865fcb351_365x352.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The purpose here is to build a synthetic three-dimensional data set for the multiple regression example and then display it as a scatter plot. It starts by choosing a grid size of 25, which controls how many values will be created along each predictor axis. Then two coordinate grids are generated with evenly spaced values from -50 to 50 for both X<em>1 and X</em>2. Using a mesh grid means every possible combination of the two predictor values is included, so the result is a full square grid of points rather than a random sample.</p><p>Those grid arrays are flattened and placed into a DataFrame so each row represents one observation with two predictors. After that, the response variable Y is created from a linear rule: a baseline of 50, plus one times X<em>1, plus three times X</em>2, with random noise added on top. That noise makes the relationship look realistic rather than perfectly flat and exact, while still preserving the overall linear trend.</p><p>Once the data are prepared, a 3D figure is created and the points are drawn as a green scatter plot. Each dot represents one synthetic observation in three-dimensional space, with X<em>1 and X</em>2 on the horizontal axes and Y on the vertical axis. The saved output shows exactly that: a cloud of green points spread across a tilted plane-like pattern. The upward tilt reflects the positive coefficients used to generate Y, and the scatter around that plane comes from the added random noise. The final styling steps remove extra plot borders and tighten the layout so the figure is cleaner and more compact.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;23f851d1-1533-4130-9bf4-5520edd09f19&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">X = data[['X_1', 'X_2']]
y = data['Y']</code></pre></div><p>The purpose here is to separate the two parts of the regression dataset into inputs and target. The two predictor columns, X<em>1 and X</em>2, are collected together into a single feature table named X, while the response column, Y, is pulled out on its own and stored as y. Behind the scenes, this is just a tidy way of organizing the data so the regression model can treat the predictors as one matrix of explanatory variables and the outcome as the value it is trying to predict.</p><p>Nothing is printed or displayed because this step only assigns variables for later use. Its role is preparatory: once X and y have been defined, they can be passed into the fitting functions that estimate the regression relationship.</p><h4>Fit a multiple regression model using statsmodels</h4><p>The upper right section of the output reports the fit measures described earlier, together with the F test, which rejects the idea that every coefficient is zero and therefore uninformative. In the same area, the t statistics show that the intercept and both slope terms are, as expected, strongly significant.</p><p>The lower portion of the summary focuses on residual diagnostics. On the left, the skewness and kurtosis values are used when assessing whether the residuals follow a normal distribution. Both the Omnibus test and the Jarque Bera test do not provide enough evidence to reject the null hypothesis of normal residuals. The Durbin Watson statistic checks for serial dependence in the residuals; its value is close to 2, and with 2 predictors and 625 observations, it does not reject the assumption that the residuals are not serially correlated.</p><p>Finally, the condition number is a clue about multicollinearity. It is computed from the design matrix of the input data by comparing the square root of the largest eigenvalue with the square root of the smallest eigenvalue. When this value is greater than 30, it can indicate that multicollinearity may be a serious issue in the regression.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;3b317355-6829-4087-83f1-95e120bb058e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">X_ols = sm.add_constant(X)
model = sm.OLS(y, X_ols).fit()
print(model.summary())</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;8460626d-cb2b-4df4-96c8-f04e5a172d56&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">                            OLS Regression Results                            
==============================================================================
Dep. Variable:                      Y   R-squared:                       0.773
Model:                            OLS   Adj. R-squared:                  0.772
Method:                 Least Squares   F-statistic:                     1058.
Date:                Thu, 15 Apr 2021   Prob (F-statistic):          7.00e-201
Time:                        14:54:57   Log-Likelihood:                -3321.0
No. Observations:                 625   AIC:                             6648.
Df Residuals:                     622   BIC:                             6661.
Df Model:                           2                                         
Covariance Type:            nonrobust                                         
==============================================================================
                 coef    std err          t      P&gt;|t|      [0.025      0.975]
------------------------------------------------------------------------------
const         52.2837      1.970     26.537      0.000      48.415      56.153
X_1            0.9547      0.066     14.560      0.000       0.826       1.083
X_2            2.8610      0.066     43.631      0.000       2.732       2.990
==============================================================================
Omnibus:                        7.484   Durbin-Watson:                   2.050
Prob(Omnibus):                  0.024   Jarque-Bera (JB):                7.714
Skew:                          -0.213   Prob(JB):                       0.0211
Kurtosis:                       3.338   Cond. No.                         30.0
==============================================================================

Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.</code></pre></div><p>The purpose here is to fit a multiple linear regression model to the predictor data already stored in X and then immediately inspect the fitted results. The first step adds a constant column to the predictor matrix so the model can estimate an intercept term. Without that extra column, the regression would be forced through the origin, which would not match the usual linear regression setup used earlier.</p><p>After the intercept column is added, an ordinary least squares model is created with the response variable y and the augmented design matrix. When the model is fit, statsmodels solves for the coefficients that minimize the sum of squared residuals, meaning it chooses the linearly weighted combination of the predictors that comes closest to the observed target values overall. The next line prints the regression summary, which is what appears in the saved output.</p><p>The summary shows that the model explains a large share of the variation in the response, with an R-squared of 0.773. That means about 77 percent of the variation in Y is captured by the linear relationship with the two predictors. The adjusted R-squared is almost the same, which is a sign that both predictors are contributing useful information rather than just inflating the fit artificially. The very large F-statistic and tiny p-value indicate that the model as a whole is highly significant.</p><p>Looking at the coefficient table, the intercept is estimated at about 52.28, which is the predicted value of Y when both predictors are zero. The coefficient for X<em>1 is about 0.95, and the coefficient for X</em>2 is about 2.86. Those values are close to the underlying pattern used to generate the synthetic data, so the fitted model is recovering the expected relationship quite well, even though random noise keeps the estimates from being exact. The small standard errors and the near-zero p-values for both predictors show that each feature has a strong linear association with the response.</p><p>The lower part of the summary gives diagnostic information about the residuals and the overall model fit. The Durbin-Watson statistic is near 2, which suggests little evidence of autocorrelation in the residuals. The omnibus and Jarque-Bera tests, along with the skewness and kurtosis values, provide a rough check on whether the residuals look approximately normal; here they are close enough for a simple teaching example, though not perfectly ideal. The condition number is modest, which suggests the predictors are not causing severe numerical instability or multicollinearity in this setup.</p><h4>Check the calculation</h4><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;038ce9d2-bce5-482a-bc0d-432dfa94aa4d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">beta = np.linalg.inv(X_ols.T.dot(X_ols)).dot(X_ols.T.dot(y))
pd.Series(beta, index=X_ols.columns)</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;95a65793-df69-42b4-8f90-7b1853f3a7f0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">const   52.28
X_1      0.95
X_2      2.86
dtype: float64</code></pre></div><p>The goal here is to recover the regression coefficients directly from the ordinary least squares formula instead of relying on the model summary. First, the predictor matrix is transposed and multiplied by itself, which forms the central matrix used in the closed-form OLS solution. That matrix is then inverted, and the result is multiplied by the transpose of the predictors and the response values. Taken together, this applies the formula that gives the coefficient estimates minimizing the sum of squared residuals.</p><p>The result is stored as a set of fitted parameters, one for the intercept and one for each predictor. Turning those values into a labeled pandas Series makes the output easy to read because each number is paired with the name of the term it belongs to. The saved output shows an intercept of about 52.28, a coefficient of about 0.95 for X<em>1, and a coefficient of about 2.86 for X</em>2. Those values are very close to the synthetic relationship used to generate the data, which is why they look the way they do: the underlying data were built from a linear rule with a constant around 50 and slopes near 1 and 3, with a small amount of noise added.</p><h4>Export the output as an image</h4>
      <p>
          <a href="https://onepagecode.substack.com/p/linear-regression-for-quant-trading">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Machine Learning Workflows for Quant Trading: KNN and Feature Selection]]></title><description><![CDATA[A complete guide to data preprocessing, cross-validation, and hyperparameter tuning in algorithmic trading]]></description><link>https://onepagecode.substack.com/p/machine-learning-workflows-for-quant</link><guid isPermaLink="false">https://onepagecode.substack.com/p/machine-learning-workflows-for-quant</guid><dc:creator><![CDATA[Onepagecode]]></dc:creator><pubDate>Mon, 22 Jun 2026 20:00:28 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!xtcc!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F34c05a2b-ddf8-4a24-a6cd-4c6020f41f06_564x384.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3>Use the button at the end of this to download the source code for this chapter</h3><p>For the main model, we will use the simple k-nearest neighbors algorithm, or KNN. This method can be applied to both regression tasks and classification tasks.</p><p>In scikit-learn&#8217;s standard implementation, KNN looks for the k closest observations using Euclidean distance. For classification, it assigns the label that appears most often among those neighbors. For regression, it returns the average of the neighbors&#8217; target values.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;08461dc3-c695-41f0-92ce-2826baea731d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">import warnings
warnings.filterwarnings('ignore')</code></pre></div><p>The purpose here is simply to quiet down warning messages before the rest of the notebook runs. Importing the warnings module gives access to Python&#8217;s warning system, and setting it to ignore tells the environment not to display those notices during execution. That can make the notebook much cleaner to read, especially when later steps use libraries that may produce harmless or repetitive warnings. Since this cell only changes a setting and does not create any visible result, there is no output to show.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;868ca59d-f882-4d74-adc2-77471fb43cbb&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">%matplotlib inline

from pathlib import Path
import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
from scipy.stats import spearmanr
from sklearn.neighbors import (KNeighborsClassifier, 
                               KNeighborsRegressor)
from sklearn.model_selection import (cross_val_score, 
                                     cross_val_predict, 
                                     GridSearchCV)
from sklearn.feature_selection import mutual_info_regression
from sklearn.preprocessing import StandardScaler, scale
from sklearn.pipeline import Pipeline
from sklearn.metrics import make_scorer
from yellowbrick.model_selection import ValidationCurve, LearningCurve</code></pre></div><p>The purpose of this cell is to gather all the tools needed for the modeling work that follows. It first turns on inline plotting so that any charts produced later will appear directly in the notebook rather than in a separate window. After that, it imports the standard libraries for handling files, numbers, tabular data, plotting, and a few statistical and machine-learning utilities.</p><p>The file-handling import is there so the notebook can refer to data paths cleanly. NumPy and pandas provide the core data manipulation features, while seaborn and matplotlib are used for visual inspection and charting. The Spearman correlation function is imported for checking monotonic relationships between variables, which is useful when exploring how house features relate to price.</p><p>The scikit-learn imports set up the main modeling workflow. The KNeighborsClassifier and KNeighborsRegressor classes provide the KNN models for classification and regression. The cross-validation tools are imported so models can be evaluated more reliably on held-out folds instead of only on the training data. GridSearchCV will later be used to search across different hyperparameter values. Mutual information regression is imported for ranking features by how informative they are about the target. StandardScaler and scale are included because KNN depends on distance calculations, so features need to be put on a common scale. Pipeline helps combine preprocessing and modeling into one clean object so scaling happens correctly inside cross-validation. The scorer utility will be used to define custom evaluation metrics. Finally, the Yellowbrick imports prepare visual diagnostics such as validation curves and learning curves, which help show how model performance changes as the model becomes more or less flexible.</p><p>There is no saved output because this cell only loads libraries and sets up the environment. Its effect is to prepare the notebook for the analysis that comes next.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;0fb3f695-164c-431d-b48c-facd7f7b0f94&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">sns.set_style('whitegrid')</code></pre></div><p>This line changes the plotting theme for seaborn so that any charts drawn after it use a white background with light grid lines. That makes graphs easier to read, especially when looking for trends, comparisons, or exact values on axes. Nothing is printed because the command only sets a style preference for future visualizations; it silently updates the plotting environment rather than producing a visible result right away.</p><h2>Load the dataset</h2><h3>King County Housing Data</h3><p>Data is sourced from kaggle </p><p>Fetch it with the API command below:</p><p><code>kaggle datasets download -d harlfoxem/housesalesprediction</code></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;177646dc-3263-447c-be0e-2737576255e6&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">DATA_PATH = Path('..', 'data')</code></pre></div><p>This line sets up a path object pointing to the parent directory&#8217;s data folder. The main idea is to create a reusable reference to where the dataset files live, instead of hardcoding a long file location each time a file is needed later. Because it only assigns a value and does not perform any printing, plotting, or loading, there is no visible output from the cell. The notebook will use this path afterward whenever it needs to locate files stored in that data directory.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;a085fe6e-a180-43c7-84fb-694716f6676f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">house_sales = pd.read_csv('kc_house_data.csv')
house_sales = house_sales.drop(
    ['id', 'zipcode', 'lat', 'long', 'date'], axis=1)
house_sales.info()</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;c2588758-9b42-4a10-bd86-756131c2081a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">&lt;class 'pandas.core.frame.DataFrame'&gt;
RangeIndex: 21613 entries, 0 to 21612
Data columns (total 16 columns):
 #   Column         Non-Null Count  Dtype  
---  ------         --------------  -----  
 0   price          21613 non-null  float64
 1   bedrooms       21613 non-null  int64  
 2   bathrooms      21613 non-null  float64
 3   sqft_living    21613 non-null  int64  
 4   sqft_lot       21613 non-null  int64  
 5   floors         21613 non-null  float64
 6   waterfront     21613 non-null  int64  
 7   view           21613 non-null  int64  
 8   condition      21613 non-null  int64  
 9   grade          21613 non-null  int64  
 10  sqft_above     21613 non-null  int64  
 11  sqft_basement  21613 non-null  int64  
 12  yr_built       21613 non-null  int64  
 13  yr_renovated   21613 non-null  int64  
 14  sqft_living15  21613 non-null  int64  
 15  sqft_lot15     21613 non-null  int64  
dtypes: float64(3), int64(13)
memory usage: 2.6 MB</code></pre></div><p>The first step here is to load the housing dataset from the CSV file into a pandas DataFrame, then remove a handful of columns that are not going to be used for modeling. Dropping the identifier, geographic location fields, and date helps simplify the table and leaves behind the numerical housing features that are more directly useful for prediction. After that, the information summary is printed so the dataset can be checked before any modeling begins.</p><p>The saved output is the result of that inspection. It shows that the cleaned DataFrame has 21,613 rows and 16 columns, with no missing values in any of the remaining columns. The summary also confirms the data types: most of the features are integers, while a few such as price, bathrooms, and floors are stored as floats. That quick check is useful because it tells us the dataset is complete, reasonably sized, and ready for the next preprocessing and modeling steps.</p><h2>Feature Selection and Transformation</h2><h3>Asset prices often show long-tailed distributions</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;68f732ca-ab71-4188-839f-9e8b9ef76b0a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">sns.distplot(house_sales.price)
sns.despine()
plt.tight_layout();</code></pre></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!xtcc!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F34c05a2b-ddf8-4a24-a6cd-4c6020f41f06_564x384.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!xtcc!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F34c05a2b-ddf8-4a24-a6cd-4c6020f41f06_564x384.png 424w, https://substackcdn.com/image/fetch/$s_!xtcc!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F34c05a2b-ddf8-4a24-a6cd-4c6020f41f06_564x384.png 848w, https://substackcdn.com/image/fetch/$s_!xtcc!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F34c05a2b-ddf8-4a24-a6cd-4c6020f41f06_564x384.png 1272w, https://substackcdn.com/image/fetch/$s_!xtcc!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F34c05a2b-ddf8-4a24-a6cd-4c6020f41f06_564x384.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!xtcc!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F34c05a2b-ddf8-4a24-a6cd-4c6020f41f06_564x384.png" width="564" height="384" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/34c05a2b-ddf8-4a24-a6cd-4c6020f41f06_564x384.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:384,&quot;width&quot;:564,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:17294,&quot;alt&quot;:&quot;Output image&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/202841311?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F34c05a2b-ddf8-4a24-a6cd-4c6020f41f06_564x384.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Output image" title="Output image" srcset="https://substackcdn.com/image/fetch/$s_!xtcc!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F34c05a2b-ddf8-4a24-a6cd-4c6020f41f06_564x384.png 424w, https://substackcdn.com/image/fetch/$s_!xtcc!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F34c05a2b-ddf8-4a24-a6cd-4c6020f41f06_564x384.png 848w, https://substackcdn.com/image/fetch/$s_!xtcc!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F34c05a2b-ddf8-4a24-a6cd-4c6020f41f06_564x384.png 1272w, https://substackcdn.com/image/fetch/$s_!xtcc!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F34c05a2b-ddf8-4a24-a6cd-4c6020f41f06_564x384.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The point of this cell is to quickly inspect how house prices are distributed before any modeling decisions are made. Plotting the price column as a distribution gives an immediate sense of shape, spread, and skewness, which is useful because raw target values often need some transformation before they behave well in a machine learning workflow.</p><p>The first line draws a histogram with a smooth density curve on top of it. That combination lets you see both the rough frequency counts and the overall trend of the values. The saved plot shows a strong concentration of homes at the lower end of the scale and a long tail stretching to the right, which means the prices are heavily right-skewed. In other words, most houses are relatively inexpensive compared with a small number of very expensive ones. That kind of shape is common in real estate data and is one of the reasons a log transformation is often used later.</p><p>The next line removes the top and right borders of the plot to make it look cleaner and easier to read. The final layout adjustment tightens the spacing so the figure fits neatly in the notebook without clipping labels or leaving unnecessary whitespace.</p><h3>Apply a log transformation</h3><p>Useful when working with <a href="http://onlinestatbook.com/2/transformations/log.html">data that are heavily skewed</a>.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;d2c2a23b-0054-417a-814b-a50a195732fb&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">X_all = house_sales.drop('price', axis=1)
y = np.log(house_sales.price)</code></pre></div><p>The goal here is to separate the dataset into inputs and the target the model will try to learn. All of the housing features are collected into one dataframe by removing the price column, since price is what we want to predict rather than something we use as an input. At the same time, the price values themselves are transformed with a natural logarithm and stored as the target variable. That log transform is a common step with house prices because raw prices usually have a long right tail, with a few very expensive homes stretching the scale. Using the logarithm makes the target more balanced and often easier for a model to work with, especially when the analysis later measures errors and compares predictions across a wide range of home values. There is no displayed output because the cell is mainly preparing variables for later modeling steps rather than printing or plotting anything.</p><h3>Ranking features with mutual information regression</h3><p>See the sklearn <a href="https://scikit-learn.org/stable/modules/feature_selection.html#univariate-feature-selection">documentation</a>. This topic is discussed later in Chapter 6 of the book.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;bc98f246-fde6-4b74-92e5-d74b0adeb1a6&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">mi_reg = pd.Series(mutual_info_regression(X_all, y),
                   index=X_all.columns).sort_values(ascending=False)
mi_reg</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;0e5ca4ea-049d-4fe8-b75a-df0b1bfa4fb8&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">sqft_living      0.347760
grade            0.342861
sqft_living15    0.269146
sqft_above       0.258574
bathrooms        0.202894
sqft_lot15       0.085729
bedrooms         0.079577
floors           0.078963
yr_built         0.076893
sqft_basement    0.069330
sqft_lot         0.062372
view             0.058605
waterfront       0.010809
yr_renovated     0.009336
condition        0.004629
dtype: float64</code></pre></div><p>The goal here is to measure how strongly each available housing feature is related to the log-transformed price, so the model can later focus on the most informative inputs. The mutual information regression function is applied to the full feature set and the target variable, and it assigns a score to each column based on how much knowing that feature reduces uncertainty about price. Unlike a simple correlation, this score can capture more general kinds of relationships, not just straight-line ones.</p><p>The result is then wrapped in a pandas Series so the scores can be matched back to the original column names, and the series is sorted from highest to lowest importance. That is why the saved output appears as a descending list of feature names with decimal scores next to them. The largest scores are for features like sqft_living and grade, which suggests those variables carry the most useful information about house price in this dataset. Features such as waterfront and condition appear near the bottom because, at least by this measure, they provide much less predictive signal than the top-ranked measurements.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;c7d6bb15-8561-46e2-81b7-134e139c2c47&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">X = X_all.loc[:, mi_reg.iloc[:10].index]</code></pre></div><p>This step narrows the feature set down to the ten variables that were ranked highest by mutual information with the target. The earlier analysis produced a sorted list of features, and this line uses the names of the top ten entries to pull just those columns out of the full feature table. The result is a smaller version of the predictors called X, which keeps the most informative housing characteristics and leaves out the rest. That kind of feature selection is useful here because KNN relies on distances between observations, so reducing the number of input variables can make the model simpler, less noisy, and often more effective. There is no printed output because the operation only creates a new dataframe for later modeling steps.</p><h3>Scatter Plots for Two Variables</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;a5d9e5aa-c60f-443d-a1e2-d3b7023c3672&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">g = sns.pairplot(X.assign(price=y), y_vars=['price'], x_vars=X.columns)
sns.despine();</code></pre></div><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!7Vgt!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F80ac2dd5-5cda-4a97-b71c-c30dfe6b6875_1766x178.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!7Vgt!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F80ac2dd5-5cda-4a97-b71c-c30dfe6b6875_1766x178.png 424w, https://substackcdn.com/image/fetch/$s_!7Vgt!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F80ac2dd5-5cda-4a97-b71c-c30dfe6b6875_1766x178.png 848w, https://substackcdn.com/image/fetch/$s_!7Vgt!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F80ac2dd5-5cda-4a97-b71c-c30dfe6b6875_1766x178.png 1272w, https://substackcdn.com/image/fetch/$s_!7Vgt!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F80ac2dd5-5cda-4a97-b71c-c30dfe6b6875_1766x178.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!7Vgt!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F80ac2dd5-5cda-4a97-b71c-c30dfe6b6875_1766x178.png" width="1456" height="147" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/80ac2dd5-5cda-4a97-b71c-c30dfe6b6875_1766x178.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:147,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:153216,&quot;alt&quot;:&quot;Output image&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/202841311?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F80ac2dd5-5cda-4a97-b71c-c30dfe6b6875_1766x178.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Output image" title="Output image" srcset="https://substackcdn.com/image/fetch/$s_!7Vgt!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F80ac2dd5-5cda-4a97-b71c-c30dfe6b6875_1766x178.png 424w, https://substackcdn.com/image/fetch/$s_!7Vgt!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F80ac2dd5-5cda-4a97-b71c-c30dfe6b6875_1766x178.png 848w, https://substackcdn.com/image/fetch/$s_!7Vgt!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F80ac2dd5-5cda-4a97-b71c-c30dfe6b6875_1766x178.png 1272w, https://substackcdn.com/image/fetch/$s_!7Vgt!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F80ac2dd5-5cda-4a97-b71c-c30dfe6b6875_1766x178.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>The purpose here is to visually compare the target, price, against each of the selected input features one at a time so you can get a quick sense of which variables seem most related to house value. By placing price into the same dataframe as the feature set and then asking seaborn for a pair plot with price as the y-axis and every feature column on the x-axis, the result is a row of scatter plots, each showing how one housing attribute lines up with the log-transformed price. Because there are ten feature columns being displayed, the saved figure is a wide strip of ten panels.</p><p>The output appears as a scatterplot matrix restricted to one target-versus-feature relationship per panel. That is why you see price on the vertical axis throughout, while the horizontal axis changes from one feature to the next, such as living area, grade, living area of nearby homes, above-ground square footage, bathrooms, lot size, bedrooms, floors, year built, and basement size. The points form different patterns depending on the feature: some show clear upward trends, which suggests a stronger positive association with price, while others look more scattered or clumped into vertical bands, which usually happens when a feature takes on only a limited set of values or has a weaker relationship with the target. Since the target has already been log-transformed, the vertical scale is compressed enough to make the patterns easier to compare visually.</p><p>The final call removes the top and right plot borders, which gives the figure a cleaner, less boxed-in appearance. That stylistic choice does not change the data at all, but it makes the relationships in the scatter plots easier to read.</p><h3>Looking at Correlations</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;334f67b0-4066-4b52-9c95-688dd6f585e2&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">X.info()</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;text&quot;,&quot;nodeId&quot;:&quot;7a2f01a3-e7c1-4073-a12d-7a87c1fdea96&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-text">&lt;class 'pandas.core.frame.DataFrame'&gt;
RangeIndex: 21613 entries, 0 to 21612
Data columns (total 10 columns):
 #   Column         Non-Null Count  Dtype  
---  ------         --------------  -----  
 0   sqft_living    21613 non-null  int64  
 1   grade          21613 non-null  int64  
 2   sqft_living15  21613 non-null  int64  
 3   sqft_above     21613 non-null  int64  
 4   bathrooms      21613 non-null  float64
 5   sqft_lot15     21613 non-null  int64  
 6   bedrooms       21613 non-null  int64  
 7   floors         21613 non-null  float64
 8   yr_built       21613 non-null  int64  
 9   sqft_basement  21613 non-null  int64  
dtypes: float64(2), int64(8)
memory usage: 1.6 MB</code></pre></div><p>A quick structural check of the feature matrix is being done here before any modeling continues. The information summary shows that the data frame has 21,613 rows and 10 columns, so every house sale still has a complete set of selected features available. The listed columns are the ten predictors being kept for the model, and the non-null counts confirm that none of them have missing values. That matters because KNN relies on computing distances directly across rows, and missing entries would make those distance calculations problematic.</p><p>The output also shows the data types for each feature. Most of the variables are integers, such as square footage and year built, while a couple are floating-point values like bathrooms and floors. That mix is normal for housing data, and it gives a quick sense of which fields are discrete counts versus measurements that can vary more continuously. The final memory usage line simply summarizes how much space this compact modeling table takes up in memory, which is modest because the dataset has been reduced to just the most relevant features.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;2f2befde-9826-40fb-af17-04d5e9489ba2&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">correl = X.apply(lambda x: spearmanr(x, y)[0])
correl.sort_values().plot.barh();</code></pre></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!lmRg!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3370dd1-d7ac-4e11-91f6-af5c70a8503b_541x330.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!lmRg!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3370dd1-d7ac-4e11-91f6-af5c70a8503b_541x330.png 424w, https://substackcdn.com/image/fetch/$s_!lmRg!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3370dd1-d7ac-4e11-91f6-af5c70a8503b_541x330.png 848w, https://substackcdn.com/image/fetch/$s_!lmRg!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3370dd1-d7ac-4e11-91f6-af5c70a8503b_541x330.png 1272w, https://substackcdn.com/image/fetch/$s_!lmRg!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3370dd1-d7ac-4e11-91f6-af5c70a8503b_541x330.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!lmRg!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3370dd1-d7ac-4e11-91f6-af5c70a8503b_541x330.png" width="541" height="330" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b3370dd1-d7ac-4e11-91f6-af5c70a8503b_541x330.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:330,&quot;width&quot;:541,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:13027,&quot;alt&quot;:&quot;Output image&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://onepagecode.substack.com/i/202841311?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3370dd1-d7ac-4e11-91f6-af5c70a8503b_541x330.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Output image" title="Output image" srcset="https://substackcdn.com/image/fetch/$s_!lmRg!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3370dd1-d7ac-4e11-91f6-af5c70a8503b_541x330.png 424w, https://substackcdn.com/image/fetch/$s_!lmRg!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3370dd1-d7ac-4e11-91f6-af5c70a8503b_541x330.png 848w, https://substackcdn.com/image/fetch/$s_!lmRg!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3370dd1-d7ac-4e11-91f6-af5c70a8503b_541x330.png 1272w, https://substackcdn.com/image/fetch/$s_!lmRg!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3370dd1-d7ac-4e11-91f6-af5c70a8503b_541x330.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The goal here is to measure how strongly each selected housing feature moves with the target price, using Spearman correlation instead of plain linear correlation. Spearman&#8217;s method is useful because it looks at whether two variables rise and fall together in a monotonic way, which can be more informative when the relationship is not perfectly linear but still clearly ordered. Since the target has already been transformed earlier, this comparison is being made against the log price, which helps reduce the impact of extreme values.</p><p>The first step applies that correlation calculation feature by feature across the columns in X. For each column, Spearman&#8217;s correlation with y is computed, and only the correlation coefficient is kept. That produces one value per feature, giving a compact summary of how each predictor relates to price. After that, the results are sorted so the features can be viewed in order from weakest to strongest association.</p><p>The saved plot is the visual result of that sorting. Because the values are shown as a horizontal bar chart, it becomes easy to compare the features at a glance and see which ones are most connected to price. The strongest bars appear for variables like grade, sqft<em>living, sqft</em>living15, sqft<em>above, and bathrooms, which suggests these are among the most important predictors in the set. The shorter bars near the bottom, such as yr</em>built and sqft_lot15, indicate weaker monotonic relationships with price. The output looks this way because the chart is ordered by correlation value, so the least associated features are placed at one end and the most associated at the other, making the ranking visually obvious.</p><h2>KNN Regression</h2>
      <p>
          <a href="https://onepagecode.substack.com/p/machine-learning-workflows-for-quant">
              Read more
          </a>
      </p>
   ]]></content:encoded></item></channel></rss>