fastNLP中的Vocabulary

Vocabulary 是包含字或词与index关系的类,用于将文本转换为index。

构建Vocabulary

from fastNLP import Vocabulary

vocab = Vocabulary()
vocab.add_word_lst(['复', '旦', '大', '学'])  # 加入新的字
vocab.add_word('上海')  # `上海`会作为一个整体
vocab.to_index('复')  # 应该会为3
vocab.to_index('我')  # 会输出1,Vocabulary中默认pad的index为0, unk(没有找到的词)的index为1

#  在构建target的Vocabulary时,词表中应该用不上pad和unk,可以通过以下的初始化
vocab = Vocabulary(unknown=None, padding=None)
vocab.add_word_lst(['positive', 'negative'])
vocab.to_index('positive')  # 输出0
vocab.to_index('neutral')  # 会报错,因为没有unk这种情况

除了通过以上的方式建立词表,Vocabulary还可以通过使用下面的函数直接从 DataSet 中的某一列建立词表以及将该列转换为index

from fastNLP import Vocabulary
from fastNLP import DataSet

dataset = DataSet({'chars': [
                                ['今', '天', '天', '气', '很', '好', '。'],
                                ['被', '这', '部', '电', '影', '浪', '费', '了', '两', '个', '小', '时', '。']
                            ],
                    'target': ['neutral', 'negative']
})

vocab = Vocabulary()
#  从该dataset中的chars列建立词表
vocab.from_dataset(dataset, field_name='chars')
#  使用vocabulary将chars列转换为index
vocab.index_dataset(dataset, field_name='chars')

target_vocab = Vocabulary(padding=None, unknown=None)
target_vocab.from_dataset(dataset, field_name='target')
target_vocab.index_dataset(dataset, field_name='target')
print(dataset)

输出内容为:

+---------------------------------------------------+--------+
|                       chars                       | target |
+---------------------------------------------------+--------+
|               [4, 2, 2, 5, 6, 7, 3]               |   0    |
| [8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 3] |   1    |
+---------------------------------------------------+--------+

一些使用tips

在使用from_dataset()函数建立词表时,将测试集和验证集放入参数no_create_entry_dataset中,如下所示

from fastNLP import Vocabulary
from fastNLP import DataSet

tr_data = DataSet({'chars': [
                                ['今', '天', '心', '情', '很', '好', '。'],
                                ['被', '这', '部', '电', '影', '浪', '费', '了', '两', '个', '小', '时', '。']
                            ],
                    'target': ['positive', 'negative']
})
dev_data = DataSet({'chars': [
                                ['住', '宿', '条', '件', '还', '不', '错'],
                                ['糟', '糕', '的', '天', '气', ',', '无', '法', '出', '行', '。']
                            ],
                    'target': ['positive', 'negative']
})

vocab = Vocabulary()
#  将验证集或者测试集在建立词表是放入no_create_entry_dataset这个参数中。
vocab.from_dataset(tr_data, field_name='chars', no_create_entry_dataset=[dev_data])

Vocabulary 中的 no_create_entry , 建议在添加来自于测试集和验证集的词的时候将该参数置为True, 或将验证集和测试集 传入 no_create_entry_dataset 参数。它们的意义是在接下来的模型会使用pretrain的embedding(包括glove, word2vec, elmo与bert)且会finetune的 情况下,如果仅使用来自于train的数据建立vocabulary,会导致只出现在test与dev中的词语无法充分利用到来自于预训练embedding的信息(因为他们 会被认为是unk),所以在建立词表的时候将test与dev考虑进来会使得最终的结果更好。

通过与fastNLP中的各种Embedding配合使用,会有如下的效果, 如果一个词出现在了train中,但是没在预训练模型中,embedding会为随机初始化,且它单独的一个vector,如果finetune embedding的话, 这个词在更新之后可能会有更好的表示; 而如果这个词仅出现在了dev或test中,那么就不能为它们单独建立vector,而应该让它指向unk这个vector的 值(当unk的值更新时,这个词也使用的是更新之后的vector)。所以被认为是no_create_entry的token,将首先从预训练的词表中寻找它的表示,如 果找到了,就使用该表示; 如果没有找到,则认为该词的表示应该为unk的表示。

下面我们结合部分 StaticEmbedding 的例子来说明下该值造成的影响,如果您对 StaticEmbedding 不太了解,您可以先参考 使用Embedding模块将文本转成向量 部分再来阅读该部分

import torch
from fastNLP.embeddings import StaticEmbedding
from fastNLP import Vocabulary

vocab = Vocabulary()
vocab.add_word('train')
vocab.add_word('only_in_train')  # 仅在train出现,但肯定在预训练词表中不存在
vocab.add_word('test', no_create_entry=True)  # 该词只在dev或test中出现
vocab.add_word('only_in_test', no_create_entry=True)  # 这个词在预训练的词表中找不到

embed = StaticEmbedding(vocab, model_dir_or_name='en-glove-6b-50d')
print(embed(torch.LongTensor([vocab.to_index('train')])))
print(embed(torch.LongTensor([vocab.to_index('only_in_train')])))
print(embed(torch.LongTensor([vocab.to_index('test')])))
print(embed(torch.LongTensor([vocab.to_index('only_in_test')])))
print(embed(torch.LongTensor([vocab.unknown_idx])))

输出结果(只截取了部分vector):

tensor([[ 0.9497,  0.3433,  0.8450, -0.8852, ...]], grad_fn=<EmbeddingBackward>)  # train,en-glove-6b-50d,找到了该词
tensor([[ 0.0540, -0.0557, -0.0514, -0.1688, ...]], grad_fn=<EmbeddingBackward>)  # only_in_train,en-glove-6b-50d,使用了随机初始化
tensor([[ 0.1318, -0.2552, -0.0679,  0.2619, ...]], grad_fn=<EmbeddingBackward>)  # test,在en-glove-6b-50d中找到了这个词
tensor([[0., 0., 0., 0., 0., ...]], grad_fn=<EmbeddingBackward>)   # only_in_test, en-glove-6b-50d中找不到这个词,使用unk的vector
tensor([[0., 0., 0., 0., 0., ...]], grad_fn=<EmbeddingBackward>)   # unk,使用zero初始化

首先train和test都能够从预训练中找到对应的vector,所以它们是各自的vector表示; only_in_train在预训练中找不到,StaticEmbedding为它 新建了一个entry,所以它有一个单独的vector; 而only_in_test在预训练中找不到改词,因此被指向了unk的值(fastNLP用零向量初始化unk),与最后一行unk的 表示相同。