当前位置:   article > 正文

Pytorch NLP入门3:用嵌入表示单词

Pytorch NLP入门3:用嵌入表示单词

初次编辑时间:2024/3/17;最后编辑时间:2024/3/17

本栏目链接:https://blog.csdn.net/qq_33345365/category_12597850.html

本人的其他栏目:

pytorch 基础的栏目链接:https://blog.csdn.net/qq_33345365/category_12591348.html

pytorch CV的栏目链接:https://blog.csdn.net/qq_33345365/category_12578430.html

用嵌入表示单词

1 嵌入 Embeddings

在我们之前的示例中,我们对长度为vocab_size的高维词袋向量进行操作,并且明确地将低维位置表示向量转换为稀疏的独热(one-hot)表示。

通过找到单词近似于其他单词的意义来理解单词的含义。这是通过取两个单词向量并分析向量中的单词如何经常一起使用来完成的。频率越高,单词之间的相关性和关系就越大。 训练词嵌入以在给定维度中找到单词之间的近似是我们将单词表示减少到低维度的方法。 嵌入向量作为单词的数值表示,并被用作输入到其他机器学习网络层。 嵌入向量成为词汇表中单词的存储查找表。 在本单元中,我们将继续探索新闻AG数据集。首先,让我们加载数据并从上一单元中获取一些定义。此外,我们将分配我们的训练和测试数据集;词汇表大小;以及我们词类的类别:世界、体育、商业和科技。

准备文件

wget -q https://raw.githubusercontent.com/MicrosoftDocs/pytorchfundamentals/main/nlp-pytorch/torchnlp.py

加载库

import torch
import torchtext
from torchtext.data import get_tokenizer
import numpy as np
from torchnlp import *
from torchinfo import summary
train_dataset, test_dataset, classes, vocab = load_dataset()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

2 处理可变的序列大小

在处理单词时,你会遇到长度不同的文本序列或句子。这可能在训练单词嵌入神经网络时造成问题。为了保持单词嵌入的一致性并提高训练性能,我们需要对标记化的数据集应用一些填充操作。这可以通过使用torch.nn.functional.pad来实现,在向量末尾的空索引处添加零值。

def padify(b):
    # b is the list of tuples of length batch_size
    #   - first element of a tuple = label, 
    #   - second = feature (text sequence)
    # build vectorized sequence
    v = [encode(x[1]) for x in b]
    # 首先,计算这个小批量中序列的最大长度
    l = max(map(len,v))
    return ( # 张量的元组 - 标签和特征
        torch.LongTensor([t[0]-1 for t in b]),
        torch.stack([torch.nn.functional.pad(torch.tensor(t),(0,l-len(t)),mode='constant',value=0) for t in v])
    )
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

让我们以前两个句子为例来查看文本长度差异和填充效果。

first_sentence = train_dataset[0][1]
second_sentence = train_dataset[1][1]

f_tokens = encode(first_sentence)
s_tokens = encode(second_sentence)

print(f'First Sentence in dataset:\n{first_sentence}')
print("Length:", len(train_dataset[0][1]))
print(f'\nSecond Sentence in dataset:\n{second_sentence}')
print("Length: ", len(train_dataset[1][1]))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

输出是:

First Sentence in dataset:
Wall St. Bears Claw Back Into the Black (Reuters) Reuters - Short-sellers, Wall Street's dwindling\band of ultra-cynics, are seeing green again.
Length: 144

Second Sentence in dataset:
Carlyle Looks Toward Commercial Aerospace (Reuters) Reuters - Private investment firm Carlyle Group,\which has a reputation for making well-timed and occasionally\controversial plays in the defense industry, has quietly placed\its bets on another part of the market.
Length:  266
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

我们将使用数据集中新闻标题的文本序列,将其转换为分词向量。正如您将看到的那样,文本序列的长度不同。我们将应用填充(padding)的方法,以使所有文本序列具有固定的长度。这种方法在您的数据集中有大量文本序列时使用。

  • 第一句和第二句的长度不同。
  • 数据集张量的最大长度是整个数据集中最长句子的长度。
  • 在张量中的空白索引处添加零。
vocab_size = len(vocab)
labels, features = padify(train_dataset)  
print(f'features: {features}')

print(f'\nlength of first sentence: {len(f_tokens)}')
print(f'length of second sentence: {len(s_tokens)}')
print(f'size of features: {features.size()}')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

