Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Frontispiece to Galileo’s Dialogue Concerning the Two Chief World Systems (Stefano della Bella 1632)

You are viewing this on a mobile device, but SITP is best viewed on a desktop — the book includes various multimedia lecture videos, visualizers, any tufte-style sidenotes with many external hyperlinks to other resources.

Philosophy is written in that great book which ever lies before our eyes—I mean the Universe—but we cannot understand it if we do not learn the language and grasp the symbols in which it is written. This book is written in the mathematical language, and the symbols are triangles, circles, and other geometrical figures without whose help it is impossible to comprehend a single word of it, without which one wanders in vain through a dark labyrinth.

Galileo Galilei































I. Elements of Networks

Although separated by over 2000 years, programmers of Silicon Valley face a daunting task from the cosmos quite similar to the one encountered by the mathematicians of Ancient Greece. That is, during this critical moment for action otherwise known to the Greeks as kairos (καιρός), they must climb back down from their current pitch and backtrack to the beginner’s mind they once had in order to contribute towards this new approach of augmenting and amplifying human intelligence that is currently underway.

No different from learning another mathematical or programming language, they must transition from their finitely discrete structures and deterministic procedures they have grown acustomed to and make the transition to the infintely continuous structures and stochastic procedures. Back then, ancient greek mathematicians were only comfortable with the finiteness of natural numbers like , , and , and had to grapple with the infinite nature of the real numbers such as , , and . Similarly, the programmers of today are being asked to transition from programming algorithms of sets, maps, lists, trees, and graphs on serial machines to optimizing the distributions of scalars, vectors, matrices, tensors, and neural networks on massively parallel machines. More coloquially, programmers interested in the deep learning approach to artificial intelligence must make the transition from software 1.0software 1.0 to software 2.0software 2.0 See (Karpathy 2017), a distinction used to differentiate the classical act of programming software line by line, and the newer approach of programming software by feeding a dataset to a neural net architecture that has a goal, and searching the space of programs with compute. How to exactly program with this approach will take the remainder of this book to explain.

While software 2.0 has increased the intelligence and autonomy of our devices throughout the past decade — to name a few, language understanding with Google Translate and Apple Siri, vision understanding with Tesla Autopilot — at the end of 2022 ChatGPT was released to the world marking the beginning of software 3.0software 3.0 See (Karpathy 2025), enabling the activity of programming with none other than the English language. What may be surprising to realize is that artificial intelligence like ChatGPT is “just” another computer program. However, rather than being implemented in a language like C, JAVA, or JAVASCRIPT, it’s implemented in one that goes by the name of PyTorch, a software 2.0 programming language centered around torch.Tensor, a multidimensional array humbly embedded within a Python package.

WeA modified excerpt from The Structure and Interpretation of Computer Programs §1: Building Abstractions with Procedures are about to study the idea of a computational process. Computational processes are abstract beings that inhabit computers. As they evolve, processes manipulate other abstract things called data. The evolution of a process is directed by a pattern of rules called a program parameters called a model. People create programs train models to direct processes. In effect, we conjure the spirits of the computer with our spells.

A computational process is indeed much like a sorcerer’s idea of a spirit. It cannot be seen or touched. It is not composed of matter at all. However, it is very real. It can perform intellectual work. It can answer questions. It can affect the world by disbursing money at a bank or by controlling a robot arm in a factory. The programs models we use to conjure processes are like a sorcerer’s spells. They are carefully composed recovered from symbolic numerical expressions in arcane and esoteric parallel programming languages that prescribe the tasks losses we want our processes to perform minimize.

So in this whimsical whirlwind tour dubbed The Structure and Interpretation of Tensor Programs (SITP), we will embark on a quest to build from scratch our own deep neural network like ChatGPT by implementing nanochat and our own deep learning framework like torch by implementing borscht. Whether you’re an eager high school student, a rising college student, or a battle-tested industrial programmer, SITP has been meticulously designed so that the only prerequisite required is a basic familiarity with the elements of programming, and high school algebra. Any additional experience is helpful, not mandatory. If you’d like to revisit your programming fundamentals, we recommend A Data-Centric Introduction to Computing (Fisler et al. 2025) (DCIC) which is a great introduction to computing with a focus on data scienceSee (Krishnamurthi and Fisler 2020) that starts with the Pyret teaching language and the table data structure (the torch.Tensor in disguise!), and smoothly graduates to Python, the starting point of our adventure herein.

So with that all said, go on young hacker. Venture forth!
Down the rabbit hole we go.

I. Table of Contents































La perspective curieuse (Jean-François Niceron 1638)

1. Self-Supervised Sequence Learning with Single-Layer Networks

In which we transition from the deterministic and discretely finite software 1.0 to the stochastic and infinitely continuous software 2.0 by rediscovering ngrams by Markov and Shannon as well as perceptrons by Rosenblatt using the languages of probability theory and linear algebra afforded by the borscht.Tensor. At the end, we will have rediscovered the early period where researchers in artificial intelligence were uncertain between methods of connectionism and logical symbolism.

1.1 From Certain to Uncertain Knowledge

Table of Contents §1.2

In which we have our first encounter with the language modeling of GPT-2 via borscht; the primary notions from probability and linear algebra thus used in modeling next-token prediction, namely random variables, their distributions, and their computational equivalent of the multidimensional array.

1.1.1 From Software 1.0 to Software 2.0

Table of Contents (§1.1 From Certain to Uncertain Knowledge)

The gifts that the information revolution brought forth to humanity, at their essence, have simple explanations. As a first approximation, the digital computer can be described as 0s and 1s, the intergalactic computer network as an information highway, and the cloud as computers in the sky. The same can be said for those that the intelligence revolution is currently bringing in. Assistants can be described as LLMs trained with thumbs up or thumbs down, reasoners as models that produce long chains of thought before answering, and agents as models which have access to tools. This magic is continuing to grow as people are even composing agents together into swarms, but the key technology that underlies everything is the large language model, which itself can be simply explained as a next-token predictor. That is, given some user prompt as input, it generates an answer by repeatedly producing a probability distribution over the next word, sampling a word, and appending such word to the input.

Although ChatGPT, Claude and friends are relatively new to our cosmos, the idea of generating sentences with next token prediction is surprisingly not new and dates back to the work of Russian mathematician Andrey Andreyevich Markov in 1913, and shortly after Claude Shannon in 1948. So why is humanity’s so-called tech tree late to such technology? Predominantly for two reasons.

Philosophically, because of the eternal tension between the discretediscrete and continuouscontinuous methods in describing our reality. Programmers were reluctant to use stochastic and infinitely continuous techniques, favoring those that were logical and finitely discrete. However, … like a physicist position of every particle in a vacuum, even with a set of initial equations for position and momentum with equations for change, describing reality with too many parts to countbitter lesson. This created the need for statistical mechanics, describing particles with probability distributionsprobability distributions.

Practically, it’s because of the fossil-fuel like subsidy of datadata provided by the aforementioned intergalactic computer network we call the web, the computecompute provided by massively parallel processors originally designed for video games we call graphic processing units (henceforth GPUs), which can be used efficiently by a neural network architecturearchitecture called transformer. This is why ChatGPT and Claude are referred to as large language modelslarge language model.


Large Language Models explained briefly (3Blue1Brown 2024)

Large language models are trained using methods from the discipline of deep learningdeep learning, which in turn, are based in the statistical machine learningmachine learning approach to artificial intelligence. This means, in order to produce such a probability distribution over possible next words, ChatGPT, Claude and others use mathematical machinery from the areas of probability theory (clearly), linear algebra, and calculus.

That is, the testingtesting resulting from the evaluation of next-token prediction is stated using the stochasticitystochasticity of probability theory; the specificationspecification of an architecture’s parameterized inductive bias is stated using the dimensionalitydimensionality of linear algebra rather than the discreteness of combinatorics; the implementationimplementation of an architecture’s realized parameters is iteratively optimizedoptimized using the approximation of differential calculus rather than the control flow of structured programming; and the computationcomputation that carries out such an optimization is speed of light acceleration using the rooflines of processor pipelines and memory hierarchies rather than the more abstract cost model of worst case complexity. Put shortly, software 2.0 is built by programming continuous mathematics on highly parallelized and distributed machines — how to exactly program with this new approach will take the remainder of the book to iteratively deepen our understanding of evalsevals, architecturearchitecture, optimizersoptimizers, and systemssystems throughout SITP.

1.0 goal1.0 method2.0 goal2.0 method
testingdeterministic unit testsevaluationnext-token prediction with stochastics of probability theory and compression of information theory
specificationmaps and graphs with discreteness of combinatoricsarchitectureparameterized inductive biases with dimensionality of linear algebra
implementationconditionals, functions with control flow of structured programmingoptimizersiterative solvers with approximation of differential calculus
computationasymptotics of serial processorssystemsspeed of light acceleration with rooflines of parallel processors

Starting with Part I. Elements of Networks, in §1. Self-Supervised Sequence Learning with Single-Layer Networks we introduce the goal or evaluation of next-token prediction and train our first architectures with linear models which correspond to a single layer of a neural net. This is followed by §2. The Massively Parallel Multidimensional Tensor where we start the implementation of our deep learning system borscht.

Then, in Part II. Neural Networks, particularly §3. Self-Supervised Sequence Learning with Deep Networks, we deepen our understanding of architecture by transitioning from linear models corresponding to single layer networks to non-linear models of multi layer networks, followed by §4. The Engine of Automatic Differentiation where we evolve the implementation of borscht to support automatic differentiation on gpu-accelerated neural network primitives. Finally, in Part III. Scaling Networks (expand).

We will introduce such mathematical primitives by keeping our language models simple at first in Part I. Elements of Networks — namely what is called the ngram model and linear models in which the aforementioned Markov and Shannon were working on around a century ago — before diving into the design of neural network architectures (including transformers with the attention operator) in Part II. Deep Neural NetworksPhysicists learn the standard model before general relativity. Similarly, we must learn the foundations of machine learning before deep learning..

1.1.2 From Variables to Random Variables

Table of Contents (§1.1 From Certain to Uncertain Knowledge)

GPT-2 2019 · next-token probabilities · runs in your browser
press predict — GPT-2 (~250 MB) downloads on first use, then runs locally
Demo 1.1

Consider the sentence “Hello world, nice to meet you” and feed it into GPT-2 with the words “meet you” missing. When you click the button “predict”, GPT-2 produces a list of real numbers approximately represented by floating points that are between 0 and 1 and sum (or normalize) to 1, which is called a distributiondistribution, because it is distributing truth across a weighted set of values, or in other words, it’s uncertaintyuncertainty about the world, which may or may not be truly stochasticstochasticity. Each number represents the probabilityprobability Stanford CS109 Probability for Computer Scientists | What is Probability? | Lecture 3, chancechance, likelihoodlikelihood, or beliefbelief that GPT-2 assigns to an outcomeoutcome, which in this case is the next word. The stochasticity of probability theory is central to software 2.0.

But rather than produce a distribution with two outcomes like a coin, six outcomes like a die, or fifty two outcomes like cards, it produces one for outcomes, where is the set of words in some vocabularyvocabulary. The size of GPT-2’s vocabulary is 50257, and the list of probabilities you see in Demo 1.1 are the top 10 most likely. As a first approximation, language models can be viewed as as an urn containing a ball labeled with each word in the vocabulary, where some balls are weighted heavier than others.

To be more precise, because we are passing an input sentence, such a distribution is a conditional distributionconditional distribution. That is, GPT-2 is producing the distribution of the next word conditioned on the sequence of words passed in as input, and is denoted by

and with Demo 1.1, evaluating GPT-2 is equivalent to . That is, language modelslanguage models are next-token predictors modeled by conditional distributions. More accurately, each number in that list of probabilities is the chance that GPT-2 assigns a random variablerandom variable taking on a specific outcome. A random variable is like a deterministic variable in that it can take on values, but rather than being assigned one specific value, it can possibly take on many with a certain degree of chance. A random variable’s distribution then corresponds to an array or list of values which we call a distribution because it distributes the truth or state of the random variable across many outcomes.

In the case of GPT-2, the conditional distribution ’s random variable is denoted as . To make random variables more explicit, some people will denote distributions with their random variables included, which in our case of a conditional distribution over next words, is . However, this brings us to our next point which is that with conditional distributions, the random variable being conditioned on is actually no longer random, because it is assumed that it has already taken on a value, in this case where , , . So, with more generally, there is no randomness associated with the history . Let us print the distribution over the next word that GPT-2 produces given the history from Demo 1.1:

import torch

# The same GPT-2 output distribution p(w | "Hello world, nice to") from above, top 10 of 50257 words
# (renormalized so this top-10 slice sums to 1)
vocab = ["see", "meet", "hear", "have", "be", "know", "you", "talk", "say", "get"]
probs = torch.tensor([0.387361, 0.154993, 0.152304, 0.127857, 0.057572, 0.043027, 0.039115, 0.015646, 0.013935, 0.008190])

