Plato’s Cave (Jan Pietersz Saenredam after Cornelis Cornelisz van Haarlem 1604)
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.
II. Neural Networks
Deep Neural Networks
Welcome young hacker! Glad to see you here. The whimsical whirlwind tour known as The Structure and Interpretation of Tensor Programs (SITP) started with Part I. Elements of Networks where we developed a solid breadth of foundation in the computational, mathematical, and performance-oriented perspectives of the statistical approach to machine learning. By now, although we’ve “only” trained what amounts to a single layer network, we are now more than comfortable in the stochastic and infinitely continuous language of software 2.0. That is, although using the precise tools of mathematics to specify architectures, loss functions, and optimizers, the essence of the discipline is more akin to synthetic chemistry or microbiology which grow chemicals and cells. In Part II. Neural Networks, we simply continue where we left off.
representation learningrepresentation learning universal approximatorsuniversal approximators double descentdouble descent
Namely, in §3.1.2 From Localist to Distributional Representations, we cover XYZ and in §3.1.3 From Linear Perceptrons to Non-Linear Multi Layer Perceptrons. we increase the expressivity of model architecture from linear perceptrons otherwise known as single-layer networks to non-linear multi-layer perceptronsmulti-layer perceptron or deep neural networksdeep neural network. Moreover, because deep networks are no longer linear, in §3.1.4 From Direct to Iterative Optimization we transition from from direct optimizatio methods that solve for the exact solution to iterative ones that solve for approximate solutions.
After Part II. Neural Networks, we will be ready for Part III. Scaling Networks where we finally complete our quest in building our own
language model such as ChatGPT by following Nanochat and subsequently, our own deep learning framework capable of running said language model like PyTorch 2,
by implementing 
borscht.compile() and 
borscht.distributed().
Part three follows the 2020-2025 time period of what is colloquially known as the
the age of scalingage of scaling
where researchers focused on scaling up the generality of transformer architecture
by adding additional phases after pre-trainingpre-training stochastic parrots.
That is, the mid-trainingmid-training
and post-trainingpost-training phases
which respectively produce helpful assistantsassistants with
reinforcement learning from human feedback and slow thinking reasonersreasoners with verifiable rewards.
But before we get there we have some work to do.
Let’s go.
II. Table of Contents
- 3. Self-Supervised Pre-training with Deep Networks
- Intermezzo Three: The Language of Optimization and Approximation
- 4. The Engine of Automatic Differentiation
3. Self-Supervised Pre-Training with Deep Networks
In which we transition from manual feature engineering implemented by single layer networks to automatic feature learning by rediscovering various inductive biases that are parameterized by neural network architectures including the FFN, CNN, RNN, LSTM, and GPT. Such parameters are optimized with iterative methods using the approximation from the language of differential calculus afforded by
borscht.funcandborscht.Tensor.backward(). At the end, we will have rediscovered the so-called age of research for ourselves, which spanned form the years 2012 to 2019.
3.1 Learning Static Representations with FFNs
In which we transition from machine learning to deep learning, which focuses on learning distributional representations automatically with deep nets; introduce such deep nets as multi-layer perceptrons, the non-linear extensions of linear models with the ReLU and Embed primitives; transition our optimization techniques to handle non-linear functions with iterative descent methods.
3.1.1 From Feature Engineering to Representation Learning
Table of Contents (§3.1 Learning Representations with FFNs)
Deep learningdeep learning builds off from such foundation, namely, programming of software 2.0 with the three step process of selecting a model architecture, scoring it with a loss, and optimizing such loss. However, deep learning focuses on the class of models known as deep neural networks or multi-layer perceptrons that introduce many layers of non-linearities that make them well-suited for learning representationsrepresentation learningSee (Bengio et al. 2012) and (LeCun et al. 2015).
localist representationlocalist representation distributional representationdistributional representation
Localist representations such as one hot vectors are used to program the software 2.0 of machine learning, whereas distributional representationsdistributional representation such as embeddingsembeddings are used to program the software 2.0 of deep learning. In this chapter, we will learn about the neural network primitive of Embed.
Stanford CS224N: NLP with Deep Learning | Lecture 1 - Intro and Word Vectors
Stanford CS224N: NLP with Deep Learning | Lecture 2 - Word Vectors and Language Models
Namely, in §3.1.2 From Localist to Distributional Representations, we cover XYZ and in §3.1.3 From Linear Perceptrons to Non-Linear Multi Layer Perceptrons. we increase the expressivity of model architecture from linear perceptrons otherwise known as single-layer networks to non-linear multi-layer perceptronsmulti-layer perceptron or deep neural networksdeep neural network. Moreover, because deep networks are no longer linear, in §3.1.4 From Direct to Iterative Optimization we transition from from direct optimizatio methods that solve for the exact solution to iterative ones that solve for approximate solutions.
| 2.0 goal | 2.0 method (Part I) | 2.0 method (Part II) |
|---|---|---|
| evaluation | next-token prediction with stochastics of probability theory and compression of information theory | next-token prediction with stochastics of probability theory and compression of information theory |
| architecture | linear single-layer perceptron with dimensionality of linear algebra | non-linear multi-layer perceptron with dimensionality of linear algebra |
| optimizers | direct solvers with linear systems of linear algebra | iterative solvers with approximation of differential calculus |
| systems | speed of light acceleration with rooflines of cpus | speed of light acceleration with rooflines of gpus |
In §3. Self-Supervised Pre-Training with Deep Networks, we will implement different neural network architectures that all express various inductive biasesinductive biases. Such inductive biases were all explored during the 2012-2019 time period of what is coloquially known as the age of researchage of research. Keeping the goal of next-token prediction constant, we start from the standard feedforward neural networksFFN (FFN), and then transition to more novel architectures such as convolutional neural networksCNN (CNN), recurrent neural networksRNN (RNN), and finally, generative pre-trained transformersGPT (GPT), specifically by reproducing GPT-2Presented in Language Models are Unsupervised Multitask Learners (Radford et al. 2019) following Nanogpt. Because deep neural networks are no longer linear functions of their parameters, we will need to transition from direct methods to iterative ones with descent-based searches. Starting from gradient descentgradient descent, momentummomentum, weight decayweight decay, AdamWAdamW.
Then, in §4. The Engine of Automatic Differentiation,
you will evolve the implementation of 
borscht from a numerical linear algebra library like Numpy
from §2. The Multidimensional Parallelized Tensor into
a full blown batteries-included deep learning framework like TORCH which will be able to run the training and inference of the said nets just trained in §3.
This means implementing the optimizers for neural network primitives whose evaluations are accelerated with massively parallel processors known as graphics processing unitsGPU (GPUs)
and whose gradients are automatically evaluated with an automatic differentationautomatic differentiation engine.
Consult last few chapters of (Jurafsky and Martin 2026)If Jurafsky and Martin is not your cup of tea, take a look at (Eisenstein 2019)., and (Bishop and Bishop 2024)Similarly, you can also take a look at (Goodfellow et al. 2016) for deep learning.
junk
Newton’s classical mechanics must be understood before Einsten’s general relativity.
Similarly, so too of Rosenblatt’s machine learning before that of Bengio, Hinton, and LeCun’s deep learning.
First, we understand that language models such as Nanogpt, ChatGpt, and Claude
are specified as next-token predictors which although decide on a final classifying token for the given context (todo ?), have a step of inference by modeling language stochastically with a conditional random variable endowed with a probability distribution where such distribution corresponds to an array
which can be represented by a 
borscht.Tensor with .shape of (V).
This was the first step in our transition from classical software 1.0 to the jazz of software 2.0,
namely, by transitioning from variables to random variables in §1.1 From Certain to Uncertain Knowledge.
(expand with coterell formalization)
Second, we understanding that such language models probabilistically specified as autoregressive next-token predictors are implemented by estimating the parameterization of their functions with weights and bias so that where is a function that takes in a one-hot vector as input (expand, context?), evaluates the weighted sum with which is the standard inner product of the dot product, and then either uses the dot product directly in the case of linear regression with §1.3 Parameterizing Classification with Linear Regression or further composes the output with a squashing function in §1.4 Parameterizing Classification with Logistic Regression. In either case, both models are specified dimensionally, and, in the case of linear models, can be implemented with the direct optimization of linear algebra’s linear systems as well
3.1.2 From Linear Perceptrons to Non-Linear Multi Layer Perceptrons
Table of Contents (§3.1 Learning Representations with FFNs)
Linear models such as a single layer of perceptrons are used to program the software 2.0 of machine learning, whereas non-linear modelsnon-linear models such as multi-layer perceptronsmulti-layer perceptrons are used to program the software 2.0 of deep learning. We will compose the primitives of Embed, Linear, and RelU together to form a deep neural network.
- wed sep.23 after the 3.1 source of flow
- work on architecture 3.1.3 (merge the various representations) and optimizers 3.1.4
- including synthesis from intermezzo
junk1 With a solid understanding of how statistical learning recovers stochastic maps of data in higher dimensional coordinate spaces, you are now ready to start training your first deep neural networks. The subdiscipline of training deep neural nets follows the same practice of given some dataset , declaring your model’s forward pass , it’s scoring function and resulting backward pass , and iteratively optimizing such score with gradient descent, where . The only difference as we’ll shortly see soon is that the inductive bias used to model the function includes non-linearities with many layers of composition. This second part of the book focuses on the unsupervised/self-supervised task of sequence learning building up to the transformer architecture behind GPT2, but let’s train our first nets using the same tasks from part one to ensure we understand the core foundation of deep neural nets.
junk2 In part 2 we modify and increase the expressivity of the function class by including non-linearities . The feedforward neural network simply put is a series of linear and non-linear layers of the form so
where , is an elementwise nonlinearity, and . Conceptually, the linear layers are performing linear transformations that rotate, reflect, shear, and scale space, whereas the nonlinear transformations perform transformations that squash and twist space.
junk3 Let’s now turn out attention to the function bodies of these ’s with a deep neural netork of hidden layer, carrying out the task of price regression and sentiment classification so that has the form . With the statistical learning foundation from part one, we will simply present the forward pass , the loss function , and the backward pass .
Stanford CS109 | Deep Learning | Lecture 25
Stanford CS229 Machine Learning | Lecture 7: Neural Networks (Architecture)
Stanford CS229 Machine Learning | Lecture 13: LLMs, Next-Word Prediction Loss
Forward Pass
The functions of deep neural networks will take the form of with being a linear classifier on feature extractor
where is the number of compositional layers, and each intermediate function has the form of .
But what is a neural network? | Deep learning chapter 1 (3Blue1Brown 2017)
The next natural question to ask then is whether the logistic regression model is considered a deep neural network? The answer is that technically yes, it can be considered a degenerative deep neural network with hidden layersIn the same way that a list can be considered a degenerative binary tree or graph. These so-called hidden layers automate the construction of the representation through learning, so that the model not only discovers the mapping from representation to output, but also the representation itself. The functions of deep neural networks will take the form of with being a linear classifier on feature extractor , where is the number of compositional layers, and each intermediate function has the form of . Each hidden layer successively and gradually lifts the complexity and abstraction of the data’s representation.
Together, these two aspects of learning non-linear, representations form the essence of deep learning.
"""
model: Neural Language Models (Bengio et al. 2003) URL: https://www.jmlr.org/papers/volume3/bengio03a/bengio03a.pdf
Dimension key:
# windows
B: batch size
T: sequence length
# input/output
V: vocabulary size
E: embedding dimension
D: model dimension
"""
import torch
import matplotlib.pyplot as plt
%matplotlib inline
# from jaxtyping import ...
g = torch.Generator().manual_seed(1337) # for .randn()
B, T = 32, 3
V, E, D = 27, 10, 200
# step: 0/200000, loss 27.63208770751953
# -> expected loss = nll = p(c) = -torch.tensor(1/V=27).log() = 3.2958
# -> self.W = torch.randn() is sampling from N(0, 1)
# -> self.W * [gain/sqrt(D_in)] (torch.init_kaimingnormal())
# residuals + normalization + Adam/RMSprop has made initialization less fragile
# -> b/c initialization is fragile/intractable with *deep* neural networks
class Linear:
def __init__(self, D_in, D_out, bias=True):
self.W_DiDo = torch.randn((D_in, D_out), generator=g) * (5/3)/D_in**0.5 # kaiming init (He et al. 2015)
self.b_Do = torch.zeros(D_out) if bias else None
def __call__(self, X_Di):
self.X_Do = X_Di @ self.W_DiDo
if self.b_Do is not None:
self.X_Do += self.b_Do
self.out = self.X_Do
return self.X_Do
def parameters(self):
return [self.W_DiDo] + ([] if self.b_Do is None else [self.b_Do])
class BatchNorm1D:
def __init__(self, dim, eps=1e-5, momentum=0.1):
self.eps = eps
self.momentum = momentum
self.training = True
# parameters (trained with backprop)
self.gamma = torch.ones(dim)
self.beta = torch.zeros(dim)
# buffers (trained with a running 'momentum update')
self.running_mean = torch.zeros(dim)
self.running_var = torch.ones(dim)
def __call__(self, x):
# calculate the forward pass
if self.training:
xmean = x.mean(0, keepdim=True) # batch mean
xvar = x.var(0, keepdim=True) # batch variance
else:
xmean = self.running_mean
xvar = self.running_var
xhat = (x - xmean) / torch.sqrt(xvar + self.eps) # normalize to unit variance
self.out = self.gamma * xhat + self.beta
# update the buffers
if self.training:
with torch.no_grad():
self.running_mean = (1 - self.momentum) * self.running_mean + self.momentum * xmean
self.running_var = (1 - self.momentum) * self.running_var + self.momentum * xvar
return self.out
def parameters(self):
return [self.gamma, self.beta]
class Tanh:
def __call__(self, X_BD):
self.X_BD = torch.tanh(X_BD)
# plt.hist(self.X_BD.view(-1).tolist(), 50); # distribution of weights
# plt.imshow(self.X_BD.abs() > 0.99, cmap='gray', interpolation='nearest') # vanishing gradients
self.out = self.X_BD
return self.X_BD
def parameters(self):
return []
model = [
Linear(T * E, D, bias=False), BatchNorm1D(D), Tanh(),
Linear(D, D, bias=False), BatchNorm1D(D), Tanh(),
Linear(D, V, bias=False), BatchNorm1D(V)
]
C = torch.randn((V,E), generator=g)
params = [C] + [p for l in model for p in l.parameters()]
for p in params:
p.requires_grad = True
To summarize, in order to transition from manually handcrafting features to automatically learning them, we transition our model parameterizations from single layer to multi-layer perceptrons with non-linearities that learn a progressive sequence of semantically meaningful linear transformations and bases upon optimization (which we’ll learn and implement in 3.1.3 From Direct to Iterative Optimization). In fact, because of such non-linearities, multi-layer perceptrons can in theory approximate any function as stated by the universal approximation theoremThis is the software 2.0 parallel to software 1.0’ turing machines which are capable of universal computation. See https://en.wikipedia.org/wiki/Universal_Turing_machine. Put shortly, a multi-layer perceptron is
where and are supported by and respectively, , and of course . To score such parameters, we continue to maintain the cross entropy loss because the overall classification from input to output space remains unchanged. That is, the function we’d like to recover is a next-token predictor that categorizes a possible next output token given an input sentence, and so the cross entropy loss of such a model is,
where and so . Now that we’ve parameterized next-token prediction with a multi-layer perceptron and have scored such parameters with the cross-entropy loss, let us move on to it’s optimization.
3.1.3 From Direct to Iterative Optimization
Table of Contents (§3.1 Learning Representations with FFNs)
Direct optimization methods used to optimize the parameters of a linear perceptron such as iterative reweighted least squares will not work for the parameterized family of non-linear multi-layer perceptrons. Consider a parameterization of the multilayer perceptron and scoring such parameters with the cross entropy loss. That is,
import borscht
import borscht.nn as nn
import borscht.nn.functional as F
class Linear(nn.Module):
def __init__(self, Di: int, Do: int):
self.W_DiDo = borscht.randn((Di,Do))
self.b_Do = borscht.randn((Do))
def __call__(self, x_Di: borscht.Tensor):
out_Do = x_Di @ self.W_DiDo
if self.b_Do is not None:
out_Do += self.b_Do
return out
class Tanh():
def __call__(self, X_BD: borscht.Tensor):
return borscht.tanh(X_BD)
V = 2
T = 1
E = 10
model = [Linear(T*E, D), Tanh()]
yhat = model(x)
loss_scalar = F.cross_entropy(yhat,y)
Similar to the linear perceptron used as a probabilistic model, the direct method of gaussian elimination does not suffice. Why so? (insert explanation)
Stanford CS229 Machine Learning | Lecture 8: Neural Networks 2 (Backprop) Stanford CS224N: NLP with Deep Learning | Lecture 3 - Backpropagation, Neural Network
Rather than find the optimal set of parameters in closed form solution directly, we need to transition our optimization method from an iterativeiterative optimization one that incrementally finds increasingly better approximations of the optimum with a sequence that takes steps. Take a look at the 3Blue1Brown visual lecture below for an intuition pump on where we’re going this chapter.
Similarly, before we iteratively optimize the original cross entropy loss function of our multi-layer perceptron which produces a real-valued score for many parameters that have support in d-dimensional Euclidean space, let us consider loss functions of the one and two dimensional case first. That is, consider some arbitrary non-linear loss function that scores a single parameter
import borscht
def my_1dloss(theta: borscht.Tensor) -> borscht.Tensor:
loss_scalar = borscht.Tensor(
((theta+2)**2 *
(theta-1)**2 *
(theta-3)**2) + 0.3
)
return loss_scalar
theta_scalar = borscht.randn()
my_1dloss_scalar = my_1dloss(theta_scalar())
as well as it’s corresponding plot of a curvecurve
import matplot
plt.plot(my_1dloss)
(insert plot)
Instead of solving for the that minimizes in closed-form directly, we can solve so iteratively. Recalling the derivative’sderivative geometric perspective from high school calculus, the derivative of a function at a certain point is the line tangenttangent to the output, which is the instantaneous rate of change between two points of a secantsecant line. This secant line corresponds to the analytical perspective of the derivativederivative defined by the limitlimit.
Geometrically speaking, starting from some randomly initialized parameter with borscht.randn(todo),
we can take iterative steps towards the minimum by __.
Now, consider another arbitrary non-linear loss function that scores not a single parameter, but rather two parameters
import borscht
def my_1dloss(theta: borscht.Tensor) -> borscht.Tensor:
loss_scalar = borscht.Tensor(
((theta+2)**2 *
(theta-1)**2 *
(theta-3)**2) + 0.3
)
return loss_scalar
theta_scalar = borscht.randn()
my_1dloss_scalar = my_1dloss(theta_scalar())
as well as it’s corresponding plot of a surfacesurface
gradientgradient with 
borscht.grad()
derivativederivative defined by the limitlimit as a linear operator
import matplot
plt.plot(my_1dloss)
(insert plot)
# 1. dataloader
import torch.nn.functional as F
import random
words = open('./data/names.txt', 'r').read().splitlines()
v = sorted(list(set(''.join(words))))
encode = { c:i+1 for i,c in enumerate(v) }
encode['.'] = 0
decode = { i:c for c,i in encode.items() }
def gen_dataset(words):
X, Y = [], []
for w in words:
context = [0] * T;
for c in w + '.':
X.append(context)
Y.append(encode[c])
# print(''.join(decode[i] for i in context), '-->', decode[encode[c]])
context = context[1:] + [encode[c]]
X, Y = torch.tensor(X), torch.tensor(Y) # X:(N,C) Y:(N)
return X, Y
random.seed(42)
random.shuffle(words)
n1, n2 = int(0.8*len(words)), int(0.9*len(words))
Xtr, Ytr = gen_dataset(words[:n1])
Xdev, Ydev = gen_dataset(words[n1:n2])
Xte, Yte = gen_dataset(words[n2:])
# 2. training loop
N = Xtr.shape[0]
losses, steps = [], []
for step in range(200000):
# 1. forward
indices_B = torch.randint(0, N, (B,))
X_B, Y_B = Xtr[indices_B], Ytr[indices_B]
X_BD = C[X_B].view(-1, T * E)
for layer in model:
X_BD = layer(X_BD)
loss = F.cross_entropy(X_BD, Y_B)
# 2. backward
for layer in model:
layer.out.retain_grad()
for p in params:
p.grad = None
loss.backward()
# 3. update
for p in params:
p.data += -0.01 * p.grad
steps.append(step)
losses.append(loss.log10().item())
if step % 10000 == 0:
print(f"step: {step}/{200000}, loss {loss.item()}")
plt.plot(steps, losses)
step: 0/200000, loss 3.811568260192871 step: 10000/200000, loss 2.625439405441284 step: 20000/200000, loss 2.426504135131836 step: 30000/200000, loss 1.8564293384552002 step: 40000/200000, loss 2.563091993331909 step: 50000/200000, loss 1.8718260526657104 step: 60000/200000, loss 2.189483642578125 step: 70000/200000, loss 1.9642211198806763 step: 80000/200000, loss 2.261333465576172 step: 90000/200000, loss 2.2367687225341797 step: 100000/200000, loss 1.7542214393615723 step: 110000/200000, loss 1.9069358110427856 step: 120000/200000, loss 2.2940409183502197 step: 130000/200000, loss 2.030090808868408 step: 140000/200000, loss 2.556252956390381 step: 150000/200000, loss 2.0693819522857666 step: 160000/200000, loss 2.2053565979003906 step: 170000/200000, loss 2.3926899433135986 step: 180000/200000, loss 2.4394421577453613 step: 190000/200000, loss 2.0519824028015137
[<matplotlib.lines.Line2D at 0x11099cce0>]
3.2 Iterative Optimization
In which we delve deeper into foundations of gradient-based iterative optimization, building ourselves up to the AdamW optimizer.
3.2.1 Taylor Series and their Derivatives, Gradients, and Hessians
Lorem ipsum MIT 6.7960 Deep Learning: Lec 07. Scaling Rules for Optimization dolor sit amet, consectetur adipiscing elit. Proin finibus interdum arcu, ut venenatis dolor rutrum eget. Nulla nec urna tellus. Donec sollicitudin hendrerit sollicitudin. Proin sagittis, velit commodo congue rhoncus, metus turpis hendrerit nisi, a accumsan quam ante ultricies sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Ut aliquet ut justo non posuere. Quisque in rutrum nunc. Phasellus vehicula tortor mi, vehicula placerat diam ultrices eget. Aliquam et consequat eros, quis fermentum tortor. Sed eu nisl viverra, auctor velit vel, elementum ante. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Proin aliquet metus sit amet nulla cursus maximus in feugiat mi. Praesent vehicula magna sed scelerisque elementum. Nulla euismod rutrum turpis sed hendrerit.
3.2.2 First Order Optimization
3.2.3 Second Order Optimization
3.2.4 Momentum
3.2.5 Weight Decay
3.3 Learning Representations with CNNs
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin finibus interdum arcu, ut venenatis dolor rutrum eget. Nulla nec urna tellus. Donec sollicitudin hendrerit sollicitudin. Proin sagittis, velit commodo congue rhoncus, metus turpis hendrerit nisi, a accumsan quam ante ultricies sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Ut aliquet ut justo non posuere. Quisque in rutrum nunc. Phasellus vehicula tortor mi, vehicula placerat diam ultrices eget. Aliquam et consequat eros, quis fermentum tortor. Sed eu nisl viverra, auctor velit vel, elementum ante. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Proin aliquet metus sit amet nulla cursus maximus in feugiat mi. Praesent vehicula magna sed scelerisque elementum. Nulla euismod rutrum turpis sed hendrerit.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin finibus interdum arcu, ut venenatis dolor rutrum eget. Nulla nec urna tellus. Donec sollicitudin hendrerit sollicitudin. Proin sagittis, velit commodo congue rhoncus, metus turpis hendrerit nisi, a accumsan quam ante ultricies sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Ut aliquet ut justo non posuere. Quisque in rutrum nunc. Phasellus vehicula tortor mi, vehicula placerat diam ultrices eget. Aliquam et consequat eros, quis fermentum tortor. Sed eu nisl viverra, auctor velit vel, elementum ante. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Proin aliquet metus sit amet nulla cursus maximus in feugiat mi. Praesent vehicula magna sed scelerisque elementum. Nulla euismod rutrum turpis sed hendrerit.
3.4 Learning Representations with RNNs
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin finibus interdum arcu, ut venenatis dolor rutrum eget. Nulla nec urna tellus. Donec sollicitudin hendrerit sollicitudin. Proin sagittis, velit commodo congue rhoncus, metus turpis hendrerit nisi, a accumsan quam ante ultricies sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Ut aliquet ut justo non posuere. Quisque in rutrum nunc. Phasellus vehicula tortor mi, vehicula placerat diam ultrices eget. Aliquam et consequat eros, quis fermentum tortor. Sed eu nisl viverra, auctor velit vel, elementum ante. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Proin aliquet metus sit amet nulla cursus maximus in feugiat mi. Praesent vehicula magna sed scelerisque elementum. Nulla euismod rutrum turpis sed hendrerit.
class RNNCell(nn.Module):
"""
the job of a 'Cell' is to:
take input at current time step x_{t} and the hidden state at the
previous time step h_{t-1} and return the resulting hidden state
h_{t} at the current timestep
"""
def __init__(self, config):
super().__init__()
self.xh_to_h = nn.Linear(config.n_embd + config.n_embd2, config.n_embd2)
def forward(self, xt, hprev):
xh = torch.cat([xt, hprev], dim=1)
ht = F.tanh(self.xh_to_h(xh))
return ht
class RNN(nn.Module):
def __init__(self, config, cell_type):
super().__init__()
self.block_size = config.block_size
self.vocab_size = config.vocab_size
self.start = nn.Parameter(torch.zeros(1, config.n_embd2)) # the starting hidden state
self.wte = nn.Embedding(config.vocab_size, config.n_embd) # token embeddings table
if cell_type == 'rnn':
self.cell = RNNCell(config)
elif cell_type == 'gru':
self.cell = GRUCell(config)
self.lm_head = nn.Linear(config.n_embd2, self.vocab_size)
def get_block_size(self):
return self.block_size
def forward(self, idx, targets=None):
device = idx.device
b, t = idx.size()
# embed all the integers up front and all at once for efficiency
emb = self.wte(idx) # (b, t, n_embd)
# sequentially iterate over the inputs and update the RNN state each tick
hprev = self.start.expand((b, -1)) # expand out the batch dimension
hiddens = []
for i in range(t):
xt = emb[:, i, :] # (b, n_embd)
ht = self.cell(xt, hprev) # (b, n_embd2)
hprev = ht
hiddens.append(ht)
# decode the outputs
hidden = torch.stack(hiddens, 1) # (b, t, n_embd2)
logits = self.lm_head(hidden)
# if we are given some desired targets also calculate the loss
loss = None
if targets is not None:
loss = F.cross_entropy(logits.view(-1, logits.size(-1)), targets.view(-1), ignore_index=-1)
return logits, loss
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin finibus interdum arcu, ut venenatis dolor rutrum eget. Nulla nec urna tellus. Donec sollicitudin hendrerit sollicitudin. Proin sagittis, velit commodo congue rhoncus, metus turpis hendrerit nisi, a accumsan quam ante ultricies sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Ut aliquet ut justo non posuere. Quisque in rutrum nunc. Phasellus vehicula tortor mi, vehicula placerat diam ultrices eget. Aliquam et consequat eros, quis fermentum tortor. Sed eu nisl viverra, auctor velit vel, elementum ante. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Proin aliquet metus sit amet nulla cursus maximus in feugiat mi. Praesent vehicula magna sed scelerisque elementum. Nulla euismod rutrum turpis sed hendrerit.
3.5 Learning Dynamic Representations with GPTs
In which we transition from long-range depencies of recurrent neural nets to the ___ of transformer-based neural nets.
3.5.1 Dynamic Embeddings with Attention
Table of Contents (§3.5 Learning Representations with GPTs)
Attention is all you need. static embeddingsstatic embeddings dynamic embeddingsdynamic embeddings Stanford CS229 Machine Learning Lecture 14: Transformers, In-Context Learning
Stanford CME295 Transformers & LLMs | Lecture 1 - Transformer
Stanford CS25: V2 I Introduction to Transformers w/ Andrej Karpathy
fluffy blue creature roamed the verdant forest. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
microgpt: https://gist.github.com/karpathy/8627fe009c40f57531cb18360106ce95
class Block(nn.Module):
def __init__(self, config):
super().__init__()
D, H = config.n_embd, config.n_head
self.ln_1 = nn.LayerNorm(D)
self.attn = MHA(config)
self.mlp = FFN(config) # .mlp for HF
self.ln_2 = nn.LayerNorm(D)
def forward(self, X_BTD):
# residuals:
# - (He et al. 2015 https://arxiv.org/abs/1512.03385)
# - (Elhage et al. 2021 https://transformer-circuits.pub/2021/framework/index.html)
X_BTD = X_BTD + self.attn(self.ln_1(X_BTD))
X_BTD = X_BTD + self.mlp(self.ln_2(X_BTD))
return X_BTD
class MHA(nn.Module):
def __init__(self, config):
super().__init__()
T, D, H = config.block_size, config.n_embd, config.n_head
assert D % H == 0
self.H = H
self.c_attn = nn.Linear(D, 3 * D)
self.c_proj = nn.Linear(D, D)
self.c_proj.GPT2_SCALE_INIT = 1
self.register_buffer('bias', torch.tril(torch.ones(T, T)).view(1, 1, T, T)) # tril -> bias for HF
def forward(self, X_BTD):
B,T,D = X_BTD.shape
H = self.H
# 1. project to learned QKV subspaces Q=WqX, K=WkX, V=WvX
Wq_DK, Wk_DK, Wv_DK = self.c_attn(X_BTD).split(D, dim=2)
Q_BHTK, K_BHTK, V_BHTK = Wq_DK.view(B, T, H, D // H).transpose(1, 2), Wk_DK.view(B, T, H, D // H).transpose(1, 2), Wv_DK.view(B, T, H, D // H).transpose(1, 2)
# 2. evaluate scores A(QKV) = softmax(QK^T/sqrt(d_k))V
# A_BHTT = Q_BHTK @ K_BHTK.transpose(-2, -1) * (1.0 / math.sqrt(K_BHTK.size(-1)))
# A_BHTT = A_BHTT.masked_fill(self.bias[:, :, :T, :T]==0, float('-inf'))
# A_BHTT = F.softmax(A_BHTT, dim=-1) # todo, when dim=-1?
# S_BHTD = A_BHTT @ V_BHTK
S_BHTD = F.scaled_dot_product_attention(Q_BHTK, K_BHTK, V_BHTK, is_causal=True)
# 3. contextualize the embeddings
S_BTD = S_BHTD.transpose(1, 2).contiguous().view(B, T, D) # performs cat
S_BTD = self.c_proj(S_BTD)
return S_BTD
3.5.2 Storing Facts with FFN
Table of Contents (§3.5 Learning Representations with GPTs)
Storing facts consectetur adipiscing elit. Donec aliquam varius mi, pharetra scelerisque nulla vehicula ut. Cras ac faucibus dolor. Nunc tempus efficitur ultricies. Nullam non fermentum elit, et tincidunt ante. Donec hendrerit metus ac urna semper, in facilisis leo egestas. Integer sit amet quam ultrices, dapibus sem a, volutpat velit. Pellentesque eget ipsum consequat sem luctus blandit a et nibh. Mauris gravida felis ut dignissim volutpat. Quisque volutpat pulvinar enim in venenatis. Integer posuere tincidunt leo vel congue. Sed ex velit, posuere ut nibh nec, ultrices malesuada nibh. Fusce scelerisque mollis neque id fringilla. Etiam rhoncus quam a leo ultrices rutrum non vitae est. Donec bibendum, arcu sagittis consequat vestibulum, lectus elit bibendum libero, non rhoncus lorem lorem a neque. Sed ex felis, rutrum non consequat eget, tincidunt ac lacus.
class FFN(nn.Module):
def __init__(self, config):
super().__init__()
D = config.n_embd
self.c_fc = nn.Linear(D, 4*D) # projecting up to extract features from context embeddings
self.gelu = nn.GELU(approximate='tanh') # (Hendrycks and Gimpel 2016 https://arxiv.org/abs/1606.08415)
self.c_proj = nn.Linear(4*D, D) # projecting back down to residual pathway
self.c_proj.GPT2_SCALE_INIT = 1
def forward(self, X_BTD):
X_BT4D = self.c_fc(X_BTD)
X_BT4D = self.gelu(X_BT4D)
X_BTD = self.c_proj(X_BT4D)
return X_BTD
3.5.2 Embedding and Unembedding
Table of Contents (§3.5 Learning Representations with GPTs)
3.5.3 Optimizing Loss with AdamW on Cross Entropy
Table of Contents (§3.5 Learning Representations with GPTs)
Intermezzo Three: The Language of Optimization and Approximation
The learning process continues to be a three-step one where we specify a parameterization, a scoring of such parameters, and the optimization of such scoring function
While automatically learning features for next-token prediction maintains the cross entropy loss for the scoring of parameters, the parameterization changes so that optimizing such loss of it’s parameters if a process such that a progressive sequence of semantically meaningful linear transformations that squash, shear, and rotate n-dimensional Euclidean space is learned
import borscht
import borscht.nn as nn
import borscht.nn.functional as F
class MLP(nn.Module):
def __init__(self):
raise NotImplementedError("")
def __call__(self, x_E: borscht.Tensor):
raise NotImplementedError("")
model = MLP(Din, Dout)
# random forward pass
x = borscht.Tensor()
y_logits = model(x)
y_distribution = F.cross_entropy_loss(y_logits)
and now that the model class is non-linear, said optimization changes to an iterative process of gradient descent.
model = MLP(Din, Dout)
for t in range(100):
# 1. forward
x = borscht.Tensor()
y_logits = model(x)
y_distribution = F.cross_entropy_loss(y_logits, ___)
# 2. backward
y_distribution.backward()
optim.step()
For this iterative optimization process, we earlier appealed to the intuitive geometric perspective of the derivative, where in the one and two-dimensional cases, the derivative of the loss function is viewed as the line and plane that is tangent to the function’s output and points in the direction up the curve and surface’s hill. Although we were unable to visualize the gradient of the n-dimensional case of the actual loss function with n being the size of the parameter space, we could generalize the gradient by appending a partial derivative for each additional parameter that the loss function takes in as an argument. That is,
print(loss.backward())
Now that we intuitively understand how the notions of descent and sensitivity from the language of optimization and approximation are used in the training deep neural networks from the geometric and computational perspective, let us analytically formalize such notions into precise concepts.
I3.1 Sequences
I3.2 Limits of Sequences
I3.3 Limits of Functions
I3.3 Derivatives as Linear Operators
4. The Engine of Automatic Differentiation
automatic differentiationautomatic differentiation optimizersoptimizers network primitivesnetwork primitives parallel programmingparallel programming massively parallel processorsmassively parallel processors graphics processing unit (GPU)GPU CUDA RustCUDA Rust PTXPTX SASSSASS
Finish strong young hacker.
4.1 Automatic Differentation
In which we …
4.1.1 Algorithmic Differentiation
Table of Contents (§4.1 Automatic Differentiation)
Automatic Differentiation as opposed to symbolic or numeric differentiation can be considered an algorithmic implementation of differentiation on a computational graph.
4.1.2 Implementing Backward for FFN
Table of Contents (§4.1 Automatic Differentiation)
4.1.5 Implementing Backward for GPT
Table of Contents (§4.1 Automatic Differentiation)
4.2 Gradient-Based Optimization
Tensor.backward() function in order to materialize the gradient of the loss on our
parameter weights which gradient descent uses in it’s update rule
.
We will now dive deeper into how .backward() is implemented. –>
4.1.4 Stochastic Gradient Descent
4.1.5 Adam
Table of Contents (§4.1 Automatic Differentiation)
4.2.3 Implementing Forward Pass for CNN
4.2.4 Implementing Forward Pass for RNN
4.2.5 Implementing Forward Pass for GPT
missing notebook: /opt/build/repo/sitp/notebooks/4.1_makemore.ipynb
missing notebook: /opt/build/repo/sitp/notebooks/4.1_makemore.ipynb