输出:

features: tensor([[  432,   426,     2,  ...,     0,     0,     0],
        [15875,  1073,   855,  ...,     0,     0,     0],
        [   59,     9,   348,  ...,     0,     0,     0],
        ...,
        [ 7736,    63,   665,  ...,     0,     0,     0],
        [   97,    17,    10,  ...,     0,     0,     0],
        [ 2155,   223,  2405,  ...,     0,     0,     0]])

length of first sentence: 29
length of second sentence: 42
size of features: torch.Size([120000, 207])
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

3 什么是嵌入 What is embedding

嵌入(Embedding)的概念是将单词映射到向量中,这反映了单词的语义含义。其向量的长度是嵌入维度的大小。我们稍后会讨论如何构建有意义的单词嵌入,但现在让我们将嵌入视为降低单词向量维度的一种方式。

因此,嵌入层会接受一个单词作为输入,并产生一个指定嵌入尺寸的输出向量。在某种意义上,它与线性层非常相似,但它不是接受独热编码向量作为输入,而是可以接受单词编号作为输入。

通过在网络中使用嵌入层作为第一层,我们可以从基于词袋(bag-of-words)的模型切换到嵌入包(embedding bag)模型,在这种模型中,我们首先将文本中的每个单词转换为相应的嵌入,然后计算所有这些嵌入的一些聚合函数,如sumaveragemax

神经网络分类器将从嵌入层开始,然后是聚合层,最后是线性分类器:

  • vocab_size 表示我们词汇表中单词的总数。
  • embed_dim 表示用于表示单词之间关系的词向量的长度。
  • num_class 表示我们尝试分类的新闻类别数量(例如世界新闻、体育新闻、商业新闻、科技新闻)。
class EmbedClassifier(torch.nn.Module):
    def __init__(self, vocab_size, embed_dim, num_class):
        super().__init__()
        self.embedding = torch.nn.Embedding(vocab_size, embed_dim)
        self.fc = torch.nn.Linear(embed_dim, num_class)

    def forward(self, x):
        x = self.embedding(x)
        x = torch.mean(x,dim=1)
        return self.fc(x)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

4 训练嵌入分类器

现在我们将定义我们的训练数据加载器,并使用collate_fn将padify函数应用于每个批次加载的数据集。结果是,训练数据集将被填充。

train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=16, collate_fn=padify, shuffle=True)
  • 1

我们可以使用前一个单元中定义的训练函数来训练模型,以运行嵌入网络。训练输出作为基于词汇表中唯一索引标记的向量查找存储。

net = EmbedClassifier(vocab_size,32,len(classes)).to(device)
train_epoch(net,train_loader, lr=1, epoch_size=25000)
  • 1
  • 2

输出是:

3200: acc=0.64875
6400: acc=0.69234375
9600: acc=0.7110416666666667
12800: acc=0.72421875
16000: acc=0.73625
19200: acc=0.7476041666666666
22400: acc=0.7541964285714285

(0.9072840441058861, 0.7583173384516955)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

请注意:出于时间考虑,我们只对 25k 条记录进行训练(不到一个完整的周期),但您可以继续训练,编写一个函数来进行多个周期的训练,并尝试不同的学习率参数以达到更高的准确率。您应该能够将准确率提高到约 90%。

5 EmbeddingBag层和变长序列表示

在以前的架构中,我们需要将所有序列填充到相同的长度,以便将它们适应一个小批量。这并不是表示可变长度序列的最有效方式 - 另一种方法是使用偏移向量,该向量将存储在一个大向量中的所有序列的偏移量。

注意:在上面的图片中,我们展示了一个字符序列,但在我们的示例中,我们处理的是单词序列。然而,使用偏移向量表示序列的一般原则保持不变。

要使用偏移表示进行工作,我们使用PyTorch的EmbeddingBag层。它类似于Embedding,但它接受内容向量和偏移向量作为输入,并且还包括平均层,可以是meansummax

这里是修改后使用EmbeddingBag的网络:

class EmbedClassifier(torch.nn.Module):
    def __init__(self, vocab_size, embed_dim, num_class):
        super().__init__()
        self.embedding = torch.nn.EmbeddingBag(vocab_size, embed_dim)
        self.fc = torch.nn.Linear(embed_dim, num_class)

    def forward(self, text, off):
        x = self.embedding(text, off)
        return self.fc(x)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