print("Asking GPT-2 what is p(y|Hello world, nice to):")
for i in torch.argsort(probs, descending=True):
    print(f"{vocab[i]} : {probs[i]:.4f}")
print(f"Total sum of p(y|Hello world, nice to): {probs.sum():.4f}")
Asking GPT-2 what is p(w|Hello world, nice to):
see : 0.3874
meet : 0.1550
hear : 0.1523
have : 0.1279
be : 0.0576
know : 0.0430
you : 0.0391
talk : 0.0156
say : 0.0139
get : 0.0082
Total sum of p(w|Hello world, nice to): 1.0000
import matplotlib.pyplot as plt

order = torch.argsort(probs, descending=True)
plt.figure(figsize=(6, 3))
plt.bar([vocab[i] for i in order], probs[order], color="#0072B2")
plt.ylabel("p(y|Hello world, nice to)")
plt.title("GPT-2 next-token distribution (top 10)")
plt.tight_layout()
plt.show()

Where the state of is being spread across 50257 possible outcomes of GPT-2’s vocabulary. Each index i of the probs array corresponds to a value in the token outcomes tokens[i], with each probs[i] corresponding to the chance, or possibility, of the random variable taking on the value tokens[i]. That is, what connects the weighted array tokens and the state space probs is the index i. It is more accurate then, to describe a random variablerandom variable Stanford CS109 | Random Variables and Expectation | Lecture 6 as states represented by indices , and the type of a distribution ends up being some function that sends states of indices to their probabilities, such that for all and . In general, any list of values between 0-1 and normalizes to 1 is a valid distribution. These properties are referred to as non-negativity and normalization respectively. We can validate that probs is a valid distribution by verifying that each probability is between 0-1 and the distribution normalizes to 1However, since we are only including the top 10 most likely words out of 50256, we’ve re-normalized probs so that the total sum of probs is 1. The original sum is 0.8181, with the other 1-0.8181=0.1819 spread across the other 50256-10=50246 unseen words.. Moreover, since the space of possible outcomes has size of 50257, the random variable is said to be a discrete random variablediscrete random variable with a probability mass functionprobability mass function as opposed to a continuous random variable with a probability density functionprobability density function.

experimentexperiment trialtrial frequencyfrequency

g = torch.Generator()

n = 10_000_000
target_e = "see"

# p must normalize to 1, so we renormalize our top-10 slice of the distribution
p = probs / probs.sum()
# draw all n samples at once, according to p(y|"Hello world, nice to")
# (tensors hold numbers, not strings, so we sample vocab indices rather than words)
outcomes = torch.multinomial(p, n, replacement=True, generator=g)

i_e = vocab.index(target_e)
prob_e = p[i_e].item()
freq_e = (outcomes == i_e).float().mean().item()
print(f"the expected probability of \"{target_e}\" is {prob_e}")
print(f"the actual frequency of \"{target_e}\" in {n} trials is {freq_e}")
the expected probability of "see" is 0.38736099004745483
the actual frequency of "see" in 10000000 trials is 0.38721099495887756

1.1.3 Probability as Array Programming

Table of Contents (§1.1 From Certain to Uncertain Knowledge)

Language models are next-token predictors modeled by a conditional probability distributions of a random variable, which, unlike deterministic variables, are modeled by a list of values in which the state or truth of the random variable is distributed over. Since the length of GPT-2’s vocabulary is 50,256 words, it’s subsequent probability distribution is a list, or a vectorvector and moreover, rather than use a vanilla Python list to represent such a vector, we use a borscht.Tensorborscht.Tensor instead.

print("gpt-2's output distribution is stored with a torch.Tensor rather than a vanilla python list")
print(f"probs.shape {type(probs.shape)}: {probs.shape}")
print(f"probs.dtype {type(probs.dtype)}: {probs.dtype}")

print("\ngpt-2's individual probabilities are accessed by indexing into")
print(f'0 ({vocab[0]}) -> {probs[0]}')
print(f'1 ({vocab[1]}) -> {probs[1]}')
print(f'2 ({vocab[2]}) -> {probs[2]}')
gpt-2's output distribution is stored with a torch.Tensor rather than a vanilla python list
probs.shape <class 'torch.Size'>: torch.Size([10])
probs.dtype <class 'torch.dtype'>: torch.float32
gpt-2's individual probabilities are accessed by indexing into
0 (see) -> 0.38736099004745483
1 (meet) -> 0.1549929976463318
2 (hear) -> 0.15230399370193481

The two primary properties of a borscht.Tensorborscht.Tensor is it’s .shapeborscht.Tensor.shape and .dtypeborscht.Tensor.dtype. evaluating to (10,) and dtype64 respectively means it’s a vector with support in whose real values are being approximately represented with double precision floating point numbers.

Since distributions can be viewed as vectors which in turn can be viewed as lists, probability can be viewed as none other than array programming.

More generally, a borscht.Tensor with .shape of (V), corresponds to a vector with a support , where is the vocabulary. (todo: context-length is dimensionality of ). For convenience sake, we analyzed the top 10 probabilities with a borscht.Tensor whose .shape was (10,), which corresponded to a vector .

The visual lecture from 3Blue1Brown’s mentions that vector can be viewed as a fancy word for a list — in the context of deep learning we use the term tensor rather than vector.

Vectors | Chapter 1, Essence of linear algebra (3Blue1Brown 2016)

Viewing tensors as lists is taking the data-oriented perspective — for instance probs being some vector . However, if you squint your eyes and view the probs array from a more functional perspective, it is also a function statistical learningAlso, if you squint your eyes at the hashmap data structure (in Python a dict) with the functional perspective, it’s a function . In fact, there was a named tensor proposal, Tensor Considered Harmful (Rush 2019), for treating and viewing tensors more like dicts rather than tuples.. The notation can be viewed as a function , or more generally for , the function . The two notations are equivalent!

missing notebook: /opt/build/repo/sitp/notebooks/1.1_gpt2output.ipynb

Important

Probability can be seen as array programming! Every valid distribution which is some function whose values are between 0 and 1 and normalizes to 1 has a corresponding borscht.Tensor with .shape of (todo).

outcomes -> events

  • todo eventevent
  • todo probability of ORprobability of OR
  • todo sum rulesum rule
import numpy as np

# The same <span class="smallcaps">GPT-2</span> output distribution p(w | "Hello world, nice to") from above, top 10 of 50257 words
tokens = ["see", "meet", "hear", "have", "be", "know", "you", "talk", "say", "get"]
probs = np.array([0.3169, 0.1268, 0.1246, 0.1046, 0.0471, 0.0352, 0.0320, 0.0128, 0.0114, 0.0067])

tokens_start_with_h_or_s_and_contains_a_probs = []
for token, prob in zip(tokens, probs):
    stripped = token.strip()
    starts_with_h = stripped.startswith("h")
    starts_with_s = stripped.startswith("s")
    contains_a = "a" in stripped
    if (starts_with_h or starts_with_s) and contains_a:
        tokens_start_with_h_or_s_and_contains_a_probs.append(prob)

prob_starts_with_h_or_s_and_contains_a = sum(tokens_start_with_h_or_s_and_contains_a_probs)

print(f"probability that word starts with letter h: {prob_starts_with_h}")
print(f"probability that word starts with letter s: {prob_starts_with_s}")
print(f"probability that word starts with letter h or s: {prob_starts_with_h + prob_starts_with_s}")
print(f"probability that word starts with letter h or s, and contains letter a: {prob_starts_with_h_or_s_and_contains_a}")

  • todo: numpy vectorization

So when you ask an LLM a question, it generates a full answer (with many sentences) word by word by repeating the following loop:

  1. evaluating the probability of the next word conditioned on the input
  2. selecting (or sampling) a word. halt if the word is the special END word.
  3. appending it to the existing text, and evaluating the probability again with the modified input

Now that you understand the basics of language modeling, the trillion dollar question is how to produce such a conditional distribution ? In some sense that’s all there is to large language models.

1.1.4 Are You Pythonic?

Table of Contents (§1.1 From Certain to Uncertain Knowledge)

Python programming language































1.2 Next Token Prediction is Classification and Compression

§1.1 Table of Contents §1.3

In which The spelled-out intro to language modeling: building makemore (Karpathy) we introduce the terminology of software 2.0 in the context of language modeling; the joint, marginal, conditional with multidimensional arrays, reductions, and broadcasting; implement our first language model by maximizing the likelihood of biased ngrams; evaluate said model’s limits with information theoretical and probabilistic quantities such as perplexity, entropy, bias variance tradeoff, the curse of dimensionality; and finally, understand intelligence more generally as compression.

1.2.1 The Estimation of Software 2.0

Table of Contents (§1.2 Next Token Prediction is Classification and Compression)

Estimating conditional distributions is the heart of software 2.0, where for large language models like GPT-2, such a conditional distribution is the probability of the next word given those that have already occured. In §1.1 From Certain to Uncertain Knowledge, we saw that with the input text "Hello world, nice to meet" as the history , GPT-2 produces a conditional distribution with a borscht.Tensor of .shape (V,) which corresponds to a vector with support in — since we are only taking the top 10 most likely words though, .shape is (10,) with support in . We now shift our attention from using language models to implementing ones, namely the conditional distribution GPT-2. We will incrementally increase the expressivity of our models culminating with GPT-2’s transformer architecture in Part II. Deep Neural Networks. But for now, we start with the basics and return to first principles.

As mentioned in the previous chapter, large language models are trained using methods from the discipline of deep learningdeep learning, which in turn are based in machine learningmachine learning, which in turn are based in statistical learningstatistical learningActually, the implementation of learning is not just limited to the stochastic and infinitely continuous methods of software 2.0, more classically known as connectionism. It’s also possible to implement with logical and finitely discrete methods of software 1.0 (symbolism), albeit with much less success. See inductive logic programming also known as reverse deduction https://en.wikipedia.org/wiki/Inductive_logic_programming., which in turn are based in statistical inferencestatistical inference, which in turn employs the use of statistical estimationstatistical estimation. What are the meanings of all these terms? Roughly speaking, people in the business of estimation like AI researchers at frontier labs are given data (in statistical jargon, observe samples) such as the internet and would like to estimate distributions (of the population) from said data, namely a like GPT-2. Because distributions are functions, statistical estimation is also known as function approximationfunction approximation.

Estimation of distributions is one possible activity of making inferences from data — as opposed to simply describing data See https://en.wikipedia.org/wiki/Descriptive_statistics. — another useful form of inference is hypothesis testingSee https://en.wikipedia.org/wiki/Statistical_hypothesis_test., the process of deciding whether observed data provide sufficient evidence to reject a null hypothesis. Estimation however is the primary statistical tool used as opposed to hypothesis testing, and once a distribution like is estimated from data by research labs, users can use such a distribution in order to generate sentences — hence this most recent wave of statistical methods from software 2.0 being coloquially known as genaigenai, other previous monikers being big databig data, data miningdata mining, data sciencedata science, and pattern recognitionpattern recognition — with an inference loop, namely predicting a distribution over the next token, sampling and appending a token to the history and repeating.

Generally speaking, the difference in direction of estimating a distribution from data versus generating data from a distribution is the primary distinction between probability and statistics, although the line as we will soon see gets quite blurryIn the same way traditional data and function blurs with software 1.0, since functions are also data.. This distinction between the two is more broadly known as the difference between analysisanalysis and synthesissynthesisSee https://plato.stanford.edu/entries/analytic-synthetic/. Researchers analyze the observed data of the internet and recover an estimated distribution whereas users synthesize data and generate sentences. Sometimes they are referred to with direction in time: synthesis is the forward direction whereas analysis is the backward or inverse. Throughout the book, we will incrementally explore increasingly expressive languge models that can do a better job at recovering from data.

The availibility of said data determines the regime of statistical learning. That is, if sample data is available in the form of input-output pairs , then the regime is known to be supervised learningsupervised learning or self-supervised learningself-supervised learning, for the machine that is attempting to learn a distribution via induction is being supervised with the expected outputs, whereas the regime where such data is unavailable is known as unsupervised learningunsupervised learning. Where in unsupervised learning the goal of inference is that of understanding like clustering or dimensionality reduction, in the regime of supervised learning the goal is the aforementioned statistical estimation of a function that predicts the expected output given the input. For instance, with the following self-supervised dataset

missing notebook: /opt/build/repo/sitp/notebooks/1.2_bigram.ipynb

Moreover, the regime of supervised learning is further refined depending on the kind of output the estimated function is predicting. In the case of a categorical output, the supervised learning is known as classificationclassification for the machine’s goal (the estimated function’s output) is trying to classify the input’s category. Modeling languagelanguage models as next-token prediction is in fact an instance of classification since it is classifying the next most likely token given an input sentence This stands in contrast to the case when the output that the estimated function is predicting is quantitative, which is known as regressionregression.

