From 39f3accacec2b3649f8c09cb1889c86fceab4689 Mon Sep 17 00:00:00 2001 From: yh Date: Fri, 12 Jul 2019 13:27:29 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0Embedding=E7=9A=84=E6=96=87?= =?UTF-8?q?=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/vocabulary.py | 9 ++- fastNLP/embeddings/__init__.py | 5 +- fastNLP/embeddings/bert_embedding.py | 28 +++++--- fastNLP/embeddings/char_embedding.py | 32 ++++++--- fastNLP/embeddings/elmo_embedding.py | 23 +++++-- fastNLP/embeddings/embedding.py | 49 +++++++++----- fastNLP/embeddings/stack_embedding.py | 4 +- fastNLP/embeddings/static_embedding.py | 85 +++++++++++++++++------- fastNLP/embeddings/utils.py | 4 +- fastNLP/modules/encoder/bert.py | 22 +++--- fastNLP/modules/utils.py | 18 ++++- test/embeddings/test_static_embedding.py | 15 +++++ 12 files changed, 210 insertions(+), 84 deletions(-) create mode 100644 test/embeddings/test_static_embedding.py diff --git a/fastNLP/core/vocabulary.py b/fastNLP/core/vocabulary.py index e4cb2546..9ce59a8c 100644 --- a/fastNLP/core/vocabulary.py +++ b/fastNLP/core/vocabulary.py @@ -108,6 +108,7 @@ class Vocabulary(object): """ self._add_no_create_entry(word_lst, no_create_entry) self.word_count.update(word_lst) + return self @_check_build_status def add(self, word, no_create_entry=False): @@ -124,6 +125,7 @@ class Vocabulary(object): """ self._add_no_create_entry(word, no_create_entry) self.word_count[word] += 1 + return self def _add_no_create_entry(self, word, no_create_entry): """ @@ -170,6 +172,7 @@ class Vocabulary(object): 则这个词将认为是需要创建单独的vector的。 """ self.update(word_lst, no_create_entry=no_create_entry) + return self def build_vocab(self): """ @@ -194,13 +197,15 @@ class Vocabulary(object): self.word2idx.update({w: i + start_idx for i, (w, _) in enumerate(words)}) self.build_reverse_vocab() self.rebuild = False - + return self + def build_reverse_vocab(self): """ 基于 `word to index` dict, 构建 `index to word` dict. """ self.idx2word = {i: w for w, i in self.word2idx.items()} + return self @_check_build_vocab def __len__(self): @@ -286,6 +291,7 @@ class Vocabulary(object): raise e else: raise RuntimeError("Only DataSet type is allowed.") + return self @property def _no_create_word_length(self): @@ -416,6 +422,7 @@ class Vocabulary(object): self.idx2word = None self.rebuild = True self._no_create_word.clear() + return self def __getstate__(self): """Use to prepare data for pickle. diff --git a/fastNLP/embeddings/__init__.py b/fastNLP/embeddings/__init__.py index 006b2c64..d378a67f 100644 --- a/fastNLP/embeddings/__init__.py +++ b/fastNLP/embeddings/__init__.py @@ -1,5 +1,8 @@ """ -embeddings 模块里实现了 +embeddings 模块主要用于从各种预训练的模型中获取词语的分布式表示,目前支持的预训练模型包括word2vec, glove, ELMO, BERT等。这里所有 +embedding的forward输入都是形状为(batch_size, max_len)的torch.LongTensor,输出都是(batch_size, max_len, embedding_dim)的 +torch.FloatTensor。所有的embedding都可以使用num_embedding获取最大的输入index范围, 用embedding_dim或embed_size获取embedding的 +输出维度。 """ __all__ = [ diff --git a/fastNLP/embeddings/bert_embedding.py b/fastNLP/embeddings/bert_embedding.py index 27de3eda..e0a677f2 100644 --- a/fastNLP/embeddings/bert_embedding.py +++ b/fastNLP/embeddings/bert_embedding.py @@ -17,24 +17,36 @@ class BertEmbedding(ContextualEmbedding): """ 别名::class:`fastNLP.embeddings.BertEmbedding` :class:`fastNLP.embeddings.bert_embedding.BertEmbedding` - 使用BERT对words进行encode的Embedding。建议将输入的words长度限制在450以内,而不要使用512。这是由于预训练的bert模型长 - 度限制为512个token,而因为输入的word是未进行word piece分割的,在分割之后长度可能会超过最大长度限制。 + 使用BERT对words进行编码的Embedding。建议将输入的words长度限制在430以内,而不要使用512(根据预训练模型参数,可能有变化)。这是由于 + 预训练的bert模型长度限制为512个token,而因为输入的word是未进行word piece分割的(word piece的分割有BertEmbedding在输入word + 时切分),在分割之后长度可能会超过最大长度限制。 - Example:: + BertEmbedding可以支持自动下载权重,当前支持的模型有以下的几种(待补充): - >>> embedding = BertEmbedding(vocab, model_dir_or_name='en-base-uncased', requires_grad=False, layers='4,-2,-1') + Example:: + >>> import torch + >>> from fastNLP import Vocabulary + >>> vocab = Vocabulary().add_word_lst("The whether is good .".split()) + >>> embed = BertEmbedding(vocab, model_dir_or_name='en-base-uncased', requires_grad=False, layers='4,-2,-1') + >>> words = torch.LongTensor([[vocab.to_index(word) for word in "The whether is good .".split()]]) + >>> outputs = embed(words) + >>> outputs.size() + >>> # torch.Size([1, 5, 2304]) :param fastNLP.Vocabulary vocab: 词表 - :param str model_dir_or_name: 模型所在目录或者模型的名称。默认值为 ``en-base-uncased``. - :param str layers:最终结果中的表示。以','隔开层数,可以以负数去索引倒数几层 + :param str model_dir_or_name: 模型所在目录或者模型的名称。当传入模型所在目录时,目录中应该包含一个词表文件(以.txt作为后缀名), + 权重文件(以.bin作为文件后缀名), 配置文件(以.json作为后缀名)。 + :param str layers:输出embedding表示来自于哪些层,不同层的结果按照layers中的顺序在最后一维concat起来。以','隔开层数,可以以负数 + 去索引倒数几层。 :param str pool_method: 因为在bert中,每个word会被表示为多个word pieces, 当获取一个word的表示的时候,怎样从它的word pieces 中计算得到它对应的表示。支持``last``, ``first``, ``avg``, ``max``。 :param float word_dropout: 以多大的概率将一个词替换为unk。这样既可以训练unk也是一定的regularize。 :param float dropout: 以多大的概率对embedding的表示进行Dropout。0.1即随机将10%的值置为0。 :param bool include_cls_sep: bool,在bert计算句子的表示的时候,需要在前面加上[CLS]和[SEP], 是否在结果中保留这两个内容。 这样 - 会使得word embedding的结果比输入的结果长两个token。在使用 :class::StackEmbedding 可能会遇到问题。 - :param bool requires_grad: 是否需要gradient。 + 会使得word embedding的结果比输入的结果长两个token。如果该值为True,则在使用 :class::StackEmbedding 可能会与其它类型的 + embedding长度不匹配。 + :param bool requires_grad: 是否需要gradient以更新Bert的权重。 """ def __init__(self, vocab: Vocabulary, model_dir_or_name: str='en-base-uncased', layers: str='-1', pool_method: str='first', word_dropout=0, dropout=0, requires_grad: bool=False, diff --git a/fastNLP/embeddings/char_embedding.py b/fastNLP/embeddings/char_embedding.py index 76297219..6ba8a197 100644 --- a/fastNLP/embeddings/char_embedding.py +++ b/fastNLP/embeddings/char_embedding.py @@ -1,3 +1,8 @@ +""" +该文件中主要包含的是character的Embedding,包括基于CNN与LSTM的character Embedding。与其它Embedding一样,这里的Embedding输入也是 +词的index而不需要使用词语中的char的index来获取表达。 +""" + import torch import torch.nn as nn @@ -14,19 +19,23 @@ class CNNCharEmbedding(TokenEmbedding): """ 别名::class:`fastNLP.embeddings.CNNCharEmbedding` :class:`fastNLP.embeddings.char_embedding.CNNCharEmbedding` - 使用CNN生成character embedding。CNN的结果为, embed(x) -> Dropout(x) -> CNN(x) -> activation(x) -> pool -> fc -> Dropout. - 不同的kernel大小的fitler结果是concat起来的。 + 使用CNN生成character embedding。CNN的结构为, embed(x) -> Dropout(x) -> CNN(x) -> activation(x) -> pool -> fc -> Dropout. + 不同的kernel大小的fitler结果是concat起来然后通过一层fully connected layer, 然后输出word的表示。 Example:: - >>> cnn_char_embed = CNNCharEmbedding(vocab) - + >>> vocab = Vocabulary().add_word_lst("The whether is good .".split()) + >>> embed = CNNCharEmbedding(vocab, embed_size=50) + >>> words = torch.LongTensor([[vocab.to_index(word) for word in "The whether is good .".split()]]) + >>> outputs = embed(words) + >>> outputs.size() + >>> # torch.Size([1, 5,50]) :param vocab: 词表 :param embed_size: 该word embedding的大小,默认值为50. :param char_emb_size: character的embed的大小。character是从vocab中生成的。默认值为50. :param float word_dropout: 以多大的概率将一个词替换为unk。这样既可以训练unk也是一定的regularize。 - :param float dropout: 以多大的概率drop + :param float dropout: 以多大的概率drop分布式表示与char embedding的输出。 :param filter_nums: filter的数量. 长度需要和kernels一致。默认值为[40, 30, 20]. :param kernel_sizes: kernel的大小. 默认值为[5, 3, 1]. :param pool_method: character的表示在合成一个表示时所使用的pool方法,支持'avg', 'max'. @@ -154,19 +163,24 @@ class LSTMCharEmbedding(TokenEmbedding): """ 别名::class:`fastNLP.embeddings.LSTMCharEmbedding` :class:`fastNLP.embeddings.char_embedding.LSTMCharEmbedding` - 使用LSTM的方式对character进行encode. embed(x) -> Dropout(x) -> LSTM(x) -> activation(x) -> pool + 使用LSTM的方式对character进行encode. embed(x) -> Dropout(x) -> LSTM(x) -> activation(x) -> pool -> Dropout Example:: - >>> lstm_char_embed = LSTMCharEmbedding(vocab) + >>> vocab = Vocabulary().add_word_lst("The whether is good .".split()) + >>> embed = LSTMCharEmbedding(vocab, embed_size=50) + >>> words = torch.LongTensor([[vocab.to_index(word) for word in "The whether is good .".split()]]) + >>> outputs = embed(words) + >>> outputs.size() + >>> # torch.Size([1, 5,50]) :param vocab: 词表 :param embed_size: embedding的大小。默认值为50. :param char_emb_size: character的embedding的大小。默认值为50. :param float word_dropout: 以多大的概率将一个词替换为unk。这样既可以训练unk也是一定的regularize。 - :param dropout: 以多大概率drop + :param dropout: 以多大概率drop character embedding的输出以及最终的word的输出。 :param hidden_size: LSTM的中间hidden的大小,如果为bidirectional的,hidden会除二,默认为50. - :param pool_method: 支持'max', 'avg' + :param pool_method: 支持'max', 'avg'。 :param activation: 激活函数,支持'relu', 'sigmoid', 'tanh', 或者自定义函数. :param min_char_freq: character的最小出现次数。默认值为2. :param bidirectional: 是否使用双向的LSTM进行encode。默认值为True。 diff --git a/fastNLP/embeddings/elmo_embedding.py b/fastNLP/embeddings/elmo_embedding.py index f669d121..f0850237 100644 --- a/fastNLP/embeddings/elmo_embedding.py +++ b/fastNLP/embeddings/elmo_embedding.py @@ -17,18 +17,27 @@ class ElmoEmbedding(ContextualEmbedding): """ 别名::class:`fastNLP.modules.ElmoEmbedding` :class:`fastNLP.modules.encoder.embedding.ElmoEmbedding` - 使用ELMo的embedding。初始化之后,只需要传入words就可以得到对应的embedding。 - 我们提供的ELMo预训练模型来自 https://github.com/HIT-SCIR/ELMoForManyLangs + 使用ELMo的embedding。初始化之后,只需要传入words就可以得到对应的embedding。当前支持的使用名称初始化的模型有以下的这些(待补充) Example:: - - >>> embedding = ElmoEmbedding(vocab, model_dir_or_name='en', layers='2', requires_grad=True) + >>> vocab = Vocabulary().add_word_lst("The whether is good .".split()) + >>> # 使用不同层的concat的结果 + >>> embed = ElmoEmbedding(vocab, model_dir_or_name='en', layers='1,2', requires_grad=False) + >>> words = torch.LongTensor([[vocab.to_index(word) for word in "The whether is good .".split()]]) + >>> outputs = embed(words) + >>> outputs.size() + >>> # torch.Size([1, 5, 2048]) + + >>> # 使用不同层的weighted sum。 + >>> embed = ElmoEmbedding(vocab, model_dir_or_name='en', layers='mix', requires_grad=False) + >>> embed.set_mix_weights_requires_grad() # 使得weighted的权重是可以学习的,但ELMO的LSTM部分是不更新 :param vocab: 词表 - :param model_dir_or_name: 可以有两种方式调用预训练好的ELMo embedding:第一种是传入ELMo权重的文件名,第二种是传入ELMo版本的名称, - 目前支持的ELMo包括{`en` : 英文版本的ELMo, `cn` : 中文版本的ELMo,}。第二种情况将自动查看缓存中是否存在该模型,没有的话将自动下载 + :param model_dir_or_name: 可以有两种方式调用预训练好的ELMo embedding:第一种是传入ELMo所在文件夹,该文件夹下面应该有两个文件, + 其中一个是以json为后缀的配置文件,另一个是以pkl为后缀的权重文件;第二种是传入ELMo版本的名称,将自动查看缓存中是否存在该模型, + 没有的话将自动下载并缓存。 :param layers: str, 指定返回的层数, 以,隔开不同的层。如果要返回第二层的结果'2', 返回后两层的结果'1,2'。不同的层的结果 - 按照这个顺序concat起来。默认为'2'。'mix'会使用可学习的权重结合不同层的表示(权重是否可训练与requires_grad保持一致, + 按照这个顺序concat起来,默认为'2'。'mix'会使用可学习的权重结合不同层的表示(权重是否可训练与requires_grad保持一致, 初始化权重对三层结果进行mean-pooling, 可以通过ElmoEmbedding.set_mix_weights_requires_grad()方法只将mix weights设置为可学习。) :param requires_grad: bool, 该层是否需要gradient, 默认为False. :param float word_dropout: 以多大的概率将一个词替换为unk。这样既可以训练unk也是一定的regularize。 diff --git a/fastNLP/embeddings/embedding.py b/fastNLP/embeddings/embedding.py index 1ac1df3b..4667bfff 100644 --- a/fastNLP/embeddings/embedding.py +++ b/fastNLP/embeddings/embedding.py @@ -1,3 +1,8 @@ +""" +该模块中的Embedding主要用于随机初始化的embedding(更推荐使用 :class: StaticEmbedding),或按照预训练权重初始化Embedding。 + +""" + import torch.nn as nn from abc import abstractmethod @@ -10,18 +15,26 @@ class Embedding(nn.Module): """ 别名::class:`fastNLP.embeddings.Embedding` :class:`fastNLP.embeddings.embedding.Embedding` - Embedding组件. 可以通过self.num_embeddings获取词表大小; self.embedding_dim获取embedding的维度""" - + 词向量嵌入,支持输入多种方式初始化. 可以通过self.num_embeddings获取词表大小; self.embedding_dim获取embedding的维度. + + Example:: + + >>> import numpy as np + >>> init_embed = (2000, 100) + >>> embed = Embedding(init_embed) # 随机初始化一个具有2000个词,每个词表示为100维的词向量 + >>> init_embed = np.zeros((2000, 100)) + >>> embed = Embedding(init_embed) # 使用numpy.ndarray的值作为初始化值初始化一个Embedding + + :param tuple(int,int),torch.FloatTensor,nn.Embedding,numpy.ndarray init_embed: 支持传入Embedding的大小(传入tuple(int, int), + 第一个int为vocab_zie, 第二个int为embed_dim); 或传入Tensor, Embedding, numpy.ndarray等则直接使用该值初始化Embedding; + :param float word_dropout: 按照一定概率随机将word设置为unk_index,这样可以使得unk这个token得到足够的训练, 且会对网络有 + 一定的regularize的作用。设置该值时,必须同时设置unk_index + :param float dropout: 对Embedding的输出的dropout。 + :param int unk_index: drop word时替换为的index。fastNLP的Vocabulary的unk_index默认为1。 + """ + def __init__(self, init_embed, word_dropout=0, dropout=0.0, unk_index=None): - """ - :param tuple(int,int),torch.FloatTensor,nn.Embedding,numpy.ndarray init_embed: Embedding的大小(传入tuple(int, int), - 第一个int为vocab_zie, 第二个int为embed_dim); 如果为Tensor, Embedding, ndarray等则直接使用该值初始化Embedding; - :param float word_dropout: 按照一定概率随机将word设置为unk_index,这样可以使得unk这个token得到足够的训练, 且会对网络有 - 一定的regularize的作用。 - :param float dropout: 对Embedding的输出的dropout。 - :param int unk_index: drop word时替换为的index。fastNLP的Vocabulary的unk_index默认为1。 - """ super(Embedding, self).__init__() self.embed = get_embeddings(init_embed) @@ -37,17 +50,17 @@ class Embedding(nn.Module): self.unk_index = unk_index self.word_dropout = word_dropout - def forward(self, x): + def forward(self, words): """ - :param torch.LongTensor x: [batch, seq_len] + :param torch.LongTensor words: [batch, seq_len] :return: torch.Tensor : [batch, seq_len, embed_dim] """ if self.word_dropout>0 and self.training: - mask = torch.ones_like(x).float() * self.word_dropout + mask = torch.ones_like(words).float() * self.word_dropout mask = torch.bernoulli(mask).byte() # dropout_word越大,越多位置为1 - x = x.masked_fill(mask, self.unk_index) - x = self.embed(x) - return self.dropout(x) + words = words.masked_fill(mask, self.unk_index) + words = self.embed(words) + return self.dropout(words) @property def num_embedding(self)->int: @@ -96,6 +109,8 @@ class Embedding(nn.Module): class TokenEmbedding(nn.Module): def __init__(self, vocab, word_dropout=0.0, dropout=0.0): super(TokenEmbedding, self).__init__() + if vocab.rebuild: + vocab.build_vocab() assert vocab.padding is not None, "Vocabulary must have a padding entry." self._word_vocab = vocab self._word_pad_index = vocab.padding_idx @@ -176,5 +191,5 @@ class TokenEmbedding(nn.Module): return torch.Size(self.num_embedding, self._embed_size) @abstractmethod - def forward(self, *input): + def forward(self, words): raise NotImplementedError diff --git a/fastNLP/embeddings/stack_embedding.py b/fastNLP/embeddings/stack_embedding.py index e5c7c7a4..8091d598 100644 --- a/fastNLP/embeddings/stack_embedding.py +++ b/fastNLP/embeddings/stack_embedding.py @@ -14,10 +14,12 @@ class StackEmbedding(TokenEmbedding): Example:: + >>> from fastNLP import Vocabulary + >>> from fastNLP.embeddings import StaticEmbedding + >>> vocab = Vocabulary().add_word_lst("The whether is good .".split()) >>> embed_1 = StaticEmbedding(vocab, model_dir_or_name='en-glove-6b-50', requires_grad=True) >>> embed_2 = StaticEmbedding(vocab, model_dir_or_name='en-word2vec-300', requires_grad=True) - :param embeds: 一个由若干个TokenEmbedding组成的list,要求每一个TokenEmbedding的词表都保持一致 :param float word_dropout: 以多大的概率将一个词替换为unk。这样既可以训练unk也是一定的regularize。不同embedidng会在相同的位置 被设置为unknown。如果这里设置了dropout,则组成的embedding就不要再设置dropout了。 diff --git a/fastNLP/embeddings/static_embedding.py b/fastNLP/embeddings/static_embedding.py index c8778e35..7f3f82a8 100644 --- a/fastNLP/embeddings/static_embedding.py +++ b/fastNLP/embeddings/static_embedding.py @@ -9,37 +9,57 @@ import warnings from ..core.vocabulary import Vocabulary from ..io.file_utils import PRETRAIN_STATIC_FILES, _get_base_url, cached_path from .embedding import TokenEmbedding - +from ..modules.utils import _get_file_name_base_on_postfix class StaticEmbedding(TokenEmbedding): """ 别名::class:`fastNLP.embeddings.StaticEmbedding` :class:`fastNLP.embeddings.static_embedding.StaticEmbedding` - StaticEmbedding组件. 给定embedding的名称,根据vocab从embedding中抽取相应的数据。该Embedding可以就按照正常的embedding使用了 + StaticEmbedding组件. 给定预训练embedding的名称或路径,根据vocab从embedding中抽取相应的数据(只会将出现在vocab中的词抽取出来, + 如果没有找到,则会随机初始化一个值(但如果该word是被标记为no_create_entry的话,则不会单独创建一个值,而是会被指向unk的index))。 + 当前支持自动下载的预训练vector有以下的几种(待补充); Example:: - >>> embed = StaticEmbedding(vocab, model_dir_or_name='en-glove-6b-50') + >>> vocab = Vocabulary().add_word_lst("The whether is good .".split()) + >>> embed = StaticEmbedding(vocab, model_dir_or_name='en-glove-50') + + >>> vocab = Vocabulary().add_word_lst(["The", 'the', "THE"]) + >>> embed = StaticEmbedding(vocab, model_dir_or_name="en-glove-50", lower=True) + >>> # "the", "The", "THE"它们共用一个vector,且将使用"the"在预训练词表中寻找它们的初始化表示。 + >>> vocab = Vocabulary().add_word_lst(["The", "the", "THE"]) + >>> embed = StaticEmbedding(vocab, model_dir_or_name=None, embedding_dim=5, lower=True) + >>> words = torch.LongTensor([[vocab.to_index(word) for word in ["The", "the", "THE"]]]) + >>> embed(words) + >>> tensor([[[ 0.5773, 0.7251, -0.3104, 0.0777, 0.4849], + [ 0.5773, 0.7251, -0.3104, 0.0777, 0.4849], + [ 0.5773, 0.7251, -0.3104, 0.0777, 0.4849]]], + grad_fn=) # 每种word的输出是一致的。 :param vocab: Vocabulary. 若该项为None则会读取所有的embedding。 - :param model_dir_or_name: 可以有两种方式调用预训练好的static embedding:第一种是传入embedding的文件名,第二种是传入embedding - 的名称。目前支持的embedding包括{`en` 或者 `en-glove-840b-300` : glove.840B.300d, `en-glove-6b-50` : glove.6B.50d, - `en-word2vec-300` : GoogleNews-vectors-negative300}。第二种情况将自动查看缓存中是否存在该模型,没有的话将自动下载。 + :param model_dir_or_name: 可以有两种方式调用预训练好的static embedding:第一种是传入embedding文件夹(文件夹下应该只有一个 + 以.txt作为后缀的文件)或文件路径;第二种是传入embedding的名称,第二种情况将自动查看缓存中是否存在该模型,没有的话将自动下载。 + 如果输入为None则使用embedding_dim的维度随机初始化一个embedding。 + :param int embedding_dim: 随机初始化的embedding的维度,仅在model_dir_or_name为None时有效。 :param bool requires_grad: 是否需要gradient. 默认为True :param callable init_method: 如何初始化没有找到的值。可以使用torch.nn.init.*中各种方法。调用该方法时传入一个tensor对象。 :param bool lower: 是否将vocab中的词语小写后再和预训练的词表进行匹配。如果你的词表中包含大写的词语,或者就是需要单独 为大写的词语开辟一个vector表示,则将lower设置为False。 :param float word_dropout: 以多大的概率将一个词替换为unk。这样既可以训练unk也是一定的regularize。 :param float dropout: 以多大的概率对embedding的表示进行Dropout。0.1即随机将10%的值置为0。 - :param bool normailize: 是否对vector进行normalize,使得每个vector的norm为1。 + :param bool normalize: 是否对vector进行normalize,使得每个vector的norm为1。 """ - def __init__(self, vocab: Vocabulary, model_dir_or_name: str='en', requires_grad: bool=True, init_method=None, - lower=False, dropout=0, word_dropout=0, normalize=False): + def __init__(self, vocab: Vocabulary, model_dir_or_name: str='en', embedding_dim=100, requires_grad: bool=True, + init_method=None, lower=False, dropout=0, word_dropout=0, normalize=False): super(StaticEmbedding, self).__init__(vocab, word_dropout=word_dropout, dropout=dropout) # 得到cache_path - if model_dir_or_name.lower() in PRETRAIN_STATIC_FILES: + if model_dir_or_name is None: + assert embedding_dim>=1, "The dimension of embedding should be larger than 1." + embedding_dim = int(embedding_dim) + model_path = None + elif model_dir_or_name.lower() in PRETRAIN_STATIC_FILES: PRETRAIN_URL = _get_base_url('static') model_name = PRETRAIN_STATIC_FILES[model_dir_or_name] model_url = PRETRAIN_URL + model_name @@ -47,6 +67,8 @@ class StaticEmbedding(TokenEmbedding): # 检查是否存在 elif os.path.isfile(os.path.expanduser(os.path.abspath(model_dir_or_name))): model_path = model_dir_or_name + elif os.path.isdir(os.path.expanduser(os.path.abspath(model_dir_or_name))): + model_path = _get_file_name_base_on_postfix(model_dir_or_name, '.txt') else: raise ValueError(f"Cannot recognize {model_dir_or_name}.") @@ -64,8 +86,10 @@ class StaticEmbedding(TokenEmbedding): lowered_vocab._no_create_word[lowered_word] += 1 print(f"All word in vocab have been lowered. There are {len(vocab)} words, {len(lowered_vocab)} unique lowered " f"words.") - embedding = self._load_with_vocab(model_path, vocab=lowered_vocab, init_method=init_method, - normalize=normalize) + if model_path: + embedding = self._load_with_vocab(model_path, vocab=lowered_vocab, init_method=init_method) + else: + embedding = self._randomly_init_embed(len(vocab), embedding_dim, init_method) # 需要适配一下 if not hasattr(self, 'words_to_words'): self.words_to_words = torch.arange(len(lowered_vocab, )).long() @@ -83,8 +107,12 @@ class StaticEmbedding(TokenEmbedding): words_to_words[index] = self.words_to_words[lowered_vocab.to_index(word)] self.words_to_words = words_to_words else: - embedding = self._load_with_vocab(model_path, vocab=vocab, init_method=init_method, - normalize=normalize) + if model_path: + embedding = self._load_with_vocab(model_path, vocab=vocab, init_method=init_method) + else: + embedding = self._randomly_init_embed(len(vocab), embedding_dim, init_method) + if normalize: + embedding /= (torch.norm(embedding, dim=1, keepdim=True) + 1e-12) self.embedding = nn.Embedding(num_embeddings=embedding.shape[0], embedding_dim=embedding.shape[1], padding_idx=vocab.padding_idx, max_norm=None, norm_type=2, scale_grad_by_freq=False, @@ -92,6 +120,23 @@ class StaticEmbedding(TokenEmbedding): self._embed_size = self.embedding.weight.size(1) self.requires_grad = requires_grad + def _randomly_init_embed(self, num_embedding, embedding_dim, init_embed=None): + """ + + :param int num_embedding: embedding的entry的数量 + :param int embedding_dim: embedding的维度大小 + :param callable init_embed: 初始化方法 + :return: torch.FloatTensor + """ + embed = torch.zeros(num_embedding, embedding_dim) + + if init_embed is None: + nn.init.uniform_(embed, -np.sqrt(3/embedding_dim), np.sqrt(3/embedding_dim)) + else: + init_embed(embed) + + return embed + @property def requires_grad(self): """ @@ -113,7 +158,7 @@ class StaticEmbedding(TokenEmbedding): param.requires_grad = value def _load_with_vocab(self, embed_filepath, vocab, dtype=np.float32, padding='', unknown='', - normalize=True, error='ignore', init_method=None): + error='ignore', init_method=None): """ 从embed_filepath这个预训练的词向量中抽取出vocab这个词表的词的embedding。EmbedLoader将自动判断embed_filepath是 word2vec(第一行只有两个元素)还是glove格式的数据。 @@ -124,7 +169,6 @@ class StaticEmbedding(TokenEmbedding): :param dtype: 读出的embedding的类型 :param str padding: 词表中padding的token :param str unknown: 词表中unknown的token - :param bool normalize: 是否将每个vector归一化到norm为1 :param str error: `ignore` , `strict` ; 如果 `ignore` ,错误将自动跳过; 如果 `strict` , 错误将抛出。 这里主要可能出错的地方在于词表有空行或者词表出现了维度不一致。 :param init_method: 如何初始化没有找到的值。可以使用torch.nn.init.*中各种方法。默认使用torch.nn.init.zeros_ @@ -173,11 +217,7 @@ class StaticEmbedding(TokenEmbedding): else: matrix[index] = None - vectors = torch.zeros(len(matrix), dim) - if init_method: - init_method(vectors) - else: - nn.init.uniform_(vectors, -np.sqrt(3/dim), np.sqrt(3/dim)) + vectors = self._randomly_init_embed(len(matrix), dim, init_method) if vocab._no_create_word_length>0: if vocab.unknown is None: # 创建一个专门的unknown @@ -197,9 +237,6 @@ class StaticEmbedding(TokenEmbedding): if vec is not None: vectors[index] = vec - if normalize: - vectors /= (torch.norm(vectors, dim=1, keepdim=True) + 1e-12) - return vectors def forward(self, words): diff --git a/fastNLP/embeddings/utils.py b/fastNLP/embeddings/utils.py index 569d3cb4..f1480eb6 100644 --- a/fastNLP/embeddings/utils.py +++ b/fastNLP/embeddings/utils.py @@ -24,7 +24,9 @@ def _construct_char_vocab_from_vocab(vocab:Vocabulary, min_freq:int=1): def get_embeddings(init_embed): """ - 根据输入的init_embed生成nn.Embedding对象。 + 根据输入的init_embed返回Embedding对象。如果输入是tuple, 则随机初始化一个nn.Embedding; 如果输入是numpy.ndarray, 则按照ndarray + 的值将nn.Embedding初始化; 如果输入是torch.Tensor, 则按该值初始化nn.Embedding; 如果输入是fastNLP中的embedding将不做处理 + 返回原对象。 :param init_embed: 可以是 tuple:(num_embedings, embedding_dim), 即embedding的大小和每个词的维度;也可以传入 nn.Embedding 对象, 此时就以传入的对象作为embedding; 传入np.ndarray也行,将使用传入的ndarray作为作为Embedding初始 diff --git a/fastNLP/modules/encoder/bert.py b/fastNLP/modules/encoder/bert.py index 6d32ae74..7d5f6248 100644 --- a/fastNLP/modules/encoder/bert.py +++ b/fastNLP/modules/encoder/bert.py @@ -17,10 +17,13 @@ import os import torch from torch import nn -import glob import sys +from ..utils import _get_file_name_base_on_postfix + CONFIG_FILE = 'bert_config.json' +VOCAB_NAME = 'vocab.txt' + class BertConfig(object): @@ -437,23 +440,16 @@ class BertModel(nn.Module): def from_pretrained(cls, pretrained_model_dir, *inputs, **kwargs): state_dict = kwargs.get('state_dict', None) kwargs.pop('state_dict', None) - cache_dir = kwargs.get('cache_dir', None) kwargs.pop('cache_dir', None) - from_tf = kwargs.get('from_tf', False) kwargs.pop('from_tf', None) # Load config - config_file = os.path.join(pretrained_model_dir, CONFIG_FILE) + config_file = _get_file_name_base_on_postfix(pretrained_model_dir, '.json') config = BertConfig.from_json_file(config_file) # logger.info("Model config {}".format(config)) # Instantiate model. model = cls(config, *inputs, **kwargs) if state_dict is None: - files = glob.glob(os.path.join(pretrained_model_dir, '*.bin')) - if len(files)==0: - raise FileNotFoundError(f"There is no *.bin file in {pretrained_model_dir}") - elif len(files)>1: - raise FileExistsError(f"There are multiple *.bin files in {pretrained_model_dir}") - weights_path = files[0] + weights_path = _get_file_name_base_on_postfix(pretrained_model_dir, '.bin') state_dict = torch.load(weights_path, map_location='cpu') old_keys = [] @@ -833,16 +829,14 @@ class BertTokenizer(object): 给定path,直接读取vocab. """ - pretrained_model_name_or_path = os.path.join(model_dir, VOCAB_NAME) + pretrained_model_name_or_path = _get_file_name_base_on_postfix(model_dir, '.txt') print("loading vocabulary file {}".format(pretrained_model_name_or_path)) max_len = 512 - kwargs['max_len'] = min(kwargs.get('max_len', int(1e12)), max_len) + kwargs['max_len'] = min(kwargs.get('max_position_embeddings', int(1e12)), max_len) # Instantiate tokenizer. tokenizer = cls(pretrained_model_name_or_path, *inputs, **kwargs) return tokenizer -VOCAB_NAME = 'vocab.txt' - class _WordPieceBertModel(nn.Module): """ diff --git a/fastNLP/modules/utils.py b/fastNLP/modules/utils.py index 4a9e034d..dbae9c73 100644 --- a/fastNLP/modules/utils.py +++ b/fastNLP/modules/utils.py @@ -117,4 +117,20 @@ def get_dropout_mask(drop_p: float, tensor: torch.Tensor): mask_x = torch.ones_like(tensor) nn.functional.dropout(mask_x, p=drop_p, training=False, inplace=True) - return mask_x \ No newline at end of file + return mask_x + +import glob + +def _get_file_name_base_on_postfix(dir_path, postfix): + """ + 在dir_path中寻找后缀为postfix的文件. + :param dir_path: str, 文件夹 + :param postfix: 形如".bin", ".json"等 + :return: str,文件的路径 + """ + files = glob.glob(os.path.join(dir_path, '*' + postfix)) + if len(files) == 0: + raise FileNotFoundError(f"There is no file endswith *.{postfix} file in {dir_path}") + elif len(files) > 1: + raise FileExistsError(f"There are multiple *.{postfix} files in {dir_path}") + return os.path.join(dir_path, files[0]) \ No newline at end of file diff --git a/test/embeddings/test_static_embedding.py b/test/embeddings/test_static_embedding.py new file mode 100644 index 00000000..0c8fc739 --- /dev/null +++ b/test/embeddings/test_static_embedding.py @@ -0,0 +1,15 @@ +import unittest + +from fastNLP.embeddings import StaticEmbedding +from fastNLP import Vocabulary +import torch + +class TestRandomSameEntry(unittest.TestCase): + def test_same_vector(self): + vocab = Vocabulary().add_word_lst(["The", "the", "THE"]) + embed = StaticEmbedding(vocab, model_dir_or_name=None, embedding_dim=5, lower=True) + words = torch.LongTensor([[vocab.to_index(word) for word in ["The", "the", "THE"]]]) + words = embed(words) + embed_0 = words[0, 0] + for i in range(1, words.size(1)): + assert torch.sum(embed_0==words[0, i]).eq(len(embed_0))