为了准备训练数据集,我们需要提供一个转换函数来准备偏移量向量:

def offsetify(b):
    # first, compute data tensor from all sequences
    x = [torch.tensor(encode(t[1])) for t in b]
    # now, compute the offsets by accumulating the tensor of sequence lengths
    o = [0] + [len(t) for t in x]
    o = torch.tensor(o[:-1]).cumsum(dim=0)
    return ( 
        torch.LongTensor([t[0]-1 for t in b]), # labels
        torch.cat(x), # text 
        o
    )

train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=16, collate_fn=offsetify, shuffle=True)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

偏移向量是通过首先将句子索引合并为一个张量序列,然后提取每个句子在序列中的起始索引位置来计算的。例如:

  • 我们训练数据集中第一句话的长度为29。这意味着偏移的第一个索引将是0
  • 数据集中第二句话的长度为42。这意味着偏移的第二个索引将是29,即第一句话结束的位置。
  • 偏移的第三个索引将是29 + 42 = 71,即第二句话结束的位置。
labels, features, offset = offsetify(train_dataset)  
print(f'offset: {offset}')
print(f'\nlength of first sentence: {len(f_tokens)}')
print(f'length of second sentence: {len(s_tokens)}')
print(f'size of data vector: {features.size()}')
print(f'size of offset vector: {offset.size()}')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

输出是:

offset: tensor([      0,      29,      71,  ..., 5193441, 5193488, 5193569])

length of first sentence: 29
length of second sentence: 42
size of data vector: torch.Size([5193609])
size of offset vector: torch.Size([120000])
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

注意:与以往所有示例不同的是,我们的网络现在接受两个参数:数据向量和偏移向量,它们的大小不同。同样,我们的数据加载器也提供了3个值,而不是2个:文本和偏移向量都作为特征提供。因此,我们需要稍微调整我们的训练函数来处理这种情况。

net = EmbedClassifier(vocab_size,32,len(classes)).to(device)

def train_epoch_emb(net,dataloader,lr=0.01,optimizer=None,loss_fn = torch.nn.CrossEntropyLoss(),epoch_size=None, report_freq=200):
    optimizer = optimizer or torch.optim.Adam(net.parameters(),lr=lr)
    loss_fn = loss_fn.to(device)
    net.train()
    total_loss,acc,count,i = 0,0,0,0
    for labels,text,off in dataloader:
        optimizer.zero_grad()
        labels,text,off = labels.to(device), text.to(device), off.to(device)
        out = net(text, off)
        loss = loss_fn(out,labels) #cross_entropy(out,labels)
        loss.backward()
        optimizer.step()
        total_loss+=loss
        _,predicted = torch.max(out,1)
        acc+=(predicted==labels).sum()
        count+=len(labels)
        i+=1
        if i%report_freq==0:
            print(f"{count}: acc={acc.item()/count}")
        if epoch_size and count>epoch_size:
            break
    return total_loss.item()/count, acc.item()/count


train_epoch_emb(net,train_loader, lr=4, epoch_size=25000)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

输出是:

3200: acc=0.6496875
6400: acc=0.6853125
9600: acc=0.7097916666666667
12800: acc=0.725546875
16000: acc=0.737375
19200: acc=0.7446354166666667
22400: acc=0.7532589285714286
(22.52296015275112, 0.7591570697376839)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

6 语义嵌入:Word2Vec

Word2Vec是一种用于训练语义嵌入模型的技术,它有两种主要的架构:Continuous Bag-of-Words(CBoW)和Continuous Skip-gram。在CBoW架构中,模型通过周围的上下文来预测当前词语,而在Skip-gram架构中,则是通过当前词语来预测周围的上下文。这些预测性的嵌入技术仅考虑局部上下文,而不考虑全局上下文。

除了Word2Vec外,还有其他的词嵌入技术,如GloVe和FastText。GloVe是通过词对出现的频率来推断词语之间的关系,而FastText则在Word2Vec的基础上学习每个词语和词中字符n-gram的向量表示,然后将这些表示的值平均为一个向量。

Gensim是一个开源的NLP Python库,提供了构建词向量、语料库、进行主题识别等NLP任务的统一接口。

