【问题标题】:NLP Transformers: Best way to get a fixed sentence embedding-vector shape?NLP Transformers:获得固定句子嵌入向量形状的最佳方法?
【发布时间】:2020-03-20 16:33:54
【问题描述】:

我正在从 torch hub 加载一个语言模型(CamemBERT 一个基于法国 RoBERTa 的模型)并使用它嵌入一些法语句子:

import torch
camembert = torch.hub.load('pytorch/fairseq', 'camembert.v0')
camembert.eval()  # disable dropout (or leave in train mode to finetune)


def embed(sentence):
   tokens = camembert.encode(sentence)
   # Extract all layer's features (layer 0 is the embedding layer)
   all_layers = camembert.extract_features(tokens, return_all_hiddens=True)
   embeddings = all_layers[0]
   return embeddings

# Here we see that the shape of the embedding vector depends on the number of tokens in the sentence

u = embed(sentence="Bonjour, ça va ?")
u.shape # torch.Size([1, 7, 768])
v = embed(sentence="Salut, comment vas-tu ?")
v.shape # torch.Size([1, 9, 768])

现在想象一下,为了进行一些语义搜索,我想计算向量(在我们的例子中是张量)uv 之间的 cosine distance

cos = torch.nn.CosineSimilarity(dim=1)
cos(u, v) # will throw an error since the shape of `u` is different from the shape of `v`

我在问,为了始终获得 相同的嵌入形状的句子不管其标记的数量,最好使用什么方法?

=> 我想到的第一个解决方案是计算mean on axis=1(句子的嵌入是嵌入其标记的平均值),因为axis=0 和axis=2 始终具有相同的大小:

cos = torch.nn.CosineSimilarity(dim=1)
cos(u.mean(axis=1), v.mean(axis=1)) # works now and gives 0.7269

但是,我担心在计算平均值时会损害句子的嵌入,因为它为每个标记赋予相同的权重(可能乘以 TF-IDF?)。

=> 第二种解决方案是将较短的句子填充出来。这意味着:

  • 一次提供要嵌入的句子列表(而不是逐句嵌入)
  • 查找具有最长标记的句子并将其嵌入,得到它的形状S
  • 对于嵌入的其余句子,然后填充零以获得相同的形状S(句子的其余维度为 0)

你的想法是什么? 您还会使用哪些其他技术以及为什么?

提前致谢!