Somewhat more confusingly, depending on the domain or context of statistical modeling, the terminology for inputs and outputs might be further refined. That is, inputs and outputs are also respectively referred to as independent variableindependent variable and dependent variablesdependent variable, as well as predictorspredictors and responsesresponses in the context of experimental design. They can also be referred to as featuresfeatures and targetstargets. The difficulty of naming is not limited to the discipline of programmingSee https://martinfowler.com/bliki/TwoHardThings.html. Whatever you want to call them, at the heart of statistical learning, machine learning and deep learning is the activity of estimation, or the approximation of functions with inputs and outputs . two cultures of statisticstwo cultures of statistics

Warning

Inference is an overloaded term, and hence ambiguous without context! In generative AI the term inference refers to the forward process of evaluating a population’s distribution in order to generate samples via prediction. In classical statistics the term inference refers to the backward process of inferring a population’s distribution from observed samples

But for the classification of language modeling, how do we reconcile the goal of estimating some function , when GPT-2 is the probability distribution which is a function of type ? The way we reconcile such a type mismatch is to map over words from their string representations in to their index representations in with a dict[str, int] instantiated by {c:i for i,c in enumerate(v)}

missing notebook: /opt/build/repo/sitp/notebooks/1.2_bigram.ipynb

However, note that the input type of a next-token classifier are sentences . (todo: BTD)

Let us now estimate our first language model

1.2.2 The Joint, Marginal, Conditional, and Chain Rule

Table of Contents (§1.2 Next Token Prediction is Classification and Compression)

Software 2.0 models language as a next-token predictor which means estimating a classifier which assigns a next word given a sentence where all under a self-supervised setting with access to input-output paired data .

Let us first start by generalizing the notion of a distribution of a single random variable which corresponds to a flat array of real-valued numbers satisfying non-negativity and normalization to that of a joint distributionjoint distribution Stanford CS109 Probability for Computer Scientists | Joint Distributions | Lecture 11, defined as a high dimensional function with type which corresponds to a multidimensional array. Both map the values of random variables to their probabilitiess. That is, evaluating

produces a single probability for random variable taking on the value and taking on the value and so on, up until the random variable taking on the value . Because the joint can be a distribution of two random variables, three, up to an arbitrary t, it’s often vectorized as .

In the case of (probabilistic) language modeling, the primary joint distribution of interest is the probability of a fixed t lengthed sentence occuring, which is or with , where the ’th random variable represents the ’th word in the -lengthed sentence. For instance, is the probability of the complete sentence "Hello nice to meet you" (todo, instead of distribution over words, it’s a distribtion over sentences).

Warning

Note with the random variables being modeled the way they are (that is, with order) that the joint is commutative. For instance, with , . Semantically speaking, is saying that the probability of the first word being “hello” and the second word being “world” is equal to the probability of the second word being “world” and the first word being “hello”. This holds more generally for any permutation of .

With us defining the notion of a joint probability as a multidimensional array, it’s probably no surprise that the joint distribution corresponds to a borscht.Tensor with .shape (V,...,V) where .ndim is t. That is, .shape is a t-lengthed tuple. Take a look at Figure 1.2.2 below:

Figure 1.2.2

In particular, let us consider the joint distribution of two random variables , whose 2 dimensional surface can be plotted. This distribution has type , which corresponds to a borscht.Tensor with a .shape of (d1,d2) — a matrixmatrix supported in , or in other words, a by rectangular array that maps indices in to their values in . Because the array for a matrix can be visualized as a rectangle when taking the data-oriented perspective, the first dimension is usually referred to as the number of rowsrow, and the second dimension is referred to the number of columnscolumn. This naming of course breaks down for tensors with a higher number of dimensions. The code example initializes a matrix with borscht.zeros((n,m)), and plots the surface with ax.plot_surface().

import borscht
x = borscht.zeros((d1,d2))
print(x)

The joint of three random variables is plotted as what is known as a voxel plot (it’s difficult to see 4 dimensions alltogether), which corresponds to a borscht.Tensor with a .shape of (d1,d2,d3) — a tensortensor supported in , a cubic array that maps indices in to their values in . (todo, remark on naming with tensor.) Similarly, the tensor is initialized with borscht.zeros((n,m,p)), is filled with values, and is plotted with ax.voxels(x).

import borscht
x = borscht.zeros((d1,d2,d3))
print(x)

Important

Once again, probability can be viewed as array programming!

