如何使用BERT找到最接近向量的单词

vis*_*ksh 6 nlp word-embedding bert-language-model

我正在尝试使用 BERT 获取给定词嵌入的文本表示(或最接近的词)。基本上我试图获得与 gensim 类似的功能:

>>> your_word_vector = array([-0.00449447, -0.00310097, 0.02421786, ...], dtype=float32)
>>> model.most_similar(positive=[your_word_vector], topn=1))
Run Code Online (Sandbox Code Playgroud)

到目前为止,我已经能够使用bert-as-service生成上下文词嵌入,但无法弄清楚如何获得与此嵌入最接近的词。我使用了预训练的 bert 模型(uncased_L-12_H-768_A-12)并且没有做任何微调。

Dav*_*ale 13

TL; 博士

Jindtrich 的回答之后,我实现了一个上下文感知的最近邻搜索器。完整代码可在我的Github gist 中找到

它需要一个类似 BERT 的模型(我使用bert-embeddings)和一个句子语料库(我从这里拿了一小部分),处理每个句子,并将上下文标记嵌入存储在一个有效可搜索的数据结构中(我使用KDTree,但是随意选择 FAISS 或 HNSW 或其他)。

例子

该模型构建如下:

# preparing the model
storage = ContextNeighborStorage(sentences=all_sentences, model=bert)
storage.process_sentences()
storage.build_search_index()
Run Code Online (Sandbox Code Playgroud)

然后可以查询上下文最相似的词,比如

# querying the model
distances, neighbors, contexts = storage.query(
    query_sent='It is a power bank.', query_word='bank', k=5)
Run Code Online (Sandbox Code Playgroud)

在这个例子中,最近的邻居将是单词“银行里的一句话”“最后,还有内置有2000mAh的功率铎的第二个版本的银行,翻转电力世界。 ”。

然而,如果我们在另一个上下文中寻找同一个词,比如

distances, neighbors, contexts = storage.query(
    query_sent='It is an investment bank.', query_word='bank', k=5)
Run Code Online (Sandbox Code Playgroud)

那么最近的邻居将出现在句子“银行还获得了 5 星,2017 年 12 月 31 日,财务数据的高级鲍尔评级。

如果我们不想检索单词“bank”或其派生词,我们可以将它们过滤掉

distances, neighbors, contexts = storage.query(
     query_sent='It is an investment bank.', query_word='bank', k=5, filter_same_word=True)
Run Code Online (Sandbox Code Playgroud)

然后最近的邻居将是“ Cahal 是德勤英国的副主席和2014 年咨询企业融资业务的主席(之前从 2005 年开始领导该业务) ”这句话中的“金融”一词。

在NER中的应用

这种方法的一个很酷的应用是可解释的命名实体识别。我们可以用 IOB 标记的示例填充搜索索引,然后使用检索到的示例来推断查询词的正确标签。

例如,与“贝索斯宣布其两日送达服务亚马逊Prime 全球订阅用户已突破 1 亿”的最近邻居是“扩展的第三方集成,包括亚马逊Alexa、谷歌助手和 IFTTT。 ”。

但是对于“大西洋有足够的波浪和潮汐能将亚马逊的大部分沉积物带到海洋中,因此这条河流并没有形成真正的三角洲”最近的邻居是“而且,今年我们的故事是旅行的作品从巴西的伊瓜苏瀑布到亚特兰大的一个养鸡场”。

因此,如果这些邻居被标记,我们可以推断在第一个上下文中“亚马逊”是一个组织,但在第二个上下文中它是一个位置。

编码

这是执行此工作的类:

import numpy as np
from sklearn.neighbors import KDTree
from tqdm.auto import tqdm


class ContextNeighborStorage:
    def __init__(self, sentences, model):
        self.sentences = sentences
        self.model = model

    def process_sentences(self):
        result = self.model(self.sentences)

        self.sentence_ids = []
        self.token_ids = []
        self.all_tokens = []
        all_embeddings = []
        for i, (toks, embs) in enumerate(tqdm(result)):
            for j, (tok, emb) in enumerate(zip(toks, embs)):
                self.sentence_ids.append(i)
                self.token_ids.append(j)
                self.all_tokens.append(tok)
                all_embeddings.append(emb)
        all_embeddings = np.stack(all_embeddings)
        # we normalize embeddings, so that euclidian distance is equivalent to cosine distance
        self.normed_embeddings = (all_embeddings.T / (all_embeddings**2).sum(axis=1) ** 0.5).T

    def build_search_index(self):
        # this takes some time
        self.indexer = KDTree(self.normed_embeddings)

    def query(self, query_sent, query_word, k=10, filter_same_word=False):
        toks, embs = self.model([query_sent])[0]

        found = False
        for tok, emb in zip(toks, embs):
            if tok == query_word:
                found = True
                break
        if not found:
            raise ValueError('The query word {} is not a single token in sentence {}'.format(query_word, toks))
        emb = emb / sum(emb**2)**0.5

        if filter_same_word:
            initial_k = max(k, 100)
        else:
            initial_k = k
        di, idx = self.indexer.query(emb.reshape(1, -1), k=initial_k)
        distances = []
        neighbors = []
        contexts = []
        for i, index in enumerate(idx.ravel()):
            token = self.all_tokens[index]
            if filter_same_word and (query_word in token or token in query_word):
                continue
            distances.append(di.ravel()[i])
            neighbors.append(token)
            contexts.append(self.sentences[self.sentence_ids[index]])
            if len(distances) == k:
                break
        return distances, neighbors, contexts
Run Code Online (Sandbox Code Playgroud)


Jin*_*ich 7

BERT 提供上下文表示,即单词和上下文的联合表示。与非上下文嵌入不同,最接近的词应该是什么意思并不清楚。

接近词的一个很好的近似当然是 BERT 作为(掩码)语言模型所做的预测。它基本上说明了在相同上下文中可能出现的相似词。但是,这不在bert-as-service客户端 API 中。您可以自己实现预测层(我认为它只是最后一层与嵌入矩阵 + softmax 的乘法,但也许还有一些额外的投影正在进行)或使用不同的实现,例如Hugginface 的 Transformers

理论上最正确(且计算成本高)的解决方案是在大型数据集上运行 BERT 并存储单词对和相应的上下文表示,然后使用例如faiss来检索最近邻,其中也包括上下文,类似于最近邻语言模型