本文探讨了Text Embedding和ElasticSearch的向量类型如何用于支持文本相似性搜索。本文将首先概述Text embedding技术,介绍该技术的一些应用场景,最后使用ElasticSearch完成一个简单的基于Text embedding的文本相似性搜索demo。
从一开始Elasticsearch就作为全文搜索引擎提供快速而强大的全文搜索功能。在Elasticsearch 7.0中,ES引入了高维向量的字段类型,现在7.3版本支持在文档评分中使用这些向量。
相似性搜索的一个简单方法是根据文档与查询共享的单词数对文档进行排名。但是文档可能与查询类似,即使它们没有太多共同的单词——更强大的相似性概念也会考虑到它的语法和语义内容。
自然语言处理(NLP)社区开发了一种称为文本嵌入的技术,它将单词和句子编码为数字向量。这些向量表示被设计用来捕获文本的语言内容,并且可以用来评估查询和文档之间的相似性。
单词嵌入模型将单词表示为密集数字向量。这些向量旨在捕获单词的语义属性 - 向量靠近在一起的单词在语义上应该是相似的。在一个训练较好的embedding中,向量空间中的方向与单词意义的不同方面相关联。例如,“加拿大”的向量可能在一个方向上接近“法国”,而在另一个方向上接近“多伦多”。
一段时间以来,自然语言处理(NLP)和搜索社区一直对单词的向量表示感兴趣。在过去的几年中,人们对单词嵌入的兴趣再次兴起,当时许多传统的任务正在使用神经网络进行重新审视。开发了一些成功的Word embedding算法,包括word2vec和GloVe,还有后来的训练速度更快的Fasttext。这些方法使用大型文本集合,并检查每个单词出现的上下文以确定其向量表示:
许多研究小组分发的模型已在大型文本语料库(如维基百科)上进行了预训练,使其便于下载和插入下游任务。尽管经常使用预训练版本,但调整模型以适应特定目标数据集和任务会很有帮助。这通常通过在预训练模型上运行轻量级微调步骤来实现。
Word embedding已被证明非常强大和有效,现在NLP任务(如机器翻译和情感分类)中使用Word embedding已经越来越多。
最近,研究人员不仅关注单词级别的Word embedding,而且开始关注较长的文本如何进行词向量表示。当前大多数的方法基于复杂的神经网络架构,并且有时在训练期间需要不断标记数据以帮助捕获语义信息和提高训练效果。
一旦经过训练,模型就能够获取一个句子并为上下文中的每个单词生成一个向量,以及整个句子的向量。与嵌入字词类似,许多模型的预训练版本可用,允许用户跳过昂贵的培训过程。虽然训练过程可能非常耗费资源,但调用模型的重量要轻得多。训练好的Sentence embeddings足够快,可以用作实时应用程序的一部分。
一些常见的句子嵌入技术包括InferSent,Universal Sentence Encoder,ELMo和BERT。改进单词和句子嵌入是一个活跃的研究领域,并且可能会引入更多强大的模型。
在传统的信息检索中,我们基于大多使用TF-IDF等基于单词个数的搜索方法,我们只是计算单词出现而不考虑句子结构。而基于text embedding等技术的搜索,将会考虑句子意思。比如“上午吃饭吗”和“我eat早餐了”这两个句子没有一个单词一样,但是其语义是完全接近的,使用text embedding将能够很好的搜索出来。
文本嵌入在某些重要方面与传统的矢量表示不同:
Elasticsearch支持词向量搜索能够在很多场景下进行应用,这里进行列举一些简单的应用,有些并不是当前场景下的最佳选择。
Elasticsearch的词向量搜索可以理解为提供了一个计算平台,而具体的应用场景需要自己评估是否适合。具体的效果好坏,其实还是取决于本身的模型训练质量和模型使用方式。
本例子以医疗领域的“智能问诊”为例进行了一个展示。在此说明这里仅仅是一个demo,重点介绍具体场景里如何使用Elasticsearch的向量搜索,其模型是否有更适合的或者效果是否满足用户使用在不做过多讨论。
用户A生病了,在demo中输入一段症状描述,demo返回给用户得了什么病。
demo需要准备的数据主要有两个:
xxxxx一行医学相关的文本,百姓所说的感冒是指“普通感冒”,又称“伤风”、急性鼻炎或上呼吸道感染。感冒是一种常见的急性上呼吸道病毒性感染性疾病,多由鼻病毒、副流感病毒、呼吸道合胞病毒、埃可病毒、柯萨奇病毒、冠状病毒、腺病毒等引起。临床表现为鼻塞、喷嚏、流涕、发热、咳嗽、头痛等,多呈自限性。大多散发,冬春季节多发,但不会出现大流行。
一行医学相关的文本xxxxx
一行医学相关的文本xxxxx
[
{
"id": "1",
"name": "肝功能异常",
"department": "消化科",
"feature": "消化功能xxxxxxxxxxxxxxxxxxxxxxx\n"
},
{
"id": "2",
"name": "反胃",
"department": "消化科",
"feature": "xxxxxxxxxxxxxx为主要表现。\n"
}
]
两者
个
个别
临
为
为了
为什么
由于数据涉及到隐私,这里不进行提供,仅仅在源码中提供了数据的格式,方便跑通程序。
这里将收集到的数据进行离线的顺联,生成 Doc2Vec 模型。离线训练模型特别花费时间,特别是在没有GPU的服务器上。该模型离线训练好后,后续会一直使用。
# 停用词
stopwords = [line.strip() for line in open('./data/ChineseStopWords.txt', encoding='UTF-8').readlines()]
def segment(sentence: str):
"""
结巴分词,并去除停用词
"""
resp = []
sentence_depart = jieba.cut(sentence.strip())
for word in sentence_depart:
if word not in stopwords:
if word != "":
resp.append(word)
return resp
def read_corpus(f_name):
"""
读数据
"""
with open(f_name, encoding="utf-8") as f:
for i, line in enumerate(f):
yield gensim.models.doc2vec.TaggedDocument(segment(line), [i])
def train():
"""
训练 Doc2Vec 模型
"""
train_file = "./data/train_data.txt"
train_corpus = list(read_corpus(train_file))
model = gensim.models.doc2vec.Doc2Vec(vector_size=300, min_count=2, epochs=10)
print(len(train_corpus))
model.build_vocab(train_corpus)
model.train(train_corpus, total_examples=model.corpus_count, epochs=model.epochs)
model.save("doc2vec.model")
从数据库中将我们标注好的疾病描述的数据拿出来,利用之前训练的模型,将每一个疾病的描述转化为向量,然后存在ES中。该向量具有表达一个疾病的含义,其是对疾病描述的embedding,在后续匹配过程中,只需要将用户输入的向量和ES中的向量进行匹配,即可找到最相关的向量。
因此,这一步,也是一个离线的过程,其包括:
def create_index():
print("begin create index")
setting = {
"settings": {
"number_of_replicas": 0,
"number_of_shards": 2
},
"mappings": {
"properties": {
"name": {
"type": "keyword"
},
"department": {
"type": "keyword"
},
"feature": {
"type": "text"
},
"feature_vector": {
"type": "dense_vector",
"dims": 300
}
}
}
}
get_es_client().indices.create(index=indexName, body=setting)
print("end create index")
2. 将文本数据转化为向量
def embed_text(sentences):
"""
将所有的句子转化为向量
"""
model = doc2vec.Doc2Vec.load("doc2vec.model")
resp = []
for s in sentences:
resp.append(model.infer_vector(segment(s)).tolist())
return resp
3. 将元数据和向量一起索引到es中
def bulk_index_data():
"""
将数据索引到es中,且其中包含描述的特征向量字段
"""
print("begin embed index data to vector")
with open("./data/data.json") as file:
load_dict = json.load(file)
features = [doc["feature"] for doc in load_dict]
print("number of lines to embed:", len(features))
features_vectors = embed_text(features)
print("begin index data to es")
requests = []
for i, doc in enumerate(load_dict):
request = {'_op_type': 'index', # 操作 index update create delete
'_index': indexName, # index
'_id': doc["id"],
'_source':
{
'name': doc["name"],
'department': doc["department"],
'feature': doc["feature"],
'feature_vector': features_vectors[i],
}
}
requests.append(request)
bulk(get_es_client(), requests)
print("end index data to es")
用户输入,我们假设从命令行输入即可。转化为向量也是使用最初训练的model进行了embed text,函数为上一个步骤使用过的embed_text。当用户的症状描述转化为一个向量时候,这时候即可从Es中进行搜索即可,在搜索的时候,需要使用Es的script_score的query,在query的scrip脚本中,将用户的向量放到查询语句的参数中,即可进行搜索,这里的搜索不是简单的文本匹配了,而是进行了语义层面的搜索。搜索结果中,我们将用户最大可能患有的疾病进行输出即可。
def test():
model = doc2vec.Doc2Vec.load("doc2vec.model")
es = get_es_client()
while True:
try:
query = input("Enter query: ")
input_vector = model.infer_vector(segment(query)).tolist()
resp = es.search(index=indexName, body={
"_source": ["name", "feature"],
"query": {
"script_score": {
"query": {
"match_all": {}
},
"script": {
"source": "cosineSimilarity(params.queryVector, doc['feature_vector'])+1",
"params": {
"queryVector": input_vector
}
}
}
}
})
print("可能获得的疾病是:", end=" ")
for hit in resp["hits"]["hits"]:
print(hit["_source"]["name"], end="\t")
print("\n")
except KeyboardInterrupt:
return
Enter query: 我眼睛充血,怎么办?
可能获得的疾病是: 红眼病 眼角膜发炎 外伤
Enter query: 呼吸不畅,咳嗽,胸闷是怎么回事?
可能获得的疾病是: 肺炎 上呼吸道感染 支气管炎
Text embedding 技术提供了一种捕获一段文本语义层信息的强大方法。通过基于embedding技术,我们可以超出传统单词级相似性概念,而使用文本语义层的信息对文档进行排名。
Elasticsearch仅仅是提供了一个计算平台,想要更好的使用词向量搜索,需要训练出适合应用场景的模型,具体是使用word embedding还是sentence embedding需要根据场景来具体选择,而通常情况下sentence embedding更加难以训练。