(todo, warning on

With the notion of a joint distribution now defined, the actual joint of interest to language modeling is the probability of a sentence taking on specific words where , let so that we can consider the joint of two random variables , which corresponds to a matrix represented by a borscht.Tensor with .shape of (V,V), in this case (5,5). In Figure 1.2.3, the surface on the left and the matrix on the right represent the same distribution . These values are one possible assignment to the function body , or equally, one possible assignment to the borscht.Tensor. For instance, we see that . (todo, it’s really . (todo, table is technically infinite. t is not fixed.). Note that the corresponding is provided, even though the distribution is defined on .

Figure I.2.3

Note

In SITP, we will incrementally increase the expressivity of our next-token predictors or classifiers in order to model language more accurately. Throughout the book, we will maintain a visual language of color in addition to an algebraic one to indicate a consistent purpose or role regardless of what space an object is represented with. For instance, input sentences will be coded with blue whether they are strings indices one hot vectors , or soon to be embeddings . Without loss of generality, the same holds for output tokens except coded with orange. More colors will also be introduced later on. This can be toggled at anytime by clicking the located in the top left corner next to the theme picker.

  • modify left figure to highlight the 0.15 and 0.16s
  • why is the marginal a uniform (0.2)?

For a single random variable , we can see that the truth of it’s state is indeed distributed across the vocabulary . For instance, scanning the blue arrow from top to bottom, we see that state of random variable is distributed across five words. In particular, for one of those values which corresponds to , it’s state in of itself is distributed across with the orange arrow from left to right. For instance, the chance of "to meet"should be more likely than "to to". The code for this joint is the transliteration of the matrix from Figure I.2.3 into borscht.Tensor with .shapeof (5,5):

missing notebook: /opt/build/repo/sitp/notebooks/1.2_bigram.ipynb

Warning

Let us digress on two technical aspects of borscht: shape suffixes and indexing.


Digression 1: Shape Suffixes

The first place to draw your attention to is in the particular choice of naming the joint distribution as p_VV. The _VV suffix is documentation-based coding brought back from the deadSee Hungarian notation https://en.wikipedia.org/wiki/Hungarian_notation. Also see Google’s C++ Style Guide — a somewhat representative and perhaps canonical standard of most source conventions — frown on Hungarian notation. Finally see Tensor Considered Harmful (Rush 2019) and Named Tensor Notation (Chiang et al. 2021). as a reading aid to humans and fellow LLMs to denote that the borscht.Tensor has a .shape of (V, V), where in this case V evaluates to 5. We even assert such fact with assert p_VV.shape == (V, V). Such judgement can actually be enforced by typechecking dynamically at runtime with the jaxtyping Python package, particularly Float[np.ndarray, "V V"] (expand on jaxtyping and python typechecking):

missing notebook: /opt/build/repo/sitp/notebooks/1.2_bigram.ipynb

The function printp_VV takes a parameter p_VV with type Float[np.ndarray, "V V"], so the first invocation with the argument p_VV itself passes. Mutating p_VV to alias a borscht.Tensor with .shape (3,) and applying the function a second time results in a type error.

Digression 2: Indexing

Second, notice how the value of is retrieved with p_VV[toi, meeti]. (todo, curry by indexing into first or second):

missing notebook: /opt/build/repo/sitp/notebooks/1.2_bigram.ipynb

Note

Digression is now over.


Now, with borscht technicalities of shape suffixes and indexing aside, let us move onto the next probabilistic concept of the marginal. From this joint distribution of two random variables , we can recover both distributions of single random variables and .

Consider again that second “level” of distribution. That is, the specific state of being distributed in of itself across the first row of the matrix according to . We can “take out” the randomness of by summing all the entries in the first row, and no longer have this second level or axis of state being distributed, and are simply left with . In general, for any , we have that . Note that is fixed! In the context of recovering a distribution of a single random variable from a joint distribution, such single random variable distribution is referred to as the marginal distributionmarginal distribution. Transliterating this to borscht, we use borscht.Tensor.sum()borscht.Tensor.sum reduce operationreduction (expand):

missing notebook: /opt/build/repo/sitp/notebooks/1.2_bigram.ipynb

(todo: generalize for randvars) torch.sum

After generalizing the notion of a distribution into a joint distribution, and recovering marginals from said joint, we are now finally ready to backtrack to refining the notion of the conditional. .

conditional distributionconditional distribution Stanford CS109 | Conditional Probability and Bayes | Lecture 4

so, the conditional is defined as the joint over the marginal distribution that is being conditioned on. chain rulechain rule product ruleproduct rule (decomposing a joint of sentences with a chain of conditionals over words requires proof (ntermezzo).) with an inference loop, namely predicting a distribution over the next token, sampling and appending a token to the history and repeating.

We can further clarify our notion of language modelslanguage models to next-token predictors performing classification which then auto-regressively feeding such classifying prediction into the next evaluation via chain or product rule.

More generally, broadcastingbroadcasting broadcast divide

There is a rule you should learn at long last,
combination of tensors the task.
Dims right-aligned,
extra left 1s assigned,
match paired dimensions: Broadcast!

9 x 1 x 3 1 x 8 x 1 9 x 8 x 3

Tensor Puzzles (Sasha Rush, Marcos Treviso)

missing notebook: /opt/build/repo/sitp/notebooks/1.2_bigram.ipynb

1.2.3 Estimating Markov Biased Histograms by Counting

Table of Contents (§1.2 Next Token Prediction is Classification and Compression)

We know that denotes the conditional distribution of the next word given the history of sentence thus far, we know this distribution corresponds to a borscht.Tensor with .shape (V,), and we know the function type is (todo: can be viewed as ) which maps sentences to distributions over next words, but how do we go about estimating the distribution ?

When initially describing the distribution of next possible tokens with the stochasticity of probability theory, we also mentioned other phenomena like coin flips, die rolls, and card draws that are modeled stochastically as well and thus with distributions. In the case where such physical objects are deemed to be fair — that is, no one has weighted the coin, die, nor rigged the deck — we can assume that all outcomes are equally likely and subsequently calculate the probability of any event (todo, go write that exposition) is total number of outcomes in the event over the number of outcomes in the sample space.

However, it’s clear that language is not a stochastic process with equally likely outcomes. For instance words “the”, “is, “a” are more likely than “quixotic” for instance. It is for this reason that the first idea to recover or estimate is by simply countingcounting the number of times the event of interest occurs from some dataset? sample? and dividing it by the total number of events in the event space, thereby calculating the relative frequency estimaterelative frequency estimate:

Taking the internet as our dataset, this ratio is asking out of all the sentences that start with “Hello world nice to meet”, how many times does “Hello world nice to meet you” occur? (todo, define unbiased estimator. in the limit..) While the relative frequency estimate is an unbiased estimatorunbiased estimator, the problem is that language as a phenomena continually evolves, grows, and adapts itself. The fact that natural language can be easily modified and innovated on by new generations of our culture is a feature rather than a bug. While new slang is invented every day, the classic examplar for the problem of using a relative frequency estimate is the following sentence posed by Noam ChomskySee https://en.wikipedia.org/wiki/Colorless_green_ideas_sleep_furiously:

Although such sentence now exists on the internet because of Noam Chomsky, the sentence shows one that is syntactically valid but perhaps not semantically meaningful, which, very much resembles the essence of slang. Now, since estimating with a relative frequency estimate is intractable, let us explore alternative ways. This motivates the need to dig deeper into the precise defintion of the conditional, what is what we explore here in 1.2.2 The Joint, Marginal, Conditional, and Chain Rule


Now that we understand the process of modeling language as a next-token predictor is to estimate a classifier ) which assigns a next word given a sentence where all under a self-supervised setting with access to input-output paired data , and moreover, that the conditional is precisely defined as the joint over the marginal, can we simply evaluate the broadcasted divide of the joint over the marginal?

Rather than strive for perfection, we can achieve the good by introducing some biasbias, known as the Markov assumptionmarkov assumption.

  • (todo, explain data d)
  • counting is the maximum likelihood estimatemaximum likelihood estimate (MLE).
  • simplest idea: unbiased estimator count the relative frequency (which is the MLE)
  • need chain/product rule here. but do not introduce bayes rule, for that will introduce discriminant vs generative modeling.
  • intractable, so need to introduce bias. with markov assumption (truncated conditional in chain rule)
missing notebook: /opt/build/repo/sitp/notebooks/1.2_bigram.ipynb
missing notebook: /opt/build/repo/sitp/notebooks/1.2_bigram.ipynb
missing notebook: /opt/build/repo/sitp/notebooks/1.2_bigram.ipynb

1.2.4 The Scoring of Software 2.0

Table of Contents (§1.2 Next Token Prediction is Classification and Compression)

  • use perplexity following jurafsky, even though karpathy only uses log likelihood?
  • karpathy scores the bigram model with log likelihood, and maintains the log likelihood for the parameterized single layer neural net
  • Scoring software 2.0 involves probabilities of probabilities
  • prediction is compression is intelligence

Scoringscore such estimation involves probabilities of probabilities.

maximum likelihood estimation Stanford CS109 Probability for Computer Scientists | M.L.E. | Lecture 21 (Piech)

missing notebook: /opt/build/repo/sitp/notebooks/1.2_bigram.ipynb

Reinventing Entropy | Compression is Intelligence Part 1 (3Blue1Brown 2026)

1.2.5 The Evaluation of Software 2.0

Table of Contents (§1.2 Next Token Prediction is Classification and Compression)

1.2.6 Summary and Further Reading

(Eisenstein 2019)’s Chapter 6. Language Models; (Jurafsky and Martin 2026)’s Chapter 3. N-gram Language Models;

(further reading for torch)































1.3 Parameterizing Classification with Discriminant Functions

§1.2 Table of Contents §1.4

In which we parameterize classification with a three step process of parameterization, scoring, and optimization; in particular, by directly optimizing the mean squared error of the perceptron; and understand the perceptron’s classifying by expanding our notion of vectors from vectors as lists to vectors as points, enabling not only array programming with distributions, but also with geometry

1.3.1 The Parameter Estimation of Software 2.0

Table of Contents (§1.3 Parameterizing Classification with Discriminant Functions)

Parameterizing classifiers and estimating such parameters forms the basis of statistical machine learning implemented by programming software 2.0 with neural networks. In §1.1 From Certain to Uncertain Knowledge and §1.2 Next Token Prediction is Classification and Compression we used and implemented our first classifier which modeled language stochastically as next-token prediction, assigning an output token for a given input sentence from self-supervised data . Particularly, we modeled as a bigram model which introduces bias with respect to the product rule via Markov assumption of truncating the history of conditionals, where such bigrams were estimated by counting relative frequencies with a row stochastic matrix . The definition of the bigram model is provided on the left

Whereas in §1.3 Parameterizing Classification with Discriminant Functions, we now change our approach to language modeling next-token prediction (and statistical machine learning more generally) from that of counting to parameterizingparameterization, meaning to define a function with parametersparameters in addition to it’s input . The type of the function on the right is changed to take in a parameter and an input sentence in order to produce an output token . However, passing in as an argument on every function evaluation is cumbersome since, after we find a good set of parameters , they in fact do not changeUnlike the human brain, which is constantly being reconfigured and rewired!. Rather, what we would like is to instantiate the function with parameters once, and then evaluate such function whenever we needed to predict the next token. The solution to this is to change the type of to

which we can partially applypartially apply by passing in as input, and receive the next-token predicting function itself. In other words, we have curriedcurried the type into a higher order functionhigher order function which can return function themselves. Transliterating a toy parameterized model which multiplies an input with a parameter to borscht then looks like

import borscht
import borscht.nn as nn

class MyParameterizedModel(nn.Module):
  def __init__(self, theta):
    self.theta = theta

  def __call__(self, x):
    return x*self.theta

theta = 42
model = MyParameterizedModel(theta)
y = model(1)
print(y)

where is the first evaluation with denoting the next-token predictor , and is the second, These correspond to the Python’s class __init__ and __call__ methods respectively. When functions are parameterized, they are denoted as or to reflect such.

Now, because the model is characterized or determined by said parameters , such parameters must be estimatedparameter estimation, usually from the dataset . The way this is done is to score such parameters with an objective known as the loss functionloss function, and subsequently find the parameters that optimizeoptimization such loss function, a process denoted by . That is, the problem of finding a good model reduces to finding a good set of parameters according to the loss, which is comprised of a three step process:

  1. parameterize the architecture with
  2. score such parameters with
  3. optimize such parameters with

todo. fri oct 2

  1. risk
  2. step zero?

The parameterization of functions and the subsequent parameter estimation are the primary workhorses in the function approximation of statistical learning. However with deep learning, we concern ourselves primarily with the class of models that are biologically inspired by the brain’s neuroanatomy. Although far from realistic, a neural network models the signals between the axon of one neuron to the dendrite of another with a set of weightsweights and biasesbias where . Such models are referred to as deep neural networks, in which we start by training single layer networks comprised of perceptronsperceptrons Stanford CS229: Machine Learning | Lecture 5 - Perceptron and Logistic Regression. The computational circuit of a perceptron displayed on the left side of Figure 1.3.1, where a perceptron produces a real-valued scalar by taking the sum of with parameters and .

Figure 1.3.1

The perceptron is a weighted sum of ’s and ’s, which can be rewritten as the dot productdot product of the two vectors and , and trivially transliterates to borscht.dot()

import borscht
import borscht.nn as nn

class Perceptron(nn.Module):
  def __init__(self):
    self.w = borscht.randn(d)

  def __call__(self, x: borscht.Tensor):
    return borscht.dot(self.w, x)

model = Perceptron()
y = model(x)
print(y)

where the bias term is folded by prepending and into and respectively, subsequently updating the dimension from to . With respect to the three step process, we have completed the first one of parameterizing the architecture . But before moving on to the second and third steps of scoring such parameters with and optimizing them with , you may have noticed that the domain of is rather than sentences . While we are now familiar with high dimensional arrays, a matrix and it’s row vectors were being used to model the stochastic matrix where discrete word indices were encoded into and out of such matrix with . Moreover, how is it’s codomain of classifying some category ?

Warning

So how is the perceptron taking in vectors directly,
and how does producing scalars perform a classification?
To answer this we need to take the proverbial red pill,
let us go deeper into the rabbit hole of the borscht.Tensor.

1.3.2 Analytic Geometry as Array Programming

Table of Contents (§1.3 Parameterizing Classification with Discriminant Functions)

Parameterizing classifiers and estimating such parameters forms the basis of programming software 2.0 with the statistical machine learning of neural networks. The primary class of parametric models under concern is that of the biologically inspired perceptrons , introduced in §1.3.1 The Parameter Estimation of Software 2.0. However, before proceeding to score and optimize the perceptron’s parameters, we realized the domain and codomain of the perceptron was and .

How is it possible that the perceptron is evaluating next-token prediction just like our bigram model, when it takes in some high dimensional array in from the get go (whereas the bigram model only used the higher dimensional matrices and arrays for the row stochastic matrix ?) and produces some quantitative output that is usually indicative of regression, rather than classification?

For now, we reconcile how the real-valued output is parameterizing classification with a direct decisiondecision without inferringinferring a probability distribution. We square the latter with the perceptron in §1.4 Parameterizing Classification with Probabilistic Models thereafter. The red pill needed for such reconciliation will deepen our understanding of vectors and matrices, both represented by the multidimensional array of the borscht.Tensor.

Important

The red pill is one which expands our previously conceived notion of vectors from the so-called computer scientist view of tensors as lists, to that of the geometrical view with vectorsvector as points in space. It is quite likely that you may have encountered vectors initially as geometrical points or lines, rather than as computational lists. Because of the inherent difficulty to visualize vectors that have support in high dimensional spaces where is greater than 3, we will start with an example of vectors in .

Let us consider a simplified language model setting where the goal is to perform binary classification with a context or history of length 1 on a vocabulary of length 2 where . And moreover, recall that we are parameterizing such next-token classification with weights and biases via perceptron , corresponding to def __call__(self, x): borscht.dot(self.w, x).

Starting with reconciling the type mismatch of our classifier with the perceptron’s domain of , we continue to map words from their string representation in to their positional indices in , however, there is an additional transformation of the input thereafter from to . The simplest idea for such a transformation is to map or embedembedding the position index to a -dimensional vector with all entries set to 0 except for the ’th dimension which is set to 1. Such a vector is referred to as a one-hot vectorone-hot vector and is known as a localist representationlocalist representation of a word, where each word’s meaning is represented by itself in isolation without a regard to the whole lanugage of English as a whole — for instance, adding two one-hot vectors is meaningless, since any vector with more than one non-zero entry is ill-defined. The final series of representational transformations of the input then looks like

where the corresponding function in borscht to map indices to their one hot vectors is borscht.nn.functional.one_hot(tensor, num_classes=-1) which takes a LongTensor with index values of shape (*) and returns a tensor of shape (*, num_classes).

import borscht
import borscht.nn.functional as F

X = borscht.Tensor([0, 1, 2, 3])
X_BV = F.one_hot(X)

When we map our words to their one-hot vector representations with the earlier constructed hashmap c2i, the point for the word is located on the -axis and the point for the word on the -axis. This is can seen in Figure 1.3.2

Figure I.3.2 On the left hand side the one hot vectors in are displayed for the strings . On the right hand side a decision boundary linearly separates into decision regions and .

Now that we’ve reconciled the type mismatch of our classifier with the perceptron’s domain , let us clear our confusion for the perceptron’s codomain of . In terms of a desirata for the decision step — that is assigning the input some outcome — it would be nice if we could divide the plane into two decision regionsdecision regions and associated with the output categories of and respectively with some line acting as a decision boundarydecision boundary such that our classifier assigned the next word given for , and perhaps given for Bear with the toy example, for although “york new” is non-sensical, it allows us to focus our attention on the mechanics of the perceptron itself..

Rather than define the decision boundary with an explicit equation for a line, another possible idea is to define it as the set of all points that are perpendicularperpendicular to the weight vector .

length(norm) = sqrt[angle(inner/dot product)]

How do we define the notion of perpendicularity without using our eye balls?

  • rotationrotation
  • orthogonal projectionorthogonal projection

foobar

  • distance with metricmetric
  • length with normnorm
  • length with Euclidean normEuclidean norm
  • length with Manhattan normManhattan norm
  • dot productdot product
  • inner productinner product
  • similarity with angleangle
  • dot productdot product =
  • pendicularity with orthogonalityorthogonal

and the final classifier has the form where is some function

Linear combinations, span, and basis vectors | Chapter 2, Essence of linear algebra (3Blue1Brown 2016)

Linear transformations and matrices | Chapter 3, Essence of linear algebra (3Blue1Brown 2016)

1.3.3 The Optimization of Scores in Software 2.0

Table of Contents (§1.3 Parameterizing Classification with Discriminant Functions)

Coding function bodies with assignment and control flow such as conditionals, loops are used to program software 1.0 whereas optimizing loss functionsoptimization such as the squared error losssquared error loss are used to program software 2.0.

baz

  • least squares lost per
    • when d=1, this is finding the line of best fit? (with bias)
    • but then we use the line of best fit as a discriminator with
  • is a linear system on =
  • min

1.3.4 Evaluation with Bias Variance Tradeoff

Table of Contents (§1.2 Next Token Prediction is Classification and Compression)

1.3.5 Summary and Further Reading

(Duda et al. 2001)’s Chapter 5. Linear Discriminant Functions; (Hastie et al. 2009)’s Chapter 3. Linear Methods for Regression; (Bishop 2006)’s Chapter 3. Linear Methods for Regression; (Murphy 2022)’s Chapter 11. Linear Regression

(Goodfellow et al. 2016)’s Chapter 2. Linear Algebra and Chapter 5. Machine Learning Basics; (Murphy 2022)’s Chapter 4: Statistics and Chapter 7. Linear Algebra

(Bishop 2006)’s Chapter 2. Probability Distributions; (Goodfellow et al. 2016)’s Chapter 3. Probability and Information Theory; (Murphy 2022)’s Chapter 2. Probability: Univariate Models, Chapter 3. Probability: Multivariate Models

(further reading for torch)































1.4 Parameterizing Classification with Probabilistic Models

Table of Contents

In which we…

1.4.1 Probabilistic Architectures: Discriminative vs Generative

In machine learningmachine learning, there are actually three ways to estimate a classifier. Recall that the definition of the conditional distributionconditional distribution is the joint over the marginal , and decomposing such joint with the product or chain rule starting with gives us a relationship of what is known as bayes rulebayes rule

Because the bigram model first produces an inferredinference conditional distribution which expresses it’s uncertainty of the next token followed by decidingdecision a specific token from such a distribution, the bigram classifier is an instance of a probabilistic architectureprobabilistic architecture. namely with a discriminative probabilistic modeldiscriminative probabilistic model

And so an alternative approach to modeling directly by (expand), the conditional distribution can be modeled as and is known as a generative probabilistic modelgenerative probabilistic model because it provides the opportunity to generate samples of the input space with where in the case of language modeling the input space is the set of all possible sentences ).

