2018年10月份,腾讯AI Lab开源了大规模高质量的中文词向量数据,包含了8824331个常用词的向量表示,维度为200。当前,向量表示已经成为nlp的重要基础功能,从我个人角度而言,其地位等同于搜索引擎中的分词功能,是注入词性标注、命名实体识别、情感分类等后续任务的基础步骤。关于腾讯词向量的进一步细节大家可以参考tencent embedding。 关于词向量的使用,腾讯AI Lab给出的官方姿势是利用gensim加载模型,然后可以使用gensim的各种API进行其他任务,词向量的提取,相似词的查找等等。示例代码为,因为gensim版本问题,与官方代码略有出路,本机gensim版本为3.5.0:
# 腾讯词向量主页示例:from gensim.models.word2vec import KeyedVectors from gensim.models import KeyedVectors wv_from_text = KeyedVectors.load_word2vec_format(file, binary=False)由于腾讯词向量较大,数据解压后得到的词表文件为16G,因此每次加载都要十分钟左右,另外对于代码的调试也十分不方便。所以想到了用缓存的方式将词及对应向量进行存储,至于相似词等高级查找功能可以利用向量索引工具来实现(如annoy、faiss等,后续将会有文章探讨向量搜索)。 首先想到的是利用redis键值数据库来缓存词表,由于redis是内存存储,所以博主用了三台机器搭建了redis集群,进行数据均衡,虽然采用redis集群部署可以提供较高的读写性能,该方案需要较多的内存资源。还有一种方法,即牺牲一定的读取性能,但对硬件的需求较低,即利用elasticsearch来进行缓存。下面将详细介绍两种方案。
博主采用python读取本地词向量文件,然后直接写入redis集群,最终耗时1个小时左右,三个节点最终占用的内存分别为6.01G、6.02G、6.02G,词数量分别为:2942462个、2941404个、2940465个,写入耗时约为半小时(可能不准确,当时忘记记录准确时间了)。示例写入代码:
# -*- coding:utf-8 -*- import codecs import rediscluster class Vector2Redis(object): def __init__(self): self.redis_client = rediscluster.StrictRedisCluster( startup_nodes=[ {"host": "ip1", "port": post1}, {"host": "ip2", "port": port2}, {"host": "ip3", "port": port3}, ]) def write_to_redis(self): f = codecs.open("../data/Tencent_AILab_ChineseEmbedding.txt", "r", "utf-8") for line in f: try: split_line = line.strip().split(" ") word = split_line[0] vector = ",".join(split_line[1:]) self.redis_client.set(word, vector) except Exception as exc: print(word, exc) if __name__ == "__main__": vr = Vector2Redis() vr.write_to_redis()其中rediscluster模块需要安装redis-py-cluster包。在调用时,创建一个redis集群连接,然后利用redis client的get api即可获取指定词的向量化表示,实测结果为10000个词同步获取,耗时2.5s,读取性能较好。
elasticsearch进行缓存不需要较多的内存资源,因为主要基于lucene索引,索引文件存于磁盘,但索引文件的大小和索引的配置有重要关系,博主经过尝试后,将mapping(类似于关系型数据的schema)最终定义为:
{ "tencent_w2v": { "mappings": { "tencent_w2v": { "_all": { "enabled": false }, "properties": { "vector": { "type": "keyword", "index": false }, "word": { "type": "keyword" } } } }, "settings": { "index": { "number_of_shards": "3", "number_of_replicas": "0" } } } }其中关闭了_all字段,分片数设为了3,副本数设为了0,并且由于我们只要能够实现类似于redis一样通过词将向量召回的功能即可,因此将word设定为keyword类型,并且对vector即向量字段不建立索引,这样可以有效较小最终索引体积,这种设置下最终的索引大小为27.2G。建立索引推送数据的示例代码为:
# -*- coding:utf-8 -*- import codecs import elasticsearch from elasticsearch import helpers class Vector2ES(object): def __init__(self): self.es_client = elasticsearch.Elasticsearch( hosts="127.0.0.1", port=9201 ) def write_to_es(self): f = codecs.open("../data/Tencent_AILab_ChineseEmbedding.txt", "r", "utf-8") actions = [] for line in f: try: split_line = line.strip().split(" ") word = split_line[0] vector = ",".join(split_line[1:]) action = { "_index": "tencent_w2v", "_type": "tencent_w2v", "_source": { "word": word, "vector": vector } } actions.append(action) if len(actions) == 1000: helpers.bulk(self.es_client, actions, index="tencent_w2v") actions = [] except Exception as exc: print(exc, line) if len(actions) > 0: helpers.bulk(self.es_client, actions, index="tencent_w2v") if __name__ == "__main__": vr = Vector2ES() vr.write_to_es()将词向量全部推送至elasticsearch建立索引后,调用词向量实际等价于通过搜索的方式获取,因此需要参照elasticsearch的搜索方法,示例代码为:
# -*- coding:utf-8 -*- import jieba import numpy import elasticsearch class ESSearch(object): def __init__(self): self.es_client = elasticsearch.Elasticsearch("127.0.0.1:9201") def encoding(self, word): result = self.es_client.search(index="tencent_w2v", doc_type="tencent_w2v", body={"_source": "vector", "query": {"term": {"word": word}}}) hits = result.get("hits") if hits.get("hits"): temp_vector = hits.get("hits")[0].get("_source").get("vector") print(temp_vector) if temp_vector: split_temp_vector = temp_vector.split(",") split_temp_vector = numpy.array([float(item) for item in split_temp_vector]) return split_temp_vector if __name__ == "__main__": es_search = ESSearch() es_search.encoding("毛笔")经过测试,单个词调用时间为10ms左右,同一个词调用1000次,时间约为1.7s,这是由于elasticsearch本身会做缓存,所以多次调用同一词时间消耗减小许多,但仍比redis慢8倍左右。
腾讯词向量的发布,使得大家可以方便进行各种embedding工作,但由于数据较大,因此调用及调试不是特别方便。这里给出了基于redis及elasticsearch的两种解决思路,但仅限于将原始词向量文件进行缓存,更高级的功能请继续关注博客,后续将会在此基础上,讨论向量搜索相关技术。