在Word2Vec的例子中,我们将使用预训练的语义嵌入模型,但了解如何使用FastText、CBoW或Skip-gram架构来训练这些嵌入向量也是很有趣的。这些练习超出了本模块的范围,但有兴趣的人可以参考Pytorch网站上关于Word Embeddings的教程。

7 Genim

gensim框架可以与Pytorch一起使用,只需几行代码就可以训练最常用的嵌入。为了使用在Google News数据集上预训练的word2vec嵌入进行实验,我们可以使用gensim库。以下是找到与’neural’最相似的单词:

**注意:**第一次创建单词向量时,下载可能需要一些时间!

import gensim.downloader as api
w2v = api.load('word2vec-google-news-300')
  • 1
  • 2

让我们来看看和’dog’相似的单词。

for w,p in w2v.most_similar('dog'):
    print(f"{w} -> {p}")
  • 1
  • 2

输出是:

dogs -> 0.8680489659309387
puppy -> 0.8106428384780884
pit_bull -> 0.780396044254303
pooch -> 0.7627377510070801
cat -> 0.7609456777572632
golden_retriever -> 0.7500902414321899
German_shepherd -> 0.7465174198150635
Rottweiler -> 0.7437614798545837
beagle -> 0.7418621778488159
pup -> 0.740691065788269
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

我们还可以从单词中提取向量嵌入,用于训练分类模型(为了清晰起见,我们只显示向量的前20个分量):

w2v.word_vec('play')[:20]
  • 1

输出是:

array([ 0.01226807,  0.06225586,  0.10693359,  0.05810547,  0.23828125,
        0.03686523,  0.05151367, -0.20703125,  0.01989746,  0.10058594,
       -0.03759766, -0.1015625 , -0.15820312, -0.08105469, -0.0390625 ,
       -0.05053711,  0.16015625,  0.2578125 ,  0.10058594, -0.25976562],
      dtype=float32)
  • 1
  • 2
  • 3
  • 4
  • 5

语义嵌入的伟大之处在于您可以操作向量编码以改变语义。例如,我们可以要求找到一个词,其向量表示与king和woman尽可能接近,但与man尽可能远。

w2v.most_similar(positive=['king','woman'],negative=['man'])[0]
  • 1

输出:

('queen', 0.7118192911148071)
  • 1

8 在PyTorch中使用预训练嵌入

我们可以修改上面的例子,将我们的嵌入层中的矩阵预先填充为语义嵌入,比如Word2Vec。我们需要考虑到预训练嵌入的词汇表是对我们已有的文本语料库的补充,因此它们可能不完全匹配。因此,我们将为缺失的单词初始化随机值的权重:

embed_size = len(w2v.get_vector('hello'))
print(f'Embedding size: {embed_size}')

net = EmbedClassifier(vocab_size,embed_size,len(classes))

print('Populating matrix, this will take some time...',end='')
found, not_found = 0,0
for i,w in enumerate(vocab.itos):
    try:
        net.embedding.weight[i].data = torch.tensor(w2v.get_vector(w))
        found+=1
    except:
        net.embedding.weight[i].data = torch.normal(0.0,1.0,(embed_size,))
        not_found+=1

print(f"Done, found {found} words, {not_found} words missing")
net = net.to(device)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

输出是:

Embedding size: 300
Populating matrix, this will take some time...Done, found 41080 words, 54732 words missing
  • 1
  • 2

现在让我们训练我们的模型。请注意,由于较大的嵌入层大小,模型训练所需的时间显着增加,因此参数数量也更多。因此,如果我们想要避免过拟合,可能需要在更多的示例上训练我们的模型。

train_epoch_emb(net,train_loader, lr=4, epoch_size=25000)
  • 1

输出是:

3200: acc=0.6409375
6400: acc=0.6875
9600: acc=0.7163541666666666
12800: acc=0.730859375
16000: acc=0.740875
19200: acc=0.7510416666666667
22400: acc=0.7584821428571429
(228.52019353806782, 0.7641154830454254)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

在我们的情况下,我们没有看到准确率的显著增加,这可能是因为词汇表有很大的差异。

为了解决不同词汇表的问题,我们可以使用以下其中一种解决方案:

  • 在我们的词汇表上重新训练Word2Vec模型
  • 使用预训练的Word2Vec模型的词汇表加载我们的数据集。在加载数据集时可以指定用于加载数据集的词汇表。