Finally, the inference stage can be skipped alltogether by simply discriminating a specific in one step (in the case of language modeling, the output space is the set of all possible next tokens ). Models that only consist of that one step are known as discriminant functionsdiscriminant functions and is where we actually begin our discussion of parametric models namely with linear regressionlinear regression corresponding to a single neuron, even though classification and regression were differentiated earlier as producing qualitative and quantitative outputs. We will revisit parameterizing the architecture of a discriminative probabilistic model with logistic regression in 1.4 Parameterizing Classification with Probabilistic Architectures.

non-parametricparametric
discriminative probabilistic modelbigramlogistic regression
generative probabilistic modelN/Anaive bayes
discriminant functionN/Alinear regression

which involves a two stage process of specification and implementation.

First, the distribution of must first be specified as a family of distributions parameterized by a set of parameters , followed by modeling itself as some parameterized function class

Our choice for is the Gaussiangaussian distribution with a meanmean and variancevariance where . That is, we are assuming that the output words are normally distributed conditioned on the input sentences .

whose parameters in turn are modeled as which is chosen to be the perceptronperceptron, a single layer neural network of weightsweights and biasesbiases where . That is, not only are we assuming that the outputs are normally distributed conditioned on the outputs , but also that the parameters to such a distribution can be modeled by a neural network .

where in turn is a function characterized or determined by it’s own set of parameters in addition to the input , and is denoted as or to reflect such. That is, is some function that is curried with parameters and returns a map from inputs to outputs , which is reflected in borscht.nn.Module where a model is first instantiated with Python’s builtin __init__, and subsequently evaluated with __call__.

With deep learning, often takes the form of a neural network parameterized by weightsweights and biasesbiases such that . In §1.3 and §1.4 we will understand and train single layer networks for to prepare us for the various network architectures explored in §3. Self-Supervised Sequence Learning with Deep Networks.

Figure 1.3.1

So, after the model family or class has been specified via parameterization, a specific model is implemented or instantiated by continuing to score the quality of the output with the likelihood (TODO) but rather as a function of parameters and moreover, optimizingoptimization said scoring function and denoted by . This process is referred to as parameter estimationparameter estimation, or more specifically, maximum likelihood estimationmaximum likelihood estimation (MLE)

Figure 1.3.2

1.4.1 Probabilistic Architecture with Logistic Regression (Old)

Table of Contents (§1.3 Parameterizing Classification with Discriminant Functions)

In contrast, in this chapter and for deep nets more generally, we will estimate by parameterizingparameters probabilities rather than counting them, namely with the weightsweights and biasesbias of a logistic regressionlogistic regression modelPerhaps logistic classification serves as better terminology, but we will shortly see why it’s called regression which can also be viewed as a single neuronneuron Stanford CS109 Probability for Computer Scientists | Logistic Regression | Lecture 24 Stanford CS229: Machine Learning | Lecture 5 - Perceptron and Logistic Regression from a deep net. That is, some function , which we will take the remainder of §1.3.1 Architecture with Logistic Regression to better understand.

A circuit for the logistic regression model is shown on the right hand side of Figure 1.3.1. Rather than produce the orange output with a stochastic matrix containing normalized relative frequency counts, the logistic regression model produces it by passing the blue input into the green summing and squashing functions and with the sum being configured — in this case weighted — with purple parameters and . Intuitively speaking, by parameterizing what we mean is that we will assume or model as some function that has configurable parameters. You can view the logistic regression model on the right as adding some indirection between the blue input and orange output with green machinery configured by purple parameters. The problem of training a good language model then reduces to that of finding a good set of parameters, in this case . That is, the process of training is parameter estimation.

Figure 1.3.1

Scanning from left to right of the neuronal circuit depicted in Figure 1.3.1, we first see that the input word "to" to the conditional probability that the logistic regression is modeling is some column vector with support in . So, starting from the string "to", we continue to map the string to it’s index in the vocabulary with some hash map , but then we need an extra step that maps the index to some vector . If you pay closer attention to the circuit, you will see that only whereas all the other . This is called a one-hot vectorone-hot vector, which semantically represents the same positional information within the vocabulary except vectorizes that information continuously with support in rather than discretely in . Because of this, the conditional probability is denoted as . Like 1.2_bigram.ipynb, the first cell in 1.3_logistic_regression.ipynb performs the data processing, except with the additional step of tensorizing integer indices over to one-hot vectors with F.one_hot(torch.tensor(indices)):

missing notebook: /opt/build/repo/sitp/notebooks/1.3_logistic_regression.ipynb
missing notebook: /opt/build/repo/sitp/notebooks/1.2_bigram.ipynb

So far we in the logistic regression notebook we have the same dataset, vocab and c2i. But because logistic regression takes in continuous inputs rather than discrete, one more step must be perform in its data processing, namely mapping (tensorizing) integer indices to their one-hot vectors (local, however). We construct list[int] first and convert to tensor’s second, for tensor.append()… (todo)

missing notebook: /opt/build/repo/sitp/notebooks/1.3_logistic_regression.ipynb

Where first five ’s are plotted as a heatmap with plt.imshow. Continuing our left to right scan of the logistic regression model depicted in Figure 1.3.1, the next aspect to the neuronal circuit is the core machinery which the one hot vectors are passed through, namely taking a weighted sum whose real-valued output is then mapped to a number between 0 and 1 with a squashing function . That is, the logistic regression assumption is .

Figure I.2.3

That is, we model as follows:

One small adjustment we can make to is to fold the final bias term into the vector as , increasing the total dimensionality so that , and adjusting accordingly so that and . Then, the equation of our line with bias is simply parameterized as

missing notebook: /opt/build/repo/sitp/notebooks/1.3_logistic_regression.ipynb

1.4.2 Optimizing Cross Entropy with Iterative Reweighted Least Squares

Table of Contents (§1.3 Parameterizing Classification with Discriminant Functions)

But what is cross-entropy? | Compression is Intelligence Part 2 (3Blue1Brown 2026)

missing notebook: /opt/build/repo/sitp/notebooks/1.3_logistic_regression.ipynb

1.4.3 Evaluation with Bias Variance Tradeoff

1.4.4 Summary and Further Reading

(Eisenstein 2019)’s Chapter 2. Linear text classification; (Jurafsky and Martin 2026)’s Chapter 4. Logistic Regression

(Hastie et al. 2009)’s Chapter 4. Linear Methods for Classification; (Bishop 2006)’s Chapter 4. Linear Methods for Classification; (Goodfellow et al. 2016)’s Chapter 5. Machine Learning Basics; (Murphy 2022)’s Chapter 10. Logistic Regression and Chapter 12. Generalized Linear Models

(MacKay 2003)’s Chapter 1. Introduction to Information Theory and Chapter 2. Probability, Entropy, and Inference; (Murphy 2022)’s Chapter 6: Information Theory

(further reading for torch)

1.4.4 bogus below

1.4.4 Architecture with Linear Regression

In the previous two chapters, we estimated the bigram and linear classifiers from self-supervised data which predicted the next word given a sentence . For the inference step, the core machinery of the bigram model was the row stochastic matrix which corresponded to a borscht.Tensor with .shape of (V, V). In contrast, the parameterization of next token prediction by the logistic regression model used a weighted sum which was passed into a squashing function, namely the generalization of the sigmoid which was the , ending with the scoring function of the cross entropy loss (todo). This corresponded to (todo)

In fact, the linear classifier of multi-class logistic regression corresponds to a single layer of a deep neural network, and architecturally speaking, you are ready to move on to §3. Self-Supervised Sequence Learning with Deep Networks in Part II. However, there is more foundation for us to build, namely with the parameterization of quantification rather than classification with the linear regression modellinear regression model, and generalizing the family of linear models with the notion of generalized linear modelsgeneralized linear models. (todo, foreshadow svd being approximated in muon with newtonschulz5?)

Let us begin with the neuronal circuit for linear regression, which is in fact a simplification of the logistic regression model. Namely, after the weighted sum, there is no squashing.

Figure 1.4.1

Recall from §1.2.1 The Estimation of Software 2.0 that the regime of regression is when the function to be estimated is of the form where .

Similarly to learning distributions from data, learning functions from data roughly follows the three step process of selecting a model for the task , a performance measure , and optimizing such measure on experience . Let’s take a look at the linearly modeling regression with squared error loss, a problem which straddles tools from all three languages of probability theory, linear algebra, and calculus.

For the task of house price prediction, the input space is the size in square feet, and the output space is the price, meaning that the function which needs to be recovered has the type of . An assumption about the structure of the data needs to be made, referred to as the inductive bias. The simplest assumption to make is to assume that there exists some linear relationship between the data and parameters so that ends up being modeled as an inner product plus bias, parameterized as :

One small adjustment we can make to is to fold the final bias term into the vector as , increasing the total dimensionality so that , and adjusting accordingly so that and . Then, the equation of our line with bias is simply parameterized as

Semantically speaking, each property of the input house is weighted by some weight , adjusted accordingly during the learning process so that it accurately reflects the property’s influence on the output price when evaluating a prediction . We can evaluate on all with a randomly intialized to ensure we’ve wired everything up correctly.

However, while specifying the equation of the linear regression model on paper screen and subsequently evaluating the computation manually might have been sufficient for pre-computational mathematicians such as Gauss and Legendre, the discipline of statistical learning is entirely predicated on computational methods. Let’s continue to use PyTorch’s core n-dimensional array datastructure with torch.Tensor, this time modeling a function rather than some distribution :

(todo: maybe change example to closure following torch.nn)

