第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