后一种方法似乎更容易,特别是因为PyTorch的torchtext框架内置了对嵌入的支持。

9 GloVe嵌入

为了从预训练的word2vec模型中加载词汇表,我们使用Glove嵌入。我们将以以下方式实例化基于手套的词汇:

vocab = torchtext.vocab.GloVe(name='6B', dim=50)
  • 1

加载的词汇表具有以下基本操作:

  • vocab.stoi 字典允许我们将单词转换为其字典索引
  • vocab.itos 则反之 - 将数字转换为单词
  • vocab.vectors 是嵌入向量的数组,因此要获取单词的嵌入,我们需要使用 vocab.vectors[vocab.stoi[s]]

这里是操纵嵌入向量的示例,以演示等式 kind-man+woman = queen(系数稍作调整以使其有效):

# get the vector corresponding to kind-man+woman
qvec = vocab.vectors[vocab.stoi['king']]-vocab.vectors[vocab.stoi['man']]+1.3*vocab.vectors[vocab.stoi['woman']]
# find the index of the closest embedding vector 
d = torch.sum((vocab.vectors-qvec)**2,dim=1)
min_idx = torch.argmin(d)
# find the corresponding word
vocab.itos[min_idx]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

输出是:

'queen'
  • 1

为了使用这些嵌入训练分类器,我们首先需要使用GloVe词汇对我们的数据集进行编码:

def offsetify(b):
    # first, compute data tensor from all sequences
    x = [torch.tensor(encode(t[1],voc=vocab)) for t in b] # pass the instance of vocab to encode function!
    # now, compute the offsets by accumulating the tensor of sequence lengths
    o = [0] + [len(t) for t in x]
    o = torch.tensor(o[:-1]).cumsum(dim=0)
    return ( 
        torch.LongTensor([t[0]-1 for t in b]), # labels
        torch.cat(x), # text 
        o
    )
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

正如我们之前所看到的,所有向量嵌入都存储在vocab.vectors矩阵中。这使得将这些权重加载到嵌入层的权重中变得非常容易,只需简单地进行复制即可。

net = EmbedClassifier(len(vocab),len(vocab.vectors[0]),len(classes))
net.embedding.weight.data = vocab.vectors
net = net.to(device)
  • 1
  • 2
  • 3

现在来训练我们的模型,看看是否能得到更好的结果:

train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=16, collate_fn=offsetify, shuffle=True)
train_epoch_emb(net,train_loader, lr=4, epoch_size=25000)
  • 1
  • 2

输出:

3200: acc=0.6359375
6400: acc=0.68203125
9600: acc=0.706875
12800: acc=0.727734375
16000: acc=0.738625
19200: acc=0.7465104166666666
22400: acc=0.7526785714285714
(35.71297184900832, 0.7573576455534229)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

我们没有看到准确率显著提高的一个原因是,我们数据集中的一些单词在预训练的GloVe词汇表中缺失,因此它们基本上被忽略了。为了克服这一问题,我们可以在我们的数据集上训练自己的嵌入模型。

10 Contextual Embeddings

传统的预训练嵌入表示(如Word2Vec)的一个关键局限性是词义问题和消除歧义的困难。虽然预训练嵌入可以捕捉词在上下文中的某些含义,但每个词的所有可能含义都被编码到同一个嵌入中。这会在下游模型中造成问题,因为像“play”这样的许多词根据使用的上下文不同而有不同的含义。

例如,这两个不同句子中的’play’有着不同的含义:

  • 我去剧院看了一场
  • 约翰想要和朋友

上述的预训练嵌入表示了’play’这个词的两种含义在同一个嵌入中。为了克服这个限制,我们需要基于语言模型构建嵌入,该模型经过大量文本语料的训练,知道如何将词在不同上下文中组合在一起。讨论上下文嵌入超出了本教程的范围,但在下一个单元中讨论语言模型时,我们将回到这个话题。

11 知识检测

假设文本语料库包含 80000 个不同的字词。 通常如何将输入向量的维数降低到神经分类器?

A. 随机选择 10% 的字词,并忽略其余字词

B. 使用卷积层,然后使用全连接分类器层

C. 使用嵌入层,然后使用全连接分类器层

D. 选择 10% 的最常用字词,并忽略其余字词

答案:C

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/盐析白兔/article/detail/362152
推荐阅读
相关标签
  

闽ICP备14008679号