{{#include ../../teeny/examples/2.2-regression.py:1:4}}

in which we can update the names of our torch.Tensor variables with the ranks of their vector spacesReferred to as Shape Suffixes, described by Noam Shazeer, an engineer from Google to clarify what dimensions these operations are evaluated on:

{{#include ../../teeny/examples/2.2-regression.py:5:8}}

and finally, actually enforce them via runtime typechecking with the jaxtyping package (try swapping the return statements below to trigger a type error with the return type):

{{#include ../../teeny/examples/2.2-regression.py:9:19}}

Let’s now sanity-check that f_typechecked is wired up correctly by evaluating it on all with random parameter w_D. (todo: use actual housing data)

{{#include ../../teeny/examples/2.2-regression.py:29:41}}
random weight vector: tensor([-0.3343,  0.0768,  2.7828, -0.1331])
expected: $500000.0, actual: $-492.46
expected: $800000.0, actual: $-690.89
expected: $250000.0, actual: $-258.08

Clearly, these outputs are unintelligible, and will only become intelligible with a “good” choice of parameter . Before we find such a parameter, let’s make one more modification to our function to increase it’s performance. Rather then evaluating f_typechecked on every input xi_D in the dataset zip(X_ND,Y_N) sequentially with a loop, we can evaluate all outputs with a single matrix-vector multiplication by updating ’s function definition to the following

and the corresponding PyTorch updated to

{{#include ../../teeny/examples/2.2-regression.py:21:27}}

Now that we’ve selected our model for the task , we can proceed with selecting our performance measure which the learner will improve on with experience .

1.4.5 Loss Function with Least Squares


After the inductive bias on the family of functions has been made, the learning algorithm must find the function with a good fit. Since artificial learning algorithms don’t have visual cortex like biological humans[], the notion of “good fit” needs to defined in a systematic fashion. This is done by selecting the parameter which maximizes the likelihood of the data . Returning to the linear regression inductive bias we’ve selected to model the house price data, we assume there exists noise in both our model (epistemic uncertainty) and data (aleatoric uncertainty), so that where

prices are normally distributed conditioned on seeing the features with the mean being the equation of the line where , then we have that

Returning to the linear regression model, we can solve this optimization with a direct method using normal equations. QR factorization, or SVD.

def fhatbatched(X_n: np.ndarray, m: float, b: float) -> np.ndarray: return X_n*m+b

if __name__ == "__main__":
  X, Y = np.array([1500, 2100, 800]), np.array([500000, 800000, 250000]) #  data

  X_b = np.column_stack((np.ones_like(X), X))              # [1, x]
  bhat, mhat = np.linalg.solve(X_b.T @ X_b, X_b.T @ Y)   # w = [b, m]

  yhats = fhatbatched(X, mhat, bhat) # yhat
  for y, yhat in zip(Y, yhats):
    print(f"expected: ${y:.0f}, actual: ${yhat:.2f}")

To summarize, we have selected and computed

  1. an inductive bias with the family of linear functions
  2. an inductive principle with the least squared loss
  3. the parameters which minimze the empirical risk, denoted as

Together, the inductive bias describes the relationship between the input and output spaces, the inductive principle is the loss function that measures prediction accuracy, and the minimization of the empirical risk finds the parameters for the best predictor.































Intermezzo One: The Language of Dimension and Stochasticity

Table of Contents

While I am trying to be as precise as possible, I chose to use English rather than formal specifications for everything except syntax and lexical analysis. This should make the document more understandable to the average reader, but will leave room for ambiguities. Consequently, if you were coming from Mars and tried to re-implement Python from this document alone, you might have to guess things and in fact you would probably end up implementing quite a different language.

Guido van Rossum, The Python Language Reference

But then, Nicias, courage, according to this new definition of yours, instead of being a part of virtue only, will be all virtue?

Socrates, in Plato’s Laches

There are good reasons why the theorems should all be easy and the definitions hard. As the evolution of Stoke’s Theorem revealed, a single simple principle can masquerade as several difficult results; the proofs of many theorems involve merely stripping away the disguise. The definitions, on the other hand, serve a twofold purpose: they are rigorous replacements for vage notions and machinery for elegant proofs.

Michael Spivak, Calculus on Manifolds





























































Fixed-Size DataA modified excerpt from How to Design Programs (Felleisen et al. 2018)’s Intermezzo 1: Beginning Student Language §1. Self-Supervised Sequence Learning with Single-Layer Networks deals with BSL the NETSOR language implemented by borscht.Tensor as if it were a natural language. It introduces the “basic words” of the language, suggests how to compose “words” into “sentences,” and appeals to your knowledge of algebra for an intuitive understanding of these “sentences.” While this kind of introduction works to some extent, truly effective communication requires some formal study.

I1.1 From Computation to Mathematics

All throughout §1. Self-Supervised Sequence Learning with Single Layer Networks, we defined various functions which called models to approximate nature’s true mapping of the input space of sentences to the outspace of tokens. The first bigram model counted probabilities with a stochastic matrix, and the second perceptron model discriminated with a weighted sum, but also inferred the distribution by composing the weighted sum with a squashing function. Although these stochastic and infinitely continuous functions are not the usual deterministic and discretely finite ones we’re used to implementing — with assigning bindings, controlling the flow with if statements and loops, printing to standard output — as programmers we are comfortable with the notion of function, whether you specify one with fn, fun, function, or of course our dear friend def. But if someone who was not as well-versed as you asks for the definition of the function as a concept itself, what is your answer?

Can we say that a function is a rule that maps one input to a unique output?

Up until the 1900s, this was an acceptable definition!
For bettter and for worse (for us programmers, it’s often the latter), the mathematicians of the 20th century wanted a language that was more precise. Take for example the familiar quadratic from high school — it’s much simpler than a bigram’s stochastic matrix or a perceptron’s weighted sum. We can write down the algebraic equation of the quadratic with pen and paper (or rather ( code \LaTeX)), computational code with borscht, and also it’s corresponding geometric curve with a plot

import torch
import matplotlib.pyplot as plt
%matplotlib inline
plt.rcParams["figure.figsize"] = (4, 3)

def f(x: torch.Tensor):
  return x ** 2

x = torch.linspace(-3, 3, 200)
y = f(x)
plt.plot(x, y)
Out[2]:
[<matplotlib.lines.Line2D at 0x117619400>]

To any mathematician whom lived before the 20th century, they would tell you that the quadratic is indeed a function. If you asked them why, they would respond, well, because it’s a rule that passes the vertical line test — there is a unique output for every input.

If you ask any mathematican who comes after, they would and will tell you that all of representations above describe a function — namely our long time friend the quadratic — and that the vertical line test indeed gets to the notion of the concept, but it does not precisely define the concept of function. To illustrate this, they would take the questioning one level further, and ask, well what does “rule” mean, and what does “unique” mean? (Notice how those two words have quotes now!) They are not satisfied by defining the concept of a function with a machine and pencil.

As programmers learning mathematics, we can view this transition of mathematics similar to different styles or paradigms of programming Even though those terms in of themselves are non-sensical!. The same way post 20th century mathematicians are uncomfortable defining the concept of a function with vague notions of a machine and pencil, functional programmers are uncomfortable evaluating expressions with mutation, objective oriented programmers on the other handA false dichotomy but useful one for our current purposes. For instance, Python has lambdas and classes, so what category is it considered as? Go take a programming language course to find out! We recommend the excellent https://www.plai.org/ can’t live without it. These two groups of mathematicians are describing the same objects, and the two groups of programmers are evalauting the same objects. They simply have different relationships to them. Specifically for mathematics, the post 20th century mathematician arguably has a deeper relationship with the notion of function. So, if they are uncomfortable defining the concept of a function with a machine and pencil, what do they want to define it with?

We know that good artists copy, while great artists steal. And these 20th century mathematicians were inspired by an Ancient Greek mathematician named Euclid who lived around two milleniums ago, who did to geometry what they were hoping to do for functions.

Start with some assumptions called axioms.
Define mathematical objects using a definitional language, precisely describing the what.
State beleifs about such objects,
and prove them using an assertion language, precisely describing the why.


The goal of this intermezzo is to do the same, formalizing the notions and beliefs of the mathematical objects we’ve been using all throughout §1. Self-Supervised Sequence Learning with Single Layer Networks. While we start with functions in §I1.1, Relations and Functions, we will be doing the same for the linear transformations of vector spaces over fields in §I1.2 Vector Spaces and Linear Maps, as well as probability distributions of random variables from probability spaces in §I1.3 Probability Spaces and Distributions.

This is going to be very frustrating for us programmers!
We are already familiar with the data structures and learning algorithms of such objects!

We know that a discriminative perceptron is a weighted sum that linearly separates space.
We know that assuming bernouilli results in the cross-entropy loss.
We know that batching them results in a linear system of equations that we can directly solve.
We know that a probabilistically discrminative perceptron squashes the weighted sum into a probability.

What’s the purpose of defining their definitions and proving their properties?
To transfer our understandingThe preface provides more motivation..

Before we deepen our relationship,
with the dimensionality provided by linear transformations of vector spaces
and the stochasticity provided by probability distributions of probability spaces,
let us first understand the language we use to describe these languages.

Deeper the rabbit hole we go.































I1.1.1 Sets and Built-in Numbers

Table of Contents (I1.1 From Computation to Mathematics)

No one shall expel us from the paradise which Cantor has created for us.

David Hilbert

Mathematics and computation have a close relationship with one another. As programmers, we naturally gravitate towards the mathematical languages that overlap with the discrete algorithms and data structures we program on a daily basis. Graph theory is the prime example, where we can model relationships between objects with a graph , which is defined as a set of vertices , and a set of unordered edges that connect them.

A subtle thing just happened with the previous sentence.
We provided a general definition of a mathematical object, and you most likely imagined a graph of concrete objects. Perhaps you thought of a graph of web pages like a search engine, a graph of people like a social network, or even a graph of autonomous vehicles. Whatever the case may be, the concept of a graph was genericallyIf you are familiar with parametric polymorphism such as generics, it is quite similar. defined by using the notion of a set, which is an unordered collection of elements.

As programmers we nod along with passing familiarity. Indeed, all programming languages provide sets. In fact with Python all that is needed is set(). From the Python tutorial’s 5.4 Sets, we can construct a set of fruits with {'apple', 'orange', 'apple', 'pear', 'orange', 'banana'}In Python, set() expect’s at most 1 argument, and {} by itself creates an empty dictionary!.

A future update to this book will use type theoretic foundations with Lean.

Another subtle thing also just happened with the previous sentence.
The mathematician’s relationship with the set is to define or produce other mathematical objects.
The programmer’s relationship with the set is to use it model real world objects.
…
The more accurate analogy for the mathematician’s set is the programmer’s type.
As a programmer, if you were asked to define a graph data structure,
you would use a programming language’s type system.
This is why mathematics begins with set theory.A future update to this book will use type theoretic foundations with Lean. The hacker’s paradise.


Up ahead, we will informally present the notion of sets and fields, in order to formalize the concept of real-valued functions. We will then be ready to formalize linear transformations on n-dimensional Euclidean spaces in I1.2 Vector Spaces and Linear Maps. That is, we will first define what precisely means so that we can do the same with .

Before we formalize the notion of a mapping from inputs and outputs into a precise concept with sets, we need to formalize the notion of sets themselves.

A setset is an unordered collection of objects.

Although we can have sets of any object’s we like — fruits as one timely example — the primary sets we concern ourselves with are sets of numbers since the functions of software 2.0 are of numbers.

To construct a set, you enclose it’s elements with curly braces ( code \{\}).

Mathematicians, like programmers, are lazy. Sometimes it’s too tedious to specify every element of a set, and most elements are ellided with an ellipsis ( code \dots). For instance, the set of all numbers from 1 to 10 is constructed as

Finally, we may have a set of infinite numbers, such as natural numbers, integers, rationals, real numbers, and the complex ones. For such infinite sets, we place the ellipsis at the beginning or end. Note how the ellipsis placed at beginning or end of a set has a very different meaning than one used to elide elements in the middle.

import torch

print(torch.int64)
print(torch.float64)
print(torch.complex64)
torch.int64
torch.float64
torch.complex64

Computationally speaking, borscht aspirationally approximates with borscht.int64, with borscht.float64 and with borscht.complex64. The finite precision representations of these sets have quite different definitions than their infinite counterparts because the unbounded elipses is quite problematic with finite memory. We’ll cover more of these differences when we implement borscht itself in 2. The Massively Parallel Multidimensional Tensor. For the time being, keep in mind this slight abuse of contrast between the mathematics and computation.

The way borscht.dtype’s are used in practice is to specify that certain computations are carried out in certain types of precision. We can generate numbers with borscht’s random sampling methods, and verify their dtype with Tensor.dtype.

import torch

some_ints = torch.randint(-10, 11, (3,))
some_reals = torch.rand(3, dtype=torch.float64)*20-10
print(some_ints, some_ints.dtype)
print(some_reals, some_reals.dtype)
tensor([9, 0, 4]) torch.int64
tensor([-5.1043,  4.5641,  9.7501], dtype=torch.float64) torch.float64
  1. To generate a list of three random integers , we call borscht.randint(low, high, size) where low is the lowest int to be drawn inclusively, high is exclusively one above the highest int, and size is a tuple for the shape of the output Tensor which is used as a list.
  2. To do the same with real numbers , we call borscht.rand(size) which produces a tensor of random numbers limited to the range where size is the shape int… of the output Tensor. We broadcast multiply by to get a range and broadcast subtract by to shift it over to .

The entire purpose of the concept of a set is to distinguish whether objects are elements or are not elements of the set.

A setset is a collection of unordered objects from a universe. The collection of everything in the universe is called the universal set denoted by ( code: \mathscr{U})

For some set , the objects are called elements and denoted by ( code: x \in X).

The productproduct of two sets and is the set

denoted by ( code: X \times Y).

For a given element (x,y) of the set product, we say that is the x-coordinate and is the y-coordinate.

I1.1.2 Relations and Functions

Table of Contents (I1.1 From Computation to Mathematics)

Although the formal concepts of sets and the real numbers are precisely defined in Appendix A. Numbers, we now better understand their notions as unordered collection of objects and an ordered field with the usual operations of addition, substraction, multiplication, and division well-defined. We have one more notion to formalize before we can define what a function precisely means. That notion is the one of relation.

A relationrelation on sets and is a subset of their product

For some relation ,
we say that the set is the domain of R,
and that the set is the range of R.

For instance, let us consider the relation

import torch
import matplotlib.pyplot as plt
%matplotlib inline
plt.rcParams["figure.figsize"] = (4, 3)

def f(x: torch.Tensor):
  return x.sqrt()

x = torch.linspace(-3, 3, 200)
y = f(x)
plt.plot(x, y)
plt.plot(x, -y)
Out[21]:
[<matplotlib.lines.Line2D at 0x117908590>]

That was quite some work. And for what? Just to have precise and well-defined concept of a real-valued function which we have already used all throughout §1. Self-Supervised Sequence Learning with Single Layer Networks? For better and for worse, yes! Now that the concept of functions are well-defined, when we represent a function algebraically with pen and paper (or rather ( code \LaTeX)), computationally with code, or geometrically with it’s plot

import torch
import matplotlib.pyplot as plt
%matplotlib inline
plt.rcParams["figure.figsize"] = (4, 3)

def f(x: torch.Tensor):
  return x ** 2

x = torch.linspace(-3, 3, 200)
y = f(x)
plt.plot(x, y)
Out[2]:
[<matplotlib.lines.Line2D at 0x117619400>]

you understand the conecpt of a function more generally, and precisely. A function is a subset of a product such that ___. Now that we have formalized the concept of real-valued functions, let us move on and formalize linear transformations on n-dimensional Euclidean spaces.































I1.1.3 Field of Scalars, and List of Coordinates

Table of Contents (I1.1 From Computation to Mathematics)

I1.2 Vector Spaces and Linear Maps

Table of Contents (Intermezzo One)

Unfortunately, no one can be told what the Matrix is. You have to see it for yourself.

Morpheus

The introduction of numbers as coordinates is an art of violence.

Herman Weyl

I1.2.1 Vector Space over Fields

We are now ready to abstract what is common in both the computational perspective of vectors as lists and the geometrical perspective of vectors as points. Such generalization is viewing vectors as objects with operations of addition and scalar multiplication.

Table of Contents (Intermezzo One)

Abstract vector spaces | Chapter 16, Essence of linear algebra (3Blue1Brown 2016)

In the video, 3Blue1Brown mentions that a vector is any mathematical object

A d-length listlist for some is an ordered collection of elements, denoted by with code (x_1, \dots, x_d).

For two lists and , we say they are equal if and only if and . That is, if they are the same length whose elements agree at every position.

A d-dimensional Euclidean spaceEuclidean space for some is the set denoted by with code: \mathbb{F}^d and it’s elements by with code: \mathbf{x}.

For some and , we say that is the ith coordinate of .

A vector spacevector space

missing Lean module: SitpLean.Vectorspace

A vectorvector or point is an element of a vector space.

A subspacesubspace is a

Summary of newly introduced Lean constructs

I1.2.2 Combinations and Bases

Table of Contents (Intermezzo 1.2 Vector Spaces and Linear Maps)

Spans, linear combination, linear dependance, bases




A linear combinationlinear combination of a list of vectors is a vector of the form

where .

The spanspan of a list of vectors is the set of all their linear combinations:

and is denoted by

If , we say that the list spans .

I1.2.3 Linear Maps and Matrices

Table of Contents (Intermezzo 1.2 Vector Spaces and Linear Maps)

Matrices are linear maps




A linear maplinear map from vector space to is a function where satisfies

additivity:

homogeneity:

The null spacenull space of a linear map is the subset of

denoted by

The column spacecolumn space of a linear map is the subset of

denoted by

An n-by-m matrixmatrix is a rectangular array:

denoted by , where .

I1.3 Probability Spaces and Distributions

Table of Contents (Intermezzo One)

In which we

I1.3.1 Probability Spaces

Informally speaking, you intuitively understand the notion of a probability and a probability distribution. That is, a probability is a number between 0 and 1 that represents the chance, likelihood, or belief of some event happening, and a distribution is a list of such numbers. Probability distributions come from the concept of probability spaces, so before defining the former we must define the latter, which are measures of the size of sets.

Figure I.1

Figure I.1 is comprised of three components. Namely, the blue box and it’s blue dots, the colored boxes, and the mapping from the colored boxes to the number line

  1. the sample space : the set of all possible outcomes in an experiment
  2. the event space : the set of all subsets in the sample space
  3. probability law : a mapping from the event space to a number between 0 and 1

Conceptually speaking, the language models we’ve seen in §1. Sequence Learning such as GPT-2, the bigram model, and the logistic regression model which have type produce (on a single evaluation) concrete probability distributions with borscht.Tensor that come from mathematically abstract probability spaces where the sample space is the set of words in their vocabulary, and their belief in a word occuring is represented by probability law defined on event space .

Caution

Probability spaces are usually NOT implemented with code. For instance, GPT-2, bigram models, and logistic regression models do NOT instantiate and return Python set()s for the sample space , the event space , nor define some function def probability_law(event_space: set) -> float for . They return a probability distributions, which we define in §I.2 Random Variables and their Distributions. Rather, the purpose of probability spaces as a mathematical definition is conceptual clarity with general definitionsDefining concepts without commiting to internal structure allows for a much broader class of objects to be considered. This is very much like type polymorphism (generics, traits, type classes), except the generality of mathematical concepts are usually “more” general than those that might be implemented with type polymorphism. For instance, this abstract definition of a probability space is generalized to cover any random experiment we’d like to model, from rolling dice, to generating language. However it’s quite unusual to have a program need to progam both. and rigorous results with theorems.

Let us now precisely define each component of a probability space , starting with the sample space .

A sample spacesample space is the set of all possible outcomes in an experiment denoted by ( code: \Omega).

  • Consider flipping a coin. Then .
  • Consider playing rock, paper, scissors. Then .
  • Consider rolling a die. Then .
  • Consider drawing a card. Then .

Consider generating a word from this very sentence. Then,

So conceptually speaking, language models like GPT2, the bigram model, and the logistic regression model of type which produce concrete distributions with borscht.Tensor on a single evaluation come from mathematically abstract probability spaces where the sample space is the set of words in their vocabulary, and their belief in a word occuring is represented by probability law defined on event space .

Important

Notice how we have NOT said anything about multiple evaluations of a language model’s output distribution (and probability space) over words to generate a complete sentence (in the probablity space of sentences). We will see how they relate soon.

An event spaceevent space is the set of all possible subsets of the sample space denoted by ( code: \mathcal{F}).

TODO: something to say here about event spaces? cardinality of them?

A probability lawprobability law is a function that sends an event to a number between 0 and 1.

Consider flipping a coin. Then we model the experiment with sample space , event space , and probability law

[!QUESTION] Why is the probability law defined as a function on the event space rather than the sample space ? Pause, think! Hint: Would something bad happen if we did such a thing?

A probability lawprobability law is a function that sends an event to a number between 0 and 1, satisfying three axioms:

  1. non-negativity:
  2. normalization:
  3. additivity:

However the event which contains all foobarbaz is composed of overlapping events (corresponding to the right figure above), so the axiom of addivitiy does not apply. We can evaluate the union of non-disjoint (overlapping) events with the following corollary, known as the sum rulesum rule:

We have now sharpened our intuitive notion of chance, belief, and probability by formally defining them as the measure of the size of sets. These sets have a home, and the measure has a domain, namely the sample space and the event space and together with the probability law , comprise the definition of a probability space , albeit abstractly so. Let us now connnect this mathematically abstract space to the more concrete probability distributions we used in §1. Sequence Learning.

I1.3.2 Random Variables and their Distributions

Table of Contents (Intermezzo One)

Probability distributions are the practical and concrete borscht.Tensors we used throughout §1. Sequence Learning when implementing the bigram and logistic regression language models. So how do these connect to the abstract concept of probability spaces just defined? The answer is with random variables and their probability mass functions, which are functions that map outcomes to states, and from states to probabilities.

Figure I.2

(todo modify picture or include another picture to show distribution (probality assignment of variable taking on state))

Figure I.2 shows the same sample space and event space from §I.1 Probability Spaces, but rather than evaluate probabilities with the probability law by mapping events to the [0,1] number line (displayed in Figure I.1), we create one level of indirection with

  • a random variable mapping outcomes in the sample space to states on the real number line and then defining
  • a probability mass function mapping states to their probabilities.

So, starting with a sample space, you have to map outcomes to their state space with a random variable before you can evaluate their probabilities with a probability mass function, rather than evaluate them directly with a probability law. In other words, the two types are not equivalent . For example, recall from §1.1 From Certain to Uncertain Knowledge GPT-2’s borscht.Tensor output probs when passed the input string "Hello world, nice to":

import numpy as np

# GPT-2's output distribution when passed input "Hello world, nice to"
# Showing top 10 most likely words out of 50257.
tokens = [" see", " meet", " hear", " have", " be", " know", " you", " talk", " say", " get"]
probs = np.array([0.3169, 0.1268, 0.1246, 0.1046, 0.0471, 0.0352, 0.0320, 0.0128, 0.0114, 0.0067])
for i in np.argsort(-probs):
    print(f"{tokens[i]} : {probs[i]:.4f}")

Here, there is a random variable which maps outcomes from the abstract sample space to concrete states in state space . That is,

But notice the level of indirection with the random variable. All we’ve done is translate from sample space to state space. To obtain actual probabilities, we need to map from the state space to probabilities with the probability mass function . That is,

In other words, starting from sample space we compose with to obtain such that evaluating results in the probability of outcome . (todo. this is wrong?)

But how is the multidimensional array probs with support in related to the abstract concept of a probability space? You may have noticed that the definition of looks quite similar to that of the probs:

import numpy as np
probs = np.array([0.3169, 0.1268, 0.1246, 0.1046, 0.0471, 0.0352, 0.0320, 0.0128, 0.0114, 0.0067])

This is because an alternative conceptualization of is some function For conveniece sake we do store the outcomes in the tokens array so that we can print them next to their probabilities by looping over states with for i in np.argsort(-probs), but it’s important to note that is defined on real numbered states (represented with indices), and not on set-valued events. that has support in , or more accurately, With these two views, an alternative conceptualization of is some function

  • todo: rvs map from one measurable space to another. we happen to like

On a single evaluation language models produce such probability mass functions defined on a state space which are mapped over from an abstract sample space via random variable. In practice, we forget about the concepts of sample spaces and random variables, simply slugging around probability mass functions with borscht.Tensors that have support in n-dimensional Euclidean space, where n is the size of sample space. In other words, we keep two as conceptual mathematical definitions without implementing them with code.

At this point, you may be wondering what is the point of the random variable’s indirection? … Now that you understand the notion of random variables and probability mass functions intuitively, let’s formally define them as concepts:

somehow motivate global view

language modelslanguage models

The intuition is very simple: every language model can be locally normalized. The precise formulation however, is not. proof with prefix probabilities

proof

however the converse direction is not a trivial result to establish examples

  • 2 state model (markov chain), non-tight
  • 2 state model (markov chain), tight (absorbs)

I1.3.3 Joint, Marginal, Conditional, and Bayes Rule

Table of Contents (Intermezzo One)































La Perspective practique (Jean Du Breuil 1642)

2. The Massively Parallel Multidimensional Tensor

Table of Contents































2.1 Virtualizing Shapes with Strides

Table of Contents

(todo, cover other subchapters). In which we introduce the so-called three language problem that deep learning frameworks suffer from, which introduce accelerated languages for the host and device with Rust and CUDA Rust, introduce the Rust programming language, and finally, set up a mixed source project of Python and Rust for borscht with Rust bindings for Python via PyO3 and it’s build tool maturin.

Table of Contents (§2. The Massively Parallel Multidimensional Tensor)

2.1.1 From Virtual to Physical Machines

Table of Contents (§2.1 Virtualizing Shapes with Strides)

The Three Language ProblemThree Language Problem is the colloquial way of describing the fact that many deep learning frameworks are implemented with three different programming languages. This multi-language implementation for a single system actually has precedent in scientific computing before the advent of deep learning. Traditionally, scientists of the natural such as physicists, chemists, biologists or psychologists who needed to perform numerical computation reached for frameworks and languages such as MATLAB, R, numpy, and julia that have a high levelhigh levelSemantics… veneer-like language for productivity with a low levellow level internal implementation language for performance. Roughly speaking, such high level languages refer to those such as Python that are closer to the English language which humans speak, whereas low level languages refer to those such as Fortran, C++ or Rust that are closer to the machine languages that electricity “speaks”. The process of computation, at the end of the day, is to bridge such a semantic chasm through the orchestration of electrons.

Figure X.Y.Z

Really, the distinction of high level vs low level programming languages is a non-sensical category error, for what this distinction is really trying to gesture at is whether a programming language’s implementation is interpretedinterpreted or compiledcompiled — in other words, whether it’s evaluatedevaluated or translatedtranslated. The difference between an evaluator and a translator is in their return value. For instance, consider a language that can only perform arithmetic on numbers. Then an evaluator returns the actual value of the number, whereas the compiler returns a translation of program from the sourcesource language to a targettarget language.

def interpret_python(python_prg: str) -> num
  raise NotImplementedError("my awesome python interpreter")
def compile_python(python_prg: str) -> str
  raise NotImplementedError("my awesome python compiler")

However, even compilers exist for higher-level languages such as the compiler that translates Typescript to Javascript, which is then evaluated by a Javascript virtual machine living in the browser you are using to read these very words. In fact, interpreters exist for lower-level languages! Rust has a user-land interpreter called evcxr, and an internal interpreter to the rustc compiler. So we must refine such distinctions one more. What we are truly after is a language whose primary implementation is a compiler which targets a low-level machine languge executed by a physical processorphysical processorIn the sciences artificial, all distinctions are usually falsely dichotomous and are moreso spectrums. Languages such as Java, Javascript, and soon to be Python can by default be interpreted by a virtual machine, and then compiled just-in-time to a physical machine. The Python JIT and Javascript previous JITs are largely influenced by the work of Cliff Click on the Java JIT.. And because the primary implementation of the Rust programming language is the compiler rustc which targets architectures such as x86, ARM, and RISC-V. Such languages are referred to as nativenative because the translated program is in the machine’s native language. So with everything said and done, we will be implementing borscht with a veneer of productive Python code which is bound to accelerated Rust code.If you want to follow along with a pure Haskell implementation, go for it. Let us take charge of our own education, as ultimately we are the captain of our own ship.

Starting with Rust, although it’s similar to Python in that is has support for structured programmingSee Structured Programming (Dahl et al. 1972). with control flow and functions, in contrast to Python, Rust is a statically typedstatically typed programming language in that it has variables whose types must be known at compile time before the program is executed. What differentiates Rust from other languages though is it’s unique combination of two language features that change the way we program data: ownershipownership inspired by systems programmingsystems programming and algebraic data typesalgebraic data types inspired by functional programmingfunctional programming. We will explain such language features in their use up ahead (and also make comparisons to C++, the lingua franca of systems programming). If you’d like to revisit Rust from the ground up, please consult Brown’s fork of The Rust Programming LanguageSee Will Crichton: Rust for Everyone! , specifically Chapter 4 for ownership, and Chapter 5 with Chapter 6 for algebraic data types.

First, get yourself an installation of the Rust toolchaintoolchain specific to your operating system architecture with the command line tool rustuprustup, which installs the Rust programming language from the official release channelsSee The Rust Book Appendix G, and The rustup Book §2.1 Although non-technical, this more social aspect of continually evolving the Rust programming language is a unique differentiator from C/C++.. The Rust programming language provides excellent documentation for it’s tooling — the rustup docs explain that:

rustup is a toolchain multiplexer. It installs and manages many Rust toolchains and presents them all through a single set of tools installed to ~/.cargo/bin. The rustc and cargo executables installed in ~/.cargo/bin are proxies that delegate to the real toolchain. rustup then provides mechanisms to easily change the active toolchain by reconfiguring the behavior of the proxies.

So when rustup is first installed, running rustc will run the proxy in $HOME/.cargo/bin/rustc, which in turn will run the stable compiler. If you later change the default toolchain to nightly with rustup default nightly, then that same proxy will run the nightly compiler instead.

This is similar to Ruby’s rbenv, Python’s pyenv, or Node’s nvm.

Such a toolchain includes several components: the core ones being standard librarystandard library stdstd, the reference compilercompiler rustcrustc, and the package managerpackage manager cargocargo to drive rustc on your behalf. There are some other tools like clippy and rustfmt, and rust-docs, which we will use in due time.

However, rather than get the standard hello world program up and running with Rust, recall that borscht requires a mixed source of Rust and Python. Luckily for framework developers in numerical and scientific computing, the Python language is aware of how crucial natively accelerating mixed source software, that there is official support to extend the Python’s reference interpreter CPython with native C/C++ code. In fact, many of Python’s standard library modules are implemented themselves in C and C++.

Alternatively pybind, and nanobind, which are used by tensorflow and torch.

We will be doing everything with the help of PyO3, a Rust package that provides Rust bindings to the Python’s reference interpreter CPython via it’s C API. Driven by the build tool maturin, the development process compiles Rust into a dynamic librarydynamic library with the C ABI (expand) which CPython can import as a native extension modulenative extension module (expand).

While we can create such a mixed source project manually, let us use the build tool maturin. First install maturin (expand). Now, although our agents read most of our man pages on behalf of us these days, let us read it’s man pages. Usually, most command line tools provide their man pages by passing in some variant of -h or --help command line flags. Let’s try maturin -h:

$ maturin -h
Build and publish crates with pyo3, cffi and uniffi bindings as well as rust binaries as
python packages

Usage: maturin [OPTIONS] <COMMAND>

Commands:
  build           Build the crate into python packages
  publish         Build and publish the crate as python packages to pypi
  list-python     Search and list the available python installations
  develop         Install the crate as module in the current virtualenv
  sdist           Build only a source distribution (sdist) without compiling
  init            Create a new cargo project in an existing directory
  new             Create a new cargo project
  generate-ci     Generate CI configuration
  upload          Upload python packages to pypi
  generate-stubs  Autogenerate type stubs
  help            Print this message or the help of the given subcommand(s)

Options:
  -v, --verbose...  Use verbose output
  -h, --help        Print help (see more with '--help')
  -V, --version     Print version

Visit https://maturin.rs to learn more about maturin.

(expand, what’s pyo3, cffi, and uniffi bindings). We even see that it points us to it’s web documentation https://maturin.rs, which you should definitely consult in addition to our journey (change this word) here. What we want is to create a new project, without any existing directory, so maturin new seems like the command we want. The Usage: line above the list of Commands: tells us how we can invoke commands with maturin, namely with maturin [OPTIONS] <COMMAND>. They also conveniently list Options below, and we see that -h is in fact a flag on commands themselves. So, let us recurse and check out the command’s man pages with maturin new -h.

$ maturin new -h
Create a new cargo project

Usage: maturin new [OPTIONS] <PATH>

Arguments:
  <PATH>  Project path

Options:
      --name <NAME>          Set the resulting package name, defaults to the
                             directory name
  -v, --verbose...           Use verbose output
      --mixed                Use mixed Rust/Python project layout
      --src                  Use Python first src layout for mixed Rust/Python
                             project
  -b, --bindings <BINDINGS>  Which kind of bindings to use [possible values:
                             pyo3, cffi, uniffi, bin]
  -h, --help                 Print help (see more with '--help')

The --mixed flag is what we need (justify). Let us enter the command now, following the provided Usage and passing in the <PATH> name of the our project directory: maturin new --mixed borscht/, in which we will be prompted with what kind of bindings to use. Select the pyo3 option (justify).

$ maturin new --mixed borscht
? 🤷 Which kind of bindings to use?
  📖 Documentation: https://maturin.rs/bindings.html ›
❯ pyo3
  cffi
  uniffi
$ maturin new --mixed borscht
✔ 🤷 Which kind of bindings to use?
  📖 Documentation: https://maturin.rs/bindings.html · pyo3
  ✨ Done! New project created borscht

If we cd into borscht and ls with the -l flag we see two configuration files with pyproject.toml and Cargo.toml, as well as two directories of Python and Rust source python/ and src/:

$ cd borscht && ls -l
Cargo.toml
pyproject.toml
python
src

Let us open the two configuration files of pyproject.toml and Cargo.toml side by side in our favorite code editor:

borscht/pyproject.toml
[build-system]
requires = ["maturin>=1.14,<2.0"]
build-backend = "maturin"

[project]
name = "borscht"
requires-python = ">=3.8"
classifiers = [
    "Programming Language :: Rust",
    "Programming Language :: Python :: Implementation :: CPython",
    "Programming Language :: Python :: Implementation :: PyPy",
]
dynamic = ["version"]
[project.optional-dependencies]
tests = [
    "pytest",
]
[tool.maturin]
python-source = "python"
exclude = [
    "**/__pycache__",
    "**/*.pyc",
    "**/*.pyo"
]
borscht/Cargo.toml
[package]
name = "borscht"
version = "0.1.0"
edition = "2024"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
name = "borscht"
crate-type = ["cdylib"]

[dependencies]
pyo3 = "0.29.0"

with the [name] [version] and [edition] [package] fields, the [name] and [crate-type] [lib] fields.

Opening borscht/python/borscht/__init__.py and borscht/src/lib.rs we see the following:

borscht/python/borscht/__init__.py
from .borscht import *


__doc__ = borscht.__doc__
if hasattr(borscht, "__all__"):
    __all__ = borscht.__all__

borscht/src/lib.rs
#![allow(unused)]
fn main() {
use pyo3::prelude::*;

/// A Python module implemented in Rust.
#[pymodule]
mod borscht {
    use pyo3::prelude::*;

    /// Formats the sum of two numbers as string.
    #[pyfunction]
    fn sum_as_string(a: usize, b: usize) -> PyResult<String> {
        Ok((a + b).to_string())
    }
}
}

pyo3 rust crate

[#pymodule] [#pyfunction]

with maturin develop:

$ uv venv
Using CPython 3.14.7 interpreter at: /opt/homebrew/opt/python@3.14/bin/python3.14
Creating virtual environment at: .venv
Activate with: source .venv/bin/activate
$ source .venv/bin/activate
(borscht) $
(borscht) $ maturin develop
    Updating crates.io index
     Locking 14 packages to latest Rust 1.92.0-nightly compatible versions
🍹 Building a mixed python/rust project
🐍 Found CPython 3.14 at /borscht/.venv/bin/python
🔗 Found pyo3 bindings
Audited 1 package in 49ms
   Compiling target-lexicon v0.13.5
   Compiling proc-macro2 v1.0.107
   Compiling quote v1.0.47
   Compiling unicode-ident v1.0.24
   Compiling libc v0.2.189
   Compiling heck v0.5.0
   Compiling once_cell v1.21.4
   Compiling pyo3-build-config v0.29.2
   Compiling syn v2.0.119
   Compiling pyo3-ffi v0.29.2
   Compiling pyo3 v0.29.2
   Compiling pyo3-macros-backend v0.29.2
   Compiling pyo3-macros v0.29.2
   Compiling borscht v0.1.0 (/borscht)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 5.92s
📦 Built wheel for CPython 3.14 to /var/folders/wh/m0ckr3n10fjbfzq08k3j0hlr0000gn/T/.tmpO6X6Ul/borscht-0.1.0-cp314-cp314-macosx_11_0_arm64.whl
✏️ Setting installed package as editable
🛠 Installed borscht-0.1.0
(borscht) $ uv pip list
Package Version Editable project location
------- ------- ----------------------------------
borscht  0.1.0   ~/borscht
(borscht) $ uv pip list
Python 3.14.7 (main, Aug  5 2026, 10:29:49) [Clang 21.0.0 (clang-2100.1.1.101)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import borscht
>>> borscht.sum_as_string(40, 2)
'42'
>>>

We now have an honest to god mixed project of Python and Rust for borscht. If you ever need to create a new project for a different use case, you now understand how the pyo3 bindings and maturin build tool work and can simply invoke maturin new --mixed <PATH>, or tell your favorite agent to do it on your behalf. Let’s now discuss how we should architect such a deep learning framework.

2.1.2 The Architecture of a Deep Learning Framework

Table of Contents (§2.1 Virtualizing Shapes with Strides)

Deep learning frameworks suffer from the three language problem, and in 2.1.1 From Virtual to Physical Machinesyou were introduced to borscht’s first two implementation languages of Python and Rust. Because of Python’s reference interpreter CPython supports extension modules with the C API (expand), we were able to use Rust’s FFI ability (expand) and PyO3 to create a native module which our Python package’s init.py imported. We were left with maturin’s boilerplate of borscht.sum_as_string(), which doesn’t seem too helpful in our journey towards building ourselves an autograd from scratch. Let us now discuss how we should architect such a framework.

  • Layer -1: Multidimensional Array
  • Layer 0: Performance Primitives
  • Layer 1: Low-level Mathematical Routines
  • Layer 2: High-level Mathematical Routines

2.1.3 Implementing Layer -1: Strides, Views and Indexing

Table of Contents (§2.1 Virtualizing Shapes with Strides)

A potential compromise here is native code components should be done modularly, but always using CPython as the encapsulation boundary. Optimized components are always implemented native Python and with C++. Impending free-threaded Python eliminates multithreading problem.

Deep learning frameworks suffer from the three language problem, and are architected into several layers.


Cornell CS 5781 Machine Learning Engineering Lecture 2.1 Tensors

#![allow(unused)]
fn main() {
enum Device {
    Cpu,
    Gpu
}

enum Dtype {
    Float64(f64),
    Float32(f32),
    Float16(f16),
    Bfloat16(bf16),
}

struct Tensor {
    shape: Vec<u8>,
    stride: Vec<u8>,
    device: Device,
    dtype: Dtype,
    storage: Vec<Dtype>
}
}

The core idea behind the borrow checker is that variables have three kinds of permissions on their data:

  • Read (R): data can be copied to another location.
  • Write (W): data can be mutated in-place.
  • Own (O): data can be moved or dropped.

2.1.4 Implementing Layer 0: Transforms, Reductions, and Broadcasts

Table of Contents (§2.1 Virtualizing Shapes with Strides)

Cornell CS 5781 Machine Learning Engineering Lecture 2.2 Tensor Functions

Cornell CS 5781 Machine Learning Engineering Lecture 2.3 Shapes































2.2 Accelerating Basic Linear Algebra on CPUs

Table of Contents (§2. The Massively Parallel Multidimensional Tensor)

In which we …

2.2.1 From rust to risc-v

Table of Contents (§2.2 Accelerating Matrix Multiplication on CPUs)

2.2.2 Instruction Sets, Pipelines, and Hierarchies

Table of Contents (§2.2 Accelerating Matrix Multiplication on CPUs)

2.2.3 Measuring Bottlenecks to Throughput with Roofline Plots

Table of Contents (§2.2 Accelerating Matrix Multiplication on CPUs)

2.2.4 Accelerating saxpy on risc-v

Table of Contents (§2.2 Accelerating Matrix Multiplication on CPUs)

2.2.5 Accelerating sgemv on risc-v

Table of Contents (§2.2 Accelerating Matrix Multiplication on CPUs)

2.2.6 Accelerating sgemm on risc-v

Table of Contents (§2.2 Accelerating Matrix Multiplication on CPUs)

Loop Reordering, Register and Cache Blocking

Instruction Level Parallelism via Loop Unrolling

2.3 From Performance Primitives to Linear Solvers

Table of Contents

2.4 Accelerating Solvers with Decompositions

Table of Contents