目录

第N4周NLP中的文本嵌入

第N4周:NLP中的文本嵌入

本人往期文章可查阅:

词嵌入是一种用于自然语言处理(NLP)的技术,用于将单词表示为数字,以便计算机可以处理它们。通俗的讲就是,一种把文本转为数值输入到计算机中的方法。 之前文章中提到的将文本转换为字典序列、one-hot编码就是最早期的词嵌入方法。

Embedding 和 EmbeddingBag 则是PyTorch 中的用来处理文本数据中词嵌入(word embedding)的工具,它们将离散的词汇映射到低维的连续向量空间中,使得词汇之间的语义关系能够在向量空间中得到体现。

1. Embedding 嵌入

Embedding 是 PyTorch 中最基本的词嵌入操作, Embedding 的输入是一个整数张量,每个整数都代表着一个词汇的索引,输出是一个浮点型的张量,每个浮点数都代表着对应词汇的词嵌入向量。

  • 输入shape: [batch,seqSize] (seqSize为单个文本长度)
  • 输出shape: [batch,seqSize,embed_dim] (embed_dim嵌入维度)

嵌入层使用 随机权重初始化 ,并将学习数据集中所有词的嵌入。它是一个灵活的层,可以以各种方式使用,如:

  • 它可以用作深度学习模型的一部分,其中嵌入于模型本身一起被学习。
  • 它可以用于加载训练好的词嵌入模型。

嵌入层被定义为网络的 第一个隐藏层

函数原型:

import torch

torch.nn.Embedding(
    num_embeddings,
    embedding_dim,
    padding_idx=None,
    max_norm=None,
    norm_type=2.0,
    scale_grad_by_freq=False,
    sparse=False,
    _weight=None,
    _freeze=False,
    device=None,
    dtype=None
)

官方API地址:

常见参数:

  • num_embeddings :嵌入的总数,也就是离散变量的类别数。例如在单词嵌入中,就是词汇表的大小,即,最大整数index+1。
  • embedding_dim :每个嵌入向量的维度,即转换后的连续向量的维度。这个维度决定了嵌入向量的表达能力,维度越高,理论上表达能力越强,但计算成本和参数量也会增加。
  • padding_idx (可选):如果设置,那么索引为 padding_idx 的嵌入向量会被初始化为全零向量,并且在训练过程中保持不变。这在处理填充(padding)操作时很有用,比如在处理不同长度的文本序列时,可以用 padding_idx 来填充较短的序列,使其长度一致。
  • max_norm (可选):如果设置,每个嵌入向量的范数会被限制在 max_norm 以内。这有助于控制嵌入向量的大小,避免过大的数值导致数值不稳定等问题。
  • norm_type (可选):与 max_norm 一起使用,指定范数的类型,默认为 2,即 L2 范数。
  • scale_grad_by_freq (可选):如果设置为 True ,那么在反向传播时,嵌入向量的梯度会被其频率缩放。这在处理稀疏数据时可能有助于稳定训练。
  • sparse (可选):如果设置为 True ,则梯度会被稀疏化,这在某些情况下可以节省内存和计算资源,但可能会导致一些优化器无法使用。

下面是一个简单的例子,用 Embedding 将两个句子转换为词嵌入向量:

1.1 自定义数据集类

import torch
from torch import nn
import torch.nn.functional as F
import torch.optim as optim
from torch.utils.data import DataLoader,Dataset

class MyDataset(Dataset):
    def __init__(self,texts,labels):
        self.texts=texts
        self.labels=labels
        
    def __len__(self):
        return len(self.labels)
    
    def __getitem__(self,idx):
        texts=self.texts[idx]
        labels=self.labels[idx]
        
        return texts,labels

1.2 定义填充函数

def collate_batch(batch):
    texts,labels=zip(*batch)
    max_len=max(len(text) for text in texts)
    padded_texts=[F.pad(text,(0,max_len-len(text)),value=0) for text in texts]
    padded_texts=torch.stack(padded_texts)
    labels=torch.tensor(labels,dtype=torch.float).unsqueeze(1)
    return padded_texts,labels

