PyTorch Word Embedding
Word embedding is one of the most fundamental and important techniques in natural language processing.
Word embedding maps discrete word symbols into continuous dense vectors, enabling machines to understand and process text data.
PyTorch providesnn.Embeddingmodule to implement this functionality, serving as the foundation for building various NLP models.
1. Basic Concepts of Word Embedding
In a computer, text is essentially a sequence of integers. Each word is assigned a unique index ID, but this discrete representation has a problem: semantically similar words may be close in meaning, yet their IDs are completely unrelated.
Word embedding solves this problem by learning anembedding matrix:
\[ E \in \mathbb{R}^{V \times D} \]
Where \(V\) is the vocabulary size and \(D\) is the embedding dimension. Each word ID corresponds to a row in the embedding matrix, and its vector representation is obtained through a lookup operation:
\[ \text{embedding} = E[\text{word\_id}] \]
The advantages of word embedding include:
- Converting high-dimensional sparse one-hot vectors into low-dimensional dense vectors, greatly reducing computational cost
- Semantically similar words are closer in vector space, and word similarity can be computed via cosine similarity
- Embedding vectors are learnable parameters and can be automatically adjusted through backpropagation
2. nn.Embedding Explained in Detail
nn.Embeddingis the word embedding layer provided by PyTorch, encapsulating the creation of the embedding matrix and the lookup operation.
2.1 Basic Usage
Example
import torch.nn as nn
# Create an embedding layer
# num_embeddings: vocabulary size (vocab_size)
# embedding_dim: embedding dimension (embedding_dim)
vocab_size = 10000
embedding_dim = 256
embedding = nn.Embedding(num_embeddings=vocab_size, embedding_dim=embedding_dim)
# View the shape of the embedding matrix
print(embedding.weight.shape) # torch.Size([10000, 256])
# Input word indices (LongTensor) to obtain embedding vectors
word_ids = torch.tensor([0, 1, 2, 9999]) # Arbitrary word indices
embedded = embedding(word_ids)
print(embedded.shape) # torch.Size([4, 256])
# Each word ID corresponds to a 256-dimensional vector
2.2 nn.Embedding Parameter Details
Example
embedding = nn.Embedding(
num_embeddings=10000, # Vocabulary size, must be greater than or equal to the maximum index value in the input
embedding_dim=256, # Embedding vector dimension, usually 50, 100, 200, 300, etc.
padding_idx=None, # Index of the padding word; the embedding vector for the padding word is all zeros
max_norm=None, # Maximum norm of the embedding vector, used for normalization
norm_type=2.0, # Normalization type, usually L2 norm
scale_grad_by_freq=False,# Scale gradients by word frequency
sparse=False, # Whether to use sparse gradients (saves GPU memory, but training is slower)
_weight=None, # Predefined weights, used for loading pretrained embeddings
)
# View the number of parameters
total_params = embedding.num_embeddings * embedding.embedding_dim
print(f"Embedding layer parameter count: {total_params:,}")
# 10000 * 256 = 2,560,000
The number of parameters in the embedding layer = vocabulary size × embedding dimension. This is a very large matrix. Usually, the embedding layer of an NLP model accounts for a large proportion of the model's total parameter count.
2.3 Padding Index padding_idx
When processing variable-length sequences, short sequences need to be padded. Usingpadding_idxcan fix the embedding vector of the padding word to a zero vector, preventing padding content from affecting the model:
Example
import torch.nn as nn
# Set padding_idx=0, meaning index 0 is the padding word
embedding = nn.Embedding(num_embeddings=10000, embedding_dim=128, padding_idx=0)
# Initialize the weights
nn.init.uniform_(embedding.weight, -0.1, 0.1)
# The embedding vector at index 0 is all zeros
word_0 = embedding(torch.tensor([0]))
print(f"Embedding of padding word: {word_0}") # All zeros
# Other indices remain normal
word_5 = embedding(torch.tensor([5]))
print(f"Embedding of word 5: {word_5}") # Non-zero values
2.4 Max Norm Normalization
Usingmax_normcan limit the norm of embedding vectors, preventing them from becoming too large during training:
Example
import torch.nn as nn
# Set the maximum norm to 1.0
embedding = nn.Embedding(num_embeddings=1000, embedding_dim=64, max_norm=1.0)
# Input arbitrary word indices
ids = torch.tensor([1, 2, 3])
embedded = embedding(ids)
# Check the L2 norm of each vector
norms = torch.norm(embedded, p=2, dim=1)
print(f"Norms of each vector: {norms}") # All values are close to 1.0
3. Loading Pretrained Word Embeddings
Using pretrained word embeddings can significantly improve model performance, especially when training data is scarce. Common pretrained word embeddings include Word2Vec, GloVe, FastText, etc.
3.1 Difference Between Training from Scratch and Loading Pretrained Embeddings
| Method | Advantages | Disadvantages | Applicable Scenarios |
|---|---|---|---|
| Random initialization and training | Fully customizable, adapts to specific tasks | Requires a large amount of training data | Many domain-specific words and sufficient training data |
| Loading pretrained embeddings | Leverages knowledge from large-scale corpora, fast training and good results | Limited vocabulary coverage, cannot handle out-of-vocabulary words | General tasks, limited training data |
| Freezing pretrained embeddings | Fast training, low GPU memory usage | Embeddings cannot be fine-tuned | Limited training resources, focusing only on the upper layers of the model |
| Fine-tuning pretrained embeddings | Can adapt to specific tasks | Slower training, high GPU memory usage | Moderate data size, certain domain differences |
3.2 Loading GloVe Pretrained Word Vectors
GloVe is a pretrained word vector released by Stanford University. The following shows how to load it:
Example
import torch.nn as nn
import numpy as np
# Simulate loading GloVe word vectors (actually need to download GloVe files)
# Assume there is already a word vector file with the format: each line contains a word followed by its vectors
def load_glove_embeddings(path, word2idx, embedding_dim=300):
"""
Load GloVe pretrained word vectors
path: path to the word vector file
word2idx: dictionary mapping words to indices
embedding_dim: word vector dimension
"""
embeddings = np.random.randn(len(word2idx), embedding_dim).astype(np.float32)
word_count = 0
with open(path, 'r', encoding='utf-8') as f:
for line in f:
values = line.strip().split()
word = values[0]
if word in word2idx:
word_idx = word2idx[word]
embeddings[word_idx] = np.asarray(values[1:], dtype=np.float32)
word_count += 1
print(f"Loaded {word_count}/{len(word2idx)} word vectors")
return torch.from_numpy(embeddings)
# Assume there is already a vocabulary
word2idx = {'hello': 0, 'world': 1, 'example': 2, 'python': 3}
EMBED_DIM = 300
# Load pretrained embeddings and create an embedding layer
# pretrained_embeddings = load_glove_embeddings('glove.6B.300d.txt', word2idx, EMBED_DIM)
# embedding = nn.Embedding.from_pretrained(pretrained_embeddings, padding_idx=0)
# Simplified example: use a randomly initialized pretrained matrix
pretrained_embeddings = torch.randn(len(word2idx), EMBED_DIM)
embedding = nn.Embedding.from_pretrained(pretrained_embeddings, padding_idx=0)
print(f"Embedding layer shape: {embedding.weight.shape}")
3.3 Freezing and Fine-Tuning the Embedding Layer
Depending on the task requirements, you can choose to freeze or fine-tune the embedding layer:
Example
import torch.nn as nn
embedding = nn.Embedding(num_embeddings=10000, embedding_dim=256)
# Method 1: Freeze the embedding layer (not participating in training)
embedding.weight.requires_grad = False
# During training, only parameters with embedding.weight.requires_grad = False will not be updated
# Method 2: Fine-tune the embedding layer (participating in training)
embedding.weight.requires_grad = True
# Method 3: Freeze partial word vectors (freeze high-frequency words like "the", "is")
# Assume the indices of high-frequency words are 0-99
embedding.weight.requires_grad = True
with torch.no_grad():
embedding.weight[0:100] *= 0 # Or assign fixed values
# Filter out frozen parameters in the optimizer
optimizer = torch.optim.Adam(
filter(lambda p: p.requires_grad, embedding.parameters()),
lr=1e-3
)
4. Combining Embedding Layer with RNN/LSTM
Word embedding is the first layer of an NLP model. It converts text IDs into dense vectors, which are then passed to sequence models such as RNN and LSTM for processing.
4.1 Embedding Layer + LSTM Text Classification
Example
import torch.nn as nn
class TextClassifier(nn.Module):
"""
Embedding Layer + LSTM Text Classification Model
"""
def __init__(self, vocab_size, embed_dim, hidden_size, num_classes, padding_idx=0):
super().__init__()
# Word embedding layer
self.embedding = nn.Embedding(
num_embeddings=vocab_size,
embedding_dim=embed_dim,
padding_idx=padding_idx
)
# LSTM layer
self.lstm = nn.LSTM(
input_size=embed_dim,
hidden_size=hidden_size,
num_layers=2,
batch_first=True,
bidirectional=True,
dropout=0.3
)
# Classifier
self.fc = nn.Linear(hidden_size * 2, num_classes)
def forward(self, x):
# x: (batch_size, seq_len) - word indices
embedded = self.embedding(x) # (batch_size, seq_len, embed_dim)
# LSTM output
output, (h_n, c_n) = self.lstm(embedded)
# Take the hidden state of the last time step (bidirectional concatenation)
# Forward last hidden state: h_n[-2]
# Backward last hidden state: h_n[-1]
h_combined = torch.cat([h_n[-2], h_n[-1]], dim=-1)
# Classification
logits = self.fc(h_combined)
return logits
# Instantiate the model
VOCAB_SIZE = 10000
EMBED_DIM = 128
HIDDEN_SIZE = 128
NUM_CLASSES = 2
model = TextClassifier(VOCAB_SIZE, EMBED_DIM, HIDDEN_SIZE, NUM_CLASSES)
# Simulated input: batch_size=4, sequence length=10
batch_input = torch.randint(1, VOCAB_SIZE, (4, 10))
output = model(batch_input)
print(f"Input shape: {batch_input.shape}") # torch.Size([4, 10])
print(f"Output shape: {output.shape}") # torch.Size([4, 2])
4.2 Using Pretrained Word Vectors
Example
import torch.nn as nn
# Assume pretrained word vectors have been loaded
pretrained_vectors = torch.randn(10000, 300) # Simulate pretrained vectors
# Create an embedding layer and load pretrained weights
embedding = nn.Embedding.from_pretrained(
pretrained_vectors,
padding_idx=0,
freeze=False # True: freeze and do not train, False: fine-tune
)
# When using GloVe or other pretrained word vectors, usually freeze for a few epochs first, then unfreeze for fine-tuning
# For the first few epochs, only train the upper layers of the model
embedding.weight.requires_grad = False
# After training for several epochs, unfreeze the embedding layer for fine-tuning
# embedding.weight.requires_grad = True
5. Positional Encoding
Unlike RNN/LSTM, the Transformer model does not include positional information. Positional encoding needs to be added additionally to let the model perceive the order of words in a sequence.
5.1 Principle of Positional Encoding
Positional encoding uses sine and cosine functions to generate position vectors:
\[ \begin{aligned} PE_{(pos, 2i)} &= \sin\left(\frac{pos}{10000^{2i/d_{model}}}\right) \\ PE_{(pos, 2i+1)} &= \cos\left(\frac{pos}{10000^{2i/d_{model}}}\right) \end{aligned} \]
The feature of this encoding method is that encodings at different positions can be converted to each other through linear transformations, making it easy for the model to learn positional relationships.
5.2 Implementing Positional Encoding
Example
import torch.nn as nn
import math
class PositionalEncoding(nn.Module):
"""
Positional Encoding Layer
"""
def __init__(self, d_model, max_len=5000):
super().__init__()
# Create positional encoding matrix
pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
# Compute the divisor term
div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
# Use sin for even indices and cos for odd indices
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
# Add a batch dimension and register it as a buffer that does not participate in gradient computation
pe = pe.unsqueeze(0) # (1, max_len, d_model)
self.register_buffer('pe', pe)
def forward(self, x):
"""
x: (batch_size, seq_len, d_model)
"""
seq_len = x.size(1)
# Slice the positional encoding of the corresponding length and add it
x = x + self.pe[:, :seq_len, :]
return x
# Usage example
d_model = 256
max_len = 100
pos_encoding = PositionalEncoding(d_model, max_len)
# Simulated input: batch_size=4, seq_len=20, d_model=256
x = torch.randn(4, 20, d_model)
x = pos_encoding(x)
print(f"Input shape: {x.shape}") # torch.Size([4, 20, 256])
Positional encoding is a key component in the Transformer architecture. It enables the model to distinguish words at different positions, even if their embedding vectors are the same.
5.3 Learnable Positional Encoding
In addition to fixed positional encoding, learnable positional encoding can also be used:
Example
import torch.nn as nn
class LearnablePositionalEncoding(nn.Module):
"""
Learnable Positional Encoding
"""
def __init__(self, d_model, max_len=5000):
super().__init__()
self.pos_embedding = nn.Embedding(max_len, d_model)
def forward(self, x):
batch_size, seq_len, d_model = x.size(1), x.size(1), x.size(2)
# Create position indices [0, 1, 2, ..., seq_len-1]
positions = torch.arange(seq_len, device=x.device).unsqueeze(0).expand(batch_size, -1)
pos_encoded = self.pos_embedding(positions)
return x + pos_encoded
# Usage example
pos_encoding = LearnablePositionalEncoding(d_model=256, max_len=100)
x = torch.randn(4, 20, 256)
x = pos_encoding(x)
print(f"Output shape: {x.shape}") # torch.Size([4, 20, 256])
6. Advanced Techniques for Embedding Layers
6.1 Reducing GPU Memory Usage
When the vocabulary is very large, the embedding layer consumes a lot of GPU memory. The following techniques can be used to optimize:
Example
import torch.nn as nn
# Technique 1: Use sparse gradients (sparse=True)
embedding = nn.Embedding(
num_embeddings=100000,
embedding_dim=256,
sparse=True # Gradients are stored in sparse format, saving GPU memory
)
# Technique 2: Use quantization
# Convert float32 to int8 or float16
embedding_int8 = embedding.to(torch.int8)
# Technique 3: Freeze infrequently used word vectors
# In a large-scale vocabulary, only train high-frequency words; keep low-frequency words frozen
embedding = nn.Embedding(num_embeddings=100000, embedding_dim=256)
embedding.weight.requires_grad = True
# Freeze word vectors with index greater than 50000
with torch.no_grad():
embedding.weight[50000:] *= 0
embedding.weight.requires_grad = False
# Only train high-frequency words
embedding.weight[1:50000].requires_grad = True
6.2 Handling Out-of-Vocabulary (OOV) Words
Words not in the vocabulary (out-of-vocabulary / OOV words) may appear in the test set and require special handling:
Example
import torch.nn as nn
class EmbeddingWithOOV(nn.Module):
"""
Embedding Layer Supporting Out-of-Vocabulary Words
"""
def __init__(self, vocab_size, embed_dim, oov_idx=None):
super().__init__()
self.embedding = nn.Embedding(vocab_size + 1, embed_dim, padding_idx=0)
self.oov_idx = oov_idx if oov_idx is not None else vocab_size # Use the last index as OOV
def forward(self, x):
# Replace the indices of OOV words with oov_idx
oov_mask = x >= self.vocab_size
x = x.clone()
x[oov_mask] = self.oov_idx
return self.embedding(x)
# Use hashing techniques to handle larger vocabularies
class HashEmbedding(nn.Module):
"""
Embedding Layer Using Hashing to Handle Vocabularies of Arbitrary Size
"""
def __init__(self, num_buckets, embed_dim):
super().__init__()
self.num_buckets = num_buckets
self.embedding = nn.Embedding(num_buckets, embed_dim)
def forward(self, x):
# Hash word indices into buckets
# Use Python dictionary's hashing method
hashed = torch.remainder(x, self.num_buckets)
return self.embedding(hashed)
6.3 Subword Embedding
For morphologically rich languages (such as German and Russian), subword embedding can effectively handle the out-of-vocabulary problem:
Example
import torch.nn as nn
class SubwordEmbedding(nn.Module):
"""
Simplified Subword Embedding Example
In practice, algorithms such as BPE and WordPiece should be used for tokenization
"""
def __init__(self, vocab_size, embed_dim, char_dim=50):
super().__init__()
# Character-level embedding
self.char_embedding = nn.Embedding(vocab_size, char_dim)
# Word-level embedding
self.word_embedding = nn.Embedding(vocab_size, embed_dim - char_dim)
# Character-level LSTM for combining character vectors
self.char_lstm = nn.LSTM(
char_dim, char_dim,
batch_first=True, bidirectional=True
)
def forward(self, word_ids, char_ids):
"""
word_ids: word indices (batch_size, seq_len)
char_ids: character indices (batch_size, seq_len, max_word_len)
"""
# Word embedding
word_emb = self.word_embedding(word_ids)
# Character embedding + LSTM
batch_size, seq_len, max_word_len = char_ids.shape
char_ids_flat = char_ids.view(-1, max_word_len) # (batch_size * seq_len, max_word_len)
char_emb = self.char_embedding(char_ids_flat) # (batch_size * seq_len, max_word_len, char_dim)
char_output, (h_n, _) = self.char_lstm(char_emb)
# Take bidirectional final hidden states and concatenate
char_rep = torch.cat([h_n[-2], h_n[-1]], dim=-1) # (batch_size * seq_len, char_dim * 2)
char_rep = char_rep.view(batch_size, seq_len, -1) # (batch_size, seq_len, char_dim * 2)
# Concatenate word embedding and character representation
combined = torch.cat([word_emb, char_rep], dim=-1)
return combined
7. Complete Practical Example: Text Classification Model
Below is a complete text classification model example, including an embedding layer, LSTM, and classifier:
Example
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# ── Configuration Parameters ──────────────────────────────────────
VOCAB_SIZE = 10000
EMBED_DIM = 128
HIDDEN_SIZE = 128
NUM_LAYERS = 2
NUM_CLASSES = 5
DROPOUT = 0.3
MAX_LEN = 200
# ── Model Definition ──────────────────────────────────────
class TextClassificationModel(nn.Module):
def __init__(self, vocab_size, embed_dim, hidden_size,
num_layers, num_classes, dropout=0.3, padding_idx=0):
super().__init__()
# Embedding layer
self.embedding = nn.Embedding(
vocab_size,
embed_dim,
padding_idx=padding_idx
)
# BiLSTM
self.lstm = nn.LSTM(
embed_dim,
hidden_size,
num_layers=num_layers,
batch_first=True,
bidirectional=True,
dropout=dropout if num_layers > 1 else 0
)
# Classifier
self.dropout = nn.Dropout(dropout)
self.fc = nn.Linear(hidden_size * 2, num_classes)
def forward(self, x):
# x: (batch_size, seq_len)
embedded = self.embedding(x) # (batch_size, seq_len, embed_dim)
# LSTM
output, (h_n, c_n) = self.lstm(embedded)
# Concatenate bidirectional final hidden states
h_forward = h_n[-2] # (batch_size, hidden_size)
h_backward = h_n[-1] # (batch_size, hidden_size)
h_combined = torch.cat([h_forward, h_backward], dim=-1)
# Classification
dropped = self.dropout(h_combined)
logits = self.fc(droped)
return logits
# ── Dataset ──────────────────────────────────────
class TextDataset(Dataset):
def __init__(self, texts, labels, vocab, max_len=200):
self.texts = texts
self.labels = labels
self.vocab = vocab
self.max_len = max_len
def __len__(self):
return len(self.texts)
def __getitem__(self, idx):
text = self.texts[idx][:self.max_len]
# Convert words to indices, unknown words represented by 1 (<UNK>)
ids = [self.vocab.get(word, 1) for word in text]
# Pad with zeros to a fixed length
if len(ids) < self.max_len:
ids += [0] * (self.max_len - len(ids))
return torch.tensor(ids), torch.tensor(self.labels[idx])
# ── Training Function ──────────────────────────────────────
def train_epoch(model, loader, optimizer, criterion, device):
model.train()
total_loss = 0
correct = 0
total = 0
for inputs, labels in loader:
inputs, labels = inputs.to(device), labels.to(device)
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
total_loss += loss.item() * inputs.size(0)
_, predicted = outputs.max(1)
correct += predicted.eq(labels).sum().item()
total += labels.size(0)
return total_loss / total, correct / total
# ── Initialization and Training ──────────────────────────────────
# Assume a vocabulary already exists
word2idx = {'<PAD>': 0, '<UNK>': 1} # The vocabulary needs to be built based on the actual corpus
model = TextClassificationModel(
VOCAB_SIZE, EMBED_DIM, HIDDEN_SIZE,
NUM_LAYERS, NUM_CLASSES, DROPOUT
).to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=1e-3)
# Simulated training
print("Start training...")
for epoch in range(10):
train_loss, train_acc = train_epoch(model, None, optimizer, criterion, device)
print(f"Epoch {epoch+1}: Loss={train_loss:.4f}, Acc={train_acc:.4f}")
8. API Quick Reference
8.1 Common Operations on nn.Embedding
| Operation | Code |
|---|---|
| Create embedding layer | nn.Embedding(num_embeddings, embedding_dim) |
| Load pretrained embeddings | nn.Embedding.from_pretrained(weights) |
| Look up table to get embeddings | embedding(word_ids) |
| Freeze embeddings | embedding.weight.requires_grad = False |
| Get embedding vectors | embedding.weight[idx] |
8.2 Pretrained Word Vector Resources
| Resource | Dimension | Features |
|---|---|---|
| GloVe | 50, 100, 200, 300 | Word co-occurrence statistics, fast training |
| Word2Vec | 50-500 | Trained by Google, wide coverage |
| FastText | 300 | Supports subwords, good at handling OOV |
| BERT | 768+ | Context-dependent, best performance |
8.3 Recommendations for Choosing an Embedding Layer
数据量小(< 10K)
-> 使用预训练词向量(GloVe/FastText)+ 冻结或微调
数据量中等(10K ~ 100K)
-> 使用预训练词向量 + 微调
数据量大(> 100K)
-> 可考虑从头训练,或使用大规模预训练模型
领域差异大
-> 使用领域相关预训练模型或增量训练
资源受限
-> 冻结嵌入层,使用较小的嵌入维度
Other Extensions