chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user