TL;DR
继金特里奇的回答 https://stackoverflow.com/a/59874553/6498293我实现了一个上下文感知的最近邻搜索器。完整的代码可以在我的Github要点 https://gist.github.com/avidale/c6b19687d333655da483421880441950
它需要一个类似 BERT 的模型(我使用bert 嵌入 https://pypi.org/project/bert-embedding/)和一个句子语料库(我从here https://wortschatz.uni-leipzig.de/en/download/),处理每个句子,并将上下文标记嵌入存储在可有效搜索的数据结构中(我使用KDTree https://scikit-learn.org/stable/modules/generated/sklearn.neighbors.KDTree.html,但请随意选择 FAISS 或 HNSW 或其他)。
Examples
模型构建如下:
# preparing the model
storage = ContextNeighborStorage(sentences=all_sentences, model=bert)
storage.process_sentences()
storage.build_search_index()
然后可以查询上下文中最相似的单词,例如
# querying the model
distances, neighbors, contexts = storage.query(
query_sent='It is a power bank.', query_word='bank', k=5)
在此示例中,最近的邻居将是单词“bank“在句子中”最后,还有第二个版本的 Duo,配备 2000mAH 电源bank,翻转动力世界。".
但是,如果我们在另一个上下文中查找同一个单词,例如
distances, neighbors, contexts = storage.query(
query_sent='It is an investment bank.', query_word='bank', k=5)
那么最近的邻居将在句子中“The bank2017 年 12 月 31 日的财务数据也被授予 5 星高级 Bauer 评级。"
如果我们不想检索单词“bank”或其派生词,我们可以将它们过滤掉
distances, neighbors, contexts = storage.query(
query_sent='It is an investment bank.', query_word='bank', k=5, filter_same_word=True)
那么最近的邻居将是单词“finance“在句子中”卡哈尔是德勤英国副主席兼咨询公司主席Finance从 2014 年开始负责业务(之前从 2005 年开始领导该业务)。".
在NER中的应用
这种方法最酷的应用之一是可解释的命名实体识别。我们可以用 IOB 标记的示例填充搜索索引,然后使用检索到的示例来推断查询词的正确标签。
例如,“的最近邻居贝索斯宣布推出两日送达服务,AmazonPrime 的全球订户数量已超过 1 亿。" is "扩展的第三方集成包括AmazonAlexa、Google Assistant 和 IFTTT。".
但对于 ”大西洋有足够的波浪和潮汐能来承载大部分Amazon的沉积物出海,因此河流并没有形成真正的三角洲“最近的邻居是”而且,今年我们的故事是从Brazil的伊瓜苏瀑布到亚特兰大的养鸡场".
因此,如果这些邻居被标记,我们可以推断在第一个上下文中“亚马逊”是一个组织,但在第二个上下文中它是一个位置。
The code
这是完成这项工作的类:
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