NLP 变形金刚:获得固定句子嵌入向量形状的最佳方法?
NLP Transformers: Best way to get a fixed sentence embedding-vector shape?
我正在从火炬中心加载语言模型(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])
想象一下,现在为了做一些语义搜索,我想计算向量(在我们的例子中是张量)u
和cosine distance
之间的cosine distance
v
:
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)
你有什么想法?
您还会使用哪些其他技术?为什么?
提前致谢!
Bert-as-service 是一个很好的例子,它完全按照你的要求去做。
他们使用填充。但是请阅读常见问题解答,了解从哪一层获得表示如何汇集它:长话短说,取决于任务。
编辑:我不是说 "use Bert-as-service";我是说 "rip off what Bert-as-service does."
在您的示例中,您得到了词嵌入(因为您从中提取了图层)。 Here is how Bert-as-service does that。因此,这实际上取决于句子长度,这并不会让您感到惊讶。
然后你谈到通过对词嵌入进行均值池化来获得句子嵌入。那是……一种方法。但是,使用 Bert-as-service as a guide for how to get a fixed-length representation from Bert...
Q: How do you get the fixed representation? Did you do pooling or something?
A: Yes, pooling is required to get a fixed representation of a sentence. In the default strategy REDUCE_MEAN, I take the second-to-last hidden layer of all of the tokens in the sentence and do average pooling.
因此,要执行 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
这是一个很笼统的问题,因为没有一个具体的正确答案。
正如您所发现的,形状当然不同,因为每个标记都有一个输出(取决于标记器,它们可以是子词单元)。换句话说,您已将所有标记编码到它们自己的向量中。你想要的是一个句子嵌入,有很多方法可以得到这些(没有一个特别正确的答案)。
特别是对于句子分类,当语言模型已经在其上训练时,我们经常使用特殊分类标记的输出(CamemBERT 使用 <s>
)。请注意,根据模型,这可能是第一个(主要是 BERT 和子代;还有 CamemBERT)或最后一个标记(CTRL、GPT2、OpenAI、XLNet)。我建议在可用时使用此选项,因为该令牌正是为此目的而训练的。
如果 [CLS]
(或 <s>
或类似)令牌不可用,则还有一些其他选项属于术语池。经常使用最大池化和平均池化。这意味着您采用最大值令牌或所有令牌的平均值。你说的 "danger" 就是你把整个句子的向量值缩减成 "some average" 或 "some max" 可能不太能代表句子。然而,文献表明这也很有效。
正如另一个答案所暗示的,您使用其输出的层也可以发挥作用。 IIRC 关于 BERT 的 Google 论文表明,他们在连接最后四层时获得了最好的分数。这是更高级的,除非有要求,否则我不会在这里讨论。
我没有使用 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])
看看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 即服务)的比较,我不建议将其用于相似性任务。此外,您还可以为您的任务微调嵌入。
尝试多语言模型也很有趣:
model = SentenceTransformer('distiluse-base-multilingual-cased')
model.encode([...])
可能会产生比均值池化 CamemBert 更好的结果。
我正在从火炬中心加载语言模型(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])
想象一下,现在为了做一些语义搜索,我想计算向量(在我们的例子中是张量)u
和cosine distance
之间的cosine distance
v
:
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)
你有什么想法? 您还会使用哪些其他技术?为什么?
提前致谢!
Bert-as-service 是一个很好的例子,它完全按照你的要求去做。
他们使用填充。但是请阅读常见问题解答,了解从哪一层获得表示如何汇集它:长话短说,取决于任务。
编辑:我不是说 "use Bert-as-service";我是说 "rip off what Bert-as-service does."
在您的示例中,您得到了词嵌入(因为您从中提取了图层)。 Here is how Bert-as-service does that。因此,这实际上取决于句子长度,这并不会让您感到惊讶。
然后你谈到通过对词嵌入进行均值池化来获得句子嵌入。那是……一种方法。但是,使用 Bert-as-service as a guide for how to get a fixed-length representation from Bert...
Q: How do you get the fixed representation? Did you do pooling or something?
A: Yes, pooling is required to get a fixed representation of a sentence. In the default strategy REDUCE_MEAN, I take the second-to-last hidden layer of all of the tokens in the sentence and do average pooling.
因此,要执行 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
这是一个很笼统的问题,因为没有一个具体的正确答案。
正如您所发现的,形状当然不同,因为每个标记都有一个输出(取决于标记器,它们可以是子词单元)。换句话说,您已将所有标记编码到它们自己的向量中。你想要的是一个句子嵌入,有很多方法可以得到这些(没有一个特别正确的答案)。
特别是对于句子分类,当语言模型已经在其上训练时,我们经常使用特殊分类标记的输出(CamemBERT 使用 <s>
)。请注意,根据模型,这可能是第一个(主要是 BERT 和子代;还有 CamemBERT)或最后一个标记(CTRL、GPT2、OpenAI、XLNet)。我建议在可用时使用此选项,因为该令牌正是为此目的而训练的。
如果 [CLS]
(或 <s>
或类似)令牌不可用,则还有一些其他选项属于术语池。经常使用最大池化和平均池化。这意味着您采用最大值令牌或所有令牌的平均值。你说的 "danger" 就是你把整个句子的向量值缩减成 "some average" 或 "some max" 可能不太能代表句子。然而,文献表明这也很有效。
正如另一个答案所暗示的,您使用其输出的层也可以发挥作用。 IIRC 关于 BERT 的 Google 论文表明,他们在连接最后四层时获得了最好的分数。这是更高级的,除非有要求,否则我不会在这里讨论。
我没有使用 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])
看看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 即服务)的比较,我不建议将其用于相似性任务。此外,您还可以为您的任务微调嵌入。
尝试多语言模型也很有趣:
model = SentenceTransformer('distiluse-base-multilingual-cased')
model.encode([...])
可能会产生比均值池化 CamemBert 更好的结果。