chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
"""
|
||||
---
|
||||
title: Transformers
|
||||
summary: >
|
||||
This is a collection of PyTorch implementations/tutorials of
|
||||
transformers and related techniques.
|
||||
---
|
||||
|
||||
# Transformers
|
||||
|
||||
This module contains [PyTorch](https://pytorch.org/)
|
||||
implementations and explanations of original transformer
|
||||
from paper [Attention Is All You Need](https://arxiv.org/abs/1706.03762),
|
||||
and derivatives and enhancements of it.
|
||||
|
||||
* [Multi-head attention](mha.html)
|
||||
* [Transformer Encoder and Decoder Models](models.html)
|
||||
* [Position-wise Feed Forward Network (FFN)](feed_forward.html)
|
||||
* [Fixed positional encoding](positional_encoding.html)
|
||||
|
||||
## [Transformer XL](xl/index.html)
|
||||
This implements Transformer XL model using
|
||||
[relative multi-head attention](xl/relative_mha.html)
|
||||
|
||||
## [Rotary Positional Embeddings](rope/index.html)
|
||||
This implements Rotary Positional Embeddings (RoPE)
|
||||
|
||||
## [Attention with Linear Biases](alibi/index.html)
|
||||
This implements Attention with Linear Biases (ALiBi).
|
||||
|
||||
## [RETRO](retro/index.html)
|
||||
This implements the Retrieval-Enhanced Transformer (RETRO).
|
||||
|
||||
## [Compressive Transformer](compressive/index.html)
|
||||
|
||||
This is an implementation of compressive transformer
|
||||
that extends upon [Transformer XL](xl/index.html) by compressing
|
||||
the oldest memories to give a longer attention span.
|
||||
|
||||
## [GPT Architecture](gpt/index.html)
|
||||
|
||||
This is an implementation of GPT-2 architecture.
|
||||
|
||||
## [GLU Variants](glu_variants/simple.html)
|
||||
|
||||
This is an implementation of the paper
|
||||
[GLU Variants Improve Transformer](https://arxiv.org/abs/2002.05202).
|
||||
|
||||
## [kNN-LM](knn/index.html)
|
||||
|
||||
This is an implementation of the paper
|
||||
[Generalization through Memorization: Nearest Neighbor Language Models](https://arxiv.org/abs/1911.00172).
|
||||
|
||||
## [Feedback Transformer](feedback/index.html)
|
||||
|
||||
This is an implementation of the paper
|
||||
[Accessing Higher-level Representations in Sequential Transformers with Feedback Memory](https://arxiv.org/abs/2002.09402).
|
||||
|
||||
## [Switch Transformer](switch/index.html)
|
||||
|
||||
This is a miniature implementation of the paper
|
||||
[Switch Transformers: Scaling to Trillion Parameter Models with Simple and Efficient Sparsity](https://arxiv.org/abs/2101.03961).
|
||||
Our implementation only has a few million parameters and doesn't do model parallel distributed training.
|
||||
It does single GPU training but we implement the concept of switching as described in the paper.
|
||||
|
||||
## [Fast Weights Transformer](fast_weights/index.html)
|
||||
|
||||
This is an implementation of the paper
|
||||
[Linear Transformers Are Secretly Fast Weight Memory Systems in PyTorch](https://arxiv.org/abs/2102.11174).
|
||||
|
||||
## [FNet: Mixing Tokens with Fourier Transforms](fnet/index.html)
|
||||
|
||||
This is an implementation of the paper
|
||||
[FNet: Mixing Tokens with Fourier Transforms](https://arxiv.org/abs/2105.03824).
|
||||
|
||||
## [Attention Free Transformer](aft/index.html)
|
||||
|
||||
This is an implementation of the paper
|
||||
[An Attention Free Transformer](https://arxiv.org/abs/2105.14103).
|
||||
|
||||
## [Masked Language Model](mlm/index.html)
|
||||
|
||||
This is an implementation of Masked Language Model used for pre-training in paper
|
||||
[BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding](https://arxiv.org/abs/1810.04805).
|
||||
|
||||
## [MLP-Mixer: An all-MLP Architecture for Vision](mlp_mixer/index.html)
|
||||
|
||||
This is an implementation of the paper
|
||||
[MLP-Mixer: An all-MLP Architecture for Vision](https://arxiv.org/abs/2105.01601).
|
||||
|
||||
## [Pay Attention to MLPs (gMLP)](gmlp/index.html)
|
||||
|
||||
This is an implementation of the paper
|
||||
[Pay Attention to MLPs](https://arxiv.org/abs/2105.08050).
|
||||
|
||||
## [Vision Transformer (ViT)](vit/index.html)
|
||||
|
||||
This is an implementation of the paper
|
||||
[An Image Is Worth 16x16 Words: Transformers For Image Recognition At Scale](https://arxiv.org/abs/2010.11929).
|
||||
|
||||
## [Primer EZ](primer_ez/index.html)
|
||||
|
||||
This is an implementation of the paper
|
||||
[Primer: Searching for Efficient Transformers for Language Modeling](https://arxiv.org/abs/2109.08668).
|
||||
|
||||
## [Hourglass](hour_glass/index.html)
|
||||
|
||||
This is an implementation of the paper
|
||||
[Hierarchical Transformers Are More Efficient Language Models](https://arxiv.org/abs/2110.13711)
|
||||
"""
|
||||
|
||||
from .configs import TransformerConfigs
|
||||
from .models import TransformerLayer, Encoder, Decoder, Generator, EncoderDecoder
|
||||
from .mha import MultiHeadAttention
|
||||
from labml_nn.transformers.xl.relative_mha import RelativeMultiHeadAttention
|
||||
@@ -0,0 +1,234 @@
|
||||
"""
|
||||
---
|
||||
title: An Attention Free Transformer
|
||||
summary: >
|
||||
This is an annotated implementation/tutorial of the AFT (Attention Free Transformer) in PyTorch.
|
||||
---
|
||||
|
||||
# An Attention Free Transformer
|
||||
|
||||
This is a [PyTorch](https://pytorch.org) implementation of the paper
|
||||
[An Attention Free Transformer](https://arxiv.org/abs/2105.14103).
|
||||
|
||||
This paper replaces the [self-attention layer](../mha.html) with a new efficient operation,
|
||||
that has memory complexity of $\mathcal{O}(Td)$, where $T$ is the sequence length
|
||||
and $d$ is the dimensionality of embeddings.
|
||||
|
||||
The paper introduces AFT along with AFT-local and AFT-conv.
|
||||
Here we have implemented AFT-local which pays attention to closeby tokens
|
||||
in an autoregressive model.
|
||||
|
||||
## Attention Free Transformer
|
||||
|
||||
AFT (similar to [MHA](../mha.html)) first transforms the embeddings $X$ into
|
||||
query $Q = XW^Q$, key $K = XW^K$ and value $V = XW^V$ tensors with learned weights.
|
||||
The output for each position $t \in [1, T]$ is calculated with the following operation.
|
||||
|
||||
$$Y_t = \sigma(Q_t) \odot
|
||||
\frac{\sum_{t'=1}^T \exp(K_{t'} + w_{t,t'}) \odot V_{t'}}
|
||||
{\sum_{t'=1}^T \exp(K_{t'} + w_{t,t'})}$$
|
||||
|
||||
, where $\odot$ is element-wise product, $\sigma$ is a non-linearity (sigmoid) and
|
||||
$w \in \mathbb{R}^{T \times T}$ is a learned matrix of pair-wise position biases.
|
||||
|
||||
This means that we take the weighted average of values
|
||||
and multiply them by the query. This eliminates the need to calculate the $T \times T$ attention
|
||||
matrix that [MHA](../mha.html) requires, and therefore reduce the memory requirement.
|
||||
|
||||
## AFT Local
|
||||
|
||||
AFT Local only apply learned pair-wise position biases locally:
|
||||
|
||||
\begin{align}
|
||||
w'_{t,t'} =
|
||||
\begin{cases}
|
||||
w_{t,t'}, & {\text{for } \lvert t-t' \rvert \lt s} \\
|
||||
0, & \text{otherwise}
|
||||
\end{cases}
|
||||
\end{align}
|
||||
|
||||
, where $s \le T$ is the local window size.
|
||||
|
||||
Although $w'_{t,t'}$ is $0$ outside the local window the AFT operation still uses key-value pairs from
|
||||
other areas. This is different from local transformers where embeddings outside the local window are
|
||||
completely not visible.
|
||||
|
||||
Here is [the training code](experiment.html) for a AFT Local model.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
|
||||
|
||||
class AFTLocal(nn.Module):
|
||||
"""
|
||||
### AFT Local Operation
|
||||
|
||||
$$Y_t = \sigma(Q_t) \odot
|
||||
\frac{\sum_{t'=1}^T \exp(K_{t'} + w_{t,t'}) \odot V_{t'}}
|
||||
{\sum_{t'=1}^T \exp(K_{t'} + w_{t,t'})}$$
|
||||
|
||||
where,
|
||||
|
||||
\begin{align}
|
||||
w'_{t,t'} =
|
||||
\begin{cases}
|
||||
w_{t,t'}, & {\text{for } \lvert t-t' \rvert \lt s} \\
|
||||
0, & \text{otherwise}
|
||||
\end{cases}
|
||||
\end{align}
|
||||
"""
|
||||
|
||||
def __init__(self, d_model: int, seq_len: int, local_window_size: int, bias: bool = True):
|
||||
"""
|
||||
* `d_model` is the number of features in the `query`, `key` and `value` vectors.
|
||||
* `seq_len` is $T$
|
||||
* `local_window_size` is the local window size $s$
|
||||
* `bias` is whether to have a bias parameter for transformations for $Q$, $K$ and $V$.
|
||||
"""
|
||||
|
||||
super().__init__()
|
||||
|
||||
# Local window size $s$
|
||||
self.local_window_size = local_window_size
|
||||
# These transform the `query`, `key` and `value` vectors.
|
||||
self.query = nn.Linear(d_model, d_model, bias=bias)
|
||||
self.key = nn.Linear(d_model, d_model, bias=bias)
|
||||
self.value = nn.Linear(d_model, d_model, bias=bias)
|
||||
# Pair-wise positional biases $w \in \mathbb{R}^{T \times T}$
|
||||
self.pos_bias = nn.Parameter(torch.zeros(seq_len, seq_len), requires_grad=True)
|
||||
# Mask for $w_{t,t'}$
|
||||
self.local_mask = nn.Parameter(self.create_local_mask(seq_len, local_window_size), requires_grad=False)
|
||||
# Activation $\sigma$
|
||||
self.activation = nn.Sigmoid()
|
||||
# Output layer
|
||||
self.output = nn.Linear(d_model, d_model)
|
||||
|
||||
@staticmethod
|
||||
def create_local_mask(seq_len, local_window_size):
|
||||
"""
|
||||
#### Create local mask
|
||||
|
||||
This creates a mask for
|
||||
|
||||
\begin{align}
|
||||
m_{t,t'} =
|
||||
\begin{cases}
|
||||
1, & {\text{for } \lvert t-t' \rvert \lt s} \\
|
||||
0, & \text{otherwise}
|
||||
\end{cases}
|
||||
\end{align}
|
||||
"""
|
||||
|
||||
# Initialize to ones
|
||||
local_mask = torch.ones(seq_len, seq_len, dtype=torch.bool)
|
||||
# Make $t' - t \ge s$ zero
|
||||
local_mask = torch.tril(local_mask, local_window_size - 1)
|
||||
# Make $t - t' \ge s$ zero
|
||||
local_mask = torch.triu(local_mask, -(local_window_size - 1))
|
||||
|
||||
#
|
||||
return local_mask
|
||||
|
||||
def forward(self, *,
|
||||
query: torch.Tensor,
|
||||
key: torch.Tensor,
|
||||
value: torch.Tensor,
|
||||
mask: Optional[torch.Tensor] = None):
|
||||
"""
|
||||
`query`, `key` and `value` are the tensors that store
|
||||
collection of token embeddings for *query*, *key* and *value*.
|
||||
They have shape `[seq_len, batch_size, d_model]`.
|
||||
|
||||
`mask` has shape `[seq_len, seq_len, batch_size]` and
|
||||
`mask[i, j, b]` indicates whether for batch `b`,
|
||||
query at position `i` has access to key-value at position `j`.
|
||||
"""
|
||||
|
||||
# `query`, `key` and `value` have shape `[seq_len, batch_size, d_model]`
|
||||
seq_len, _, _ = query.shape
|
||||
|
||||
if mask is not None:
|
||||
# `mask` has shape `[seq_len_q, seq_len_k, batch_size]`,
|
||||
# where first dimension is the query dimension.
|
||||
# If the query dimension is equal to $1$ it will be broadcasted.
|
||||
assert mask.shape[0] == 1 or mask.shape[0] == query.shape[0]
|
||||
assert mask.shape[1] == key.shape[0]
|
||||
assert mask.shape[2] == 1 or mask.shape[2] == query.shape[1]
|
||||
|
||||
# Transform query, key and value embeddings
|
||||
query = self.query(query)
|
||||
key = self.key(key)
|
||||
value = self.value(value)
|
||||
|
||||
# Get
|
||||
#
|
||||
# \begin{align}
|
||||
# w'_{t,t'} =
|
||||
# \begin{cases}
|
||||
# w_{t,t'}, & {\text{for }\lvert t-t' \rvert \lt s} \\
|
||||
# 0, & \text{otherwise}
|
||||
# \end{cases}
|
||||
# \end{align}
|
||||
#
|
||||
# using the mask
|
||||
pos_bias = self.pos_bias[:seq_len, :seq_len] * self.local_mask[:seq_len, :seq_len]
|
||||
pos_bias = pos_bias.unsqueeze(-1)
|
||||
pos_bias.masked_fill_(~mask, float('-inf'))
|
||||
|
||||
# \begin{align}
|
||||
# Y_t &= \sigma(Q_t) \odot
|
||||
# \frac{\sum_{t'=1}^T \exp(K_{t'} + w_{t,t'}) \odot V_{t'}}
|
||||
# {\sum_{t'=1}^T \exp(K_{t'} + w_{t,t'})} \\
|
||||
# &= \sigma(Q_t) \odot
|
||||
# \frac{\sum_{t'=1}^T \exp(w_{t,t'}) \odot \exp(K_{t'}) \odot V_{t'}}
|
||||
# {\sum_{t'=1}^T \exp(w_{t,t'}) \odot \exp(K_{t'})}
|
||||
# \end{align}
|
||||
#
|
||||
# We compute $\exp(w_{t,t'})$, $\exp(K_{t'}) \odot V_{t'}$ and $\exp(K_{t'})$
|
||||
# separately and do a matrix multiplication. We use einsum for clarity.
|
||||
|
||||
# We subtract $\max_{t'}(K_{t'})$ and $\max_{t'}(w_{t,t'})$ before calculating the exponents to stabilize
|
||||
# the softmax calculation.
|
||||
#
|
||||
# If $x_i$ is large $\exp(x_i)$ becomes huge and the computation of
|
||||
# $\frac{\sum\exp(x_i)y_i}{\sum\exp(x_i)}$becomes unstable.
|
||||
# Subtracting a constant before calculating the exponent from numerator and denominator will cancel out.
|
||||
# and can help stabilize the computation.
|
||||
# So we subtract $\max(x_i)$ to stabilize the computation.
|
||||
max_key = key.max(dim=0, keepdims=True)[0]
|
||||
max_pos_bias = pos_bias.max(dim=1, keepdims=True)[0]
|
||||
|
||||
# $\exp \big(K_{t'}- \max_{t'}(K_{t'})\big)$
|
||||
exp_key = torch.exp(key - max_key)
|
||||
# $\exp \big(w_{t,t'} - \max_{t'}(w_{t,t'})\big)$
|
||||
exp_pos_bias = torch.exp(pos_bias - max_pos_bias)
|
||||
|
||||
# The numerator part $\sum_{t'=1}^T \exp(w_{t,t'}) \odot \exp(K_{t'}) \odot V_{t'}$
|
||||
num = torch.einsum('ijb,jbd->ibd', exp_pos_bias, exp_key * value)
|
||||
# The denominator part $\sum_{t'=1}^T \exp(w_{t,t'}) \odot \exp(K_{t'})$
|
||||
den = torch.einsum('ijb,jbd->ibd', exp_pos_bias, exp_key)
|
||||
|
||||
# Output $$Y_t = \sigma(Q_t) \odot
|
||||
# \frac{\sum_{t'=1}^T \exp(w_{t,t'}) \odot \exp(K_{t'}) \odot V_{t'}}
|
||||
# {\sum_{t'=1}^T \exp(w_{t,t'}) \odot \exp(K_{t'})}$$
|
||||
y = self.activation(query) * num / den
|
||||
|
||||
# Output layer
|
||||
return self.output(y)
|
||||
|
||||
|
||||
def _test_local_mask():
|
||||
"""
|
||||
Test local mask
|
||||
"""
|
||||
from labml.logger import inspect
|
||||
inspect(AFTLocal.create_local_mask(10, 4))
|
||||
|
||||
|
||||
#
|
||||
if __name__ == '__main__':
|
||||
_test_local_mask()
|
||||
@@ -0,0 +1,162 @@
|
||||
"""
|
||||
---
|
||||
title: Attention Free Transformer (AFT) Experiment
|
||||
summary: This experiment trains an Attention Free Transformer (AFT) based model on Tiny Shakespeare dataset.
|
||||
---
|
||||
|
||||
# [Attention Free Transformer (AFT)](index.html) Experiment
|
||||
|
||||
This is an annotated PyTorch experiment to train a [AFT model](index.html).
|
||||
|
||||
This is based on
|
||||
[general training loop and configurations for auto-regressive NLP task](../../experiments/nlp_autoregression.html).
|
||||
"""
|
||||
import torch
|
||||
from labml import experiment
|
||||
from labml.configs import option
|
||||
from labml_nn.experiments.nlp_autoregression import NLPAutoRegressionConfigs
|
||||
from labml_nn.transformers import TransformerConfigs, Encoder
|
||||
from labml_nn.transformers.utils import subsequent_mask
|
||||
from torch import nn
|
||||
|
||||
|
||||
class AutoregressiveTransformer(nn.Module):
|
||||
"""
|
||||
## Simple autoregressive model
|
||||
|
||||
This consists of a token embedding layer, transformer encoder, and
|
||||
a final linear layer that gives token logits.
|
||||
"""
|
||||
|
||||
def __init__(self, encoder: Encoder, src_embed: nn.Module, generator: nn.Module):
|
||||
"""
|
||||
* `encoder` is the transformer [Encoder](../models.html#Encoder)
|
||||
* `src_embed` is the token
|
||||
[embedding module (with positional encodings)](../models.html#EmbeddingsWithLearnedPositionalEncoding)
|
||||
* `generator` is the [final fully connected layer](../models.html#Generator) that gives the logits.
|
||||
"""
|
||||
super().__init__()
|
||||
self.src_embed = src_embed
|
||||
self.encoder = encoder
|
||||
self.generator = generator
|
||||
|
||||
# The mask will be initialized on the first call
|
||||
self.mask = None
|
||||
|
||||
def forward(self, x: torch.Tensor):
|
||||
# Create subsequent mask if mask is not initialized
|
||||
# or if the size of the mask is different
|
||||
if self.mask is None or self.mask.size(0) != len(x):
|
||||
# Subsequent mask, will mask out tokens from seeing future tokens
|
||||
self.mask = subsequent_mask(len(x)).to(x.device)
|
||||
|
||||
# Get the token embeddings with positional encodings
|
||||
x = self.src_embed(x)
|
||||
# Transformer encoder
|
||||
x = self.encoder(x, self.mask)
|
||||
# Get logits
|
||||
x = self.generator(x)
|
||||
|
||||
# Return results
|
||||
# (second value is for state, since our trainer is used with RNNs also)
|
||||
return x, None
|
||||
|
||||
|
||||
class Configs(NLPAutoRegressionConfigs):
|
||||
"""
|
||||
## Configurations
|
||||
|
||||
This inherits from
|
||||
[`NLPAutoRegressionConfigs`](../../experiments/nlp_autoregression.html#NLPAutoRegressionConfigs)
|
||||
"""
|
||||
|
||||
# GPT model
|
||||
model: AutoregressiveTransformer
|
||||
# Transformer
|
||||
transformer: TransformerConfigs
|
||||
|
||||
local_window_size: int = 32
|
||||
|
||||
|
||||
@option(Configs.transformer, 'Transformer')
|
||||
def _transformer_configs(c: Configs):
|
||||
"""
|
||||
### Transformer configurations
|
||||
"""
|
||||
|
||||
# We use our
|
||||
# [configurable transformer implementation](../configs.html#TransformerConfigs)
|
||||
conf = TransformerConfigs()
|
||||
# Set the vocabulary sizes for embeddings and generating logits
|
||||
conf.n_src_vocab = c.n_tokens
|
||||
conf.n_tgt_vocab = c.n_tokens
|
||||
# Set the embedding size
|
||||
conf.d_model = c.d_model
|
||||
# Replace self-attention with an [AFT Local Module](index.html)
|
||||
from labml_nn.transformers.aft import AFTLocal
|
||||
conf.encoder_attn = AFTLocal(c.d_model, c.seq_len, c.local_window_size)
|
||||
|
||||
#
|
||||
return conf
|
||||
|
||||
|
||||
@option(Configs.model)
|
||||
def _model(c: Configs):
|
||||
"""
|
||||
Create an auto-regressive model
|
||||
"""
|
||||
m = AutoregressiveTransformer(c.transformer.encoder,
|
||||
c.transformer.src_embed,
|
||||
c.transformer.generator).to(c.device)
|
||||
|
||||
return m
|
||||
|
||||
|
||||
def main():
|
||||
# Create experiment
|
||||
experiment.create(name="aft")
|
||||
# Create configs
|
||||
conf = Configs()
|
||||
# Override configurations
|
||||
experiment.configs(conf, {
|
||||
# Use character level tokenizer
|
||||
'tokenizer': 'character',
|
||||
# Prompt separator is blank
|
||||
'prompt_separator': '',
|
||||
# Starting prompt for sampling
|
||||
'prompt': 'It is ',
|
||||
# Use Tiny Shakespeare dataset
|
||||
'text': 'tiny_shakespeare',
|
||||
|
||||
# Use a context size of $128$
|
||||
'seq_len': 256,
|
||||
# Train for $32$ epochs
|
||||
'epochs': 128,
|
||||
# Batch size $128$
|
||||
'batch_size': 32,
|
||||
# Switch between training and validation for $10$ times
|
||||
# per epoch
|
||||
'inner_iterations': 10,
|
||||
|
||||
# Embedding size
|
||||
'd_model': 128,
|
||||
# FFN hidden dimension size
|
||||
'transformer.ffn.d_ff': 256,
|
||||
|
||||
# Optimizer
|
||||
'optimizer.optimizer': 'Noam',
|
||||
'optimizer.learning_rate': 1.,
|
||||
})
|
||||
|
||||
# Set models for saving and loading
|
||||
experiment.add_pytorch_models({'model': conf.model})
|
||||
|
||||
# Start the experiment
|
||||
with experiment.start():
|
||||
# Run training
|
||||
conf.run()
|
||||
|
||||
|
||||
#
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,13 @@
|
||||
# [An Attention Free Transformer](https://nn.labml.ai/transformers/aft/index.html)
|
||||
|
||||
This is a [PyTorch](https://pytorch.org) implementation of the paper
|
||||
[An Attention Free Transformer](https://arxiv.org/abs/2105.14103).
|
||||
|
||||
This paper replaces the [self-attention layer](https://nn.labml.ai/transformers/mha.html)
|
||||
with a new efficient operation,
|
||||
that has memory complexity of O(Td), where T is the sequence length
|
||||
and $d$ is the dimensionality of embeddings.
|
||||
|
||||
The paper introduces AFT along with AFT-local and AFT-conv.
|
||||
Here we have implemented AFT-local which pays attention to closeby tokens
|
||||
in an autoregressive model.
|
||||
@@ -0,0 +1,205 @@
|
||||
"""
|
||||
---
|
||||
title: Attention with Linear Biases (ALiBi)
|
||||
summary: >
|
||||
Documented implementation with explanations of Attention with Linear Biases (ALiBi)
|
||||
---
|
||||
|
||||
# Attention with Linear Biases (ALiBi)
|
||||
|
||||
This is an implementation of Attention with Linear Biases (ALiBi) from the paper
|
||||
[Train Short, Test Long: Attention with Linear Biases Enables Input Length Extrapolation](https://arxiv.org/abs/2108.12409).
|
||||
|
||||
This replaces positional encodings with biases added to attention scores (attention logits, before the softmax).
|
||||
This is a relative scheme tested on autoregressive tasks, and the bias is higher for closeby tokens
|
||||
and lower for far-away tokens.
|
||||
The biases decrease linearly in the log scale (because it's before the softmax) and each head has a different slope.
|
||||
|
||||
Here's the attention formula for $i$-th token,
|
||||
|
||||
\begin{align}
|
||||
\mathbf{a}_i
|
||||
&= \text{softmax} \bigg( \mathbf{q}_i \mathbf{K}^\top + m \cdot \big[-(i-1), \dots, -1, 0 \big] \bigg) \\
|
||||
&= \text{softmax} \bigg( \mathbf{q}_i \mathbf{K}^\top + m \cdot \big[0, 1, \dots, (i - 1) \big] \bigg)
|
||||
\end{align}
|
||||
|
||||
where $\mathbf{q}_i \in \mathbb{R}^d$ is the query of the $i$-th token, $K \in \mathbb{R}^{i \times d}$ are the keys
|
||||
up to $i$, and $d$ the number of features per head.
|
||||
Note that the above equality halts because $\text{softmax}$ is invariant to translations
|
||||
(you can add any constant to all elements without changing the result).
|
||||
|
||||
Here is [the training code](experiment.html) for a ALiBi model.
|
||||
"""
|
||||
import math
|
||||
from typing import Optional
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
from labml.logger import inspect
|
||||
from labml_nn.transformers.mha import MultiHeadAttention
|
||||
|
||||
|
||||
def get_slopes(n_heads: int):
|
||||
"""
|
||||
## Get head-specific slope $m$ for each head
|
||||
|
||||
* `n_heads` is the number of heads in the attention layer $n$
|
||||
|
||||
The slope for first head is
|
||||
|
||||
$$\frac{1}{2^{\frac{8}{n}}} = 2^{-\frac{8}{n}}$$
|
||||
|
||||
The slopes for the rest of the heads are in a geometric series with a ratio same as above.
|
||||
|
||||
For instance when the number of heads is $8$ the slopes are
|
||||
$$\frac{1}{2^1}, \frac{1}{2^2}, \dots, \frac{1}{2^8}$$
|
||||
"""
|
||||
|
||||
# Get the closest power of 2 to `n_heads`.
|
||||
# If `n_heads` is not a power of 2, then we first calculate slopes to the closest (smaller) power of 2,
|
||||
# and then add the remaining slopes.
|
||||
n = 2 ** math.floor(math.log2(n_heads))
|
||||
# $2^{-\frac{8}{n}}$
|
||||
m_0 = 2.0 ** (-8.0 / n)
|
||||
# $2^{-1\frac{8}{n}}, 2^{-2 \frac{8}{n}}, 2^{-3 \frac{8}{n}}, \dots$
|
||||
m = torch.pow(m_0, torch.arange(1, 1 + n))
|
||||
|
||||
# If `n_heads` is not a power of 2, then we add the remaining slopes.
|
||||
# We calculate the remaining slopes for $n * 2$ (avoiding slopes added previously).
|
||||
# And pick the slopes upto `n_heads`.
|
||||
if n < n_heads:
|
||||
# $2^{-\frac{8}{2n}}$
|
||||
m_hat_0 = 2.0 ** (-4.0 / n)
|
||||
# $2^{-1\frac{8}{2n}}, 2^{-3 \frac{8}{2n}}, 2^{-5 \frac{8}{2n}}, \dots$
|
||||
# Note that we take steps by $2$ to avoid slopes added previously.
|
||||
m_hat = torch.pow(m_hat_0, torch.arange(1, 1 + 2 * (n_heads - n), 2))
|
||||
# Concatenate the slopes with the remaining slopes.
|
||||
m = torch.cat([m, m_hat])
|
||||
|
||||
return m
|
||||
|
||||
|
||||
@torch.no_grad()
|
||||
def get_alibi_biases(n_heads: int, mask: torch.Tensor):
|
||||
"""
|
||||
## Calculate the attention biases matrix
|
||||
|
||||
* `n_heads` is the number of heads in the attention layer
|
||||
* `mask` is the attention mask of shape `[seq_len_q, seq_len_k]`
|
||||
|
||||
This returns a matrix of shape `[seq_len_q, seq_len_k, n_heads, ]` with ALiBi attention biases.
|
||||
"""
|
||||
|
||||
# Get slopes $m$ for each head
|
||||
m = get_slopes(n_heads).to(mask.device)
|
||||
|
||||
# Calculate distances $[0, 1, \dots, N]$
|
||||
# Here we calculate the distances using the mask.
|
||||
#
|
||||
# Since it's causal mask we can just use $[0, 1, \dots, N]$ too.
|
||||
# `distance = torch.arange(mask.shape[1], dtype=torch.long, device=mask.device)[None, :]`
|
||||
distance = mask.cumsum(dim=-1)
|
||||
|
||||
# Multiply them pair-wise to get the AliBi bias matrix
|
||||
return distance[:, :, None] * m[None, None, :]
|
||||
|
||||
|
||||
class AlibiMultiHeadAttention(MultiHeadAttention):
|
||||
"""
|
||||
## Attention with Linear Biases (ALiBi)
|
||||
|
||||
We override [Multi-Head Attention](../mha.html).
|
||||
"""
|
||||
|
||||
def __init__(self, heads: int, d_model: int, dropout_prob: float = 0.1):
|
||||
super().__init__(heads, d_model, dropout_prob)
|
||||
|
||||
# To cache AliBi the biases
|
||||
self.alibi_biases = None
|
||||
|
||||
def forward(self, *,
|
||||
query: torch.Tensor,
|
||||
key: torch.Tensor,
|
||||
value: torch.Tensor,
|
||||
mask: Optional[torch.Tensor] = None):
|
||||
"""
|
||||
`query`, `key` and `value` are the tensors that store
|
||||
collection of *query*, *key* and *value* vectors.
|
||||
They have shape `[seq_len, batch_size, d_model]`.
|
||||
|
||||
`mask` has shape `[seq_len, seq_len, batch_size]` and
|
||||
`mask[i, j, b]` indicates whether for batch `b`,
|
||||
query at position `i` has access to key-value at position `j`.
|
||||
"""
|
||||
|
||||
# ALiBi only works with causal masks.
|
||||
assert mask is not None
|
||||
assert mask.shape[0] == mask.shape[1] and mask.shape[2] == 1
|
||||
|
||||
# `query`, `key` and `value` have shape `[seq_len, batch_size, d_model]`
|
||||
seq_len, batch_size, _ = query.shape
|
||||
|
||||
# Add head dimension to mask and check its shape.
|
||||
mask = self.prepare_mask(mask, query.shape, key.shape)
|
||||
|
||||
# Prepare `query`, `key` and `value` for attention computation.
|
||||
# These will then have shape `[seq_len, batch_size, heads, d_k]`.
|
||||
query = self.query(query)
|
||||
key = self.key(key)
|
||||
value = self.value(value)
|
||||
|
||||
# Compute attention scores $Q K^\top$.
|
||||
# This gives a tensor of shape `[seq_len, seq_len, batch_size, heads]`.
|
||||
scores = self.get_scores(query, key)
|
||||
|
||||
# Scale scores $\frac{Q K^\top}{\sqrt{d_k}}$
|
||||
scores *= self.scale
|
||||
|
||||
# Create AliBi biases if it's not cached
|
||||
if self.alibi_biases is None or self.alibi_biases.shape[1] < seq_len:
|
||||
# `mask` has shape `[seq_len, seq_len, 1, 1]`
|
||||
self.alibi_biases = get_alibi_biases(scores.shape[-1], mask[:, :, 0, 0])
|
||||
|
||||
# Add AliBi biases to attention scores.
|
||||
# ALiBi biases has shape `[seq_len, seq_len, n_heads]`
|
||||
# and `scores` has shape `[seq_len, seq_len, batch_size, n_heads]`
|
||||
scores += self.alibi_biases[:seq_len, :seq_len, None, :]
|
||||
|
||||
# Apply mask
|
||||
scores = scores.masked_fill(mask == 0, float('-inf'))
|
||||
|
||||
# $softmax$ attention along the key sequence dimension
|
||||
# $\underset{seq}{softmax}\Bigg(\frac{Q K^\top}{\sqrt{d_k}}\Bigg)$
|
||||
attn = self.softmax(scores)
|
||||
|
||||
# Apply dropout
|
||||
attn = self.dropout(attn)
|
||||
|
||||
# Multiply by values
|
||||
# $$\underset{seq}{softmax}\Bigg(\frac{Q K^\top}{\sqrt{d_k}}\Bigg)V$$
|
||||
x = torch.einsum("ijbh,jbhd->ibhd", attn, value)
|
||||
|
||||
# Concatenate multiple heads
|
||||
x = x.reshape(seq_len, batch_size, -1)
|
||||
|
||||
# Output layer
|
||||
return self.output(x)
|
||||
|
||||
|
||||
def _test_alibi():
|
||||
"""
|
||||
Simple test function to see the slopes.
|
||||
"""
|
||||
inspect(get_slopes(12).tolist(), _n=-1)
|
||||
from labml_nn.transformers.utils import subsequent_mask
|
||||
|
||||
mask = subsequent_mask(8)[:, :, 0]
|
||||
inspect(mask)
|
||||
|
||||
inspect(get_alibi_biases(12, mask)[:, :, 3], _n=-1)
|
||||
|
||||
|
||||
#
|
||||
if __name__ == '__main__':
|
||||
_test_alibi()
|
||||
@@ -0,0 +1,155 @@
|
||||
"""
|
||||
---
|
||||
title: Attention with Linear Biases (ALiBi) Experiment
|
||||
summary: This experiment trains an Attention with Linear Biases (ALiBi) based model on Tiny Shakespeare dataset.
|
||||
---
|
||||
|
||||
# [Attention with Linear Biases (ALiBi)](index.html) Experiment
|
||||
|
||||
This is an annotated PyTorch experiment to train a [ALiBi model](index.html).
|
||||
|
||||
This is based on [our GPT model](../gpt/index.html).
|
||||
"""
|
||||
|
||||
import torch
|
||||
from torch.utils.data import DataLoader
|
||||
|
||||
from labml import experiment, tracker
|
||||
from labml.configs import option, calculate
|
||||
from labml_nn.helpers.datasets import SequentialUnBatchedDataset
|
||||
from labml_nn.transformers.alibi import AlibiMultiHeadAttention
|
||||
from labml_nn.experiments.nlp_autoregression import transpose_batch
|
||||
from labml_nn.transformers import TransformerConfigs
|
||||
from labml_nn.transformers.gpt import Configs as GPTConfigs
|
||||
|
||||
|
||||
class Configs(GPTConfigs):
|
||||
"""
|
||||
## Configurations
|
||||
|
||||
We extend [GPT configurations](../gpt/index.html) and change the attention mechanism.
|
||||
"""
|
||||
|
||||
# ALiBi based transformer (defined below)
|
||||
transformer: TransformerConfigs = 'GPT_ALiBi'
|
||||
# Longer validation set
|
||||
valid_seq_len: int = 128
|
||||
valid_loader = 'shuffled_longer_valid_loader'
|
||||
|
||||
def other_metrics(self, output: torch.Tensor, target: torch.Tensor):
|
||||
"""
|
||||
Log losses at the initial and final tokens
|
||||
"""
|
||||
# If there are more tokens that the training sequence length (during validation),
|
||||
if self.seq_len < output.shape[0]:
|
||||
# Log the loss at training sequence length
|
||||
tracker.add(f'loss.{self.seq_len - 1}.', self.loss_func(output[self.seq_len - 1], target[self.seq_len - 1]))
|
||||
# Log the loss at the first token
|
||||
tracker.add(f'loss.0.', self.loss_func(output[0], target[0]))
|
||||
# Log the loss at the final token
|
||||
tracker.add(f'loss.{int(output.shape[0]) - 1}.', self.loss_func(output[-1], target[-1]))
|
||||
|
||||
|
||||
def _alibi_mha(c: TransformerConfigs):
|
||||
"""
|
||||
Create an ALiBi attention module
|
||||
"""
|
||||
return AlibiMultiHeadAttention(c.n_heads, c.d_model, dropout_prob=c.dropout)
|
||||
|
||||
|
||||
# Set all attention mechanisms to ALiBi
|
||||
calculate(TransformerConfigs.encoder_attn, 'alibi_mha', _alibi_mha)
|
||||
calculate(TransformerConfigs.decoder_attn, 'alibi_mha', _alibi_mha)
|
||||
calculate(TransformerConfigs.decoder_mem_attn, 'alibi_mha', _alibi_mha)
|
||||
|
||||
|
||||
@option(Configs.valid_loader)
|
||||
def shuffled_longer_valid_loader(c: Configs):
|
||||
"""
|
||||
Shuffled validation data loader with `valid_seq_len` sequence length
|
||||
"""
|
||||
return DataLoader(SequentialUnBatchedDataset(text=c.text.valid,
|
||||
dataset=c.text,
|
||||
seq_len=c.valid_seq_len),
|
||||
batch_size=c.batch_size,
|
||||
collate_fn=transpose_batch,
|
||||
shuffle=True)
|
||||
|
||||
|
||||
@option(Configs.transformer, 'GPT_ALiBi')
|
||||
def _transformer_configs(c: Configs):
|
||||
"""
|
||||
### ALiBi based Transformer configurations
|
||||
"""
|
||||
|
||||
# We use our
|
||||
# [configurable transformer implementation](../configs.html#TransformerConfigs)
|
||||
conf = TransformerConfigs()
|
||||
# Set the vocabulary sizes for embeddings and generating logits
|
||||
conf.n_src_vocab = c.n_tokens
|
||||
conf.n_tgt_vocab = c.n_tokens
|
||||
# GPT uses GELU activation for position wise feedforward
|
||||
conf.ffn.activation = 'GELU'
|
||||
|
||||
# ALiBi doesn't use positional embeddings
|
||||
conf.src_embed = 'no_pos'
|
||||
conf.tgt_embed = 'no_pos'
|
||||
|
||||
# Set all attention mechanisms to ALiBi
|
||||
conf.encoder_attn = 'alibi_mha'
|
||||
conf.decoder_attn = 'alibi_mha'
|
||||
conf.decoder_mem_attn = 'alibi_mha'
|
||||
|
||||
#
|
||||
return conf
|
||||
|
||||
|
||||
def main():
|
||||
# Create experiment
|
||||
experiment.create(name="gpt_alibi")
|
||||
# Create configs
|
||||
conf = Configs()
|
||||
# Override configurations
|
||||
experiment.configs(conf, {
|
||||
# Use character level tokenizer
|
||||
'tokenizer': 'character',
|
||||
# Prompt separator is blank
|
||||
'prompt_separator': '',
|
||||
# Starting prompt for sampling
|
||||
'prompt': 'It is ',
|
||||
# Use Tiny Shakespeare dataset
|
||||
'text': 'tiny_shakespeare',
|
||||
# 'text': 'tiny_shakespeare_no_split',
|
||||
|
||||
# Use a context size of $128$
|
||||
'seq_len': 64,
|
||||
# Use a context size of $128$
|
||||
'valid_seq_len': 80,
|
||||
# Train for $32$ epochs
|
||||
'epochs': 128,
|
||||
# Batch size $128$
|
||||
'batch_size': 128,
|
||||
# Switch between training and validation for $10$ times
|
||||
# per epoch
|
||||
'inner_iterations': 10,
|
||||
|
||||
# Transformer configurations
|
||||
'transformer.d_model': 128,
|
||||
'transformer.ffn.d_ff': 512,
|
||||
'transformer.n_heads': 8,
|
||||
'transformer.n_layers': 4,
|
||||
'transformer.dropout': 0.1,
|
||||
})
|
||||
|
||||
# Set models for saving and loading
|
||||
experiment.add_pytorch_models({'model': conf.model})
|
||||
|
||||
# Start the experiment
|
||||
with experiment.start():
|
||||
# Run training
|
||||
conf.run()
|
||||
|
||||
|
||||
#
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,295 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "AYV_dMVDxyc2",
|
||||
"pycharm": {
|
||||
"name": "#%% md\n"
|
||||
}
|
||||
},
|
||||
"source": [
|
||||
"[](https://github.com/labmlai/annotated_deep_learning_paper_implementations)\n",
|
||||
"[](https://colab.research.google.com/github/labmlai/annotated_deep_learning_paper_implementations/blob/master/labml_nn/transformers/basic/autoregressive_experiment.ipynb)\n",
|
||||
"\n",
|
||||
"## Transformer Experiment\n",
|
||||
"\n",
|
||||
"This trains a simple transformer with\n",
|
||||
"[multi headed attention](https://nn.labml.ai/transformers/mha.html)\n",
|
||||
"introduced in [Attention Is All You Need](https://arxiv.org/abs/1706.03762)\n",
|
||||
"on an NLP auto-regression task (with Tiny Shakespeare dataset)."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "AahG_i2y5tY9",
|
||||
"pycharm": {
|
||||
"name": "#%% md\n"
|
||||
}
|
||||
},
|
||||
"source": [
|
||||
"### Install the packages"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"base_uri": "https://localhost:8080/"
|
||||
},
|
||||
"id": "ZCzmCrAIVg0L",
|
||||
"outputId": "cf107fb2-4d50-4c67-af34-367624553421",
|
||||
"pycharm": {
|
||||
"name": "#%%\n"
|
||||
}
|
||||
},
|
||||
"source": [
|
||||
"!pip install labml-nn --quiet"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "SE2VUQ6L5zxI",
|
||||
"pycharm": {
|
||||
"name": "#%% md\n"
|
||||
}
|
||||
},
|
||||
"source": [
|
||||
"### Imports"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"id": "0hJXx_g0wS2C",
|
||||
"pycharm": {
|
||||
"name": "#%%\n"
|
||||
}
|
||||
},
|
||||
"source": [
|
||||
"from labml import experiment\n",
|
||||
"from labml_nn.transformers.basic.autoregressive_experiment import Configs"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "Lpggo0wM6qb-",
|
||||
"pycharm": {
|
||||
"name": "#%% md\n"
|
||||
}
|
||||
},
|
||||
"source": [
|
||||
"### Create an experiment"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"id": "bFcr9k-l4cAg",
|
||||
"pycharm": {
|
||||
"name": "#%%\n"
|
||||
}
|
||||
},
|
||||
"source": [
|
||||
"experiment.create(name=\"transformer\", writers={'screen'})"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "-OnHLi626tJt",
|
||||
"pycharm": {
|
||||
"name": "#%% md\n"
|
||||
}
|
||||
},
|
||||
"source": [
|
||||
"### Configurations"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"id": "Piz0c5f44hRo",
|
||||
"pycharm": {
|
||||
"name": "#%%\n"
|
||||
}
|
||||
},
|
||||
"source": [
|
||||
"conf = Configs()"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "wwMzCqpD6vkL",
|
||||
"pycharm": {
|
||||
"name": "#%% md\n"
|
||||
}
|
||||
},
|
||||
"source": [
|
||||
"Set experiment configurations and assign a configurations dictionary to override configurations"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"base_uri": "https://localhost:8080/",
|
||||
"height": 17
|
||||
},
|
||||
"id": "e6hmQhTw4nks",
|
||||
"outputId": "29634715-42f4-4405-fb11-fc9522608627",
|
||||
"pycharm": {
|
||||
"name": "#%%\n"
|
||||
}
|
||||
},
|
||||
"source": [
|
||||
"experiment.configs(conf, {\n",
|
||||
" # Use character level tokenizer\n",
|
||||
" 'tokenizer': 'character',\n",
|
||||
" # Prompt separator is blank\n",
|
||||
" 'prompt_separator': '',\n",
|
||||
" # Starting prompt for sampling\n",
|
||||
" 'prompt': 'It is ',\n",
|
||||
" # Use Tiny Shakespeare dataset\n",
|
||||
" 'text': 'tiny_shakespeare',\n",
|
||||
"\n",
|
||||
" # Use a context size of $256$\n",
|
||||
" 'seq_len': 512,\n",
|
||||
" # Train for 32 epochs\n",
|
||||
" 'epochs': 32,\n",
|
||||
" # Batch size $32$\n",
|
||||
" 'batch_size': 16,\n",
|
||||
" # Switch between training and validation for $10$ times\n",
|
||||
" # per epoch\n",
|
||||
" 'inner_iterations': 10,\n",
|
||||
"\n",
|
||||
" # Model size\n",
|
||||
" 'd_model': 256,\n",
|
||||
" 'transformer.n_heads': 16,\n",
|
||||
" 'transformer.ffn.d_ff': 1024,\n",
|
||||
"\n",
|
||||
" # Use [Noam optimizer](../../optimizers/noam.html)\n",
|
||||
" 'optimizer.optimizer': 'Noam',\n",
|
||||
" 'optimizer.learning_rate': 1.,\n",
|
||||
"})"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "EvI7MtgJ61w5",
|
||||
"pycharm": {
|
||||
"name": "#%% md\n"
|
||||
}
|
||||
},
|
||||
"source": [
|
||||
"Set PyTorch models for loading and saving"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"base_uri": "https://localhost:8080/",
|
||||
"height": 255
|
||||
},
|
||||
"id": "GDlt7dp-5ALt",
|
||||
"outputId": "e7548e8f-c541-4618-dc5a-1597cae42003",
|
||||
"pycharm": {
|
||||
"name": "#%%\n"
|
||||
}
|
||||
},
|
||||
"source": [
|
||||
"experiment.add_pytorch_models({'model': conf.model})"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "KJZRf8527GxL",
|
||||
"pycharm": {
|
||||
"name": "#%% md\n"
|
||||
}
|
||||
},
|
||||
"source": [
|
||||
"### Start the experiment and run the training loop."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"base_uri": "https://localhost:8080/",
|
||||
"height": 1000
|
||||
},
|
||||
"id": "aIAWo7Fw5DR8",
|
||||
"outputId": "db979785-bfe3-4eda-d3eb-8ccbe61053e5",
|
||||
"pycharm": {
|
||||
"name": "#%%\n"
|
||||
}
|
||||
},
|
||||
"source": [
|
||||
"# Start the experiment\n",
|
||||
"with experiment.start():\n",
|
||||
" conf.run()"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"id": "oBXXlP2b7XZO",
|
||||
"pycharm": {
|
||||
"name": "#%%\n"
|
||||
}
|
||||
},
|
||||
"source": [],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"accelerator": "GPU",
|
||||
"colab": {
|
||||
"collapsed_sections": [],
|
||||
"name": "Transformer",
|
||||
"provenance": []
|
||||
},
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.7.11"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 4
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
"""
|
||||
---
|
||||
title: Transformer Auto-Regression Experiment
|
||||
summary: >
|
||||
This trains a simple transformer model on NLP auto-regression.
|
||||
---
|
||||
|
||||
# Transformer Auto-Regression Experiment
|
||||
|
||||
[](https://colab.research.google.com/github/labmlai/annotated_deep_learning_paper_implementations/blob/master/labml_nn/transformers/basic/autoregressive_experiment.ipynb)
|
||||
|
||||
This trains a simple transformer introduced in [Attention Is All You Need](https://arxiv.org/abs/1706.03762)
|
||||
on an NLP auto-regression task (with Tiny Shakespeare dataset).
|
||||
"""
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
from labml import experiment
|
||||
from labml.configs import option
|
||||
from labml_nn.experiments.nlp_autoregression import NLPAutoRegressionConfigs
|
||||
from labml_nn.transformers import TransformerConfigs, Encoder
|
||||
from labml_nn.transformers.utils import subsequent_mask
|
||||
|
||||
|
||||
class AutoregressiveTransformer(nn.Module):
|
||||
"""
|
||||
## Auto-Regressive model
|
||||
"""
|
||||
def __init__(self, encoder: Encoder, src_embed: nn.Module, generator: nn.Module):
|
||||
"""
|
||||
* `encoder` is the transformer [Encoder](../models.html#Encoder)
|
||||
* `src_embed` is the token
|
||||
[embedding module (with positional encodings)](../models.html#EmbeddingsWithLearnedPositionalEncoding)
|
||||
* `generator` is the [final fully connected layer](../models.html#Generator) that gives the logits.
|
||||
"""
|
||||
super().__init__()
|
||||
self.src_embed = src_embed
|
||||
self.encoder = encoder
|
||||
self.generator = generator
|
||||
|
||||
# The mask will be initialized on the first call
|
||||
self.mask = None
|
||||
|
||||
def forward(self, x: torch.Tensor):
|
||||
# Create subsequent mask if mask is not initialized
|
||||
# or if the size of the mask is different
|
||||
if self.mask is None or self.mask.size(0) != len(x):
|
||||
# Subsequent mask, will mask out tokens from seeing future tokens
|
||||
self.mask = subsequent_mask(len(x)).to(x.device)
|
||||
# Get the token embeddings with positional encodings
|
||||
x = self.src_embed(x)
|
||||
# Transformer encoder
|
||||
x = self.encoder(x, self.mask)
|
||||
# Get logits
|
||||
x = self.generator(x)
|
||||
|
||||
# Return results
|
||||
# (second value is for state, since our trainer is used with RNNs also)
|
||||
return x, None
|
||||
|
||||
|
||||
class Configs(NLPAutoRegressionConfigs):
|
||||
"""
|
||||
## Configurations
|
||||
|
||||
This inherits from
|
||||
[`NLPAutoRegressionConfigs`](../../experiments/nlp_autoregression.html#NLPAutoRegressionConfigs)
|
||||
"""
|
||||
|
||||
# GPT model
|
||||
model: AutoregressiveTransformer
|
||||
# Transformer
|
||||
transformer: TransformerConfigs
|
||||
|
||||
|
||||
@option(Configs.transformer, 'Transformer')
|
||||
def _transformer_configs(c: Configs):
|
||||
"""
|
||||
### Transformer configurations
|
||||
"""
|
||||
|
||||
# We use our
|
||||
# [configurable transformer implementation](../configs.html#TransformerConfigs)
|
||||
conf = TransformerConfigs()
|
||||
# Set the vocabulary sizes for embeddings and generating logits
|
||||
conf.n_src_vocab = c.n_tokens
|
||||
conf.n_tgt_vocab = c.n_tokens
|
||||
#
|
||||
conf.d_model = c.d_model
|
||||
|
||||
#
|
||||
return conf
|
||||
|
||||
|
||||
@option(Configs.model)
|
||||
def _model(c: Configs):
|
||||
"""
|
||||
Create GPT model and initialize weights
|
||||
"""
|
||||
m = AutoregressiveTransformer(c.transformer.encoder,
|
||||
c.transformer.src_embed,
|
||||
c.transformer.generator).to(c.device)
|
||||
|
||||
return m
|
||||
|
||||
|
||||
def main():
|
||||
# Create experiment
|
||||
experiment.create(name="transformer")
|
||||
# Create configs
|
||||
conf = Configs()
|
||||
# Override configurations
|
||||
experiment.configs(conf, {
|
||||
# Use character level tokenizer
|
||||
'tokenizer': 'character',
|
||||
# Prompt separator is blank
|
||||
'prompt_separator': '',
|
||||
# Starting prompt for sampling
|
||||
'prompt': 'It is ',
|
||||
# Use Tiny Shakespeare dataset
|
||||
'text': 'tiny_shakespeare',
|
||||
|
||||
# Use a context size of $256$
|
||||
'seq_len': 512,
|
||||
# Train for 32 epochs
|
||||
'epochs': 32,
|
||||
# Batch size $32$
|
||||
'batch_size': 16,
|
||||
# Switch between training and validation for $10$ times
|
||||
# per epoch
|
||||
'inner_iterations': 10,
|
||||
|
||||
# Model size
|
||||
'd_model': 256,
|
||||
'transformer.n_heads': 16,
|
||||
'transformer.ffn.d_ff': 1024,
|
||||
|
||||
# Use [Noam optimizer](../../optimizers/noam.html)
|
||||
'optimizer.optimizer': 'Noam',
|
||||
'optimizer.learning_rate': 1.,
|
||||
})
|
||||
|
||||
# Set models for saving and loading
|
||||
experiment.add_pytorch_models({'model': conf.model})
|
||||
|
||||
# Start the experiment
|
||||
with experiment.start():
|
||||
# Run training
|
||||
conf.run()
|
||||
|
||||
|
||||
#
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,159 @@
|
||||
"""
|
||||
---
|
||||
title: Transformer Auto-Regression Experiment with [Sophia-G optimizer](../../optimizers/sophia.html)
|
||||
summary: >
|
||||
This trains a simple transformer model on NLP auto-regression with Sophia-G optimizer.
|
||||
---
|
||||
|
||||
# Transformer Auto-Regression Experiment with [Sophia-G optimizer](../../optimizers/sophia.html)
|
||||
|
||||
This trains a simple transformer introduced in [Attention Is All You Need](https://arxiv.org/abs/1706.03762)
|
||||
on an NLP auto-regression task (with Tiny Shakespeare dataset) with [Sophia-G optimizer](../../optimizers/sophia.html).
|
||||
"""
|
||||
import torch
|
||||
|
||||
from labml import experiment, tracker
|
||||
from labml_nn.helpers.trainer import BatchIndex
|
||||
from labml_nn.optimizers.sophia import Sophia
|
||||
from labml_nn.transformers.basic.autoregressive_experiment import Configs as TransformerAutoRegressionConfigs
|
||||
|
||||
|
||||
class Configs(TransformerAutoRegressionConfigs):
|
||||
"""
|
||||
## Configurations
|
||||
|
||||
This inherits from [`Configs`](autoregressive_experiment.html)
|
||||
"""
|
||||
|
||||
hess_interval: int = 10
|
||||
|
||||
optimizer: Sophia
|
||||
|
||||
def step(self, batch: any, batch_idx: BatchIndex):
|
||||
"""
|
||||
### Training or validation step with Gauss-Newton-Bartlett (GNB) Hessian diagonal estimator
|
||||
"""
|
||||
|
||||
# Set training/eval mode
|
||||
self.model.train(self.mode.is_train)
|
||||
|
||||
# Move data to the device
|
||||
data, target = batch[0].to(self.device), batch[1].to(self.device)
|
||||
|
||||
# Estimate the Hessian diagonal every $k$ steps
|
||||
if isinstance(self.optimizer, Sophia) and self.mode.is_train and batch_idx.idx % self.hess_interval == 0:
|
||||
# Get model outputs
|
||||
output, *_ = self.model(data)
|
||||
|
||||
# Create a categorical distribution from logits
|
||||
samp_dist = torch.distributions.Categorical(logits=output)
|
||||
# Sample $\hat{y}$
|
||||
y_sample = samp_dist.sample()
|
||||
|
||||
# Calculate and log loss
|
||||
loss = self.loss_func(output, y_sample)
|
||||
tracker.add("loss.hess.", loss)
|
||||
|
||||
# Calculate gradients
|
||||
loss.backward()
|
||||
# Clip gradients
|
||||
torch.nn.utils.clip_grad_norm_(self.model.parameters(), max_norm=self.grad_norm_clip)
|
||||
# Update EMA Hessian diagonal
|
||||
#
|
||||
# \begin{align}
|
||||
# \hat{h}_t &= B \cdot \nabla_\theta \hat{L} (\theta) \odot \nabla_\theta \hat{L} (\theta) \\
|
||||
# h_t &= \beta_2 h_{t-k} + (1 - \beta_2) \hat{h}_t
|
||||
# \end{align}
|
||||
self.optimizer.update_hessian(data.numel())
|
||||
# Clear the gradients
|
||||
self.optimizer.zero_grad()
|
||||
else:
|
||||
# Move data to the device
|
||||
data, target = batch[0].to(self.device), batch[1].to(self.device)
|
||||
|
||||
# Update global step (number of tokens processed) when in training mode
|
||||
if self.mode.is_train:
|
||||
tracker.add_global_step(data.shape[0] * data.shape[1])
|
||||
|
||||
# Get model outputs.
|
||||
# It's returning a tuple for states when using RNNs.
|
||||
# This is not implemented yet. 😜
|
||||
output, *_ = self.model(data)
|
||||
|
||||
# Calculate and log loss
|
||||
loss = self.loss_func(output, target)
|
||||
tracker.add("loss.", loss)
|
||||
|
||||
# Calculate and log accuracy
|
||||
self.accuracy(output, target)
|
||||
self.accuracy.track()
|
||||
|
||||
self.other_metrics(output, target)
|
||||
|
||||
# Train the model
|
||||
if self.mode.is_train:
|
||||
# Calculate gradients
|
||||
loss.backward()
|
||||
# Clip gradients
|
||||
torch.nn.utils.clip_grad_norm_(self.model.parameters(), max_norm=self.grad_norm_clip)
|
||||
# Take optimizer step
|
||||
self.optimizer.step()
|
||||
# Log the model parameters and gradients on last batch of every epoch
|
||||
if batch_idx.is_last and self.is_log_model_params_grads:
|
||||
tracker.add('model', self.model)
|
||||
# Clear the gradients
|
||||
self.optimizer.zero_grad()
|
||||
|
||||
# Save the tracked metrics
|
||||
tracker.save()
|
||||
|
||||
|
||||
def main():
|
||||
# Create experiment
|
||||
experiment.create(name="transformer")
|
||||
# Create configs
|
||||
conf = Configs()
|
||||
# Override configurations
|
||||
experiment.configs(conf, {
|
||||
# Use character level tokenizer
|
||||
'tokenizer': 'character',
|
||||
# Prompt separator is blank
|
||||
'prompt_separator': '',
|
||||
# Starting prompt for sampling
|
||||
'prompt': 'It is ',
|
||||
# Use Tiny Shakespeare dataset
|
||||
'text': 'tiny_shakespeare',
|
||||
|
||||
# Use a context size of $256$
|
||||
'seq_len': 512,
|
||||
# Train for 32 epochs
|
||||
'epochs': 32,
|
||||
# Batch size $32$
|
||||
'batch_size': 16,
|
||||
# Switch between training and validation for $10$ times
|
||||
# per epoch
|
||||
'inner_iterations': 10,
|
||||
|
||||
# Model size
|
||||
'd_model': 256,
|
||||
'transformer.n_heads': 16,
|
||||
'transformer.ffn.d_ff': 1024,
|
||||
|
||||
# Use [Sophia optimizer](../../optimizers/sophia.html)
|
||||
'optimizer.optimizer': 'Sophia',
|
||||
'optimizer.learning_rate': 3e-4,
|
||||
'optimizer.rho': 0.03,
|
||||
})
|
||||
|
||||
# Set models for saving and loading
|
||||
experiment.add_pytorch_models({'model': conf.model})
|
||||
|
||||
# Start the experiment
|
||||
with experiment.start():
|
||||
# Run training
|
||||
conf.run()
|
||||
|
||||
|
||||
#
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,334 @@
|
||||
"""
|
||||
---
|
||||
title: Compressive Transformer
|
||||
summary: >
|
||||
Documented implementation with explanations of a
|
||||
Compressive Transformer model.
|
||||
---
|
||||
|
||||
# Compressive Transformer
|
||||
|
||||
This is an implementation of
|
||||
[Compressive Transformers for Long-Range Sequence Modelling](https://arxiv.org/abs/1911.05507)
|
||||
in [PyTorch](https://pytorch.org).
|
||||
|
||||
This is an extension of [Transformer XL](../xl/index.html) where past memories
|
||||
are compressed to give a longer attention range.
|
||||
That is, the furthest $n_{cm} c$ memories are compressed into
|
||||
$n_{cm}$ memories, where $c$ is the compression rate.
|
||||
|
||||
## Compression operation
|
||||
|
||||
The compression operation is defined as
|
||||
$f_c: \mathbb{R}^{nc \times d} \rightarrow \mathbb{R}^{n \times d}$.
|
||||
The paper introduces multiple choices for $f_c$ and we have only implemented
|
||||
1D convolution which seems to give the best results.
|
||||
Each layer has a separate compression operation $f_c^{(i)}$ where
|
||||
$i$ is the layer number.
|
||||
|
||||
## Training compression operation
|
||||
|
||||
Since training compression with BPTT requires maintaining
|
||||
a very large computational graph (many time steps), the paper proposes
|
||||
an *auto-encoding loss* and an *attention reconstruction loss*.
|
||||
The auto-encoding loss decodes the original memories from the compressed memories
|
||||
and calculates the loss.
|
||||
Attention reconstruction loss computes the multi-headed attention results
|
||||
on the compressed memory and on uncompressed memory and gets a mean squared error
|
||||
between them.
|
||||
We have implemented the latter here since it gives better results.
|
||||
|
||||
This implementation uses pre-layer normalization
|
||||
while the paper uses post-layer normalization.
|
||||
Pre-layer norm does the layer norm before [FFN](../feedforward.html) and
|
||||
self-attention, and the pass-through in the residual connection is not normalized.
|
||||
This is supposed to be more stable in standard transformer setups.
|
||||
|
||||
Here are [the training code](experiment.html) and a notebook for training a compressive transformer
|
||||
model on the Tiny Shakespeare dataset.
|
||||
|
||||
[](https://colab.research.google.com/github/labmlai/annotated_deep_learning_paper_implementations/blob/master/labml_nn/transformers/compressive/experiment.ipynb)
|
||||
"""
|
||||
|
||||
from typing import Optional, List
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
from torch import nn
|
||||
|
||||
from labml_nn.transformers.feed_forward import FeedForward
|
||||
from labml_nn.transformers.mha import PrepareForMultiHeadAttention
|
||||
from labml_nn.transformers.xl.relative_mha import RelativeMultiHeadAttention
|
||||
from labml_nn.utils import clone_module_list
|
||||
|
||||
|
||||
class Conv1dCompression(nn.Module):
|
||||
"""
|
||||
## 1D Convolution Compression $f_c$
|
||||
|
||||
This is a simple wrapper around
|
||||
[`nn.Conv1d`](https://pytorch.org/docs/stable/generated/torch.nn.Conv1d.html)
|
||||
with some tensor dimension permutations.
|
||||
"""
|
||||
def __init__(self, compression_rate: int, d_model: int):
|
||||
"""
|
||||
* `compression_rate` $c$
|
||||
* `d_model` is the embedding size
|
||||
"""
|
||||
super().__init__()
|
||||
self.conv = nn.Conv1d(d_model, d_model, kernel_size=compression_rate, stride=compression_rate)
|
||||
|
||||
def forward(self, mem: torch.Tensor):
|
||||
"""
|
||||
`mem` has shape `[seq_len, batch, d_model]`
|
||||
"""
|
||||
|
||||
# Permute the dimensions of `mem` so that we can run it through the convolution layer.
|
||||
# The convolution layer accepts in the form `[batch, features, sequence]`
|
||||
mem = mem.permute(1, 2, 0)
|
||||
# Get compressed memory by running it through the convolution layer
|
||||
c_mem = self.conv(mem)
|
||||
# Permute back to form `[seq_len, batch, d_model]`
|
||||
return c_mem.permute(2, 0, 1)
|
||||
|
||||
|
||||
class CompressiveTransformerLayer(nn.Module):
|
||||
"""
|
||||
## Compressive Transformer Layer
|
||||
|
||||
This is the implementation of a single compressive transformer layer
|
||||
"""
|
||||
def __init__(self, *,
|
||||
d_model: int,
|
||||
self_attn: RelativeMultiHeadAttention,
|
||||
feed_forward: FeedForward,
|
||||
dropout_prob: float,
|
||||
compress: Conv1dCompression):
|
||||
"""
|
||||
* `d_model` is the token embedding size
|
||||
* `self_attn` is the [self attention module](../xl/relative_mha.html)
|
||||
* `feed_forward` is the [feed forward module](../feed_forward.html)
|
||||
* `dropout_prob` is the probability of dropping out after self attention and FFN
|
||||
* `compress` is the compression function $f_c$
|
||||
"""
|
||||
super().__init__()
|
||||
self.compress = compress
|
||||
self.size = d_model
|
||||
self.self_attn = self_attn
|
||||
self.feed_forward = feed_forward
|
||||
self.dropout = nn.Dropout(dropout_prob)
|
||||
self.norm_self_attn = nn.LayerNorm([d_model])
|
||||
self.norm_ff = nn.LayerNorm([d_model])
|
||||
|
||||
def concat_memory(self, z: torch.Tensor, mem: Optional[torch.Tensor], c_mem: Optional[torch.Tensor]):
|
||||
"""
|
||||
Concatenate the normalized token embeddings with memory and compressed memory.
|
||||
|
||||
* `z` is layer normalized token embeddings.
|
||||
* `mem` and `c_mem` are memory and compressed memory (not normalized).
|
||||
"""
|
||||
|
||||
# If there is no memory just return the token embeddings
|
||||
if mem is None:
|
||||
return z
|
||||
|
||||
# If there are compressed memory concatenate that with memory
|
||||
if c_mem is not None:
|
||||
mem = torch.cat((c_mem, mem), dim=0)
|
||||
|
||||
# Run the memory through the normalization layer
|
||||
mem = self.norm_self_attn(mem)
|
||||
# Concatenate normalized memory and normalized token embeddings
|
||||
return torch.cat((mem, z), dim=0)
|
||||
|
||||
def forward(self, *,
|
||||
x: torch.Tensor,
|
||||
mem: Optional[torch.Tensor],
|
||||
c_mem: Optional[torch.Tensor],
|
||||
mask: torch.Tensor):
|
||||
"""
|
||||
* `x` is a tensor of token level feature vectors of shape `[seq_len, batch_size, d_model]`
|
||||
* `mem` is a tensor of the past token level feature vectors (memory) of shape `[mem_len, batch_size, d_model]`
|
||||
* `c_mem` is a tensor of the compressed memory `[c_mem_len, batch_size, d_model]`
|
||||
* `mask` is a matrix of shape `[seq_len, c_mem_len + mem_len + seq_len, batch_size]` or `[seq_len, c_mem_len + mem_len + seq_len, 1]`.
|
||||
`mask[i, j]` is true if token at `i` can see token at `j`.
|
||||
"""
|
||||
|
||||
# Normalize the vectors before doing self attention
|
||||
z = self.norm_self_attn(x)
|
||||
# Normalize and concatenate memory and compressed memory
|
||||
m_z = self.concat_memory(z, mem, c_mem)
|
||||
# Attention
|
||||
self_attn = self.self_attn(query=z, key=m_z, value=m_z, mask=mask)
|
||||
# Add the attention results
|
||||
x = x + self.dropout(self_attn)
|
||||
|
||||
# Normalize for feed-forward
|
||||
z = self.norm_ff(x)
|
||||
# Pass through the feed-forward network
|
||||
ff = self.feed_forward(z)
|
||||
# Add the feed-forward results back
|
||||
x = x + self.dropout(ff)
|
||||
|
||||
#
|
||||
return x
|
||||
|
||||
|
||||
class CompressiveTransformer(nn.Module):
|
||||
"""
|
||||
## Compressive Transformer Model
|
||||
|
||||
This consists of multiple compressive transformer layers
|
||||
"""
|
||||
|
||||
def __init__(self, layer: CompressiveTransformerLayer, n_layers: int):
|
||||
super().__init__()
|
||||
# Make copies of the transformer layer
|
||||
self.layers = clone_module_list(layer, n_layers)
|
||||
# Final normalization layer
|
||||
self.norm = nn.LayerNorm([layer.size])
|
||||
|
||||
def forward(self, x: torch.Tensor, mem: List[torch.Tensor], c_mem: List[torch.Tensor], mask: torch.Tensor):
|
||||
"""
|
||||
* `x` is a tensor of the token embeddings vectors of shape `[seq_len, batch_size, d_model]`
|
||||
* `mem` is a list of tensors of the past token level feature vectors of shape
|
||||
`[mem_len, batch_size, d_model]` for each layer
|
||||
* `c_mem` is a list of tensors of the compressed memory
|
||||
`[c_mem_len, batch_size, d_model]` for each layer
|
||||
* `mask` is the masking matrix
|
||||
"""
|
||||
# List to store token level feature vectors,
|
||||
# which will become the memories for the next sequential batch.
|
||||
new_mem = []
|
||||
# Run through each transformer layer
|
||||
for i, layer in enumerate(self.layers):
|
||||
# Add to the list of feature vectors
|
||||
new_mem.append(x.detach())
|
||||
# Memory
|
||||
m = mem[i] if mem else None
|
||||
# Compressed Memory
|
||||
cm = c_mem[i] if c_mem else None
|
||||
# Run through the transformer XL layer
|
||||
x = layer(x=x, mem=m, c_mem=cm, mask=mask)
|
||||
# Finally, normalize the vectors
|
||||
return self.norm(x), new_mem
|
||||
|
||||
|
||||
class AttentionReconstructionLoss:
|
||||
"""
|
||||
## Attention Reconstruction Loss
|
||||
|
||||
Attention reconstruction loss recreates the self-attention output with
|
||||
uncompressed memory and with compressed memory and calculates the mean squared error
|
||||
between the two. It does this without positional encoding.
|
||||
|
||||
When calculating and training the compression function $f_c$ with attention
|
||||
reconstruction loss, all parameters but $f_c$ are frozen.
|
||||
This includes key/value projections and bias/scaling after normalization.
|
||||
|
||||
Since this loss can be computed independently of the cross-entropy-loss of the model
|
||||
you can have a separate optimizer that only updates $f_c$.
|
||||
However, we use the same optimizer to update $f_c$ so when calculating
|
||||
attention reconstruction loss, we detach all other parameters except $f_c$
|
||||
from the gradient computation.
|
||||
"""
|
||||
def __init__(self, layers: nn.ModuleList):
|
||||
"""
|
||||
`layers` is the list of Compressive Transformer layers
|
||||
"""
|
||||
self.layers = layers
|
||||
self.loss_func = nn.MSELoss()
|
||||
|
||||
def prepare_for_attn(self, pmha: PrepareForMultiHeadAttention, x: torch.Tensor):
|
||||
"""
|
||||
This is a reimplementation of ['PrepareForMultiHeadAttention'](../mha.html#PrepareMHA)
|
||||
where the projections are done with the parameters detached from gradient computation.
|
||||
|
||||
* `pmha` is the ['PrepareForMultiHeadAttention'](../mha.html#PrepareMHA) module
|
||||
* `x` is tensor with the token embeddings
|
||||
"""
|
||||
|
||||
# Shape of the input except embedding dimension; `[seq_len, batch_size]`.
|
||||
head_shape = x.shape[:-1]
|
||||
|
||||
# Detach projection weights and bias
|
||||
weight = pmha.linear.weight.detach()
|
||||
bias = pmha.linear.bias.detach() if pmha.linear.bias is not None else None
|
||||
# Linear transform
|
||||
x = F.linear(x, weight, bias)
|
||||
|
||||
# Split last dimension into heads
|
||||
x = x.view(*head_shape, pmha.heads, pmha.d_k)
|
||||
|
||||
# Output has shape `[seq_len, batch_size, heads, d_k]` or `[batch_size, d_model]`
|
||||
return x
|
||||
|
||||
def attn(self, layer: RelativeMultiHeadAttention, query: torch.Tensor, key: torch.Tensor, value: torch.Tensor):
|
||||
"""
|
||||
This is a reimplementation of ['Multi-Head Attention'](../mha.html#MHA) which calls
|
||||
`prepare_for_attn` instead of ['PrepareForMultiHeadAttention'](../mha.html#PrepareMHA)
|
||||
to detach projection parameters.
|
||||
"""
|
||||
# Calculate query, key and value projections
|
||||
query = self.prepare_for_attn(layer.query, query)
|
||||
key = self.prepare_for_attn(layer.key, key)
|
||||
value = self.prepare_for_attn(layer.value, value)
|
||||
|
||||
# Compute attention scores $Q K^\top$.
|
||||
# This gives a tensor of shape `[seq_len, seq_len, batch_size, heads]`.
|
||||
scores = torch.einsum('ibhd,jbhd->ijbh', query, key)
|
||||
|
||||
# Scale scores $\frac{Q K^\top}{\sqrt{d_k}}$
|
||||
scores *= layer.scale
|
||||
|
||||
# $softmax$ attention along the key sequence dimension
|
||||
# $\underset{seq}{softmax}\Bigg(\frac{Q K^\top}{\sqrt{d_k}}\Bigg)$
|
||||
attn = layer.softmax(scores)
|
||||
|
||||
# Multiply by values
|
||||
# $$\underset{seq}{softmax}\Bigg(\frac{Q K^\top}{\sqrt{d_k}}\Bigg)V$$
|
||||
return torch.einsum("ijbh,jbhd->ibhd", attn, value)
|
||||
|
||||
def norm(self, ln: nn.LayerNorm, x: torch.Tensor):
|
||||
"""
|
||||
Perform layer normalization with shift and scale parameters detached.
|
||||
"""
|
||||
|
||||
# Detach shift(`bias`) and scaling(`weight`) parameters
|
||||
weight = ln.weight.detach() if ln.weight is not None else None
|
||||
bias = ln.bias.detach() if ln.bias is not None else None
|
||||
|
||||
# Layer normalization
|
||||
return F.layer_norm(x, ln.normalized_shape, weight, bias, ln.eps)
|
||||
|
||||
def calc_loss(self, layer: CompressiveTransformerLayer, h: torch.Tensor, mem: torch.Tensor):
|
||||
"""
|
||||
This calculates the loss for a layer
|
||||
"""
|
||||
|
||||
# Detach the token embeddings and memory.
|
||||
h = h.detach()
|
||||
mem = mem.detach()
|
||||
|
||||
# Compress the memory with $f_c^{(i)}$.
|
||||
# The parameters of $f_c^{(i)}$ are the only parameters not detached from gradient computation.
|
||||
c_mem = layer.compress(mem)
|
||||
|
||||
# Normalize the embeddings and memories
|
||||
h = self.norm(layer.norm_self_attn, h)
|
||||
mem = self.norm(layer.norm_self_attn, mem)
|
||||
c_mem = self.norm(layer.norm_self_attn, c_mem)
|
||||
|
||||
# Calculate the attention with uncompressed memory
|
||||
attn_mem = self.attn(layer.self_attn, h, mem, mem)
|
||||
# Calculate the attention with compressed memory
|
||||
attn_cmem = self.attn(layer.self_attn, h, c_mem, c_mem)
|
||||
|
||||
# Calculate the mean square error
|
||||
return self.loss_func(attn_cmem, attn_mem)
|
||||
|
||||
def __call__(self, h: List[torch.Tensor], mem: List[torch.Tensor]):
|
||||
# Calculate the losses for each layer
|
||||
losses = [self.calc_loss(layer, h[n], mem[n]) for n, layer in enumerate(self.layers)]
|
||||
# Sum of the losses
|
||||
return sum(losses)
|
||||
@@ -0,0 +1,227 @@
|
||||
{
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 0,
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"name": "Compressive Transformer",
|
||||
"provenance": [],
|
||||
"collapsed_sections": []
|
||||
},
|
||||
"kernelspec": {
|
||||
"name": "python3",
|
||||
"display_name": "Python 3"
|
||||
},
|
||||
"accelerator": "GPU"
|
||||
},
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "AYV_dMVDxyc2"
|
||||
},
|
||||
"source": [
|
||||
"[](https://github.com/labmlai/annotated_deep_learning_paper_implementations)\n",
|
||||
"[](https://colab.research.google.com/github/labmlai/annotated_deep_learning_paper_implementations/blob/master/labml_nn/transformers/compressive/experiment.ipynb) \n",
|
||||
"\n",
|
||||
"## Compressive Transformer\n",
|
||||
"\n",
|
||||
"This is an experiment training Shakespeare dataset with a Compressive Transformer model."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "AahG_i2y5tY9"
|
||||
},
|
||||
"source": [
|
||||
"Install the `labml-nn` package"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"id": "ZCzmCrAIVg0L",
|
||||
"colab": {
|
||||
"base_uri": "https://localhost:8080/"
|
||||
},
|
||||
"outputId": "cf107fb2-4d50-4c67-af34-367624553421"
|
||||
},
|
||||
"source": [
|
||||
"!pip install labml-nn"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "SE2VUQ6L5zxI"
|
||||
},
|
||||
"source": [
|
||||
"Imports"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"id": "0hJXx_g0wS2C"
|
||||
},
|
||||
"source": [
|
||||
"import torch\n",
|
||||
"import torch.nn as nn\n",
|
||||
"\n",
|
||||
"from labml import experiment\n",
|
||||
"from labml.configs import option\n",
|
||||
"from labml_nn.transformers.compressive.experiment import Configs"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "Lpggo0wM6qb-"
|
||||
},
|
||||
"source": [
|
||||
"Create an experiment"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"id": "bFcr9k-l4cAg"
|
||||
},
|
||||
"source": [
|
||||
"experiment.create(name=\"compressive_transformer\")"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "-OnHLi626tJt"
|
||||
},
|
||||
"source": [
|
||||
"Initialize configurations"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"id": "Piz0c5f44hRo"
|
||||
},
|
||||
"source": [
|
||||
"conf = Configs()"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "wwMzCqpD6vkL"
|
||||
},
|
||||
"source": [
|
||||
"Set experiment configurations and assign a configurations dictionary to override configurations"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"base_uri": "https://localhost:8080/",
|
||||
"height": 17
|
||||
},
|
||||
"id": "e6hmQhTw4nks",
|
||||
"outputId": "29634715-42f4-4405-fb11-fc9522608627"
|
||||
},
|
||||
"source": [
|
||||
"experiment.configs(conf,\n",
|
||||
" # A dictionary of configurations to override\n",
|
||||
" {'tokenizer': 'character',\n",
|
||||
" 'text': 'tiny_shakespeare',\n",
|
||||
" 'optimizer.learning_rate': 2.5e-4,\n",
|
||||
" 'optimizer.optimizer': 'AdamW',\n",
|
||||
" 'prompt': 'It is',\n",
|
||||
" 'prompt_separator': '',\n",
|
||||
"\n",
|
||||
" 'train_loader': 'sequential_train_loader',\n",
|
||||
" 'valid_loader': 'sequential_valid_loader',\n",
|
||||
"\n",
|
||||
" 'seq_len': 8,\n",
|
||||
" 'mem_len': 8,\n",
|
||||
" 'epochs': 128,\n",
|
||||
" 'batch_size': 32,\n",
|
||||
" 'inner_iterations': 25,\n",
|
||||
" 'compression_rate': 2,\n",
|
||||
" })"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "EvI7MtgJ61w5"
|
||||
},
|
||||
"source": [
|
||||
"Set PyTorch models for loading and saving"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"base_uri": "https://localhost:8080/",
|
||||
"height": 255
|
||||
},
|
||||
"id": "GDlt7dp-5ALt",
|
||||
"outputId": "e7548e8f-c541-4618-dc5a-1597cae42003"
|
||||
},
|
||||
"source": [
|
||||
"experiment.add_pytorch_models({'model': conf.model})"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "KJZRf8527GxL"
|
||||
},
|
||||
"source": [
|
||||
"Start the experiment and run the training loop."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"base_uri": "https://localhost:8080/",
|
||||
"height": 1000
|
||||
},
|
||||
"id": "aIAWo7Fw5DR8",
|
||||
"outputId": "db979785-bfe3-4eda-d3eb-8ccbe61053e5"
|
||||
},
|
||||
"source": [
|
||||
"# Start the experiment\n",
|
||||
"with experiment.start():\n",
|
||||
" conf.run()"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"id": "oBXXlP2b7XZO"
|
||||
},
|
||||
"source": [
|
||||
""
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,346 @@
|
||||
"""
|
||||
---
|
||||
title: Compressive Transformer Experiment
|
||||
summary: This experiment trains a compressive transformer model on tiny Shakespeare dataset.
|
||||
---
|
||||
|
||||
# Compressive Transformer Experiment
|
||||
|
||||
This is an annotated PyTorch experiment to train a compressive transformer model.
|
||||
"""
|
||||
from typing import List, Tuple, NamedTuple
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from labml import experiment, tracker, monit, logger
|
||||
from labml.configs import option
|
||||
from labml.logger import Text
|
||||
from labml_nn.experiments.nlp_autoregression import NLPAutoRegressionConfigs
|
||||
from labml_nn.helpers.metrics import SimpleStateModule
|
||||
from labml_nn.helpers.trainer import BatchIndex
|
||||
from labml_nn.transformers.compressive import CompressiveTransformer, AttentionReconstructionLoss, \
|
||||
CompressiveTransformerLayer, Conv1dCompression
|
||||
|
||||
|
||||
class CompressedMemory(NamedTuple):
|
||||
mem: List[torch.Tensor]
|
||||
c_mem: List[torch.Tensor]
|
||||
|
||||
|
||||
class AutoregressiveModel(nn.Module):
|
||||
"""
|
||||
## Auto regressive model
|
||||
"""
|
||||
|
||||
def __init__(self, n_vocab: int, d_model: int, transformer: CompressiveTransformer):
|
||||
super().__init__()
|
||||
# Token embedding module
|
||||
self.src_embed = nn.Embedding(n_vocab, d_model)
|
||||
# Transformer
|
||||
self.transformer = transformer
|
||||
# Final layer
|
||||
self.generator = nn.Linear(d_model, n_vocab)
|
||||
# Masks
|
||||
self.mask_x = None
|
||||
self.mask_mem = None
|
||||
|
||||
def forward(self, x: torch.Tensor, mem: CompressedMemory):
|
||||
# Get memory and compressed memory
|
||||
if mem is not None:
|
||||
mem, c_mem = mem.mem, mem.c_mem
|
||||
else:
|
||||
mem = []
|
||||
c_mem = []
|
||||
|
||||
# Total length of the memory and compressed memory (for masks)
|
||||
m_len = len(mem[0]) if mem else 0
|
||||
if c_mem:
|
||||
m_len += len(c_mem[0])
|
||||
|
||||
# Create a subsequent mask for tokens
|
||||
if self.mask_x is None or self.mask_x.shape[0] < len(x):
|
||||
from labml_nn.transformers.utils import subsequent_mask
|
||||
self.mask_x = subsequent_mask(len(x)).to(x.device)
|
||||
# Create an all ones (full visibility) mask for memory
|
||||
if self.mask_mem is None or self.mask_mem.shape[1] < m_len or self.mask_mem.shape[0] < len(x):
|
||||
self.mask_mem = self.mask_x.new_ones(len(x), m_len, 1)
|
||||
|
||||
# Concatenate the masks if there is memory
|
||||
if m_len:
|
||||
mask = torch.cat((self.mask_mem[:len(x), :m_len], self.mask_x[:len(x), :len(x)]), dim=1)
|
||||
# Use only the subsequent mask otherwise
|
||||
else:
|
||||
mask = self.mask_x[:len(x), :len(x)]
|
||||
|
||||
# Token embeddings
|
||||
x = self.src_embed(x)
|
||||
# Run it through the transformer
|
||||
res, mem = self.transformer(x, mem, c_mem, mask)
|
||||
# Generate logits of the next token
|
||||
res = self.generator(res)
|
||||
#
|
||||
return res, mem
|
||||
|
||||
|
||||
class Configs(NLPAutoRegressionConfigs):
|
||||
"""
|
||||
## Configurations
|
||||
|
||||
The default configurations can and will be overridden when we start the experiment.
|
||||
"""
|
||||
|
||||
model: AutoregressiveModel
|
||||
|
||||
# Token embedding size
|
||||
d_model: int = 128
|
||||
# Number of attention heads
|
||||
heads: int = 4
|
||||
# Dropout probability
|
||||
dropout: float = 0.0
|
||||
# Number of features in FFN hidden layer
|
||||
d_ff: int = 256
|
||||
# Number of transformer layers
|
||||
n_layers: int = 6
|
||||
# Number of memories to keep
|
||||
mem_len: int = 8
|
||||
# State module to maintain memories when switching between training and validation
|
||||
memory = SimpleStateModule()
|
||||
# Attention Reconstruction Loss
|
||||
attention_reconstruction_loss: AttentionReconstructionLoss
|
||||
# Compression rate
|
||||
compression_rate: int = 4
|
||||
# Compressed memory length
|
||||
c_mem_len: int = 128
|
||||
|
||||
def init(self):
|
||||
# Set tracker configurations
|
||||
tracker.set_scalar("accuracy.*", True)
|
||||
tracker.set_scalar("loss.*", True)
|
||||
# Do not print the attention reconstruction loss in the terminal
|
||||
tracker.set_scalar("ar_loss.*", False)
|
||||
# This will keep the accuracy metric stats and memories separate for training and validation.
|
||||
self.state_modules = [self.accuracy, self.memory]
|
||||
|
||||
@torch.no_grad()
|
||||
def merge_compress_memory(self, mem: CompressedMemory, new_mem: List[torch.Tensor]) \
|
||||
-> Tuple[CompressedMemory, List[torch.Tensor]]:
|
||||
"""
|
||||
Concatenate new memories and compress the oldest memories.
|
||||
"""
|
||||
|
||||
# If the configurations specify not to use memory
|
||||
if self.mem_len == 0 and self.c_mem_len == 0:
|
||||
return CompressedMemory([], []), []
|
||||
|
||||
# Get memory and compressed memory
|
||||
if mem is not None:
|
||||
mem, c_mem = mem.mem, mem.c_mem
|
||||
else:
|
||||
mem, c_mem = [], []
|
||||
|
||||
# Concatenate new memories with old memory
|
||||
if mem:
|
||||
mem = [torch.cat((m, x), dim=0) for m, x in zip(mem, new_mem)]
|
||||
else:
|
||||
mem = new_mem
|
||||
|
||||
# Compress the oldest memories if there are more memories than `mem_len`
|
||||
if len(mem[0]) > self.mem_len:
|
||||
# Calculate the number of compressed memories to make $n_{cm} = \bigg\lceil\frac{n'_m - N_m}{c}\bigg\rceil$,
|
||||
# where $n'_m$ is the number of memories we have
|
||||
# and $N_m$ is the maximum number of memories we maintain (`mem_len`).
|
||||
n_c_mem = (len(mem[0]) - self.mem_len + self.compression_rate - 1) // self.compression_rate
|
||||
# Number of memories to compress $c n_{cm}$
|
||||
n_old = n_c_mem * self.compression_rate
|
||||
# A list to keep memories that need to be compressed for each layer.
|
||||
mem_to_compress = []
|
||||
# A list to keep the memories that do not get compressed for each layer.
|
||||
uncompressed_mem = []
|
||||
# Iterate through memories of each layer.
|
||||
for m in mem:
|
||||
# Split the memories at $c n_{cm}$
|
||||
cm, m = torch.split(m, [n_old, len(m) - n_old])
|
||||
# Collect memories to compress
|
||||
mem_to_compress.append(cm)
|
||||
# Collect remaining memories
|
||||
uncompressed_mem.append(m)
|
||||
# Update the memories
|
||||
mem = uncompressed_mem
|
||||
|
||||
# Compress the memories
|
||||
new_c_mem = []
|
||||
for i, layer in enumerate(self.model.transformer.layers):
|
||||
new_c_mem.append(layer.compress(mem_to_compress[i]))
|
||||
|
||||
# Concatenate newly compressed memories with old compressed memories
|
||||
if c_mem:
|
||||
c_mem = [torch.cat((m, nm), dim=0) for m, nm in zip(c_mem, new_c_mem)]
|
||||
# If there are no old compressed memories
|
||||
else:
|
||||
c_mem = new_c_mem
|
||||
|
||||
# Truncate old memories
|
||||
if len(c_mem[0]) > self.c_mem_len:
|
||||
c_mem = [m[-self.c_mem_len:] for m in c_mem]
|
||||
# No memories are compressed if the number of memories is less than `mem_len`
|
||||
else:
|
||||
mem_to_compress = []
|
||||
|
||||
# Return memories and the memories that were compressed.
|
||||
# Memories that were compressed are needed for the reconstruction loss computation.
|
||||
return CompressedMemory(mem, c_mem), mem_to_compress
|
||||
|
||||
def step(self, batch: any, batch_idx: BatchIndex):
|
||||
"""
|
||||
### Training/validation step
|
||||
"""
|
||||
|
||||
# Move data to the device
|
||||
data, target = batch[0].to(self.device), batch[1].to(self.device)
|
||||
|
||||
# Update global step (number of tokens processed) when in training mode
|
||||
if self.mode.is_train:
|
||||
tracker.add_global_step(data.shape[0] * data.shape[1])
|
||||
|
||||
# Get memories
|
||||
mem = self.memory.get()
|
||||
# Run the model
|
||||
output, new_mem = self.model(data, mem)
|
||||
# Merge and compress memory
|
||||
mem, mem_to_compress = self.merge_compress_memory(mem, new_mem)
|
||||
# Update memories
|
||||
self.memory.set(mem)
|
||||
|
||||
# Calculate and log cross entropy loss
|
||||
loss = self.loss_func(output, target)
|
||||
tracker.add("loss.", loss)
|
||||
|
||||
# Calculate attention reconstruction loss if memories were compressed in this step
|
||||
if mem_to_compress:
|
||||
# Get attention reconstruction loss
|
||||
ar_loss = self.attention_reconstruction_loss(new_mem, mem_to_compress)
|
||||
# Track attention reconstruction loss
|
||||
tracker.add("ar_loss.", ar_loss)
|
||||
# Add attention reconstruction loss to loss
|
||||
loss = loss + ar_loss
|
||||
|
||||
# Calculate and log accuracy
|
||||
self.accuracy(output, target)
|
||||
self.accuracy.track()
|
||||
|
||||
# Train the model
|
||||
if self.mode.is_train:
|
||||
# Calculate gradients
|
||||
loss.backward()
|
||||
# Clip gradients
|
||||
torch.nn.utils.clip_grad_norm_(self.model.parameters(), max_norm=self.grad_norm_clip)
|
||||
# Take optimizer step
|
||||
self.optimizer.step()
|
||||
# Log the model parameters and gradients on last batch of every epoch
|
||||
if batch_idx.is_last:
|
||||
tracker.add('model', self.model)
|
||||
# Clear the gradients
|
||||
self.optimizer.zero_grad()
|
||||
|
||||
# Save the tracked metrics
|
||||
tracker.save()
|
||||
|
||||
def sample(self):
|
||||
"""
|
||||
### Sampling function to generate samples periodically while training
|
||||
"""
|
||||
|
||||
# Starting prompt
|
||||
prompt = self.prompt
|
||||
# Collect output for printing
|
||||
log = [(prompt, Text.subtle)]
|
||||
# memory
|
||||
mem = CompressedMemory([], [])
|
||||
# Sample 25 tokens
|
||||
for i in monit.iterate('Sample', 25):
|
||||
# Tokenize the prompt
|
||||
data = self.text.text_to_i(prompt).unsqueeze(-1)
|
||||
# Move to device
|
||||
data = data.to(self.device)
|
||||
# Get the model output
|
||||
output, new_mem = self.model(data, mem)
|
||||
# Get the model prediction (greedy)
|
||||
output = output.argmax(dim=-1).squeeze(1)
|
||||
# Add the prediction to prompt
|
||||
prompt += self.prompt_separator + self.text.itos[output[-1]]
|
||||
# Only feed the last character to model in next iteration, rest will go in as memories
|
||||
prompt = prompt[-1:]
|
||||
# Add the prediction for logging
|
||||
log += [(self.prompt_separator + self.text.itos[output[-1]], Text.value)]
|
||||
# Update and compress memory
|
||||
mem, _ = self.merge_compress_memory(mem, new_mem)
|
||||
|
||||
# Print the sampled output
|
||||
logger.log(log)
|
||||
|
||||
|
||||
@option(Configs.model)
|
||||
def autoregressive_model(c: Configs):
|
||||
"""
|
||||
### Initialize the auto-regressive model
|
||||
"""
|
||||
from labml_nn.transformers.xl import RelativeMultiHeadAttention
|
||||
from labml_nn.transformers.feed_forward import FeedForward
|
||||
m = AutoregressiveModel(c.n_tokens, c.d_model, CompressiveTransformer(
|
||||
CompressiveTransformerLayer(d_model=c.d_model,
|
||||
self_attn=RelativeMultiHeadAttention(c.heads, c.d_model, c.dropout),
|
||||
feed_forward=FeedForward(c.d_model, c.d_ff, c.dropout),
|
||||
dropout_prob=c.dropout,
|
||||
compress=Conv1dCompression(c.compression_rate, c.d_model)), c.n_layers))
|
||||
return m.to(c.device)
|
||||
|
||||
|
||||
@option(Configs.attention_reconstruction_loss)
|
||||
def attention_reconstruction_loss(c: Configs):
|
||||
"""
|
||||
### Initialize the attention reconstruction loss
|
||||
"""
|
||||
return AttentionReconstructionLoss(c.model.transformer.layers)
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
### Run the experiment
|
||||
"""
|
||||
# Create experiment
|
||||
experiment.create(name="compressive_transformer", comment='')
|
||||
# Create configs
|
||||
conf = Configs()
|
||||
# Load configurations
|
||||
experiment.configs(conf,
|
||||
# A dictionary of configurations to override
|
||||
{'tokenizer': 'character',
|
||||
'text': 'tiny_shakespeare',
|
||||
'optimizer.learning_rate': 2.5e-4,
|
||||
'optimizer.optimizer': 'AdamW',
|
||||
'prompt': 'It is',
|
||||
'prompt_separator': '',
|
||||
|
||||
'train_loader': 'sequential_train_loader',
|
||||
'valid_loader': 'sequential_valid_loader',
|
||||
|
||||
'seq_len': 8,
|
||||
'mem_len': 8,
|
||||
'epochs': 128,
|
||||
'batch_size': 32,
|
||||
'inner_iterations': 25,
|
||||
'compression_rate': 2,
|
||||
})
|
||||
|
||||
# Set models for saving and loading
|
||||
experiment.add_pytorch_models({'model': conf.model})
|
||||
|
||||
# Start the experiment
|
||||
with experiment.start():
|
||||
# `TrainValidConfigs.run`
|
||||
conf.run()
|
||||
|
||||
|
||||
#
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,42 @@
|
||||
# [Compressive Transformer](https://nn.labml.ai/transformers/compressive/index.html)
|
||||
|
||||
This is an implementation of
|
||||
[Compressive Transformers for Long-Range Sequence Modelling](https://arxiv.org/abs/1911.05507)
|
||||
in [PyTorch](https://pytorch.org).
|
||||
|
||||
This is an extension of [Transformer XL](https://nn.labml.ai/transformers/xl/index.html) where past memories
|
||||
are compressed to give a longer attention range.
|
||||
That is, the furthest $n_{cm} c$ memories are compressed into
|
||||
$n_{cm}$ memories, where $c$ is the compression rate.
|
||||
|
||||
## Compression operation
|
||||
|
||||
The compression operation is defined as
|
||||
$f_c: \mathbb{R}^{nc \times d} \rightarrow \mathbb{R}^{n \times d}$.
|
||||
The paper introduces multiple choices for $f_c$ and we have only implemented
|
||||
1D convolution which seems to give the best results.
|
||||
Each layer has a separate compression operation $f_c^{(i)}$ where
|
||||
$i$ is the layer number.
|
||||
|
||||
## Training compression operation
|
||||
|
||||
Since training compression with BPTT requires maintaining
|
||||
a very large computational graph (many time steps), the paper proposes
|
||||
an *auto-encoding loss* and an *attention reconstruction loss*.
|
||||
The auto-encoding loss decodes the original memories from the compressed memories
|
||||
and calculates the loss.
|
||||
Attention reconstruction loss computes the multi-headed attention results
|
||||
on the compressed memory and on uncompressed memory and gets a mean squared error
|
||||
between them.
|
||||
We have implemented the latter here since it gives better results.
|
||||
|
||||
This implementation uses pre-layer normalization
|
||||
while the paper uses post-layer normalization.
|
||||
Pre-layer norm does the layer norm before [FFN](../feedforward.html) and
|
||||
self-attention, and the pass-through in the residual connection is not normalized.
|
||||
This is supposed to be more stable in standard transformer setups.
|
||||
|
||||
Here are [the training code](https://nn.labml.ai/transformers/compressive/experiment.html) and a notebook for training a compressive transformer
|
||||
model on the Tiny Shakespeare dataset.
|
||||
|
||||
[](https://colab.research.google.com/github/labmlai/annotated_deep_learning_paper_implementations/blob/master/labml_nn/transformers/compressive/experiment.ipynb)
|
||||
@@ -0,0 +1,325 @@
|
||||
"""
|
||||
---
|
||||
title: Configurable Transformer Components
|
||||
summary: These are configurable components that can be re-used quite easily.
|
||||
---
|
||||
|
||||
# Configurable Transformer Components
|
||||
"""
|
||||
import copy
|
||||
|
||||
import torch.nn as nn
|
||||
|
||||
from labml.configs import BaseConfigs, option, calculate, aggregate
|
||||
from .feed_forward import FeedForward
|
||||
from .mha import MultiHeadAttention
|
||||
from .models import EmbeddingsWithPositionalEncoding, EmbeddingsWithLearnedPositionalEncoding, TransformerLayer, \
|
||||
Encoder, Decoder, Generator, EncoderDecoder
|
||||
|
||||
|
||||
class FeedForwardConfigs(BaseConfigs):
|
||||
"""
|
||||
<a id="FFN"></a>
|
||||
|
||||
## FFN Configurations
|
||||
|
||||
Creates a Position-wise FeedForward Network defined in
|
||||
[`feed_forward.py`](feed_forward.html).
|
||||
"""
|
||||
# Position-wise feedforward layer
|
||||
ffn: FeedForward
|
||||
# Number of features in the embedding
|
||||
d_model: int
|
||||
# Number of features in in the hidden layer
|
||||
d_ff: int = 2048
|
||||
# Dropout probability
|
||||
dropout: float = 0.1
|
||||
# Activation in position-wise feedforward layer
|
||||
activation: nn.Module = 'ReLU'
|
||||
# Whether the FFN layer should be gated
|
||||
is_gated: bool = False
|
||||
# Whether the first fully connected layer should have a learnable bias
|
||||
bias1: bool = True
|
||||
# Whether the second fully connected layer should have a learnable bias
|
||||
bias2: bool = True
|
||||
# Whether the fully connected layer for the gate should have a learnable bias
|
||||
bias_gate: bool = False
|
||||
# Predefined GLU variants
|
||||
glu_variant: str = 'none'
|
||||
|
||||
|
||||
@option(FeedForwardConfigs.activation, 'ReLU')
|
||||
def _ffn_activation_relu():
|
||||
"""
|
||||
### ReLU activation
|
||||
|
||||
$$\max(0, x)$$
|
||||
"""
|
||||
return nn.ReLU()
|
||||
|
||||
|
||||
@option(FeedForwardConfigs.activation, 'GELU')
|
||||
def _ffn_activation_gelu():
|
||||
"""
|
||||
### GELU activation
|
||||
|
||||
$$x \Phi(x)$$ where $\Phi(x) = P(X \le x), X \sim \mathcal{N}(0,1)$
|
||||
|
||||
It was introduced in paper [Gaussian Error Linear Units](https://arxiv.org/abs/1606.08415).
|
||||
"""
|
||||
return nn.GELU()
|
||||
|
||||
|
||||
@option(FeedForwardConfigs.ffn, 'default')
|
||||
def _feed_forward(c: FeedForwardConfigs):
|
||||
"""
|
||||
Initialize a [feed forward network](feed_forward.html)
|
||||
"""
|
||||
return FeedForward(c.d_model, c.d_ff,
|
||||
dropout=c.dropout,
|
||||
activation=c.activation,
|
||||
is_gated=c.is_gated,
|
||||
bias1=c.bias1,
|
||||
bias2=c.bias2,
|
||||
bias_gate=c.bias_gate)
|
||||
|
||||
# ## GLU Variants
|
||||
# These are variants with gated hidden layers for the FFN
|
||||
# as introduced in paper [GLU Variants Improve Transformer](https://arxiv.org/abs/2002.05202).
|
||||
# We have omitted the bias terms as specified in the paper.
|
||||
|
||||
# ### FFN with Gated Linear Units
|
||||
#
|
||||
# $$FFN_{GLU}(x)(x, W_1, V, W_2) = (\sigma(x W_1) \otimes x V) W_2$$
|
||||
aggregate(FeedForwardConfigs.glu_variant, 'GLU',
|
||||
(FeedForwardConfigs.is_gated, True),
|
||||
(FeedForwardConfigs.bias1, False),
|
||||
(FeedForwardConfigs.bias2, False),
|
||||
(FeedForwardConfigs.bias_gate, False),
|
||||
(FeedForwardConfigs.activation, nn.Sigmoid()))
|
||||
|
||||
# ### FFN with Bilinear hidden layer
|
||||
#
|
||||
# $$FFN_{Bilinear}(x)(x, W_1, V, W_2) = (x W_1 \otimes x V) W_2$$
|
||||
aggregate(FeedForwardConfigs.glu_variant, 'Bilinear',
|
||||
(FeedForwardConfigs.is_gated, True),
|
||||
(FeedForwardConfigs.bias1, False),
|
||||
(FeedForwardConfigs.bias2, False),
|
||||
(FeedForwardConfigs.bias_gate, False),
|
||||
(FeedForwardConfigs.activation, nn.Identity()))
|
||||
|
||||
# ### FFN with ReLU gate
|
||||
#
|
||||
# $$FFN_{ReGLU}(x)(x, W_1, V, W_2) = (\max(0, x W_1) \otimes x V) W_2$$
|
||||
aggregate(FeedForwardConfigs.glu_variant, 'ReGLU',
|
||||
(FeedForwardConfigs.is_gated, True),
|
||||
(FeedForwardConfigs.bias1, False),
|
||||
(FeedForwardConfigs.bias2, False),
|
||||
(FeedForwardConfigs.bias_gate, False),
|
||||
(FeedForwardConfigs.activation, nn.ReLU()))
|
||||
|
||||
# ### FFN with GELU gate
|
||||
#
|
||||
# $$FFN_{GEGLU}(x)(x, W_1, V, W_2) = (\text{GELU}(x W_1) \otimes x V) W_2$$
|
||||
aggregate(FeedForwardConfigs.glu_variant, 'GEGLU',
|
||||
(FeedForwardConfigs.is_gated, True),
|
||||
(FeedForwardConfigs.bias1, False),
|
||||
(FeedForwardConfigs.bias2, False),
|
||||
(FeedForwardConfigs.bias_gate, False),
|
||||
(FeedForwardConfigs.activation, nn.GELU()))
|
||||
|
||||
# ### FFN with Swish gate
|
||||
#
|
||||
# $$FFN_{SwiGLU}(x)(x, W_1, V, W_2) = (\text{Swish}_1(x W_1) \otimes x V) W_2$$
|
||||
# where $\text{Swish}_\beta(x) = x \sigma(\beta x)$
|
||||
aggregate(FeedForwardConfigs.glu_variant, 'SwiGLU',
|
||||
(FeedForwardConfigs.is_gated, True),
|
||||
(FeedForwardConfigs.bias1, False),
|
||||
(FeedForwardConfigs.bias2, False),
|
||||
(FeedForwardConfigs.bias_gate, False),
|
||||
(FeedForwardConfigs.activation, nn.SiLU()))
|
||||
|
||||
|
||||
class TransformerConfigs(BaseConfigs):
|
||||
"""
|
||||
<a id="TransformerConfigs"></a>
|
||||
|
||||
## Transformer Configurations
|
||||
|
||||
This defines configurations for a transformer.
|
||||
The configurations are calculate using option functions.
|
||||
These are lazy loaded and therefore only the necessary modules
|
||||
are calculated.
|
||||
"""
|
||||
# Number of attention heads
|
||||
n_heads: int = 8
|
||||
# Transformer embedding size
|
||||
d_model: int = 512
|
||||
# Number of layers
|
||||
n_layers: int = 6
|
||||
# Dropout probability
|
||||
dropout: float = 0.1
|
||||
# Number of tokens in the source vocabulary (for token embeddings)
|
||||
n_src_vocab: int
|
||||
# Number of tokens in the target vocabulary (to generate logits for prediction)
|
||||
n_tgt_vocab: int
|
||||
|
||||
# The encoder self attention
|
||||
encoder_attn: MultiHeadAttention = 'mha'
|
||||
# The decoder self attention
|
||||
decoder_attn: MultiHeadAttention = 'mha'
|
||||
# The decoder memory attention
|
||||
decoder_mem_attn: MultiHeadAttention = 'mha'
|
||||
|
||||
# Configurable Feedforward Layer
|
||||
ffn: FeedForwardConfigs
|
||||
|
||||
# Encoder layer
|
||||
encoder_layer: TransformerLayer = 'default'
|
||||
# Decoder layer
|
||||
decoder_layer: TransformerLayer = 'default'
|
||||
|
||||
# Encoder consisting of multiple encoder layers
|
||||
encoder: Encoder = 'default'
|
||||
# Encoder consisting of multiple decoder layers
|
||||
decoder: Decoder = 'default'
|
||||
|
||||
# Embedding layer for source
|
||||
src_embed: nn.Module = 'fixed_pos'
|
||||
# Embedding layer for target (for decoder)
|
||||
tgt_embed: nn.Module = 'fixed_pos'
|
||||
|
||||
# Logit generator for prediction
|
||||
generator: Generator = 'default'
|
||||
|
||||
# Encoder-decoder
|
||||
encoder_decoder: EncoderDecoder
|
||||
|
||||
|
||||
# ### Multi-head Attention
|
||||
def _mha(c: TransformerConfigs):
|
||||
return MultiHeadAttention(c.n_heads, c.d_model, dropout_prob=c.dropout)
|
||||
|
||||
|
||||
calculate(TransformerConfigs.encoder_attn, 'mha', _mha)
|
||||
calculate(TransformerConfigs.decoder_attn, 'mha', _mha)
|
||||
calculate(TransformerConfigs.decoder_mem_attn, 'mha', _mha)
|
||||
|
||||
|
||||
# ### Relative Multi-head Attention
|
||||
def _relative_mha(c: TransformerConfigs):
|
||||
from labml_nn.transformers.xl.relative_mha import RelativeMultiHeadAttention
|
||||
return RelativeMultiHeadAttention(c.n_heads, c.d_model)
|
||||
|
||||
|
||||
calculate(TransformerConfigs.encoder_attn, 'relative', _relative_mha)
|
||||
calculate(TransformerConfigs.decoder_attn, 'relative', _relative_mha)
|
||||
calculate(TransformerConfigs.decoder_mem_attn, 'relative', _relative_mha)
|
||||
|
||||
|
||||
@option(TransformerConfigs.ffn, 'default')
|
||||
def _feed_forward(c: TransformerConfigs):
|
||||
"""
|
||||
Create feedforward layer configurations
|
||||
"""
|
||||
conf = FeedForwardConfigs()
|
||||
conf.set_default(FeedForwardConfigs.d_model, func=lambda: c.d_model)
|
||||
conf.set_default(FeedForwardConfigs.dropout, func=lambda: c.dropout)
|
||||
return conf
|
||||
|
||||
|
||||
@option(TransformerConfigs.encoder_layer, 'default')
|
||||
def _encoder_layer(c: TransformerConfigs):
|
||||
"""
|
||||
Encoder layer
|
||||
"""
|
||||
return TransformerLayer(d_model=c.d_model, self_attn=c.encoder_attn,
|
||||
src_attn=None, feed_forward=copy.deepcopy(c.ffn.ffn),
|
||||
dropout_prob=c.dropout)
|
||||
|
||||
|
||||
@option(TransformerConfigs.decoder_layer, 'default')
|
||||
def _decoder_layer(c: TransformerConfigs):
|
||||
"""
|
||||
Decoder layer
|
||||
"""
|
||||
return TransformerLayer(d_model=c.d_model, self_attn=c.decoder_attn,
|
||||
src_attn=c.decoder_mem_attn, feed_forward=copy.deepcopy(c.ffn.ffn),
|
||||
dropout_prob=c.dropout)
|
||||
|
||||
|
||||
@option(TransformerConfigs.encoder, 'default')
|
||||
def _encoder(c: TransformerConfigs):
|
||||
"""
|
||||
Encoder
|
||||
"""
|
||||
return Encoder(c.encoder_layer, c.n_layers)
|
||||
|
||||
|
||||
@option(TransformerConfigs.decoder, 'default')
|
||||
def _decoder(c: TransformerConfigs):
|
||||
"""
|
||||
Decoder
|
||||
"""
|
||||
return Decoder(c.decoder_layer, c.n_layers)
|
||||
|
||||
|
||||
@option(TransformerConfigs.generator, 'default')
|
||||
def _generator(c: TransformerConfigs):
|
||||
"""
|
||||
Logit generator
|
||||
"""
|
||||
return Generator(c.n_tgt_vocab, c.d_model)
|
||||
|
||||
|
||||
# ### Fixed Positional Embeddings
|
||||
@option(TransformerConfigs.src_embed, 'fixed_pos')
|
||||
def _src_embed_with_positional(c: TransformerConfigs):
|
||||
"""
|
||||
Source embedding with fixed positional encodings
|
||||
"""
|
||||
return EmbeddingsWithPositionalEncoding(c.d_model, c.n_src_vocab)
|
||||
|
||||
|
||||
@option(TransformerConfigs.tgt_embed, 'fixed_pos')
|
||||
def _tgt_embed_with_positional(c: TransformerConfigs):
|
||||
"""
|
||||
Target embedding with fixed positional encodings
|
||||
"""
|
||||
return EmbeddingsWithPositionalEncoding(c.d_model, c.n_tgt_vocab)
|
||||
|
||||
|
||||
# ### Learned Positional Embeddings
|
||||
@option(TransformerConfigs.src_embed, 'learned_pos')
|
||||
def _src_embed_with_learned_positional(c: TransformerConfigs):
|
||||
"""
|
||||
Source embedding with learned positional encodings
|
||||
"""
|
||||
return EmbeddingsWithLearnedPositionalEncoding(c.d_model, c.n_src_vocab)
|
||||
|
||||
|
||||
@option(TransformerConfigs.tgt_embed, 'learned_pos')
|
||||
def _tgt_embed_with_learned_positional(c: TransformerConfigs):
|
||||
"""
|
||||
Target embedding with learned positional encodings
|
||||
"""
|
||||
return EmbeddingsWithLearnedPositionalEncoding(c.d_model, c.n_tgt_vocab)
|
||||
|
||||
|
||||
# ### No Positional Embeddings
|
||||
@option(TransformerConfigs.src_embed, 'no_pos')
|
||||
def _src_embed_without_positional(c: TransformerConfigs):
|
||||
"""
|
||||
Source embedding without positional encodings
|
||||
"""
|
||||
return nn.Embedding(c.n_src_vocab, c.d_model)
|
||||
|
||||
|
||||
@option(TransformerConfigs.tgt_embed, 'no_pos')
|
||||
def _tgt_embed_without_positional(c: TransformerConfigs):
|
||||
return nn.Embedding(c.n_tgt_vocab, c.d_model)
|
||||
|
||||
|
||||
@option(TransformerConfigs.encoder_decoder, 'default')
|
||||
def _encoder_decoder(c: TransformerConfigs):
|
||||
return EncoderDecoder(c.encoder, c.decoder, c.src_embed, c.tgt_embed, c.generator)
|
||||
@@ -0,0 +1,327 @@
|
||||
"""
|
||||
---
|
||||
title: Linear Transformers Are Secretly Fast Weight Memory Systems
|
||||
summary: >
|
||||
This is an annotated implementation/tutorial of
|
||||
Linear Transformers Are Secretly Fast Weight Memory Systems in PyTorch.
|
||||
---
|
||||
|
||||
# Fast weights transformer
|
||||
|
||||
The paper
|
||||
[Linear Transformers Are Secretly Fast Weight Memory Systems in PyTorch](https://arxiv.org/abs/2102.11174)
|
||||
finds similarities between linear self-attention and fast weight systems
|
||||
and makes modifications to self-attention update rule based on that.
|
||||
It also introduces a simpler, yet effective kernel function.
|
||||
|
||||
*The authors have provided an [official implementation](https://github.com/ischlag/fast-weight-transformers)
|
||||
of the paper including other variants they compare with in the paper.*
|
||||
|
||||
## Fast weights
|
||||
|
||||
Consider a sequence of inputs $\big\{x^{(i)}\big\}^L_{i=1}$ or length $L$
|
||||
and each step is a vector of size $d_{in}$; i.e. $x \in \mathbb{R}^{d_{in}}$.
|
||||
The fast weight model generates a weight matrix at each step to produce output
|
||||
$\big\{y^{(i)}\big\}^L_{i=1}$, $y \in \mathbb{R}^{d_{out}}$
|
||||
|
||||
\begin{align}
|
||||
a^{(i)}, b^{(i)} &= \textcolor{orange}{W_a} x^{(i)}, \textcolor{orange}{W_b} x^{(i)} \\
|
||||
\textcolor{cyan}{W^{(i)}} &= \sigma \Big( \textcolor{cyan}{W^{(i-1)}} + a^{(i)} \otimes b^{(i)} \Big) \\
|
||||
y^{(i)} &= \textcolor{cyan}{W^{(i)}} x^{(i)}
|
||||
\end{align}
|
||||
|
||||
$\otimes$ is the outer product ($a \otimes b = a b^\top$), where elements of the two vectors are multiplied with each other
|
||||
to give a matrix.
|
||||
$\sigma$ is an activation function.
|
||||
$\textcolor{orange}{W_a}$ and $\textcolor{orange}{W_b}$ are trainable weights (parameters).
|
||||
$\textcolor{cyan}{W^{(i)}}$ are the fast weights that are generated at each step.
|
||||
|
||||
## Linear self-attention
|
||||
|
||||
Original transformer self-attention is, (omitting $\frac{1}{d_k}$ for clarity)
|
||||
|
||||
\begin{align}
|
||||
y^{(i)} &= \Big[v^{(1)}, v^{(2)}, ..., v^{(i)}\Big] \text{softmax}
|
||||
\bigg(
|
||||
\Big[k^{(1)}, k^{(2)}, ..., k^{(i)}\Big] ^\top
|
||||
q^{(i)}
|
||||
\bigg) \\
|
||||
&= \sum^i_{j=1} \frac
|
||||
{ v^{(j)} \kappa(k^{(j)}, q^{(i)}) }
|
||||
{ \sum^i_{j'=1} \kappa(k^{(j')}, q^{(i)}) } \\
|
||||
\end{align}
|
||||
|
||||
where $\kappa(k, q) = \text{exp}(k \cdot q)$
|
||||
|
||||
The idea behind linearizing self attention is to replace softmax
|
||||
kernel $\kappa$ with a different kernel $\kappa '$ so that we can calculate the
|
||||
denominator of the self attention function faster:
|
||||
|
||||
$$\kappa '(k, q) = \textcolor{lightgreen}{\phi(k)}^\top \textcolor{lightgreen}{\phi(q)}$$
|
||||
|
||||
This gives
|
||||
|
||||
\begin{align}
|
||||
y^{(i)} &= \frac
|
||||
{\Big( \sum^i_{j=1} v^{(j)} \otimes \textcolor{lightgreen}{\phi(k^{(j)})} \Big)
|
||||
\textcolor{lightgreen}{\phi(q^{(i)})} }
|
||||
{ \Big( \sum^i_{j'=1}
|
||||
\textcolor{lightgreen}{\phi(k^{(j')})} \Big)
|
||||
\textcolor{lightgreen}{\phi(q^{(i)})} }
|
||||
\end{align}
|
||||
|
||||
With $\textcolor{cyan}{W^{(i)}} = \sum^i_{j=1} v^{(j)} \otimes \phi(k^{(j)})$ and
|
||||
$z^{(i)} = \sum^i_{j=1} \textcolor{lightgreen}{\phi(k^{(j)})}$, we can calculate them efficiently:
|
||||
|
||||
\begin{align}
|
||||
\textcolor{cyan}{W^{(i)}} &= \textcolor{cyan}{W^{(i-1)}} + v^{(i)} \otimes \textcolor{lightgreen}{\phi(k^{(i)})} \\
|
||||
z^{(i)} &= z{(i)} + \textcolor{lightgreen}{\phi(k^{(i)})} \\
|
||||
y^{(i)} &= \frac{1}{z^{(i)} \cdot \textcolor{lightgreen}{\phi(q^{(i)})}}
|
||||
W^{(i)} \textcolor{lightgreen}{\phi(q^{(i)})}
|
||||
\end{align}
|
||||
|
||||
This is quite similar to fast weights.
|
||||
|
||||
The paper introduces a new linear attention projection function $\textcolor{lightgreen}{\phi}$
|
||||
a new update rule for $\textcolor{cyan}{W^{(i)}} = f(\textcolor{cyan}{W^{(i-1)}})$ and change the normalization
|
||||
$\frac{1}{z^{(i)} \cdot \textcolor{lightgreen}{\phi(q^{(i)})}}$
|
||||
|
||||
Here are [the training code](experiment.html) and a notebook for training a fast weights
|
||||
transformer on the Tiny Shakespeare dataset.
|
||||
|
||||
[](https://colab.research.google.com/github/labmlai/annotated_deep_learning_paper_implementations/blob/master/labml_nn/transformers/fast_weights/experiment.ipynb)
|
||||
"""
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
from labml_nn.transformers.feed_forward import FeedForward
|
||||
from labml_nn.transformers.mha import PrepareForMultiHeadAttention
|
||||
from labml_nn.utils import clone_module_list
|
||||
|
||||
|
||||
class DPFP(nn.Module):
|
||||
"""
|
||||
## Deterministic Parameter Free Project (DPFP)
|
||||
|
||||
This is the new projection function $\textcolor{lightgreen}{\phi}$ introduced in the paper.
|
||||
DPFP projects $k$ of dimensionality $d_{key}$ to dimensionality $d_{dot} = 2 d_{key} \nu$,
|
||||
where $\nu \in \\{1, 2, ..., 2 d_{key} - 1 \\}$ is a hyper-parameter.
|
||||
|
||||
$$\textcolor{lightgreen}{\phi_{2 d_{key} (i - 1) + j}(k)}
|
||||
= \text{ReLU}\Big(\big[k, -k\big]\Big)_{j}
|
||||
\text{ReLU}\Big(\big[k, -k\big]\Big)_{i + j}$$
|
||||
|
||||
where $\big[k, -k\big]$ is the concatenation of $k$ and $-k$ to give a vector of
|
||||
size $2 d_{key}$, $i \in \\{1, 2, ..., \nu \\}$, and $j \in \\{1, 2, ..., 2 d_{key}\\}$.
|
||||
$x_i$ is the $i$-th element of vector $x$ and is rolled around if
|
||||
$i$ is larger than the number of elements in $x$.
|
||||
|
||||
Basically, it creates a new vector by multiplying elements of $[k, -k]$ shifted by $i$.
|
||||
|
||||
This produces projections that are sparse (only a few elements of $phi$ are non-zero) and
|
||||
orthogonal ($\textcolor{lightgreen}{\phi(k^{(i)})} \cdot \textcolor{lightgreen}{\phi(k^{(j)})}
|
||||
\approx 0$ for most $i, j$
|
||||
unless $k^{(i)}$ and $k^{(j)}$ are very similar.
|
||||
|
||||
### Normalization
|
||||
|
||||
Paper introduces a simple normalization for $\textcolor{lightgreen}{\phi}$,
|
||||
|
||||
$$\textcolor{lightgreen}{\phi '(k)} =
|
||||
\frac{\textcolor{lightgreen}{\phi(k)}}{\sum^{d_{dot}}_{j=1} \textcolor{lightgreen}{\phi(k)_j}}$$
|
||||
|
||||
*Check the paper for derivation.*
|
||||
"""
|
||||
|
||||
def __init__(self, nu: int = 1, eps: float = 1e-6):
|
||||
"""
|
||||
* `nu` is the hyper-parameter $\nu$.
|
||||
* `eps` is the small value used to make sure there is no division-by-zero when normalizing.
|
||||
"""
|
||||
super().__init__()
|
||||
self.nu = nu
|
||||
self.relu = nn.ReLU()
|
||||
self.eps = eps
|
||||
|
||||
def forward(self, k: torch.Tensor):
|
||||
# Get $\textcolor{lightgreen}{\phi(k)}$
|
||||
k = self.dpfp(k)
|
||||
# Normalize by $\sum^{d_{dot}}_{j=1} \textcolor{lightgreen}{\phi(k)_j}$
|
||||
return k / (torch.sum(k, dim=-1, keepdim=True) + self.eps)
|
||||
|
||||
def dpfp(self, k: torch.Tensor):
|
||||
"""
|
||||
$$\textcolor{lightgreen}{\phi(k)}$$
|
||||
"""
|
||||
# $x = \text{ReLU}\Big(\big[k, -k\big]\Big)$
|
||||
x = self.relu(torch.cat([k, -k], dim=-1))
|
||||
# Shift and roll by $i \in \\{1, 2, ..., \nu \\}$,
|
||||
# to get $$x'_{i,j} = \text{ReLU}\Big(\big[k, -k\big]\Big)_{i+j}$$
|
||||
x_rolled = [x.roll(shifts=i, dims=-1) for i in range(1, self.nu + 1)]
|
||||
# Concatenate to get
|
||||
# $$x'_{2 d_{key} (i - 1) + j} = \text{ReLU}\Big(\big[k, -k\big]\Big)_{i+j}$$
|
||||
x_rolled = torch.cat(x_rolled, dim=-1)
|
||||
# Concatenate copies of $x$
|
||||
x_repeat = torch.cat([x] * self.nu, dim=-1)
|
||||
|
||||
# Multiply them,
|
||||
# $$\textcolor{lightgreen}{\phi_{2 d_{key} (i - 1) + j}(k)}
|
||||
# = \text{ReLU}\Big(\big[k, -k\big]\Big)_{j}
|
||||
# \text{ReLU}\Big(\big[k, -k\big]\Big)_{i + j}$$
|
||||
return x_repeat * x_rolled
|
||||
|
||||
|
||||
class FastWeightsAttention(nn.Module):
|
||||
"""
|
||||
## Fast Weights Attention
|
||||
|
||||
The paper introduces a new update rule for calculating $\textcolor{cyan}{W^{(i)}}$.
|
||||
The model first retrieves the current value
|
||||
$\bar{v}^{(i)}$ paired with the key $k^{(i)}$.
|
||||
Then stores a combination $v^{(i)}_{new}$
|
||||
of the retrieved value $\bar{v}^{(i)}$ and the input $v^{(i)}$.
|
||||
|
||||
\begin{align}
|
||||
k^{(i)}, v^{(i)}, q^{(i)} &=
|
||||
\textcolor{orange}{W_k} x^{(i)}, \textcolor{orange}{W_v} x^{(i)}, \textcolor{orange}{W_q} x^{(i)} \\
|
||||
\bar{v}^{(i)} &= \textcolor{cyan}{W^{(i-1)}} \textcolor{lightgreen}{\phi'(k^{(i)})} \\
|
||||
\beta^{(i)} &= \sigma \Big(\textcolor{orange}{W_\beta} x^{(i)} \Big) \\
|
||||
v^{(i)}_{new} &= \beta^{(i)} v^{(i)} + \Big(1 - \beta^{(i)} \Big) \bar{v}^{(i)} \\
|
||||
\textcolor{cyan}{W^{(i)}}
|
||||
&= \textcolor{cyan}{W^{(i-1)}} + v^{(i)}_{new} \otimes \textcolor{lightgreen}{\phi'(k^{(i)})} \\
|
||||
&= \textcolor{cyan}{W^{(i-1)}} +
|
||||
\beta^{(i)} \Big( v^{(i)} - \bar{v}^{(i)} \Big ) \otimes \textcolor{lightgreen}{\phi'(k^{(i)})} \\
|
||||
y^{(i)} &= \textcolor{cyan}{W^{(i)}} \textcolor{lightgreen}{\phi'(q^{(i)})}
|
||||
\end{align}
|
||||
|
||||
where $\textcolor{orange}{W_\beta}$ is a trainable parameter and $\sigma$ is the sigmoid function.
|
||||
|
||||
Note that we don't need the normalization term $z$ because $\textcolor{lightgreen}{\phi'}$ is normalized.
|
||||
"""
|
||||
|
||||
def __init__(self, heads: int, d_model: int, dropout_prob: float, phi: DPFP):
|
||||
super().__init__()
|
||||
|
||||
# Number of features per head $d_k$
|
||||
self.d_k = d_model // heads
|
||||
# Number of heads
|
||||
self.heads = heads
|
||||
|
||||
# These transform the `query`, `key` and `value` multi-headed attention.
|
||||
self.query = PrepareForMultiHeadAttention(d_model, heads, self.d_k, bias=False)
|
||||
self.key = PrepareForMultiHeadAttention(d_model, heads, self.d_k, bias=False)
|
||||
self.value = PrepareForMultiHeadAttention(d_model, heads, self.d_k, bias=False)
|
||||
|
||||
# Interpolation weight function $\sigma \Big(\textcolor{orange}{W_\beta} x^{(i)} \Big)$ for each head
|
||||
self.interpolation_weight = nn.Sequential(
|
||||
PrepareForMultiHeadAttention(d_model, heads, 1, bias=False),
|
||||
nn.Sigmoid()
|
||||
)
|
||||
|
||||
# $\textcolor{lightgreen}{\phi'}$
|
||||
self.phi = phi
|
||||
|
||||
# Output layer
|
||||
self.output = nn.Linear(d_model, d_model)
|
||||
# Dropout
|
||||
self.dropout = nn.Dropout(dropout_prob)
|
||||
|
||||
def forward(self, x: torch.Tensor):
|
||||
# Get the number of steps $L$
|
||||
seq_len = x.shape[0]
|
||||
# $\textcolor{lightgreen}{\phi'(q^{(i)})}$ for all steps and heads
|
||||
query = self.phi(self.query(x))
|
||||
# $\textcolor{lightgreen}{\phi'(k^{(i)})}$ for all steps and heads
|
||||
key = self.phi(self.key(x))
|
||||
# $v^{(i)}$ for all steps and heads
|
||||
value = self.value(x)
|
||||
# $\beta^{(i)}$ for all steps and heads
|
||||
beta = self.interpolation_weight(x)
|
||||
|
||||
# $\textcolor{cyan}{W^{(0)}}$
|
||||
weights = key.new_zeros((key.shape[1], key.shape[2], value.shape[3], key.shape[3]))
|
||||
# List to store outputs $y^{(i)}$
|
||||
outputs = []
|
||||
|
||||
# Iterate through steps
|
||||
for i in range(seq_len):
|
||||
# $$\bar{v}^{(i)} = \textcolor{cyan}{W^{(i-1)}} \textcolor{lightgreen}{\phi'(k^{(i)})}$$
|
||||
value_existing = torch.einsum('bhvk,bhk->bhv', weights, key[i])
|
||||
|
||||
# $$\textcolor{cyan}{W^{(i)}}
|
||||
# = \textcolor{cyan}{W^{(i-1)}} +
|
||||
# \beta^{(i)} \Big( v^{(i)} - \bar{v}^{(i)} \Big ) \otimes \textcolor{lightgreen}{\phi'(k^{(i)})}$$
|
||||
weights = weights + torch.einsum('bhv,bhk->bhvk', beta[i] * (value[i] - value_existing), key[i])
|
||||
|
||||
# $$y^{(i)} = \textcolor{cyan}{W^{(i)}} \textcolor{lightgreen}{\phi'(q^{(i)})}$$
|
||||
y = torch.einsum('bhvk,bhk->bhv', weights, query[i])
|
||||
|
||||
# Merge multiple heads and append to `outputs`
|
||||
outputs.append(y.reshape(y.shape[0], -1))
|
||||
|
||||
# Stack outputs at each step into a single tensor
|
||||
x = torch.stack(outputs)
|
||||
|
||||
# Output layer
|
||||
return self.output(x)
|
||||
|
||||
|
||||
class FastWeightsAttentionTransformerLayer(nn.Module):
|
||||
"""
|
||||
This is a general transformer layer that combines self attention and feedforward network.
|
||||
"""
|
||||
def __init__(self, *,
|
||||
d_model: int,
|
||||
attn: FastWeightsAttention,
|
||||
feed_forward: FeedForward,
|
||||
dropout_prob: float):
|
||||
super().__init__()
|
||||
# Transformer size $d_{model}$
|
||||
self.size = d_model
|
||||
# Fast weights attention module
|
||||
self.attn = attn
|
||||
# Feed-forward network
|
||||
self.feed_forward = feed_forward
|
||||
# Dropout layer
|
||||
self.dropout = nn.Dropout(dropout_prob)
|
||||
|
||||
# Normalization layers
|
||||
self.norm_self_attn = nn.LayerNorm([d_model])
|
||||
self.norm_ff = nn.LayerNorm([d_model])
|
||||
|
||||
def forward(self, x: torch.Tensor):
|
||||
# Calculate fast weights self attention
|
||||
attn = self.attn(x)
|
||||
# Add the self attention results
|
||||
x = x + self.dropout(attn)
|
||||
|
||||
# Normalize for feed-forward
|
||||
z = self.norm_ff(x)
|
||||
# Pass through the feed-forward network
|
||||
ff = self.feed_forward(z)
|
||||
# Add the feed-forward results back
|
||||
x = x + self.dropout(ff)
|
||||
|
||||
#
|
||||
return x
|
||||
|
||||
|
||||
class FastWeightsAttentionTransformer(nn.Module):
|
||||
"""
|
||||
This is a general transformer module with multiple transformer layers
|
||||
"""
|
||||
def __init__(self, layer: FastWeightsAttentionTransformerLayer, n_layers: int):
|
||||
super().__init__()
|
||||
# Make copies of the transformer layer
|
||||
self.layers = clone_module_list(layer, n_layers)
|
||||
# Final normalization layer
|
||||
self.norm = nn.LayerNorm([layer.size])
|
||||
|
||||
def forward(self, x: torch.Tensor):
|
||||
for i, layer in enumerate(self.layers):
|
||||
# Get layer output
|
||||
x = layer(x)
|
||||
|
||||
# Normalize the output
|
||||
return self.norm(x)
|
||||
@@ -0,0 +1,219 @@
|
||||
{
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 0,
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"name": "Fast Weights Transformer",
|
||||
"provenance": [],
|
||||
"collapsed_sections": []
|
||||
},
|
||||
"kernelspec": {
|
||||
"name": "python3",
|
||||
"display_name": "Python 3"
|
||||
},
|
||||
"accelerator": "GPU"
|
||||
},
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "AYV_dMVDxyc2"
|
||||
},
|
||||
"source": [
|
||||
"[](https://github.com/labmlai/annotated_deep_learning_paper_implementations)\n",
|
||||
"[](https://colab.research.google.com/github/labmlai/annotated_deep_learning_paper_implementations/blob/master/labml_nn/transformers/fast_weights/experiment.ipynb) \n",
|
||||
"\n",
|
||||
"## Fast Weights Transformer\n",
|
||||
"\n",
|
||||
"This is an experiment training Shakespeare dataset with a Compressive Transformer model."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "AahG_i2y5tY9"
|
||||
},
|
||||
"source": [
|
||||
"Install the `labml-nn` package"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"id": "ZCzmCrAIVg0L",
|
||||
"colab": {
|
||||
"base_uri": "https://localhost:8080/"
|
||||
},
|
||||
"outputId": "b73a038d-62dc-48e0-c2f1-9454224e9137"
|
||||
},
|
||||
"source": [
|
||||
"!pip install labml-nn"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "SE2VUQ6L5zxI"
|
||||
},
|
||||
"source": [
|
||||
"Imports"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"id": "0hJXx_g0wS2C"
|
||||
},
|
||||
"source": [
|
||||
"from labml import experiment\n",
|
||||
"from labml_nn.transformers.fast_weights.experiment import Configs"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "Lpggo0wM6qb-"
|
||||
},
|
||||
"source": [
|
||||
"Create an experiment"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"id": "bFcr9k-l4cAg"
|
||||
},
|
||||
"source": [
|
||||
"experiment.create(name=\"fast_weights_transformer\")"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "-OnHLi626tJt"
|
||||
},
|
||||
"source": [
|
||||
"Initialize configurations"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"id": "Piz0c5f44hRo"
|
||||
},
|
||||
"source": [
|
||||
"conf = Configs()"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "wwMzCqpD6vkL"
|
||||
},
|
||||
"source": [
|
||||
"Set experiment configurations and assign a configurations dictionary to override configurations"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"base_uri": "https://localhost:8080/",
|
||||
"height": 17
|
||||
},
|
||||
"id": "e6hmQhTw4nks",
|
||||
"outputId": "6660f5c6-347c-4370-f19a-ff7bd9b96535"
|
||||
},
|
||||
"source": [
|
||||
"experiment.configs(conf,\n",
|
||||
" # A dictionary of configurations to override\n",
|
||||
" {'tokenizer': 'character',\n",
|
||||
" 'text': 'tiny_shakespeare',\n",
|
||||
" 'optimizer.learning_rate': 1.0,\n",
|
||||
" 'optimizer.optimizer': 'Noam',\n",
|
||||
" 'prompt': 'It is',\n",
|
||||
" 'prompt_separator': '',\n",
|
||||
"\n",
|
||||
" 'train_loader': 'shuffled_train_loader',\n",
|
||||
" 'valid_loader': 'shuffled_valid_loader',\n",
|
||||
"\n",
|
||||
" 'seq_len': 128,\n",
|
||||
" 'epochs': 128,\n",
|
||||
" 'batch_size': 16,\n",
|
||||
" 'inner_iterations': 25})"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "EvI7MtgJ61w5"
|
||||
},
|
||||
"source": [
|
||||
"Set PyTorch models for loading and saving"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"base_uri": "https://localhost:8080/",
|
||||
"height": 255
|
||||
},
|
||||
"id": "GDlt7dp-5ALt",
|
||||
"outputId": "5154be3b-dbfc-4002-82f6-1b6d58970e21"
|
||||
},
|
||||
"source": [
|
||||
"experiment.add_pytorch_models({'model': conf.model})"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "KJZRf8527GxL"
|
||||
},
|
||||
"source": [
|
||||
"Start the experiment and run the training loop."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"base_uri": "https://localhost:8080/",
|
||||
"height": 1000
|
||||
},
|
||||
"id": "aIAWo7Fw5DR8",
|
||||
"outputId": "583dc28a-338e-4089-a415-e21d8b65fa3e"
|
||||
},
|
||||
"source": [
|
||||
"with experiment.start():\n",
|
||||
" conf.run()"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"id": "oBXXlP2b7XZO"
|
||||
},
|
||||
"source": [
|
||||
""
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
"""
|
||||
---
|
||||
title: Train Fast Weights Transformer
|
||||
summary: This is training code with notes for a Fast Weights Transformer.
|
||||
---
|
||||
|
||||
# Train Fast Weights Transformer
|
||||
|
||||
This trains a fast weights transformer model for auto-regression.
|
||||
|
||||
Here’s a Colab notebook for training a fast weights transformer on Tiny Shakespeare dataset.
|
||||
|
||||
[](https://colab.research.google.com/github/labmlai/annotated_deep_learning_paper_implementations/blob/master/labml_nn/transformers/fast_weights/experiment.ipynb)
|
||||
"""
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
from labml import experiment
|
||||
from labml.configs import option
|
||||
from labml.utils.pytorch import get_modules
|
||||
from labml_nn.experiments.nlp_autoregression import NLPAutoRegressionConfigs
|
||||
|
||||
|
||||
class AutoregressiveModel(nn.Module):
|
||||
"""
|
||||
## Auto regressive model
|
||||
"""
|
||||
|
||||
def __init__(self, n_vocab: int, d_model: int, transformer: nn.Module):
|
||||
super().__init__()
|
||||
# Token embedding module
|
||||
self.src_embed = nn.Embedding(n_vocab, d_model)
|
||||
self.transformer = transformer
|
||||
self.generator = nn.Linear(d_model, n_vocab)
|
||||
|
||||
def forward(self, x: torch.Tensor):
|
||||
# Embed the tokens
|
||||
x = self.src_embed(x)
|
||||
# Run it through the the transformer
|
||||
res = self.transformer(x)
|
||||
# Generate logits of the next token
|
||||
return self.generator(res), None
|
||||
|
||||
|
||||
class Configs(NLPAutoRegressionConfigs):
|
||||
"""
|
||||
## Configurations
|
||||
|
||||
The default configs can and will be over-ridden when we start the experiment
|
||||
"""
|
||||
|
||||
model: AutoregressiveModel
|
||||
|
||||
d_model: int = 512
|
||||
nu: int = 1
|
||||
heads: int = 8
|
||||
dropout: float = 0.0
|
||||
d_ff: int = 2048
|
||||
n_layers: int = 6
|
||||
|
||||
|
||||
@option(Configs.model)
|
||||
def fast_weights_transformer(c: Configs):
|
||||
"""
|
||||
Create [fast weights transformer](index.html).
|
||||
"""
|
||||
from labml_nn.transformers.fast_weights import FastWeightsAttentionTransformer, \
|
||||
FastWeightsAttentionTransformerLayer, FastWeightsAttention, FeedForward
|
||||
|
||||
from labml_nn.transformers.fast_weights import DPFP
|
||||
return AutoregressiveModel(
|
||||
c.n_tokens, c.d_model,
|
||||
FastWeightsAttentionTransformer(
|
||||
FastWeightsAttentionTransformerLayer(d_model=c.d_model,
|
||||
attn=FastWeightsAttention(c.heads, c.d_model, c.dropout, DPFP(nu=c.nu)),
|
||||
feed_forward=FeedForward(c.d_model, c.d_ff, c.dropout),
|
||||
dropout_prob=c.dropout),
|
||||
c.n_layers)).to(c.device)
|
||||
|
||||
|
||||
def main():
|
||||
# Create experiment
|
||||
experiment.create(name="fast_weights_transformer")
|
||||
# Create configs
|
||||
conf = Configs()
|
||||
# Load configurations
|
||||
experiment.configs(conf,
|
||||
# A dictionary of configurations to override
|
||||
{'tokenizer': 'character',
|
||||
'text': 'tiny_shakespeare',
|
||||
'optimizer.learning_rate': 1.0,
|
||||
'optimizer.optimizer': 'Noam',
|
||||
'prompt': 'It is',
|
||||
'prompt_separator': '',
|
||||
|
||||
'train_loader': 'shuffled_train_loader',
|
||||
'valid_loader': 'shuffled_valid_loader',
|
||||
|
||||
'seq_len': 128,
|
||||
'epochs': 128,
|
||||
'batch_size': 16,
|
||||
'inner_iterations': 25})
|
||||
|
||||
# Set models for saving and loading
|
||||
experiment.add_pytorch_models(get_modules(conf))
|
||||
|
||||
# Start the experiment
|
||||
with experiment.start():
|
||||
# Run the training loop
|
||||
conf.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,10 @@
|
||||
# [Fast weights transformer](https://nn.labml.ai/transformers/fast_weights/index.html)
|
||||
|
||||
This is an annotated implementation of the paper
|
||||
[Linear Transformers Are Secretly Fast Weight Memory Systems in PyTorch](https://arxiv.org/abs/2102.11174).
|
||||
|
||||
Here is the [annotated implementation](https://nn.labml.ai/transformers/fast_weights/index.html).
|
||||
Here are [the training code](https://nn.labml.ai/transformers/fast_weights/experiment.html)
|
||||
and a notebook for training a fast weights transformer on the Tiny Shakespeare dataset.
|
||||
|
||||
[](https://colab.research.google.com/github/labmlai/annotated_deep_learning_paper_implementations/blob/master/labml_nn/transformers/fast_weights/experiment.ipynb)
|
||||
@@ -0,0 +1,129 @@
|
||||
"""
|
||||
---
|
||||
title: Fast Weight Systems
|
||||
summary: >
|
||||
This is an annotated implementation/tutorial of
|
||||
Linear Transformers Are Secretly Fast Weight Memory Systems in PyTorch.
|
||||
---
|
||||
"""
|
||||
from typing import Optional
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
from labml_nn.transformers.fast_weights import DPFP
|
||||
from labml_nn.transformers.feed_forward import FeedForward
|
||||
from labml_nn.transformers.mha import PrepareForMultiHeadAttention
|
||||
from labml_nn.utils import clone_module_list
|
||||
|
||||
|
||||
class FastWeightsAttention(nn.Module):
|
||||
def __init__(self, heads: int, d_model: int, dropout_prob: float, phi: DPFP):
|
||||
super().__init__()
|
||||
|
||||
# Number of features per head
|
||||
self.d_k = d_model // heads
|
||||
#
|
||||
self.heads = heads
|
||||
|
||||
# These transform the `query` multi-headed attention.
|
||||
self.query = PrepareForMultiHeadAttention(d_model, heads, self.d_k, bias=False)
|
||||
# These transform the `key` and `value` for multi-headed attention.
|
||||
self.key = PrepareForMultiHeadAttention(d_model, heads, self.d_k, bias=False)
|
||||
self.value = PrepareForMultiHeadAttention(d_model, heads, self.d_k, bias=False)
|
||||
|
||||
self.gate = nn.Sequential(PrepareForMultiHeadAttention(d_model, heads, 1, bias=False),
|
||||
nn.Sigmoid())
|
||||
|
||||
self.phi = phi
|
||||
|
||||
# Output layer
|
||||
self.output = nn.Linear(d_model, d_model)
|
||||
# Dropout
|
||||
self.dropout = nn.Dropout(dropout_prob)
|
||||
|
||||
def forward(self, x: torch.Tensor, weights: Optional[torch.Tensor]):
|
||||
query = self.phi(self.query(x))
|
||||
key = self.phi(self.key(x))
|
||||
value = self.value(x)
|
||||
|
||||
if weights is None:
|
||||
weights = key.new_zeros((key.shape[0], key.shape[1], value.shape[2], key.shape[2]))
|
||||
|
||||
value_existing = torch.einsum('bhvk,bhk->bhv', weights, key)
|
||||
|
||||
beta = self.gate(x)
|
||||
|
||||
weights = weights + torch.einsum('bhv,bhk->bhvk', beta * (value - value_existing), key)
|
||||
|
||||
x = torch.einsum('bhvk,bhk->bhv', weights, query)
|
||||
|
||||
# Concatenate multiple heads
|
||||
x = x.reshape(x.shape[0], -1)
|
||||
|
||||
# Output layer
|
||||
return self.output(x), weights
|
||||
|
||||
|
||||
class FastWeightsAttentionTransformerLayer(nn.Module):
|
||||
def __init__(self, *,
|
||||
d_model: int,
|
||||
attn: FastWeightsAttention,
|
||||
feed_forward: FeedForward,
|
||||
dropout_prob: float):
|
||||
super().__init__()
|
||||
# Transformer size $d_{model}$
|
||||
self.size = d_model
|
||||
#
|
||||
self.attn = attn
|
||||
self.feed_forward = feed_forward
|
||||
self.dropout = nn.Dropout(dropout_prob)
|
||||
|
||||
# Normalization layers
|
||||
self.norm_self_attn = nn.LayerNorm([d_model])
|
||||
self.norm_ff = nn.LayerNorm([d_model])
|
||||
|
||||
def forward(self, x: torch.Tensor, weights: Optional[torch.Tensor]):
|
||||
attn, weights = self.attn(x, weights)
|
||||
# Add the self attention results
|
||||
x = x + self.dropout(attn)
|
||||
|
||||
# Normalize for feed-forward
|
||||
z = self.norm_ff(x)
|
||||
# Pass through the feed-forward network
|
||||
ff = self.feed_forward(z)
|
||||
# Add the feed-forward results back
|
||||
x = x + self.dropout(ff)
|
||||
|
||||
#
|
||||
return x, weights
|
||||
|
||||
|
||||
class FastWeightsAttentionTransformer(nn.Module):
|
||||
def __init__(self, layer: FastWeightsAttentionTransformerLayer, n_layers: int):
|
||||
super().__init__()
|
||||
# Make copies of the transformer layer
|
||||
self.layers = clone_module_list(layer, n_layers)
|
||||
# Final normalization layer
|
||||
self.norm = nn.LayerNorm([layer.size])
|
||||
|
||||
def forward(self, x_seq: torch.Tensor):
|
||||
# Split the input to a list along the sequence axis
|
||||
x_seq = torch.unbind(x_seq, dim=0)
|
||||
# List to store the outputs
|
||||
res = []
|
||||
# For each input step
|
||||
weights = [None for _ in range(len(self.layers))]
|
||||
|
||||
for x in x_seq:
|
||||
# Run through each layer
|
||||
for i, layer in enumerate(self.layers):
|
||||
# Get layer output
|
||||
x, weights[i] = layer(x, weights[i])
|
||||
|
||||
res.append(x)
|
||||
|
||||
# Stack the output tensors
|
||||
res = torch.stack(res)
|
||||
# Normalize the output
|
||||
return self.norm(res)
|
||||
@@ -0,0 +1,93 @@
|
||||
"""
|
||||
---
|
||||
title: Position-wise Feed-Forward Network (FFN)
|
||||
summary: Documented reusable implementation of the position wise feedforward network.
|
||||
---
|
||||
|
||||
# Position-wise Feed-Forward Network (FFN)
|
||||
|
||||
This is a [PyTorch](https://pytorch.org) implementation
|
||||
of position-wise feedforward network used in transformer.
|
||||
|
||||
FFN consists of two fully connected layers.
|
||||
Number of dimensions in the hidden layer $d_{ff}$, is generally set to around
|
||||
four times that of the token embedding $d_{model}$.
|
||||
So it is sometime also called the expand-and-contract network.
|
||||
|
||||
There is an activation at the hidden layer, which is
|
||||
usually set to ReLU (Rectified Linear Unit) activation, $$\max(0, x)$$
|
||||
|
||||
That is, the FFN function is,
|
||||
$$FFN(x, W_1, W_2, b_1, b_2) = \max(0, x W_1 + b_1) W_2 + b_2$$
|
||||
where $W_1$, $W_2$, $b_1$ and $b_2$ are learnable parameters.
|
||||
|
||||
Sometimes the
|
||||
GELU (Gaussian Error Linear Unit) activation is also used instead of ReLU.
|
||||
$$x \Phi(x)$$ where $\Phi(x) = P(X \le x), X \sim \mathcal{N}(0,1)$
|
||||
|
||||
### Gated Linear Units
|
||||
|
||||
This is a generic implementation that supports different variants including
|
||||
[Gated Linear Units](https://arxiv.org/abs/2002.05202) (GLU).
|
||||
We have also implemented experiments on these:
|
||||
|
||||
* [experiment that uses `labml.configs`](glu_variants/experiment.html)
|
||||
* [simpler version from scratch](glu_variants/simple.html)
|
||||
"""
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
|
||||
|
||||
class FeedForward(nn.Module):
|
||||
"""
|
||||
## FFN module
|
||||
"""
|
||||
|
||||
def __init__(self, d_model: int, d_ff: int,
|
||||
dropout: float = 0.1,
|
||||
activation=nn.ReLU(),
|
||||
is_gated: bool = False,
|
||||
bias1: bool = True,
|
||||
bias2: bool = True,
|
||||
bias_gate: bool = True):
|
||||
"""
|
||||
* `d_model` is the number of features in a token embedding
|
||||
* `d_ff` is the number of features in the hidden layer of the FFN
|
||||
* `dropout` is dropout probability for the hidden layer
|
||||
* `is_gated` specifies whether the hidden layer is gated
|
||||
* `bias1` specified whether the first fully connected layer should have a learnable bias
|
||||
* `bias2` specified whether the second fully connected layer should have a learnable bias
|
||||
* `bias_gate` specified whether the fully connected layer for the gate should have a learnable bias
|
||||
"""
|
||||
super().__init__()
|
||||
# Layer one parameterized by weight $W_1$ and bias $b_1$
|
||||
self.layer1 = nn.Linear(d_model, d_ff, bias=bias1)
|
||||
# Layer one parameterized by weight $W_1$ and bias $b_1$
|
||||
self.layer2 = nn.Linear(d_ff, d_model, bias=bias2)
|
||||
# Hidden layer dropout
|
||||
self.dropout = nn.Dropout(dropout)
|
||||
# Activation function $f$
|
||||
self.activation = activation
|
||||
# Whether there is a gate
|
||||
self.is_gated = is_gated
|
||||
if is_gated:
|
||||
# If there is a gate the linear layer to transform inputs to
|
||||
# be multiplied by the gate, parameterized by weight $V$ and bias $c$
|
||||
self.linear_v = nn.Linear(d_model, d_ff, bias=bias_gate)
|
||||
|
||||
def forward(self, x: torch.Tensor):
|
||||
# $f(x W_1 + b_1)$
|
||||
g = self.activation(self.layer1(x))
|
||||
# If gated, $f(x W_1 + b_1) \otimes (x V + b) $
|
||||
if self.is_gated:
|
||||
x = g * self.linear_v(x)
|
||||
# Otherwise
|
||||
else:
|
||||
x = g
|
||||
# Apply dropout
|
||||
x = self.dropout(x)
|
||||
# $(f(x W_1 + b_1) \otimes (x V + b)) W_2 + b_2$ or $f(x W_1 + b_1) W_2 + b_2$
|
||||
# depending on whether it is gated
|
||||
return self.layer2(x)
|
||||
@@ -0,0 +1,528 @@
|
||||
"""
|
||||
---
|
||||
title: Feedback Transformer
|
||||
summary: >
|
||||
This is an annotated implementation/tutorial the Feedback Transformer in PyTorch.
|
||||
---
|
||||
|
||||
# Feedback Transformer
|
||||
|
||||
This is a [PyTorch](https://pytorch.org) implementation of the paper
|
||||
[Accessing Higher-level Representations in Sequential Transformers with Feedback Memory](https://arxiv.org/abs/2002.09402).
|
||||
|
||||
Normal transformers process tokens in parallel. Each transformer layer pays attention
|
||||
to the outputs of the previous layer.
|
||||
Feedback transformer pays attention to the output of all layers in previous steps.
|
||||
So this adds recurrence, and we need to process token-by-token.
|
||||
This slows down the training significantly (about 5X - 10X depending on the sequence length).
|
||||
However, when predicting Feedback Transformer is faster because you can predict the next token
|
||||
if you cache the memory vectors.
|
||||
|
||||
In order to speed up the training, the paper discusses starting with a short sequence length and
|
||||
gradually increasing it.
|
||||
They also discuss using a pretrained parallel transformer as the starting point.
|
||||
|
||||
The original feedback transformer doesn't keep the outputs of all layers.
|
||||
Instead it keeps weighted sum of the output of all layers.
|
||||
This reduces the memory used for caching during prediction.
|
||||
The first half of this file implements this.
|
||||
|
||||
The updated feedback transformer shares weights $W^l_k$ and $W^l_v$ used
|
||||
to calculate keys and values among the layers.
|
||||
We then calculate the keys and values for each step only once and keep
|
||||
them cached.
|
||||
The [second half](#shared_kv) of this file implements this.
|
||||
We implemented a custom PyTorch function to improve performance.
|
||||
|
||||
Here's [the training code](experiment.html) and a notebook for training a feedback transformer on Tiny Shakespeare dataset.
|
||||
|
||||
[](https://colab.research.google.com/github/labmlai/annotated_deep_learning_paper_implementations/blob/master/labml_nn/transformers/feedback/experiment.ipynb)
|
||||
"""
|
||||
|
||||
import math
|
||||
from typing import Optional
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
from labml_nn.transformers.feed_forward import FeedForward
|
||||
from labml_nn.transformers.mha import PrepareForMultiHeadAttention
|
||||
from labml_nn.utils import clone_module_list
|
||||
|
||||
|
||||
class FeedbackAttention(nn.Module):
|
||||
r"""
|
||||
## Feedback Attention
|
||||
|
||||
|
||||
This module computes recurrent attention similar to attention from original transformers
|
||||
paper.
|
||||
|
||||
$$\mathop{Attention}(Q, K, V) = \underset{seq}{\mathop{softmax}}\Bigg(\frac{Q^\top K}{\sqrt{d_k}}\Bigg)V$$
|
||||
"""
|
||||
|
||||
def __init__(self, heads: int, d_model: int, dropout_prob: float = 0.1, *,
|
||||
is_kv_precomputed: bool = False):
|
||||
"""
|
||||
* 'heads' is the number of attention heads
|
||||
* `d_model` is the number of features in the transformer
|
||||
* `dropout_prob` is the attention dropout probability
|
||||
* `is_kv_precomputed` is whether key, value tensors are already calculated
|
||||
"""
|
||||
|
||||
super().__init__()
|
||||
|
||||
# Number of features per head
|
||||
self.d_k = d_model // heads
|
||||
#
|
||||
self.heads = heads
|
||||
|
||||
# These transform the `query` multi-headed attention.
|
||||
self.query = PrepareForMultiHeadAttention(d_model, heads, self.d_k, bias=False)
|
||||
# These transform the `key` and `value` for multi-headed attention.
|
||||
if not is_kv_precomputed:
|
||||
self.key = PrepareForMultiHeadAttention(d_model, heads, self.d_k, bias=False)
|
||||
self.value = PrepareForMultiHeadAttention(d_model, heads, self.d_k, bias=True)
|
||||
# Keys and values are already calculated
|
||||
else:
|
||||
self.key = None
|
||||
self.value = None
|
||||
|
||||
# Output layer
|
||||
self.output = nn.Linear(d_model, d_model)
|
||||
# Dropout
|
||||
self.dropout = nn.Dropout(dropout_prob)
|
||||
# Scaling factor before the softmax
|
||||
self.scale = 1 / math.sqrt(self.d_k)
|
||||
|
||||
# Softmax for attention along the time dimension of `key`
|
||||
self.softmax = nn.Softmax(dim=0)
|
||||
|
||||
# Number of relative positions
|
||||
self.P = 2 ** 12
|
||||
|
||||
# Relative positional embeddings for key relative to the query.
|
||||
self.key_pos_embeddings = nn.Parameter(torch.zeros((self.P, heads, self.d_k)), requires_grad=True)
|
||||
# Relative positional embedding bias for key relative to the query.
|
||||
self.key_pos_bias = nn.Parameter(torch.zeros((self.P, heads)), requires_grad=True)
|
||||
# Positional embeddings for the query is independent of the position of the query
|
||||
self.query_pos_bias = nn.Parameter(torch.zeros((heads, self.d_k)), requires_grad=True)
|
||||
|
||||
# We store attentions so that it can be used for logging, or other computations if needed
|
||||
self.attn = None
|
||||
|
||||
def get_scores(self, query: torch.Tensor, key: torch.Tensor):
|
||||
r"""
|
||||
### Get attention scores
|
||||
|
||||
We use relative positional encodings for attention, similar
|
||||
to [relative multi-head attention form Transformer-XL paper](../relative_mha.html).
|
||||
|
||||
Attention from current step's query to key in step $j$ (relative to current step) is,
|
||||
|
||||
\begin{align}
|
||||
A_{j} &= Q^\top K_j \\
|
||||
&= lin_q(X^q + P_q)^\top lin_k(X^k_j + P_j) \\
|
||||
&= (Q + U^Q)^\top(K_j + U^K_j) \\
|
||||
&= \underset{\textcolor{lightgreen}{A}}{Q^\top K_j} +
|
||||
\underset{\textcolor{lightgreen}{B}}{Q^\top U^K_j} +
|
||||
\underset{\textcolor{lightgreen}{C}}{{U^Q}^\top K_j} +
|
||||
\underset{\textcolor{lightgreen}{D}}{{U^Q}^\top U^K_j}
|
||||
\end{align}
|
||||
|
||||
where $Q, K_j$, are linear transformations of
|
||||
original embeddings $X^q, X^k_j$
|
||||
and $U^Q, U^K_j$ are linear transformations of
|
||||
positional encodings $P_q, P_j$.
|
||||
|
||||
We replace term $\textcolor{lightgreen}{D}$ with $S_j$.
|
||||
"""
|
||||
|
||||
# $U^K_j$
|
||||
key_pos_emb = self.key_pos_embeddings[-key.shape[0]:]
|
||||
# $U^Q$
|
||||
query_pos_bias = self.query_pos_bias[None, :, :]
|
||||
# $S_j$
|
||||
key_pos_bias = self.key_pos_bias[-key.shape[0]:]
|
||||
|
||||
# $\underset{\textcolor{lightgreen}{A}}{Q^\top K_j} + \underset{\textcolor{lightgreen}{C}}{{U^Q}^\top K_j}$
|
||||
ac = torch.einsum('bhd,jbhd->jbh', query + query_pos_bias, key)
|
||||
# $\underset{\textcolor{lightgreen}{B}}{Q^\top U^K_j} + \underset{\textcolor{lightgreen}{D}}{S_j}$
|
||||
bd = torch.einsum('bhd,jhd->jbh', query, key_pos_emb) + key_pos_bias[:, None, :]
|
||||
|
||||
# $A_j$
|
||||
return ac + bd
|
||||
|
||||
def forward(self, *,
|
||||
query: torch.Tensor,
|
||||
key: torch.Tensor,
|
||||
value: torch.Tensor):
|
||||
"""
|
||||
* `query` has shape `[batch_size, d_model]`
|
||||
* `key` and `value` has shape `[seq_len, batch_size, d_model]`
|
||||
"""
|
||||
|
||||
# Prepare `query`, `key` and `value` for attention computation
|
||||
# `key` and `value` will then have shape `[seq_len, batch_size, heads, d_k]`
|
||||
# and `query` will have shape `[batch_size, heads, d_k]`
|
||||
query = self.query(query)
|
||||
if self.key:
|
||||
key = self.key(key)
|
||||
if self.value:
|
||||
value = self.value(value)
|
||||
|
||||
# Compute attention scores.
|
||||
# Results in a tensor of shape `[seq_len, batch_size, heads]`
|
||||
scores = self.get_scores(query, key)
|
||||
|
||||
# Scale scores $\frac{1}{\sqrt{d_k}}$
|
||||
scores *= self.scale
|
||||
|
||||
# Softmax
|
||||
attn = self.softmax(scores)
|
||||
|
||||
# Apply dropout
|
||||
attn = self.dropout(attn)
|
||||
|
||||
# Multiply by the values
|
||||
x = torch.einsum("jbh,jbhd->bhd", attn, value)
|
||||
|
||||
# Concatenate multiple heads
|
||||
x = x.reshape(x.shape[0], -1)
|
||||
|
||||
# Output layer
|
||||
return self.output(x)
|
||||
|
||||
|
||||
class FeedbackTransformerLayer(nn.Module):
|
||||
"""
|
||||
## Feedback Transformer Layer
|
||||
|
||||
This implements a single transformer layer in the feedback transformer.
|
||||
"""
|
||||
|
||||
def __init__(self, *,
|
||||
d_model: int,
|
||||
attn: FeedbackAttention,
|
||||
feed_forward: FeedForward,
|
||||
dropout_prob: float):
|
||||
"""
|
||||
* `d_model` is the number of features in the transformer
|
||||
* `attn` is the feedback attention module
|
||||
* `feed_forward` is the position-wise feed forward layer
|
||||
* `dropout_prob` is the dropout probability for dropout layers after attention and feed-forward
|
||||
"""
|
||||
super().__init__()
|
||||
# Transformer size $d_{model}$
|
||||
self.size = d_model
|
||||
#
|
||||
self.attn = attn
|
||||
self.feed_forward = feed_forward
|
||||
self.dropout = nn.Dropout(dropout_prob)
|
||||
|
||||
# Normalization layers
|
||||
self.norm_self_attn = nn.LayerNorm([d_model])
|
||||
self.norm_ff = nn.LayerNorm([d_model])
|
||||
|
||||
def forward(self, *,
|
||||
x: torch.Tensor,
|
||||
key: Optional[torch.Tensor],
|
||||
value: Optional[torch.Tensor]):
|
||||
# If there is memory
|
||||
if key is not None:
|
||||
# Normalize the vectors before doing self attention
|
||||
z = self.norm_self_attn(x)
|
||||
# Run through self attention, i.e. keys and values are from self
|
||||
self_attn = self.attn(query=z, key=key, value=value)
|
||||
# Add the self attention results
|
||||
x = x + self.dropout(self_attn)
|
||||
|
||||
# Normalize for feed-forward
|
||||
z = self.norm_ff(x)
|
||||
# Pass through the feed-forward network
|
||||
ff = self.feed_forward(z)
|
||||
# Add the feed-forward results back
|
||||
x = x + self.dropout(ff)
|
||||
|
||||
#
|
||||
return x
|
||||
|
||||
|
||||
class FeedbackTransformer(nn.Module):
|
||||
"""
|
||||
## Feedback Transformer Module
|
||||
"""
|
||||
|
||||
def __init__(self, layer: FeedbackTransformerLayer, n_layers: int):
|
||||
"""
|
||||
* `layer` is the feedback transformer layer, which we clone for each layer
|
||||
* `n_layers` is the number of layers in the transformer
|
||||
"""
|
||||
|
||||
super().__init__()
|
||||
# Make copies of the transformer layer
|
||||
self.layers = clone_module_list(layer, n_layers)
|
||||
# Final normalization layer
|
||||
self.norm = nn.LayerNorm([layer.size])
|
||||
# Memory vectors are computed as a weighted sum of representations of each layer.
|
||||
# This is the weights parameter for that.
|
||||
self.weights = nn.Parameter(torch.ones(n_layers + 1), requires_grad=True)
|
||||
# Softmax for weights before taking the weighted sum
|
||||
self.softmax = nn.Softmax(0)
|
||||
|
||||
def forward(self, x_seq: torch.Tensor):
|
||||
"""
|
||||
* `x_seq` is the input with shape `[seq_len, batch_size, d_model]`
|
||||
"""
|
||||
|
||||
# Split the input to a list along the sequence axis
|
||||
x_seq = torch.unbind(x_seq, dim=0)
|
||||
# List to store the outputs
|
||||
res = []
|
||||
# List to store the memory vectors
|
||||
mem = []
|
||||
# For each input step
|
||||
for x in x_seq:
|
||||
# List to store layer outputs
|
||||
layer_outputs = [x]
|
||||
|
||||
# If there is memory, stack them into a vector
|
||||
mem_tensor = torch.stack(mem) if mem else None
|
||||
|
||||
# Run through each layer
|
||||
for layer in self.layers:
|
||||
# Get layer output
|
||||
x = layer(x=x, key=mem_tensor, value=mem_tensor)
|
||||
# Append them to the list of layer outputs
|
||||
layer_outputs.append(x)
|
||||
|
||||
# Stack the layer outputs to a tensor
|
||||
layer_outputs = torch.stack(layer_outputs)
|
||||
# Calculate the memory vector as a weighted sum of layer outputs
|
||||
mem.append(torch.einsum('lbd,l->bd', layer_outputs, self.softmax(self.weights)))
|
||||
# Append the output to results
|
||||
res.append(x)
|
||||
|
||||
# Stack the output tensors
|
||||
res = torch.stack(res)
|
||||
# Normalize the output
|
||||
return self.norm(res)
|
||||
|
||||
|
||||
# <a id="shared_kv"></a>
|
||||
#
|
||||
# # Shared keys and values among layers
|
||||
|
||||
class StackFunction(torch.autograd.Function):
|
||||
"""
|
||||
### Stack Function implementation
|
||||
|
||||
We implement a custom function instead of appending to a python list
|
||||
and then doing `torch.stack`.
|
||||
This greatly improves the performance over calling `torch.stack` at
|
||||
each step along the sequence.
|
||||
Everytime `torch.stack` is called, it creates a new tensor, while
|
||||
this method and the accompanying class `Stack` share memory for each step.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, memory, memory_grad, last, n):
|
||||
"""
|
||||
* `ctx` is the context of the function (which lets us cache stuff)
|
||||
* `memory` is the shared memory tensor where we stack and store the values of each step (keys & values)
|
||||
* `memory_grad` is the shared memory tensor to store and accumulate gradients of each step
|
||||
* `last` is the last value stacked
|
||||
* `n` is the number of steps (i.e. size of the stack)
|
||||
|
||||
This returns the stacked tensor for steps upto `n`.
|
||||
"""
|
||||
|
||||
# Cache accumulated gradients
|
||||
ctx._mem_grad = memory_grad
|
||||
# Cache the size of the stack
|
||||
ctx._n = n
|
||||
# Return the stack
|
||||
return memory[:n + 1]
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, grad_output):
|
||||
"""
|
||||
* `grad_output` is the gradient with respect to the output of about `forward` function
|
||||
|
||||
This accumulates the gradients in the shared memory tensor and return the
|
||||
gradients with respect to the `last` result in the stack.
|
||||
"""
|
||||
# Get the current size of the stack
|
||||
n = ctx._n
|
||||
# Get the accumulated gradients
|
||||
memory_grad = ctx._mem_grad
|
||||
# Add the gradients
|
||||
memory_grad[:n + 1] += grad_output
|
||||
# Return the gradients w.r.t to last value in the stack
|
||||
return None, None, memory_grad[n], None
|
||||
|
||||
|
||||
class Stack:
|
||||
"""
|
||||
### Stack Module
|
||||
|
||||
This uses the stack function defined above, and does the necessary initializations.
|
||||
"""
|
||||
|
||||
def __init__(self, max_len: int):
|
||||
"""
|
||||
* `max_len` is the maximum size of the stack
|
||||
"""
|
||||
self.max_len = max_len
|
||||
self.memory = None
|
||||
self.memory_grad = None
|
||||
self.last = None
|
||||
self.n = -1
|
||||
self.last_get_n = -1
|
||||
|
||||
def append(self, n: int, value: torch.Tensor):
|
||||
"""
|
||||
* `n` is the size of the stack
|
||||
* `value` is the tensor that needs to be added to the stack
|
||||
"""
|
||||
|
||||
# You need to get (use) the stack after adding a value.
|
||||
# Otherwise this implementation fails
|
||||
assert n == 0 or self.last_get_n == n - 1, f"{n}, {self.last_get_n}"
|
||||
|
||||
# Do this without gradients
|
||||
with torch.no_grad():
|
||||
# Initialize the shared memory tensor to keep the stack
|
||||
if self.memory is None or self.memory.shape[1:] != value.shape:
|
||||
# This should only happen when the stack is empty
|
||||
assert n == 0
|
||||
# Create a tensor for the stack
|
||||
self.memory = value.new_zeros(self.max_len, *value.shape, requires_grad=False)
|
||||
# Create a tensor to accumulate the gradients
|
||||
self.memory_grad = value.new_zeros(self.memory.shape, requires_grad=False)
|
||||
# The memory is already initialized but we are resetting the stack.
|
||||
#
|
||||
# This could have been another function like `reset`, but
|
||||
# we found this easier to use.
|
||||
elif n == 0:
|
||||
# Reset accumulated gradients
|
||||
self.memory_grad.fill_(0.)
|
||||
|
||||
# Set the value in the correct position of the stack
|
||||
self.memory.data[n] = value.detach()
|
||||
# Keep track of the stack (for debugging)
|
||||
self.n = n
|
||||
|
||||
# Keep track of the last value added to the stack.
|
||||
# We need this to be passed on to `StackFunction` in order
|
||||
# to get the gradients propagated backwards.
|
||||
self.last = value
|
||||
|
||||
def get(self):
|
||||
"""
|
||||
Returns the stack
|
||||
"""
|
||||
|
||||
# Keep track of the size of the stack when it was used.
|
||||
# This is used for a sanity check in `append`.
|
||||
self.last_get_n = self.n
|
||||
# Take it all through `StackFunction` so that `StackFunction.backwards`
|
||||
# is called by PyTorch during backpropagation.
|
||||
return StackFunction.apply(self.memory, self.memory_grad, self.last, self.n)
|
||||
|
||||
def free(self):
|
||||
"""
|
||||
To release memory
|
||||
"""
|
||||
|
||||
self.memory = None
|
||||
self.memory_grad = None
|
||||
self.last = None
|
||||
|
||||
|
||||
class FeedbackTransformerKV(nn.Module):
|
||||
"""
|
||||
## Updated Feedback Transformer Module
|
||||
|
||||
This is the updated feedback transformer module that caches the keys and values.
|
||||
"""
|
||||
|
||||
def __init__(self, layer: FeedbackTransformerLayer, n_layers: int, d_model: int, heads: int):
|
||||
"""
|
||||
* `layer` is the feedback transformer layer, which we clone for each layer
|
||||
* `n_layers` is the number of layers in the transformer
|
||||
* `d_model` is the number of features in the transformer
|
||||
* 'heads' is the number of attention heads
|
||||
"""
|
||||
|
||||
super().__init__()
|
||||
# Make copies of the transformer layer
|
||||
self.layers = clone_module_list(layer, n_layers)
|
||||
# Final normalization layer
|
||||
self.norm = nn.LayerNorm([layer.size])
|
||||
# Memory vectors are computed as a weighted sum of representations of each layer.
|
||||
# This is the weights parameter for that.
|
||||
self.weights = nn.Parameter(torch.ones(n_layers + 1), requires_grad=True)
|
||||
# Softmax for weights before taking the weighted sum
|
||||
self.softmax = nn.Softmax(0)
|
||||
|
||||
# Number of features in a head
|
||||
d_k = d_model // heads
|
||||
# Module to transform embeddings (memory) to get keys
|
||||
self.key = PrepareForMultiHeadAttention(d_model, heads, d_k, bias=False)
|
||||
# Module to transform embeddings (memory) to get keys
|
||||
self.value = PrepareForMultiHeadAttention(d_model, heads, d_k, bias=False)
|
||||
|
||||
# Memory for stacked keys
|
||||
self.mem_key = Stack(512)
|
||||
# Memory for stacked values
|
||||
self.mem_value = Stack(512)
|
||||
|
||||
def forward(self, x_seq: torch.Tensor):
|
||||
"""
|
||||
* `x_seq` is the input with shape `[seq_len, batch_size, d_model]`
|
||||
"""
|
||||
|
||||
# Split the input to a list along the sequence axis
|
||||
x_seq = torch.unbind(x_seq, dim=0)
|
||||
# List to store the outputs
|
||||
res = []
|
||||
# For each input step
|
||||
for step, x in enumerate(x_seq):
|
||||
# List to store layer outputs
|
||||
layer_outputs = [x]
|
||||
|
||||
# Stack of keys and values
|
||||
key_tensor = None
|
||||
value_tensor = None
|
||||
# Get the keys and values tensors if we are beyond the initial step
|
||||
if step > 0:
|
||||
key_tensor = self.mem_key.get()
|
||||
value_tensor = self.mem_value.get()
|
||||
|
||||
# Run through each layer
|
||||
for layer in self.layers:
|
||||
# Get layer output
|
||||
x = layer(x=x, key=key_tensor, value=value_tensor)
|
||||
# Append them to the list of layer outputs
|
||||
layer_outputs.append(x)
|
||||
|
||||
# Stack the layer outputs to a tensor
|
||||
layer_outputs = torch.stack(layer_outputs)
|
||||
# Calculate the memory vector as a weighted sum of layer outputs
|
||||
mem = torch.einsum('lbd,l->bd', layer_outputs, self.softmax(self.weights))
|
||||
# Calculate the keys from memory and add it to the stack
|
||||
self.mem_key.append(step, self.key(mem))
|
||||
# Calculate the values from memory and add it to the stack
|
||||
self.mem_value.append(step, self.value(mem))
|
||||
# Append the output to results
|
||||
res.append(x)
|
||||
|
||||
# Stack the output tensors
|
||||
res = torch.stack(res)
|
||||
# Normalize the output
|
||||
return self.norm(res)
|
||||
|
||||
def free(self):
|
||||
self.mem_key.free()
|
||||
self.mem_value.free()
|
||||
@@ -0,0 +1,232 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "AYV_dMVDxyc2"
|
||||
},
|
||||
"source": [
|
||||
"[](https://github.com/labmlai/annotated_deep_learning_paper_implementations)\n",
|
||||
"[](https://colab.research.google.com/github/labmlai/annotated_deep_learning_paper_implementations/blob/master/labml_nn/transformers/feedback/experiment.ipynb) \n",
|
||||
"\n",
|
||||
"## Feedback Transformer\n",
|
||||
"\n",
|
||||
"This is an experiment training Shakespeare dataset with Feedback Transformer."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "AahG_i2y5tY9"
|
||||
},
|
||||
"source": [
|
||||
"Install the `labml-nn` package"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"base_uri": "https://localhost:8080/"
|
||||
},
|
||||
"id": "ZCzmCrAIVg0L",
|
||||
"outputId": "aa1fe63d-1755-4394-dcdf-9897ac6c1ee4"
|
||||
},
|
||||
"source": [
|
||||
"!pip install labml-nn"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "SE2VUQ6L5zxI"
|
||||
},
|
||||
"source": [
|
||||
"Imports"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"id": "0hJXx_g0wS2C"
|
||||
},
|
||||
"source": [
|
||||
"from labml import experiment\n",
|
||||
"from labml_nn.transformers.feedback.experiment import Configs"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "Lpggo0wM6qb-"
|
||||
},
|
||||
"source": [
|
||||
"Create an experiment"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"id": "bFcr9k-l4cAg"
|
||||
},
|
||||
"source": [
|
||||
"experiment.create(name=\"feedback_transformer\")"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "-OnHLi626tJt"
|
||||
},
|
||||
"source": [
|
||||
"Initialize configurations"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"id": "Piz0c5f44hRo"
|
||||
},
|
||||
"source": [
|
||||
"conf = Configs()"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "wwMzCqpD6vkL"
|
||||
},
|
||||
"source": [
|
||||
"Set experiment configurations and assign a configurations dictionary to override configurations"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"base_uri": "https://localhost:8080/",
|
||||
"height": 17
|
||||
},
|
||||
"id": "e6hmQhTw4nks",
|
||||
"outputId": "91d99011-7a61-48fa-ee5c-2fa845883cec"
|
||||
},
|
||||
"source": [
|
||||
"experiment.configs(conf,\n",
|
||||
" {'tokenizer': 'character',\n",
|
||||
" 'text': 'tiny_shakespeare',\n",
|
||||
" 'optimizer.learning_rate': 1.0,\n",
|
||||
" 'optimizer.optimizer': 'Noam',\n",
|
||||
" 'prompt': 'It is',\n",
|
||||
" 'prompt_separator': '',\n",
|
||||
"\n",
|
||||
" 'model': 'feedback_transformer',\n",
|
||||
"\n",
|
||||
" 'train_loader': 'shuffled_train_loader',\n",
|
||||
" 'valid_loader': 'shuffled_valid_loader',\n",
|
||||
"\n",
|
||||
" 'seq_len': 64,\n",
|
||||
" 'epochs': 128,\n",
|
||||
" 'batch_size': 80,\n",
|
||||
" 'inner_iterations': 25})"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "EvI7MtgJ61w5"
|
||||
},
|
||||
"source": [
|
||||
"Set PyTorch models for loading and saving"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"base_uri": "https://localhost:8080/",
|
||||
"height": 255
|
||||
},
|
||||
"id": "GDlt7dp-5ALt",
|
||||
"outputId": "bee57c09-4e71-4329-debb-b23ed309a3ef"
|
||||
},
|
||||
"source": [
|
||||
"experiment.add_pytorch_models({'model': conf.model})"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "KJZRf8527GxL"
|
||||
},
|
||||
"source": [
|
||||
"Start the experiment and run the training loop."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"base_uri": "https://localhost:8080/",
|
||||
"height": 1000
|
||||
},
|
||||
"id": "aIAWo7Fw5DR8",
|
||||
"outputId": "bfb52e21-1913-4bd0-b67f-bef8a14f0f05"
|
||||
},
|
||||
"source": [
|
||||
"# Start the experiment\n",
|
||||
"with experiment.start():\n",
|
||||
" conf.run()"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"id": "oBXXlP2b7XZO"
|
||||
},
|
||||
"source": [],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"accelerator": "GPU",
|
||||
"colab": {
|
||||
"collapsed_sections": [],
|
||||
"name": "Feedback Transformer",
|
||||
"provenance": []
|
||||
},
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.7.5"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 4
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
"""
|
||||
---
|
||||
title: Train Feedback Transformer
|
||||
summary: This is training code with notes for a feedback transformer.
|
||||
---
|
||||
|
||||
# Train Feedback Transformer
|
||||
|
||||
This trains a [feedback transformer](index.html) model for auto-regression.
|
||||
You can pick the original feedback transformer or the new version
|
||||
where the keys and values are precalculated.
|
||||
|
||||
Here's a Colab notebook for training a feedback transformer on Tiny Shakespeare dataset.
|
||||
|
||||
[](https://colab.research.google.com/github/labmlai/annotated_deep_learning_paper_implementations/blob/master/labml_nn/transformers/feedback/experiment.ipynb)
|
||||
"""
|
||||
|
||||
import torch
|
||||
from labml import experiment
|
||||
from labml.configs import option
|
||||
from labml.utils.pytorch import get_modules
|
||||
from labml_nn.experiments.nlp_autoregression import NLPAutoRegressionConfigs
|
||||
from torch import nn
|
||||
|
||||
|
||||
class AutoregressiveModel(nn.Module):
|
||||
"""
|
||||
## Auto regressive model
|
||||
"""
|
||||
|
||||
def __init__(self, n_vocab: int, d_model: int, transformer: nn.Module):
|
||||
super().__init__()
|
||||
# Token embedding module
|
||||
self.src_embed = nn.Embedding(n_vocab, d_model)
|
||||
self.transformer = transformer
|
||||
self.generator = nn.Linear(d_model, n_vocab)
|
||||
|
||||
def forward(self, x: torch.Tensor):
|
||||
# Embed the tokens
|
||||
x = self.src_embed(x)
|
||||
# Run it through the the transformer
|
||||
res = self.transformer(x)
|
||||
# Generate logits of the next token
|
||||
return self.generator(res), None
|
||||
|
||||
|
||||
class Configs(NLPAutoRegressionConfigs):
|
||||
"""
|
||||
## Configurations
|
||||
|
||||
The default configs can and will be over-ridden when we start the experiment
|
||||
"""
|
||||
|
||||
model: AutoregressiveModel
|
||||
|
||||
d_model: int = 512
|
||||
heads: int = 8
|
||||
dropout: float = 0.0
|
||||
d_ff: int = 2048
|
||||
n_layers: int = 6
|
||||
|
||||
|
||||
@option(Configs.model)
|
||||
def feedback_transformer(c: Configs):
|
||||
"""
|
||||
Create [original feedback transformer](index.html).
|
||||
"""
|
||||
from labml_nn.transformers.feedback import FeedbackTransformer, FeedbackTransformerLayer, \
|
||||
FeedbackAttention, FeedForward
|
||||
|
||||
return AutoregressiveModel(
|
||||
c.n_tokens, c.d_model,
|
||||
FeedbackTransformer(
|
||||
FeedbackTransformerLayer(d_model=c.d_model,
|
||||
attn=FeedbackAttention(c.heads, c.d_model, c.dropout),
|
||||
feed_forward=FeedForward(c.d_model, c.d_ff, c.dropout),
|
||||
dropout_prob=c.dropout),
|
||||
c.n_layers)).to(c.device)
|
||||
|
||||
|
||||
@option(Configs.model)
|
||||
def feedback_transformer_kv(c: Configs):
|
||||
"""
|
||||
Create [updated feedback transformer](index.html#kv_shared), with precalculated keys and values.
|
||||
"""
|
||||
from labml_nn.transformers.feedback import FeedbackTransformerKV, FeedbackTransformerLayer, \
|
||||
FeedbackAttention, FeedForward
|
||||
|
||||
return AutoregressiveModel(
|
||||
c.n_tokens, c.d_model,
|
||||
FeedbackTransformerKV(
|
||||
FeedbackTransformerLayer(d_model=c.d_model,
|
||||
attn=FeedbackAttention(c.heads, c.d_model, c.dropout,
|
||||
is_kv_precomputed=True),
|
||||
feed_forward=FeedForward(c.d_model, c.d_ff, c.dropout),
|
||||
dropout_prob=c.dropout),
|
||||
c.n_layers, c.d_model, c.heads)).to(c.device)
|
||||
|
||||
|
||||
def main():
|
||||
# Create experiment
|
||||
experiment.create(name="feedback_transformer")
|
||||
# Create configs
|
||||
conf = Configs()
|
||||
# Load configurations
|
||||
experiment.configs(conf,
|
||||
# A dictionary of configurations to override
|
||||
{'tokenizer': 'character',
|
||||
'text': 'tiny_shakespeare',
|
||||
'optimizer.learning_rate': 1.0,
|
||||
'optimizer.optimizer': 'Noam',
|
||||
'prompt': 'It is',
|
||||
'prompt_separator': '',
|
||||
|
||||
# Use `feedback_transformer` for original feedback transformer
|
||||
'model': 'feedback_transformer_kv',
|
||||
|
||||
'train_loader': 'shuffled_train_loader',
|
||||
'valid_loader': 'shuffled_valid_loader',
|
||||
|
||||
'seq_len': 128,
|
||||
'epochs': 128,
|
||||
'batch_size': 64,
|
||||
'inner_iterations': 25})
|
||||
|
||||
# Set models for saving and loading
|
||||
experiment.add_pytorch_models(get_modules(conf))
|
||||
|
||||
# Start the experiment
|
||||
with experiment.start():
|
||||
# Run the training loop
|
||||
conf.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,34 @@
|
||||
# [Feedback Transformer](https://nn.labml.ai/transformers/feedback/index.html)
|
||||
|
||||
This is a [PyTorch](https://pytorch.org) implementation of the paper
|
||||
[Accessing Higher-level Representations in Sequential Transformers with Feedback Memory](https://arxiv.org/abs/2002.09402).
|
||||
|
||||
Normal transformers process tokens in parallel. Each transformer layer pays attention
|
||||
to the outputs of the previous layer.
|
||||
Feedback transformer pays attention to the output of all layers in previous steps.
|
||||
So this adds recurrence, and we need to process token-by-token.
|
||||
This slows down the training significantly (about 5X - 10X depending on the sequence length).
|
||||
However, when predicting Feedback Transformer is faster because you can predict the next token
|
||||
if you cache the memory vectors.
|
||||
|
||||
In order to speed up the training the paper discusses starting with a short sequence length and
|
||||
gradually increasing it.
|
||||
They also discuss using a pretrained parallel transformer as the starting point.
|
||||
|
||||
The original feedback transformer doesn't keep the outputs of all layers.
|
||||
Instead it keeps weighted sum of the output of all layers.
|
||||
This reduces the memory used for caching during prediction.
|
||||
The first half of this file implements this.
|
||||
|
||||
The updated feedback transformer shares weights used
|
||||
to calculate keys and values among the layers.
|
||||
We then calculate the keys and values for each step only once and keep
|
||||
them cached.
|
||||
The [second half](#shared_kv) of this file implements this.
|
||||
We implemented a custom PyTorch function to improve performance.
|
||||
|
||||
Here's [the training code](experiment.html) and a notebook for training a feedback transformer on Tiny Shakespeare dataset.
|
||||
|
||||
[Colab Notebook](https://colab.research.google.com/github/labmlai/annotated_deep_learning_paper_implementations/blob/master/labml_nn/transformers/feedback/experiment.ipynb)
|
||||
|
||||
[](https://colab.research.google.com/github/labmlai/annotated_deep_learning_paper_implementations/blob/master/labml_nn/transformers/feedback/experiment.ipynb)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,179 @@
|
||||
"""
|
||||
### Test Flash Attention Implementation
|
||||
|
||||
This is the code to test and measure performance of our flash attention implementation
|
||||
"""
|
||||
|
||||
import torch
|
||||
import triton
|
||||
|
||||
from labml import logger, monit
|
||||
from labml_nn.transformers.flash import attention
|
||||
|
||||
HI_PRES_TORCH = torch.float32
|
||||
|
||||
|
||||
@torch.no_grad()
|
||||
def _calc_abs_rel_error(a: torch.Tensor, b: torch.Tensor, atol=1e-2):
|
||||
"""
|
||||
#### Calculate absolute and relative error for reporting
|
||||
"""
|
||||
d = (a - b).abs()
|
||||
max_abs = d.max()
|
||||
d = (d - atol).clamp(min=0)
|
||||
d = d / b.abs()
|
||||
max_rel = d.max()
|
||||
|
||||
return max_abs.cpu().item(), max_rel.cpu().item()
|
||||
|
||||
|
||||
def test_fwd_bwd(batch_size, n_heads, k_heads, q_seq_len, kv_seq_len, d_head, causal, dtype, device):
|
||||
"""
|
||||
#### Compare our implementation with naive PyTorch attention
|
||||
"""
|
||||
|
||||
with monit.section(f'Init {q_seq_len} {kv_seq_len} {d_head}'):
|
||||
torch.manual_seed(20)
|
||||
q = (torch.empty((batch_size, n_heads, q_seq_len, d_head),
|
||||
dtype=dtype, device=device).normal_(mean=0.0, std=0.5).requires_grad_())
|
||||
k = (torch.empty((batch_size, k_heads, kv_seq_len, d_head),
|
||||
dtype=dtype, device=device).normal_(mean=0.0, std=0.5).requires_grad_())
|
||||
v = (torch.empty((batch_size, k_heads, kv_seq_len, d_head),
|
||||
dtype=dtype, device=device).normal_(mean=0.0, std=0.5).requires_grad_())
|
||||
sm_scale = d_head ** -0.5
|
||||
d_out = torch.randn_like(q)
|
||||
# reference implementation
|
||||
mask = torch.tril(torch.ones((q_seq_len, kv_seq_len), device=device, dtype=torch.bool))
|
||||
torch.cuda.synchronize()
|
||||
|
||||
with monit.section('Pytorch'):
|
||||
p = torch.matmul(q.view(batch_size, k_heads, -1, q_seq_len, d_head),
|
||||
k.transpose(2, 3)[:, :, None, :, :]) * sm_scale
|
||||
if causal:
|
||||
p[:, :, :, ~mask] = float("-inf")
|
||||
p = torch.softmax(p.to(HI_PRES_TORCH), dim=-1).to(dtype)
|
||||
ref_out = torch.matmul(p, v[:, :, None, :, :])
|
||||
ref_out = ref_out.view(q.shape)
|
||||
ref_out.backward(d_out)
|
||||
ref_dv, v.grad = v.grad.clone(), None
|
||||
ref_dk, k.grad = k.grad.clone(), None
|
||||
ref_dq, q.grad = q.grad.clone(), None
|
||||
torch.cuda.synchronize()
|
||||
|
||||
with monit.section('Triton'):
|
||||
assert q.dtype == dtype
|
||||
tri_out = attention(q, k, v, causal, sm_scale).to(dtype)
|
||||
monit.progress(0.5)
|
||||
|
||||
tri_out.backward(d_out)
|
||||
monit.progress(0.9)
|
||||
tri_dv, v.grad = v.grad.clone(), None # type: ignore
|
||||
tri_dk, k.grad = k.grad.clone(), None # type: ignore
|
||||
tri_dq, q.grad = q.grad.clone(), None # type: ignore
|
||||
torch.cuda.synchronize()
|
||||
|
||||
with monit.section('Test') as s:
|
||||
# compare
|
||||
passed = True
|
||||
if not torch.allclose(tri_out, ref_out, atol=1e-2, rtol=0.):
|
||||
abs_err, rel_err = _calc_abs_rel_error(ref_out, tri_out)
|
||||
logger.log(('[FAILED]', logger.Text.danger), f' Out mismatch {abs_err} {rel_err}')
|
||||
passed = False
|
||||
rtol = 1e-1
|
||||
if not torch.allclose(tri_dq, ref_dq, atol=1e-2, rtol=rtol):
|
||||
abs_err, rel_err = _calc_abs_rel_error(ref_dq, tri_dq)
|
||||
logger.log(('[FAILED]', logger.Text.danger), f' dQ mismatch {abs_err} {rel_err}')
|
||||
passed = False
|
||||
if not torch.allclose(tri_dv, ref_dv, atol=1e-2, rtol=rtol):
|
||||
abs_err, rel_err = _calc_abs_rel_error(ref_dv, tri_dv)
|
||||
logger.log(('[FAILED]', logger.Text.danger), f' dV mismatch {abs_err} {rel_err}')
|
||||
passed = False
|
||||
if not torch.allclose(tri_dk, ref_dk, atol=1e-2, rtol=rtol):
|
||||
abs_err, rel_err = _calc_abs_rel_error(ref_dk, tri_dk)
|
||||
logger.log(('[FAILED]', logger.Text.danger), f' dK mismatch {abs_err} {rel_err}')
|
||||
passed = False
|
||||
|
||||
if passed:
|
||||
logger.log('[PASSED]', logger.Text.success)
|
||||
s.success = True
|
||||
else:
|
||||
s.success = False
|
||||
torch.cuda.synchronize()
|
||||
|
||||
|
||||
def _perf_triton_fn(*, device, dtype, batch_size, k_heads, n_groups, seq_len, d_head, causal):
|
||||
"""
|
||||
Get a partial function to test performance of our implementation
|
||||
"""
|
||||
q = torch.randn((batch_size, k_heads * n_groups, seq_len, d_head), dtype=dtype, device=device, requires_grad=True)
|
||||
k = torch.randn((batch_size, k_heads, seq_len, d_head), dtype=dtype, device=device, requires_grad=True)
|
||||
v = torch.randn((batch_size, k_heads, seq_len, d_head), dtype=dtype, device=device, requires_grad=True)
|
||||
sm_scale = d_head ** -0.5
|
||||
return lambda: attention(q, k, v, causal, sm_scale)
|
||||
|
||||
|
||||
def _perf_flash(*, batch_size, k_heads, n_groups, seq_len, d_head, causal, device, dtype):
|
||||
"""
|
||||
Get a partial function to test performance of original flash implementation
|
||||
"""
|
||||
q = torch.randn((batch_size, seq_len, k_heads * n_groups, d_head), dtype=dtype, device=device, requires_grad=True)
|
||||
k = torch.randn((batch_size, seq_len, k_heads, d_head), dtype=dtype, device=device, requires_grad=True)
|
||||
v = torch.randn((batch_size, seq_len, k_heads, d_head), dtype=dtype, device=device, requires_grad=True)
|
||||
from flash_attn import flash_attn_func
|
||||
return lambda: flash_attn_func(q, k, v, causal=causal)
|
||||
|
||||
|
||||
def measure_performance(name, fn, *, batch_size, k_heads, n_groups, seq_len, d_head, causal, is_bwd: bool):
|
||||
"""
|
||||
### Measure the speed
|
||||
"""
|
||||
if is_bwd:
|
||||
o = fn()
|
||||
do = torch.randn_like(o)
|
||||
fn = lambda: o.backward(do, retain_graph=True)
|
||||
ms = triton.testing.do_bench(fn)
|
||||
|
||||
flops_per_matmul = 2.0 * batch_size * k_heads * n_groups * seq_len * seq_len * d_head
|
||||
total_flops = 2 * flops_per_matmul
|
||||
if causal:
|
||||
total_flops *= 0.5
|
||||
if is_bwd:
|
||||
total_flops *= 2.5 # 2.0(bwd) + 0.5(recompute)
|
||||
|
||||
tf_ps = total_flops * 1e-12 / (ms * 1e-3)
|
||||
logger.log((f'{name}', logger.Text.key), ': ', f'{ms :,.1f}ms', ' ', f'{tf_ps :,.2f}TFps')
|
||||
|
||||
|
||||
def main():
|
||||
device = torch.device('cuda:0')
|
||||
torch.cuda.set_device(device)
|
||||
|
||||
dtype = torch.float16
|
||||
|
||||
# only works on post-Ampere GPUs right now
|
||||
test_fwd_bwd(1, 4, 1, 2048, 2048, 128, True, dtype=dtype, device=device)
|
||||
test_fwd_bwd(16, 32, 8, 2001, 4001, 128, False, dtype=dtype, device=device)
|
||||
test_fwd_bwd(4, 32, 8, 2048, 1024, 128, False, dtype=dtype, device=device)
|
||||
test_fwd_bwd(4, 32, 8, 2001, 4001, 128, True, dtype=dtype, device=device)
|
||||
|
||||
_conf = {
|
||||
'batch_size': 16,
|
||||
'k_heads': 8,
|
||||
'n_groups': 4,
|
||||
'seq_len': 2048,
|
||||
'd_head': 128,
|
||||
}
|
||||
|
||||
for _causal in [False, True]:
|
||||
for is_bwd in [False, True]:
|
||||
logger.log(f'{"Causal" if _causal else "Non-causal"} {" Backward" if is_bwd else ""}', logger.Text.title)
|
||||
measure_performance(f'flash', _perf_flash(causal=_causal, device=device, dtype=dtype, **_conf),
|
||||
is_bwd=is_bwd,
|
||||
causal=_causal, **_conf)
|
||||
measure_performance(f'triton', _perf_triton_fn(causal=_causal, device=device, dtype=dtype, **_conf),
|
||||
is_bwd=is_bwd,
|
||||
causal=_causal, **_conf)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,91 @@
|
||||
"""
|
||||
---
|
||||
title: "FNet: Mixing Tokens with Fourier Transforms"
|
||||
summary: >
|
||||
This is an annotated implementation/tutorial of FNet in PyTorch.
|
||||
---
|
||||
|
||||
# FNet: Mixing Tokens with Fourier Transforms
|
||||
|
||||
This is a [PyTorch](https://pytorch.org) implementation of the paper
|
||||
[FNet: Mixing Tokens with Fourier Transforms](https://arxiv.org/abs/2105.03824).
|
||||
|
||||
This paper replaces the [self-attention layer](../mha.html) with two
|
||||
[Fourier transforms](https://en.wikipedia.org/wiki/Discrete_Fourier_transform) to
|
||||
*mix* tokens.
|
||||
This is a $7 \times$ more efficient than self-attention.
|
||||
The accuracy loss of using this over self-attention is about 92% for
|
||||
[BERT](https://paperswithcode.com/method/bert) on
|
||||
[GLUE benchmark](https://paperswithcode.com/dataset/glue).
|
||||
|
||||
## Mixing tokens with two Fourier transforms
|
||||
|
||||
We apply Fourier transform along the hidden dimension (embedding dimension)
|
||||
and then along the sequence dimension.
|
||||
|
||||
$$
|
||||
\mathcal{R}\big(\mathcal{F}_\text{seq} \big(\mathcal{F}_\text{hidden} (x) \big) \big)
|
||||
$$
|
||||
|
||||
where $x$ is the embedding input, $\mathcal{F}$ stands for the fourier transform and
|
||||
$\mathcal{R}$ stands for the real component in complex numbers.
|
||||
|
||||
This is very simple to implement on PyTorch - just 1 line of code.
|
||||
The paper suggests using a precomputed DFT matrix and doing matrix multiplication to get the
|
||||
Fourier transformation.
|
||||
|
||||
Here is [the training code](experiment.html) for using a FNet based model for classifying
|
||||
[AG News](https://paperswithcode.com/dataset/ag-news).
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
|
||||
class FNetMix(nn.Module):
|
||||
"""
|
||||
## FNet - Mix tokens
|
||||
|
||||
This module simply implements
|
||||
$$
|
||||
\mathcal{R}\big(\mathcal{F}_\text{seq} \big(\mathcal{F}_\text{hidden} (x) \big) \big)
|
||||
$$
|
||||
|
||||
The structure of this module is made similar to a [standard attention module](../mha.html) so that we can simply
|
||||
replace it.
|
||||
"""
|
||||
|
||||
def forward(self, query: torch.Tensor, key: torch.Tensor, value: torch.Tensor, mask: Optional[torch.Tensor] = None):
|
||||
"""
|
||||
The [normal attention module](../mha.html) can be fed with different token embeddings for
|
||||
$\text{query}$,$\text{key}$, and $\text{value}$ and a mask.
|
||||
|
||||
We follow the same function signature so that we can replace it directly.
|
||||
|
||||
For FNet mixing, $$x = \text{query} = \text{key} = \text{value}$$ and masking is not possible.
|
||||
Shape of `query` (and `key` and `value`) is `[seq_len, batch_size, d_model]`.
|
||||
"""
|
||||
|
||||
# $\text{query}$,$\text{key}$, and $\text{value}$ all should be equal to $x$ for token mixing
|
||||
assert query is key and key is value
|
||||
# Token mixing doesn't support masking. i.e. all tokens will see all other token embeddings.
|
||||
assert mask is None
|
||||
|
||||
# Assign to `x` for clarity
|
||||
x = query
|
||||
|
||||
# Apply the Fourier transform along the hidden (embedding) dimension
|
||||
# $$\mathcal{F}_\text{hidden} (x)$$
|
||||
#
|
||||
# The output of the Fourier transform is a tensor of
|
||||
# [complex numbers](https://pytorch.org/docs/stable/complex_numbers.html).
|
||||
fft_hidden = torch.fft.fft(x, dim=2)
|
||||
# Apply the Fourier transform along the sequence dimension
|
||||
# $$\mathcal{F}_\text{seq} \big(\mathcal{F}_\text{hidden} (x) \big)$$
|
||||
fft_seq = torch.fft.fft(fft_hidden, dim=0)
|
||||
|
||||
# Get the real component
|
||||
# $$\mathcal{R}\big(\mathcal{F}_\text{seq} \big(\mathcal{F}_\text{hidden} (x) \big) \big)$$
|
||||
return torch.real(fft_seq)
|
||||
@@ -0,0 +1,154 @@
|
||||
"""
|
||||
---
|
||||
title: FNet Experiment
|
||||
summary: This experiment trains a FNet based model on AG News dataset.
|
||||
---
|
||||
|
||||
# [FNet](index.html) Experiment
|
||||
|
||||
This is an annotated PyTorch experiment to train a [FNet model](index.html).
|
||||
|
||||
This is based on
|
||||
[general training loop and configurations for AG News classification task](../../experiments/nlp_classification.html).
|
||||
"""
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
from labml import experiment
|
||||
from labml.configs import option
|
||||
from labml_nn.experiments.nlp_classification import NLPClassificationConfigs
|
||||
from labml_nn.transformers import Encoder
|
||||
from labml_nn.transformers import TransformerConfigs
|
||||
|
||||
|
||||
class TransformerClassifier(nn.Module):
|
||||
"""
|
||||
# Transformer based classifier model
|
||||
"""
|
||||
def __init__(self, encoder: Encoder, src_embed: nn.Module, generator: nn.Linear):
|
||||
"""
|
||||
* `encoder` is the transformer [Encoder](../models.html#Encoder)
|
||||
* `src_embed` is the token
|
||||
[embedding module (with positional encodings)](../models.html#EmbeddingsWithLearnedPositionalEncoding)
|
||||
* `generator` is the [final fully connected layer](../models.html#Generator) that gives the logits.
|
||||
"""
|
||||
super().__init__()
|
||||
self.src_embed = src_embed
|
||||
self.encoder = encoder
|
||||
self.generator = generator
|
||||
|
||||
def forward(self, x: torch.Tensor):
|
||||
# Get the token embeddings with positional encodings
|
||||
x = self.src_embed(x)
|
||||
# Transformer encoder
|
||||
x = self.encoder(x, None)
|
||||
# Get logits for classification.
|
||||
#
|
||||
# We set the `[CLS]` token at the last position of the sequence.
|
||||
# This is extracted by `x[-1]`, where `x` is of
|
||||
# shape `[seq_len, batch_size, d_model]`
|
||||
x = self.generator(x[-1])
|
||||
|
||||
# Return results
|
||||
# (second value is for state, since our trainer is used with RNNs also)
|
||||
return x, None
|
||||
|
||||
|
||||
class Configs(NLPClassificationConfigs):
|
||||
"""
|
||||
## Configurations
|
||||
|
||||
This inherits from
|
||||
[`NLPClassificationConfigs`](../../experiments/nlp_classification.html)
|
||||
"""
|
||||
|
||||
# Classification model
|
||||
model: TransformerClassifier
|
||||
# Transformer
|
||||
transformer: TransformerConfigs
|
||||
|
||||
|
||||
@option(Configs.transformer)
|
||||
def _transformer_configs(c: Configs):
|
||||
"""
|
||||
### Transformer configurations
|
||||
"""
|
||||
|
||||
# We use our
|
||||
# [configurable transformer implementation](../configs.html#TransformerConfigs)
|
||||
conf = TransformerConfigs()
|
||||
# Set the vocabulary sizes for embeddings and generating logits
|
||||
conf.n_src_vocab = c.n_tokens
|
||||
conf.n_tgt_vocab = c.n_tokens
|
||||
|
||||
#
|
||||
return conf
|
||||
|
||||
|
||||
@option(TransformerConfigs.encoder_attn)
|
||||
def fnet_mix():
|
||||
"""
|
||||
Create `FNetMix` module that can replace the self-attention in
|
||||
[transformer encoder layer](../models.html#TransformerLayer)
|
||||
.
|
||||
"""
|
||||
from labml_nn.transformers.fnet import FNetMix
|
||||
return FNetMix()
|
||||
|
||||
|
||||
@option(Configs.model)
|
||||
def _model(c: Configs):
|
||||
"""
|
||||
Create classification model
|
||||
"""
|
||||
m = TransformerClassifier(c.transformer.encoder,
|
||||
c.transformer.src_embed,
|
||||
nn.Linear(c.d_model, c.n_classes)).to(c.device)
|
||||
|
||||
return m
|
||||
|
||||
|
||||
def main():
|
||||
# Create experiment
|
||||
experiment.create(name="fnet")
|
||||
# Create configs
|
||||
conf = Configs()
|
||||
# Override configurations
|
||||
experiment.configs(conf, {
|
||||
# Use world level tokenizer
|
||||
'tokenizer': 'basic_english',
|
||||
|
||||
# Train for $32$ epochs
|
||||
'epochs': 32,
|
||||
# Switch between training and validation for $10$ times
|
||||
# per epoch
|
||||
'inner_iterations': 10,
|
||||
|
||||
# Transformer configurations (same as defaults)
|
||||
'transformer.d_model': 512,
|
||||
'transformer.ffn.d_ff': 2048,
|
||||
'transformer.n_heads': 8,
|
||||
'transformer.n_layers': 6,
|
||||
|
||||
# Use [FNet](index.html) instead of self-a
|
||||
# ttention
|
||||
'transformer.encoder_attn': 'fnet_mix',
|
||||
|
||||
# Use [Noam optimizer](../../optimizers/noam.html)
|
||||
'optimizer.optimizer': 'Noam',
|
||||
'optimizer.learning_rate': 1.,
|
||||
})
|
||||
|
||||
# Set models for saving and loading
|
||||
experiment.add_pytorch_models({'model': conf.model})
|
||||
|
||||
# Start the experiment
|
||||
with experiment.start():
|
||||
# Run training
|
||||
conf.run()
|
||||
|
||||
|
||||
#
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,12 @@
|
||||
# [FNet: Mixing Tokens with Fourier Transforms](https://nn.labml.ai/transformers/fnet/index.html)
|
||||
|
||||
This is a [PyTorch](https://pytorch.org) implementation of the paper
|
||||
[FNet: Mixing Tokens with Fourier Transforms](https://arxiv.org/abs/2105.03824).
|
||||
|
||||
This paper replaces the [self-attention layer](https://nn.labml.ai/transformers//mha.html) with two
|
||||
[Fourier transforms](https://en.wikipedia.org/wiki/Discrete_Fourier_transform) to
|
||||
*mix* tokens.
|
||||
This is a 7X more efficient than self-attention.
|
||||
The accuracy loss of using this over self-attention is about 92% for
|
||||
[BERT](https://paperswithcode.com/method/bert) on
|
||||
[GLUE benchmark](https://paperswithcode.com/dataset/glue).
|
||||
@@ -0,0 +1,13 @@
|
||||
"""
|
||||
---
|
||||
title: Gated Linear Units and Variants
|
||||
summary: >
|
||||
Train an auto-regressive transformer with Gated Linear Units and variants
|
||||
for the position-wise feedforward network (FFN).
|
||||
---
|
||||
|
||||
# Gated Linear Units and Variants
|
||||
|
||||
* [Experiment that uses `labml.configs`](experiment.html)
|
||||
* [Simpler version from scratch](simple.html)
|
||||
"""
|
||||
@@ -0,0 +1,132 @@
|
||||
"""
|
||||
---
|
||||
title: Gated Linear Units and Variants
|
||||
summary: >
|
||||
Train an auto-regressive transformer with Gated Linear Units and variants
|
||||
for the position-wise feedforward network (FFN).
|
||||
---
|
||||
|
||||
# Gated Linear Units and Variants
|
||||
|
||||
This trains a simple [transformer](../../) model for auto-regression.
|
||||
We try different variants for the [position-wise feedforward network](../feed_forward).
|
||||
The reusable & configurable are defined in [`configs.py`](configs.html).
|
||||
"""
|
||||
|
||||
import torch
|
||||
from labml import experiment
|
||||
from labml.configs import option
|
||||
from labml.utils.pytorch import get_modules
|
||||
from labml_nn.experiments.nlp_autoregression import NLPAutoRegressionConfigs
|
||||
from labml_nn.transformers import Encoder, Generator, TransformerConfigs
|
||||
from labml_nn.transformers.utils import subsequent_mask
|
||||
from torch import nn
|
||||
|
||||
|
||||
class AutoregressiveModel(nn.Module):
|
||||
"""
|
||||
## Auto regressive model
|
||||
"""
|
||||
|
||||
def __init__(self, src_embed: nn.Module, encoder: Encoder, generator: Generator):
|
||||
super().__init__()
|
||||
# Token embedding module
|
||||
self.src_embed = src_embed
|
||||
# Transformer based encoder
|
||||
self.encoder = encoder
|
||||
# Next token generation layer;
|
||||
# this give logits of the the next token
|
||||
self.generator = generator
|
||||
# This will be initialized on the first call
|
||||
self.src_mask = None
|
||||
|
||||
def forward(self, src: torch.Tensor):
|
||||
# Create subsequent mask, so that the transformer can only pay attention to past tokens.
|
||||
if self.src_mask is None or self.src_mask.size(0) != len(src):
|
||||
self.src_mask = subsequent_mask(len(src)).to(src.device)
|
||||
# Embed the tokens (`src`) and run it through the the transformer
|
||||
res = self.encoder(self.src_embed(src), self.src_mask)
|
||||
# Generate logits of the next token
|
||||
return self.generator(res), None
|
||||
|
||||
|
||||
class Configs(NLPAutoRegressionConfigs):
|
||||
"""
|
||||
## Configurations
|
||||
|
||||
The default configs can and will be over-ridden when we start the experiment
|
||||
"""
|
||||
|
||||
transformer: TransformerConfigs
|
||||
model: AutoregressiveModel
|
||||
|
||||
|
||||
@option(Configs.model)
|
||||
def autoregressive_model(c: Configs):
|
||||
"""
|
||||
Initialize the auto-regressive model
|
||||
"""
|
||||
m = AutoregressiveModel(c.transformer.src_embed, c.transformer.encoder, c.transformer.generator)
|
||||
return m.to(c.device)
|
||||
|
||||
|
||||
@option(Configs.transformer)
|
||||
def transformer_c(c: Configs):
|
||||
"""
|
||||
Initialize the [configurable transformer](../configs.html) encoder for our autoregressive model.
|
||||
"""
|
||||
tc = TransformerConfigs()
|
||||
tc.n_src_vocab = c.n_tokens
|
||||
tc.n_tgt_vocab = c.n_tokens
|
||||
|
||||
return tc
|
||||
|
||||
|
||||
def main():
|
||||
# Create experiment
|
||||
experiment.create(name="glu_variants")
|
||||
# Create configs
|
||||
conf = Configs()
|
||||
# Load configurations
|
||||
experiment.configs(conf,
|
||||
# A dictionary of configurations to override
|
||||
{'tokenizer': 'character',
|
||||
'prompt_separator': '',
|
||||
'prompt': 'It is ',
|
||||
'text': 'tiny_shakespeare',
|
||||
|
||||
'optimizer.optimizer': 'Noam',
|
||||
'optimizer.learning_rate': 1.,
|
||||
'optimizer.d_model': 256,
|
||||
|
||||
'seq_len': 1024,
|
||||
'epochs': 128,
|
||||
'batch_size': 6,
|
||||
'inner_iterations': 10,
|
||||
|
||||
# GLU Variant, one of GLU, Bilinear, ReGLU, GEGLU, SwiGLU
|
||||
#
|
||||
# These are defined in the [configurable FFN](../configs.html#FFN)
|
||||
# implementation
|
||||
'transformer.ffn.glu_variant': 'Bilinear',
|
||||
|
||||
# Transformer configurations
|
||||
'transformer.d_model': 256,
|
||||
'transformer.ffn.d_ff': 1024,
|
||||
'transformer.n_heads': 8,
|
||||
'transformer.n_layers': 6})
|
||||
|
||||
# This is needed to initialize models
|
||||
conf.n_tokens = conf.text.n_tokens
|
||||
|
||||
# Set models for saving and loading
|
||||
experiment.add_pytorch_models(get_modules(conf))
|
||||
|
||||
# Start the experiment
|
||||
with experiment.start():
|
||||
# `TrainValidConfigs.run`
|
||||
conf.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,216 @@
|
||||
{
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 0,
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"name": "Gated Linear Units and Variants",
|
||||
"provenance": [],
|
||||
"collapsed_sections": [],
|
||||
"toc_visible": true
|
||||
},
|
||||
"kernelspec": {
|
||||
"name": "python3",
|
||||
"display_name": "Python 3"
|
||||
},
|
||||
"accelerator": "GPU"
|
||||
},
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "AYV_dMVDxyc2"
|
||||
},
|
||||
"source": [
|
||||
"[](https://github.com/labmlai/annotated_deep_learning_paper_implementations)\n",
|
||||
"[](https://colab.research.google.com/github/labmlai/annotated_deep_learning_paper_implementations/blob/master/labml_nn/transformers/glu_variants/simple.ipynb) \n",
|
||||
"\n",
|
||||
"## Gated Linear Units and Variants\n",
|
||||
"\n",
|
||||
"This trains a simple [transformer](https://nn.labml.ai/transformers/) model for auto-regression.\n",
|
||||
"We try different variants for the [position-wise feedforward network](https://nn.labml.ai/transformers/feed_forward.html).\n",
|
||||
"\n",
|
||||
"Annotated trainer code is at [`simple.py`](https://nn.labml.ai/transformers/glu_variants/simple.html)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "AahG_i2y5tY9"
|
||||
},
|
||||
"source": [
|
||||
"Install the `labml-nn` package"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"id": "ZCzmCrAIVg0L",
|
||||
"colab": {
|
||||
"base_uri": "https://localhost:8080/"
|
||||
},
|
||||
"outputId": "2de76edb-9911-496d-9f8c-281dad6f5680"
|
||||
},
|
||||
"source": [
|
||||
"!pip install labml-nn"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "SE2VUQ6L5zxI"
|
||||
},
|
||||
"source": [
|
||||
"Imports"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"id": "0hJXx_g0wS2C"
|
||||
},
|
||||
"source": [
|
||||
"import dataclasses\n",
|
||||
"\n",
|
||||
"import torch\n",
|
||||
"import torch.nn as nn\n",
|
||||
"from labml import experiment\n",
|
||||
"from labml_nn.transformers.glu_variants.simple import Configs, Trainer"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "Lpggo0wM6qb-"
|
||||
},
|
||||
"source": [
|
||||
"Create an experiment"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"id": "bFcr9k-l4cAg"
|
||||
},
|
||||
"source": [
|
||||
"experiment.create(name=\"glu_variants\")"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "-OnHLi626tJt"
|
||||
},
|
||||
"source": [
|
||||
"Initialize configurations"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"id": "Piz0c5f44hRo"
|
||||
},
|
||||
"source": [
|
||||
"conf = Configs()"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "wwMzCqpD6vkL"
|
||||
},
|
||||
"source": [
|
||||
"Set experiment configurations and assign a configurations dictionary to override configurations"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"base_uri": "https://localhost:8080/",
|
||||
"height": 17
|
||||
},
|
||||
"id": "e6hmQhTw4nks",
|
||||
"outputId": "77eca625-7205-49ea-f275-23f2710c4d84"
|
||||
},
|
||||
"source": [
|
||||
"experiment.configs(dataclasses.asdict(conf))"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "DHyNvXfnzeWQ"
|
||||
},
|
||||
"source": [
|
||||
"Create [`Trainer`](https://nn.labml.ai/transformers/glu_variants/simple.html)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"id": "59ZeTv5SzcVe"
|
||||
},
|
||||
"source": [
|
||||
"trainer = Trainer(conf)"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "EvI7MtgJ61w5"
|
||||
},
|
||||
"source": [
|
||||
"Set PyTorch models for loading and saving"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"id": "GDlt7dp-5ALt"
|
||||
},
|
||||
"source": [
|
||||
"experiment.add_pytorch_models({'model': trainer.model})"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "KJZRf8527GxL"
|
||||
},
|
||||
"source": [
|
||||
"Start the experiment and run the training loop."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"base_uri": "https://localhost:8080/",
|
||||
"height": 255
|
||||
},
|
||||
"id": "aIAWo7Fw5DR8",
|
||||
"outputId": "18b8b334-f9e7-458b-f900-5828b4f9a5c8"
|
||||
},
|
||||
"source": [
|
||||
"with experiment.start():\n",
|
||||
" trainer.train()"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
"""
|
||||
---
|
||||
title: Gated Linear Units and Variants
|
||||
summary: >
|
||||
Train an auto-regressive transformer with Gated Linear Units and variants
|
||||
for the position-wise feedforward network (FFN).
|
||||
---
|
||||
|
||||
# Gated Linear Units and Variants
|
||||
|
||||
This trains a simple [transformer](../../) model for auto-regression.
|
||||
We try different variants for the [position-wise feedforward network](../feed_forward).
|
||||
|
||||
*This is a simpler implementation that doesn't use [`labml.configs`](experiment.html) module.
|
||||
We decided to write a simpler implementation to make it easier for readers who are not familiar.*
|
||||
|
||||
[](https://colab.research.google.com/github/labmlai/annotated_deep_learning_paper_implementations/blob/master/labml_nn/transformers/glu_variants/simple.ipynb)
|
||||
"""
|
||||
import dataclasses
|
||||
|
||||
import torch
|
||||
from labml import experiment, lab, tracker, monit, logger
|
||||
from labml.logger import Text
|
||||
from labml.utils.download import download_file
|
||||
from labml_nn.experiments.nlp_autoregression import transpose_batch
|
||||
from labml_nn.optimizers.noam import Noam
|
||||
from labml_nn.transformers import Encoder, MultiHeadAttention
|
||||
from labml_nn.transformers.feed_forward import FeedForward
|
||||
from labml_nn.transformers.models import EmbeddingsWithPositionalEncoding, TransformerLayer
|
||||
from labml_nn.transformers.utils import subsequent_mask
|
||||
from torch import nn
|
||||
from torch.utils.data import Dataset, DataLoader
|
||||
|
||||
|
||||
class AutoregressiveModel(nn.Module):
|
||||
"""
|
||||
## Auto regressive model
|
||||
"""
|
||||
|
||||
def __init__(self, src_embed: nn.Module, encoder: Encoder, generator: nn.Module):
|
||||
super().__init__()
|
||||
# Token embedding module
|
||||
self.src_embed = src_embed
|
||||
# Transformer based encoder
|
||||
self.encoder = encoder
|
||||
# Next token generation layer;
|
||||
# this gives logits of the the next token
|
||||
self.generator = generator
|
||||
# This will be initialized on the first call
|
||||
self.src_mask = None
|
||||
|
||||
def forward(self, src: torch.Tensor):
|
||||
# Create subsequent mask, so that the transformer can only pay attention to past tokens.
|
||||
if self.src_mask is None or self.src_mask.size(0) != len(src):
|
||||
self.src_mask = subsequent_mask(len(src)).to(src.device)
|
||||
# Embed the tokens (`src`) and run it through the the transformer
|
||||
res = self.encoder(self.src_embed(src), self.src_mask)
|
||||
# Generate logits of the next token
|
||||
return self.generator(res)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Configs:
|
||||
"""
|
||||
### Configurations
|
||||
"""
|
||||
d_model: int = 512
|
||||
seq_len: int = 128
|
||||
batch_size: int = 32
|
||||
n_layers: int = 6
|
||||
n_heads: int = 8
|
||||
dropout: float = 0.1
|
||||
d_ff: int = 2048
|
||||
glu_variant: str = 'GLU'
|
||||
epochs: int = 5
|
||||
grad_norm_clip: float = 0.5
|
||||
|
||||
|
||||
class TinyShakespeareDataset(Dataset):
|
||||
"""
|
||||
### Tiny Shakespeare Dataset
|
||||
"""
|
||||
|
||||
def __init__(self, seq_len: int):
|
||||
# Location of the text file
|
||||
path = lab.get_data_path() / 'tiny_shakespeare.txt'
|
||||
# Download the file
|
||||
download_file('https://raw.githubusercontent.com/karpathy/char-rnn/master/data/tinyshakespeare/input.txt', path)
|
||||
# Read the downloaded file
|
||||
with open(str(path), 'r') as f:
|
||||
text = f.read()
|
||||
|
||||
# Extract the characters
|
||||
chars = list(set(text))
|
||||
# Character to id (integer) map
|
||||
self.stoi = {c: i for i, c in enumerate(chars)}
|
||||
# Id to character map
|
||||
self.itos = {i: c for i, c in enumerate(chars)}
|
||||
# Length of a training sample
|
||||
self.seq_len = seq_len
|
||||
# Data in the form of a tensor of ids
|
||||
self.data = self.text_to_i(text)
|
||||
|
||||
def text_to_i(self, text: str):
|
||||
"""
|
||||
Transform the text into a tensor of ids
|
||||
"""
|
||||
return torch.tensor([self.stoi[c] for c in text], dtype=torch.long)
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Number of samples in the dataset.
|
||||
|
||||
*This will read the dataset `seq_len` times in a single epoch.*
|
||||
"""
|
||||
return len(self.data) - self.seq_len - 1
|
||||
|
||||
def __getitem__(self, idx):
|
||||
"""
|
||||
Return a sample
|
||||
"""
|
||||
return self.data[idx:idx + self.seq_len], self.data[idx + 1:idx + self.seq_len + 1]
|
||||
|
||||
|
||||
class Trainer:
|
||||
"""
|
||||
## Trainer
|
||||
"""
|
||||
|
||||
def __init__(self, configs: Configs):
|
||||
# Get the device
|
||||
self.device = torch.device('cpu')
|
||||
if torch.cuda.is_available():
|
||||
self.device = torch.device('cuda:0')
|
||||
# Initialize the dataset
|
||||
self.dataset = TinyShakespeareDataset(configs.seq_len)
|
||||
# Initialize the dataloader
|
||||
self.dataloader = DataLoader(self.dataset,
|
||||
batch_size=configs.batch_size,
|
||||
collate_fn=transpose_batch,
|
||||
shuffle=True)
|
||||
|
||||
# FFN with Gated Linear Unit
|
||||
# $$FFN_{GLU}(x)(x, W_1, V, W_2) = (\sigma(x W_1) \otimes x V) W_2$$
|
||||
if configs.glu_variant == 'GLU':
|
||||
ffn = FeedForward(configs.d_model, configs.d_ff, configs.dropout, nn.Sigmoid(), True, False, False, False)
|
||||
# FFN with Bilinear hidden layer
|
||||
# $$FFN_{Bilinear}(x)(x, W_1, V, W_2) = (x W_1 \otimes x V) W_2$$
|
||||
elif configs.glu_variant == 'Bilinear':
|
||||
ffn = FeedForward(configs.d_model, configs.d_ff, configs.dropout, nn.Identity(), True, False, False, False)
|
||||
# FFN with ReLU gate
|
||||
# $$FFN_{ReGLU}(x)(x, W_1, V, W_2) = (\max(0, x W_1) \otimes x V) W_2$$
|
||||
elif configs.glu_variant == 'ReGLU':
|
||||
ffn = FeedForward(configs.d_model, configs.d_ff, configs.dropout, nn.ReLU(), True, False, False, False)
|
||||
# FFN with GELU gate
|
||||
# $$FFN_{GEGLU}(x)(x, W_1, V, W_2) = (\text{GELU}(x W_1) \otimes x V) W_2$$
|
||||
elif configs.glu_variant == 'GEGLU':
|
||||
ffn = FeedForward(configs.d_model, configs.d_ff, configs.dropout, nn.GELU(), True, False, False, False)
|
||||
# FFN with Swish gate
|
||||
# $$FFN_{SwiGLU}(x)(x, W_1, V, W_2) = (\text{Swish}_1(x W_1) \otimes x V) W_2$$
|
||||
# where $\text{Swish}_\beta(x) = x \sigma(\beta x)$
|
||||
elif configs.glu_variant == 'SwiGLU':
|
||||
ffn = FeedForward(configs.d_model, configs.d_ff, configs.dropout, nn.SiLU(), True, False, False, False)
|
||||
# FFN with ReLU activation
|
||||
# $$FFN_{ReLU}(x)(x, W_1, W_2, b_1, b_2) = \text{ReLU}_1(x W_1 + b_1) W_2 + b_2$$
|
||||
elif configs.glu_variant == 'ReLU':
|
||||
ffn = FeedForward(configs.d_model, configs.d_ff, configs.dropout, nn.ReLU())
|
||||
# FFN with ReLU activation
|
||||
# $$FFN_{GELU}(x)(x, W_1, W_2, b_1, b_2) = \text{GELU}_1(x W_1 + b_1) W_2 + b_2$$
|
||||
elif configs.glu_variant == 'GELU':
|
||||
ffn = FeedForward(configs.d_model, configs.d_ff, configs.dropout, nn.GELU())
|
||||
else:
|
||||
raise ValueError(f'Unknown variant {configs.glu_variant}')
|
||||
|
||||
# Number of different characters
|
||||
n_chars = len(self.dataset.stoi)
|
||||
|
||||
# Initialize [Multi-Head Attention module](../mha.html)
|
||||
mha = MultiHeadAttention(configs.n_heads, configs.d_model, configs.dropout)
|
||||
# Initialize the [Transformer Block](../models.html#TransformerLayer)
|
||||
transformer_layer = TransformerLayer(d_model=configs.d_model, self_attn=mha, src_attn=None,
|
||||
feed_forward=ffn, dropout_prob=configs.dropout)
|
||||
# Initialize the model with an
|
||||
# [embedding layer](../models.html#EmbeddingsWithPositionalEncoding)
|
||||
# (with fixed positional encoding)
|
||||
# [transformer encoder](../models.html#Encoder) and
|
||||
# a linear layer to generate logits.
|
||||
self.model = AutoregressiveModel(EmbeddingsWithPositionalEncoding(configs.d_model, n_chars),
|
||||
Encoder(transformer_layer, configs.n_layers),
|
||||
nn.Linear(configs.d_model, n_chars))
|
||||
|
||||
# Move the model to the current device
|
||||
self.model.to(self.device)
|
||||
|
||||
# Initialize [Noam optimizer](../../optimizers/noam.html)
|
||||
self.optimizer = Noam(self.model.parameters(), lr=1.0, warmup=2_000, d_model=configs.d_model)
|
||||
|
||||
# Cross-entropy loss
|
||||
self.loss_func = nn.CrossEntropyLoss()
|
||||
# Number of training epochs;
|
||||
# *note that our dataset definition repeats the data `seq_len` times in a single epoch*
|
||||
self.epochs = configs.epochs
|
||||
# Gradient clipping norm
|
||||
self.grad_norm_clip = configs.grad_norm_clip
|
||||
|
||||
# Set tracker configurations
|
||||
tracker.set_scalar("loss.*", True)
|
||||
|
||||
def sample(self):
|
||||
"""
|
||||
### Sampling function to generate samples periodically while training
|
||||
"""
|
||||
|
||||
# Starting prompt
|
||||
prompt = 'It is'
|
||||
# Collect output for printing
|
||||
log = [(prompt, Text.subtle)]
|
||||
# Sample 25 tokens
|
||||
for i in monit.iterate('Sample', 25):
|
||||
# Tokenize the prompt
|
||||
data = self.dataset.text_to_i(prompt).unsqueeze(-1)
|
||||
data = data.to(self.device)
|
||||
# Get the model output
|
||||
output = self.model(data)
|
||||
# Get the model prediction (greedy)
|
||||
output = output.argmax(dim=-1).squeeze()
|
||||
# Add the prediction to prompt
|
||||
prompt += self.dataset.itos[output[-1].item()]
|
||||
# Add the prediction for logging
|
||||
log += [(self.dataset.itos[output[-1].item()], Text.value)]
|
||||
|
||||
# Print the sampled output
|
||||
logger.log(log)
|
||||
|
||||
def train(self):
|
||||
"""
|
||||
### Train the model
|
||||
"""
|
||||
|
||||
# Loop for the given number of epochs
|
||||
for _ in monit.loop(self.epochs):
|
||||
# Iterate over the minibatches
|
||||
for i, batch in monit.enum('Train', self.dataloader):
|
||||
# Move data to the device
|
||||
data, target = batch[0].to(self.device), batch[1].to(self.device)
|
||||
|
||||
# Set tracker step, as the number of characters trained on
|
||||
tracker.add_global_step(data.shape[0] * data.shape[1])
|
||||
|
||||
# Set model state to training
|
||||
self.model.train()
|
||||
# Evaluate the model
|
||||
output = self.model(data)
|
||||
|
||||
# Calculate loss
|
||||
loss = self.loss_func(output.view(-1, output.shape[-1]), target.view(-1))
|
||||
# Log the loss
|
||||
tracker.add("loss.train", loss)
|
||||
|
||||
# Calculate gradients
|
||||
loss.backward()
|
||||
# Clip gradients
|
||||
torch.nn.utils.clip_grad_norm_(self.model.parameters(), max_norm=self.grad_norm_clip)
|
||||
# Take optimizer step
|
||||
self.optimizer.step()
|
||||
# Log the model parameters and gradients
|
||||
if (i + 1) % 100 == 0:
|
||||
tracker.add('model', self.model)
|
||||
# Clear the gradients
|
||||
self.optimizer.zero_grad()
|
||||
|
||||
# Generate a sample
|
||||
if (i + 1) % 100 == 0:
|
||||
self.model.eval()
|
||||
with torch.no_grad():
|
||||
self.sample()
|
||||
|
||||
# Save the tracked metrics
|
||||
if (i + 1) % 10 == 0:
|
||||
tracker.save()
|
||||
|
||||
|
||||
def main():
|
||||
# Create experiment
|
||||
experiment.create(name="glu_variants")
|
||||
# Create configs
|
||||
configs = Configs()
|
||||
# Load configurations
|
||||
experiment.configs(dataclasses.asdict(configs))
|
||||
|
||||
# Create trainer
|
||||
trainer = Trainer(configs)
|
||||
# Set models for training and loading
|
||||
experiment.add_pytorch_models({'model': trainer.model})
|
||||
|
||||
# Start the experiment
|
||||
with experiment.start():
|
||||
# Train the model
|
||||
trainer.train()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,157 @@
|
||||
"""
|
||||
---
|
||||
title: Pay Attention to MLPs (gMLP)
|
||||
summary: >
|
||||
This is an annotated implementation/tutorial of Pay Attention to MLPs (gMLP) in PyTorch.
|
||||
---
|
||||
|
||||
# Pay Attention to MLPs (gMLP)
|
||||
|
||||
This is a [PyTorch](https://pytorch.org) implementation of the paper
|
||||
[Pay Attention to MLPs](https://arxiv.org/abs/2105.08050).
|
||||
|
||||
This paper introduces a Multilayer Perceptron (MLP) based architecture with gating,
|
||||
which they name **gMLP**. It consists of a stack of $L$ *gMLP* blocks.
|
||||
|
||||
Here is [the training code](experiment.html) for a gMLP model based autoregressive model.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
|
||||
class GMLPBlock(nn.Module):
|
||||
"""
|
||||
## gMLP Block
|
||||
|
||||
Each block does the following transformations to input embeddings
|
||||
$X \in \mathbb{R}^{n \times d}$ where $n$ is the sequence length
|
||||
and $d$ is the dimensionality of the embeddings:
|
||||
|
||||
\begin{align}
|
||||
Z &= \sigma(XU) \\
|
||||
\tilde{Z} &= s(Z) \\
|
||||
Y &= \tilde{Z}V \\
|
||||
\end{align}
|
||||
|
||||
where $V$ and $U$ are learnable projection weights.
|
||||
$s(\cdot)$ is the Spacial Gating Unit defined below.
|
||||
Output dimensionality of $s(\cdot)$ will be half of $Z$.
|
||||
$\sigma$ is an activation function such as
|
||||
[GeLU](https://pytorch.org/docs/stable/generated/torch.nn.GELU.html).
|
||||
"""
|
||||
|
||||
def __init__(self, d_model: int, d_ffn: int, seq_len: int):
|
||||
"""
|
||||
* `d_model` is the dimensionality ($d$) of $X$
|
||||
* `d_ffn` is the dimensionality of $Z$
|
||||
* `seq_len` is the length of the token sequence ($n$)
|
||||
"""
|
||||
super().__init__()
|
||||
# Normalization layer fro Pre-Norm
|
||||
self.norm = nn.LayerNorm([d_model])
|
||||
# Activation function $\sigma$
|
||||
self.activation = nn.GELU()
|
||||
# Projection layer for $Z = \sigma(XU)$
|
||||
self.proj1 = nn.Linear(d_model, d_ffn)
|
||||
# Spacial Gating Unit $s(\cdot)$
|
||||
self.sgu = SpacialGatingUnit(d_ffn, seq_len)
|
||||
# Projection layer for $Y = \tilde{Z}V$
|
||||
self.proj2 = nn.Linear(d_ffn // 2, d_model)
|
||||
# Embedding size (required by [Encoder](../models.html#Encoder).
|
||||
# We use the encoder module from transformer architecture and plug
|
||||
# *gMLP* block as a replacement for the [Transformer Layer](../models.html#Encoder).
|
||||
self.size = d_model
|
||||
|
||||
def forward(self, *, x: torch.Tensor, mask: Optional[torch.Tensor] = None):
|
||||
"""
|
||||
* `x` is the input embedding tensor $X$ of shape `[seq_len, batch_size, d_model]`
|
||||
* `mask` is a boolean mask of shape `[seq_len, seq_len, 1]` that controls the visibility of tokens
|
||||
among each other.
|
||||
"""
|
||||
# Keep a copy for shortcut connection
|
||||
shortcut = x
|
||||
# Normalize $X$
|
||||
x = self.norm(x)
|
||||
# Projection and activation $Z = \sigma(XU)$
|
||||
z = self.activation(self.proj1(x))
|
||||
# Spacial Gating Unit $\tilde{Z} = s(Z)$
|
||||
z = self.sgu(z, mask)
|
||||
# Final projection $Y = \tilde{Z}V$
|
||||
z = self.proj2(z)
|
||||
|
||||
# Add the shortcut connection
|
||||
return z + shortcut
|
||||
|
||||
|
||||
class SpacialGatingUnit(nn.Module):
|
||||
"""
|
||||
## Spatial Gating Unit
|
||||
|
||||
$$s(Z) = Z_1 \odot f_{W,b}(Z_2)$$
|
||||
|
||||
where $f_{W,b}(Z) = W Z + b$ is a linear transformation along the sequence dimension,
|
||||
and $\odot$ is element-wise multiplication.
|
||||
$Z$ is split into to parts of equal size $Z_1$ and $Z_2$ along the channel dimension (embedding dimension).
|
||||
"""
|
||||
def __init__(self, d_z: int, seq_len: int):
|
||||
"""
|
||||
* `d_z` is the dimensionality of $Z$
|
||||
* `seq_len` is the sequence length
|
||||
"""
|
||||
super().__init__()
|
||||
# Normalization layer before applying $f_{W,b}(\cdot)$
|
||||
self.norm = nn.LayerNorm([d_z // 2])
|
||||
# Weight $W$ in $f_{W,b}(\cdot)$.
|
||||
#
|
||||
# The paper notes that it's important to initialize weights to small values and the bias to $1$,
|
||||
# so that during the initial training $s(\cdot)$ is close to identity (apart from the split).
|
||||
self.weight = nn.Parameter(torch.zeros(seq_len, seq_len).uniform_(-0.01, 0.01), requires_grad=True)
|
||||
# Weight $b$ in $f_{W,b}(\cdot)$
|
||||
#
|
||||
# The paper notes that it's important to initialize bias to $1$.
|
||||
self.bias = nn.Parameter(torch.ones(seq_len), requires_grad=True)
|
||||
|
||||
def forward(self, z: torch.Tensor, mask: Optional[torch.Tensor] = None):
|
||||
"""
|
||||
* `z` is the input $Z$ of shape `[seq_len, batch_size, d_z]`
|
||||
* `mask` is is a boolean mask of shape `[seq_len, seq_len, 1]` that controls the visibility of tokens
|
||||
among each other. The last dimension of size `1` is the batch, which we have in other transformer
|
||||
implementations and was left for compatibility.
|
||||
"""
|
||||
|
||||
# Get sequence length
|
||||
seq_len = z.shape[0]
|
||||
# Split $Z$ into $Z_1$ and $Z_2$
|
||||
z1, z2 = torch.chunk(z, 2, dim=-1)
|
||||
|
||||
# Check mask
|
||||
if mask is not None:
|
||||
# `mask` has shape `[seq_len_q, seq_len_k, batch_size]`.
|
||||
# The batch dimension should be of size `1` because this implementation supports
|
||||
# only same mask for all samples in the batch.
|
||||
assert mask.shape[0] == 1 or mask.shape[0] == seq_len
|
||||
assert mask.shape[1] == seq_len
|
||||
# Here we only support the same mask for all samples
|
||||
assert mask.shape[2] == 1
|
||||
# Remove the batch dimension
|
||||
mask = mask[:, :, 0]
|
||||
|
||||
# Normalize $Z_2$ before $f_{W,b}(\cdot)$
|
||||
z2 = self.norm(z2)
|
||||
# Get the weight matrix; truncate if larger than `seq_len`
|
||||
weight = self.weight[:seq_len, :seq_len]
|
||||
# Apply mask to the weights.
|
||||
#
|
||||
# If $W_{i,j}$ is $0$ then $f_{W,b}(Z_2)_i$ will not get any information
|
||||
# from token $j$.
|
||||
if mask is not None:
|
||||
weight = weight * mask
|
||||
|
||||
# $f_{W,b}(Z_2) = W Z_2 + b$
|
||||
z2 = torch.einsum('ij,jbd->ibd', weight, z2) + self.bias[:seq_len, None, None]
|
||||
|
||||
# $Z_1 \odot f_{W,b}(Z_2)$
|
||||
return z1 * z2
|
||||
@@ -0,0 +1,113 @@
|
||||
"""
|
||||
---
|
||||
title: Pay Attention to MLPs (gMLP) Experiment
|
||||
summary: This experiment trains a gMLP based model on Tiny Shakespeare dataset.
|
||||
---
|
||||
|
||||
# [Pay Attention to MLPs (gMLP)](index.html) Experiment
|
||||
|
||||
This is an annotated PyTorch experiment to train a [gMLP model](index.html).
|
||||
The paper also applies a Stochastic Depth regularization where some layers are removed randomly during training.
|
||||
We have not implemented that here.
|
||||
|
||||
This is based on
|
||||
[training loop and configurations for a simple transformer auto-regressive NLP task](../basic/autoregressive_experiment.html).
|
||||
"""
|
||||
from labml import experiment
|
||||
from labml.configs import option
|
||||
from labml_nn.transformers import TransformerConfigs
|
||||
from labml_nn.transformers.basic.autoregressive_experiment import Configs as BasicAutoRegressionConfigs
|
||||
from labml_nn.transformers.gmlp import GMLPBlock
|
||||
|
||||
|
||||
class Configs(BasicAutoRegressionConfigs):
|
||||
"""
|
||||
## Configurations
|
||||
|
||||
This inherits from
|
||||
[training loop and configurations for a simple transformer auto-regressive NLP task](../basic/autoregressive_transformer.html).
|
||||
"""
|
||||
|
||||
# Transformer
|
||||
transformer: TransformerConfigs = 'gMLP'
|
||||
# gMLP Block
|
||||
gmlp: GMLPBlock
|
||||
# `d_ffn` for gMLP projection layer
|
||||
d_ffn: int = 2048
|
||||
|
||||
|
||||
@option(Configs.gmlp, 'gMLP')
|
||||
def _gmlp_configs(c: Configs):
|
||||
"""
|
||||
### Create a gMLP block
|
||||
"""
|
||||
return GMLPBlock(c.d_model, c.d_ffn, c.seq_len)
|
||||
|
||||
|
||||
@option(Configs.transformer, 'gMLP')
|
||||
def _transformer_configs(c: Configs):
|
||||
"""
|
||||
### Transformer configurations
|
||||
"""
|
||||
|
||||
# We use our
|
||||
# [configurable transformer implementation](../configs.html#TransformerConfigs)
|
||||
conf = TransformerConfigs()
|
||||
# Set the vocabulary sizes for embeddings and generating logits
|
||||
conf.n_src_vocab = c.n_tokens
|
||||
conf.n_tgt_vocab = c.n_tokens
|
||||
# Set model size
|
||||
conf.d_model = c.d_model
|
||||
# Replace the encoder layer with a gMLP layer
|
||||
conf.encoder_layer = c.gmlp
|
||||
|
||||
return conf
|
||||
|
||||
|
||||
def main():
|
||||
# Create experiment
|
||||
experiment.create(name="gMLP")
|
||||
# Create configs
|
||||
conf = Configs()
|
||||
# Override configurations
|
||||
experiment.configs(conf, {
|
||||
# Use character level tokenizer
|
||||
'tokenizer': 'character',
|
||||
# Prompt separator is blank
|
||||
'prompt_separator': '',
|
||||
# Starting prompt for sampling
|
||||
'prompt': 'It is ',
|
||||
# Use Tiny Shakespeare dataset
|
||||
'text': 'tiny_shakespeare',
|
||||
|
||||
# Use a context size of $256$
|
||||
'seq_len': 256,
|
||||
# Train for $128$ epochs
|
||||
'epochs': 128,
|
||||
# Batch size $32$
|
||||
'batch_size': 32,
|
||||
# Switch between training and validation for $10$ times
|
||||
# per epoch
|
||||
'inner_iterations': 10,
|
||||
|
||||
# Model size
|
||||
'd_model': 512,
|
||||
'd_ffn': 2048,
|
||||
|
||||
# Use [Noam optimizer](../../optimizers/noam.html)
|
||||
'optimizer.optimizer': 'Noam',
|
||||
'optimizer.learning_rate': 1.,
|
||||
})
|
||||
|
||||
# Set models for saving and loading
|
||||
experiment.add_pytorch_models({'model': conf.model})
|
||||
|
||||
# Start the experiment
|
||||
with experiment.start():
|
||||
# Run training
|
||||
conf.run()
|
||||
|
||||
|
||||
#
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,9 @@
|
||||
# [Pay Attention to MLPs (gMLP)](https://nn.labml.ai/transformers/gmlp/index.html)
|
||||
|
||||
This is a [PyTorch](https://pytorch.org) implementation of the paper
|
||||
[Pay Attention to MLPs](https://arxiv.org/abs/2105.08050).
|
||||
|
||||
This paper introduces a Multilayer Perceptron (MLP) based architecture with gating,
|
||||
which they name **gMLP**. It consists of a stack of $L$ *gMLP* blocks.
|
||||
|
||||
Here is [the training code](https://nn.labml.ai/transformers/gmlp/experiment.html) for a gMLP model based autoregressive model.
|
||||
@@ -0,0 +1,264 @@
|
||||
"""
|
||||
---
|
||||
title: GPT
|
||||
summary: >
|
||||
Implementation/tutorial of GPT model and training code.
|
||||
---
|
||||
|
||||
# GPT
|
||||
|
||||
This is a tutorial/implementation of
|
||||
[OpenAI GPT architecture](https://openai.com/blog/better-language-models/)
|
||||
in [PyTorch](https://pytorch.org).
|
||||
We got a bunch of implementation details from
|
||||
[minGPT](https://github.com/karpathy/minGPT)
|
||||
by [@karpathy](https://twitter.com/karpathy).
|
||||
This implementation also uses character tiny shakespeare dataset.
|
||||
|
||||
GPT model is essentially a standard transformer with a few tweaks.
|
||||
GPT-2 and especially GPT-3 models are quite large and won't fit on a
|
||||
single GPU and will need model parallelism.
|
||||
This implementation doesn't even use data parallelism and is intended to be
|
||||
more of a tutorial.
|
||||
|
||||
Main differences of this compared to a simple autoregressive transformer
|
||||
are the parameter initialization, weight decay, and learning rate schedule.
|
||||
For the transformer we reuse the
|
||||
[existing labml/nn transformer implementation](../transformers/index.html).
|
||||
|
||||
Here's a notebook for training a GPT model on Tiny Shakespeare dataset.
|
||||
|
||||
[](https://colab.research.google.com/github/labmlai/annotated_deep_learning_paper_implementations/blob/master/labml_nn/transformers/gpt/experiment.ipynb)
|
||||
"""
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
from labml import experiment
|
||||
from labml.configs import option
|
||||
from labml_nn.experiments.nlp_autoregression import NLPAutoRegressionConfigs
|
||||
from labml_nn.optimizers.configs import OptimizerConfigs
|
||||
from labml_nn.transformers import TransformerConfigs, Encoder
|
||||
from labml_nn.transformers.utils import subsequent_mask
|
||||
|
||||
|
||||
class GPT(nn.Module):
|
||||
"""
|
||||
## GPT model
|
||||
|
||||
This consists of a token embedding layer, transformer encoder, and
|
||||
a final linear layer that gives token logits.
|
||||
"""
|
||||
|
||||
def __init__(self, encoder: Encoder, src_embed: nn.Module, generator: nn.Module):
|
||||
"""
|
||||
* `encoder` is the transformer [Encoder](../models.html#Encoder)
|
||||
* `src_embed` is the token
|
||||
[embedding module (with positional encodings)](../models.html#EmbeddingsWithLearnedPositionalEncoding)
|
||||
* `generator` is the [final fully connected layer](../models.html#Generator) that gives the logits.
|
||||
"""
|
||||
super().__init__()
|
||||
self.src_embed = src_embed
|
||||
self.encoder = encoder
|
||||
self.generator = generator
|
||||
|
||||
# The mask will be initialized on the first call
|
||||
self.mask = None
|
||||
|
||||
def forward(self, x: torch.Tensor):
|
||||
# Create subsequent mask if mask is not initialized
|
||||
# or if the size of the mask is different
|
||||
if self.mask is None or self.mask.size(0) != len(x):
|
||||
# Subsequent mask, will mask out tokens from seeing future tokens
|
||||
self.mask = subsequent_mask(len(x)).to(x.device)
|
||||
# Get the token embeddings with positional encodings
|
||||
x = self.src_embed(x)
|
||||
# Transformer encoder
|
||||
x = self.encoder(x, self.mask)
|
||||
# Get logits
|
||||
x = self.generator(x)
|
||||
|
||||
# Return results
|
||||
# (second value is for state, since our trainer is used with RNNs also)
|
||||
return x, None
|
||||
|
||||
|
||||
class Configs(NLPAutoRegressionConfigs):
|
||||
"""
|
||||
## Configurations
|
||||
|
||||
This inherits from
|
||||
[`NLPAutoRegressionConfigs`](../../experiments/nlp_autoregression.html#NLPAutoRegressionConfigs)
|
||||
"""
|
||||
|
||||
# GPT model
|
||||
model: GPT
|
||||
# Transformer
|
||||
transformer: TransformerConfigs
|
||||
# Weight decay
|
||||
weight_decay: float = 0.1
|
||||
# Number of tokens for wamup
|
||||
warmup_steps: int = 128 * 128 * 20
|
||||
|
||||
# Custom optimizer
|
||||
optimizer = 'transformer_optimizer'
|
||||
|
||||
|
||||
@option(Configs.transformer, 'GPT')
|
||||
def _transformer_configs(c: Configs):
|
||||
"""
|
||||
### Transformer configurations
|
||||
"""
|
||||
|
||||
# We use our
|
||||
# [configurable transformer implementation](../configs.html#TransformerConfigs)
|
||||
conf = TransformerConfigs()
|
||||
# Set the vocabulary sizes for embeddings and generating logits
|
||||
conf.n_src_vocab = c.n_tokens
|
||||
conf.n_tgt_vocab = c.n_tokens
|
||||
# GPT uses GELU activation for position wise feedforward
|
||||
conf.ffn.activation = 'GELU'
|
||||
|
||||
#
|
||||
return conf
|
||||
|
||||
|
||||
def _init_weights(module):
|
||||
"""
|
||||
### Initialize weights
|
||||
|
||||
Weights of linear layers and embedding layers are initialized
|
||||
to $\mathcal{N}(0, 0.02)$
|
||||
instead of the default Xavier initialzation.
|
||||
"""
|
||||
|
||||
if not isinstance(module, (nn.Linear, nn.Embedding)):
|
||||
return
|
||||
|
||||
module.weight.data.normal_(mean=0.0, std=0.02)
|
||||
|
||||
# Initialize biases to $0$
|
||||
if isinstance(module, nn.Linear) and module.bias is not None:
|
||||
module.bias.data.zero_()
|
||||
|
||||
|
||||
@option(Configs.model)
|
||||
def _model(c: Configs):
|
||||
"""
|
||||
Create GPT model and initialize weights
|
||||
"""
|
||||
m = GPT(c.transformer.encoder,
|
||||
c.transformer.src_embed,
|
||||
c.transformer.generator).to(c.device)
|
||||
|
||||
# Apply custom weight initialization
|
||||
m.apply(_init_weights)
|
||||
|
||||
return m
|
||||
|
||||
|
||||
@option(NLPAutoRegressionConfigs.optimizer)
|
||||
def transformer_optimizer(c: NLPAutoRegressionConfigs):
|
||||
"""
|
||||
### Create custom optimizer with weight decay
|
||||
|
||||
This code is taken from [minGPT](https://github.com/karpathy/minGPT).
|
||||
This applies weight decay only to weights of linear layers.
|
||||
"""
|
||||
# Collect names of parameters to apply weight decay
|
||||
decay = set()
|
||||
for mn, m in c.model.named_modules():
|
||||
for pn, p in m.named_parameters():
|
||||
fpn = f'{mn}.{pn}' if mn else pn # full param name
|
||||
|
||||
if fpn.endswith('weight') and isinstance(m, nn.Linear):
|
||||
decay.add(fpn)
|
||||
|
||||
# Get all the parameters
|
||||
param_dict = {pn: p for pn, p in c.model.named_parameters()}
|
||||
# Parameters that are not decayed
|
||||
no_decay = set(param_dict.keys()) - decay
|
||||
|
||||
# create the pytorch optimizer object
|
||||
opt_groups = [
|
||||
{"params": [param_dict[pn] for pn in sorted(list(decay))], "weight_decay": c.weight_decay},
|
||||
{"params": [param_dict[pn] for pn in sorted(list(no_decay))], "weight_decay": 0.0},
|
||||
]
|
||||
|
||||
# Create a [configurable optimizer](../optimizers/configs.html#OptimizerConfigs),
|
||||
# so that we can change these simply by passing
|
||||
# a config dictionary.
|
||||
optimizer = OptimizerConfigs()
|
||||
|
||||
# Set parameter groups for optimization.
|
||||
optimizer.parameters = opt_groups
|
||||
# Use [cosine decay optimizer](../optimizers/adam_warmup_cosine_decay.html).
|
||||
# This is what GPT uses.
|
||||
optimizer.optimizer = 'AdamWarmupCosineDecay'
|
||||
# Set model embedding size, required if we use [Noam optimizer](../optimizers/noam.html)
|
||||
# which has an exponential decay.
|
||||
optimizer.d_model = c.d_model
|
||||
# Set default weight decay.
|
||||
# This is not required since we set the weight decay in the parameter groups.
|
||||
optimizer.weight_decay = c.weight_decay
|
||||
# GPT uses a maximum learning rate of $6 \times 10^{-4}$.
|
||||
optimizer.learning_rate = 6e-4
|
||||
# $\beta_1 = 0.9, \beta_2 = 0.95$
|
||||
optimizer.betas = (0.9, 0.95)
|
||||
# $\epsilon = 10^{-8}$
|
||||
optimizer.eps = 1e-8
|
||||
# Weight decay is decoupled from gradients
|
||||
optimizer.weight_decouple = True
|
||||
# Total number of optimization steps for learning rate cosine decay
|
||||
optimizer.total_steps = c.epochs * len(c.text.train) // (c.batch_size * c.seq_len)
|
||||
# Number of warmup optimization steps
|
||||
optimizer.warmup = c.warmup_steps // (c.batch_size * c.seq_len)
|
||||
|
||||
return optimizer
|
||||
|
||||
|
||||
def main():
|
||||
# Create experiment
|
||||
experiment.create(name="gpt")
|
||||
# Create configs
|
||||
conf = Configs()
|
||||
# Override configurations
|
||||
experiment.configs(conf, {
|
||||
# Use character level tokenizer
|
||||
'tokenizer': 'character',
|
||||
# Prompt separator is blank
|
||||
'prompt_separator': '',
|
||||
# Starting prompt for sampling
|
||||
'prompt': 'It is ',
|
||||
# Use Tiny Shakespeare dataset
|
||||
'text': 'tiny_shakespeare',
|
||||
|
||||
# Use a context size of $128$
|
||||
'seq_len': 128,
|
||||
# Train for $32$ epochs
|
||||
'epochs': 32,
|
||||
# Batch size $128$
|
||||
'batch_size': 128,
|
||||
# Switch between training and validation for $10$ times
|
||||
# per epoch
|
||||
'inner_iterations': 10,
|
||||
|
||||
# Transformer configurations
|
||||
'transformer.d_model': 512,
|
||||
'transformer.ffn.d_ff': 2048,
|
||||
'transformer.n_heads': 8,
|
||||
'transformer.n_layers': 6
|
||||
})
|
||||
|
||||
# Set models for saving and loading
|
||||
experiment.add_pytorch_models({'model': conf.model})
|
||||
|
||||
# Start the experiment
|
||||
with experiment.start():
|
||||
# Run training
|
||||
conf.run()
|
||||
|
||||
|
||||
#
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,230 @@
|
||||
{
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 0,
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"name": "GPT",
|
||||
"provenance": [],
|
||||
"collapsed_sections": []
|
||||
},
|
||||
"kernelspec": {
|
||||
"name": "python3",
|
||||
"display_name": "Python 3"
|
||||
},
|
||||
"accelerator": "GPU"
|
||||
},
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "AYV_dMVDxyc2"
|
||||
},
|
||||
"source": [
|
||||
"[](https://github.com/labmlai/annotated_deep_learning_paper_implementations)\n",
|
||||
"[](https://colab.research.google.com/github/labmlai/annotated_deep_learning_paper_implementations/blob/master/labml_nn/transformers/gpt/experiment.ipynb) \n",
|
||||
"\n",
|
||||
"## Training a model with GPT architecture\n",
|
||||
"\n",
|
||||
"This is an experiment training Tiny Shakespeare dataset with GPT architecture model."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "AahG_i2y5tY9"
|
||||
},
|
||||
"source": [
|
||||
"Install the `labml-nn` package"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"id": "ZCzmCrAIVg0L",
|
||||
"colab": {
|
||||
"base_uri": "https://localhost:8080/"
|
||||
},
|
||||
"outputId": "9c544df4-3fc7-4152-b50d-07919dbfb9de"
|
||||
},
|
||||
"source": [
|
||||
"!pip install labml-nn"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "SE2VUQ6L5zxI"
|
||||
},
|
||||
"source": [
|
||||
"Imports"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"id": "0hJXx_g0wS2C"
|
||||
},
|
||||
"source": [
|
||||
"from labml import experiment\n",
|
||||
"from labml_nn.transformers.gpt import Configs"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "Lpggo0wM6qb-"
|
||||
},
|
||||
"source": [
|
||||
"Create an experiment"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"id": "bFcr9k-l4cAg"
|
||||
},
|
||||
"source": [
|
||||
"experiment.create(name=\"gpt\")"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "-OnHLi626tJt"
|
||||
},
|
||||
"source": [
|
||||
"Initialize [GPT configurations](https://nn.labml.ai/transformers/gpt/)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"id": "Piz0c5f44hRo"
|
||||
},
|
||||
"source": [
|
||||
"conf = Configs()"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "wwMzCqpD6vkL"
|
||||
},
|
||||
"source": [
|
||||
"Set experiment configurations and assign a configurations dictionary to override configurations"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"base_uri": "https://localhost:8080/",
|
||||
"height": 17
|
||||
},
|
||||
"id": "e6hmQhTw4nks",
|
||||
"outputId": "018b39d7-7d84-4651-8b33-80de77d58ace"
|
||||
},
|
||||
"source": [
|
||||
"experiment.configs(conf, {\n",
|
||||
" # Use character level tokenizer\n",
|
||||
" 'tokenizer': 'character',\n",
|
||||
" # Prompt separator is blank\n",
|
||||
" 'prompt_separator': '',\n",
|
||||
" # Starting prompt for sampling\n",
|
||||
" 'prompt': 'It is ',\n",
|
||||
" # Use Tiny Shakespeare dataset\n",
|
||||
" 'text': 'tiny_shakespeare',\n",
|
||||
"\n",
|
||||
" # Use a context size of $128$\n",
|
||||
" 'seq_len': 128,\n",
|
||||
" # Train for $32$ epochs\n",
|
||||
" 'epochs': 32,\n",
|
||||
" # Batch size $128$\n",
|
||||
" 'batch_size': 128,\n",
|
||||
" # Switch between training and validation for $10$ times\n",
|
||||
" # per epoch\n",
|
||||
" 'inner_iterations': 10,\n",
|
||||
"\n",
|
||||
" # Transformer configurations\n",
|
||||
" 'transformer.d_model': 512,\n",
|
||||
" 'transformer.ffn.d_ff': 2048,\n",
|
||||
" 'transformer.n_heads': 8,\n",
|
||||
" 'transformer.n_layers': 6\n",
|
||||
"})"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "EvI7MtgJ61w5"
|
||||
},
|
||||
"source": [
|
||||
"Set PyTorch models for loading and saving"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"base_uri": "https://localhost:8080/",
|
||||
"height": 459
|
||||
},
|
||||
"id": "GDlt7dp-5ALt",
|
||||
"outputId": "e7768185-4a3c-4fec-f688-73c0463db015"
|
||||
},
|
||||
"source": [
|
||||
"experiment.add_pytorch_models({'model': conf.model})"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "KJZRf8527GxL"
|
||||
},
|
||||
"source": [
|
||||
"Start the experiment and run the training loop."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"base_uri": "https://localhost:8080/",
|
||||
"height": 1000
|
||||
},
|
||||
"id": "aIAWo7Fw5DR8",
|
||||
"outputId": "d4ca5ae0-6a97-439e-a318-010cb3f288cd"
|
||||
},
|
||||
"source": [
|
||||
"# Start the experiment\n",
|
||||
"with experiment.start():\n",
|
||||
" conf.run()"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"id": "oBXXlP2b7XZO"
|
||||
},
|
||||
"source": [
|
||||
""
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
"""
|
||||
---
|
||||
title: Hierarchical Transformers Are More Efficient Language Models
|
||||
summary: >
|
||||
This is an annotated implementation/tutorial of hourglass model in PyTorch.
|
||||
---
|
||||
|
||||
# Hierarchical Transformers Are More Efficient Language Models
|
||||
|
||||
This is a [PyTorch](https://pytorch.org) implementation of the paper
|
||||
[Hierarchical Transformers Are More Efficient Language Models](https://arxiv.org/abs/2110.13711).
|
||||
|
||||
This paper introduces a hierarchical transformer architecture to handle long sequences
|
||||
efficiently. The first half of the transformer layers down-sample tokens and the second
|
||||
half up-samples with direct skip connections between layers of the same resolution.
|
||||
This is a little similar to [U-Net](../../diffusion/ddpm/unet.html) for vision tasks.
|
||||
|
||||
They try different up-sampling and down-sampling techniques and build a model
|
||||
with the best performing up and down-sampling techniques which they call the
|
||||
hourglass model.
|
||||
|
||||
Here we have implemented the simplest up-sampling and down-sampling techniques for simplicity.
|
||||
We will consider adding more complex (and better performing) implementations later.
|
||||
|
||||
Here is [the training code](experiment.html) for the hourglass model.
|
||||
"""
|
||||
|
||||
from typing import List
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
from labml_nn.transformers import MultiHeadAttention, TransformerLayer
|
||||
from labml_nn.transformers.feed_forward import FeedForward
|
||||
from labml_nn.transformers.utils import subsequent_mask
|
||||
|
||||
|
||||
class HourGlass(nn.Module):
|
||||
"""
|
||||
## Hourglass model
|
||||
|
||||
This model recursively adds layers to the middle while shortening the sequence by down-sampling.
|
||||
The shortened sequence processed by another hourglass model is sandwiched between two normal transformer
|
||||
layers. (A transformer layer has a [self-attention layer](../mha.html)
|
||||
and a [position-wise feed-forward layer](../feed_forward.html)).
|
||||
"""
|
||||
|
||||
def __init__(self, n_heads: int, d_model: int, dropout: float, d_ff: int, shortening_factors: List[int]):
|
||||
"""
|
||||
* `n_heads` is the number of heads in [multi-head attention layers](../mha.html)
|
||||
* `d_model` is the size of the token embeddings
|
||||
* `dropout` is the dropout probability
|
||||
* `d_ff` is the dimensionality of the hidden layer in [position-wise feed-forward layers](../feed_forward.html)
|
||||
* `shortening_factors` is the list of shortening factors
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
# The transformer layer before down-sampling
|
||||
self.pre = TransformerLayer(d_model=d_model,
|
||||
# [Multi-head attention layer](../mha.html)
|
||||
self_attn=MultiHeadAttention(n_heads, d_model, dropout),
|
||||
# [Position wise feed-forward layers](.. / feed_forward.html)
|
||||
feed_forward=FeedForward(d_model, d_ff, dropout),
|
||||
#
|
||||
dropout_prob=dropout)
|
||||
# Auto-regressive mask
|
||||
self.mask = AutoregressiveMask()
|
||||
|
||||
# The shortening factor $k$ (or the down-sampling rate)
|
||||
k = shortening_factors[0]
|
||||
|
||||
# We shift the tokens to the right by $k - 1$ steps to make sure
|
||||
# information doesn't leak from the future tokens to past tokens
|
||||
# as a result of down-sampling and up-sampling
|
||||
self.shift_right = ShiftRight(k - 1)
|
||||
# Shortening or the down-sampling layer. We use the simplest form - average pooling.
|
||||
# The paper shows that attention based down sampling works best, which we haven't implemented yet.
|
||||
self.shortening = AvgPoolShortening(k)
|
||||
|
||||
# If there are no more shortening (middle of the hourglass)
|
||||
if len(shortening_factors) == 1:
|
||||
# The center layer is another transformer layer
|
||||
self.shortened = TransformerLayer(d_model=d_model,
|
||||
self_attn=MultiHeadAttention(n_heads, d_model, dropout),
|
||||
feed_forward=FeedForward(d_model, d_ff, dropout),
|
||||
dropout_prob=dropout)
|
||||
# Autoregressive mask
|
||||
self.mask_short = AutoregressiveMask()
|
||||
self.hour_glass = None
|
||||
else:
|
||||
# Insert another hourglass model recursively
|
||||
self.hour_glass = HourGlass(n_heads, d_model, dropout, d_ff, shortening_factors[1:])
|
||||
|
||||
# Up-sampling layer. We use naive up-sampling for simplicity and the paper shows attention based up sampling
|
||||
# works better.
|
||||
self.up_sampling = NaiveUpSampling(k)
|
||||
|
||||
# The final transformer layer after up-sampling
|
||||
self.post = TransformerLayer(d_model=d_model,
|
||||
self_attn=MultiHeadAttention(n_heads, d_model, dropout),
|
||||
feed_forward=FeedForward(d_model, d_ff, dropout),
|
||||
dropout_prob=dropout)
|
||||
|
||||
def forward(self, x: torch.Tensor):
|
||||
# Initial transformer layer
|
||||
# $$x \leftarrow PreVanillaLayers(x)$$
|
||||
x = self.pre(x=x, mask=self.mask(x))
|
||||
# Shifting and shortening
|
||||
# $$x' \leftarrow Shortening(ShiftRight(x,k−1),k)$$
|
||||
x_short = self.shortening(self.shift_right(x))
|
||||
|
||||
# If we are at the center of the hourglass,
|
||||
# $$\textbf{\small if } \text{\small E\scriptsize MPTY}(shorten\_factors) \textbf{\small then}$$
|
||||
if self.hour_glass is None:
|
||||
# Center transformer layer
|
||||
# $$x' \leftarrow ShortenedLayers(x')$$
|
||||
x_short = self.shortened(x=x_short, mask=self.mask_short(x_short))
|
||||
# $$\textbf{else}$$
|
||||
else:
|
||||
# $$x' \leftarrow \text{\small H\scriptsize OURGLASS}(x, shorten\_factors)$$
|
||||
x_short = self.hour_glass(x_short)
|
||||
|
||||
# Up-sample the shortened sequence and add a skip connection
|
||||
# $$x \leftarrow x + Upsampling(x, x', k)$$
|
||||
x = x + self.up_sampling(x, x_short)
|
||||
# Final transformer layer
|
||||
# $$x \leftarrow PostVanillaLayers(x)$$
|
||||
x = self.post(x=x, mask=self.mask(x))
|
||||
|
||||
#
|
||||
return x
|
||||
|
||||
|
||||
class ShiftRight(nn.Module):
|
||||
"""
|
||||
### Shift right operation
|
||||
|
||||
This shifts the sequence to the right by the given number of steps
|
||||
"""
|
||||
|
||||
def __init__(self, shift: int):
|
||||
"""
|
||||
* `shift` is the number of steps to shift by
|
||||
"""
|
||||
super().__init__()
|
||||
# cannot be negative
|
||||
assert shift >= 0
|
||||
#
|
||||
self.shift = shift
|
||||
|
||||
def forward(self, x: torch.Tensor):
|
||||
"""
|
||||
* `x` is a tensor of shape `[seq_len, ...]`
|
||||
"""
|
||||
# If the shift is $0$ return the original
|
||||
if self.shift == 0:
|
||||
return x
|
||||
# Zeros to be appended to the left
|
||||
prefix = x.new_zeros([self.shift, *x.shape[1:]])
|
||||
# Concatenate the zeros and truncate the right
|
||||
return torch.cat([prefix, x[:-self.shift]])
|
||||
|
||||
|
||||
class AvgPoolShortening(nn.Module):
|
||||
"""
|
||||
### Average pool shortening
|
||||
|
||||
This down-samples by a given factor with average pooling
|
||||
"""
|
||||
|
||||
def __init__(self, k: int):
|
||||
"""
|
||||
* `k` is the shortening factor
|
||||
"""
|
||||
super().__init__()
|
||||
# Average pooling layer
|
||||
self.pool = nn.AvgPool1d(k, ceil_mode=True)
|
||||
|
||||
def forward(self, x: torch.Tensor):
|
||||
"""
|
||||
* `x` is of shape `[seq_len, batch_size, d_model]`
|
||||
"""
|
||||
# Pooling layer accepts shape `[batch_size, d_model, seq_len]` so we
|
||||
# permute axes.
|
||||
return self.pool(x.permute(1, 2, 0)).permute(2, 0, 1)
|
||||
|
||||
|
||||
class NaiveUpSampling(nn.Module):
|
||||
"""
|
||||
### Naive up-sampling
|
||||
|
||||
This up-samples by repeating
|
||||
"""
|
||||
|
||||
def __init__(self, k: int):
|
||||
"""
|
||||
* `k` is the shortening factor
|
||||
"""
|
||||
super().__init__()
|
||||
self.k = k
|
||||
|
||||
def forward(self, x: torch.Tensor, x_short: torch.Tensor):
|
||||
"""
|
||||
* `x` is the tensor with embeddings before down-sampling
|
||||
* `x_short` is the tensor of higher density (to be up-sampled) representations
|
||||
"""
|
||||
# Repeat across the sequence dimension
|
||||
expanded = torch.repeat_interleave(x_short, self.k, dim=0)
|
||||
# Truncate the extra embeddings at the end
|
||||
expanded = expanded[:x.shape[0]]
|
||||
|
||||
#
|
||||
return expanded
|
||||
|
||||
|
||||
class AutoregressiveMask(nn.Module):
|
||||
"""
|
||||
### Generate auto-regressive mask
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.mask = None
|
||||
|
||||
def forward(self, x: torch.Tensor):
|
||||
# Create a mask if we haven't created or sizes have changed
|
||||
if self.mask is None or self.mask.size(0) != len(x):
|
||||
# [Subsequent mask](../utils.html), will mask out tokens from seeing future tokens
|
||||
self.mask = subsequent_mask(len(x)).to(x.device)
|
||||
|
||||
#
|
||||
return self.mask
|
||||
|
||||
|
||||
class LinearPoolingShortening(nn.Module):
|
||||
"""
|
||||
### 🚧 Linear pooling for down-sampling
|
||||
|
||||
This concatenates the consecutive tokens embeddings that need to be merged and do a linear
|
||||
transformation to map it to the size of a single token embedding.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class AttentionBasedShortening(nn.Module):
|
||||
"""
|
||||
### 🚧 Down-sampling with attention
|
||||
|
||||
\begin{align}
|
||||
x' &= S(x) + Attention \Big(Q=S(x),K = x, V =x \Big) \\
|
||||
x' &= x' + FFN(x')
|
||||
\end{align}
|
||||
|
||||
where $S(x)$ is average pooling or linear pooling.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class LinearUpSampling(nn.Module):
|
||||
"""
|
||||
### 🚧 Linear projection for up-sampling
|
||||
|
||||
Make a linear projection of dense token embeddings to a size of $d_{\text{model}} k$.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class AttentionBasedUpSampling(nn.Module):
|
||||
"""
|
||||
### 🚧 Attention based up-sampling
|
||||
|
||||
\begin{align}
|
||||
x &= U(x,x') + Attention \Big(Q=U(x,x'),K = x', V = x' \Big) \\
|
||||
x &= x + FFN(x)
|
||||
\end{align}
|
||||
|
||||
where $U(x,x') = x + LinearUpsampling(x')$
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
raise NotImplementedError
|
||||
@@ -0,0 +1,157 @@
|
||||
"""
|
||||
---
|
||||
title: Hierarchical Transformers Are More Efficient Language Models Experiment
|
||||
summary: This experiment trains a hourglass model on Tiny Shakespeare dataset.
|
||||
---
|
||||
|
||||
# [Hierarchical Transformers Are More Efficient Language Models](index.html) Experiment
|
||||
|
||||
This is an annotated PyTorch experiment to train a [hourglass](index.html).
|
||||
|
||||
This is based on
|
||||
[training loop and configurations for a simple transformer auto-regressive NLP task](../basic/autoregressive_experiment.html).
|
||||
"""
|
||||
import math
|
||||
from typing import List
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
from labml import experiment
|
||||
from labml.configs import option
|
||||
from labml_nn.experiments.nlp_autoregression import NLPAutoRegressionConfigs
|
||||
from labml_nn.transformers.hour_glass import HourGlass
|
||||
from labml_nn.transformers.positional_encoding import PositionalEncoding
|
||||
|
||||
|
||||
class AutoregressiveTransformer(nn.Module):
|
||||
"""
|
||||
## Autoregressive language model
|
||||
"""
|
||||
|
||||
def __init__(self, n_tokens: int, d_model: int, dropout: float, hour_glass: HourGlass):
|
||||
"""
|
||||
* `n_tokens` is the vocabulary size
|
||||
* `d_model` is the size of the token embeddings
|
||||
* `dropout` is the dropout probability
|
||||
* `hour_glass` is the [hourglass model](index.html)
|
||||
"""
|
||||
super().__init__()
|
||||
# Token embeddings
|
||||
self.embedding = nn.Embedding(n_tokens, d_model)
|
||||
# [Fixed positional embeddings](../positional_encoding.html).
|
||||
#
|
||||
# 📝 The
|
||||
# [official paper implementation](https://github.com/google/trax/blob/master/trax/models/research/hourglass.py)
|
||||
# use [relative attention](../xl/relative_mha.html)
|
||||
self.pos_embedding = PositionalEncoding(d_model, dropout)
|
||||
# [hourglass model](index.html)
|
||||
self.hour_glass = hour_glass
|
||||
# To normalize the final embeddings
|
||||
self.norm = nn.LayerNorm([d_model])
|
||||
# Embedding size
|
||||
self.d_model = d_model
|
||||
# Final linear layer to predict the logits
|
||||
self.output = nn.Linear(d_model, n_tokens)
|
||||
|
||||
def __call__(self, x: torch.Tensor):
|
||||
"""
|
||||
* `x` is the tensor with token indexes of shape `[seq_len, batch_size]`
|
||||
"""
|
||||
# Get embeddings
|
||||
x = self.embedding(x)
|
||||
|
||||
# Add [positional embeddings](../positional_encoding.html)
|
||||
if self.pos_embedding is not None:
|
||||
x = self.pos_embedding(x * math.sqrt(self.d_model))
|
||||
|
||||
# Hourglass
|
||||
x = self.hour_glass(x)
|
||||
|
||||
# Get logits
|
||||
output = self.output(self.norm(x))
|
||||
|
||||
# Return the logits
|
||||
return output, None
|
||||
|
||||
|
||||
class Configs(NLPAutoRegressionConfigs):
|
||||
"""
|
||||
## Configurations
|
||||
|
||||
This inherits from
|
||||
[training loop and configurations for a simple transformer auto-regressive NLP task](../basic/autoregressive_transformer.html).
|
||||
"""
|
||||
# Model
|
||||
model: AutoregressiveTransformer
|
||||
# Number of attention heads
|
||||
n_heads: int = 8
|
||||
# Dropout probability
|
||||
dropout: float = 0.1
|
||||
# Size of feed-forward hidden layer
|
||||
d_ff: int = 512
|
||||
# Token embedding size
|
||||
d_model: int = 256
|
||||
# Shortening factors
|
||||
shortening_factors: List[int] = [8, 4]
|
||||
|
||||
|
||||
@option(Configs.model)
|
||||
def _model(c: Configs):
|
||||
"""
|
||||
Create the model
|
||||
"""
|
||||
|
||||
# Create hourglass model
|
||||
hour_glass = HourGlass(c.n_heads, c.d_model, c.dropout, c.d_ff, c.shortening_factors)
|
||||
# Create the auto-regressive wrapper
|
||||
m = AutoregressiveTransformer(c.n_tokens, c.d_model, c.dropout, hour_glass).to(c.device)
|
||||
|
||||
#
|
||||
return m
|
||||
|
||||
|
||||
def main():
|
||||
# Create experiment
|
||||
experiment.create(name="hour_glass")
|
||||
# Create configs
|
||||
conf = Configs()
|
||||
# Override configurations
|
||||
experiment.configs(conf, {
|
||||
# Use character level tokenizer
|
||||
'tokenizer': 'character',
|
||||
# Prompt separator is blank
|
||||
'prompt_separator': '',
|
||||
# Starting prompt for sampling
|
||||
'prompt': 'It is ',
|
||||
# Use Tiny Shakespeare dataset
|
||||
'text': 'tiny_shakespeare',
|
||||
|
||||
# Use a context size of $256$
|
||||
'seq_len': 256,
|
||||
# Train for $128$ epochs
|
||||
'epochs': 128,
|
||||
# Batch size $32$
|
||||
'batch_size': 32,
|
||||
# Switch between training and validation for $10$ times
|
||||
# per epoch
|
||||
'inner_iterations': 10,
|
||||
|
||||
# Use [Noam optimizer](../../optimizers/noam.html)
|
||||
'optimizer.optimizer': 'Noam',
|
||||
'optimizer.learning_rate': 1.,
|
||||
#
|
||||
})
|
||||
|
||||
# Set models for saving and loading
|
||||
experiment.add_pytorch_models({'model': conf.model})
|
||||
|
||||
# Start the experiment
|
||||
with experiment.start():
|
||||
# Run training
|
||||
conf.run()
|
||||
|
||||
|
||||
#
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,43 @@
|
||||
"""
|
||||
---
|
||||
title: k-Nearest Neighbor Language Models
|
||||
summary: >
|
||||
This is a simple PyTorch implementation/tutorial of the paper
|
||||
Generalization through Memorization: Nearest Neighbor Language Models using FAISS.
|
||||
It runs a kNN model on the final transformer layer embeddings to improve the
|
||||
loss of transformer based language models.
|
||||
It's also great for domain adaptation without pre-training.
|
||||
---
|
||||
|
||||
# k-Nearest Neighbor Language Models
|
||||
|
||||
This is a [PyTorch](https://pytorch.org) implementation of the paper
|
||||
[Generalization through Memorization: Nearest Neighbor Language Models](https://arxiv.org/abs/1911.00172).
|
||||
It uses k-nearest neighbors to improve perplexity of autoregressive transformer models.
|
||||
|
||||
An autoregressive language model estimates $p(w_t | \textcolor{yellowgreen}{c_t})$,
|
||||
where $w_t$ is the token at step $t$
|
||||
and $c_t$ is the context, $\textcolor{yellowgreen}{c_t} = (w_1, w_2, ..., w_{t-1})$.
|
||||
|
||||
This paper, improves $p(w_t | \textcolor{yellowgreen}{c_t})$ using a k-nearest neighbor search
|
||||
on key-value pairs $\big(f(c_i), w_i\big)$, with search key $f(\textcolor{yellowgreen}{c_t})$.
|
||||
Here $f(\textcolor{yellowgreen}{c_t})$ is an embedding of the context $\textcolor{yellowgreen}{c_t}$.
|
||||
The paper (and this implementation) uses the **input to the feed-forward layer of the
|
||||
final layer of the transformer** as $f(\textcolor{yellowgreen}{c_t})$.
|
||||
|
||||
We use [FAISS](https://github.com/facebookresearch/faiss) to index $f(c_i)$.
|
||||
|
||||
### Implementation
|
||||
|
||||
So to run $k$NN-LM we need to:
|
||||
|
||||
* [Train a transformer model](train_model.html)
|
||||
* [Build an index](build_index.html) of $\big(f(c_i), w_i\big)$
|
||||
* [Evaluate kNN-ML](eval_knn.html) using $k$NN seach on $\big(f(c_i), w_i\big)$
|
||||
with $f(\textcolor{yellowgreen}{c_t})$
|
||||
|
||||
This experiment uses a small dataset so that we can run this without using up a few hundred giga-bytes
|
||||
of disk space for the index.
|
||||
|
||||
The official implementation of $k$NN-LM can be found [here](https://github.com/urvashik/knnlm).
|
||||
"""
|
||||
@@ -0,0 +1,156 @@
|
||||
"""
|
||||
---
|
||||
title: Build FAISS index for k-NN search
|
||||
summary: This builds the FAISS index with the transformer embeddings.
|
||||
---
|
||||
|
||||
# Build FAISS index for k-NN search
|
||||
|
||||
We want to build the index of $\big(f(c_i), w_i\big)$.
|
||||
We store $f(c_i)$ and $w_i$ in memory mapped numpy arrays.
|
||||
We find $f(c_i)$ nearest to $f(c_t)$ using [FAISS](https://github.com/facebookresearch/faiss).
|
||||
FAISS indexes $\big(f(c_i), i\big)$ and we query it with $f(c_t)$.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import faiss
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
from labml import experiment, monit, lab
|
||||
from labml.utils.pytorch import get_modules
|
||||
from labml_nn.transformers.knn.train_model import Configs
|
||||
|
||||
|
||||
def load_experiment(run_uuid: str, checkpoint: Optional[int] = None):
|
||||
"""
|
||||
Load a saved experiment from [train model](train_model.html).
|
||||
"""
|
||||
|
||||
# Create configurations object
|
||||
conf = Configs()
|
||||
# Load custom configurations used in the experiment
|
||||
conf_dict = experiment.load_configs(run_uuid)
|
||||
# We need to get inputs to the feed forward layer, $f(c_i)$
|
||||
conf_dict['is_save_ff_input'] = True
|
||||
|
||||
# This experiment is just an evaluation; i.e. nothing is tracked or saved
|
||||
experiment.evaluate()
|
||||
# Initialize configurations
|
||||
experiment.configs(conf, conf_dict)
|
||||
# Set models for saving/loading
|
||||
experiment.add_pytorch_models(get_modules(conf))
|
||||
# Specify the experiment to load from
|
||||
experiment.load(run_uuid, checkpoint)
|
||||
|
||||
# Start the experiment; this is when it actually loads models
|
||||
experiment.start()
|
||||
|
||||
return conf
|
||||
|
||||
|
||||
def gather_keys(conf: Configs):
|
||||
"""
|
||||
## Gather $\big(f(c_i), w_i\big)$ and save them in numpy arrays
|
||||
|
||||
*Note that these numpy arrays will take up a lot of space (even few hundred gigabytes)
|
||||
depending on the size of your dataset*.
|
||||
"""
|
||||
|
||||
# Dimensions of $f(c_i)$
|
||||
d_model = conf.transformer.d_model
|
||||
# Training data loader
|
||||
data_loader = conf.trainer.data_loader
|
||||
# Number of contexts; i.e. number of tokens in the training data minus one.
|
||||
# $\big(f(c_i), w_i\big)$ for $i \in [2, T]$
|
||||
n_keys = data_loader.data.shape[0] * data_loader.data.shape[1] - 1
|
||||
# Numpy array for $f(c_i)$
|
||||
keys_store = np.memmap(str(lab.get_data_path() / 'keys.npy'), dtype=np.float32, mode='w+', shape=(n_keys, d_model))
|
||||
# Numpy array for $w_i$
|
||||
vals_store = np.memmap(str(lab.get_data_path() / 'vals.npy'), dtype=np.int, mode='w+', shape=(n_keys, 1))
|
||||
|
||||
# Number of keys $f(c_i)$ collected
|
||||
added = 0
|
||||
with torch.no_grad():
|
||||
# Loop through data
|
||||
for i, batch in monit.enum("Collect data", data_loader, is_children_silent=True):
|
||||
# $w_i$ the target labels
|
||||
vals = batch[1].view(-1, 1)
|
||||
# Input data moved to the device of the model
|
||||
data = batch[0].to(conf.device)
|
||||
# Run the model
|
||||
_ = conf.model(data)
|
||||
# Get $f(c_i)$
|
||||
keys = conf.model.ff_input.view(-1, d_model)
|
||||
# Save keys, $f(c_i)$ in the memory mapped numpy array
|
||||
keys_store[added: added + keys.shape[0]] = keys.cpu()
|
||||
# Save values, $w_i$ in the memory mapped numpy array
|
||||
vals_store[added: added + keys.shape[0]] = vals
|
||||
# Increment the number of collected keys
|
||||
added += keys.shape[0]
|
||||
|
||||
|
||||
def build_index(conf: Configs, n_centeroids: int = 2048, code_size: int = 64, n_probe: int = 8, n_train: int = 200_000):
|
||||
"""
|
||||
## Build FAISS index
|
||||
|
||||
[Getting started](https://github.com/facebookresearch/faiss/wiki/Getting-started),
|
||||
[faster search](https://github.com/facebookresearch/faiss/wiki/Faster-search),
|
||||
and [lower memory footprint](https://github.com/facebookresearch/faiss/wiki/Lower-memory-footprint)
|
||||
tutorials on FAISS will help you learn more about FAISS usage.
|
||||
"""
|
||||
# Dimensions of $f(c_i)$
|
||||
d_model = conf.transformer.d_model
|
||||
# Training data loader
|
||||
data_loader = conf.trainer.data_loader
|
||||
# Number of contexts; i.e. number of tokens in the training data minus one.
|
||||
# $\big(f(c_i), w_i\big)$ for $i \in [2, T]$
|
||||
n_keys = data_loader.data.shape[0] * data_loader.data.shape[1] - 1
|
||||
|
||||
# Build an index with Verenoi cell based faster search with compression that
|
||||
# doesn't store full vectors.
|
||||
quantizer = faiss.IndexFlatL2(d_model)
|
||||
index = faiss.IndexIVFPQ(quantizer, d_model, n_centeroids, code_size, 8)
|
||||
index.nprobe = n_probe
|
||||
|
||||
# Load the memory mapped numpy array of keys
|
||||
keys_store = np.memmap(str(lab.get_data_path() / 'keys.npy'), dtype=np.float32, mode='r', shape=(n_keys, d_model))
|
||||
|
||||
# Pick a random sample of keys to train the index with
|
||||
random_sample = np.random.choice(np.arange(n_keys), size=[min(n_train, n_keys)], replace=False)
|
||||
|
||||
with monit.section('Train index'):
|
||||
# Train the index to store the keys
|
||||
index.train(keys_store[random_sample])
|
||||
|
||||
# Add keys to the index; $\big(f(c_i), i\big)$
|
||||
for s in monit.iterate('Index', range(0, n_keys, 1024)):
|
||||
e = min(s + 1024, n_keys)
|
||||
# $f(c_i)$
|
||||
keys = keys_store[s:e]
|
||||
# $i$
|
||||
idx = np.arange(s, e)
|
||||
# Add to index
|
||||
index.add_with_ids(keys, idx)
|
||||
|
||||
with monit.section('Save'):
|
||||
# Save the index
|
||||
faiss.write_index(index, str(lab.get_data_path() / 'faiss.index'))
|
||||
|
||||
|
||||
def main():
|
||||
# Load the experiment. Replace the run uuid with you run uuid from
|
||||
# [training the model](train_model.html).
|
||||
conf = load_experiment('4984b85c20bf11eb877a69c1a03717cd')
|
||||
# Set model to evaluation mode
|
||||
conf.model.eval()
|
||||
|
||||
# Collect $\big(f(c_i), w_i\big)$
|
||||
gather_keys(conf)
|
||||
# Add them to the index for fast search
|
||||
build_index(conf)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,157 @@
|
||||
"""
|
||||
---
|
||||
title: Evaluate k-nearest neighbor language model
|
||||
summary: >
|
||||
This runs the kNN model and merges the kNN results with transformer output to
|
||||
achieve better results than just using the transformer.
|
||||
---
|
||||
|
||||
# Evaluate k-nearest neighbor language model
|
||||
"""
|
||||
from typing import Optional, List
|
||||
|
||||
import faiss
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
from labml import monit, lab
|
||||
from labml.logger import inspect
|
||||
from labml_nn.transformers.knn.train_model import Configs
|
||||
|
||||
|
||||
def knn(queries: torch.Tensor, index: faiss.IndexFlatL2, keys_store: np.ndarray, vals_store: np.ndarray, n_tokens: int):
|
||||
"""
|
||||
## $k$-NN to get $p(w_t, c_t)$
|
||||
|
||||
Here we refer to $f(\textcolor{yellowgreen}{c_t})$ as queries,
|
||||
$f(c_i)$ as keys and $w_i$ as values.
|
||||
"""
|
||||
|
||||
# Save shape of queries to reshape results
|
||||
queries_shape = queries.shape
|
||||
|
||||
# Flatten the `batch` and `sequence` dimensions of queries
|
||||
queries = queries.view(-1, queries_shape[-1])
|
||||
|
||||
# Find 10 nearest neighbors of $f(\textcolor{yellowgreen}{c_t})$ among $f(c_i)$.
|
||||
# `distance` is the distance given by FAISS and `idx`, $i$ is the index of it in `keys_store`.
|
||||
distance, idx = index.search(queries.numpy(), 10)
|
||||
|
||||
# Get $f(c_i)$
|
||||
keys_found = queries.new_tensor(keys_store[idx])
|
||||
# Get $w_i$
|
||||
vals_found = torch.tensor(vals_store[idx]).squeeze(-1)
|
||||
|
||||
# We are going to calculate the cosine similarity between normalized vectors
|
||||
|
||||
# Normalize $f(c_i)$
|
||||
keys_found_n = keys_found / torch.sqrt((keys_found ** 2).sum(-1, keepdims=True) + 1e-10)
|
||||
# Normalize $f(\textcolor{yellowgreen}{c_t})$
|
||||
queries_n = queries / torch.sqrt((queries ** 2).sum(-1, keepdims=True) + 1e-10)
|
||||
|
||||
# Get the dot-product, or cosine similarity
|
||||
dot_prod = (keys_found_n * queries_n.unsqueeze(1)).sum(-1)
|
||||
|
||||
# Token-wise logits
|
||||
logits_token = dot_prod.new_zeros(queries.shape[0], n_tokens)
|
||||
# Scatter and accumulate token logits based on the nearest neighbors
|
||||
_ = logits_token.scatter_(dim=1, index=vals_found, src=dot_prod, reduce='add')
|
||||
|
||||
# Reshape the logits
|
||||
logits_token = logits_token.reshape(queries_shape[0], queries_shape[1], -1)
|
||||
|
||||
return logits_token
|
||||
|
||||
|
||||
def validation_loss(knn_weights: List[float], last_n: Optional[int], conf: Configs, index: faiss.IndexFlatL2,
|
||||
keys_store: np.ndarray, vals_store: np.ndarray):
|
||||
"""
|
||||
## Calculate validation loss
|
||||
|
||||
We calculate the validation loss of the combined on $k$-NN prediction and transformer prediction.
|
||||
The weight given to the $k$-NN model is given by `knn_weight`.
|
||||
It's a list of weights and we calculate the validation loss for each.
|
||||
"""
|
||||
|
||||
# List of losses for each `knn_weights`
|
||||
losses = [[] for _ in knn_weights]
|
||||
# Number of samples in each batch
|
||||
n_samples = []
|
||||
with torch.no_grad():
|
||||
# Iterate through validation data
|
||||
for i, batch in monit.enum("Validation", conf.validator.data_loader, is_children_silent=True):
|
||||
# Get data and target labels
|
||||
data, target = batch[0].to(conf.device), batch[1].to(conf.device)
|
||||
# Run the model and get predictions $p(w_t, c_t)$
|
||||
res = conf.model(data)
|
||||
# Get $k$-NN predictions
|
||||
res_knn = knn(conf.model.ff_input.cpu(), index, keys_store, vals_store, conf.n_tokens)
|
||||
res_knn = res_knn.to(conf.device)
|
||||
|
||||
# This is to calculate only the loss for `last_n` tokens.
|
||||
# This is important because the first predictions (along the sequence)
|
||||
# of transformer model has very few past tokens to look at.
|
||||
if last_n:
|
||||
res = res[-last_n:]
|
||||
res_knn = res_knn[-last_n:]
|
||||
target = target[-last_n:]
|
||||
|
||||
# Number of samples
|
||||
n_s = res.shape[0] * data.shape[1]
|
||||
n_samples.append(n_s)
|
||||
|
||||
# Calculate scores for each of `knn_weights`.
|
||||
for i, c in enumerate(knn_weights):
|
||||
# Calculate the loss
|
||||
loss = conf.loss_func(res_knn * c + (1 - c) * res, target)
|
||||
losses[i].append(loss * n_s)
|
||||
|
||||
return losses, n_samples
|
||||
|
||||
|
||||
def load_index(conf: Configs, n_probe: int = 8):
|
||||
"""
|
||||
## Load the index
|
||||
"""
|
||||
# Dimensions of $f(c_i)$
|
||||
d_model = conf.transformer.d_model
|
||||
# Training data loader
|
||||
data_loader = conf.trainer.data_loader
|
||||
# Number of contexts; i.e. number of tokens in the training data minus one.
|
||||
# $\big(f(c_i), w_i\big)$ for $i \in [2, T]$
|
||||
n_keys = data_loader.data.shape[0] * data_loader.data.shape[1] - 1
|
||||
|
||||
# Load FAISS index
|
||||
with monit.section('Load index'):
|
||||
index = faiss.read_index(str(lab.get_data_path() / 'faiss.index'))
|
||||
# Set number of cells to probe
|
||||
index.nprobe = n_probe
|
||||
|
||||
# Load memory mapped numpy arrays
|
||||
keys_store = np.memmap(str(lab.get_data_path() / 'keys.npy'), dtype=np.float32, mode='r', shape=(n_keys, d_model))
|
||||
vals_store = np.memmap(str(lab.get_data_path() / 'vals.npy'), dtype=np.int, mode='r', shape=(n_keys, 1))
|
||||
|
||||
return index, keys_store, vals_store
|
||||
|
||||
|
||||
def main():
|
||||
from labml_nn.transformers.knn.build_index import load_experiment
|
||||
# Load the experiment. Replace the run uuid with you run uuid from
|
||||
# [training the model](train_model.html).
|
||||
conf = load_experiment('4984b85c20bf11eb877a69c1a03717cd')
|
||||
# Set model to evaluation mode
|
||||
conf.model.eval()
|
||||
|
||||
# Load index
|
||||
index, keys_store, vals_store = load_index(conf)
|
||||
# List of weights given to $k$-NN prediction. We will evaluate the validation loss for
|
||||
# each of the weights
|
||||
knn_weights = [i / 20 for i in range(10)]
|
||||
# Evaluate validation loss
|
||||
losses, n_samples = validation_loss(knn_weights, None, conf, index, keys_store, vals_store)
|
||||
# Output the losses for each of `knn_weights`.
|
||||
inspect({c: np.sum(losses[i]) / np.sum(n_samples) for i, c in enumerate(knn_weights)})
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,144 @@
|
||||
"""
|
||||
---
|
||||
title: Train Autoregressive Transformer
|
||||
summary: This is training code with notes for a basic auto-regressive transformer.
|
||||
---
|
||||
|
||||
# Train Autoregressive Transformer
|
||||
|
||||
This trains a simple [transformer](../../) model for auto-regression.
|
||||
"""
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
from labml import experiment
|
||||
from labml.configs import option
|
||||
from labml.utils.pytorch import get_modules
|
||||
|
||||
from labml_nn.experiments.nlp_autoregression import NLPAutoRegressionConfigs
|
||||
from labml_nn.transformers import Encoder, Generator, TransformerConfigs
|
||||
from labml_nn.transformers.utils import subsequent_mask
|
||||
|
||||
|
||||
class AutoregressiveModel(nn.Module):
|
||||
"""
|
||||
## Auto regressive model
|
||||
"""
|
||||
|
||||
def __init__(self, src_embed: nn.Module, encoder: Encoder, generator: Generator, *,
|
||||
is_save_ff_input: bool = False):
|
||||
super().__init__()
|
||||
# Token embedding module
|
||||
self.src_embed = src_embed
|
||||
# Transformer based encoder
|
||||
self.encoder = encoder
|
||||
# Whether the last layer of the encoder should
|
||||
# save the input to the feed-forward layer.
|
||||
# This is out $f(c_t)$, the embedding of the context.
|
||||
self.encoder.layers[-1].is_save_ff_input = is_save_ff_input
|
||||
# Next token generation layer;
|
||||
# this give logits of the the next token
|
||||
self.generator = generator
|
||||
# This will be initialized on the first call
|
||||
self.src_mask = None
|
||||
|
||||
@property
|
||||
def ff_input(self) -> torch.Tensor:
|
||||
"""
|
||||
Retrieve saved $f(c_t)$
|
||||
"""
|
||||
return self.encoder.layers[-1].ff_input
|
||||
|
||||
def forward(self, src: torch.Tensor):
|
||||
# Create subsequent mask, so that the transformer can only pay attention to past tokens.
|
||||
if self.src_mask is None or self.src_mask.size(0) != len(src):
|
||||
self.src_mask = subsequent_mask(len(src)).to(src.device)
|
||||
# Embed the tokens (`src`) and run it through the the transformer
|
||||
res = self.encoder(self.src_embed(src), self.src_mask)
|
||||
# Generate logits of the next token
|
||||
return self.generator(res), None
|
||||
|
||||
|
||||
class Configs(NLPAutoRegressionConfigs):
|
||||
"""
|
||||
## Configurations
|
||||
|
||||
The default configs can and will be over-ridden when we start the experiment
|
||||
"""
|
||||
|
||||
transformer: TransformerConfigs
|
||||
model: AutoregressiveModel
|
||||
|
||||
is_save_ff_input = False
|
||||
|
||||
|
||||
@option(Configs.model)
|
||||
def autoregressive_model(c: Configs):
|
||||
"""
|
||||
Initialize the auto-regressive model
|
||||
"""
|
||||
m = AutoregressiveModel(
|
||||
# Get the source token embedding layer, encoder and
|
||||
# final token generator from configurable transformer
|
||||
src_embed=c.transformer.src_embed,
|
||||
encoder=c.transformer.encoder,
|
||||
generator=c.transformer.generator,
|
||||
# Whether to save $f(c_t)$
|
||||
is_save_ff_input=c.is_save_ff_input)
|
||||
return m.to(c.device)
|
||||
|
||||
|
||||
@option(Configs.transformer)
|
||||
def transformer_c(c: Configs):
|
||||
"""
|
||||
Initialize the configurable transformer encoder for our autoregressive model
|
||||
"""
|
||||
tc = TransformerConfigs()
|
||||
tc.n_src_vocab = c.n_tokens
|
||||
tc.n_tgt_vocab = c.n_tokens
|
||||
|
||||
return tc
|
||||
|
||||
|
||||
def main():
|
||||
# Create experiment
|
||||
experiment.create(name="knn_lm")
|
||||
# Create configs
|
||||
conf = Configs()
|
||||
# Load configurations
|
||||
experiment.configs(conf,
|
||||
# A dictionary of configurations to override
|
||||
{'tokenizer': 'character',
|
||||
'prompt_separator': '',
|
||||
'prompt': 'It is ',
|
||||
'text': 'tiny_shakespeare',
|
||||
|
||||
'optimizer.optimizer': 'Noam',
|
||||
'optimizer.learning_rate': 1.,
|
||||
'optimizer.d_model': 256,
|
||||
|
||||
'seq_len': 1024,
|
||||
'epochs': 128,
|
||||
'batch_size': 6,
|
||||
'inner_iterations': 10,
|
||||
|
||||
# Transformer configurations
|
||||
'transformer.d_model': 256,
|
||||
'transformer.ffn.d_ff': 1024,
|
||||
'transformer.n_heads': 8,
|
||||
'transformer.n_layers': 6})
|
||||
|
||||
# This is needed to initialize models
|
||||
conf.n_tokens = conf.text.n_tokens
|
||||
|
||||
# Set models for saving and loading
|
||||
experiment.add_pytorch_models(get_modules(conf))
|
||||
|
||||
# Start the experiment
|
||||
with experiment.start():
|
||||
# `TrainValidConfigs.run`
|
||||
conf.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,68 @@
|
||||
"""
|
||||
---
|
||||
title: Label Smoothing Loss
|
||||
summary: >
|
||||
This is an implementation of label smoothing loss, that can be used as
|
||||
an alternative to cross entropy loss for improved accuracy.
|
||||
---
|
||||
|
||||
# Label Smoothing Loss
|
||||
"""
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
|
||||
class LabelSmoothingLoss(nn.Module):
|
||||
def __init__(self, size: int, padding_idx: int, smoothing: float = 0.0):
|
||||
super().__init__()
|
||||
self.loss = nn.KLDivLoss(reduction='sum')
|
||||
self.padding_idx = padding_idx
|
||||
self.confidence = 1.0 - smoothing
|
||||
self.smoothing = smoothing
|
||||
self.size = size
|
||||
self.true_dist = None
|
||||
|
||||
def forward(self, x: torch.Tensor, target: torch.Tensor):
|
||||
assert x.shape[1] == self.size
|
||||
true_dist = x.clone()
|
||||
true_dist.fill_(self.smoothing / (self.size - 2))
|
||||
true_dist.scatter_(1, target.unsqueeze(1), self.confidence)
|
||||
true_dist[:, self.padding_idx] = 0
|
||||
mask = torch.nonzero(target == self.padding_idx, as_tuple=False)
|
||||
if mask.dim() > 0:
|
||||
true_dist.index_fill_(0, mask.squeeze(), 0.0)
|
||||
self.true_dist = true_dist
|
||||
return self.loss(x, true_dist.detach())
|
||||
|
||||
|
||||
def _test_label_smoothing():
|
||||
smooth_loss = LabelSmoothingLoss(5, 0, 0.4)
|
||||
predict = torch.tensor([[0, 0.2, 0.7, 0.1, 0],
|
||||
[0, 0.2, 0.7, 0.1, 0],
|
||||
[0, 0.2, 0.7, 0.1, 0]], dtype=torch.float)
|
||||
_ = smooth_loss(predict.log(),
|
||||
torch.tensor([2, 1, 0], dtype=torch.long))
|
||||
|
||||
# Show the target distributions expected by the system.
|
||||
plt.imshow(smooth_loss.true_dist)
|
||||
plt.show()
|
||||
|
||||
smooth_loss = LabelSmoothingLoss(5, 0, 0.1)
|
||||
|
||||
def loss_sample(x):
|
||||
d = x + 3 * 1
|
||||
predict2 = torch.tensor([[0, x / d, 1 / d, 1 / d, 1 / d],
|
||||
], dtype=torch.float)
|
||||
# print(predict)
|
||||
return smooth_loss(predict2.log(),
|
||||
torch.tensor([1], dtype=torch.long)).item()
|
||||
|
||||
plt.plot(np.arange(1, 100), [loss_sample(x) for x in range(1, 100)])
|
||||
plt.show()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
_test_label_smoothing()
|
||||
@@ -0,0 +1,206 @@
|
||||
"""
|
||||
---
|
||||
title: Multi-Headed Attention (MHA)
|
||||
summary: >
|
||||
This implements the Multi-Headed Attention used in transformers
|
||||
using PyTorch with explanations.
|
||||
---
|
||||
|
||||
# Multi-Headed Attention (MHA)
|
||||
|
||||
[](https://colab.research.google.com/github/labmlai/annotated_deep_learning_paper_implementations/blob/master/labml_nn/transformers/basic/autoregressive_experiment.ipynb)
|
||||
|
||||
This is a tutorial/implementation of multi-headed attention
|
||||
from paper [Attention Is All You Need](https://arxiv.org/abs/1706.03762)
|
||||
in [PyTorch](https://pytorch.org/).
|
||||
The implementation is inspired from [Annotated Transformer](https://nlp.seas.harvard.edu/2018/04/03/attention.html).
|
||||
|
||||
Here is the [training code](basic/autoregressive_experiment.html) that uses a basic transformer
|
||||
with MHA for NLP auto-regression.
|
||||
|
||||
[Here is an experiment implementation](basic/autoregressive_experiment.html) that trains a simple transformer.
|
||||
"""
|
||||
|
||||
import math
|
||||
from typing import Optional, List
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
from labml import tracker
|
||||
|
||||
|
||||
class PrepareForMultiHeadAttention(nn.Module):
|
||||
"""
|
||||
<a id="PrepareMHA"></a>
|
||||
|
||||
## Prepare for multi-head attention
|
||||
|
||||
This module does a linear transformation and splits the vector into given
|
||||
number of heads for multi-head attention.
|
||||
This is used to transform **key**, **query**, and **value** vectors.
|
||||
"""
|
||||
|
||||
def __init__(self, d_model: int, heads: int, d_k: int, bias: bool):
|
||||
super().__init__()
|
||||
# Linear layer for linear transform
|
||||
self.linear = nn.Linear(d_model, heads * d_k, bias=bias)
|
||||
# Number of heads
|
||||
self.heads = heads
|
||||
# Number of dimensions in vectors in each head
|
||||
self.d_k = d_k
|
||||
|
||||
def forward(self, x: torch.Tensor):
|
||||
# Input has shape `[seq_len, batch_size, d_model]` or `[batch_size, d_model]`.
|
||||
# We apply the linear transformation to the last dimension and split that into
|
||||
# the heads.
|
||||
head_shape = x.shape[:-1]
|
||||
|
||||
# Linear transform
|
||||
x = self.linear(x)
|
||||
|
||||
# Split last dimension into heads
|
||||
x = x.view(*head_shape, self.heads, self.d_k)
|
||||
|
||||
# Output has shape `[seq_len, batch_size, heads, d_k]` or `[batch_size, heads, d_model]`
|
||||
return x
|
||||
|
||||
|
||||
class MultiHeadAttention(nn.Module):
|
||||
r"""
|
||||
<a id="MHA"></a>
|
||||
|
||||
## Multi-Head Attention Module
|
||||
|
||||
This computes scaled multi-headed attention for given `query`, `key` and `value` vectors.
|
||||
|
||||
$$\mathop{Attention}(Q, K, V) = \underset{seq}{\mathop{softmax}}\Bigg(\frac{Q K^\top}{\sqrt{d_k}}\Bigg)V$$
|
||||
|
||||
In simple terms, it finds keys that matches the query, and gets the values of
|
||||
those keys.
|
||||
|
||||
It uses dot-product of query and key as the indicator of how matching they are.
|
||||
Before taking the $softmax$ the dot-products are scaled by $\frac{1}{\sqrt{d_k}}$.
|
||||
This is done to avoid large dot-product values causing softmax to
|
||||
give very small gradients when $d_k$ is large.
|
||||
|
||||
Softmax is calculated along the axis of of the sequence (or time).
|
||||
"""
|
||||
|
||||
def __init__(self, heads: int, d_model: int, dropout_prob: float = 0.1, bias: bool = True):
|
||||
"""
|
||||
* `heads` is the number of heads.
|
||||
* `d_model` is the number of features in the `query`, `key` and `value` vectors.
|
||||
"""
|
||||
|
||||
super().__init__()
|
||||
|
||||
# Number of features per head
|
||||
self.d_k = d_model // heads
|
||||
# Number of heads
|
||||
self.heads = heads
|
||||
|
||||
# These transform the `query`, `key` and `value` vectors for multi-headed attention.
|
||||
self.query = PrepareForMultiHeadAttention(d_model, heads, self.d_k, bias=bias)
|
||||
self.key = PrepareForMultiHeadAttention(d_model, heads, self.d_k, bias=bias)
|
||||
self.value = PrepareForMultiHeadAttention(d_model, heads, self.d_k, bias=True)
|
||||
|
||||
# Softmax for attention along the time dimension of `key`
|
||||
self.softmax = nn.Softmax(dim=1)
|
||||
|
||||
# Output layer
|
||||
self.output = nn.Linear(d_model, d_model)
|
||||
# Dropout
|
||||
self.dropout = nn.Dropout(dropout_prob)
|
||||
# Scaling factor before the softmax
|
||||
self.scale = 1 / math.sqrt(self.d_k)
|
||||
|
||||
# We store attentions so that it can be used for logging, or other computations if needed
|
||||
self.attn = None
|
||||
|
||||
def get_scores(self, query: torch.Tensor, key: torch.Tensor):
|
||||
"""
|
||||
### Calculate scores between queries and keys
|
||||
|
||||
This method can be overridden for other variations like relative attention.
|
||||
"""
|
||||
|
||||
# Calculate $Q K^\top$ or $S_{ijbh} = \sum_d Q_{ibhd} K_{jbhd}$
|
||||
return torch.einsum('ibhd,jbhd->ijbh', query, key)
|
||||
|
||||
def prepare_mask(self, mask: torch.Tensor, query_shape: List[int], key_shape: List[int]):
|
||||
"""
|
||||
`mask` has shape `[seq_len_q, seq_len_k, batch_size]`, where first dimension is the query dimension.
|
||||
If the query dimension is equal to $1$ it will be broadcasted.
|
||||
"""
|
||||
|
||||
assert mask.shape[0] == 1 or mask.shape[0] == query_shape[0]
|
||||
assert mask.shape[1] == key_shape[0]
|
||||
assert mask.shape[2] == 1 or mask.shape[2] == query_shape[1]
|
||||
|
||||
# Same mask applied to all heads.
|
||||
mask = mask.unsqueeze(-1)
|
||||
|
||||
# resulting mask has shape `[seq_len_q, seq_len_k, batch_size, heads]`
|
||||
return mask
|
||||
|
||||
def forward(self, *,
|
||||
query: torch.Tensor,
|
||||
key: torch.Tensor,
|
||||
value: torch.Tensor,
|
||||
mask: Optional[torch.Tensor] = None):
|
||||
"""
|
||||
`query`, `key` and `value` are the tensors that store
|
||||
collection of *query*, *key* and *value* vectors.
|
||||
They have shape `[seq_len, batch_size, d_model]`.
|
||||
|
||||
`mask` has shape `[seq_len, seq_len, batch_size]` and
|
||||
`mask[i, j, b]` indicates whether for batch `b`,
|
||||
query at position `i` has access to key-value at position `j`.
|
||||
"""
|
||||
|
||||
# `query`, `key` and `value` have shape `[seq_len, batch_size, d_model]`
|
||||
seq_len, batch_size, _ = query.shape
|
||||
|
||||
if mask is not None:
|
||||
mask = self.prepare_mask(mask, query.shape, key.shape)
|
||||
|
||||
# Prepare `query`, `key` and `value` for attention computation.
|
||||
# These will then have shape `[seq_len, batch_size, heads, d_k]`.
|
||||
query = self.query(query)
|
||||
key = self.key(key)
|
||||
value = self.value(value)
|
||||
|
||||
# Compute attention scores $Q K^\top$.
|
||||
# This gives a tensor of shape `[seq_len, seq_len, batch_size, heads]`.
|
||||
scores = self.get_scores(query, key)
|
||||
|
||||
# Scale scores $\frac{Q K^\top}{\sqrt{d_k}}$
|
||||
scores *= self.scale
|
||||
|
||||
# Apply mask
|
||||
if mask is not None:
|
||||
scores = scores.masked_fill(mask == 0, float('-inf'))
|
||||
|
||||
# $softmax$ attention along the key sequence dimension
|
||||
# $\underset{seq}{softmax}\Bigg(\frac{Q K^\top}{\sqrt{d_k}}\Bigg)$
|
||||
attn = self.softmax(scores)
|
||||
|
||||
# Save attentions if debugging
|
||||
tracker.debug('attn', attn)
|
||||
|
||||
# Apply dropout
|
||||
attn = self.dropout(attn)
|
||||
|
||||
# Multiply by values
|
||||
# $$\underset{seq}{softmax}\Bigg(\frac{Q K^\top}{\sqrt{d_k}}\Bigg)V$$
|
||||
x = torch.einsum("ijbh,jbhd->ibhd", attn, value)
|
||||
|
||||
# Save attentions for any other calculations
|
||||
self.attn = attn.detach()
|
||||
|
||||
# Concatenate multiple heads
|
||||
x = x.reshape(seq_len, batch_size, -1)
|
||||
|
||||
# Output layer
|
||||
return self.output(x)
|
||||
@@ -0,0 +1,139 @@
|
||||
"""
|
||||
---
|
||||
title: Masked Language Model
|
||||
summary: >
|
||||
This is an annotated implementation/tutorial of the Masked Language Model in PyTorch.
|
||||
---
|
||||
|
||||
# Masked Language Model (MLM)
|
||||
|
||||
This is a [PyTorch](https://pytorch.org) implementation of the Masked Language Model (MLM)
|
||||
used to pre-train the BERT model introduced in the paper
|
||||
[BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding](https://arxiv.org/abs/1810.04805).
|
||||
|
||||
## BERT Pretraining
|
||||
|
||||
BERT model is a transformer model.
|
||||
The paper pre-trains the model using MLM and with next sentence prediction.
|
||||
We have only implemented MLM here.
|
||||
|
||||
### Next sentence prediction
|
||||
|
||||
In *next sentence prediction*, the model is given two sentences `A` and `B` and the model
|
||||
makes a binary prediction whether `B` is the sentence that follows `A` in the actual text.
|
||||
The model is fed with actual sentence pairs 50% of the time and random pairs 50% of the time.
|
||||
This classification is done while applying MLM. *We haven't implemented this here.*
|
||||
|
||||
## Masked LM
|
||||
|
||||
This masks a percentage of tokens at random and trains the model to predict
|
||||
the masked tokens.
|
||||
They **mask 15% of the tokens** by replacing them with a special `[MASK]` token.
|
||||
|
||||
The loss is computed on predicting the masked tokens only.
|
||||
This causes a problem during fine-tuning and actual usage since there are no `[MASK]` tokens
|
||||
at that time.
|
||||
Therefore we might not get any meaningful representations.
|
||||
|
||||
To overcome this **10% of the masked tokens are replaced with the original token**,
|
||||
and another **10% of the masked tokens are replaced with a random token**.
|
||||
This trains the model to give representations about the actual token whether or not the
|
||||
input token at that position is a `[MASK]`.
|
||||
And replacing with a random token causes it to
|
||||
give a representation that has information from the context as well;
|
||||
because it has to use the context to fix randomly replaced tokens.
|
||||
|
||||
## Training
|
||||
|
||||
MLMs are harder to train than autoregressive models because they have a smaller training signal.
|
||||
i.e. only a small percentage of predictions are trained per sample.
|
||||
|
||||
Another problem is since the model is bidirectional, any token can see any other token.
|
||||
This makes the "credit assignment" harder.
|
||||
Let's say you have the character level model trying to predict `home *s where i want to be`.
|
||||
At least during the early stages of the training, it'll be super hard to figure out why the
|
||||
replacement for `*` should be `i`, it could be anything from the whole sentence.
|
||||
Whilst, in an autoregressive setting the model will only have to use `h` to predict `o` and
|
||||
`hom` to predict `e` and so on. So the model will initially start predicting with a shorter context first
|
||||
and then learn to use longer contexts later.
|
||||
Since MLMs have this problem it's a lot faster to train if you start with a smaller sequence length
|
||||
initially and then use a longer sequence length later.
|
||||
|
||||
Here is [the training code](experiment.html) for a simple MLM model.
|
||||
"""
|
||||
|
||||
from typing import List
|
||||
|
||||
import torch
|
||||
|
||||
|
||||
class MLM:
|
||||
"""
|
||||
## Masked LM (MLM)
|
||||
|
||||
This class implements the masking procedure for a given batch of token sequences.
|
||||
"""
|
||||
|
||||
def __init__(self, *,
|
||||
padding_token: int, mask_token: int, no_mask_tokens: List[int], n_tokens: int,
|
||||
masking_prob: float = 0.15, randomize_prob: float = 0.1, no_change_prob: float = 0.1,
|
||||
):
|
||||
"""
|
||||
* `padding_token` is the padding token `[PAD]`.
|
||||
We will use this to mark the labels that shouldn't be used for loss calculation.
|
||||
* `mask_token` is the masking token `[MASK]`.
|
||||
* `no_mask_tokens` is a list of tokens that should not be masked.
|
||||
This is useful if we are training the MLM with another task like classification at the same time,
|
||||
and we have tokens such as `[CLS]` that shouldn't be masked.
|
||||
* `n_tokens` total number of tokens (used for generating random tokens)
|
||||
* `masking_prob` is the masking probability
|
||||
* `randomize_prob` is the probability of replacing with a random token
|
||||
* `no_change_prob` is the probability of replacing with original token
|
||||
"""
|
||||
self.n_tokens = n_tokens
|
||||
self.no_change_prob = no_change_prob
|
||||
self.randomize_prob = randomize_prob
|
||||
self.masking_prob = masking_prob
|
||||
self.no_mask_tokens = no_mask_tokens + [padding_token, mask_token]
|
||||
self.padding_token = padding_token
|
||||
self.mask_token = mask_token
|
||||
|
||||
def __call__(self, x: torch.Tensor):
|
||||
"""
|
||||
* `x` is the batch of input token sequences.
|
||||
It's a tensor of type `long` with shape `[seq_len, batch_size]`.
|
||||
"""
|
||||
|
||||
# Mask `masking_prob` of tokens
|
||||
full_mask = torch.rand(x.shape, device=x.device) < self.masking_prob
|
||||
# Unmask `no_mask_tokens`
|
||||
for t in self.no_mask_tokens:
|
||||
full_mask &= x != t
|
||||
|
||||
# A mask for tokens to be replaced with original tokens
|
||||
unchanged = full_mask & (torch.rand(x.shape, device=x.device) < self.no_change_prob)
|
||||
# A mask for tokens to be replaced with a random token
|
||||
random_token_mask = full_mask & (torch.rand(x.shape, device=x.device) < self.randomize_prob)
|
||||
# Indexes of tokens to be replaced with random tokens
|
||||
random_token_idx = torch.nonzero(random_token_mask, as_tuple=True)
|
||||
# Random tokens for each of the locations
|
||||
random_tokens = torch.randint(0, self.n_tokens, (len(random_token_idx[0]),), device=x.device)
|
||||
# The final set of tokens that are going to be replaced by `[MASK]`
|
||||
mask = full_mask & ~random_token_mask & ~unchanged
|
||||
|
||||
# Make a clone of the input for the labels
|
||||
y = x.clone()
|
||||
|
||||
# Replace with `[MASK]` tokens;
|
||||
# note that this doesn't include the tokens that will have the original token unchanged and
|
||||
# those that get replace with a random token.
|
||||
x.masked_fill_(mask, self.mask_token)
|
||||
# Assign random tokens
|
||||
x[random_token_idx] = random_tokens
|
||||
|
||||
# Assign token `[PAD]` to all the other locations in the labels.
|
||||
# The labels equal to `[PAD]` will not be used in the loss.
|
||||
y.masked_fill_(~full_mask, self.padding_token)
|
||||
|
||||
# Return the masked input and the labels
|
||||
return x, y
|
||||
@@ -0,0 +1,308 @@
|
||||
"""
|
||||
---
|
||||
title: Masked Language Model Experiment
|
||||
summary: This experiment trains Masked Language Model (MLM) on Tiny Shakespeare dataset.
|
||||
---
|
||||
|
||||
# [Masked Language Model (MLM)](index.html) Experiment
|
||||
|
||||
This is an annotated PyTorch experiment to train a [Masked Language Model](index.html).
|
||||
"""
|
||||
from typing import List
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
from labml import experiment, tracker, logger
|
||||
from labml.configs import option
|
||||
from labml.logger import Text
|
||||
from labml_nn.helpers.metrics import Accuracy
|
||||
from labml_nn.helpers.trainer import BatchIndex
|
||||
from labml_nn.experiments.nlp_autoregression import NLPAutoRegressionConfigs
|
||||
from labml_nn.transformers import Encoder, Generator
|
||||
from labml_nn.transformers import TransformerConfigs
|
||||
from labml_nn.transformers.mlm import MLM
|
||||
|
||||
|
||||
class TransformerMLM(nn.Module):
|
||||
"""
|
||||
# Transformer based model for MLM
|
||||
"""
|
||||
|
||||
def __init__(self, *, encoder: Encoder, src_embed: nn.Module, generator: Generator):
|
||||
"""
|
||||
* `encoder` is the transformer [Encoder](../models.html#Encoder)
|
||||
* `src_embed` is the token
|
||||
[embedding module (with positional encodings)](../models.html#EmbeddingsWithLearnedPositionalEncoding)
|
||||
* `generator` is the [final fully connected layer](../models.html#Generator) that gives the logits.
|
||||
"""
|
||||
super().__init__()
|
||||
self.generator = generator
|
||||
self.src_embed = src_embed
|
||||
self.encoder = encoder
|
||||
|
||||
def forward(self, x: torch.Tensor):
|
||||
# Get the token embeddings with positional encodings
|
||||
x = self.src_embed(x)
|
||||
# Transformer encoder
|
||||
x = self.encoder(x, None)
|
||||
# Logits for the output
|
||||
y = self.generator(x)
|
||||
|
||||
# Return results
|
||||
# (second value is for state, since our trainer is used with RNNs also)
|
||||
return y, None
|
||||
|
||||
|
||||
class Configs(NLPAutoRegressionConfigs):
|
||||
"""
|
||||
## Configurations
|
||||
|
||||
This inherits from
|
||||
[`NLPAutoRegressionConfigs`](../../experiments/nlp_autoregression.html)
|
||||
because it has the data pipeline implementations that we reuse here.
|
||||
We have implemented a custom training step form MLM.
|
||||
"""
|
||||
|
||||
# MLM model
|
||||
model: TransformerMLM
|
||||
# Transformer
|
||||
transformer: TransformerConfigs
|
||||
|
||||
# Number of tokens
|
||||
n_tokens: int = 'n_tokens_mlm'
|
||||
# Tokens that shouldn't be masked
|
||||
no_mask_tokens: List[int] = []
|
||||
# Probability of masking a token
|
||||
masking_prob: float = 0.15
|
||||
# Probability of replacing the mask with a random token
|
||||
randomize_prob: float = 0.1
|
||||
# Probability of replacing the mask with original token
|
||||
no_change_prob: float = 0.1
|
||||
# [Masked Language Model (MLM) class](index.html) to generate the mask
|
||||
mlm: MLM
|
||||
|
||||
# `[MASK]` token
|
||||
mask_token: int
|
||||
# `[PADDING]` token
|
||||
padding_token: int
|
||||
|
||||
# Prompt to sample
|
||||
prompt: str = [
|
||||
"We are accounted poor citizens, the patricians good.",
|
||||
"What authority surfeits on would relieve us: if they",
|
||||
"would yield us but the superfluity, while it were",
|
||||
"wholesome, we might guess they relieved us humanely;",
|
||||
"but they think we are too dear: the leanness that",
|
||||
"afflicts us, the object of our misery, is as an",
|
||||
"inventory to particularise their abundance; our",
|
||||
"sufferance is a gain to them Let us revenge this with",
|
||||
"our pikes, ere we become rakes: for the gods know I",
|
||||
"speak this in hunger for bread, not in thirst for revenge.",
|
||||
]
|
||||
|
||||
def init(self):
|
||||
"""
|
||||
### Initialization
|
||||
"""
|
||||
|
||||
# `[MASK]` token
|
||||
self.mask_token = self.n_tokens - 1
|
||||
# `[PAD]` token
|
||||
self.padding_token = self.n_tokens - 2
|
||||
|
||||
# [Masked Language Model (MLM) class](index.html) to generate the mask
|
||||
self.mlm = MLM(padding_token=self.padding_token,
|
||||
mask_token=self.mask_token,
|
||||
no_mask_tokens=self.no_mask_tokens,
|
||||
n_tokens=self.n_tokens,
|
||||
masking_prob=self.masking_prob,
|
||||
randomize_prob=self.randomize_prob,
|
||||
no_change_prob=self.no_change_prob)
|
||||
|
||||
# Accuracy metric (ignore the labels equal to `[PAD]`)
|
||||
self.accuracy = Accuracy(ignore_index=self.padding_token)
|
||||
# Cross entropy loss (ignore the labels equal to `[PAD]`)
|
||||
self.loss_func = nn.CrossEntropyLoss(ignore_index=self.padding_token)
|
||||
#
|
||||
super().init()
|
||||
|
||||
def step(self, batch: any, batch_idx: BatchIndex):
|
||||
"""
|
||||
### Training or validation step
|
||||
"""
|
||||
|
||||
# Move the input to the device
|
||||
data = batch[0].to(self.device)
|
||||
|
||||
# Update global step (number of tokens processed) when in training mode
|
||||
if self.mode.is_train:
|
||||
tracker.add_global_step(data.shape[0] * data.shape[1])
|
||||
|
||||
# Get the masked input and labels
|
||||
with torch.no_grad():
|
||||
data, labels = self.mlm(data)
|
||||
|
||||
# Get model outputs.
|
||||
# It's returning a tuple for states when using RNNs.
|
||||
# This is not implemented yet.
|
||||
output, *_ = self.model(data)
|
||||
|
||||
# Calculate and log the loss
|
||||
loss = self.loss_func(output.view(-1, output.shape[-1]), labels.view(-1))
|
||||
tracker.add("loss.", loss)
|
||||
|
||||
# Calculate and log accuracy
|
||||
self.accuracy(output, labels)
|
||||
self.accuracy.track()
|
||||
|
||||
# Train the model
|
||||
if self.mode.is_train:
|
||||
# Calculate gradients
|
||||
loss.backward()
|
||||
# Clip gradients
|
||||
torch.nn.utils.clip_grad_norm_(self.model.parameters(), max_norm=self.grad_norm_clip)
|
||||
# Take optimizer step
|
||||
self.optimizer.step()
|
||||
# Log the model parameters and gradients on last batch of every epoch
|
||||
if batch_idx.is_last:
|
||||
tracker.add('model', self.model)
|
||||
# Clear the gradients
|
||||
self.optimizer.zero_grad()
|
||||
|
||||
# Save the tracked metrics
|
||||
tracker.save()
|
||||
|
||||
@torch.no_grad()
|
||||
def sample(self):
|
||||
"""
|
||||
### Sampling function to generate samples periodically while training
|
||||
"""
|
||||
|
||||
# Empty tensor for data filled with `[PAD]`.
|
||||
data = torch.full((self.seq_len, len(self.prompt)), self.padding_token, dtype=torch.long)
|
||||
# Add the prompts one by one
|
||||
for i, p in enumerate(self.prompt):
|
||||
# Get token indexes
|
||||
d = self.text.text_to_i(p)
|
||||
# Add to the tensor
|
||||
s = min(self.seq_len, len(d))
|
||||
data[:s, i] = d[:s]
|
||||
# Move the tensor to current device
|
||||
data = data.to(self.device)
|
||||
|
||||
# Get masked input and labels
|
||||
data, labels = self.mlm(data)
|
||||
# Get model outputs
|
||||
output, *_ = self.model(data)
|
||||
|
||||
# Print the samples generated
|
||||
for j in range(data.shape[1]):
|
||||
# Collect output from printing
|
||||
log = []
|
||||
# For each token
|
||||
for i in range(len(data)):
|
||||
# If the label is not `[PAD]`
|
||||
if labels[i, j] != self.padding_token:
|
||||
# Get the prediction
|
||||
t = output[i, j].argmax().item()
|
||||
# If it's a printable character
|
||||
if t < len(self.text.itos):
|
||||
# Correct prediction
|
||||
if t == labels[i, j]:
|
||||
log.append((self.text.itos[t], Text.value))
|
||||
# Incorrect prediction
|
||||
else:
|
||||
log.append((self.text.itos[t], Text.danger))
|
||||
# If it's not a printable character
|
||||
else:
|
||||
log.append(('*', Text.danger))
|
||||
# If the label is `[PAD]` (unmasked) print the original.
|
||||
elif data[i, j] < len(self.text.itos):
|
||||
log.append((self.text.itos[data[i, j]], Text.subtle))
|
||||
|
||||
# Print
|
||||
logger.log(log)
|
||||
|
||||
|
||||
@option(Configs.n_tokens)
|
||||
def n_tokens_mlm(c: Configs):
|
||||
"""
|
||||
Number of tokens including `[PAD]` and `[MASK]`
|
||||
"""
|
||||
return c.text.n_tokens + 2
|
||||
|
||||
|
||||
@option(Configs.transformer)
|
||||
def _transformer_configs(c: Configs):
|
||||
"""
|
||||
### Transformer configurations
|
||||
"""
|
||||
|
||||
# We use our
|
||||
# [configurable transformer implementation](../configs.html#TransformerConfigs)
|
||||
conf = TransformerConfigs()
|
||||
# Set the vocabulary sizes for embeddings and generating logits
|
||||
conf.n_src_vocab = c.n_tokens
|
||||
conf.n_tgt_vocab = c.n_tokens
|
||||
# Embedding size
|
||||
conf.d_model = c.d_model
|
||||
|
||||
#
|
||||
return conf
|
||||
|
||||
|
||||
@option(Configs.model)
|
||||
def _model(c: Configs):
|
||||
"""
|
||||
Create classification model
|
||||
"""
|
||||
m = TransformerMLM(encoder=c.transformer.encoder,
|
||||
src_embed=c.transformer.src_embed,
|
||||
generator=c.transformer.generator).to(c.device)
|
||||
|
||||
return m
|
||||
|
||||
|
||||
def main():
|
||||
# Create experiment
|
||||
experiment.create(name="mlm")
|
||||
# Create configs
|
||||
conf = Configs()
|
||||
# Override configurations
|
||||
experiment.configs(conf, {
|
||||
# Batch size
|
||||
'batch_size': 64,
|
||||
# Sequence length of $32$. We use a short sequence length to train faster.
|
||||
# Otherwise it takes forever to train.
|
||||
'seq_len': 32,
|
||||
|
||||
# Train for 1024 epochs.
|
||||
'epochs': 1024,
|
||||
# Switch between training and validation for $1$ times
|
||||
# per epoch
|
||||
'inner_iterations': 1,
|
||||
|
||||
# Transformer configurations (same as defaults)
|
||||
'd_model': 128,
|
||||
'transformer.ffn.d_ff': 256,
|
||||
'transformer.n_heads': 8,
|
||||
'transformer.n_layers': 6,
|
||||
|
||||
# Use [Noam optimizer](../../optimizers/noam.html)
|
||||
'optimizer.optimizer': 'Noam',
|
||||
'optimizer.learning_rate': 1.,
|
||||
})
|
||||
|
||||
# Set models for saving and loading
|
||||
experiment.add_pytorch_models({'model': conf.model})
|
||||
|
||||
# Start the experiment
|
||||
with experiment.start():
|
||||
# Run training
|
||||
conf.run()
|
||||
|
||||
|
||||
#
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,55 @@
|
||||
# [Masked Language Model (MLM)](https://nn.labml.ai/transformers/mlm/index.html)
|
||||
|
||||
This is a [PyTorch](https://pytorch.org) implementation of Masked Language Model (MLM)
|
||||
used to pre-train the BERT model introduced in the paper
|
||||
[BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding](https://arxiv.org/abs/1810.04805).
|
||||
|
||||
## BERT Pretraining
|
||||
|
||||
BERT model is a transformer model.
|
||||
The paper pre-trains the model using MLM and with next sentence prediction.
|
||||
We have only implemented MLM here.
|
||||
|
||||
### Next sentence prediction
|
||||
|
||||
In *next sentence prediction*, the model is given two sentences `A` and `B` and the model
|
||||
makes a binary prediction whether `B` is the sentence that follows `A` in the actual text.
|
||||
The model is fed with actual sentence pairs 50% of the time and random pairs 50% of the time.
|
||||
This classification is done while applying MLM. *We haven't implemented this here.*
|
||||
|
||||
## Masked LM
|
||||
|
||||
This masks a percentage of tokens at random and trains the model to predict
|
||||
the masked tokens.
|
||||
They **mask 15% of the tokens** by replacing them with a special `[MASK]` token.
|
||||
|
||||
The loss is computed on predicting the masked tokens only.
|
||||
This causes a problem during fine-tuning and actual usage since there are no `[MASK]` tokens
|
||||
at that time.
|
||||
Therefore we might not get any meaningful representations.
|
||||
|
||||
To overcome this **10% of the masked tokens are replaced with the original token**,
|
||||
and another **10% of the masked tokens are replaced with a random token**.
|
||||
This trains the model to give representations about the actual token whether or not the
|
||||
input token at that position is a `[MASK]`.
|
||||
And replacing with a random token causes it to
|
||||
give a representation that has information from the context as well;
|
||||
because it has to use the context to fix randomly replaced tokens.
|
||||
|
||||
## Training
|
||||
|
||||
MLMs are harder to train than autoregressive models because they have a smaller training signal.
|
||||
i.e. only a small percentage of predictions are trained per sample.
|
||||
|
||||
Another problem is since the model is bidirectional, any token can see any other token.
|
||||
This makes the "credit assignment" harder.
|
||||
Let's say you have the character level model trying to predict `home *s where i want to be`.
|
||||
At least during the early stages of the training, it'll be super hard to figure out why the
|
||||
replacement for `*` should be `i`, it could be anything from the whole sentence.
|
||||
Whilst, in an autoregressive setting the model will only have to use `h` to predict `o` and
|
||||
`hom` to predict `e` and so on. So the model will initially start predicting with a shorter context first
|
||||
and then learn to use longer contexts later.
|
||||
Since MLMs have this problem it's a lot faster to train if you start with a smaller sequence length
|
||||
initially and then use a longer sequence length later.
|
||||
|
||||
Here is [the training code](https://nn.labml.ai/transformers/mlm/experiment.html) for a simple MLM model.
|
||||
@@ -0,0 +1,78 @@
|
||||
"""
|
||||
---
|
||||
title: "MLP-Mixer: An all-MLP Architecture for Vision"
|
||||
summary: >
|
||||
This is an annotated implementation/tutorial of MLP-Mixer: An all-MLP Architecture for Vision in PyTorch.
|
||||
---
|
||||
|
||||
# MLP-Mixer: An all-MLP Architecture for Vision
|
||||
|
||||
This is a [PyTorch](https://pytorch.org) implementation of the paper
|
||||
[MLP-Mixer: An all-MLP Architecture for Vision](https://arxiv.org/abs/2105.01601).
|
||||
|
||||
This paper applies the model on vision tasks.
|
||||
The model is similar to a transformer with attention layer being replaced by a MLP
|
||||
that is applied across the patches (or tokens in case of a NLP task).
|
||||
|
||||
Our implementation of MLP Mixer is a drop in replacement for the [self-attention layer](../mha.html)
|
||||
in [our transformer implementation](../models.html).
|
||||
So it's just a couple of lines of code, transposing the tensor to apply the MLP
|
||||
across the sequence dimension.
|
||||
|
||||
Although the paper applied MLP Mixer on vision tasks,
|
||||
we tried it on a [masked language model](../mlm/index.html).
|
||||
[Here is the experiment code](experiment.html).
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
|
||||
class MLPMixer(nn.Module):
|
||||
"""
|
||||
## MLP Mixer
|
||||
|
||||
This module is a drop-in replacement for [self-attention layer](../mha.html).
|
||||
It transposes the input tensor before feeding it to the MLP and transposes back,
|
||||
so that the MLP is applied across the sequence dimension (across tokens or image patches) instead
|
||||
of the feature dimension.
|
||||
"""
|
||||
|
||||
def __init__(self, mlp: nn.Module):
|
||||
"""
|
||||
* `ffn` is the MLP module.
|
||||
"""
|
||||
super().__init__()
|
||||
self.mlp = mlp
|
||||
|
||||
def forward(self, query: torch.Tensor, key: torch.Tensor, value: torch.Tensor, mask: Optional[torch.Tensor] = None):
|
||||
"""
|
||||
The [normal attention module](../mha.html) can be fed with different token embeddings for
|
||||
$\text{query}$,$\text{key}$, and $\text{value}$ and a mask.
|
||||
|
||||
We follow the same function signature so that we can replace it directly.
|
||||
|
||||
For MLP mixing, $$x = \text{query} = \text{key} = \text{value}$$ and masking is not possible.
|
||||
Shape of `query` (and `key` and `value`) is `[seq_len, batch_size, d_model]`.
|
||||
"""
|
||||
|
||||
# $\text{query}$,$\text{key}$, and $\text{value}$ all should be the same
|
||||
assert query is key and key is value
|
||||
# MLP mixer doesn't support masking. i.e. all tokens will see all other token embeddings.
|
||||
assert mask is None
|
||||
|
||||
# Assign to `x` for clarity
|
||||
x = query
|
||||
|
||||
# Transpose so that the last dimension is the sequence dimension.
|
||||
# New shape is `[d_model, batch_size, seq_len]`
|
||||
x = x.transpose(0, 2)
|
||||
# Apply the MLP across tokens
|
||||
x = self.mlp(x)
|
||||
# Transpose back into original form
|
||||
x = x.transpose(0, 2)
|
||||
|
||||
#
|
||||
return x
|
||||
@@ -0,0 +1,115 @@
|
||||
"""
|
||||
---
|
||||
title: MLP Mixer experiment
|
||||
summary: This experiment trains MLP Mixer on Tiny Shakespeare dataset.
|
||||
---
|
||||
|
||||
# [MLP Mixer](index.html) Experiment
|
||||
|
||||
This is an annotated PyTorch experiment to train a [MLP Mixer Model](index.html).
|
||||
"""
|
||||
|
||||
from labml import experiment
|
||||
from labml.configs import option
|
||||
from labml_nn.transformers import TransformerConfigs
|
||||
from labml_nn.transformers.configs import FeedForwardConfigs
|
||||
from labml_nn.transformers.mlm.experiment import TransformerMLM, Configs as MLMConfigs
|
||||
|
||||
|
||||
class Configs(MLMConfigs):
|
||||
"""
|
||||
## Configurations
|
||||
|
||||
This inherits from
|
||||
[`MLMConfigs`](../mlm/experiment.html) where we define an experiment for
|
||||
[Masked Language Models](../mlm.index.html).
|
||||
"""
|
||||
|
||||
# Configurable [Feed-Forward Network](../feed_forward.html) for the MLP
|
||||
mix_mlp: FeedForwardConfigs
|
||||
|
||||
|
||||
@option(Configs.mix_mlp)
|
||||
def _mix_mlp_configs(c: Configs):
|
||||
"""
|
||||
The mixing MLP configurations
|
||||
"""
|
||||
|
||||
conf = FeedForwardConfigs()
|
||||
# Size of the MLP is the sequence length, because it is applied across tokens
|
||||
conf.d_model = c.seq_len
|
||||
# The paper suggests $GELU$ activation
|
||||
conf.activation = 'GELU'
|
||||
|
||||
#
|
||||
return conf
|
||||
|
||||
|
||||
@option(Configs.transformer)
|
||||
def _transformer_configs(c: Configs):
|
||||
"""
|
||||
### Transformer configurations
|
||||
"""
|
||||
|
||||
# We use our
|
||||
# [configurable transformer implementation](../configs.html#TransformerConfigs)
|
||||
conf = TransformerConfigs()
|
||||
# Set the vocabulary sizes for embeddings and generating logits
|
||||
conf.n_src_vocab = c.n_tokens
|
||||
conf.n_tgt_vocab = c.n_tokens
|
||||
# Embedding size
|
||||
conf.d_model = c.d_model
|
||||
# Change attention module to [MLPMixer](index.html)
|
||||
from labml_nn.transformers.mlp_mixer import MLPMixer
|
||||
conf.encoder_attn = MLPMixer(c.mix_mlp.ffn)
|
||||
|
||||
#
|
||||
return conf
|
||||
|
||||
|
||||
def main():
|
||||
# Create experiment
|
||||
experiment.create(name="mlp_mixer_mlm")
|
||||
# Create configs
|
||||
conf = Configs()
|
||||
# Override configurations
|
||||
experiment.configs(conf, {
|
||||
# Batch size
|
||||
'batch_size': 64,
|
||||
# Sequence length of $32$. We use a short sequence length to train faster.
|
||||
# Otherwise MLM models take forever to train.
|
||||
'seq_len': 32,
|
||||
|
||||
# Train for 1024 epochs.
|
||||
'epochs': 1024,
|
||||
# Switch between training and validation for $1$ times
|
||||
# per epoch
|
||||
'inner_iterations': 1,
|
||||
|
||||
# Transformer configurations
|
||||
'd_model': 128,
|
||||
'transformer.ffn.d_ff': 256,
|
||||
'transformer.n_heads': 8,
|
||||
'transformer.n_layers': 6,
|
||||
'transformer.ffn.activation': 'GELU',
|
||||
|
||||
# Mixer MLP hidden layer size
|
||||
'mix_mlp.d_ff': 128,
|
||||
|
||||
# Use [Noam optimizer](../../optimizers/noam.html)
|
||||
'optimizer.optimizer': 'Noam',
|
||||
'optimizer.learning_rate': 1.,
|
||||
})
|
||||
|
||||
# Set models for saving and loading
|
||||
experiment.add_pytorch_models({'model': conf.model})
|
||||
|
||||
# Start the experiment
|
||||
with experiment.start():
|
||||
# Run training
|
||||
conf.run()
|
||||
|
||||
|
||||
#
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,17 @@
|
||||
# [MLP-Mixer: An all-MLP Architecture for Vision](https://nn.labml.ai/transformers/mlp_mixer/index.html)
|
||||
|
||||
This is a [PyTorch](https://pytorch.org) implementation of the paper
|
||||
[MLP-Mixer: An all-MLP Architecture for Vision](https://arxiv.org/abs/2105.01601).
|
||||
|
||||
This paper applies the model on vision tasks.
|
||||
The model is similar to a transformer with attention layer being replaced by a MLP
|
||||
that is applied across the patches (or tokens in case of a NLP task).
|
||||
|
||||
Our implementation of MLP Mixer is a drop in replacement for the [self-attention layer](https://nn.labml.ai/transformers/mha.html)
|
||||
in [our transformer implementation](https://nn.labml.ai/transformers/models.html).
|
||||
So it's just a couple of lines of code, transposing the tensor to apply the MLP
|
||||
across the sequence dimension.
|
||||
|
||||
Although the paper applied MLP Mixer on vision tasks,
|
||||
we tried it on a [masked language model](https://nn.labml.ai/transformers/mlm/index.html).
|
||||
[Here is the experiment code](https://nn.labml.ai/transformers/mlp_mixer/experiment.html).
|
||||
@@ -0,0 +1,224 @@
|
||||
"""
|
||||
---
|
||||
title: Transformer Encoder and Decoder Models
|
||||
summary: >
|
||||
These are PyTorch implementations of Transformer based encoder and decoder models,
|
||||
as well as other related modules.
|
||||
---
|
||||
|
||||
# Transformer Encoder and Decoder Models
|
||||
|
||||
[](https://colab.research.google.com/github/labmlai/annotated_deep_learning_paper_implementations/blob/master/labml_nn/transformers/basic/autoregressive_experiment.ipynb)
|
||||
"""
|
||||
import math
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
from labml_nn.utils import clone_module_list
|
||||
from .feed_forward import FeedForward
|
||||
from .mha import MultiHeadAttention
|
||||
from .positional_encoding import get_positional_encoding
|
||||
|
||||
|
||||
class EmbeddingsWithPositionalEncoding(nn.Module):
|
||||
"""
|
||||
<a id="EmbeddingsWithPositionalEncoding"></a>
|
||||
|
||||
## Embed tokens and add [fixed positional encoding](positional_encoding.html)
|
||||
"""
|
||||
|
||||
def __init__(self, d_model: int, n_vocab: int, max_len: int = 5000):
|
||||
super().__init__()
|
||||
self.linear = nn.Embedding(n_vocab, d_model)
|
||||
self.d_model = d_model
|
||||
self.register_buffer('positional_encodings', get_positional_encoding(d_model, max_len))
|
||||
|
||||
def forward(self, x: torch.Tensor):
|
||||
pe = self.positional_encodings[:x.shape[0]].requires_grad_(False)
|
||||
return self.linear(x) * math.sqrt(self.d_model) + pe
|
||||
|
||||
|
||||
class EmbeddingsWithLearnedPositionalEncoding(nn.Module):
|
||||
"""
|
||||
<a id="EmbeddingsWithLearnedPositionalEncoding"></a>
|
||||
|
||||
## Embed tokens and add parameterized positional encodings
|
||||
"""
|
||||
|
||||
def __init__(self, d_model: int, n_vocab: int, max_len: int = 5000):
|
||||
super().__init__()
|
||||
self.linear = nn.Embedding(n_vocab, d_model)
|
||||
self.d_model = d_model
|
||||
self.positional_encodings = nn.Parameter(torch.zeros(max_len, 1, d_model), requires_grad=True)
|
||||
|
||||
def forward(self, x: torch.Tensor):
|
||||
pe = self.positional_encodings[:x.shape[0]]
|
||||
return self.linear(x) * math.sqrt(self.d_model) + pe
|
||||
|
||||
|
||||
class TransformerLayer(nn.Module):
|
||||
"""
|
||||
<a id="TransformerLayer"></a>
|
||||
|
||||
## Transformer Layer
|
||||
|
||||
This can act as an encoder layer or a decoder layer. We use pre-norm.
|
||||
"""
|
||||
|
||||
def __init__(self, *,
|
||||
d_model: int,
|
||||
self_attn: MultiHeadAttention,
|
||||
src_attn: MultiHeadAttention = None,
|
||||
feed_forward: FeedForward,
|
||||
dropout_prob: float):
|
||||
"""
|
||||
* `d_model` is the token embedding size
|
||||
* `self_attn` is the self attention module
|
||||
* `src_attn` is the source attention module (when this is used in a decoder)
|
||||
* `feed_forward` is the feed forward module
|
||||
* `dropout_prob` is the probability of dropping out after self attention and FFN
|
||||
"""
|
||||
super().__init__()
|
||||
self.size = d_model
|
||||
self.self_attn = self_attn
|
||||
self.src_attn = src_attn
|
||||
self.feed_forward = feed_forward
|
||||
self.dropout = nn.Dropout(dropout_prob)
|
||||
self.norm_self_attn = nn.LayerNorm([d_model])
|
||||
if self.src_attn is not None:
|
||||
self.norm_src_attn = nn.LayerNorm([d_model])
|
||||
self.norm_ff = nn.LayerNorm([d_model])
|
||||
# Whether to save input to the feed forward layer
|
||||
self.is_save_ff_input = False
|
||||
|
||||
def forward(self, *,
|
||||
x: torch.Tensor,
|
||||
mask: torch.Tensor,
|
||||
src: torch.Tensor = None,
|
||||
src_mask: torch.Tensor = None):
|
||||
# Normalize the vectors before doing self attention
|
||||
z = self.norm_self_attn(x)
|
||||
# Run through self attention, i.e. keys and values are from self
|
||||
self_attn = self.self_attn(query=z, key=z, value=z, mask=mask)
|
||||
# Add the self attention results
|
||||
x = x + self.dropout(self_attn)
|
||||
|
||||
# If a source is provided, get results from attention to source.
|
||||
# This is when you have a decoder layer that pays attention to
|
||||
# encoder outputs
|
||||
if src is not None:
|
||||
# Normalize vectors
|
||||
z = self.norm_src_attn(x)
|
||||
# Attention to source. i.e. keys and values are from source
|
||||
attn_src = self.src_attn(query=z, key=src, value=src, mask=src_mask)
|
||||
# Add the source attention results
|
||||
x = x + self.dropout(attn_src)
|
||||
|
||||
# Normalize for feed-forward
|
||||
z = self.norm_ff(x)
|
||||
# Save the input to the feed forward layer if specified
|
||||
if self.is_save_ff_input:
|
||||
self.ff_input = z.clone()
|
||||
# Pass through the feed-forward network
|
||||
ff = self.feed_forward(z)
|
||||
# Add the feed-forward results back
|
||||
x = x + self.dropout(ff)
|
||||
|
||||
return x
|
||||
|
||||
|
||||
class Encoder(nn.Module):
|
||||
"""
|
||||
<a id="Encoder"></a>
|
||||
|
||||
## Transformer Encoder
|
||||
"""
|
||||
|
||||
def __init__(self, layer: TransformerLayer, n_layers: int):
|
||||
super().__init__()
|
||||
# Make copies of the transformer layer
|
||||
self.layers = clone_module_list(layer, n_layers)
|
||||
# Final normalization layer
|
||||
self.norm = nn.LayerNorm([layer.size])
|
||||
|
||||
def forward(self, x: torch.Tensor, mask: torch.Tensor):
|
||||
# Run through each transformer layer
|
||||
for layer in self.layers:
|
||||
x = layer(x=x, mask=mask)
|
||||
# Finally, normalize the vectors
|
||||
return self.norm(x)
|
||||
|
||||
|
||||
class Decoder(nn.Module):
|
||||
"""
|
||||
<a id="Decoder"></a>
|
||||
|
||||
## Transformer Decoder
|
||||
"""
|
||||
|
||||
def __init__(self, layer: TransformerLayer, n_layers: int):
|
||||
super().__init__()
|
||||
# Make copies of the transformer layer
|
||||
self.layers = clone_module_list(layer, n_layers)
|
||||
# Final normalization layer
|
||||
self.norm = nn.LayerNorm([layer.size])
|
||||
|
||||
def forward(self, x: torch.Tensor, memory: torch.Tensor, src_mask: torch.Tensor, tgt_mask: torch.Tensor):
|
||||
# Run through each transformer layer
|
||||
for layer in self.layers:
|
||||
x = layer(x=x, mask=tgt_mask, src=memory, src_mask=src_mask)
|
||||
# Finally, normalize the vectors
|
||||
return self.norm(x)
|
||||
|
||||
|
||||
class Generator(nn.Module):
|
||||
"""
|
||||
<a id="Generator"></a>
|
||||
|
||||
## Generator
|
||||
|
||||
This predicts the tokens and gives the lof softmax of those.
|
||||
You don't need this if you are using `nn.CrossEntropyLoss`.
|
||||
"""
|
||||
|
||||
def __init__(self, n_vocab: int, d_model: int):
|
||||
super().__init__()
|
||||
self.projection = nn.Linear(d_model, n_vocab)
|
||||
|
||||
def forward(self, x):
|
||||
return self.projection(x)
|
||||
|
||||
|
||||
class EncoderDecoder(nn.Module):
|
||||
"""
|
||||
<a id="EncoderDecoder"></a>
|
||||
|
||||
## Combined Encoder-Decoder
|
||||
"""
|
||||
|
||||
def __init__(self, encoder: Encoder, decoder: Decoder, src_embed: nn.Module, tgt_embed: nn.Module, generator: nn.Module):
|
||||
super().__init__()
|
||||
self.encoder = encoder
|
||||
self.decoder = decoder
|
||||
self.src_embed = src_embed
|
||||
self.tgt_embed = tgt_embed
|
||||
self.generator = generator
|
||||
|
||||
# This was important from their code.
|
||||
# Initialize parameters with Glorot / fan_avg.
|
||||
for p in self.parameters():
|
||||
if p.dim() > 1:
|
||||
nn.init.xavier_uniform_(p)
|
||||
|
||||
def forward(self, src: torch.Tensor, tgt: torch.Tensor, src_mask: torch.Tensor, tgt_mask: torch.Tensor):
|
||||
# Run the source through encoder
|
||||
enc = self.encode(src, src_mask)
|
||||
# Run encodings and targets through decoder
|
||||
return self.decode(enc, src_mask, tgt, tgt_mask)
|
||||
|
||||
def encode(self, src: torch.Tensor, src_mask: torch.Tensor):
|
||||
return self.encoder(self.src_embed(src), src_mask)
|
||||
|
||||
def decode(self, memory: torch.Tensor, src_mask: torch.Tensor, tgt: torch.Tensor, tgt_mask: torch.Tensor):
|
||||
return self.decoder(self.tgt_embed(tgt), memory, src_mask, tgt_mask)
|
||||
@@ -0,0 +1,76 @@
|
||||
"""
|
||||
---
|
||||
title: Fixed Positional Encodings
|
||||
summary: >
|
||||
Implementation with explanation of fixed positional encodings as
|
||||
described in paper Attention is All You Need.
|
||||
---
|
||||
|
||||
# Fixed Positional Encodings
|
||||
|
||||
The positional encoding encodes the position along the sequence into
|
||||
a vector of size `d_model`.
|
||||
|
||||
\begin{align}
|
||||
PE_{p,2i} &= sin\Bigg(\frac{p}{10000^{\frac{2i}{d_{model}}}}\Bigg) \\
|
||||
PE_{p,2i + 1} &= cos\Bigg(\frac{p}{10000^{\frac{2i}{d_{model}}}}\Bigg)
|
||||
\end{align}
|
||||
|
||||
Where $1 \leq 2i, 2i + 1 \leq d_{model}$
|
||||
are the feature indexes in the encoding, and $p$ is the position.
|
||||
"""
|
||||
|
||||
import math
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
|
||||
class PositionalEncoding(nn.Module):
|
||||
def __init__(self, d_model: int, dropout_prob: float, max_len: int = 5000):
|
||||
super().__init__()
|
||||
self.dropout = nn.Dropout(dropout_prob)
|
||||
|
||||
self.register_buffer('positional_encodings', get_positional_encoding(d_model, max_len), False)
|
||||
|
||||
def forward(self, x: torch.Tensor):
|
||||
pe = self.positional_encodings[:x.shape[0]].detach().requires_grad_(False)
|
||||
x = x + pe
|
||||
x = self.dropout(x)
|
||||
return x
|
||||
|
||||
|
||||
def get_positional_encoding(d_model: int, max_len: int = 5000):
|
||||
# Empty encodings vectors
|
||||
encodings = torch.zeros(max_len, d_model)
|
||||
# Position indexes
|
||||
position = torch.arange(0, max_len, dtype=torch.float32).unsqueeze(1)
|
||||
# $2 * i$
|
||||
two_i = torch.arange(0, d_model, 2, dtype=torch.float32)
|
||||
# $10000^{\frac{2i}{d_{model}}}$
|
||||
div_term = torch.exp(two_i * -(math.log(10000.0) / d_model))
|
||||
# $PE_{p,2i} = sin\Bigg(\frac{p}{10000^{\frac{2i}{d_{model}}}}\Bigg)$
|
||||
encodings[:, 0::2] = torch.sin(position * div_term)
|
||||
# $PE_{p,2i + 1} = cos\Bigg(\frac{p}{10000^{\frac{2i}{d_{model}}}}\Bigg)$
|
||||
encodings[:, 1::2] = torch.cos(position * div_term)
|
||||
|
||||
# Add batch dimension
|
||||
encodings = encodings.unsqueeze(1).requires_grad_(False)
|
||||
|
||||
return encodings
|
||||
|
||||
|
||||
def _test_positional_encoding():
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
plt.figure(figsize=(15, 5))
|
||||
pe = get_positional_encoding(20, 100)
|
||||
plt.plot(np.arange(100), pe[:, 0, 4:8].numpy())
|
||||
plt.legend(["dim %d" % p for p in [4, 5, 6, 7]])
|
||||
plt.title("Positional encoding")
|
||||
plt.show()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
_test_positional_encoding()
|
||||
@@ -0,0 +1,129 @@
|
||||
"""
|
||||
---
|
||||
title: "Primer: Searching for Efficient Transformers for Language Modeling"
|
||||
summary: >
|
||||
This is an annotated implementation/tutorial of
|
||||
Primer: Searching for Efficient Transformers for Language Modeling for Vision in PyTorch.
|
||||
---
|
||||
|
||||
# Primer: Searching for Efficient Transformers for Language Modeling
|
||||
|
||||
This is a [PyTorch](https://pytorch.org) implementation of the paper
|
||||
[Primer: Searching for Efficient Transformers for Language Modeling](https://arxiv.org/abs/2109.08668).
|
||||
|
||||
The authors do an evolutionary search for transformer architectures.
|
||||
They name the architecture found using the search Primer (PRIMitives searched transformER).
|
||||
**Primer EZ** is the architecture with the two most robust modifications in Primer compared to
|
||||
the original transformer.
|
||||
Primer EZ trains a lot faster than the vanilla transformer.
|
||||
|
||||
### Squared ReLU
|
||||
|
||||
The most effective modification found by the search is using a square ReLU instead of ReLU in
|
||||
the [position-wise feedforward module](../feed_forward.html).
|
||||
|
||||
$$y = {\max(x, 0)}^2$$
|
||||
|
||||
### Multi-DConv-Head Attention (MDHA)
|
||||
|
||||
The next effective modification is a depth-wise $3 \times 1$ convolution after multi-head projection
|
||||
for queries, keys, and values.
|
||||
The convolution is along the sequence dimension and per channel (depth-wise).
|
||||
To be clear, if the number of channels in each head is $d_k$ the convolution will have $1 \times 3$
|
||||
kernels for each of the $d_k$ channels.
|
||||
|
||||
[Here is the experiment code](experiment.html), for Primer EZ.
|
||||
"""
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
from labml_nn.transformers import MultiHeadAttention
|
||||
|
||||
|
||||
class SquaredReLU(nn.Module):
|
||||
"""
|
||||
## Squared ReLU activation
|
||||
|
||||
$$y = {\max(x, 0)}^2$$
|
||||
|
||||
Squared ReLU is used as the activation function in the
|
||||
[position wise feedforward module](../feed_forward.html).
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.relu = nn.ReLU()
|
||||
|
||||
def forward(self, x: torch.Tensor):
|
||||
# Apply ReLU
|
||||
x = self.relu(x)
|
||||
# Square it
|
||||
return x * x
|
||||
|
||||
|
||||
class SpatialDepthWiseConvolution(nn.Module):
|
||||
"""
|
||||
## Spatial Depth Wise Convolution
|
||||
"""
|
||||
|
||||
def __init__(self, d_k: int, kernel_size: int = 3):
|
||||
"""
|
||||
* `d_k` is the number of channels in each head
|
||||
"""
|
||||
super().__init__()
|
||||
self.kernel_size = kernel_size
|
||||
# We use PyTorch's `Conv1d` module.
|
||||
# We set the number of groups to be equal to the number of channels so that it does a separate convolution
|
||||
# (with different kernels) for each channel.
|
||||
# We add padding to both sides and later crop the right most `kernel_size - 1` results
|
||||
self.conv = nn.Conv1d(in_channels=d_k, out_channels=d_k,
|
||||
kernel_size=(kernel_size,), padding=(kernel_size - 1,), groups=d_k)
|
||||
|
||||
def forward(self, x: torch.Tensor):
|
||||
"""
|
||||
`x` has shape `[seq_len, batch_size, heads, d_k]`
|
||||
"""
|
||||
|
||||
# Get the shape
|
||||
seq_len, batch_size, heads, d_k = x.shape
|
||||
# Permute to `[batch_size, heads, d_k, seq_len]`
|
||||
x = x.permute(1, 2, 3, 0)
|
||||
# Change the shape to `[batch_size * heads, d_k, seq_len]`
|
||||
x = x.view(batch_size * heads, d_k, seq_len)
|
||||
|
||||
# 1D convolution accepts input of the form `[N, channels, sequence]`
|
||||
x = self.conv(x)
|
||||
# Crop the right most `kernel_size - 1` results since we padded both sides
|
||||
x = x[:, :, :-(self.kernel_size - 1)]
|
||||
# Reshape to `[batch_size, heads, d_k, seq_len]`
|
||||
x = x.view(batch_size, heads, d_k, seq_len)
|
||||
# Permute to `[seq_len, batch_size, heads, d_k]`
|
||||
x = x.permute(3, 0, 1, 2)
|
||||
|
||||
#
|
||||
return x
|
||||
|
||||
|
||||
class MultiDConvHeadAttention(MultiHeadAttention):
|
||||
"""
|
||||
## Multi-DConv-Head Attention (MDHA)
|
||||
|
||||
We extend our original implementation of [Multi-Head Attention](../mha.html#MHA)
|
||||
and add the spatial depth-wise convolution to query, key and value projections.
|
||||
"""
|
||||
|
||||
def __init__(self, heads: int, d_model: int, dropout_prob: float = 0.1):
|
||||
super().__init__(heads, d_model, dropout_prob)
|
||||
|
||||
# [Multi-Head Attention](../mha.html#MHA) will create query, key and value projection modules
|
||||
# `self.query`, `self.key`, and `self.value`.
|
||||
#
|
||||
# We combine a spatial depth-wise convolution layer to each of them and replace
|
||||
# `self.query`, `self.key`, and `self.value`.
|
||||
#
|
||||
# 📝 *We feel this cleaner implementation is easier to understand since it clearly shows the difference
|
||||
# between this and vanilla transformer multi-head attention*.
|
||||
self.query = nn.Sequential(self.query, SpatialDepthWiseConvolution(self.d_k))
|
||||
self.key = nn.Sequential(self.key, SpatialDepthWiseConvolution(self.d_k))
|
||||
self.value = nn.Sequential(self.value, SpatialDepthWiseConvolution(self.d_k))
|
||||
@@ -0,0 +1,60 @@
|
||||
import math
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
from labml_nn.transformers import MultiHeadAttention
|
||||
|
||||
|
||||
class SpatialDepthWiseConvolution(nn.Module):
|
||||
"""
|
||||
## Spatial Depth Wise Convolution
|
||||
|
||||
This is actually slower
|
||||
"""
|
||||
|
||||
def __init__(self, d_k: int, kernel_size: int = 3):
|
||||
"""
|
||||
* `d_k` is the number of channels in each head
|
||||
"""
|
||||
super().__init__()
|
||||
self.kernel_size = kernel_size
|
||||
# We use PyTorch's `Conv1d` module.
|
||||
# We set the number of groups to be equal to the number of channels so that it does a separate convolution
|
||||
# (with different kernels) for each channel.
|
||||
# We add padding to both sides and later crop the right most `kernel_size - 1` results
|
||||
rng = 1 / math.sqrt(kernel_size)
|
||||
self.kernels = nn.Parameter(torch.zeros((kernel_size, d_k)).uniform_(-rng, rng))
|
||||
|
||||
def forward(self, x: torch.Tensor):
|
||||
"""
|
||||
`x` has shape `[seq_len, batch_size, heads, d_k]`
|
||||
"""
|
||||
|
||||
res = x * self.kernels[0].view(1, 1, 1, -1)
|
||||
|
||||
for i in range(1, len(self.kernels)):
|
||||
res[i:] += x[:-i] * self.kernels[i].view(1, 1, 1, -1)
|
||||
|
||||
return res
|
||||
|
||||
|
||||
class MultiDConvHeadAttention(MultiHeadAttention):
|
||||
"""
|
||||
## Multi-DConv-Head Attention (MDHA)
|
||||
|
||||
We extend our original implementation of [Multi-Head Attention](../mha.html#MHA)
|
||||
and add the spatial depth-wise convolution to query, key and value projections.
|
||||
"""
|
||||
|
||||
def __init__(self, heads: int, d_model: int, dropout_prob: float = 0.1):
|
||||
super().__init__(heads, d_model, dropout_prob)
|
||||
|
||||
# [Multi-Head Attention](../mha.html#MHA) will create query, key and value projection modules
|
||||
# `self.query`, `self.key`, and `self.value`.
|
||||
#
|
||||
# We combine a spatial depth-wise convolution layer to each of them and replace
|
||||
# `self.query`, `self.key`, and `self.value`.
|
||||
self.query = nn.Sequential(self.query, SpatialDepthWiseConvolution(self.d_k))
|
||||
self.key = nn.Sequential(self.key, SpatialDepthWiseConvolution(self.d_k))
|
||||
self.value = nn.Sequential(self.value, SpatialDepthWiseConvolution(self.d_k))
|
||||
@@ -0,0 +1,126 @@
|
||||
"""
|
||||
---
|
||||
title: Primer EZ experiment
|
||||
summary: This experiment trains Primer EZ on Tiny Shakespeare dataset.
|
||||
---
|
||||
|
||||
# [Primer EZ](index.html) Experiment
|
||||
|
||||
This is an annotated PyTorch experiment to train a [Primer EZ transformer](index.html).
|
||||
|
||||
This is based on our [vanilla transformer experiment](../basic/experiment.html).
|
||||
We use the same experiment and add the Primer EZ modifications.
|
||||
"""
|
||||
|
||||
from labml import experiment
|
||||
from labml.configs import option
|
||||
from labml_nn.transformers import TransformerConfigs
|
||||
from labml_nn.transformers.basic.autoregressive_experiment import Configs
|
||||
from labml_nn.transformers.configs import FeedForwardConfigs
|
||||
from labml_nn.transformers.primer_ez import SquaredReLU
|
||||
|
||||
|
||||
@option(FeedForwardConfigs.activation, 'SquaredReLU')
|
||||
def _squared_relu():
|
||||
"""
|
||||
Add the [option](https://docs.labml.ai/api/configs.html#labml.configs.option)
|
||||
of [**squared ReLU**](index.html) to [configurable](../configs.html#FFN)
|
||||
[feed forward module](../feed_forward.html).
|
||||
"""
|
||||
return SquaredReLU()
|
||||
|
||||
|
||||
@option(TransformerConfigs.encoder_attn, 'MultiDConvHeadAttention')
|
||||
def _d_conv_mha(c: TransformerConfigs):
|
||||
"""
|
||||
Add the [option](https://docs.labml.ai/api/configs.html#labml.configs.option)
|
||||
of [**Multi-DConv-Head Attention**](index.html) to
|
||||
[configurable transformer](../configs.html#TransformerConfigs)
|
||||
"""
|
||||
from labml_nn.transformers.primer_ez import MultiDConvHeadAttention
|
||||
return MultiDConvHeadAttention(c.n_heads, c.d_model, dropout_prob=c.dropout)
|
||||
|
||||
|
||||
@option(TransformerConfigs.encoder_attn, 'MultiDSharedConvHeadAttention')
|
||||
def _d_shared_conv_mha(c: TransformerConfigs):
|
||||
"""
|
||||
Add the [option](https://docs.labml.ai/api/configs.html#labml.configs.option)
|
||||
of [**Multi Depth-wise Shared Conv Head Attention**](variations.html) to
|
||||
[configurable transformer](../configs.html#TransformerConfigs)
|
||||
|
||||
📝 *This is a variation we tried*
|
||||
"""
|
||||
from labml_nn.transformers.primer_ez.variations import MultiDSharedConvHeadAttention
|
||||
return MultiDSharedConvHeadAttention(c.n_heads, c.d_model, dropout_prob=c.dropout)
|
||||
|
||||
|
||||
@option(TransformerConfigs.encoder_attn, 'MultiDPHConvHeadAttention')
|
||||
def _d_per_head_conv_mha(c: TransformerConfigs):
|
||||
"""
|
||||
Add the [option](https://docs.labml.ai/api/configs.html#labml.configs.option)
|
||||
of [**Multi Depth-wise Per Head Conv Head Attention**](variation.html) to
|
||||
[configurable transformer](../configs.html#TransformerConfigs)
|
||||
|
||||
📝 *This is a variation we tried*
|
||||
"""
|
||||
from labml_nn.transformers.primer_ez.variations import MultiDPHConvHeadAttention
|
||||
return MultiDPHConvHeadAttention(c.n_heads, c.d_model, dropout_prob=c.dropout)
|
||||
|
||||
|
||||
def main():
|
||||
# Create experiment
|
||||
experiment.create(name="primer_ez")
|
||||
# Create configs
|
||||
conf = Configs()
|
||||
# Override configurations
|
||||
experiment.configs(conf, {
|
||||
# Use character level tokenizer
|
||||
'tokenizer': 'character',
|
||||
# Prompt separator is blank
|
||||
'prompt_separator': '',
|
||||
# Starting prompt for sampling
|
||||
'prompt': 'It is ',
|
||||
# Use Tiny Shakespeare dataset
|
||||
'text': 'tiny_shakespeare',
|
||||
|
||||
# Use a context size of $256$
|
||||
'seq_len': 256,
|
||||
# Train for $128$ epochs
|
||||
'epochs': 128,
|
||||
# Batch size $32$
|
||||
'batch_size': 32,
|
||||
# Switch between training and validation for $10$ times
|
||||
# per epoch
|
||||
'inner_iterations': 10,
|
||||
|
||||
# Model size
|
||||
'd_model': 512,
|
||||
'transformer.ffn.d_ff': 2048,
|
||||
|
||||
# Use Adam optimizer
|
||||
'optimizer.optimizer': 'Adam',
|
||||
'optimizer.learning_rate': 2.5e-4,
|
||||
|
||||
# ⭐️ Use [**squared ReLU**](index.html) activation in the feed forward network.
|
||||
#
|
||||
# *Replace this with `ReLU` for $ReLU$.*
|
||||
'transformer.ffn.activation': 'SquaredReLU',
|
||||
|
||||
# ⭐️ Use [**Multi-DConv-Head Attention**](index.html) for encoder attention.
|
||||
#
|
||||
# *Replace this with `mha` for original multi-head attention.*
|
||||
'transformer.encoder_attn': 'MultiDConvHeadAttention',
|
||||
})
|
||||
|
||||
# Set models for saving and loading
|
||||
experiment.add_pytorch_models({'model': conf.model})
|
||||
|
||||
# Start the experiment
|
||||
with experiment.start():
|
||||
# Run training
|
||||
conf.run()
|
||||
|
||||
|
||||
#
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,25 @@
|
||||
# [Primer: Searching for Efficient Transformers for Language Modeling](https://nn.labml.ai/transformers/primer_ez/index.html)
|
||||
|
||||
This is a [PyTorch](https://pytorch.org) implementation of the paper
|
||||
[Primer: Searching for Efficient Transformers for Language Modeling](https://arxiv.org/abs/2109.08668).
|
||||
|
||||
The authors do an evolutionary search for transformer architectures.
|
||||
They name the architecture found using the search as Primer (PRIMitives searched transformER).
|
||||
**Primer EZ** is the architecture with the two most robust modifications in Primer compared to
|
||||
the original transformer.
|
||||
Primer EZ trains a lot faster than the vanilla transformer.
|
||||
|
||||
### Squared ReLU
|
||||
|
||||
The most effective modification found by the search is using a square ReLU instead of ReLU in
|
||||
the [position-wise feedforward module](https://nn.labml.ai/transformers/feed_forward.html).
|
||||
|
||||
### Multi-DConv-Head Attention (MDHA)
|
||||
|
||||
The next effective modification is a depth-wise 3 X 1 convolution after multi-head projection
|
||||
for queries, keys, and values.
|
||||
The convolution is along the sequence dimension and per channel (depth-wise).
|
||||
To be clear, if the number of channels in each head is d_k the convolution will have 1 X 3
|
||||
kernels for each of the d_k channels.
|
||||
|
||||
[Here is the experiment code](https://nn.labml.ai/transformers/primer_ez/experiment.html), for Primer EZ.
|
||||
@@ -0,0 +1,143 @@
|
||||
"""
|
||||
---
|
||||
title: Primer EZ variations
|
||||
summary: We tried some variations to Primer EZ.
|
||||
---
|
||||
|
||||
# [Primer EZ](index.html) Variations
|
||||
|
||||
We tried some variations to see which changes in Primer EZ has most benefits.
|
||||
"""
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
from labml_nn.transformers import MultiHeadAttention
|
||||
|
||||
|
||||
class SpatialDepthWiseSharedConvolution(nn.Module):
|
||||
"""
|
||||
## Spatial Depth Wise Shared Convolution
|
||||
|
||||
We share the same kernel across all channels.
|
||||
"""
|
||||
|
||||
def __init__(self, kernel_size: int = 3):
|
||||
"""
|
||||
"""
|
||||
super().__init__()
|
||||
self.kernel_size = kernel_size
|
||||
# We use PyTorch's `Conv1d` module.
|
||||
# We add padding to both sides and later crop the right most `kernel_size - 1` results
|
||||
self.conv = nn.Conv1d(in_channels=1, out_channels=1, kernel_size=(kernel_size,), padding=(kernel_size - 1,))
|
||||
|
||||
def forward(self, x: torch.Tensor):
|
||||
"""
|
||||
`x` has shape `[seq_len, batch_size, heads, d_k]`
|
||||
"""
|
||||
|
||||
# Get the shape
|
||||
seq_len, batch_size, heads, d_k = x.shape
|
||||
# Permute to `[batch_size, heads, d_k, seq_len]`
|
||||
x = x.permute(1, 2, 3, 0)
|
||||
# Change the shape to `[batch_size * heads * d_k, seq_len]`
|
||||
x = x.view(batch_size * heads * d_k, 1, seq_len)
|
||||
|
||||
# 1D convolution accepts input of the form `[N, channels, sequence]`
|
||||
x = self.conv(x)
|
||||
# Crop the right most `kernel_size - 1` results since we padded both sides
|
||||
x = x[:, :, :-(self.kernel_size - 1)]
|
||||
# Reshape to `[batch_size, heads, d_k, seq_len]`
|
||||
x = x.view(batch_size, heads, d_k, seq_len)
|
||||
# Permute to `[seq_len, batch_size, heads, d_k]`
|
||||
x = x.permute(3, 0, 1, 2)
|
||||
|
||||
#
|
||||
return x
|
||||
|
||||
|
||||
class MultiDSharedConvHeadAttention(MultiHeadAttention):
|
||||
"""
|
||||
## Multi-Depth-wise-Shared-Conv-Head Attention
|
||||
|
||||
We extend our original implementation of [Multi-Head Attention](../mha.html#MHA)
|
||||
and add the spatial depth-wise shared convolution to query, key and value projections.
|
||||
"""
|
||||
|
||||
def __init__(self, heads: int, d_model: int, dropout_prob: float = 0.1):
|
||||
super().__init__(heads, d_model, dropout_prob)
|
||||
|
||||
# [Multi-Head Attention](../mha.html#MHA) will create query, key and value projection modules
|
||||
# `self.query`, `self.key`, and `self.value`.
|
||||
#
|
||||
# We combine a spatial depth-wise shared convolution layer to each of them and replace
|
||||
# `self.query`, `self.key`, and `self.value`.
|
||||
self.query = nn.Sequential(self.query, SpatialDepthWiseSharedConvolution())
|
||||
self.key = nn.Sequential(self.key, SpatialDepthWiseSharedConvolution())
|
||||
self.value = nn.Sequential(self.value, SpatialDepthWiseSharedConvolution())
|
||||
|
||||
|
||||
class SpatialDepthWisePerHeadConvolution(nn.Module):
|
||||
"""
|
||||
## Spatial Depth Wise Per Head Convolution
|
||||
"""
|
||||
|
||||
def __init__(self, heads: int, d_k: int, kernel_size: int = 3):
|
||||
"""
|
||||
* `heads` is the number of heads
|
||||
* `d_k` is the number of channels in each head
|
||||
"""
|
||||
super().__init__()
|
||||
self.kernel_size = kernel_size
|
||||
# We use PyTorch's `Conv1d` module.
|
||||
# We set the number of groups to be equal to the number of channels from each head
|
||||
# so that it does a separate convolution
|
||||
# (with different kernels) for each channel and head.
|
||||
# We add padding to both sides and later crop the right most `kernel_size - 1` results
|
||||
self.conv = nn.Conv1d(in_channels=d_k * heads, out_channels=d_k * heads,
|
||||
kernel_size=(kernel_size,), padding=(kernel_size - 1,), groups=d_k * heads)
|
||||
|
||||
def forward(self, x: torch.Tensor):
|
||||
"""
|
||||
`x` has shape `[seq_len, batch_size, heads, d_k]`
|
||||
"""
|
||||
|
||||
# Get the shape
|
||||
seq_len, batch_size, heads, d_k = x.shape
|
||||
# Permute to `[batch_size, heads, d_k, seq_len]`
|
||||
x = x.permute(1, 2, 3, 0)
|
||||
# Change the shape to `[batch_size heads * d_k, seq_len]`
|
||||
x = x.view(batch_size, heads * d_k, seq_len)
|
||||
|
||||
# 1D convolution accepts input of the form `[N, channels, sequence]`
|
||||
x = self.conv(x)
|
||||
# Crop the right most `kernel_size - 1` results since we padded both sides
|
||||
x = x[:, :, :-(self.kernel_size - 1)]
|
||||
# Reshape to `[batch_size, heads, d_k, seq_len]`
|
||||
x = x.view(batch_size, heads, d_k, seq_len)
|
||||
# Permute to `[seq_len, batch_size, heads, d_k]`
|
||||
x = x.permute(3, 0, 1, 2)
|
||||
|
||||
#
|
||||
return x
|
||||
|
||||
|
||||
class MultiDPHConvHeadAttention(MultiHeadAttention):
|
||||
"""
|
||||
## Multi-per-Head-Depth-wise-Conv-Head Attention
|
||||
|
||||
We extend our original implementation of [Multi-Head Attention](../mha.html#MHA)
|
||||
and add the spatial depth-wise convolution to query, key and value projections.
|
||||
"""
|
||||
|
||||
def __init__(self, heads: int, d_model: int, dropout_prob: float = 0.1):
|
||||
super().__init__(heads, d_model, dropout_prob)
|
||||
|
||||
# [Multi-Head Attention](../mha.html#MHA) will create query, key and value projection modules
|
||||
# `self.query`, `self.key`, and `self.value`.
|
||||
#
|
||||
# We combine a spatial per-head depth-wise convolution layer to each of them and replace
|
||||
# `self.query`, `self.key`, and `self.value`.
|
||||
self.query = nn.Sequential(self.query, SpatialDepthWisePerHeadConvolution(heads, self.d_k))
|
||||
self.key = nn.Sequential(self.key, SpatialDepthWisePerHeadConvolution(heads, self.d_k))
|
||||
self.value = nn.Sequential(self.value, SpatialDepthWisePerHeadConvolution(heads, self.d_k))
|
||||
@@ -0,0 +1,7 @@
|
||||
"""
|
||||
---
|
||||
title: Relative Multi-Headed Attention
|
||||
summary: Relative Multi-Headed Attention from paper Transformer-XL.
|
||||
redirect: https://nn.labml.ai/transformers/xl/relative_mha.html
|
||||
---
|
||||
"""
|
||||
@@ -0,0 +1,35 @@
|
||||
"""
|
||||
---
|
||||
title: Retrieval-Enhanced Transformer (Retro)
|
||||
summary: >
|
||||
This is a PyTorch implementation/tutorial of the paper
|
||||
Improving language models by retrieving from trillions of tokens.
|
||||
It builds a key-value database of chunks of text and retrieves and uses them when
|
||||
making predictions.
|
||||
---
|
||||
|
||||
# Retrieval-Enhanced Transformer (Retro)
|
||||
|
||||
This is a [PyTorch](https://pytorch.org) implementation of the paper
|
||||
[Improving language models by retrieving from trillions of tokens](https://arxiv.org/abs/2112.04426).
|
||||
|
||||
It builds a database of chunks of text.
|
||||
It is a key-value database where the keys are indexed by the BERT embeddings of the chunks.
|
||||
They use a frozen pre-trained BERT model to calculate these embeddings.
|
||||
The values are the corresponding chunks and an equal length of text proceeding that chunk.
|
||||
|
||||
Then the model retrieves text similar (nearest neighbors) to the input to the model from this database.
|
||||
These retrieved texts are used to predict the output.
|
||||
|
||||
Since we use a frozen BERT model for retrieval we can pre-calculate all the nearest neighbors for the
|
||||
training dataset.
|
||||
This speeds up the training process.
|
||||
|
||||
Components:
|
||||
|
||||
* [BERT embeddings](bert_embeddings.html): Code to get BERT embeddings of chunks of text.
|
||||
* [Key-value database](database.html): Build and retrieve chunks
|
||||
* [Model](model.html)
|
||||
* [Dataset](dataset.html): Pre-calculate the nearest neighbors
|
||||
* [Training code](train.html)
|
||||
"""
|
||||
@@ -0,0 +1,148 @@
|
||||
"""
|
||||
---
|
||||
title: BERT Embeddings of chunks of text
|
||||
summary: >
|
||||
Generate BERT embeddings for chunks using a frozen BERT model
|
||||
---
|
||||
|
||||
# BERT Embeddings of chunks of text
|
||||
|
||||
This is the code to get BERT embeddings of chunks for [RETRO model](index.html).
|
||||
"""
|
||||
|
||||
from typing import List
|
||||
|
||||
import torch
|
||||
from transformers import BertTokenizer, BertModel
|
||||
|
||||
from labml import lab, monit
|
||||
|
||||
|
||||
class BERTChunkEmbeddings:
|
||||
"""
|
||||
## BERT Embeddings
|
||||
|
||||
For a given chunk of text $N$ this class generates BERT embeddings $\text{B\small{ERT}}(N)$.
|
||||
$\text{B\small{ERT}}(N)$ is the average of BERT embeddings of all the tokens in $N$.
|
||||
"""
|
||||
|
||||
def __init__(self, device: torch.device):
|
||||
self.device = device
|
||||
|
||||
# Load the BERT tokenizer from [HuggingFace](https://huggingface.co/bert-base-uncased)
|
||||
with monit.section('Load BERT tokenizer'):
|
||||
self.tokenizer = BertTokenizer.from_pretrained('bert-base-uncased',
|
||||
cache_dir=str(
|
||||
lab.get_data_path() / 'cache' / 'bert-tokenizer'))
|
||||
|
||||
# Load the BERT model from [HuggingFace](https://huggingface.co/bert-base-uncased)
|
||||
with monit.section('Load BERT model'):
|
||||
self.model = BertModel.from_pretrained("bert-base-uncased",
|
||||
cache_dir=str(lab.get_data_path() / 'cache' / 'bert-model'))
|
||||
|
||||
# Move the model to `device`
|
||||
self.model.to(device)
|
||||
|
||||
@staticmethod
|
||||
def _trim_chunk(chunk: str):
|
||||
"""
|
||||
In this implementation, we do not make chunks with a fixed number of tokens.
|
||||
One of the reasons is that this implementation uses character-level tokens and BERT
|
||||
uses its sub-word tokenizer.
|
||||
|
||||
So this method will truncate the text to make sure there are no partial tokens.
|
||||
|
||||
For instance, a chunk could be like `s a popular programming la`, with partial
|
||||
words (partial sub-word tokens) on the ends.
|
||||
We strip them off to get better BERT embeddings.
|
||||
As mentioned earlier this is not necessary if we broke chunks after tokenizing.
|
||||
"""
|
||||
# Strip whitespace
|
||||
stripped = chunk.strip()
|
||||
# Break words
|
||||
parts = stripped.split()
|
||||
# Remove first and last pieces
|
||||
stripped = stripped[len(parts[0]):-len(parts[-1])]
|
||||
|
||||
# Remove whitespace
|
||||
stripped = stripped.strip()
|
||||
|
||||
# If empty return original string
|
||||
if not stripped:
|
||||
return chunk
|
||||
# Otherwise, return the stripped string
|
||||
else:
|
||||
return stripped
|
||||
|
||||
def __call__(self, chunks: List[str]):
|
||||
"""
|
||||
### Get $\text{B\small{ERT}}(N)$ for a list of chunks.
|
||||
"""
|
||||
|
||||
# We don't need to compute gradients
|
||||
with torch.no_grad():
|
||||
# Trim the chunks
|
||||
trimmed_chunks = [self._trim_chunk(c) for c in chunks]
|
||||
|
||||
# Tokenize the chunks with BERT tokenizer
|
||||
tokens = self.tokenizer(trimmed_chunks, return_tensors='pt', add_special_tokens=False, padding=True)
|
||||
|
||||
# Move token ids, attention mask and token types to the device
|
||||
input_ids = tokens['input_ids'].to(self.device)
|
||||
attention_mask = tokens['attention_mask'].to(self.device)
|
||||
token_type_ids = tokens['token_type_ids'].to(self.device)
|
||||
# Evaluate the model
|
||||
output = self.model(input_ids=input_ids,
|
||||
attention_mask=attention_mask,
|
||||
token_type_ids=token_type_ids)
|
||||
|
||||
# Get the token embeddings
|
||||
state = output['last_hidden_state']
|
||||
# Calculate the average token embeddings.
|
||||
# Note that the attention mask is `0` if the token is empty padded.
|
||||
# We get empty tokens because the chunks are of different lengths.
|
||||
emb = (state * attention_mask[:, :, None]).sum(dim=1) / attention_mask[:, :, None].sum(dim=1)
|
||||
|
||||
#
|
||||
return emb
|
||||
|
||||
|
||||
def _test():
|
||||
"""
|
||||
### Code to test BERT embeddings
|
||||
"""
|
||||
from labml.logger import inspect
|
||||
|
||||
# Initialize
|
||||
device = torch.device('cuda:0')
|
||||
bert = BERTChunkEmbeddings(device)
|
||||
|
||||
# Sample
|
||||
text = ["Replace me by any text you'd like.",
|
||||
"Second sentence"]
|
||||
|
||||
# Check BERT tokenizer
|
||||
encoded_input = bert.tokenizer(text, return_tensors='pt', add_special_tokens=False, padding=True)
|
||||
|
||||
inspect(encoded_input, _expand=True)
|
||||
|
||||
# Check BERT model outputs
|
||||
output = bert.model(input_ids=encoded_input['input_ids'].to(device),
|
||||
attention_mask=encoded_input['attention_mask'].to(device),
|
||||
token_type_ids=encoded_input['token_type_ids'].to(device))
|
||||
|
||||
inspect({'last_hidden_state': output['last_hidden_state'],
|
||||
'pooler_output': output['pooler_output']},
|
||||
_expand=True)
|
||||
|
||||
# Check recreating text from token ids
|
||||
inspect(bert.tokenizer.convert_ids_to_tokens(encoded_input['input_ids'][0]), _n=-1)
|
||||
inspect(bert.tokenizer.convert_ids_to_tokens(encoded_input['input_ids'][1]), _n=-1)
|
||||
|
||||
# Get chunk embeddings
|
||||
inspect(bert(text))
|
||||
|
||||
|
||||
#
|
||||
if __name__ == '__main__':
|
||||
_test()
|
||||
@@ -0,0 +1,156 @@
|
||||
"""
|
||||
---
|
||||
title: Database for nearest neighbor retrieval
|
||||
summary: >
|
||||
Nearest neighbor retrieval and creation of the database
|
||||
---
|
||||
|
||||
# Database for nearest neighbor retrieval
|
||||
|
||||
This is the build the database and retrieves nearest neighbors for
|
||||
[RETRO model](index.html).
|
||||
|
||||
We use [FAISS library](https://faiss.ai/) for the database whilst the paper had used the SCaNN library.
|
||||
"""
|
||||
|
||||
from typing import List, Optional
|
||||
|
||||
import faiss
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
from labml import lab, monit
|
||||
from labml_nn.helpers.datasets import TextFileDataset
|
||||
from labml_nn.transformers.retro.bert_embeddings import BERTChunkEmbeddings
|
||||
|
||||
|
||||
def build_database(chunk_len: int = 16, batch_size: int = 64, d_emb: int = 768, n_centeroids: int = 256,
|
||||
code_size: int = 64, n_probe: int = 8, n_train: int = 50_000):
|
||||
"""
|
||||
## Build Database
|
||||
|
||||
* `chunk_len` is the length of a chunk (number of characters)
|
||||
* `batch_size` is the batch size to use when calculating $\text{B\small{ERT}}(N)$
|
||||
* `d_emb` is the number of features in $\text{B\small{ERT}}(N)$ embeddings
|
||||
[lists to select in FAISS index](https://faiss.ai/cpp_api/struct/structfaiss_1_1IndexIVFPQ.html)
|
||||
* `n_centeroids` is the number of lists in the index
|
||||
* `code_size` encoded vector size in the index
|
||||
* `n_probe` is the number of lists to probe
|
||||
* `n_train' is the number of keys to train the index on
|
||||
"""
|
||||
|
||||
# Load the dataset text file
|
||||
dataset = TextFileDataset(
|
||||
lab.get_data_path() / 'tiny_shakespeare.txt',
|
||||
list,
|
||||
url='https://raw.githubusercontent.com/karpathy/char-rnn/master/data/tinyshakespeare/input.txt')
|
||||
|
||||
# Get training data (a string)
|
||||
text = dataset.train
|
||||
|
||||
# Split the text into chunks of `chunk_length`
|
||||
chunks = [text[i:i + chunk_len] for i in range(0, len(text), chunk_len) if i + chunk_len * 2 < len(text)]
|
||||
# Get the offsets of each of the chunks
|
||||
chunk_offsets = np.array([i for i in range(0, len(text), chunk_len) if i + chunk_len * 2 < len(text)])
|
||||
# Number of chunks
|
||||
n_chunks = len(chunks)
|
||||
|
||||
# Initialize BERT to get $\text{B\small{ERT}}(N)$
|
||||
bert = BERTChunkEmbeddings(torch.device('cuda:0'))
|
||||
|
||||
# Get chunk embeddings by processing `batch_size` number of chunks on each iteration
|
||||
chunk_emb = []
|
||||
for i in monit.iterate('Get embeddings', range(0, n_chunks, batch_size)):
|
||||
chunk_emb.append(bert(chunks[i: i + batch_size]).cpu())
|
||||
# Merge them into a single tensor
|
||||
chunk_emb = torch.cat(chunk_emb, dim=0).numpy()
|
||||
|
||||
# Create the [FAISS index](https://faiss.ai/cpp_api/struct/structfaiss_1_1IndexIVFPQ.html)
|
||||
quantizer = faiss.IndexFlatL2(d_emb)
|
||||
index = faiss.IndexIVFPQ(quantizer, d_emb, n_centeroids, code_size, 8)
|
||||
index.nprobe = n_probe
|
||||
|
||||
# Get a random sample of the the chunk indexes
|
||||
random_sample = np.random.choice(np.arange(n_chunks), size=[min(n_train, n_chunks)], replace=False)
|
||||
|
||||
# Train the index to store the keys
|
||||
with monit.section('Train index'):
|
||||
index.train(chunk_emb[random_sample])
|
||||
|
||||
# Add the chunks to the index in batches of size `1024`
|
||||
for s in monit.iterate('Index', range(0, n_chunks, 1024)):
|
||||
e = min(s + 1024, n_chunks)
|
||||
# Add to index
|
||||
index.add_with_ids(chunk_emb[s:e], chunk_offsets[s: e])
|
||||
|
||||
# Save the index
|
||||
with monit.section('Save'):
|
||||
faiss.write_index(index, str(lab.get_data_path() / 'retro.index'))
|
||||
|
||||
|
||||
class RetroIndex:
|
||||
"""
|
||||
## Index for retrieving nearest neighbors
|
||||
"""
|
||||
|
||||
def __init__(self, chunk_len: int = 16, n_probe: int = 8,
|
||||
n_neighbors: int = 2, n_extra: int = 2,
|
||||
exclude_neighbor_span: int = 8):
|
||||
"""
|
||||
* `chunk_len` is the chunk length
|
||||
* `n_probe` is the number of lists to probe
|
||||
* `n_neighbors` is the number of neighbors to retrieve
|
||||
* `n_extra` is the number of extra neighbors to retrieve since we will be
|
||||
removing neighbors overlapping with the query chunk
|
||||
* `exclude_neighbor_span` is the extra text length to avoid when checking for overlaps
|
||||
"""
|
||||
|
||||
self.n_neighbors = n_neighbors
|
||||
self.chunk_len = chunk_len
|
||||
self.exclude_neighbor_span = exclude_neighbor_span
|
||||
self.n_extra = n_extra
|
||||
|
||||
# Initialize BERT to get $\text{B\small{ERT}}(N)$
|
||||
self.bert = BERTChunkEmbeddings(torch.device('cuda:0'))
|
||||
# Load the database
|
||||
with monit.section('Load index'):
|
||||
self.index = faiss.read_index(str(lab.get_data_path() / 'retro.index'))
|
||||
self.index.nprobe = n_probe
|
||||
|
||||
def filter_neighbors(self, offset: int, neighbor_offsets: List[int]):
|
||||
"""
|
||||
#### Filter neighbors that overlap with the query
|
||||
|
||||
The positions of the neighbors are given by `neighbor_offsets` and the position
|
||||
of the query chunk is `offset`.
|
||||
"""
|
||||
return [n for n in neighbor_offsets
|
||||
if n < offset - (self.chunk_len + self.exclude_neighbor_span)
|
||||
or n > offset + (self.chunk_len + self.exclude_neighbor_span)]
|
||||
|
||||
def __call__(self, query_chunks: List[str], offsets: Optional[List[int]]):
|
||||
"""
|
||||
### Retrieve nearest neighbors
|
||||
"""
|
||||
|
||||
# Get $\text{B\small{ERT}}(N)$ of query chunks
|
||||
emb = self.bert(query_chunks).cpu()
|
||||
|
||||
# Get `n_neighbors + n_extra` nearest neighbors from the database
|
||||
distance, neighbor_offsets = self.index.search(emb.numpy(), self.n_neighbors + self.n_extra)
|
||||
|
||||
# If the query chunk offsets are given filter out overlapping chunks
|
||||
if offsets is not None:
|
||||
neighbor_offsets = [self.filter_neighbors(off, n_off)
|
||||
for off, n_off in zip(offsets, neighbor_offsets)]
|
||||
|
||||
# Get the closest `n_neighbors` after filtering
|
||||
neighbor_offsets = [n_off[:self.n_neighbors] for n_off in neighbor_offsets]
|
||||
|
||||
#
|
||||
return neighbor_offsets
|
||||
|
||||
|
||||
#
|
||||
if __name__ == '__main__':
|
||||
build_database()
|
||||
@@ -0,0 +1,137 @@
|
||||
"""
|
||||
---
|
||||
title: Training dataset for RETRO
|
||||
summary: >
|
||||
Create a dataset for RETRO model training
|
||||
---
|
||||
|
||||
# RETRO training dataset
|
||||
|
||||
We pre-retrieve nearest neighbors from the [key-value database](database.html)
|
||||
and create the dataset to train the [RETRO](index.html)
|
||||
[model](model.html).
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
from torch.utils.data import Dataset as PyTorchDataset
|
||||
|
||||
from labml import lab, monit
|
||||
from labml_nn.helpers.datasets import TextFileDataset, TextDataset
|
||||
from labml_nn.transformers.retro.database import RetroIndex
|
||||
|
||||
|
||||
def build_dataset(chunk_len: int = 16, chunks_per_sample: int = 32, skip_range: int = 8):
|
||||
"""
|
||||
## Build the dataset
|
||||
|
||||
* `chunk_len` is the chunk length
|
||||
* `chunks_per_sample` is the number of chunks per training sample
|
||||
* `skip_range` is the maximum number of characters to skip between two samples.
|
||||
We skip a few characters between samples to make sure the samples
|
||||
aren't aligned perfectly with the chunks in the [database](database.html)
|
||||
"""
|
||||
|
||||
# Load the text file
|
||||
dataset = TextFileDataset(
|
||||
lab.get_data_path() / 'tiny_shakespeare.txt',
|
||||
list,
|
||||
url='https://raw.githubusercontent.com/karpathy/char-rnn/master/data/tinyshakespeare/input.txt')
|
||||
|
||||
# Training portion of it
|
||||
text = dataset.train
|
||||
|
||||
# Load the index for retrieving neighbors
|
||||
index = RetroIndex()
|
||||
|
||||
# The input sample offsets
|
||||
sample_offsets = []
|
||||
# Cursor for the text
|
||||
i = 0
|
||||
while i < len(text):
|
||||
# Skip a few characters to make sure it's not aligned with the neighbors
|
||||
skip = np.random.randint(skip_range)
|
||||
i += skip
|
||||
|
||||
# Stop if we've reached the end of the text
|
||||
if i + chunks_per_sample * chunk_len > len(text):
|
||||
break
|
||||
|
||||
# Collect the offset
|
||||
sample_offsets.append(i)
|
||||
|
||||
# Increment the cursor
|
||||
i += chunks_per_sample * chunk_len
|
||||
|
||||
# For samples
|
||||
samples = []
|
||||
# Iterate through sample offsets
|
||||
for i in monit.iterate('Gather Neighbors', sample_offsets):
|
||||
# Get the sample including an extra character (for prediction)
|
||||
sample = text[i: i + chunks_per_sample * chunk_len + 1]
|
||||
# The input
|
||||
src = sample[:-1]
|
||||
# Break it into chunks
|
||||
chunks = [src[j:j + chunk_len] for j in range(0, len(src), chunk_len)]
|
||||
# The chunk offsets
|
||||
chunk_offsets = [j + i for j in range(0, len(src), chunk_len)]
|
||||
|
||||
# Retrieve nearest neighbors
|
||||
neighbor_offsets = index(chunks, chunk_offsets)
|
||||
|
||||
# Get neighbor texts. The neighbor length is twice the `chunk_len`
|
||||
neighbors = [[text[j: j + chunk_len * 2] for j in n_off] for n_off in neighbor_offsets]
|
||||
|
||||
# Add to list of samples
|
||||
samples.append((sample[:-1], sample[1:], neighbors))
|
||||
|
||||
# Save the samples in JSON.
|
||||
# We don't need to use complex dataset storage mechanisms or pre-tokenize
|
||||
# since our dataset is small.
|
||||
with open(str(lab.get_data_path() / 'retro_train_dataset.json'), 'w') as f:
|
||||
f.write(json.dumps(samples))
|
||||
|
||||
|
||||
class Dataset(PyTorchDataset):
|
||||
"""
|
||||
## Dataset
|
||||
|
||||
This is the PyTorch dataset that loads the dataset created
|
||||
by `build_dataset`.
|
||||
"""
|
||||
def __init__(self, file_path: Path, tds: TextDataset):
|
||||
"""
|
||||
* `file_path` is the path of the saved JSON file
|
||||
* `tds` is the `TextDataset`
|
||||
"""
|
||||
|
||||
self.tds = tds
|
||||
# Load the samples
|
||||
with open(str(file_path), 'r') as f:
|
||||
self.samples = json.loads(f.read())
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Number of samples
|
||||
"""
|
||||
return len(self.samples)
|
||||
|
||||
def __getitem__(self, idx: int):
|
||||
"""
|
||||
Get a sample
|
||||
"""
|
||||
# Get the sample
|
||||
s = self.samples[idx]
|
||||
# Tokenize
|
||||
src = self.tds.text_to_i(s[0])
|
||||
tgt = self.tds.text_to_i(s[1])
|
||||
neighbors = torch.stack([torch.stack([self.tds.text_to_i(n) for n in chunks]) for chunks in s[2]])
|
||||
#
|
||||
return src, tgt, neighbors
|
||||
|
||||
#
|
||||
if __name__ == '__main__':
|
||||
build_dataset()
|
||||
@@ -0,0 +1,619 @@
|
||||
"""
|
||||
---
|
||||
title: RETRO model
|
||||
summary: >
|
||||
RETRO model with encoder for neighbors and autoregressive decoder
|
||||
---
|
||||
|
||||
# RETRO model
|
||||
|
||||
This is the model definition for
|
||||
[RETRO](index.html).
|
||||
"""
|
||||
|
||||
import math
|
||||
from typing import Set
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
from labml.logger import inspect
|
||||
|
||||
|
||||
class RotaryPositionalEmbeddings(nn.Module):
|
||||
"""
|
||||
## [RoPE embeddings](../rope/index.html)
|
||||
|
||||
*We use rotary position embeddings in self-attention layers.
|
||||
We assume the positional information gets embedded in embeddings
|
||||
and therefore not use them in causal attention.
|
||||
[Non-causal self-attention needs explicit positional information
|
||||
because it cannot infer it](https://arxiv.org/abs/3999902edc8511eba3db37f65e372566).*
|
||||
"""
|
||||
|
||||
def __init__(self, d: int, base: int = 10_000):
|
||||
"""
|
||||
* `d` is the number of features $d$
|
||||
* `base` is the constant used for calculating $\Theta$
|
||||
"""
|
||||
super().__init__()
|
||||
# $\Theta = {\theta_i = 10000^{\frac{2(i-1)}{d}}, i \in [1, 2, ..., \frac{d}{2}]}$
|
||||
self.theta = nn.Parameter(1. / (base ** (torch.arange(0, d, 2).float() / d)), requires_grad=False)
|
||||
|
||||
def forward(self, x: torch.Tensor):
|
||||
"""
|
||||
* `x` is the Tensor at the head of a key or a query with shape `[ batch_size, seq_len, n_heads, d]`
|
||||
"""
|
||||
# Extract the shape
|
||||
batch_size, seq_len, n_heads, d = x.shape
|
||||
|
||||
# $\frac{d}{2}$
|
||||
d_2 = d // 2
|
||||
|
||||
# Create position indexes `[0, 1, ..., seq_len - 1]`
|
||||
seq_idx = torch.arange(seq_len, device=x.device).type_as(self.theta)
|
||||
|
||||
# Calculate the product of position index and $\theta_i$
|
||||
idx_theta = torch.einsum('n,d->nd', seq_idx, self.theta)
|
||||
|
||||
# Concatenate so that for row $m$ we have
|
||||
# $[m \theta_0, m \theta_1, ..., m \theta_{\frac{d}{2}}, m \theta 0, m \theta 1, ..., m \theta_{\frac{d}{2}}]$
|
||||
idx_theta2 = torch.cat([idx_theta, idx_theta], dim=1)
|
||||
|
||||
# Calculate
|
||||
# $[-x^{(\frac{d}{2} + 1)}, -x^{(\frac{d}{2} + 2)}, ..., -x^{(d)}, x^{(1)}, x^{(2)}, ..., -x^{(\frac{d}{2})}]$
|
||||
neg_half_x = torch.cat([-x[:, :, :, d_2:], x[:, :, :, :d_2]], dim=-1)
|
||||
|
||||
# Calculate
|
||||
#
|
||||
# \begin{align}
|
||||
# \begin{pmatrix}
|
||||
# x^{(i)}_m \cos m \theta_i - x^{(i + \frac{d}{2})}_m \sin m \theta_i \\
|
||||
# x^{(i + \frac{d}{2})}_m \cos m\theta_i + x^{(i)}_m \sin m \theta_i \\
|
||||
# \end{pmatrix} \\
|
||||
# \end{align}
|
||||
#
|
||||
# for $i \in {1, 2, ..., \frac{d}{2}}$
|
||||
rx = (x * idx_theta2.cos()[None, :, None, :]) + (neg_half_x * idx_theta2.sin()[None, :, None, :])
|
||||
|
||||
#
|
||||
return rx
|
||||
|
||||
|
||||
class SelfAttention(nn.Module):
|
||||
"""
|
||||
## Self-Attention Layer $\text{A\small{TTN}}$
|
||||
|
||||
This applies causal and non-causal [multi-headed self-attention](../mha.html).
|
||||
"""
|
||||
|
||||
def __init__(self, d_model: int, n_heads: int, d_k: int, is_causal: bool):
|
||||
"""
|
||||
* `d_model` is the number of features in transformer embeddings
|
||||
* `n_heads` is the number of attention heads
|
||||
* `d_k` is the number of features per head
|
||||
* `is_causal` indicates whether this is causal attention (masked)
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
self.is_causal = is_causal
|
||||
self.n_heads = n_heads
|
||||
self.d_k = d_k
|
||||
|
||||
# To scale attentions before softmax by $\frac{1}{\sqrt{d_k}}$
|
||||
self.scale = 1 / math.sqrt(self.d_k)
|
||||
|
||||
# Linear layers for query, key and value heads.
|
||||
self.query = nn.Linear(d_model, n_heads * d_k)
|
||||
self.key = nn.Linear(d_model, n_heads * d_k)
|
||||
self.value = nn.Linear(d_model, n_heads * d_k)
|
||||
|
||||
# Pre-norm layer. The paper uses RMSNorm instead.
|
||||
self.norm = nn.LayerNorm(d_model)
|
||||
|
||||
# Softmax for attention probabilities
|
||||
self.softmax = nn.Softmax(dim=-1)
|
||||
|
||||
# Rotary positional embeddings
|
||||
self.rotary_pe = RotaryPositionalEmbeddings(self.d_k)
|
||||
|
||||
# Final linear layer
|
||||
self.output = nn.Linear(n_heads * d_k, d_model)
|
||||
|
||||
def mask_attention(self, attn: torch.Tensor):
|
||||
"""
|
||||
### Mask the attention layer for causal attention
|
||||
|
||||
* `attn` is the attention matrix of shape `[batch_size, n_heads, seq_len, seq_len]`
|
||||
"""
|
||||
|
||||
# No masking for non-causal attention
|
||||
if not self.is_causal:
|
||||
return attn
|
||||
|
||||
# Create a triangular mask
|
||||
mask = torch.tril(attn.new_ones(attn.shape[-2:]))
|
||||
# Filter by the mask
|
||||
return attn.masked_fill(mask == 0, float('-inf'))
|
||||
|
||||
def forward(self, h: torch.Tensor):
|
||||
"""
|
||||
* `h` is the transformer embeddings of shape `[batch_size, seq_len, d_model]`
|
||||
"""
|
||||
|
||||
# Residual connection
|
||||
h_res = h
|
||||
|
||||
# Pre-normalization
|
||||
h = self.norm(h)
|
||||
|
||||
# Get query, key, and values and split them in to heads.
|
||||
# These will have shapes `[batch_size, seq_len, n_heads, d_k]`
|
||||
mh_shape = (*h.shape[:-1], self.n_heads, self.d_k)
|
||||
q = self.query(h).view(mh_shape)
|
||||
k = self.key(h).view(mh_shape)
|
||||
v = self.value(h).view(mh_shape)
|
||||
|
||||
# Apply rotary positional embeddings
|
||||
q = self.rotary_pe(q)
|
||||
k = self.rotary_pe(k)
|
||||
|
||||
# Calculate attentions
|
||||
attn = torch.einsum('bihd,bjhd->bhij', q, k)
|
||||
# Scale it by $\frac{1}{\sqrt{d_k}}$
|
||||
attn = attn * self.scale
|
||||
|
||||
# Apply masks if it's causal attention
|
||||
attn = self.mask_attention(attn)
|
||||
|
||||
# Calculate attention probabilities
|
||||
attn = self.softmax(attn)
|
||||
|
||||
# Get values
|
||||
h = torch.einsum("bhij,bjhd->bihd", attn, v)
|
||||
|
||||
# Change from shape `[batch_size, seq_len, n_heads, d_k]`
|
||||
# to `[batch_size, seq_len, n_heads * d_k]`
|
||||
h = h.reshape(*h.shape[:-2], -1)
|
||||
|
||||
# Apply final linear layer.
|
||||
# The result will have shape `[batch_size, seq_len, d_model]`
|
||||
h = self.output(h)
|
||||
|
||||
# Add the residual connection
|
||||
return h + h_res
|
||||
|
||||
|
||||
class CrossAttention(nn.Module):
|
||||
"""
|
||||
## Cross-Attention Layer $\text{C\small{A}}$
|
||||
|
||||
This is similar to the self-attention layer defined above, except that
|
||||
it gets keys and values from a different set of embeddings than the queries.
|
||||
|
||||
This is used in the encoder to encode the retrieved chunks based on the
|
||||
input chunks.
|
||||
|
||||
*We do not use any explicit positional embeddings here.
|
||||
We assume that the model can represent positional information in the embeddings implicitly.*
|
||||
"""
|
||||
|
||||
def __init__(self, d_model: int, n_heads: int, d_k: int):
|
||||
"""
|
||||
* `d_model` is the number of features in transformer embeddings
|
||||
* `n_heads` is the number of attention heads
|
||||
* `d_k` is the number of features per head
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
self.n_heads = n_heads
|
||||
self.d_k = d_k
|
||||
|
||||
# To scale attentions before softmax by $\frac{1}{\sqrt{d_k}}$
|
||||
self.scale = 1 / math.sqrt(self.d_k)
|
||||
|
||||
# Linear layers for query, key and value heads.
|
||||
self.query = nn.Linear(d_model, n_heads * d_k)
|
||||
self.key = nn.Linear(d_model, n_heads * d_k)
|
||||
self.value = nn.Linear(d_model, n_heads * d_k)
|
||||
|
||||
# Pre-norm layer for the query embeddings. The paper uses RMSNorm instead.
|
||||
self.norm = nn.LayerNorm(d_model)
|
||||
|
||||
# Softmax for attention probabilities
|
||||
self.softmax = nn.Softmax(dim=-1)
|
||||
|
||||
# Final linear layer
|
||||
self.output = nn.Linear(n_heads * d_k, d_model)
|
||||
|
||||
def forward(self, e: torch.Tensor, h: torch.Tensor):
|
||||
"""
|
||||
* `e` are the retrieved nearest neighbor chunk embeddings with shape
|
||||
`[batch_size, chunks, neighbors, neighbor_len, d_model]`
|
||||
* `h` are the input chunks from which the nearest neighbors were retrieved with shape
|
||||
`[batch_size, chunks, chunk_len, d_model]`. This is already normalized.
|
||||
"""
|
||||
|
||||
# Residual connection
|
||||
e_res = e
|
||||
|
||||
# Normalize retrieved chunks
|
||||
e = self.norm(e)
|
||||
|
||||
# Get query from the retrieved chunks
|
||||
q = self.query(e).view(*e.shape[:-1], self.n_heads, self.d_k)
|
||||
# Get keys and values from the input chunks
|
||||
k = self.key(h).view(*h.shape[:-1], self.n_heads, self.d_k)
|
||||
v = self.value(h).view(*h.shape[:-1], self.n_heads, self.d_k)
|
||||
|
||||
# Calculate attention scores for all chunks.
|
||||
# Each retrieved neighbor will pay attention to the original chunk that retrieved it.
|
||||
# This will have shape `[batch_size, chunks, neighbors, n_heads, neighbor_len, chunk_len]`
|
||||
attn = torch.einsum('bcnihd,bcjhd->bcnhij', q, k)
|
||||
# Scale attention scores
|
||||
attn = attn * self.scale
|
||||
|
||||
# Calculate softmax across the last dimension
|
||||
attn = self.softmax(attn)
|
||||
|
||||
# Gather values
|
||||
e = torch.einsum("bcnhij,bcjhd->bcnihd", attn, v)
|
||||
|
||||
# Change from shape `[batch_size, chunks, neighbors, neighbor_len, n_heads, d_k]`
|
||||
# to `[batch_size, chunks, neighbors, neighbor_len, n_heads * d_k]`
|
||||
e = e.reshape(*e.shape[:-2], -1)
|
||||
|
||||
# Apply final linear layer.
|
||||
# The result will have shape `[batch_size, chunks, neighbors, neighbor_len, d_model]`
|
||||
e = self.output(e)
|
||||
|
||||
# Add residual connection
|
||||
return e + e_res
|
||||
|
||||
|
||||
class ChunkedCrossAttention(nn.Module):
|
||||
"""
|
||||
## Chunked Cross-Attention Layer $\text{C\small{CA}}$
|
||||
|
||||
This is similar to the cross-attention layer defined above.
|
||||
|
||||
This is used in the decoder to pay attention to the retrieved neighbor chunks.
|
||||
|
||||
*We do not use any explicit positional embeddings here.
|
||||
We assume that the model can represent positional information in the embeddings implicitly.*
|
||||
"""
|
||||
|
||||
def __init__(self, d_model: int, n_heads: int, d_k: int, chunk_len: int):
|
||||
"""
|
||||
* `d_model` is the number of features in transformer embeddings
|
||||
* `n_heads` is the number of attention heads
|
||||
* `d_k` is the number of features per head
|
||||
* `chunk_len` is the length of a chunk
|
||||
"""
|
||||
|
||||
super().__init__()
|
||||
|
||||
self.chunk_len = chunk_len
|
||||
self.n_heads = n_heads
|
||||
self.d_k = d_k
|
||||
|
||||
# To scale attentions before softmax by $\frac{1}{\sqrt{d_k}}$
|
||||
self.scale = 1 / math.sqrt(self.d_k)
|
||||
|
||||
# Linear layers for query, key and value heads.
|
||||
self.query = nn.Linear(d_model, n_heads * d_k)
|
||||
self.key = nn.Linear(d_model, n_heads * d_k)
|
||||
self.value = nn.Linear(d_model, n_heads * d_k)
|
||||
|
||||
# Pre-norm layer for the query embeddings. The paper uses RMSNorm instead.
|
||||
self.norm = nn.LayerNorm(d_model)
|
||||
|
||||
# Softmax for attention probabilities
|
||||
self.softmax = nn.Softmax(dim=-1)
|
||||
|
||||
# Final linear layer
|
||||
self.output = nn.Linear(n_heads * d_k, d_model)
|
||||
|
||||
def forward(self, h: torch.Tensor, e: torch.Tensor):
|
||||
"""
|
||||
`h` are the input embeddings of shape `[batch_size, seq_len, d_model]`
|
||||
`e` are the retrieved nearest neighbors of shape `[batch_size, chunks, neighbors, neighbor_len, d_model]`
|
||||
"""
|
||||
|
||||
# Get shape
|
||||
batch_size, chunks, neighbors, neighbor_len, d_model = e.shape
|
||||
|
||||
# No attention if there are no chunks (for short inputs when sampling)
|
||||
if chunks == 0:
|
||||
return h
|
||||
|
||||
# Residual connection
|
||||
h_res = h
|
||||
|
||||
# Remove the first `chunk_len - 1` embeddings.
|
||||
# The input pays attention to neighbors retrieved and encoded using the past tokens only;
|
||||
# so that there is no information leakage.
|
||||
# That is the retrieved neighbors from the first chunks will have information from the first chunk.
|
||||
# So by shifting the sequence to the left by `chunk_len - 1` we make sure that information only flows
|
||||
# to the right.
|
||||
h = h[:, self.chunk_len - 1:]
|
||||
# Pre-norm
|
||||
h = self.norm(h)
|
||||
# Append empty embeddings to the end to be able to split the input into chunks
|
||||
if h.shape[1] < chunks * self.chunk_len:
|
||||
h = torch.cat((h, h.new_zeros(batch_size, chunks * self.chunk_len - h.shape[1], d_model)), dim=1)
|
||||
# Reshape the input into chunks.
|
||||
h = h.reshape(batch_size, chunks, self.chunk_len, d_model)
|
||||
|
||||
# Get query from the input
|
||||
q = self.query(h).view(*h.shape[:-1], self.n_heads, self.d_k)
|
||||
# Get keys and values from the retrieved neighbors
|
||||
k = self.key(e).view(*e.shape[:-1], self.n_heads, self.d_k)
|
||||
v = self.value(e).view(*e.shape[:-1], self.n_heads, self.d_k)
|
||||
|
||||
# Calculate attention scores for input chunks.
|
||||
# Each chunk will pay attention to neighbors retrieved by the previous chunk.
|
||||
# This will have shape `[batch_size, chunks, heads, chunk_len, neighbors, neighbor_len]`
|
||||
attn = torch.einsum('bcihd,bcnjhd->bchinj', q, k)
|
||||
# Scale attention scores
|
||||
attn = attn * self.scale
|
||||
|
||||
# Apply softmax over the last two dimensions `neighbors, neighbor_len`
|
||||
attn = self.softmax(attn.view(*attn.shape[:-2], -1)).view(attn.shape)
|
||||
|
||||
# Gather values
|
||||
h = torch.einsum("bchinj,bcnjhd->bcihd", attn, v)
|
||||
|
||||
# Change from shape `[batch_size, chunks, chunk_len, n_heads, d_k]`
|
||||
# to `[batch_size, chunks * chunk_len, n_heads * d_k]`
|
||||
h = h.reshape(batch_size, chunks * self.chunk_len, -1)
|
||||
|
||||
# Apply final linear layer.
|
||||
# The result will have shape `[batch_size, chunks * chunk_len, d_model]`
|
||||
h = self.output(h)
|
||||
|
||||
# Append `chunk_len - 1` zero embedding to the left; i.e. right shift it back
|
||||
h = torch.cat((h.new_zeros(batch_size, self.chunk_len - 1, d_model), h), dim=1)
|
||||
|
||||
# Truncate and add the residual connection
|
||||
return h[:, :h_res.shape[1]] + h_res
|
||||
|
||||
|
||||
class FeedForward(nn.Module):
|
||||
"""
|
||||
### Position-wise Feed Forward Layer $\text{F\small{FW}}$
|
||||
|
||||
This consists of two linear layers and an activation in the middle.
|
||||
"""
|
||||
|
||||
def __init__(self, d_model: int, d_ff: int):
|
||||
"""
|
||||
* `d_model` is the number of features in transformer embeddings
|
||||
* `d_ff` is the number features in the hidden layer
|
||||
"""
|
||||
|
||||
super().__init__()
|
||||
|
||||
# The two linear layers
|
||||
self.lin1 = nn.Linear(d_model, d_ff)
|
||||
self.lin2 = nn.Linear(d_ff, d_model)
|
||||
|
||||
# ReLU Activation
|
||||
self.act = nn.ReLU()
|
||||
|
||||
# Pre-norm layer
|
||||
self.norm = nn.LayerNorm(d_model)
|
||||
|
||||
def forward(self, h: torch.Tensor):
|
||||
"""
|
||||
`h` are the embeddings of shape `[batch_size, seq_len, d_model]`
|
||||
"""
|
||||
|
||||
# Residual
|
||||
h_res = h
|
||||
# Pre-norm
|
||||
h = self.norm(h)
|
||||
# First linear layer
|
||||
h = self.lin1(h)
|
||||
# Activation
|
||||
h = self.act(h)
|
||||
# Second linear layer
|
||||
h = self.lin2(h)
|
||||
|
||||
# Add the residual connection
|
||||
return h + h_res
|
||||
|
||||
|
||||
class NearestNeighborEncoder(nn.Module):
|
||||
"""
|
||||
## Nearest Neighbor Encoder $\text{E\small{NCODER}}(\text{R\small{ET}}(C_u)_{1 \le u \le l}, H)$
|
||||
|
||||
This module encodes the retrieved nearest neighbors
|
||||
"""
|
||||
|
||||
def __init__(self, chunk_len: int, n_layers: int, ca_layers: Set[int],
|
||||
d_model: int, n_heads: int, d_k: int, d_ff: int):
|
||||
"""
|
||||
* `chunk_len` is the length of a chunk
|
||||
* `n_layer` is the number of layers in the encoder $L_{\text{enc}}$
|
||||
* `ca_layers` are the layers with cross attention $P_{\text{enc}}$
|
||||
* `d_model` is the number of features in embeddings
|
||||
* `n_heads` is the number of heads in attention layers
|
||||
* `d_k` is the size of attention heads
|
||||
* `d_ff` is the size of the feed-forward networks hidden layers
|
||||
"""
|
||||
|
||||
super().__init__()
|
||||
self.ca_layers = ca_layers
|
||||
self.chunk_len = chunk_len
|
||||
# Cross-attention layers
|
||||
self.ca = nn.ModuleList([CrossAttention(d_model, n_heads, d_k) for _ in range(len(ca_layers))])
|
||||
# Bi-directional self attention layers
|
||||
self.attn = nn.ModuleList([SelfAttention(d_model, n_heads, d_k, is_causal=False) for _ in range(n_layers)])
|
||||
# Feed forward layers
|
||||
self.ffw = nn.ModuleList([FeedForward(d_model, d_ff) for _ in range(n_layers)])
|
||||
|
||||
# Pre-normalization layer for $H$
|
||||
self.norm_h = nn.LayerNorm(d_model)
|
||||
|
||||
def forward(self, e: torch.Tensor, h: torch.Tensor):
|
||||
"""
|
||||
* `e` are token embeddings of the retrieved nearest neighbors,
|
||||
$\text{E\small{MB}}\big(\text{R\small{ET}}(C_u)_{1 \le u \le l}\big)$
|
||||
of shape `[batch_size, chunks, neighbors, neighbor_len, d_model]`
|
||||
|
||||
* `h` is are the input token embeddings, $H$
|
||||
of shape `[batch_size, seq_len, d_model]`
|
||||
|
||||
*The chunks $u \in [1, l]$ and neighbors $j \in [1, k]$ are processed in parallel.*
|
||||
"""
|
||||
|
||||
# Get shape
|
||||
batch_size, chunks, neighbors, neighbor_len, d_model = e.shape
|
||||
|
||||
# $(H_u)_{u \in [1, l]} \leftarrow \text{S\small{PLIT}}(H)$
|
||||
h_split = h[:, :self.chunk_len * chunks, :].reshape(batch_size, chunks, self.chunk_len, d_model)
|
||||
|
||||
# Pre-norm
|
||||
h_split = self.norm_h(h_split)
|
||||
|
||||
# Keep the index of the cross attention layer
|
||||
p_ca = 0
|
||||
# For all layers $p' \in [1, L_{\text{enc}}]$
|
||||
for p in range(len(self.attn)):
|
||||
# Bi-directional self attention
|
||||
# $E^j_u \leftarrow \text{A\small{TTN}}_{\text{enc}}(E^j_u)$
|
||||
e = self.attn[p](e.view(-1, neighbor_len, d_model)).view(e.shape)
|
||||
|
||||
# Cross attention if $p' \in P_{\text{enc}}$
|
||||
if p in self.ca_layers:
|
||||
# $E^j_u \leftarrow \text{C\small{A}}_{\text{enc}}(E^j_u, H_u)$
|
||||
e = self.ca[p_ca](e, h_split)
|
||||
# Incremnt the cross attention index
|
||||
p_ca += 1
|
||||
|
||||
# Feed forward layer $E^j_u \leftarrow \text{F\small{FW}}_{\text{enc}}(E^j_u)$
|
||||
e = self.ffw[p](e)
|
||||
|
||||
# return $E$
|
||||
return e
|
||||
|
||||
|
||||
class RetroModel(nn.Module):
|
||||
"""
|
||||
## Retro Model
|
||||
|
||||
This is the Retro decoder
|
||||
"""
|
||||
|
||||
def __init__(self, n_vocab: int, d_model: int, n_layers: int, ca_layers: Set[int], chunk_len: int,
|
||||
n_heads: int, d_k: int, d_ff: int, encoder: NearestNeighborEncoder):
|
||||
"""
|
||||
* `v_vocab` is the number of tokens in the vocabulary
|
||||
* `d_model` is the number of features in embeddings
|
||||
* `n_layers` is the number of layers in the decoder $L$
|
||||
* `ca_layers` are the layers with cross attention $P$
|
||||
* `chunk_len` is the length of a chunk
|
||||
* `n_heads` is the number of heads in attention layers
|
||||
* `d_k` is the size of attention heads
|
||||
* `d_ff` is the size of the feed-forward networks hidden layers
|
||||
* `encoder` is the nearest neighbor encoder
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
self.ca_layers = ca_layers
|
||||
self.encoder = encoder
|
||||
|
||||
# Token embedding layer
|
||||
self.emb = nn.Embedding(n_vocab, d_model)
|
||||
# Chunked cross attention layers $\text{C\small{CA}}$
|
||||
self.cca = nn.ModuleList(
|
||||
[ChunkedCrossAttention(d_model, n_heads, d_k, chunk_len) for _ in range(len(ca_layers))])
|
||||
# Attention layers $\text{A\small{TTN}}$
|
||||
self.attn = nn.ModuleList([SelfAttention(d_model, n_heads, d_k, is_causal=True) for _ in range(n_layers)])
|
||||
# Feed forward layers $\text{F\small{FW}}$
|
||||
self.ffw = nn.ModuleList([FeedForward(d_model, d_ff) for _ in range(n_layers)])
|
||||
# Readout layer $\text{R\small{EAD}}$
|
||||
self.read = nn.Linear(d_model, n_vocab)
|
||||
|
||||
# Pre-normalization layer for nearest neighbor embeddings from
|
||||
# $\text{E\small{NCODER}}(\text{R\small{ET}}(C_u)_{1 \le u \le l}, H)$
|
||||
self.norm_e = nn.LayerNorm(d_model)
|
||||
|
||||
def forward(self, x: torch.Tensor, ret: torch.Tensor):
|
||||
"""
|
||||
* `x` is the input sequence, $X$ of shape `[batch_size, seq_len]`
|
||||
* `ret` are the retrieved neighbors
|
||||
$\text{R\small{ET}}(C_u)_{1 \le u \le l}$
|
||||
of shape `[batch_size, chunks, neighbors, neighbor_len]`
|
||||
"""
|
||||
|
||||
# Get input embeddings $H \leftarrow \text{E\small{MB}}(X)$
|
||||
h = self.emb(x)
|
||||
|
||||
# Embeddings of the retrieved neighbors
|
||||
# $E^j_u = \text{E\small{MB}}_{\text{enc}}\big(\text{R\small{ET}}(C_u)^j\big)$.
|
||||
#
|
||||
# We use same embeddings for both input and neighbors
|
||||
ret_emb = self.emb(ret)
|
||||
|
||||
# Keep index of the chunked cross attention layer
|
||||
p_ca = 0
|
||||
# For all layers $p \in [1, L]$
|
||||
for p in range(len(self.attn)):
|
||||
# Causal self attention $H \leftarrow \text{A\small{TTN}}(H)$
|
||||
h = self.attn[p](h)
|
||||
|
||||
# Get encoder embeddings before the first $\text{C\small{CA}}$ layer,
|
||||
# when $p = \min(P)$
|
||||
if self.ca_layers and p == min(self.ca_layers):
|
||||
# $E = \text{E\small{NCODER}}(\text{R\small{ET}}(C_u)_{1 \le u \le l}, H)$
|
||||
#
|
||||
# We passed the embeddings of $\text{R\small{ET}}(C_u)_{1 \le u \le l}$ to encoder.
|
||||
e = self.encoder(ret_emb, h)
|
||||
# Normalize encoder embeddings
|
||||
e = self.norm_e(e)
|
||||
|
||||
# Chunked-cross attention if $p \in P$
|
||||
if p in self.ca_layers:
|
||||
# $H \leftarrow \text{C\small{CA}}(H, E)$
|
||||
h = self.cca[p_ca](h, e)
|
||||
# Increment chunked cross-attention index
|
||||
p_ca += 1
|
||||
|
||||
# $H \leftarrow \text{F\small{FW}}(H)$
|
||||
h = self.ffw[p](h)
|
||||
|
||||
# $O \leftarrow \text{R\small{EAD}}(H)$
|
||||
return self.read(h)
|
||||
|
||||
|
||||
def _test():
|
||||
"""
|
||||
### Test the model with fake data
|
||||
"""
|
||||
chunk_len = 4
|
||||
d_model = 8
|
||||
d_ff = 32
|
||||
n_heads = 2
|
||||
d_k = 4
|
||||
|
||||
device = torch.device('cuda:0')
|
||||
|
||||
m = RetroModel(5, d_model, 6, {2, 5}, chunk_len, n_heads, d_k, d_ff,
|
||||
encoder=NearestNeighborEncoder(chunk_len, 2, {1}, d_model, n_heads, d_k, d_ff))
|
||||
|
||||
m.to(device)
|
||||
x = [1, 2, 4, 4, 0, 1, 2, 3, 4, 3]
|
||||
ret = [
|
||||
[[0, 0, 0, 0, 0, 0], [1, 1, 1, 1, 1, 1]],
|
||||
[[0, 0, 0, 0, 0, 0], [1, 1, 1, 1, 1, 1]],
|
||||
]
|
||||
res = m(torch.tensor([x] * 10).to(device), torch.tensor([ret] * 10).to(device))
|
||||
|
||||
inspect(res)
|
||||
|
||||
|
||||
#
|
||||
if __name__ == '__main__':
|
||||
_test()
|
||||
@@ -0,0 +1,223 @@
|
||||
"""
|
||||
---
|
||||
title: RETRO training
|
||||
summary: >
|
||||
Training RETRO model with Tiny Shakespeare dataset
|
||||
---
|
||||
|
||||
# RETRO training
|
||||
|
||||
This is the training code for
|
||||
[RETRO](index.html).
|
||||
"""
|
||||
|
||||
import torch
|
||||
from labml import monit, lab, tracker, experiment, logger
|
||||
from labml.logger import Text
|
||||
from labml_nn.helpers.datasets import TextFileDataset
|
||||
from labml_nn.optimizers.noam import Noam
|
||||
from labml_nn.transformers.retro import model as retro
|
||||
from labml_nn.transformers.retro.dataset import Dataset, RetroIndex
|
||||
from labml_nn.transformers.retro.model import RetroModel, NearestNeighborEncoder
|
||||
from torch import nn
|
||||
from torch.utils.data import DataLoader, RandomSampler
|
||||
|
||||
|
||||
class Sampler:
|
||||
"""
|
||||
## Sampler
|
||||
|
||||
This class greedily samples from a model.
|
||||
"""
|
||||
|
||||
def __init__(self, device: torch.device, model: retro.RetroModel, tds: TextFileDataset, chunk_len: int):
|
||||
"""
|
||||
* `device` is the device of the model
|
||||
* `model` is the [Retro mode](retro.html)
|
||||
* `tds` is the text dataset (used to get neighbor chunks)
|
||||
* `chunk_len` is the length of a chunk
|
||||
"""
|
||||
self.chunk_len = chunk_len
|
||||
self.tds = tds
|
||||
self.model = model
|
||||
self.device = device
|
||||
|
||||
# [Retro index](database.html)
|
||||
self.index = RetroIndex()
|
||||
|
||||
def retrieve_nearest_neighbours(self, chunk: str):
|
||||
"""
|
||||
### Retrieve nearest neighbors of a given chunk
|
||||
"""
|
||||
|
||||
# Retrieve the offsets of the nearest neighbors
|
||||
neighbor_offsets = self.index([chunk], None)
|
||||
|
||||
# Get the neighbors (with neighbor length equal to `chunk_len * 2`)
|
||||
text = self.tds.train
|
||||
neighbors = [text[j: j + self.chunk_len * 2] for j in neighbor_offsets[0]]
|
||||
|
||||
#
|
||||
return neighbors
|
||||
|
||||
def sample(self, prompt: str, sample_len: int):
|
||||
"""
|
||||
### Sample text from the given prompt
|
||||
"""
|
||||
|
||||
# To store nearest neighbors as strings
|
||||
neighbors_str = []
|
||||
|
||||
# Sampled text
|
||||
sampled = ''
|
||||
|
||||
# Sample `sample_len` tokens
|
||||
for i in range(sample_len):
|
||||
# We need to retrieve neighbors,
|
||||
# if there are more sampled chunks than we have already retrieved for
|
||||
while len(neighbors_str) < len(prompt) // self.chunk_len:
|
||||
# Get the last chunk for which we haven't retrieved neighbors
|
||||
off = len(neighbors_str) * self.chunk_len
|
||||
chunk = prompt[off: off + self.chunk_len]
|
||||
# Retrieve nearest neighbors
|
||||
neighbors_str.append(self.retrieve_nearest_neighbours(chunk))
|
||||
|
||||
# Tokenize the input
|
||||
src = self.tds.text_to_i(prompt)
|
||||
# Tokenize the retrieved neighbors
|
||||
neighbors = torch.stack([torch.stack([self.tds.text_to_i(n) for n in chunk]) for chunk in neighbors_str])
|
||||
|
||||
# Move them to the same device as the model
|
||||
src = src.to(self.device)
|
||||
neighbors = neighbors.to(self.device)
|
||||
|
||||
# Get model output
|
||||
res = self.model(src[None, :], neighbors[None, :, :, :])
|
||||
|
||||
# Greedily sample the last token
|
||||
token = res[0, -1, :].argmax(dim=-1)
|
||||
|
||||
# Add the sampled token text to the prompt and sample text
|
||||
prompt += self.tds.itos[token.item()]
|
||||
sampled += self.tds.itos[token.item()]
|
||||
|
||||
#
|
||||
return sampled
|
||||
|
||||
|
||||
class Trainer:
|
||||
"""
|
||||
## Retro trainer
|
||||
"""
|
||||
|
||||
def __init__(self, device: torch.device, model: retro.RetroModel,
|
||||
dataloader: DataLoader, optimizer: torch.optim.Optimizer):
|
||||
"""
|
||||
* `device` is the device of the model
|
||||
* `model` is the [Retro mode](retro.html)
|
||||
* `dataloader` is the dataloader for the [dataset with pre-retrieved neighbors](dataset.html)
|
||||
* `optimizer` is the optimizer
|
||||
"""
|
||||
self.optimizer = optimizer
|
||||
self.device = device
|
||||
self.dataloader = dataloader
|
||||
self.model = model
|
||||
self.loss_func = nn.CrossEntropyLoss()
|
||||
|
||||
def __call__(self):
|
||||
"""
|
||||
### Train the model for an epoch
|
||||
"""
|
||||
|
||||
# Iterate through training data
|
||||
for i, (src, tgt, neighbors) in monit.enum('Train', self.dataloader):
|
||||
# Move data to the device
|
||||
src, tgt, neighbors = src.to(self.device), tgt.to(self.device), neighbors.to(self.device)
|
||||
|
||||
# Forward pass
|
||||
res = self.model(src, neighbors)
|
||||
# Calculate loss
|
||||
loss = self.loss_func(res.view(-1, res.shape[-1]), tgt.view(-1))
|
||||
|
||||
# Clear the gradients
|
||||
self.optimizer.zero_grad()
|
||||
# Backward pass
|
||||
loss.backward()
|
||||
# Optimize the model
|
||||
self.optimizer.step()
|
||||
|
||||
# Save training statistics and increment the global step counter
|
||||
tracker.save({'loss.train': loss})
|
||||
tracker.add_global_step(len(src))
|
||||
|
||||
|
||||
def train():
|
||||
"""
|
||||
## Create and train a small model
|
||||
"""
|
||||
|
||||
# Create an experiment
|
||||
experiment.create(name='retro_small')
|
||||
|
||||
# GPU device
|
||||
device = torch.device('cuda:0')
|
||||
|
||||
# Load Tiny Shakespeare dataset
|
||||
tds = TextFileDataset(
|
||||
lab.get_data_path() / 'tiny_shakespeare.txt',
|
||||
list,
|
||||
url='https://raw.githubusercontent.com/karpathy/char-rnn/master/data/tinyshakespeare/input.txt')
|
||||
|
||||
# Load [Retro dataset](dataset.html)
|
||||
train_dataset = Dataset(lab.get_data_path() / 'retro_train_dataset.json', tds)
|
||||
|
||||
# Create dataloader
|
||||
train_dl = DataLoader(train_dataset,
|
||||
batch_size=4,
|
||||
sampler=RandomSampler(train_dataset, replacement=True))
|
||||
|
||||
# Hyper-parameters
|
||||
chunk_len = 16
|
||||
d_model = 128
|
||||
d_ff = 512
|
||||
n_heads = 16
|
||||
d_k = 16
|
||||
|
||||
# Create the nearest neighbor encoder
|
||||
nearest_neighbor_encoder = NearestNeighborEncoder(chunk_len, 6, {3}, d_model, n_heads, d_k, d_ff)
|
||||
# Create the model
|
||||
model = RetroModel(tds.n_tokens, d_model, 6,
|
||||
{3, 5},
|
||||
chunk_len, n_heads, d_k, d_ff,
|
||||
encoder=nearest_neighbor_encoder)
|
||||
# Move the model to the device
|
||||
model = model.to(device)
|
||||
# Create the optimizer
|
||||
optimizer = Noam(model.parameters(), lr=1., d_model=d_model, warmup=2_000)
|
||||
# Create the `Trainer`
|
||||
trainer = Trainer(device, model, train_dl, optimizer)
|
||||
# Create the `Sampler`
|
||||
sampler = Sampler(device, model, tds, chunk_len)
|
||||
#
|
||||
prompt = '''Second Citizen:\nOne word, good citizens.\n\nFirst Citizen:'''
|
||||
|
||||
# Set models for saving and loading
|
||||
experiment.add_pytorch_models(model=model)
|
||||
|
||||
# Start the experiment
|
||||
with experiment.start():
|
||||
# Train for `32` epochs
|
||||
for epoch in monit.loop(32):
|
||||
# Train
|
||||
trainer()
|
||||
# Print a new line
|
||||
tracker.new_line()
|
||||
# Sample from the `prompt`
|
||||
logger.log([(prompt.replace('\n', '\\n\n'), Text.subtle),
|
||||
(sampler.sample(prompt, 128).replace('\n', '\\n\n'), Text.none)])
|
||||
# Save models
|
||||
|
||||
|
||||
#
|
||||
if __name__ == '__main__':
|
||||
train()
|
||||
@@ -0,0 +1,235 @@
|
||||
"""
|
||||
---
|
||||
title: Rotary Positional Embeddings (RoPE)
|
||||
summary: >
|
||||
Annotated implementation of RoPE from paper
|
||||
RoFormer: Enhanced Transformer with Rotary Position Embedding
|
||||
---
|
||||
|
||||
# Rotary Positional Embeddings (RoPE)
|
||||
|
||||
This is an implementation of
|
||||
[Rotary Positional Embeddings (RoPE)](https://arxiv.org/abs/2104.09864)
|
||||
in [PyTorch](https://pytorch.org).
|
||||
|
||||
Rotary Positional Embeddings (RoPE) encode position information of tokens
|
||||
with a rotation matrix that naturally incorporates explicit relative position
|
||||
dependency.
|
||||
|
||||
Here's [the training code](experiment.html) for training a transformer model with RoPE
|
||||
on Tiny Shakespeare dataset.
|
||||
"""
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
from labml.logger import inspect
|
||||
from labml_nn.transformers.mha import MultiHeadAttention
|
||||
|
||||
|
||||
class RotaryPositionalEmbeddings(nn.Module):
|
||||
"""
|
||||
## RoPE module
|
||||
|
||||
Rotary encoding transforms pairs of features by rotating in the 2D plane.
|
||||
That is, it organizes the $d$ features as $\frac{d}{2}$ pairs.
|
||||
Each pair can be considered a coordinate in a 2D plane, and the encoding will rotate it
|
||||
by an angle depending on the position of the token.
|
||||
|
||||
### For a pair of features
|
||||
|
||||
Let $x^{(1)}_m$ and $x^{(2)}_m$ be two features of the
|
||||
key or query of any head at position $m$.
|
||||
Or for simplicity assume $x$ has only two features.
|
||||
Then the transformation is,
|
||||
|
||||
\begin{align}
|
||||
RoPE\big(x^{(1)}_m, x^{(2)}_m, m\big) &=
|
||||
\begin{pmatrix}
|
||||
\cos m \theta & - \sin m \theta \\
|
||||
\sin m \theta & \cos m \theta
|
||||
\end{pmatrix}
|
||||
\begin{pmatrix}
|
||||
x^{(1)}_m \\
|
||||
x^{(2)}_m \\
|
||||
\end{pmatrix} \\
|
||||
&=
|
||||
\begin{pmatrix}
|
||||
x^{(1)}_m \cos m\theta - x^{(2)}_m \sin m \theta \\
|
||||
x^{(2)}_m \cos m\theta + x^{(1)}_m \sin m \theta \\
|
||||
\end{pmatrix} \\
|
||||
\end{align}
|
||||
|
||||
where $\theta$ is a constant angle. The other pairs of features are transformed similarly.
|
||||
|
||||
### Attention is relative
|
||||
|
||||
For a pair of features, dot-product attention score between two positions $m$ and $n$ would be
|
||||
|
||||
\begin{align}
|
||||
\Big \langle RoPE\big(x^{(1)}_m, x^{(2)}_m, m\big), RoPE\big(x^{(1)}_n, x^{(2)}_n, n\big) \Big \rangle &= \\
|
||||
(x^{(1)}_m \cos m\theta - x^{(2)}_m \sin m \theta)(x^{(1)}_n \cos n\theta - x^{(2)}_n \sin n \theta) &+ \\
|
||||
(x^{(2)}_m \cos m\theta + x^{(1)}_m \sin m \theta)(x^{(2)}_n \cos n\theta + x^{(1)}_n \sin n \theta) &= \\
|
||||
x^{(1)}_m x^{(1)}_n (\cos m\theta \cos n\theta + \sin m \theta \sin n \theta) &+ \\
|
||||
x^{(1)}_m x^{(2)}_n (-\cos m\theta \sin n\theta + \sin m \theta \cos n \theta) &+ \\
|
||||
x^{(2)}_m x^{(1)}_n (-\sin m\theta \cos n\theta + \cos m \theta \sin n \theta) &+ \\
|
||||
x^{(2)}_m x^{(2)}_n (\sin m\theta \sin n\theta + \cos m \theta \cos n \theta) &= \\
|
||||
|
||||
x^{(1)}_m x^{(1)}_n \cos (m - n) \theta +
|
||||
x^{(1)}_m x^{(2)}_n \sin(m - n) \theta &+ \\
|
||||
- x^{(2)}_m x^{(1)}_n \sin (m - n) \theta +
|
||||
x^{(2)}_m x^{(2)}_n \cos (m - n) \theta &= \\
|
||||
|
||||
\big(x^{(1)}_m \cos (m - n)\theta - x^{(2)}_m \sin (m - n) \theta\big) x^{(1)}_n &+ \\
|
||||
\big(x^{(2)}_m \cos (m - n)\theta + x^{(1)}_m \sin (m - n) \theta\big) x^{(2)}_n &= \\
|
||||
|
||||
\Big \langle RoPE\big(x^{(1)}_m, x^{(2)}_m, m - n\big), RoPE\big(x^{(1)}_n, x^{(2)}_n, 0\big) \Big \rangle
|
||||
\end{align}
|
||||
|
||||
This shows that for dot-production attention the rotary encodings gives relative attention.
|
||||
|
||||
### For all features
|
||||
|
||||
The features are grouped into pairs and handled as above. They use a different $\theta$ for each pair.
|
||||
|
||||
The paper suggests using $\Theta = {\theta_i = 10000^{\frac{2(i-1)}{d}}, i \in [1, 2, ..., \frac{d}{2}]}$
|
||||
for the $\frac{d}{2}$ pairs of features.
|
||||
|
||||
The original implementation of RoPE divide the $d$-dimension features into $\frac{d}{2}$ pairs of features ($i$, $i + 1$).
|
||||
In this implementation we pair feature $i$ with feature $i + \frac{d}{2}$. So for position $m$ we transform
|
||||
|
||||
\begin{align}
|
||||
\begin{pmatrix}
|
||||
x^{(i)}_m \\
|
||||
x^{(i + \frac{d}{2})}_m
|
||||
\end{pmatrix}
|
||||
\end{align}
|
||||
|
||||
to
|
||||
|
||||
\begin{align}
|
||||
\begin{pmatrix}
|
||||
x^{(i)}_m \cos m \theta_i - x^{(i + \frac{d}{2})}_m \sin m \theta_i \\
|
||||
x^{(i + \frac{d}{2})}_m \cos m\theta_i + x^{(i)}_m \sin m \theta_i \\
|
||||
\end{pmatrix} \\
|
||||
\end{align}
|
||||
"""
|
||||
|
||||
def __init__(self, d: int, base: int = 10_000):
|
||||
"""
|
||||
* `d` is the number of features $d$
|
||||
* `base` is the constant used for calculating $\Theta$
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
self.base = base
|
||||
self.d = d
|
||||
self.cos_cached = None
|
||||
self.sin_cached = None
|
||||
|
||||
def _build_cache(self, x: torch.Tensor):
|
||||
"""
|
||||
Cache $\cos$ and $\sin$ values
|
||||
"""
|
||||
# Return if cache is already built
|
||||
if self.cos_cached is not None and x.shape[0] <= self.cos_cached.shape[0]:
|
||||
return
|
||||
|
||||
# Get sequence length
|
||||
seq_len = x.shape[0]
|
||||
|
||||
# $\Theta = {\theta_i = 10000^{-\frac{2(i-1)}{d}}, i \in [1, 2, ..., \frac{d}{2}]}$
|
||||
theta = 1. / (self.base ** (torch.arange(0, self.d, 2).float() / self.d)).to(x.device)
|
||||
|
||||
# Create position indexes `[0, 1, ..., seq_len - 1]`
|
||||
seq_idx = torch.arange(seq_len, device=x.device).float().to(x.device)
|
||||
|
||||
# Calculate the product of position index and $\theta_i$
|
||||
idx_theta = torch.einsum('n,d->nd', seq_idx, theta)
|
||||
|
||||
# Concatenate so that for row $m$ we have
|
||||
# $[m \theta_0, m \theta_1, ..., m \theta_{\frac{d}{2}}, m \theta_0, m \theta_1, ..., m \theta_{\frac{d}{2}}]$
|
||||
idx_theta2 = torch.cat([idx_theta, idx_theta], dim=1)
|
||||
|
||||
# Cache them
|
||||
self.cos_cached = idx_theta2.cos()[:, None, None, :]
|
||||
self.sin_cached = idx_theta2.sin()[:, None, None, :]
|
||||
|
||||
def _neg_half(self, x: torch.Tensor):
|
||||
# $\frac{d}{2}$
|
||||
d_2 = self.d // 2
|
||||
|
||||
# Calculate $[-x^{(\frac{d}{2} + 1)}, -x^{(\frac{d}{2} + 2)}, ..., -x^{(d)}, x^{(1)}, x^{(2)}, ..., x^{(\frac{d}{2})}]$
|
||||
return torch.cat([-x[:, :, :, d_2:], x[:, :, :, :d_2]], dim=-1)
|
||||
|
||||
def forward(self, x: torch.Tensor):
|
||||
"""
|
||||
* `x` is the Tensor at the head of a key or a query with shape `[seq_len, batch_size, n_heads, d]`
|
||||
"""
|
||||
# Cache $\cos$ and $\sin$ values
|
||||
self._build_cache(x)
|
||||
|
||||
# Sequence length
|
||||
seq_len = x.shape[0]
|
||||
|
||||
# Split the features, we can choose to apply rotary embeddings only to a partial set of features.
|
||||
x_rope, x_pass = x[..., :self.d], x[..., self.d:]
|
||||
|
||||
# Calculate
|
||||
# $[-x^{(\frac{d}{2} + 1)}, -x^{(\frac{d}{2} + 2)}, ..., -x^{(d)}, x^{(1)}, x^{(2)}, ..., x^{(\frac{d}{2})}]$
|
||||
neg_half_x = self._neg_half(x_rope)
|
||||
|
||||
# Calculate
|
||||
#
|
||||
# \begin{align}
|
||||
# \begin{pmatrix}
|
||||
# x^{(i)}_m \cos m \theta_i - x^{(i + \frac{d}{2})}_m \sin m \theta_i \\
|
||||
# x^{(i + \frac{d}{2})}_m \cos m\theta_i + x^{(i)}_m \sin m \theta_i \\
|
||||
# \end{pmatrix} \\
|
||||
# \end{align}
|
||||
#
|
||||
# for $i \in {1, 2, ..., \frac{d}{2}}$
|
||||
x_rope = (x_rope * self.cos_cached[:seq_len]) + (neg_half_x * self.sin_cached[:seq_len])
|
||||
|
||||
#
|
||||
return torch.cat((x_rope, x_pass), dim=-1)
|
||||
|
||||
|
||||
class RotaryPEMultiHeadAttention(MultiHeadAttention):
|
||||
"""
|
||||
## Multi-head attention with rotary positional embeddings
|
||||
|
||||
We override [multi-head attention from original transformer](../mha.html).
|
||||
"""
|
||||
|
||||
def __init__(self, heads: int, d_model: int, rope_percentage: float = 0.5, dropout_prob: float = 0.0):
|
||||
super().__init__(heads, d_model, dropout_prob)
|
||||
|
||||
# Rotary positional embedding layers
|
||||
d_rope = int(self.d_k * rope_percentage)
|
||||
self.query_rotary_pe = RotaryPositionalEmbeddings(d_rope)
|
||||
self.key_rotary_pe = RotaryPositionalEmbeddings(d_rope)
|
||||
|
||||
def get_scores(self, query: torch.Tensor, key: torch.Tensor):
|
||||
"""
|
||||
### Calculate scores between queries and keys
|
||||
"""
|
||||
|
||||
# Calculate dot-product with RoPE
|
||||
return torch.einsum('ibhd,jbhd->ijbh', self.query_rotary_pe(query), self.key_rotary_pe(key))
|
||||
|
||||
|
||||
def _test_rotary():
|
||||
"""
|
||||
Testing RoPE with a simple example
|
||||
"""
|
||||
x = torch.tensor([[1, 2, 3, 4], [4, 5, 6, 7], [7, 8, 9, 10]], dtype=torch.float)
|
||||
x = x[:, None, None, :]
|
||||
inspect(x)
|
||||
|
||||
rotary_pe = RotaryPositionalEmbeddings(4)
|
||||
inspect(rotary_pe(x))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
_test_rotary()
|
||||
@@ -0,0 +1,102 @@
|
||||
"""
|
||||
---
|
||||
title: Rotary Positional Embeddings (RoPE) Experiment
|
||||
summary: This experiment trains a transformer model with Rotary Positional Embeddings (RoPE) on tiny Shakespeare dataset.
|
||||
---
|
||||
|
||||
# Rotary Positional Embeddings (RoPE) Experiment
|
||||
|
||||
This is an annotated PyTorch experiment to train a transformer model with Rotary Positional Embeddings (RoPE).
|
||||
"""
|
||||
|
||||
from labml import experiment
|
||||
from labml.configs import option, calculate
|
||||
from labml_nn.transformers import TransformerConfigs
|
||||
from labml_nn.transformers.basic.autoregressive_experiment import AutoregressiveTransformer, Configs
|
||||
|
||||
|
||||
# ### Rotary PE attention
|
||||
def _rotary_pe_mha(c: TransformerConfigs):
|
||||
from labml_nn.transformers.rope import RotaryPEMultiHeadAttention
|
||||
return RotaryPEMultiHeadAttention(c.n_heads, c.d_model, 1.)
|
||||
|
||||
|
||||
# Configuration options
|
||||
calculate(TransformerConfigs.encoder_attn, 'rotary', _rotary_pe_mha)
|
||||
calculate(TransformerConfigs.decoder_attn, 'rotary', _rotary_pe_mha)
|
||||
calculate(TransformerConfigs.decoder_mem_attn, 'rotary', _rotary_pe_mha)
|
||||
|
||||
|
||||
@option(Configs.model, 'rotary_pe_transformer')
|
||||
def _model(c: Configs):
|
||||
"""
|
||||
Create an autoregressive model and initialize weights
|
||||
"""
|
||||
m = AutoregressiveTransformer(c.transformer.encoder,
|
||||
c.transformer.src_embed,
|
||||
c.transformer.generator).to(c.device)
|
||||
|
||||
return m
|
||||
|
||||
|
||||
def main():
|
||||
# Create experiment
|
||||
experiment.create(name="rotary_pe_transformer", writers={'screen'})
|
||||
# Create configs
|
||||
conf = Configs()
|
||||
# Override configurations
|
||||
experiment.configs(conf, {
|
||||
# No fixed positional embeddings
|
||||
'transformer.src_embed': 'no_pos',
|
||||
'transformer.tgt_embed': 'no_pos',
|
||||
|
||||
# Encoder with RoPE
|
||||
'transformer.encoder_attn': 'rotary',
|
||||
|
||||
#
|
||||
'model': 'rotary_pe_transformer',
|
||||
|
||||
# Use character level tokenizer
|
||||
'tokenizer': 'character',
|
||||
# Prompt separator is blank
|
||||
'prompt_separator': '',
|
||||
# Starting prompt for sampling
|
||||
'prompt': 'It is ',
|
||||
# Use Tiny Shakespeare dataset
|
||||
'text': 'tiny_shakespeare',
|
||||
|
||||
# Use a context size of $256$
|
||||
'seq_len': 512,
|
||||
# Train for 32 epochs
|
||||
'epochs': 32,
|
||||
# Batch size $4$
|
||||
'batch_size': 4,
|
||||
# Switch between training and validation for $10$ times
|
||||
# per epoch
|
||||
'inner_iterations': 10,
|
||||
|
||||
# Model size
|
||||
'd_model': 128,
|
||||
'transformer.ffn.d_ff': 512,
|
||||
'transformer.n_heads': 16,
|
||||
'transformer.dropout': 0.0,
|
||||
|
||||
# Use [Noam optimizer](../../optimizers/noam.html)
|
||||
'optimizer.optimizer': 'Noam',
|
||||
'optimizer.learning_rate': 1.,
|
||||
|
||||
'dataloader_shuffle_with_replacement': True
|
||||
})
|
||||
|
||||
# Set models for saving and loading
|
||||
experiment.add_pytorch_models({'model': conf.model})
|
||||
|
||||
# Start the experiment
|
||||
with experiment.start():
|
||||
# Run training
|
||||
conf.run()
|
||||
|
||||
|
||||
#
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,246 @@
|
||||
"""
|
||||
---
|
||||
title: Rotary Positional Embeddings with Relative distance (RoPER)
|
||||
summary: >
|
||||
This is an implementation of RoPER which adds relative distance information to embeddings on
|
||||
top of RoPE introduced in RoFormer: Enhanced Transformer with Rotary Position Embedding
|
||||
---
|
||||
|
||||
*RoPER is work by [Georges Harik (@gharik)](https://twitter.com/gharik),
|
||||
and this implementation is based on his original code.*
|
||||
|
||||
# Rotary Positional Embeddings with Relative distance (RoPER)
|
||||
|
||||
[Rotary Positional Embeddings (RoPE)](https://arxiv.org/abs/2104.09864) includes
|
||||
relative positions in attention score calculation.
|
||||
However, the embeddings themselves do not get any positional information
|
||||
, [except what it can get implicitly from causal attention](https://arxiv.org/abs/2c364684b15b11ecac827bce58715ee7).
|
||||
|
||||
RoPER adds relative positional information explicitly to value embeddings.
|
||||
Specifically, it adds the relative positions of the tokens it paid attention to.
|
||||
We use same rotary positional embeddings to rotate the values in attention,
|
||||
Then, after taking the weighted sum,
|
||||
we rotate the final in the opposite direction.
|
||||
Which is equivalent to rotating each of the values (before attention) relative to the current position.
|
||||
|
||||
Here's [the training code](experiment.html) for training a transformer model with RoPER
|
||||
on an arithmetic addition where we can see significant improvement over RoPE.
|
||||
|
||||
### Relative distances in embeddings
|
||||
|
||||
For any head, let $a_{n,i}$ be the attention from position $n$ to position $i$,
|
||||
and $v_i$ be the value embeddings at position $i$. Let's denote individual features
|
||||
as $v^{(1)}_i, v^{(2)}_i, \dots$.
|
||||
|
||||
Normally, we would take the weight sum of value embeddings
|
||||
|
||||
$$o^{(j)}_n = \sum_i a_{n,i} v^{(j)}_i$$
|
||||
|
||||
This doesn't explicitly add any distance information about the positions $i$ to final
|
||||
result $o^{(j)}_n$.
|
||||
|
||||
RoPER pairs features like RoPE and transform them.
|
||||
For a pair $v^{(1)}_m$ and $v^{(2)}_m$ it transforms them by
|
||||
$RoPE\big(v^{(1)}_m, v^{(2)}_m, m\big)$.
|
||||
Let us donate the transformed features with $\hat{v}^{(1)}_m, \hat{v}^{(2)}_m$.
|
||||
Then it rotates the weighted sum $\hat{o}^{(j)}_n$ in the the reverse direction with
|
||||
$RoPE\big(\hat{o}^{(1)}_n, \hat{o}^{(2)}_n, -n\big)$.
|
||||
*Note the *$-n$.
|
||||
|
||||
Note that,
|
||||
|
||||
\begin{align}
|
||||
RoPE\big(x^{(1)}_m, x^{(2)}_m, m\big) &=
|
||||
\begin{pmatrix}
|
||||
\cos m \theta & - \sin m \theta \\
|
||||
\sin m \theta & \cos m \theta
|
||||
\end{pmatrix}
|
||||
\begin{pmatrix}
|
||||
x^{(1)}_m \\
|
||||
x^{(2)}_m \\
|
||||
\end{pmatrix} \\
|
||||
&=
|
||||
\begin{pmatrix}
|
||||
x^{(1)}_m \cos m\theta - x^{(2)}_m \sin m \theta \\
|
||||
x^{(2)}_m \cos m\theta + x^{(1)}_m \sin m \theta \\
|
||||
\end{pmatrix} \\
|
||||
\end{align}
|
||||
|
||||
Final output after with the transformations is,
|
||||
|
||||
\begin{align}
|
||||
RoPE\big(\hat{o}^{(1)}_n, \hat{o}^{(2)}_n, -n\big) &= \\
|
||||
\begin{pmatrix}
|
||||
\hat{o}^{(1)}_n \cos n\theta + \hat{o}^{(2)}_n \sin n \theta \\
|
||||
\hat{o}^{(2)}_n \cos n\theta - \hat{o}^{(1)}_n \sin n \theta \\
|
||||
\end{pmatrix} \\
|
||||
\end{align}
|
||||
|
||||
*Note that *$\sin (-n \theta) = -\sin n \theta$.
|
||||
|
||||
Let's expand the first term $\hat{o}^{(1)}_n \cos n\theta + \hat{o}^{(2)}_n \sin n \theta$,
|
||||
|
||||
\begin{align}
|
||||
\hat{o}^{(1)}_n \cos n\theta + \hat{o}^{(2)}_n \sin n \theta &= \\
|
||||
\sum_i a_{n,i} \hat{v}^{(1)}_i \cos n\theta + \sum_i a_{n,i} \hat{v}^{(2)}_i \sin n \theta &= \\
|
||||
|
||||
\sum_i a_{n,i} \Big( v^{(1)}_i \cos i\theta - v^{(2)}_i \sin i \theta \Big) \cos n\theta &+ \\
|
||||
\sum_i a_{n,i} \Big( v^{(2)}_i \cos i\theta + v^{(1)}_i \sin i \theta \Big) \sin m \theta &= \\
|
||||
|
||||
\sum_i a_{n,i} v^{(1)}_i \Big( \cos i\theta \cos n\theta + \sin i \theta \sin n \theta \Big) &+ \\
|
||||
\sum_i a_{n,i} v^{(2)}_i \Big( \cos i\theta \sin n\theta - \sin i \theta \cos n \theta \Big) &= \\
|
||||
|
||||
\sum_i a_{n,i} v^{(1)}_i \cos (i - n) \theta - \sum_i a_{n,i} v^{(2)}_i \sin (i - n) \theta &= \\
|
||||
|
||||
\sum_i a_{n,i} v^{(1)}_i \cos (i - n) \theta - \sum_i a_{n,i} v^{(2)}_i \sin (i - n) \theta
|
||||
\end{align}
|
||||
|
||||
Simiarly we can show the second term is equal to,
|
||||
|
||||
$$\sum_i a_{n,i} v^{(1)}_i \cos (i - n) \theta + \sum_i a_{n,i} v^{(2)}_i \sin (i - n) \theta$$
|
||||
|
||||
Which gives,
|
||||
|
||||
\begin{align}
|
||||
RoPE\big(\hat{o}^{(1)}_n, \hat{o}^{(2)}_n, -n\big) &= \\
|
||||
\begin{pmatrix}
|
||||
\sum_i a_{n,i} v^{(1)}_i \cos (i - n) \theta - \sum_i a_{n,i} v^{(2)}_i \sin (i - n) \theta \\
|
||||
\sum_i a_{n,i} v^{(1)}_i \cos (i - n) \theta + \sum_i a_{n,i} v^{(2)}_i \sin (i - n) \theta \\
|
||||
\end{pmatrix} &= \\
|
||||
\sum_i a_{n,i} RoPE \big (v^{(1)}_i, v^{(1)}_i, (i - n) \theta \big)
|
||||
\end{align}
|
||||
|
||||
That is, the weighted average of values rotated relative to current position.
|
||||
|
||||
[Here's an experiment](arithmetic_experiment.html) that uses RoPER on an arthmetic addition task.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import torch
|
||||
|
||||
from labml_nn.transformers.rope import RotaryPositionalEmbeddings, RotaryPEMultiHeadAttention
|
||||
|
||||
|
||||
class ReverseRotaryPositionalEmbeddings(RotaryPositionalEmbeddings):
|
||||
"""
|
||||
## RoPE module that rotates in the opposite direction
|
||||
|
||||
This inherits from [RoPE rotation implementation](../index.html) and changes the direction.
|
||||
"""
|
||||
|
||||
def forward(self, x: torch.Tensor):
|
||||
"""
|
||||
* `x` is the Tensor at the head of a key or a query with shape `[seq_len, batch_size, n_heads, d]`
|
||||
"""
|
||||
# Cache $\cos$ and $\sin$ values
|
||||
self._build_cache(x)
|
||||
|
||||
# Split the features, we can choose to apply rotary embeddings only to a partial set of features.
|
||||
x_rope, x_pass = x[..., :self.d], x[..., self.d:]
|
||||
|
||||
# Calculate
|
||||
# $[-x^{(\frac{d}{2} + 1)}, -x^{(\frac{d}{2} + 2)}, ..., -x^{(d)}, x^{(1)}, x^{(2)}, ..., x^{(\frac{d}{2})}]$
|
||||
neg_half_x = self._neg_half(x_rope)
|
||||
|
||||
# Calculate
|
||||
#
|
||||
# \begin{align}
|
||||
# \begin{pmatrix}
|
||||
# x^{(i)}_m \cos -m \theta_i - x^{(i + \frac{d}{2})}_m \sin -m \theta_i \\
|
||||
# x^{(i + \frac{d}{2})}_m \cos -m\theta_i + x^{(i)}_m \sin -m \theta_i \\
|
||||
# \end{pmatrix} = \\
|
||||
# \begin{pmatrix}
|
||||
# x^{(i)}_m \cos m \theta_i + x^{(i + \frac{d}{2})}_m \sin m \theta_i \\
|
||||
# x^{(i + \frac{d}{2})}_m \cos m\theta_i - x^{(i)}_m \sin m \theta_i \\
|
||||
# \end{pmatrix} \\
|
||||
# \end{align}
|
||||
#
|
||||
# for $i \in {1, 2, ..., \frac{d}{2}}$
|
||||
x_rope = (x_rope * self.cos_cached[:x.shape[0]]) - (neg_half_x * self.sin_cached[:x.shape[0]])
|
||||
|
||||
#
|
||||
return torch.cat((x_rope, x_pass), dim=-1)
|
||||
|
||||
|
||||
class RotaryValuePEMultiHeadAttention(RotaryPEMultiHeadAttention):
|
||||
"""
|
||||
## Multi-head attention with rotary positional embeddings
|
||||
|
||||
We override [multi-head attention from original transformer](../mha.html).
|
||||
"""
|
||||
|
||||
def __init__(self, heads: int, d_model: int,
|
||||
rope_percentage: float = 0.5, rope_value_percentage: float = 0.5,
|
||||
dropout_prob: float = 0.0):
|
||||
super().__init__(heads, d_model, rope_percentage, dropout_prob)
|
||||
|
||||
# Rotary positional embedding layers
|
||||
d_rope_value = int(self.d_k * rope_value_percentage)
|
||||
|
||||
self.value_rotary_pe = RotaryPositionalEmbeddings(d_rope_value)
|
||||
self.value_reverse_rotary_pe = ReverseRotaryPositionalEmbeddings(d_rope_value)
|
||||
|
||||
def forward(self, *,
|
||||
query: torch.Tensor,
|
||||
key: torch.Tensor,
|
||||
value: torch.Tensor,
|
||||
mask: Optional[torch.Tensor] = None):
|
||||
"""
|
||||
`query`, `key` and `value` are the tensors that store
|
||||
collection of *query*, *key* and *value* vectors.
|
||||
They have shape `[seq_len, batch_size, d_model]`.
|
||||
|
||||
`mask` has shape `[seq_len, seq_len, batch_size]` and
|
||||
`mask[i, j, b]` indicates whether for batch `b`,
|
||||
query at position `i` has access to key-value at position `j`.
|
||||
"""
|
||||
|
||||
# `query`, `key` and `value` have shape `[seq_len, batch_size, d_model]`
|
||||
seq_len, batch_size, _ = query.shape
|
||||
|
||||
if mask is not None:
|
||||
mask = self.prepare_mask(mask, query.shape, key.shape)
|
||||
|
||||
# Prepare `query`, `key` and `value` for attention computation.
|
||||
# These will then have shape `[seq_len, batch_size, heads, d_k]`.
|
||||
query = self.query(query)
|
||||
key = self.key(key)
|
||||
value = self.value(value)
|
||||
|
||||
# Compute attention scores $Q K^\top$.
|
||||
# This gives a tensor of shape `[seq_len, seq_len, batch_size, heads]`.
|
||||
scores = self.get_scores(query, key)
|
||||
|
||||
# Scale scores $\frac{Q K^\top}{\sqrt{d_k}}$
|
||||
scores *= self.scale
|
||||
|
||||
# Apply mask
|
||||
if mask is not None:
|
||||
scores = scores.masked_fill(mask == 0, float('-inf'))
|
||||
|
||||
# $softmax$ attention along the key sequence dimension
|
||||
# $\underset{seq}{softmax}\Bigg(\frac{Q K^\top}{\sqrt{d_k}}\Bigg)$
|
||||
attn = self.softmax(scores)
|
||||
|
||||
# Apply dropout
|
||||
attn = self.dropout(attn)
|
||||
|
||||
# Rotate value embeddings before taking the weighted sum so that they contain positional information
|
||||
value = self.value_rotary_pe(value)
|
||||
|
||||
# Multiply by values
|
||||
# $$\underset{seq}{softmax}\Bigg(\frac{Q K^\top}{\sqrt{d_k}}\Bigg)V$$
|
||||
x = torch.einsum("ijbh,jbhd->ibhd", attn, value)
|
||||
|
||||
# Rotate in the opposite direction so that each embedding hold the relative positions
|
||||
x = self.value_reverse_rotary_pe(x)
|
||||
|
||||
# Save attentions for any other calculations
|
||||
self.attn = attn.detach()
|
||||
|
||||
# Concatenate multiple heads
|
||||
x = x.reshape(seq_len, batch_size, -1)
|
||||
|
||||
# Output layer
|
||||
return self.output(x)
|
||||
@@ -0,0 +1,93 @@
|
||||
"""
|
||||
---
|
||||
title: Rotary Positional Embeddings with Relative distance (RoPER) Experiment
|
||||
summary: This experiment trains a transformer model with Rotary Positional Embeddings with
|
||||
Relative Distance (RoPER) on the arithmetic addition task.
|
||||
---
|
||||
|
||||
# Rotary Positional Embeddings with Relative distance ([RoPER](index.html)) Experiment
|
||||
"""
|
||||
|
||||
from labml import experiment
|
||||
from labml.configs import calculate
|
||||
from labml_nn.experiments.arithmetic_dataset import ArithmeticAutoregression
|
||||
from labml_nn.transformers import TransformerConfigs
|
||||
from labml_nn.transformers.rope.experiment import Configs as RoPEConfigs
|
||||
|
||||
|
||||
class Configs(RoPEConfigs, ArithmeticAutoregression):
|
||||
"""
|
||||
We inherit [RoPE experiment](../experiment.html) and use it for
|
||||
[arithmetic addition task](../../experiments/arithmetic_dataset.html).
|
||||
|
||||
We add the option to change attention to use Rotary Positional Embeddings with Relative distance (RoPER)
|
||||
below.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def _rotary_value_pe_mha(c: TransformerConfigs):
|
||||
"""
|
||||
Use Rotary Positional Embeddings with Relative distance ([RoPER](index.html)) in attention.
|
||||
"""
|
||||
from labml_nn.transformers.rope.value_pe import RotaryValuePEMultiHeadAttention
|
||||
return RotaryValuePEMultiHeadAttention(c.n_heads, c.d_model, 1., 1.)
|
||||
|
||||
|
||||
# Configuration options
|
||||
calculate(TransformerConfigs.encoder_attn, 'rotary_value', _rotary_value_pe_mha)
|
||||
calculate(TransformerConfigs.decoder_attn, 'rotary_value', _rotary_value_pe_mha)
|
||||
calculate(TransformerConfigs.decoder_mem_attn, 'rotary_value', _rotary_value_pe_mha)
|
||||
|
||||
|
||||
def main():
|
||||
# Create experiment
|
||||
experiment.create(name="roper_addition", comment="rotary value 7", writers={'screen', 'labml'})
|
||||
# Create configs
|
||||
conf = Configs()
|
||||
# Override configurations
|
||||
experiment.configs(conf, {
|
||||
'max_digits': 7,
|
||||
|
||||
# No fixed positional embeddings
|
||||
'transformer.src_embed': 'no_pos',
|
||||
'transformer.tgt_embed': 'no_pos',
|
||||
|
||||
# Encoder with RoPER attention
|
||||
'transformer.encoder_attn': 'rotary_value',
|
||||
# Encoder with RoPE attention
|
||||
# 'transformer.encoder_attn': 'rotary',
|
||||
|
||||
#
|
||||
'model': 'rotary_pe_transformer',
|
||||
|
||||
# Use a context size of $256$
|
||||
'seq_len': 512,
|
||||
# Train for 32 epochs
|
||||
'epochs': 20,
|
||||
# Batch size $4$
|
||||
'batch_size': 16,
|
||||
|
||||
# Model size
|
||||
'd_model': 128,
|
||||
'transformer.ffn.d_ff': 512,
|
||||
'transformer.n_heads': 4,
|
||||
'transformer.dropout': 0.0,
|
||||
|
||||
# Use [Adam optimizer](../../optimizers/noam.html)
|
||||
'optimizer.optimizer': 'Adam',
|
||||
'optimizer.learning_rate': 2.5e-4,
|
||||
})
|
||||
|
||||
# Set models for saving and loading
|
||||
experiment.add_pytorch_models({'model': conf.model})
|
||||
|
||||
# Start the experiment
|
||||
with experiment.start():
|
||||
# Run training
|
||||
conf.run()
|
||||
|
||||
|
||||
#
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,96 @@
|
||||
"""
|
||||
---
|
||||
title: Rotary Positional Embeddings (RoPE) Experiment
|
||||
summary: This experiment trains a transformer model with Rotary Positional Embeddings (RoPE) on tiny Shakespeare dataset.
|
||||
---
|
||||
|
||||
# Rotary Positional Embeddings (RoPE) Experiment
|
||||
|
||||
This is an annotated PyTorch experiment to train a transformer model with Rotary Positional Embeddings (RoPE).
|
||||
"""
|
||||
|
||||
from labml import experiment
|
||||
from labml.configs import calculate
|
||||
from labml_nn.transformers import TransformerConfigs
|
||||
from labml_nn.transformers.rope.experiment import Configs as RoPEConfigs
|
||||
|
||||
|
||||
# ### Rotary PE attention
|
||||
|
||||
class Configs(RoPEConfigs): # , ArithmeticAutoregression):
|
||||
pass
|
||||
|
||||
|
||||
def _rotary_value_pe_mha(c: TransformerConfigs):
|
||||
from labml_nn.transformers.rope.value_pe import RotaryValuePEMultiHeadAttention
|
||||
return RotaryValuePEMultiHeadAttention(c.n_heads, c.d_model, 1., 1.)
|
||||
|
||||
|
||||
# Configuration options
|
||||
calculate(TransformerConfigs.encoder_attn, 'rotary_value', _rotary_value_pe_mha)
|
||||
calculate(TransformerConfigs.decoder_attn, 'rotary_value', _rotary_value_pe_mha)
|
||||
calculate(TransformerConfigs.decoder_mem_attn, 'rotary_value', _rotary_value_pe_mha)
|
||||
|
||||
|
||||
def main():
|
||||
# Create experiment
|
||||
experiment.create(name="rotary_shakespeare", comment="rotary value", writers={'screen', 'labml'})
|
||||
# Create configs
|
||||
conf = Configs()
|
||||
# Override configurations
|
||||
experiment.configs(conf, {
|
||||
# No fixed positional embeddings
|
||||
'transformer.src_embed': 'no_pos',
|
||||
'transformer.tgt_embed': 'no_pos',
|
||||
|
||||
# Encoder with RoPE
|
||||
'transformer.encoder_attn': 'rotary_value',
|
||||
# 'transformer.encoder_attn': 'rotary',
|
||||
|
||||
#
|
||||
'model': 'rotary_pe_transformer',
|
||||
|
||||
# Use character level tokenizer
|
||||
'tokenizer': 'character',
|
||||
# Prompt separator is blank
|
||||
'prompt_separator': '',
|
||||
# Starting prompt for sampling
|
||||
'prompt': 'It is ',
|
||||
# Use Tiny Shakespeare dataset
|
||||
'text': 'tiny_shakespeare',
|
||||
|
||||
# Use a context size of $256$
|
||||
'seq_len': 512,
|
||||
# Train for 32 epochs
|
||||
'epochs': 24,
|
||||
# Batch size $4$
|
||||
'batch_size': 16,
|
||||
# Switch between training and validation for $10$ times
|
||||
# per epoch
|
||||
'inner_iterations': 4,
|
||||
|
||||
# Model size
|
||||
'd_model': 128,
|
||||
'transformer.ffn.d_ff': 512,
|
||||
'transformer.n_heads': 4,
|
||||
'transformer.dropout': 0.0,
|
||||
|
||||
# Use [Adam optimizer](../../optimizers/noam.html)
|
||||
'optimizer.optimizer': 'Adam',
|
||||
'optimizer.learning_rate': 2.5e-4,
|
||||
|
||||
'dataloader_shuffle_with_replacement': True
|
||||
})
|
||||
|
||||
# Set models for saving and loading
|
||||
experiment.add_pytorch_models({'model': conf.model})
|
||||
|
||||
# Start the experiment
|
||||
with experiment.start():
|
||||
# Run training
|
||||
conf.run()
|
||||
|
||||
|
||||
#
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,238 @@
|
||||
"""
|
||||
---
|
||||
title: Switch Transformer
|
||||
summary: >
|
||||
This is an annotated implementation/tutorial a miniature version of Switch Transformer in PyTorch.
|
||||
---
|
||||
|
||||
# Switch Transformer
|
||||
|
||||
This is a miniature [PyTorch](https://pytorch.org) implementation of the paper
|
||||
[Switch Transformers: Scaling to Trillion Parameter Models with Simple and Efficient Sparsity](https://arxiv.org/abs/2101.03961).
|
||||
Our implementation only has a few million parameters and doesn't do model parallel distributed training.
|
||||
It does single GPU training, but we implement the concept of switching as described in the paper.
|
||||
|
||||
The Switch Transformer uses different parameters for each token by switching among parameters
|
||||
based on the token.
|
||||
Therefore, only a fraction of parameters are chosen for each token.
|
||||
So you can have more parameters but less computational cost.
|
||||
|
||||
The switching happens at the Position-wise Feedforward network (FFN) of each transformer block.
|
||||
Position-wise feedforward network consists of two sequentially fully connected layers.
|
||||
In switch transformer we have multiple FFNs (multiple experts),
|
||||
and we chose which one to use based on a router.
|
||||
The output is a set of probabilities for picking a FFN,
|
||||
and we pick the one with the highest probability and only evaluate that.
|
||||
So essentially the computational cost is the same as having a single FFN.
|
||||
In our implementation this doesn't parallelize well when you have many or large FFNs since it's all
|
||||
happening on a single GPU.
|
||||
In a distributed setup you would have each FFN (each very large) on a different device.
|
||||
|
||||
The paper introduces another loss term to balance load among the experts (FFNs) and
|
||||
discusses dropping tokens when routing is not balanced.
|
||||
|
||||
Here's [the training code](experiment.html) and a notebook for training a switch transformer on Tiny Shakespeare dataset.
|
||||
|
||||
[](https://colab.research.google.com/github/labmlai/annotated_deep_learning_paper_implementations/blob/master/labml_nn/transformers/switch/experiment.ipynb)
|
||||
"""
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
from labml_nn.transformers.feed_forward import FeedForward
|
||||
from labml_nn.transformers.mha import MultiHeadAttention
|
||||
from labml_nn.utils import clone_module_list
|
||||
|
||||
|
||||
class SwitchFeedForward(nn.Module):
|
||||
"""
|
||||
## Routing among multiple FFNs
|
||||
"""
|
||||
|
||||
def __init__(self, *,
|
||||
capacity_factor: float,
|
||||
drop_tokens: bool,
|
||||
is_scale_prob: bool,
|
||||
n_experts: int,
|
||||
expert: FeedForward,
|
||||
d_model: int):
|
||||
"""
|
||||
* `capacity_factor` is the capacity of each expert as a factor relative to ideally balanced load
|
||||
* `drop_tokens` specifies whether to drop tokens if more tokens are routed to an expert than the capacity
|
||||
* `is_scale_prob` specifies whether to multiply the input to the FFN by the routing probability
|
||||
* `n_experts` is the number of experts
|
||||
* `expert` is the expert layer, a [FFN module](../feed_forward.html)
|
||||
* `d_model` is the number of features in a token embedding
|
||||
* `d_ff` is the number of features in the hidden layer of the FFN
|
||||
* `dropout` is dropout probability in the FFN
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
self.capacity_factor = capacity_factor
|
||||
self.is_scale_prob = is_scale_prob
|
||||
self.n_experts = n_experts
|
||||
self.drop_tokens = drop_tokens
|
||||
|
||||
# make copies of the FFNs
|
||||
self.experts = clone_module_list(expert, n_experts)
|
||||
# Routing layer and softmax
|
||||
self.switch = nn.Linear(d_model, n_experts)
|
||||
self.softmax = nn.Softmax(dim=-1)
|
||||
|
||||
def forward(self, x: torch.Tensor):
|
||||
"""
|
||||
* `x` is the input to the switching module with shape `[seq_len, batch_size, d_model]`
|
||||
"""
|
||||
|
||||
# Capture the shape to change shapes later
|
||||
seq_len, batch_size, d_model = x.shape
|
||||
# Flatten the sequence and batch dimensions
|
||||
x = x.view(-1, d_model)
|
||||
|
||||
# Get routing probabilities for each of the tokens.
|
||||
# $$p_i(x) = \frac{e^{h(x)_i}}{\sum^N_j e^{h(x)_j}}$$
|
||||
# where $N$ is the number of experts `n_experts` and
|
||||
# $h(\cdot)$ is the linear transformation of token embeddings.
|
||||
route_prob = self.softmax(self.switch(x))
|
||||
|
||||
# Get the maximum routing probabilities and the routes.
|
||||
# We route to the expert with highest probability
|
||||
route_prob_max, routes = torch.max(route_prob, dim=-1)
|
||||
|
||||
# Get indexes of tokens going to each expert
|
||||
indexes_list = [torch.eq(routes, i).nonzero(as_tuple=True)[0] for i in range(self.n_experts)]
|
||||
|
||||
# Initialize an empty tensor to store outputs
|
||||
final_output = x.new_zeros(x.shape)
|
||||
|
||||
# Capacity of each expert.
|
||||
# $$\mathrm{expert\;capacity} =
|
||||
# \frac{\mathrm{tokens\;per\;batch}}{\mathrm{number\;of\;experts}}
|
||||
# \times \mathrm{capacity\;factor}$$
|
||||
capacity = int(self.capacity_factor * len(x) / self.n_experts)
|
||||
# Number of tokens routed to each expert.
|
||||
counts = x.new_tensor([len(indexes_list[i]) for i in range(self.n_experts)])
|
||||
|
||||
# Initialize an empty list of dropped tokens
|
||||
dropped = []
|
||||
# Only drop tokens if `drop_tokens` is `True`.
|
||||
if self.drop_tokens:
|
||||
# Drop tokens in each of the experts
|
||||
for i in range(self.n_experts):
|
||||
# Ignore if the expert is not over capacity
|
||||
if len(indexes_list[i]) <= capacity:
|
||||
continue
|
||||
# Shuffle indexes before dropping
|
||||
indexes_list[i] = indexes_list[i][torch.randperm(len(indexes_list[i]))]
|
||||
# Collect the tokens over capacity as dropped tokens
|
||||
dropped.append(indexes_list[i][capacity:])
|
||||
# Keep only the tokens upto the capacity of the expert
|
||||
indexes_list[i] = indexes_list[i][:capacity]
|
||||
|
||||
# Get outputs of the expert FFNs
|
||||
expert_output = [self.experts[i](x[indexes_list[i], :]) for i in range(self.n_experts)]
|
||||
|
||||
# Assign to final output
|
||||
for i in range(self.n_experts):
|
||||
final_output[indexes_list[i], :] = expert_output[i]
|
||||
|
||||
# Pass through the dropped tokens
|
||||
if dropped:
|
||||
dropped = torch.cat(dropped)
|
||||
final_output[dropped, :] = x[dropped, :]
|
||||
|
||||
if self.is_scale_prob:
|
||||
# Multiply by the expert outputs by the probabilities $y = p_i(x) E_i(x)$
|
||||
final_output = final_output * route_prob_max.view(-1, 1)
|
||||
else:
|
||||
# Don't scale the values but multiply by $\frac{p}{\hat{p}} = 1$ so that the gradients flow
|
||||
# (this is something we experimented with).
|
||||
final_output = final_output * (route_prob_max / route_prob_max.detach()).view(-1, 1)
|
||||
|
||||
# Change the shape of the final output back to `[seq_len, batch_size, d_model]`
|
||||
final_output = final_output.view(seq_len, batch_size, d_model)
|
||||
|
||||
# Return
|
||||
#
|
||||
# * the final output
|
||||
# * number of tokens routed to each expert
|
||||
# * sum of probabilities for each expert
|
||||
# * number of tokens dropped.
|
||||
# * routing probabilities of the selected experts
|
||||
#
|
||||
# These are used for the load balancing loss and logging
|
||||
return final_output, counts, route_prob.sum(0), len(dropped), route_prob_max
|
||||
|
||||
|
||||
class SwitchTransformerLayer(nn.Module):
|
||||
"""
|
||||
# Switch Transformer Block
|
||||
|
||||
This is the same as [normal transformer block](../models.html#TransformerLayer)
|
||||
with handling extra outputs of switch feedforward module.
|
||||
"""
|
||||
|
||||
def __init__(self, *,
|
||||
d_model: int,
|
||||
attn: MultiHeadAttention,
|
||||
feed_forward: SwitchFeedForward,
|
||||
dropout_prob: float):
|
||||
"""
|
||||
* `d_model` is the token embedding size
|
||||
* `attn` is the attention module
|
||||
* `feed_forward` is the feed forward module (which is the switching module in this case)
|
||||
* `dropout_prob` is the probability of dropping out after self attention and FFN
|
||||
"""
|
||||
super().__init__()
|
||||
self.size = d_model
|
||||
self.attn = attn
|
||||
self.feed_forward = feed_forward
|
||||
self.dropout = nn.Dropout(dropout_prob)
|
||||
self.norm_self_attn = nn.LayerNorm([d_model])
|
||||
self.norm_ff = nn.LayerNorm([d_model])
|
||||
|
||||
def forward(self, *,
|
||||
x: torch.Tensor,
|
||||
mask: torch.Tensor):
|
||||
# Normalize the vectors before doing self attention
|
||||
z = self.norm_self_attn(x)
|
||||
# Run through self attention, i.e. keys and values are from self
|
||||
self_attn = self.attn(query=z, key=z, value=z, mask=mask)
|
||||
# Add the self attention results
|
||||
x = x + self.dropout(self_attn)
|
||||
|
||||
# Normalize for feed-forward
|
||||
z = self.norm_ff(x)
|
||||
# Pass through the switching feed-forward network
|
||||
ff, counts, route_prob, n_dropped, route_prob_max = self.feed_forward(z)
|
||||
# Add the feed-forward results back
|
||||
x = x + self.dropout(ff)
|
||||
|
||||
return x, counts, route_prob, n_dropped, route_prob_max
|
||||
|
||||
|
||||
class SwitchTransformer(nn.Module):
|
||||
"""
|
||||
## Switch Transformer
|
||||
"""
|
||||
|
||||
def __init__(self, layer: SwitchTransformerLayer, n_layers: int):
|
||||
super().__init__()
|
||||
# Make copies of the transformer layer
|
||||
self.layers = clone_module_list(layer, n_layers)
|
||||
# Final normalization layer
|
||||
self.norm = nn.LayerNorm([layer.size])
|
||||
|
||||
def forward(self, x: torch.Tensor, mask: torch.Tensor):
|
||||
# Run through each transformer layer
|
||||
counts, route_prob, n_dropped, route_prob_max = [], [], [], []
|
||||
for layer in self.layers:
|
||||
x, f, p, n_d, p_max = layer(x=x, mask=mask)
|
||||
counts.append(f)
|
||||
route_prob.append(p)
|
||||
n_dropped.append(n_d)
|
||||
route_prob_max.append(p_max)
|
||||
# Finally, normalize the vectors
|
||||
x = self.norm(x)
|
||||
#
|
||||
return x, torch.stack(counts), torch.stack(route_prob), n_dropped, torch.stack(route_prob_max)
|
||||
@@ -0,0 +1,228 @@
|
||||
{
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 0,
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"name": "Switch Transformer",
|
||||
"provenance": [],
|
||||
"collapsed_sections": []
|
||||
},
|
||||
"kernelspec": {
|
||||
"name": "python3",
|
||||
"display_name": "Python 3"
|
||||
},
|
||||
"accelerator": "GPU"
|
||||
},
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "AYV_dMVDxyc2"
|
||||
},
|
||||
"source": [
|
||||
"[](https://github.com/labmlai/annotated_deep_learning_paper_implementations)\n",
|
||||
"[](https://colab.research.google.com/github/labmlai/annotated_deep_learning_paper_implementations/blob/master/labml_nn/transformers/switch/experiment.ipynb) \n",
|
||||
"\n",
|
||||
"## Switch Transformer\n",
|
||||
"\n",
|
||||
"This is an experiment training Shakespeare dataset with a small Switch Transformer."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "AahG_i2y5tY9"
|
||||
},
|
||||
"source": [
|
||||
"Install the `labml-nn` package"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"id": "ZCzmCrAIVg0L",
|
||||
"colab": {
|
||||
"base_uri": "https://localhost:8080/"
|
||||
},
|
||||
"outputId": "41bb262e-d7e4-4dd9-cf8c-b2a1724889b7"
|
||||
},
|
||||
"source": [
|
||||
"!pip install labml-nn"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "SE2VUQ6L5zxI"
|
||||
},
|
||||
"source": [
|
||||
"Imports"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"id": "0hJXx_g0wS2C"
|
||||
},
|
||||
"source": [
|
||||
"from labml import experiment\n",
|
||||
"from labml_nn.transformers.switch.experiment import Configs"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "Lpggo0wM6qb-"
|
||||
},
|
||||
"source": [
|
||||
"Create an experiment"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"id": "bFcr9k-l4cAg"
|
||||
},
|
||||
"source": [
|
||||
"experiment.create(name=\"switch_transformer\")"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "-OnHLi626tJt"
|
||||
},
|
||||
"source": [
|
||||
"Initialize configurations"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"id": "Piz0c5f44hRo"
|
||||
},
|
||||
"source": [
|
||||
"conf = Configs()"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "wwMzCqpD6vkL"
|
||||
},
|
||||
"source": [
|
||||
"Set experiment configurations and assign a configurations dictionary to override configurations"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"base_uri": "https://localhost:8080/",
|
||||
"height": 17
|
||||
},
|
||||
"id": "e6hmQhTw4nks",
|
||||
"outputId": "0bc4e738-adc7-4003-a030-4080df882bbb"
|
||||
},
|
||||
"source": [
|
||||
"experiment.configs(conf,\n",
|
||||
" # A dictionary of configurations to override\n",
|
||||
" {'tokenizer': 'character',\n",
|
||||
" 'text': 'tiny_shakespeare',\n",
|
||||
" 'optimizer.learning_rate': 1.,\n",
|
||||
" 'optimizer.optimizer': 'Noam',\n",
|
||||
" 'prompt': 'It is',\n",
|
||||
" 'prompt_separator': '',\n",
|
||||
"\n",
|
||||
" 'transformer': 'switch_transformer',\n",
|
||||
" 'is_scale_prob': False,\n",
|
||||
" 'n_experts': 4,\n",
|
||||
"\n",
|
||||
" 'drop_tokens': True,\n",
|
||||
" 'capacity_factor': 1.2,\n",
|
||||
"\n",
|
||||
" 'train_loader': 'shuffled_train_loader',\n",
|
||||
" 'valid_loader': 'shuffled_valid_loader',\n",
|
||||
"\n",
|
||||
" 'seq_len': 64,\n",
|
||||
" 'epochs': 128,\n",
|
||||
" 'batch_size': 32,\n",
|
||||
" 'inner_iterations': 25,\n",
|
||||
" })"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "EvI7MtgJ61w5"
|
||||
},
|
||||
"source": [
|
||||
"Set PyTorch models for loading and saving"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"base_uri": "https://localhost:8080/",
|
||||
"height": 272
|
||||
},
|
||||
"id": "GDlt7dp-5ALt",
|
||||
"outputId": "93e0f3b1-d0fe-4525-d9f6-9ffab9ea7f9b"
|
||||
},
|
||||
"source": [
|
||||
"experiment.add_pytorch_models({'model': conf.model})"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "KJZRf8527GxL"
|
||||
},
|
||||
"source": [
|
||||
"Start the experiment and run the training loop."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"base_uri": "https://localhost:8080/",
|
||||
"height": 1000
|
||||
},
|
||||
"id": "aIAWo7Fw5DR8",
|
||||
"outputId": "12a92c2e-d248-436b-a6f1-7cf92b5289e9"
|
||||
},
|
||||
"source": [
|
||||
"# Start the experiment\n",
|
||||
"with experiment.start():\n",
|
||||
" conf.run()"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"id": "oBXXlP2b7XZO"
|
||||
},
|
||||
"source": [
|
||||
""
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
"""
|
||||
---
|
||||
title: Switch Transformer Experiment
|
||||
summary: This experiment trains a small switch transformer on tiny Shakespeare dataset.
|
||||
---
|
||||
|
||||
# Switch Transformer Experiment
|
||||
|
||||
This is an annotated PyTorch experiment to train a switch transformer.
|
||||
|
||||
[](https://colab.research.google.com/github/labmlai/annotated_deep_learning_paper_implementations/blob/master/labml_nn/transformers/switch/experiment.ipynb)
|
||||
"""
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
from labml import experiment, tracker
|
||||
from labml.configs import option
|
||||
from labml_nn.helpers.trainer import BatchIndex
|
||||
from labml_nn.experiments.nlp_autoregression import NLPAutoRegressionConfigs
|
||||
|
||||
|
||||
class AutoregressiveModel(nn.Module):
|
||||
"""
|
||||
## Auto regressive model
|
||||
"""
|
||||
|
||||
def __init__(self, n_vocab: int, d_model: int, transformer: nn.Module):
|
||||
super().__init__()
|
||||
# Token embedding module
|
||||
self.src_embed = nn.Embedding(n_vocab, d_model)
|
||||
# Transformer
|
||||
self.transformer = transformer
|
||||
# Final layer
|
||||
self.generator = nn.Linear(d_model, n_vocab)
|
||||
self.mask = None
|
||||
|
||||
def forward(self, x: torch.Tensor):
|
||||
# Initialize the subsequent mask
|
||||
if self.mask is None or self.mask.size(0) != len(x):
|
||||
from labml_nn.transformers.utils import subsequent_mask
|
||||
self.mask = subsequent_mask(len(x)).to(x.device)
|
||||
# Token embeddings
|
||||
x = self.src_embed(x)
|
||||
# Run it through the transformer
|
||||
res, counts, route_prob, n_dropped, route_prob_max = self.transformer(x, self.mask)
|
||||
# Generate logits of the next token
|
||||
res = self.generator(res)
|
||||
#
|
||||
return res, counts, route_prob, n_dropped, route_prob_max
|
||||
|
||||
|
||||
class Configs(NLPAutoRegressionConfigs):
|
||||
"""
|
||||
## Configurations
|
||||
|
||||
This extends [`NLPAutoRegressionConfigs`](../../experiments/nlp_autoregression.html).
|
||||
|
||||
The default configs can and will be over-ridden when we start the experiment
|
||||
"""
|
||||
|
||||
model: AutoregressiveModel
|
||||
transformer: nn.Module
|
||||
|
||||
# Token embedding size
|
||||
d_model: int = 128
|
||||
# Number of attention heads
|
||||
heads: int = 4
|
||||
# Dropout probability
|
||||
dropout: float = 0.0
|
||||
# Number of features in FFN hidden layer
|
||||
d_ff: int = 256
|
||||
# Number of transformer layers
|
||||
n_layers: int = 6
|
||||
# Number of experts
|
||||
n_experts: int = 4
|
||||
# Load balancing coefficient
|
||||
load_balancing_loss_ceof = 0.01
|
||||
# Whether to scale the chosen expert outputs by the routing probability
|
||||
is_scale_prob: bool = True
|
||||
# Whether to drop tokens
|
||||
drop_tokens: bool = False
|
||||
# Capacity factor to determine capacity of each model
|
||||
capacity_factor: float = 1.0
|
||||
|
||||
def init(self):
|
||||
super().init()
|
||||
# Initialize tracking indicators
|
||||
tracker.set_scalar("lb_loss.*", False)
|
||||
tracker.set_scalar("route.*", False)
|
||||
tracker.set_scalar("dropped.*", False)
|
||||
|
||||
def step(self, batch: any, batch_idx: BatchIndex):
|
||||
"""
|
||||
### Training or validation step
|
||||
"""
|
||||
|
||||
# Move data to the device
|
||||
data, target = batch[0].to(self.device), batch[1].to(self.device)
|
||||
|
||||
# Update global step (number of tokens processed) when in training mode
|
||||
if self.mode.is_train:
|
||||
tracker.add_global_step(data.shape[0] * data.shape[1])
|
||||
|
||||
# Get model outputs.
|
||||
output, counts, route_prob, n_dropped, route_prob_max = self.model(data)
|
||||
|
||||
# Calculate and cross entropy loss
|
||||
cross_entropy_loss = self.loss_func(output, target)
|
||||
# Total number of tokens processed, $T$, in the current batch $\mathscr{B}$
|
||||
total = counts.sum(dim=-1, keepdims=True)
|
||||
# Fraction of tokens routed to each expert
|
||||
# $$f_i = \frac{1}{T} \sum_{x \in \mathscr{B}} \mathbf{1} \{ \mathop{argmax} p(x), i \}$$
|
||||
# $f_i$ is the count of tokens where the argmax of $p(x)$ is equal to $i$.
|
||||
route_frac = counts / total
|
||||
# Mean routing probability
|
||||
# $$P_i = \frac{1}{T} \sum_{x \in \mathscr{B}} p_i (x)$$
|
||||
route_prob = route_prob / total
|
||||
# Load balancing loss
|
||||
# $$\mathscr{L} = N \sum_{i=1}^N f_i \cdot P_i$$
|
||||
# $\mathscr{L}$ is the loss for a single layer and here we are
|
||||
# taking the sum of losses across all layers.
|
||||
load_balancing_loss = self.n_experts * (route_frac * route_prob).sum()
|
||||
|
||||
# Track stats
|
||||
tracker.add('dropped.', total.new_tensor(n_dropped) / total)
|
||||
tracker.add('route.min.', route_frac.min())
|
||||
tracker.add('route.max.', route_frac.max())
|
||||
tracker.add('route.std.', route_frac.std())
|
||||
tracker.add('route.max_prob.', route_prob_max)
|
||||
tracker.add("loss.", cross_entropy_loss)
|
||||
tracker.add("lb_loss.", load_balancing_loss)
|
||||
|
||||
# Combined loss.
|
||||
# The load balancing loss is multiplied by a coefficient $\alpha$ which is
|
||||
# set to something small like $\alpha = 0.01$.
|
||||
loss = cross_entropy_loss + self.load_balancing_loss_ceof * load_balancing_loss
|
||||
|
||||
# Calculate and log accuracy
|
||||
self.accuracy(output, target)
|
||||
self.accuracy.track()
|
||||
|
||||
# Train the model
|
||||
if self.mode.is_train:
|
||||
# Calculate gradients
|
||||
loss.backward()
|
||||
# Clip gradients
|
||||
torch.nn.utils.clip_grad_norm_(self.model.parameters(), max_norm=self.grad_norm_clip)
|
||||
# Take optimizer step
|
||||
self.optimizer.step()
|
||||
# Log the model parameters and gradients on last batch of every epoch
|
||||
if batch_idx.is_last:
|
||||
tracker.add('model', self.model)
|
||||
# Clear the gradients
|
||||
self.optimizer.zero_grad()
|
||||
|
||||
# Save the tracked metrics
|
||||
tracker.save()
|
||||
|
||||
|
||||
@option(Configs.model)
|
||||
def autoregressive_model(c: Configs):
|
||||
"""
|
||||
### Initialize the auto-regressive model
|
||||
"""
|
||||
m = AutoregressiveModel(c.n_tokens, c.d_model, c.transformer)
|
||||
return m.to(c.device)
|
||||
|
||||
|
||||
@option(Configs.transformer)
|
||||
def switch_transformer(c: Configs):
|
||||
"""
|
||||
### Initialize the switch transformer
|
||||
"""
|
||||
from labml_nn.transformers.switch import SwitchTransformer, SwitchTransformerLayer, SwitchFeedForward
|
||||
from labml_nn.transformers import MultiHeadAttention
|
||||
from labml_nn.transformers.feed_forward import FeedForward
|
||||
|
||||
return SwitchTransformer(
|
||||
SwitchTransformerLayer(d_model=c.d_model,
|
||||
attn=MultiHeadAttention(c.heads, c.d_model, c.dropout),
|
||||
feed_forward=SwitchFeedForward(capacity_factor=c.capacity_factor,
|
||||
drop_tokens=c.drop_tokens,
|
||||
is_scale_prob=c.is_scale_prob,
|
||||
n_experts=c.n_experts,
|
||||
expert=FeedForward(c.d_model, c.d_ff, c.dropout),
|
||||
d_model=c.d_model),
|
||||
dropout_prob=c.dropout),
|
||||
c.n_layers)
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
### Run the experiment
|
||||
"""
|
||||
# Create experiment
|
||||
experiment.create(name="switch_transformer", comment='')
|
||||
# Create configs
|
||||
conf = Configs()
|
||||
# Load configurations
|
||||
experiment.configs(conf,
|
||||
# A dictionary of configurations to override
|
||||
{'tokenizer': 'character',
|
||||
'text': 'tiny_shakespeare',
|
||||
'optimizer.learning_rate': 1.,
|
||||
'optimizer.optimizer': 'Noam',
|
||||
'prompt': 'It is',
|
||||
'prompt_separator': '',
|
||||
|
||||
'transformer': 'switch_transformer',
|
||||
'n_experts': 4,
|
||||
|
||||
'drop_tokens': True,
|
||||
'capacity_factor': 1.2,
|
||||
|
||||
'train_loader': 'shuffled_train_loader',
|
||||
'valid_loader': 'shuffled_valid_loader',
|
||||
|
||||
'seq_len': 64,
|
||||
'epochs': 128,
|
||||
'batch_size': 32,
|
||||
'inner_iterations': 25,
|
||||
})
|
||||
|
||||
# Set models for saving and loading
|
||||
experiment.add_pytorch_models({'model': conf.model})
|
||||
|
||||
# Start the experiment
|
||||
with experiment.start():
|
||||
# `TrainValidConfigs.run`
|
||||
conf.run()
|
||||
|
||||
|
||||
#
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,27 @@
|
||||
# [Switch Transformer](https://nn.labml.ai/transformers/switch/index.html)
|
||||
|
||||
This is a miniature [PyTorch](https://pytorch.org) implementation of the paper
|
||||
[Switch Transformers: Scaling to Trillion Parameter Models with Simple and Efficient Sparsity](https://arxiv.org/abs/2101.03961).
|
||||
Our implementation only has a few million parameters and doesn't do model parallel distributed training.
|
||||
It does single GPU training, but we implement the concept of switching as described in the paper.
|
||||
|
||||
The Switch Transformer uses different parameters for each token by switching among parameters
|
||||
based on the token.
|
||||
Therefore, only a fraction of parameters are chosen for each token.
|
||||
So you can have more parameters but less computational cost.
|
||||
|
||||
The switching happens at the Position-wise Feedforward network (FFN) of each transformer block.
|
||||
Position-wise feedforward network consists of two sequentially fully connected layers.
|
||||
In switch transformer we have multiple FFNs (multiple experts),
|
||||
and we chose which one to use based on a router.
|
||||
The output is a set of probabilities for picking a FFN,
|
||||
and we pick the one with the highest probability and only evaluate that.
|
||||
So essentially the computational cost is the same as having a single FFN.
|
||||
In our implementation this doesn't parallelize well when you have many or large FFNs since it's all
|
||||
happening on a single GPU.
|
||||
In a distributed setup you would have each FFN (each very large) on a different device.
|
||||
|
||||
The paper introduces another loss term to balance load among the experts (FFNs) and
|
||||
discusses dropping tokens when routing is not balanced.
|
||||
|
||||
Here's [the training code](experiment.html) and a notebook for training a switch transformer on Tiny Shakespeare dataset.
|
||||
@@ -0,0 +1,27 @@
|
||||
"""
|
||||
---
|
||||
title: Utilities for Transformer
|
||||
summary: A bunch of utility functions and classes for transformers.
|
||||
---
|
||||
|
||||
# Utilities for Transformer
|
||||
"""
|
||||
|
||||
import torch
|
||||
|
||||
|
||||
def subsequent_mask(seq_len):
|
||||
"""
|
||||
## Subsequent mask to mask out data from future (subsequent) time steps
|
||||
"""
|
||||
mask = torch.tril(torch.ones(seq_len, seq_len)).to(torch.bool).unsqueeze(-1)
|
||||
return mask
|
||||
|
||||
|
||||
def _subsequent_mask():
|
||||
from labml.logger import inspect
|
||||
inspect(subsequent_mask(10)[:, :, 0])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
_subsequent_mask()
|
||||
@@ -0,0 +1,213 @@
|
||||
"""
|
||||
---
|
||||
title: Vision Transformer (ViT)
|
||||
summary: >
|
||||
A PyTorch implementation/tutorial of the paper
|
||||
"An Image Is Worth 16x16 Words: Transformers For Image Recognition At Scale"
|
||||
---
|
||||
|
||||
# Vision Transformer (ViT)
|
||||
|
||||
This is a [PyTorch](https://pytorch.org) implementation of the paper
|
||||
[An Image Is Worth 16x16 Words: Transformers For Image Recognition At Scale](https://arxiv.org/abs/2010.11929).
|
||||
|
||||
Vision transformer applies a pure transformer to images
|
||||
without any convolution layers.
|
||||
They split the image into patches and apply a transformer on patch embeddings.
|
||||
[Patch embeddings](#PathEmbeddings) are generated by applying a simple linear transformation
|
||||
to the flattened pixel values of the patch.
|
||||
Then a standard transformer encoder is fed with the patch embeddings, along with a
|
||||
classification token `[CLS]`.
|
||||
The encoding on the `[CLS]` token is used to classify the image with an MLP.
|
||||
|
||||
When feeding the transformer with the patches, learned positional embeddings are
|
||||
added to the patch embeddings, because the patch embeddings do not have any information
|
||||
about where that patch is from.
|
||||
The positional embeddings are a set of vectors for each patch location that get trained
|
||||
with gradient descent along with other parameters.
|
||||
|
||||
ViTs perform well when they are pre-trained on large datasets.
|
||||
The paper suggests pre-training them with an MLP classification head and
|
||||
then using a single linear layer when fine-tuning.
|
||||
The paper beats SOTA with a ViT pre-trained on a 300 million image dataset.
|
||||
They also use higher resolution images during inference while keeping the
|
||||
patch size the same.
|
||||
The positional embeddings for new patch locations are calculated by interpolating
|
||||
learning positional embeddings.
|
||||
|
||||
Here's [an experiment](experiment.html) that trains ViT on CIFAR-10.
|
||||
This doesn't do very well because it's trained on a small dataset.
|
||||
It's a simple experiment that anyone can run and play with ViTs.
|
||||
"""
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
from labml_nn.transformers import TransformerLayer
|
||||
from labml_nn.utils import clone_module_list
|
||||
|
||||
|
||||
class PatchEmbeddings(nn.Module):
|
||||
"""
|
||||
<a id="PatchEmbeddings"></a>
|
||||
|
||||
## Get patch embeddings
|
||||
|
||||
The paper splits the image into patches of equal size and do a linear transformation
|
||||
on the flattened pixels for each patch.
|
||||
|
||||
We implement the same thing through a convolution layer, because it's simpler to implement.
|
||||
"""
|
||||
|
||||
def __init__(self, d_model: int, patch_size: int, in_channels: int):
|
||||
"""
|
||||
* `d_model` is the transformer embeddings size
|
||||
* `patch_size` is the size of the patch
|
||||
* `in_channels` is the number of channels in the input image (3 for rgb)
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
# We create a convolution layer with a kernel size and and stride length equal to patch size.
|
||||
# This is equivalent to splitting the image into patches and doing a linear
|
||||
# transformation on each patch.
|
||||
self.conv = nn.Conv2d(in_channels, d_model, patch_size, stride=patch_size)
|
||||
|
||||
def forward(self, x: torch.Tensor):
|
||||
"""
|
||||
* `x` is the input image of shape `[batch_size, channels, height, width]`
|
||||
"""
|
||||
# Apply convolution layer
|
||||
x = self.conv(x)
|
||||
# Get the shape.
|
||||
bs, c, h, w = x.shape
|
||||
# Rearrange to shape `[patches, batch_size, d_model]`
|
||||
x = x.permute(2, 3, 0, 1)
|
||||
x = x.view(h * w, bs, c)
|
||||
|
||||
# Return the patch embeddings
|
||||
return x
|
||||
|
||||
|
||||
class LearnedPositionalEmbeddings(nn.Module):
|
||||
"""
|
||||
<a id="LearnedPositionalEmbeddings"></a>
|
||||
|
||||
## Add parameterized positional encodings
|
||||
|
||||
This adds learned positional embeddings to patch embeddings.
|
||||
"""
|
||||
|
||||
def __init__(self, d_model: int, max_len: int = 5_000):
|
||||
"""
|
||||
* `d_model` is the transformer embeddings size
|
||||
* `max_len` is the maximum number of patches
|
||||
"""
|
||||
super().__init__()
|
||||
# Positional embeddings for each location
|
||||
self.positional_encodings = nn.Parameter(torch.zeros(max_len, 1, d_model), requires_grad=True)
|
||||
|
||||
def forward(self, x: torch.Tensor):
|
||||
"""
|
||||
* `x` is the patch embeddings of shape `[patches, batch_size, d_model]`
|
||||
"""
|
||||
# Get the positional embeddings for the given patches
|
||||
pe = self.positional_encodings[:x.shape[0]]
|
||||
# Add to patch embeddings and return
|
||||
return x + pe
|
||||
|
||||
|
||||
class ClassificationHead(nn.Module):
|
||||
"""
|
||||
<a id="ClassificationHead"></a>
|
||||
|
||||
## MLP Classification Head
|
||||
|
||||
This is the two layer MLP head to classify the image based on `[CLS]` token embedding.
|
||||
"""
|
||||
def __init__(self, d_model: int, n_hidden: int, n_classes: int):
|
||||
"""
|
||||
* `d_model` is the transformer embedding size
|
||||
* `n_hidden` is the size of the hidden layer
|
||||
* `n_classes` is the number of classes in the classification task
|
||||
"""
|
||||
super().__init__()
|
||||
# First layer
|
||||
self.linear1 = nn.Linear(d_model, n_hidden)
|
||||
# Activation
|
||||
self.act = nn.ReLU()
|
||||
# Second layer
|
||||
self.linear2 = nn.Linear(n_hidden, n_classes)
|
||||
|
||||
def forward(self, x: torch.Tensor):
|
||||
"""
|
||||
* `x` is the transformer encoding for `[CLS]` token
|
||||
"""
|
||||
# First layer and activation
|
||||
x = self.act(self.linear1(x))
|
||||
# Second layer
|
||||
x = self.linear2(x)
|
||||
|
||||
#
|
||||
return x
|
||||
|
||||
|
||||
class VisionTransformer(nn.Module):
|
||||
"""
|
||||
## Vision Transformer
|
||||
|
||||
This combines the [patch embeddings](#PatchEmbeddings),
|
||||
[positional embeddings](#LearnedPositionalEmbeddings),
|
||||
transformer and the [classification head](#ClassificationHead).
|
||||
"""
|
||||
def __init__(self, transformer_layer: TransformerLayer, n_layers: int,
|
||||
patch_emb: PatchEmbeddings, pos_emb: LearnedPositionalEmbeddings,
|
||||
classification: ClassificationHead):
|
||||
"""
|
||||
* `transformer_layer` is a copy of a single [transformer layer](../models.html#TransformerLayer).
|
||||
We make copies of it to make the transformer with `n_layers`.
|
||||
* `n_layers` is the number of [transformer layers](../models.html#TransformerLayer).
|
||||
* `patch_emb` is the [patch embeddings layer](#PatchEmbeddings).
|
||||
* `pos_emb` is the [positional embeddings layer](#LearnedPositionalEmbeddings).
|
||||
* `classification` is the [classification head](#ClassificationHead).
|
||||
"""
|
||||
super().__init__()
|
||||
# Patch embeddings
|
||||
self.patch_emb = patch_emb
|
||||
self.pos_emb = pos_emb
|
||||
# Classification head
|
||||
self.classification = classification
|
||||
# Make copies of the transformer layer
|
||||
self.transformer_layers = clone_module_list(transformer_layer, n_layers)
|
||||
|
||||
# `[CLS]` token embedding
|
||||
self.cls_token_emb = nn.Parameter(torch.randn(1, 1, transformer_layer.size), requires_grad=True)
|
||||
# Final normalization layer
|
||||
self.ln = nn.LayerNorm([transformer_layer.size])
|
||||
|
||||
def forward(self, x: torch.Tensor):
|
||||
"""
|
||||
* `x` is the input image of shape `[batch_size, channels, height, width]`
|
||||
"""
|
||||
# Get patch embeddings. This gives a tensor of shape `[patches, batch_size, d_model]`
|
||||
x = self.patch_emb(x)
|
||||
# Concatenate the `[CLS]` token embeddings before feeding the transformer
|
||||
cls_token_emb = self.cls_token_emb.expand(-1, x.shape[1], -1)
|
||||
x = torch.cat([cls_token_emb, x])
|
||||
# Add positional embeddings
|
||||
x = self.pos_emb(x)
|
||||
|
||||
# Pass through transformer layers with no attention masking
|
||||
for layer in self.transformer_layers:
|
||||
x = layer(x=x, mask=None)
|
||||
|
||||
# Get the transformer output of the `[CLS]` token (which is the first in the sequence).
|
||||
x = x[0]
|
||||
|
||||
# Layer normalization
|
||||
x = self.ln(x)
|
||||
|
||||
# Classification head, to get logits
|
||||
x = self.classification(x)
|
||||
|
||||
#
|
||||
return x
|
||||
@@ -0,0 +1,94 @@
|
||||
"""
|
||||
---
|
||||
title: Train a Vision Transformer (ViT) on CIFAR 10
|
||||
summary: >
|
||||
Train a Vision Transformer (ViT) on CIFAR 10
|
||||
---
|
||||
|
||||
# Train a [Vision Transformer (ViT)](index.html) on CIFAR 10
|
||||
"""
|
||||
|
||||
from labml import experiment
|
||||
from labml.configs import option
|
||||
from labml_nn.experiments.cifar10 import CIFAR10Configs
|
||||
from labml_nn.transformers import TransformerConfigs
|
||||
|
||||
|
||||
class Configs(CIFAR10Configs):
|
||||
"""
|
||||
## Configurations
|
||||
|
||||
We use [`CIFAR10Configs`](../../experiments/cifar10.html) which defines all the
|
||||
dataset related configurations, optimizer, and a training loop.
|
||||
"""
|
||||
|
||||
# [Transformer configurations](../configs.html#TransformerConfigs)
|
||||
# to get [transformer layer](../models.html#TransformerLayer)
|
||||
transformer: TransformerConfigs
|
||||
|
||||
# Size of a patch
|
||||
patch_size: int = 4
|
||||
# Size of the hidden layer in classification head
|
||||
n_hidden_classification: int = 2048
|
||||
# Number of classes in the task
|
||||
n_classes: int = 10
|
||||
|
||||
|
||||
@option(Configs.transformer)
|
||||
def _transformer():
|
||||
"""
|
||||
Create transformer configs
|
||||
"""
|
||||
return TransformerConfigs()
|
||||
|
||||
|
||||
@option(Configs.model)
|
||||
def _vit(c: Configs):
|
||||
"""
|
||||
### Create model
|
||||
"""
|
||||
from labml_nn.transformers.vit import VisionTransformer, LearnedPositionalEmbeddings, ClassificationHead, \
|
||||
PatchEmbeddings
|
||||
|
||||
# Transformer size from [Transformer configurations](../configs.html#TransformerConfigs)
|
||||
d_model = c.transformer.d_model
|
||||
# Create a vision transformer
|
||||
return VisionTransformer(c.transformer.encoder_layer, c.transformer.n_layers,
|
||||
PatchEmbeddings(d_model, c.patch_size, 3),
|
||||
LearnedPositionalEmbeddings(d_model),
|
||||
ClassificationHead(d_model, c.n_hidden_classification, c.n_classes)).to(c.device)
|
||||
|
||||
|
||||
def main():
|
||||
# Create experiment
|
||||
experiment.create(name='ViT', comment='cifar10')
|
||||
# Create configurations
|
||||
conf = Configs()
|
||||
# Load configurations
|
||||
experiment.configs(conf, {
|
||||
# Optimizer
|
||||
'optimizer.optimizer': 'Adam',
|
||||
'optimizer.learning_rate': 2.5e-4,
|
||||
|
||||
# Transformer embedding size
|
||||
'transformer.d_model': 512,
|
||||
|
||||
# Training epochs and batch size
|
||||
'epochs': 32,
|
||||
'train_batch_size': 64,
|
||||
|
||||
# Augment CIFAR 10 images for training
|
||||
'train_dataset': 'cifar10_train_augmented',
|
||||
# Do not augment CIFAR 10 images for validation
|
||||
'valid_dataset': 'cifar10_valid_no_augment',
|
||||
})
|
||||
# Set model for saving/loading
|
||||
experiment.add_pytorch_models({'model': conf.model})
|
||||
# Start the experiment and run the training loop
|
||||
with experiment.start():
|
||||
conf.run()
|
||||
|
||||
|
||||
#
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,32 @@
|
||||
# [Vision Transformer (ViT)](https://nn.labml.ai/transformer/vit/index.html)
|
||||
|
||||
This is a [PyTorch](https://pytorch.org) implementation of the paper
|
||||
[An Image Is Worth 16x16 Words: Transformers For Image Recognition At Scale](https://arxiv.org/abs/2010.11929).
|
||||
|
||||
Vision transformer applies a pure transformer to images
|
||||
without any convolution layers.
|
||||
They split the image into patches and apply a transformer on patch embeddings.
|
||||
[Patch embeddings](https://nn.labml.ai/transformer/vit/index.html#PathEmbeddings) are generated by applying a simple linear transformation
|
||||
to the flattened pixel values of the patch.
|
||||
Then a standard transformer encoder is fed with the patch embeddings, along with a
|
||||
classification token `[CLS]`.
|
||||
The encoding on the `[CLS]` token is used to classify the image with an MLP.
|
||||
|
||||
When feeding the transformer with the patches, learned positional embeddings are
|
||||
added to the patch embeddings, because the patch embeddings do not have any information
|
||||
about where that patch is from.
|
||||
The positional embeddings are a set of vectors for each patch location that get trained
|
||||
with gradient descent along with other parameters.
|
||||
|
||||
ViTs perform well when they are pre-trained on large datasets.
|
||||
The paper suggests pre-training them with an MLP classification head and
|
||||
then using a single linear layer when fine-tuning.
|
||||
The paper beats SOTA with a ViT pre-trained on a 300 million image dataset.
|
||||
They also use higher resolution images during inference while keeping the
|
||||
patch size the same.
|
||||
The positional embeddings for new patch locations are calculated by interpolating
|
||||
learning positional embeddings.
|
||||
|
||||
Here's [an experiment](https://nn.labml.ai/transformer/vit/experiment.html) that trains ViT on CIFAR-10.
|
||||
This doesn't do very well because it's trained on a small dataset.
|
||||
It's a simple experiment that anyone can run and play with ViTs.
|
||||
@@ -0,0 +1,140 @@
|
||||
"""
|
||||
---
|
||||
title: Transformer XL
|
||||
summary: >
|
||||
Documented implementation with explanations of a
|
||||
Transformer-XL model.
|
||||
---
|
||||
|
||||
# Transformer XL
|
||||
|
||||
This is an implementation of
|
||||
[Transformer-XL: Attentive Language Models Beyond a Fixed-Length Context](https://arxiv.org/abs/1901.02860)
|
||||
in [PyTorch](https://pytorch.org).
|
||||
|
||||
Transformer has a limited attention span,
|
||||
equal to the length of the sequence trained in parallel.
|
||||
All these positions have a fixed positional encoding.
|
||||
Transformer XL increases this attention span by letting
|
||||
each of the positions pay attention to precalculated past embeddings.
|
||||
For instance if the context length is $l$, it will keep the embeddings of
|
||||
all layers for previous batch of length $l$ and feed them to current step.
|
||||
If we use fixed-positional encodings these pre-calculated embeddings will have
|
||||
the same positions as the current context.
|
||||
They introduce relative positional encoding, where the positional encodings
|
||||
are introduced at the attention calculation.
|
||||
|
||||
Annotated implementation of relative multi-headed attention is in [`relative_mha.py`](relative_mha.html).
|
||||
|
||||
Here's [the training code](experiment.html) and a notebook for training a transformer XL model on Tiny Shakespeare dataset.
|
||||
|
||||
[](https://colab.research.google.com/github/labmlai/annotated_deep_learning_paper_implementations/blob/master/labml_nn/transformers/xl/experiment.ipynb)
|
||||
"""
|
||||
|
||||
|
||||
from typing import List, Optional
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
from labml_nn.utils import clone_module_list
|
||||
from .relative_mha import RelativeMultiHeadAttention
|
||||
from ..feed_forward import FeedForward
|
||||
|
||||
|
||||
class TransformerXLLayer(nn.Module):
|
||||
"""
|
||||
## Transformer XL Layer
|
||||
|
||||
The transformer XL model comprises of a number of these layers.
|
||||
"""
|
||||
def __init__(self, *,
|
||||
d_model: int,
|
||||
self_attn: RelativeMultiHeadAttention,
|
||||
feed_forward: FeedForward,
|
||||
dropout_prob: float):
|
||||
"""
|
||||
* `d_model` is the token embedding size
|
||||
* `self_attn` is the [self attention module](relative_mha.html)
|
||||
* `feed_forward` is the feed forward module
|
||||
* `dropout_prob` is the probability of dropping out after self attention and FFN
|
||||
"""
|
||||
super().__init__()
|
||||
self.size = d_model
|
||||
self.self_attn = self_attn
|
||||
self.feed_forward = feed_forward
|
||||
self.dropout = nn.Dropout(dropout_prob)
|
||||
self.norm_self_attn = nn.LayerNorm([d_model])
|
||||
self.norm_ff = nn.LayerNorm([d_model])
|
||||
|
||||
def forward(self, *,
|
||||
x: torch.Tensor,
|
||||
mem: Optional[torch.Tensor],
|
||||
mask: torch.Tensor):
|
||||
"""
|
||||
* `x` is a tensor of the token level feature vectors of shape `[seq_len, batch_size, d_model]`
|
||||
* `mem` is a tensor of the past token level feature vectors of shape `[mem_len, batch_size, d_model]`
|
||||
* `mask` is a matrix of shape `[seq_len, mem_len + seq_len, batch_size]` or `[seq_len, mem_len + seq_len, 1]`.
|
||||
`mask[i, j]` is true if token at `i` can see token at `j`.
|
||||
"""
|
||||
# Normalize the vectors before doing self attention
|
||||
z = self.norm_self_attn(x)
|
||||
# If there is memory
|
||||
if mem is not None:
|
||||
# Normalize it
|
||||
mem = self.norm_self_attn(mem)
|
||||
# Concatenate with `z`
|
||||
m_z = torch.cat((mem, z), dim=0)
|
||||
# Ignore if there is no memory
|
||||
else:
|
||||
m_z = z
|
||||
# Attention
|
||||
self_attn = self.self_attn(query=z, key=m_z, value=m_z, mask=mask)
|
||||
# Add the attention results
|
||||
x = x + self.dropout(self_attn)
|
||||
|
||||
# Normalize for feed-forward
|
||||
z = self.norm_ff(x)
|
||||
# Pass through the feed-forward network
|
||||
ff = self.feed_forward(z)
|
||||
# Add the feed-forward results back
|
||||
x = x + self.dropout(ff)
|
||||
|
||||
#
|
||||
return x
|
||||
|
||||
|
||||
class TransformerXL(nn.Module):
|
||||
"""
|
||||
## Transformer XL Model
|
||||
|
||||
This consists of multiple transformer XL layers
|
||||
"""
|
||||
|
||||
def __init__(self, layer: TransformerXLLayer, n_layers: int):
|
||||
super().__init__()
|
||||
# Make copies of the transformer layer
|
||||
self.layers = clone_module_list(layer, n_layers)
|
||||
# Final normalization layer
|
||||
self.norm = nn.LayerNorm([layer.size])
|
||||
|
||||
def forward(self, x: torch.Tensor, mem: List[torch.Tensor], mask: torch.Tensor):
|
||||
"""
|
||||
* `x` is a tensor of the token embeddings vectors of shape `[seq_len, batch_size, d_model]`
|
||||
* `mem` is a list of tensors of the past token level feature vectors of shape
|
||||
`[mem_len, batch_size, d_model]` for each layer
|
||||
* `mask` is the masking matrix
|
||||
"""
|
||||
# List to store token level feature vectors,
|
||||
# which will become the memories for the next sequential batch.
|
||||
new_mem = []
|
||||
# Run through each transformer layer
|
||||
for i, layer in enumerate(self.layers):
|
||||
# Add to the list of feature vectors
|
||||
new_mem.append(x.detach())
|
||||
# Memory
|
||||
m = mem[i] if mem else None
|
||||
# Run through the transformer XL layer
|
||||
x = layer(x=x, mem=m, mask=mask)
|
||||
# Finally, normalize the vectors
|
||||
return self.norm(x), new_mem
|
||||
@@ -0,0 +1,222 @@
|
||||
{
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 0,
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"name": "Transformer XL",
|
||||
"provenance": [],
|
||||
"collapsed_sections": []
|
||||
},
|
||||
"kernelspec": {
|
||||
"name": "python3",
|
||||
"display_name": "Python 3"
|
||||
},
|
||||
"accelerator": "GPU"
|
||||
},
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "AYV_dMVDxyc2"
|
||||
},
|
||||
"source": [
|
||||
"[](https://github.com/labmlai/annotated_deep_learning_paper_implementations)\n",
|
||||
"[](https://colab.research.google.com/github/labmlai/annotated_deep_learning_paper_implementations/blob/master/labml_nn/transformers/xl/experiment.ipynb) \n",
|
||||
"\n",
|
||||
"## Transformer XL\n",
|
||||
"\n",
|
||||
"This is an experiment training Shakespeare dataset with a Transformer XL model."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "AahG_i2y5tY9"
|
||||
},
|
||||
"source": [
|
||||
"Install the `labml-nn` package"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"id": "ZCzmCrAIVg0L",
|
||||
"colab": {
|
||||
"base_uri": "https://localhost:8080/"
|
||||
},
|
||||
"outputId": "1a4a59ce-b300-4d9f-baee-15720d696773"
|
||||
},
|
||||
"source": [
|
||||
"!pip install labml-nn"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "SE2VUQ6L5zxI"
|
||||
},
|
||||
"source": [
|
||||
"Imports"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"id": "0hJXx_g0wS2C"
|
||||
},
|
||||
"source": [
|
||||
"from labml import experiment\n",
|
||||
"from labml_nn.transformers.xl.experiment import Configs"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "Lpggo0wM6qb-"
|
||||
},
|
||||
"source": [
|
||||
"Create an experiment"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"id": "bFcr9k-l4cAg"
|
||||
},
|
||||
"source": [
|
||||
"experiment.create(name=\"transformer_xl\")"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "-OnHLi626tJt"
|
||||
},
|
||||
"source": [
|
||||
"Initialize configurations"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"id": "Piz0c5f44hRo"
|
||||
},
|
||||
"source": [
|
||||
"conf = Configs()"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "wwMzCqpD6vkL"
|
||||
},
|
||||
"source": [
|
||||
"Set experiment configurations and assign a configurations dictionary to override configurations"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"base_uri": "https://localhost:8080/",
|
||||
"height": 17
|
||||
},
|
||||
"id": "e6hmQhTw4nks",
|
||||
"outputId": "839820be-f5a9-476d-d458-7d2ff3278e89"
|
||||
},
|
||||
"source": [
|
||||
"experiment.configs(conf,\n",
|
||||
" # A dictionary of configurations to override\n",
|
||||
" {'tokenizer': 'character',\n",
|
||||
" 'text': 'tiny_shakespeare',\n",
|
||||
" 'optimizer.learning_rate': 1.,\n",
|
||||
" 'optimizer.optimizer': 'Noam',\n",
|
||||
" 'prompt': 'It is',\n",
|
||||
" 'prompt_separator': '',\n",
|
||||
"\n",
|
||||
" 'train_loader': 'sequential_train_loader',\n",
|
||||
" 'valid_loader': 'sequential_valid_loader',\n",
|
||||
"\n",
|
||||
" 'seq_len': 2,\n",
|
||||
" 'mem_len': 32,\n",
|
||||
" 'epochs': 128,\n",
|
||||
" 'batch_size': 32,\n",
|
||||
" 'inner_iterations': 25,\n",
|
||||
" })"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "EvI7MtgJ61w5"
|
||||
},
|
||||
"source": [
|
||||
"Set PyTorch models for loading and saving"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"base_uri": "https://localhost:8080/",
|
||||
"height": 255
|
||||
},
|
||||
"id": "GDlt7dp-5ALt",
|
||||
"outputId": "0543a726-fcf4-4493-dbe9-ab62c5bea94f"
|
||||
},
|
||||
"source": [
|
||||
"experiment.add_pytorch_models({'model': conf.model})"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "KJZRf8527GxL"
|
||||
},
|
||||
"source": [
|
||||
"Start the experiment and run the training loop."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"base_uri": "https://localhost:8080/",
|
||||
"height": 493
|
||||
},
|
||||
"id": "aIAWo7Fw5DR8",
|
||||
"outputId": "64764a3f-e8d7-4c06-b803-0da6ff756163"
|
||||
},
|
||||
"source": [
|
||||
"# Start the experiment\n",
|
||||
"with experiment.start():\n",
|
||||
" conf.run()"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"id": "oBXXlP2b7XZO"
|
||||
},
|
||||
"source": [
|
||||
""
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
"""
|
||||
---
|
||||
title: Transformer XL Experiment
|
||||
summary: This experiment trains a transformer XL model on tiny Shakespeare dataset.
|
||||
---
|
||||
|
||||
# Transformer XL Experiment
|
||||
|
||||
This is an annotated PyTorch experiment to train a transformer xl model.
|
||||
"""
|
||||
from typing import List
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from labml import experiment, tracker, monit, logger
|
||||
from labml.configs import option
|
||||
from labml.logger import Text
|
||||
from labml_nn.experiments.nlp_autoregression import NLPAutoRegressionConfigs
|
||||
from labml_nn.helpers.metrics import SimpleStateModule
|
||||
from labml_nn.helpers.trainer import BatchIndex
|
||||
from labml_nn.transformers.xl import TransformerXL, TransformerXLLayer
|
||||
|
||||
|
||||
class AutoregressiveModel(nn.Module):
|
||||
"""
|
||||
## Auto regressive model
|
||||
"""
|
||||
|
||||
def __init__(self, n_vocab: int, d_model: int, transformer: TransformerXL):
|
||||
super().__init__()
|
||||
# Token embedding module
|
||||
self.src_embed = nn.Embedding(n_vocab, d_model)
|
||||
# Transformer
|
||||
self.transformer = transformer
|
||||
# Final layer
|
||||
self.generator = nn.Linear(d_model, n_vocab)
|
||||
# Masks
|
||||
self.mask_x = None
|
||||
self.mask_mem = None
|
||||
|
||||
def forward(self, x: torch.Tensor, mem: List[torch.Tensor]):
|
||||
# Length of the memory
|
||||
m_len = len(mem[0]) if mem else 0
|
||||
# Create a subsequent mask for tokens
|
||||
if self.mask_x is None or self.mask_x.shape[0] < len(x):
|
||||
from labml_nn.transformers.utils import subsequent_mask
|
||||
self.mask_x = subsequent_mask(len(x)).to(x.device)
|
||||
# Create an all ones (full visibility) mask for memory
|
||||
if self.mask_mem is None or self.mask_mem.shape[1] < m_len or self.mask_mem.shape[0] < len(x):
|
||||
self.mask_mem = self.mask_x.new_ones(len(x), m_len, 1)
|
||||
|
||||
# Concatenate the masks if there is memory
|
||||
if m_len:
|
||||
mask = torch.cat((self.mask_mem[:len(x), :m_len], self.mask_x[:len(x), :len(x)]), dim=1)
|
||||
# Use the subsequent mask otherwise
|
||||
else:
|
||||
mask = self.mask_x[:len(x), :len(x)]
|
||||
|
||||
# Token embeddings
|
||||
x = self.src_embed(x)
|
||||
# Run it through the transformer
|
||||
res, mem = self.transformer(x, mem, mask)
|
||||
# Generate logits of the next token
|
||||
res = self.generator(res)
|
||||
#
|
||||
return res, mem
|
||||
|
||||
|
||||
class Configs(NLPAutoRegressionConfigs):
|
||||
"""
|
||||
## Configurations
|
||||
|
||||
The default configs can and will be over-ridden when we start the experiment
|
||||
"""
|
||||
|
||||
model: AutoregressiveModel
|
||||
|
||||
# Token embedding size
|
||||
d_model: int = 128
|
||||
# Number of attention heads
|
||||
heads: int = 4
|
||||
# Dropout probability
|
||||
dropout: float = 0.0
|
||||
# Number of features in FFN hidden layer
|
||||
d_ff: int = 256
|
||||
# Number of transformer layers
|
||||
n_layers: int = 6
|
||||
# Number of memories to keep
|
||||
mem_len: int = 128
|
||||
# State module to maintain memories when switching between training and validation
|
||||
memory = SimpleStateModule()
|
||||
|
||||
def init(self):
|
||||
# Set tracker configurations
|
||||
tracker.set_scalar("accuracy.*", True)
|
||||
tracker.set_scalar("loss.*", True)
|
||||
# This will keep the accuracy metric stats and memories separate for training and validation.
|
||||
self.state_modules = [self.accuracy, self.memory]
|
||||
|
||||
def merge_memory(self, old_mem, new_mem):
|
||||
"""
|
||||
Concatenate memories and remove old memories to keep a maximum of
|
||||
`mem_len` memories.
|
||||
"""
|
||||
|
||||
# If it's configured not to use memory
|
||||
if self.mem_len == 0:
|
||||
return []
|
||||
|
||||
# Concatenate with old memory
|
||||
if old_mem:
|
||||
mem = [torch.cat((m, x), dim=0) for m, x in zip(old_mem, new_mem)]
|
||||
else:
|
||||
mem = new_mem
|
||||
|
||||
# Truncate old memories
|
||||
if len(mem[0]) > self.mem_len:
|
||||
mem = [m[-self.mem_len:] for m in mem]
|
||||
|
||||
#
|
||||
return mem
|
||||
|
||||
def step(self, batch: any, batch_idx: BatchIndex):
|
||||
"""
|
||||
### Training/validation step
|
||||
"""
|
||||
|
||||
# Move data to the device
|
||||
data, target = batch[0].to(self.device), batch[1].to(self.device)
|
||||
|
||||
# Update global step (number of tokens processed) when in training mode
|
||||
if self.mode.is_train:
|
||||
tracker.add_global_step(data.shape[0] * data.shape[1])
|
||||
|
||||
# Get memories
|
||||
mem = self.memory.get()
|
||||
# Run the model
|
||||
output, new_mem = self.model(data, mem)
|
||||
# Merge memory
|
||||
mem = self.merge_memory(mem, new_mem)
|
||||
# Update memories
|
||||
self.memory.set(mem)
|
||||
|
||||
# Calculate and log cross entropy loss
|
||||
loss = self.loss_func(output, target)
|
||||
tracker.add("loss.", loss)
|
||||
|
||||
# Calculate and log accuracy
|
||||
self.accuracy(output, target)
|
||||
self.accuracy.track()
|
||||
|
||||
# Train the model
|
||||
if self.mode.is_train:
|
||||
# Calculate gradients
|
||||
loss.backward()
|
||||
# Clip gradients
|
||||
torch.nn.utils.clip_grad_norm_(self.model.parameters(), max_norm=self.grad_norm_clip)
|
||||
# Take optimizer step
|
||||
self.optimizer.step()
|
||||
# Log the model parameters and gradients on last batch of every epoch
|
||||
if batch_idx.is_last:
|
||||
tracker.add('model', self.model)
|
||||
# Clear the gradients
|
||||
self.optimizer.zero_grad()
|
||||
|
||||
# Save the tracked metrics
|
||||
tracker.save()
|
||||
|
||||
def sample(self):
|
||||
"""
|
||||
### Sampling function to generate samples periodically while training
|
||||
"""
|
||||
|
||||
# Starting prompt
|
||||
prompt = self.prompt
|
||||
# Collect output for printing
|
||||
log = [(prompt, Text.subtle)]
|
||||
# memory
|
||||
mem = []
|
||||
# Sample 25 tokens
|
||||
for i in monit.iterate('Sample', 25):
|
||||
# Tokenize the prompt
|
||||
data = self.text.text_to_i(prompt).unsqueeze(-1)
|
||||
# Move to device
|
||||
data = data.to(self.device)
|
||||
# Get the model output
|
||||
output, new_mem = self.model(data, mem)
|
||||
# Get the model prediction (greedy)
|
||||
output = output.argmax(dim=-1).squeeze(1)
|
||||
# Add the prediction to prompt
|
||||
prompt += self.prompt_separator + self.text.itos[output[-1]]
|
||||
# Only feed the last character to model in next iteration, rest will go in as memories
|
||||
prompt = prompt[-1:]
|
||||
# Add the prediction for logging
|
||||
log += [(self.prompt_separator + self.text.itos[output[-1]], Text.value)]
|
||||
# Update memory
|
||||
mem = self.merge_memory(mem, new_mem)
|
||||
|
||||
# Print the sampled output
|
||||
logger.log(log)
|
||||
|
||||
|
||||
@option(Configs.model)
|
||||
def autoregressive_model(c: Configs):
|
||||
"""
|
||||
### Initialize the auto-regressive model
|
||||
"""
|
||||
from labml_nn.transformers.xl import RelativeMultiHeadAttention
|
||||
from labml_nn.transformers.feed_forward import FeedForward
|
||||
m = AutoregressiveModel(c.n_tokens, c.d_model, TransformerXL(
|
||||
TransformerXLLayer(d_model=c.d_model,
|
||||
self_attn=RelativeMultiHeadAttention(c.heads, c.d_model, c.dropout),
|
||||
feed_forward=FeedForward(c.d_model, c.d_ff, c.dropout),
|
||||
dropout_prob=c.dropout), c.n_layers))
|
||||
return m.to(c.device)
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
### Run the experiment
|
||||
"""
|
||||
# Create experiment
|
||||
experiment.create(name="transformer_xl", comment='')
|
||||
# Create configs
|
||||
conf = Configs()
|
||||
# Load configurations
|
||||
experiment.configs(conf,
|
||||
# A dictionary of configurations to override
|
||||
{'tokenizer': 'character',
|
||||
'text': 'tiny_shakespeare',
|
||||
'optimizer.learning_rate': 1.,
|
||||
'optimizer.optimizer': 'Noam',
|
||||
'prompt': 'It is',
|
||||
'prompt_separator': '',
|
||||
|
||||
'train_loader': 'sequential_train_loader',
|
||||
'valid_loader': 'sequential_valid_loader',
|
||||
|
||||
'seq_len': 2,
|
||||
'mem_len': 32,
|
||||
'epochs': 128,
|
||||
'batch_size': 32,
|
||||
'inner_iterations': 25,
|
||||
})
|
||||
|
||||
# Set models for saving and loading
|
||||
experiment.add_pytorch_models({'model': conf.model})
|
||||
|
||||
# Start the experiment
|
||||
with experiment.start():
|
||||
# `TrainValidConfigs.run`
|
||||
conf.run()
|
||||
|
||||
|
||||
#
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,23 @@
|
||||
# [Transformer XL](https://nn.labml.ai/transformers/xl/index.html)
|
||||
|
||||
This is an implementation of
|
||||
[Transformer-XL: Attentive Language Models Beyond a Fixed-Length Context](https://arxiv.org/abs/1901.02860)
|
||||
in [PyTorch](https://pytorch.org).
|
||||
|
||||
Transformer has a limited attention span,
|
||||
equal to the length of the sequence trained in parallel.
|
||||
All these positions have a fixed positional encoding.
|
||||
Transformer XL increases this attention span by letting
|
||||
each of the positions pay attention to precalculated past embeddings.
|
||||
For instance if the context length is $l$, it will keep the embeddings of
|
||||
all layers for previous batch of length $l$ and feed them to current step.
|
||||
If we use fixed-positional encodings these pre-calculated embeddings will have
|
||||
the same positions as the current context.
|
||||
They introduce relative positional encoding, where the positional encodings
|
||||
are introduced at the attention calculation.
|
||||
|
||||
Annotated implementation of relative multi-headed attention is in [`relative_mha.py`](https://nn.labml.ai/transformers/xl/relative_mha.html).
|
||||
|
||||
Here's [the training code](https://nn.labml.ai/transformers/xl/experiment.html) and a notebook for training a transformer XL model on Tiny Shakespeare dataset.
|
||||
|
||||
[](https://colab.research.google.com/github/labmlai/annotated_deep_learning_paper_implementations/blob/master/labml_nn/transformers/xl/experiment.ipynb)
|
||||
@@ -0,0 +1,152 @@
|
||||
"""
|
||||
---
|
||||
title: Relative Multi-Headed Attention
|
||||
summary: >
|
||||
Documented implementation with explanations of
|
||||
Relative Multi-Headed Attention from paper Transformer-XL.
|
||||
---
|
||||
|
||||
# Relative Multi-Headed Attention
|
||||
|
||||
This is an implementation of relative multi-headed attention from paper
|
||||
[Transformer-XL: Attentive Language Models Beyond a Fixed-Length Context](https://arxiv.org/abs/1901.02860)
|
||||
in [PyTorch](https://pytorch.org).
|
||||
"""
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
from labml.logger import inspect
|
||||
from labml_nn.transformers.mha import MultiHeadAttention
|
||||
|
||||
|
||||
def shift_right(x: torch.Tensor):
|
||||
"""
|
||||
This method shifts $i^{th}$ row of a matrix by $i$ columns.
|
||||
|
||||
If the input is `[[1, 2 ,3], [4, 5 ,6], [7, 8, 9]]`, the shifted
|
||||
result would be `[[1, 2 ,3], [0, 4, 5], [6, 0, 7]]`.
|
||||
*Ideally we should mask out the lower triangle but it's ok for our purpose*.
|
||||
"""
|
||||
|
||||
# Concatenate a column of zeros
|
||||
zero_pad = x.new_zeros(x.shape[0], 1, *x.shape[2:])
|
||||
x_padded = torch.cat([x, zero_pad], dim=1)
|
||||
|
||||
# Reshape and remove excess elements from the end
|
||||
x_padded = x_padded.view(x.shape[1] + 1, x.shape[0], *x.shape[2:])
|
||||
x = x_padded[:-1].view_as(x)
|
||||
|
||||
#
|
||||
return x
|
||||
|
||||
|
||||
class RelativeMultiHeadAttention(MultiHeadAttention):
|
||||
"""
|
||||
## Relative Multi-Head Attention Module
|
||||
|
||||
We override [Multi-Head Attention](mha.html) module so we only need to
|
||||
write the `get_scores` method.
|
||||
"""
|
||||
|
||||
def __init__(self, heads: int, d_model: int, dropout_prob: float = 0.1):
|
||||
# The linear transformations do not need a bias since we
|
||||
# explicitly include it when calculating scores.
|
||||
# However having a bias for `value` might make sense.
|
||||
super().__init__(heads, d_model, dropout_prob, bias=False)
|
||||
|
||||
# Number of relative positions
|
||||
self.P = 2 ** 12
|
||||
|
||||
# Relative positional embeddings for key relative to the query.
|
||||
# We need $2P$ embeddings because the keys can be before or after the query.
|
||||
self.key_pos_embeddings = nn.Parameter(torch.zeros((self.P * 2, heads, self.d_k)), requires_grad=True)
|
||||
# Relative positional embedding bias for key relative to the query.
|
||||
self.key_pos_bias = nn.Parameter(torch.zeros((self.P * 2, heads)), requires_grad=True)
|
||||
# Positional embeddings for the query is independent of the position of the query
|
||||
self.query_pos_bias = nn.Parameter(torch.zeros((heads, self.d_k)), requires_grad=True)
|
||||
|
||||
def get_scores(self, query: torch.Tensor, key: torch.Tensor):
|
||||
r"""
|
||||
### Get relative attention scores
|
||||
|
||||
With absolute attention
|
||||
|
||||
\begin{align}
|
||||
A^{abs}_{j} &= lin_q(X^q_i + P_i)^\top lin_k(X^k_j + P_j) \\
|
||||
&= \underset{\textcolor{lightgreen}{A}}{Q_i^\top K_j} +
|
||||
\underset{\textcolor{lightgreen}{B}}{Q_i^\top U^K_j} +
|
||||
\underset{\textcolor{lightgreen}{C}}{{U^Q_i}^\top K_j} +
|
||||
\underset{\textcolor{lightgreen}{D}}{{U^Q_i}^\top U^K_j}
|
||||
\end{align}
|
||||
|
||||
where $Q_i, K_j$, are linear transformations of
|
||||
original embeddings $X^q_i, X^k_j$
|
||||
and $U^Q_i, U^K_j$ are linear transformations of
|
||||
absolute positional encodings $P_i, P_j$.
|
||||
|
||||
They reason out that the attention to a given key should be the same regardless of
|
||||
the position of query.
|
||||
Hence replace $\underset{\textcolor{lightgreen}{C}}{{U^Q_i}^\top K_j}$
|
||||
with a constant $\underset{\textcolor{lightgreen}{C}}{\textcolor{orange}{v^\top} K_j}$.
|
||||
|
||||
For the second and third terms relative positional encodings are introduced.
|
||||
So $\underset{\textcolor{lightgreen}{B}}{Q_i^\top U^K_j}$ is
|
||||
replaced with $\underset{\textcolor{lightgreen}{B}}{Q_i^\top \textcolor{orange}{R_{i - j}}}$
|
||||
and $\underset{\textcolor{lightgreen}{D}}{{U^Q_i}^\top U^K_j}$
|
||||
with $\underset{\textcolor{lightgreen}{D}}{\textcolor{orange}{S_{i-j}}}$.
|
||||
|
||||
\begin{align}
|
||||
A^{rel}_{i,j} &= \underset{\mathbf{\textcolor{lightgreen}{A}}}{Q_i^\top K_j} +
|
||||
\underset{\mathbf{\textcolor{lightgreen}{B}}}{Q_i^\top \textcolor{orange}{R_{i - j}}} +
|
||||
\underset{\mathbf{\textcolor{lightgreen}{C}}}{\textcolor{orange}{v^\top} K_j} +
|
||||
\underset{\mathbf{\textcolor{lightgreen}{D}}}{\textcolor{orange}{S_{i-j}}}
|
||||
\end{align}
|
||||
"""
|
||||
|
||||
# $\textcolor{orange}{R_k}$
|
||||
key_pos_emb = self.key_pos_embeddings[self.P - key.shape[0]:self.P + query.shape[0]]
|
||||
# $\textcolor{orange}{S_k}$
|
||||
key_pos_bias = self.key_pos_bias[self.P - key.shape[0]:self.P + query.shape[0]]
|
||||
# $\textcolor{orange}{v^\top}$
|
||||
query_pos_bias = self.query_pos_bias[None, None, :, :]
|
||||
|
||||
# ${(\textcolor{lightgreen}{\mathbf{A + C}})}_{i,j} =
|
||||
# Q_i^\top K_j +
|
||||
# \textcolor{orange}{v^\top} K_j$
|
||||
ac = torch.einsum('ibhd,jbhd->ijbh', query + query_pos_bias, key)
|
||||
# $\textcolor{lightgreen}{\mathbf{B'}_{i,k}} = Q_i^\top \textcolor{orange}{R_k}$
|
||||
b = torch.einsum('ibhd,jhd->ijbh', query, key_pos_emb)
|
||||
# $\textcolor{lightgreen}{\mathbf{D'}_{i,k}} = \textcolor{orange}{S_k}$
|
||||
d = key_pos_bias[None, :, None, :]
|
||||
# Shift the rows of $\textcolor{lightgreen}{\mathbf{(B' + D')}_{i,k}}$
|
||||
# to get $$\textcolor{lightgreen}{\mathbf{(B + D)}_{i,j} = \mathbf{(B' + D')}_{i,i - j}}$$
|
||||
bd = shift_right(b + d)
|
||||
# Remove extra positions
|
||||
bd = bd[:, -key.shape[0]:]
|
||||
|
||||
# Return the sum $$
|
||||
# \underset{\mathbf{\textcolor{lightgreen}{A}}}{Q_i^\top K_j} +
|
||||
# \underset{\mathbf{\textcolor{lightgreen}{B}}}{Q_i^\top \textcolor{orange}{R_{i - j}}} +
|
||||
# \underset{\mathbf{\textcolor{lightgreen}{C}}}{\textcolor{orange}{v^\top} K_j} +
|
||||
# \underset{\mathbf{\textcolor{lightgreen}{D}}}{\textcolor{orange}{S_{i-j}}}
|
||||
# $$
|
||||
return ac + bd
|
||||
|
||||
|
||||
def _test_shift_right():
|
||||
x = torch.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
|
||||
inspect(x)
|
||||
inspect(shift_right(x))
|
||||
|
||||
x = torch.arange(1, 6)[None, :, None, None].repeat(5, 1, 1, 1)
|
||||
inspect(x[:, :, 0, 0])
|
||||
inspect(shift_right(x)[:, :, 0, 0])
|
||||
|
||||
x = torch.arange(1, 6)[None, :, None, None].repeat(3, 1, 1, 1)
|
||||
inspect(x[:, :, 0, 0])
|
||||
inspect(shift_right(x)[:, :, 0, 0])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
_test_shift_right()
|
||||
Reference in New Issue
Block a user