向量召回 ES

应用场景

Elasticsearch支持词向量搜索能够在很多场景下进行应用,这里进行列举一些简单的应用,有些并不是当前场景下的最佳选择。

  1. QA:用户输入一段描述,给出最佳匹配的答案。传统基于关键字搜索问答的局限性之一在于用户必须了解一些特殊的名词,假如关键字没有匹配上则没有返回结果。而在使用词向量之后,直接输入类似的描述性语言可以获得最佳匹配的答案。
  2. 文章搜索:有时候只记得一篇文章在表达什么意思,而忘记了文章标题和关键字。这时候只需要输入自己记得的大致意思和记得句子,即可根据描述中隐藏的语义信息搜索到最佳匹配的文章。
  3. 图片搜索:这里的图片搜索有两种含义,一种是讲图片中的特征值进行提取生成向量,实现以图搜图模式的搜索。另一种是基于图片tag的方式,将tag进行向量化,这样可以搜索到语义相近的tag的图片,而不必完全相等。这两种方式在ES的词向量搜索中都可以支持。
  4. 社交网络:社交网络中的人都是一个单词,而其关注和粉丝都是和其相关的单词,因此可以每一个人的关注和粉丝形成一段“文本”去训练模型。想计算两个人是否相似或者两个的距离,只需要计算两个人的向量即可。

Elasticsearch的词向量搜索可以理解为提供了一个计算平台,而具体的应用场景需要自己评估是否适合。具体的效果好坏,其实还是取决于本身的模型训练质量和模型使用方式。

最佳实践

本例子以医疗领域的“智能问诊”为例进行了一个展示。在此说明这里仅仅是一个demo,重点介绍具体场景里如何使用Elasticsearch的向量搜索,其模型是否有更适合的或者效果是否满足用户使用在不做过多讨论。

预期功能

用户A生病了,在demo中输入一段症状描述,demo返回给用户得了什么病。

数据准备

demo需要准备的数据主要有两个:

  • 用以训练模型的文本数据(下方流程图的Texts):这是大量的和医疗相关的文本,可以是从维基百科爬取的整篇整篇的文章,或者免费版权的医学杂志、网站等获得的文本段落。该数据只要和医疗相关即可,格式为一行一个段落,如下:
xxxxx一行医学相关的文本,百姓所说的感冒是指“普通感冒”,又称“伤风”、急性鼻炎或上呼吸道感染。感冒是一种常见的急性上呼吸道病毒性感染性疾病,多由鼻病毒、副流感病毒、呼吸道合胞病毒、埃可病毒、柯萨奇病毒、冠状病毒、腺病毒等引起。临床表现为鼻塞、喷嚏、流涕、发热、咳嗽、头痛等,多呈自限性。大多散发,冬春季节多发,但不会出现大流行。 
一行医学相关的文本xxxxx 
一行医学相关的文本xxxxx
  • 专业的疾病描述文本数据(下方流程图的Data):比如“感冒:伴随有发烧、流鼻涕、浑身无尽...”,该数据用以和用户的输入进行匹配,返回给用户最相关的疾病。数据格式为json,其最重要的为具体的症状描述一栏,如下:
[
    {
        "id": "1",
        "name": "肝功能异常",
        "department": "消化科",
        "feature": "消化功能xxxxxxxxxxxxxxxxxxxxxxx\n"
    },
    {
        "id": "2",
        "name": "反胃",
        "department": "消化科",
        "feature": "xxxxxxxxxxxxxx为主要表现。\n"
    }
]
  • 停用词表:分词时候去除停用词的。数据格式为一行一行的单个单词,如下:
两者 
个
个别
临
为
为了
为什么

由于数据涉及到隐私,这里不进行提供,仅仅在源码中提供了数据的格式,方便跑通程序。

流程及代码实现

1:离线模型训练

这里将收集到的数据进行离线的顺联,生成 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")

2~3:特征数据转化为向量,并存到ES中

从数据库中将我们标注好的疾病描述的数据拿出来,利用之前训练的模型,将每一个疾病的描述转化为向量,然后存在ES中。该向量具有表达一个疾病的含义,其是对疾病描述的embedding,在后续匹配过程中,只需要将用户输入的向量和ES中的向量进行匹配,即可找到最相关的向量。

因此,这一步,也是一个离线的过程,其包括:

  1. ES中使用指定的mapping创建索引。这里需要将向量这个Field“feature_vector”的类型设置为“dense_vector”,由于我们在model训练期间设置的纬度是300,这里需要指定dims为300.
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")

4~8:用户输入症状表现,并转化为向量,从ES中搜索最相关的TopN个疾病

用户输入,我们假设从命令行输入即可。转化为向量也是使用最初训练的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: 呼吸不畅,咳嗽,胸闷是怎么回事?
可能获得的疾病是: 肺炎  上呼吸道感染   支气管炎

摘自: Elasticsearch: 基于Text Embedding的文本相似性搜索 - 知乎

相关文档:

https://www.woshipm.com/pmd/5541932.html
https://www.woshipm.com/u/1142016/page/3
https://www.woshipm.com/pmd/5541949.html
https://www.woshipm.com/it/5545840.html
https://www.woshipm.com/it/5545840.html#toc-3
https://www.woshipm.com/pmd/5572342.html
https://www.6aiq.com/article/1601296161290
https://developer.aliyun.com/article/1207600
https://zhuanlan.zhihu.com/p/107663526
https://zhuanlan.zhihu.com/p/80737146

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值