好的,让我们开始吧。
首先您需要了解 BERT 有 13 层。第一层基本上只是 BERT 在初始训练期间通过的嵌入层。您可以使用它,但可能不想使用它,因为它本质上是静态嵌入,而您正在使用动态嵌入。为简单起见,我将只使用 BERT 的最后一个隐藏层。
您在这里使用了两个词:“New”和“York”。您可以在预处理期间将其视为一个,并将其组合成“New-York”或其他如果您真的想要的东西。在这种情况下,我会将其视为两个单独的单词,并对 BERT 产生的嵌入进行平均。
这可以用几个步骤来描述:
- 标记输入
- 确定标记器在哪里有纽约和纽约的 word_id(suuuuper 很重要)
- 通过 BERT
- 平均
- 余弦相似度
首先需要导入的内容:from transformers import AutoTokenizer, AutoModel
现在我们可以创建分词器和模型了:
tokenizer = AutoTokenizer.from_pretrained('bert-base-cased')
model = model = AutoModel.from_pretrained('bert-base-cased', output_hidden_states=True).eval()
确保在评估模式下使用模型,除非您尝试微调!
接下来我们需要标记化(步骤 1):
tok1 = tokenizer(sent1, return_tensors='pt')
tok2 = tokenizer(sent2, return_tensors='pt')
Step 2. 需要确定单词的索引在哪里匹配
# This is where the "New" and "York" can be found in sent1
sent1_idxs = [4, 5]
sent2_idxs = [0, 1]
tok1_ids = [np.where(np.array(tok1.word_ids()) == idx) for idx in sent1_idxs]
tok2_ids = [np.where(np.array(tok2.word_ids()) == idx) for idx in sent2_idxs]
上面的代码检查了分词器生成的 word_ids() 与原始句子中的单词索引重叠的位置。这是必要的,因为分词器会拆分稀有词。因此,如果您有类似“土豚”之类的东西,当您对其进行标记并查看它时,您实际上会得到:
In [90]: tokenizer.convert_ids_to_tokens( tokenizer('aardvark').input_ids)
Out[90]: ['[CLS]', 'a', '##ard', '##var', '##k', '[SEP]']
In [91]: tokenizer('aardvark').word_ids()
Out[91]: [None, 0, 0, 0, 0, None]
步骤 3. 通过 BERT
现在我们抓取 BERT 在我们生成的令牌 ID 中生成的嵌入:
with torch.no_grad():
out1 = model(**tok1)
out2 = model(**tok2)
# Only grab the last hidden state
states1 = out1.hidden_states[-1].squeeze()
states2 = out2.hidden_states[-1].squeeze()
# Select the tokens that we're after corresponding to "New" and "York"
embs1 = states1[[tup[0][0] for tup in tok1_ids]]
embs2 = states2[[tup[0][0] for tup in tok2_ids]]
现在您将有两个嵌入。每个都是形状(2, 768)。第一个大小是因为我们正在查看两个词:“New”和“York”。第二个大小是 BERT 的嵌入大小。
第 4 步。平均
好的,所以这不一定是您想要做的,但这将取决于您如何处理这些嵌入。我们有两个(2, 768) 形状的嵌入。您可以将 New 与 New 进行比较,将 York 与 York 进行比较,也可以将 New York 合并为一个平均值。我会这样做,但如果它更适合你的任务,你可以轻松地做另一个。
avg1 = embs1.mean(axis=0)
avg2 = embs2.mean(axis=0)
第 5 步。余弦模拟
余弦相似度使用torch 很容易:
torch.cosine_similarity(avg1.reshape(1,-1), avg2.reshape(1,-1))
# tensor([0.6440])
这很好!它们指向同一个方向。它们不完全是 1,但可以通过多种方式进行改进。
- 您可以对训练集进行微调
- 您可以尝试平均不同的层,而不是像我那样只计算最后一个隐藏层
- 您可以尝试创造性地将纽约和纽约结合起来。我取了平均值,但也许有更好的方法可以满足您的确切需求。