此处定义了一个自定义的批处理函数,用于将数据加载到 DataLoader 时对文本数据进行填充(padding)和标签处理。这个函数的目的是确保每个批次中的文本数据长度一致,并将标签转换为适合模型输入的格式。

  • texts, labels = zip(*batch)
    将批次中的数据解包为文本和标签。
  • max_len = max(len(text) for text in texts)
    计算批次中所有文本的最大长度。
  • padded_texts
    计算每个文本需要填充的长度: max_len - len(text), 使用 F.pad 在文本的右侧填充(padding)零,使其长度与最大长度一致(max_len)。
(1)
F.pad
这是 PyTorch 的 torch.nn.functional.pad 函数,用于对张量进行填充。
(2)
text
这里假设 text 是一个一维张量,表示一个文本序列(例如,经过编码的 input_ids
(3)
max_len
批次中所有文本的最大长度
(4)
len(text)
当前文本的长度
(5)
value=0
填充的值为 0
  • padded_texts=torch.stack(padded_texts): 将填充后的张量列表堆叠成一个更高维度的张量。这一步是必要的,因为 torch.stack 可以将多个张量沿着一个新的维度堆叠起来,从而形成一个批次化的张量。

假设 padded_texts 是一个填充后的张量列表,每个张量的形状为 [sequence_length]torch.stack 会将这些张量堆叠成一个更高维度的张量,形状为 [batch_size, sequence_length]

示例:

padded_texts = [
    torch.tensor([101, 2023, 2003, 102]),
    torch.tensor([101, 2003, 102, 0]),
    torch.tensor([101, 2054, 2000, 102])
]

使用 torch.stack(padded_texts) 后,结果为:

tensor([
    [101, 2023, 2003, 102],
    [101, 2003, 102,   0],
    [101, 2054, 2000, 102]
])
  • labels=torch.tensor(labels,dtype=torch.float).unsqueeze(1): 将标签转换为浮点型张量,并增加一个维度。这种操作在某些任务中非常常见,尤其是在处理分类任务时,需要将标签转换为适合模型输出的形式。
(1)
torch.tensor(labels, dtype=torch.float)
将标签列表转换为浮点型张量。这通常用于二分类任务,其中标签通常是 0 和 1。
(2)
.unsqueeze(1)
在第 1 维度(索引从 0 开始)增加一个维度。这会将标签从一维张量 [batch_size] 转换为二维张量 [batch_size, 1] 。

为什么需要 .unsqueeze(1)

在某些情况下,模型的输出是一个二维张量,形状为 [batch_size, num_classes] ,而标签需要与模型输出的形状一致。例如:

  • 在二分类任务中,模型输出的形状可能是 [batch_size, 1]
  • 在多分类任务中,模型输出的形状可能是 [batch_size, num_classes] ,但标签需要从 [batch_size] 转换为 [batch_size, 1]

通过使用 .unsqueeze(1) ,可以确保标签的形状与模型输出一致,从而避免形状不匹配的错误。

1.3 准备数据和数据加载器

# 假设我们有以下三个样本,分别由不同数量的单词索引组成
text_data=[
    torch.tensor([1,1,1,1],dtype=torch.long),  # 样本1
    torch.tensor([2,2,2],dtype=torch.long),  # 样本2
    torch.tensor([3,3],dtype=torch.long)   # 样本3
]

# 对应的标签
labels=torch.tensor([4,5,6],dtype=torch.float)

# 创建数据集和数据加载器
my_dataset=MyDataset(text_data,labels)
data_loader=DataLoader(my_dataset,batch_size=2,shuffle=True,collate_fn=collate_batch)

for batch in data_loader:
    print(batch)

代码输出:

(tensor([[2, 2, 2],
        [3, 3, 0]]), tensor([[5.],
        [6.]]))
(tensor([[1, 1, 1, 1]]), tensor([[4.]]))

1.4 定义模型

class EmbeddingModel(nn.Module):
    def __init__(self,vocab_size,embed_dim):
        super(EmbeddingModel,self).__init__()
        self.embedding=nn.Embedding(vocab_size,embed_dim)
        self.fc=nn.Linear(embed_dim,1) # 假设我们做一个二分类任务
        
    def forward(self,text):
        print("embedding输入文本是:",text)
        print("embedding输入文本是shape:",text.shape)
        embedding=self.embedding(text)
        embedding_mean=embedding.mean(dim=1) # 对每个样本的嵌入向量进行平均
        print("embedding输出文本shape:",embedding_mean.shape)
        return self.fc(embedding_mean)
(1)
nn.Embedding(vocab_size, embed_dim)
这是一个嵌入层,将输入的索引序列( text )映射到一个固定维度的嵌入向量
  • vocab_size
    词汇表大小,即输入索引的最大值加1。
  • embed_dim
    嵌入向量的维度。

(2) embedding_mean = embedding.mean(dim=1)

  • 这一步对每个样本的嵌入向量沿着序列长度维度( dim=1 )进行平均,得到每个样本的固定长度嵌入表示。
  • 输出的形状为 [batch_size, embed_dim]
(3) nn.Linear(embed_dim, 1)
  • 这是一个全连接层,将嵌入向量映射到输出维度为 1 的结果。
  • 输出的形状为 [batch_size, 1] ,适用于二分类任务。

特别注意:

如果使用 embedding_mean=embedding.mean(dim=1) 语句对每个样本的嵌入向量求平均,输出shape为 [batch,embed_dim] 。若注释掉该语句,输出shape则为 [batch,seqSize,embed_dim] 。

1.5 训练模型

# 示例词典大小和嵌入维度
vocab_size=10
embed_dim=6

# 创建模型实例
model=EmbeddingModel(vocab_size,embed_dim)

# 定义一个简单的损失函数和优化器
criterion=nn.BCEWithLogitsLoss()
optimizer=optim.SGD(model.parameters(),lr=0.01)

# 训练模型
for epoch in range(1): # 训练1个epoch
    for batch in data_loader:
        texts,labels=batch
        
        # 前向传播
        outputs=model(texts)
        loss=criterion(outputs,labels)
        
        # 反向传播和优化
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        
    print(f'Epoch {epoch+1},Loss:{loss.item()}')

代码输出:

embedding输入文本是: tensor([[3, 3, 0],
        [2, 2, 2]])
embedding输入文本是shape: torch.Size([2, 3])
embedding输出文本shape: torch.Size([2, 6])
embedding输入文本是: tensor([[1, 1, 1, 1]])
embedding输入文本是shape: torch.Size([1, 4])
embedding输出文本shape: torch.Size([1, 6])
Epoch 1,Loss:3.57496976852417

2. EmbeddingBag嵌入

EmbeddingBag 是在 Embedding 基础上进一步优化的工具,其核心思想是将每个输入序列的嵌入向量进行合并,能够处理可变长度的输入序列,并且减少了计算和存储的开销,并且可以计算句子中所有词汇的词嵌入向量的均值或总和。

在PyTorch中, EmbeddingBag 的输入是一个 整数张量 和一个 偏移量张量 ,每个整数都代表着一个词汇的索引,偏移量则表示句子中每个词汇的位置,输出是一个浮点型的张量,每个浮点数都代表着对应句子的词嵌入向量的均值或总和。

  • 输入shape: [seqSize] (seqSize为单个文本长度)
  • 输出shape: [batch, embed_dim] (embed_dim嵌入维度)

假定原始输入数据为: [[1,1,1,1],[2,2,2],[3,3]]

1. 输入:

  • 输入是一个展平的词汇索引张量( input ),例如 [2,2,2,1,1,1,1] 。
  • 对应的偏移量( offsets ),例如 [0,3] ,表示每个样本在展平张量中的起始位置。

2. 合并操作:

  • 根据偏移量,将嵌入向量进行合并操作。
  • 合并操作可以是求和、平均或取最大值,默认是平均 ( mean )。

函数原型:

torch.nn.EmbeddingBag(
    num_embeddings=*,
    embedding_dim,
    max_norm=None,
    norm_type=2.0,
    scale_grad_by_freq=False,
    mode='mean',
    sparse=False,
    _weight=None,
    include_last_offset=False,
    padding_idx=None,
    device=None,
    dtype=None
)

主要参数:

  • num_embeddings ( int ):词典的大小。
  • embedding_dim ( int ):每个词向量的维度,即嵌入向量的长度。
  • mode ( str ):指定嵌入向量的聚合方式。可选值为 ‘sum’ 、 ‘mean’ 和 ‘max’ 。
    • (假设有一个序列 [2,3,1] ,每个数字表示一个离散特征的索引,对应的嵌入向量分别为 [[0.1,0.2,0.3],[0.2,0.3,0.4],[0.3,0.4,0.5]] )
    • ‘sum’ :对所有的嵌入向量求和,则使用 ‘sum’ 模式汇总后的嵌入向量为 [0.6,0.9,1.2] 。
    • ‘mean’ :对所有的嵌入向量求平均值,使用 ‘mean’ 模式汇总后的嵌入向量为 [0.2,0.3,0.4] 。
    • ‘max’ :对所有的嵌入向量求最大值,使用 ‘max’ 模式汇总后的嵌入向量为 [0.3,0.4,0.5] 。

下面是一个简单的例子,用 EmbeddingBag 将两个句子转换为词嵌入向量并计算它们的均值。

先放上上周的数据集构建的代码,不清楚的同学可以回过头看看上周的代码。

2.1 自定义数据集类

import torch
from torch import nn
import torch.nn.functional as F
import torch.optim as optim
from torch.utils.data import DataLoader,Dataset

class MyDataset(Dataset):
    def __init__(self,texts,labels):
        self.texts=texts
        self.labels=labels
        
    def __len__(self):
        return len(self.labels)
    
    def __getitem__(self,idx):
        texts=self.texts[idx]
        labels=self.labels[idx]
        
        return texts,labels

2.2 准备数据和数据加载器

# 假设我们有以下三个样本,分别由不同数量的单词索引组成
text_data=[
    torch.tensor([1,1,1,1],dtype=torch.long),  # 样本1
    torch.tensor([2,2,2],dtype=torch.long),  # 样本2
    torch.tensor([3,3],dtype=torch.long)   # 样本3
]

# 对应的标签
labels=torch.tensor([4,5,6],dtype=torch.float)

# 创建数据集和数据加载器
my_dataset=MyDataset(text_data,labels)
data_loader=DataLoader(my_dataset,batch_size=2,shuffle=True,collate_fn=lambda x:x)

for batch in data_loader:
    print(batch)

2.3 定义模型

class EmbeddingBagModel(nn.Module):
    def __init__(self,vocab_size,embed_dim):
        super(EmbeddingBagModel,self).__init__()
        self.embedding_bag=nn.EmbeddingBag(vocab_size,embed_dim,mode='mean')
        self.fc=nn.Linear(embed_dim,1)
        
    def forward(self,text,offsets):
        print("embedding_bag输入文本是:",text)
        print("embedding_bag输入文本shape:",text.shape)
        
        embedded=self.embedding_bag(text,offsets)
        print("embedding_bag输出文本shape:",embedded.shape)
        return self.fc(embedded)

2.4 训练模型

# 示例词典大小和嵌入维度
vocab_size=10
embed_dim=6

# 创建模型实例
model=EmbeddingBagModel(vocab_size,embed_dim)

# 定义一个简单的损失函数和优化器
criterion=nn.BCEWithLogitsLoss()
optimizer=optim.SGD(model.parameters(),lr=0.01)

# 训练模型
for epoch in range(1): # 训练1个epoch
    for batch in data_loader:
        # 将批处理的数据展平并计算偏移量
        texts,labels=zip(*batch)
        
        offsets=[0]+[len(text) for text in texts[:-1]]
        offsets=torch.tensor(offsets).cumsum(dim=0)
        texts=torch.cat(texts)
        labels=torch.tensor(labels).unsqueeze(1)
        
        # 前向传播
        outputs=model(texts,offsets)
        loss=criterion(outputs,labels)
        
        # 反向传播和优化
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        
    print(f'Epoch {epoch+1},Loss:{loss.item()}')

代码输出:

embedding_bag输入文本是: tensor([2, 2, 2, 1, 1, 1, 1])
embedding_bag输入文本shape: torch.Size([7])
embedding_bag输出文本shape: torch.Size([2, 6])
embedding_bag输入文本是: tensor([3, 3])
embedding_bag输入文本shape: torch.Size([2])
embedding_bag输出文本shape: torch.Size([1, 6])
Epoch 1,Loss:-2.8094725608825684