【问题讨论】:

    标签: machine-learning deep-learning nlp pytorch word-embedding


    【解决方案1】:

    看看sentence-transformers。您的模型可以实现为:

    from sentence_transformers import SentenceTransformer
    word_embedding_model = models.CamemBERT('camembert-base')
    dim = word_embedding_model.get_word_embedding_dimension()
    pooling_model = models.Pooling(dim, pooling_mode_mean_tokens=True, pooling_mode_cls_token=False, pooling_mode_max_tokens=False)
    
    model = SentenceTransformer(modules=[word_embedding_model, pooling_model])
    sentences = ['sentence 1', 'sentence 3', 'sentence 3']
    sentence_embeddings = model.encode(sentences)
    
    

    benchmark section 中,您可以看到与几种嵌入方法的比较,例如我不推荐用于相似性任务的 Bert as a Service。此外,您可以为您的任务微调嵌入。

    尝试多语言模型也很有趣:

    model = SentenceTransformer('distiluse-base-multilingual-cased')
    model.encode([...])
    

    可能会比平均池化 CamemBert 产生更好的结果。

    【讨论】:

      【解决方案2】:

      这是一个相当笼统的问题,因为没有一个特定的正确答案。

      正如您所发现的,形状当然不同,因为每个标记都有一个输出(取决于标记器,这些可以是子字单元)。换句话说,您已将所有标记编码到它们自己的向量中。你想要的是一个句子嵌入,并且有很多方法可以得到这些(没有一个特别正确的答案)。

      特别是对于句子分类,我们经常在语言模型经过训练时使用特殊分类标记的输出(CamemBERT 使用<s>)。请注意,根据模型的不同,这可能是第一个(主要是 BERT 和孩子;还有 CamemBERT)或最后一个令牌(CTRL、GPT2、OpenAI、XLNet)。我建议在可用时使用此选项,因为该令牌正是为此目的而训练的。

      如果[CLS](或<s> 或类似)令牌不可用,则还有一些其他选项属于术语池。经常使用最大值和均值池。这意味着您采用最大值令牌或所有令牌的平均值。正如您所说,“危险”是您将整个句子的向量值减少到可能不是很能代表句子的“某个平均值”或“某个最大值”。然而,文献表明这也很有效。

      正如另一个答案所暗示的,您使用其输出的层也可以发挥作用。 IIRC 关于 BERT 的谷歌论文表明他们在连接最后四层时获得了最高分。这是更高级的,除非有要求,否则我不会在这里介绍。

      我没有使用 fairseq 的经验,但是使用 transformers 库,我会写这样的东西(CamemBERT 可在 v2.2.0 的库中使用):

      import torch
      from transformers import CamembertModel, CamembertTokenizer
      
      text = "Salut, comment vas-tu ?"
      
      tokenizer = CamembertTokenizer.from_pretrained('camembert-base')
      
      # encode() automatically adds the classification token <s>
      token_ids = tokenizer.encode(text)
      tokens = [tokenizer._convert_id_to_token(idx) for idx in token_ids]
      print(tokens)
      
      # unsqueeze token_ids because batch_size=1
      token_ids = torch.tensor(token_ids).unsqueeze(0)
      print(token_ids)
      
      # load model
      model = CamembertModel.from_pretrained('camembert-base')
      
      # forward method returns a tuple (we only want the logits)
      # squeeze() because batch_size=1
      output = model(token_ids)[0].squeeze()
      # only grab output of CLS token (<s>), which is the first token
      cls_out = output[0]
      print(cls_out.size())
      

      打印输出是(按顺序)标记化后的标记、标记 ID 和最终大小。

      ['<s>', '▁Salut', ',', '▁comment', '▁vas', '-', 'tu', '▁?', '</s>']
      tensor([[   5, 5340,    7,  404, 4660,   26,  744,  106,    6]])
      torch.Size([768])
      

      【讨论】:

      • 感谢您对我正在考虑的解决方案的反馈!与纯 Torch 代码相比,您的 Hugging Face 转换器代码添加了什么?
      • 您对语义搜索 NLP 任务有什么其他解决方案吗?例如本文讨论了添加池化层的想法arxiv.org/pdf/1908.10084.pdf
      • 它只是提供了一个接口来加载模型和分词器。其他部分纯粹是 Python/torch。请注意,您的代码也不是“纯火炬”。这是纯粹的fairseq。所以这取决于你想使用什么样的库。
      • 正如我在帖子中提到的,最有效的方法取决于模型和您的任务。 CamemBERT 相对较新,所以我还没有看到用它完成的实际工作。因此,您可以自行尝试,看看哪种方案最适合您的方案。池化可能比使用 CLS 令牌效果更好,或者可能更糟。使用倒数第二层可能会更好,也可能不会。连接最后四层可能会更好,也可能不会。这些都是你在下游任务上测试的东西。
      • @julien_c 谢谢!已在帖子中编辑。
      【解决方案3】:

      Bert-as-service 是一个很好的例子,可以完全按照您的要求去做。

      他们使用填充。但是请阅读常见问题解答,关于从哪个层获取表示如何汇集它:长话短说,取决于任务。

      编辑:我不是说“使用 Bert-as-service”;我说的是“抄袭 Bert-as-service 所做的事情”。

      在您的示例中,您获得了词嵌入(因为您从中提取的层)。 Here is how Bert-as-service does that。因此,这取决于句子的长度,这实际上不应该让您感到惊讶。

      然后您将讨论通过对词嵌入进行平均池化来获得句子嵌入。那是……一种方法。但是,使用Bert-as-service as a guide for how to get a fixed-length representation from Bert...

      问:如何获得固定表示?你有没有做pooling之类的?

      A:是的,需要池化来获得一个句子的固定表示。在默认策略REDUCE_MEAN中,我取句子中所有标记的倒数第二个隐藏层并进行平均池化。

      所以,要执行 Bert-as-service 的默认行为,您应该这样做

      def embed(sentence):
         tokens = camembert.encode(sentence)
         # Extract all layer's features (layer 0 is the embedding layer)
         all_layers = camembert.extract_features(tokens, return_all_hiddens=True)
         pooling_layer = all_layers[-2]
         embedded = pooling_layer.mean(1)  # 1 is the dimension you want to average ovber
         # note, using numpy to take the mean is bad if you want to stay on GPU
         return embedded
      

      【讨论】:

      • 感谢您的回答 :) 我不想使用任何其他我想保留纯 Torch 代码的库。您能否解释或展示如何使用填充层?
      • 编辑的答案更清楚 - 我说的是“撕掉 Bert-as-service 所做的事情”而不是“使用 bert-as-service”
      猜你喜欢
      • 2020-04-07
      • 2015-02-08
      • 1970-01-01
      • 1970-01-01
      • 2010-09-30
      • 1970-01-01
      • 1970-01-01
      • 2022-11-02
      • 2019-03-16
      相关资源
      最近更新 更多