テンセント語ベクトルの優雅な使用方法:redisクラスタ&elasticsearchに基づく姿勢


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)

テンセント語ベクトルが大きいため、データの解凍後に得られる語表ファイルは16 Gであるため、ロードするたびに10分程度かかり、またコードのデバッグにも非常に不便である.そこで,キャッシュ方式で語や対応ベクトルを格納することを考え,類似語などの高度な検索機能はベクトルインデックスツール(annoy,faissなど)を用いて実現できる.  まず思いついたのはredisキー値データベースを利用して用語表をキャッシュすることであり、redisはメモリストレージであるため、ブロガーは3台の機械でredisクラスタを構築し、データの均衡を行い、redisクラスタの配置を採用することでより高い読み書き性能を提供することができるが、この方案はより多くのメモリ資源を必要とする.もう1つの方法は、一定の読み取り性能を犠牲にするが、ハードウェアに対する需要が低い、すなわちelasticsearchを利用してキャッシュすることである.2つのシナリオについて詳しく説明します.
Redisスキーム(Redisクラスタ構築方法を理解する必要がある)
  ブロガーはpythonを用いてローカル語ベクトルファイルを読み取り、redisクラスタに直接書き込み、最終的に1時間程度かかり、3つのノードが最終的に消費するメモリはそれぞれ6.01 G、6.02 G、6.02 Gであり、語数はそれぞれ2942462個、2941404個、2940465個であり、書き込み時間は約30分(正確ではないかもしれないが、正確な時間を記録することを忘れた)である.サンプル書き込みコード:
# -*- 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クライアントのget apiを使用して指定語のベクトル化表現を取得します.実測結果は10000語で同期取得され、2.5 sかかり、読み取り性能が優れています.
Elasticsearchスキーム(esについてある程度理解する必要がある)
 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.2 Gと小さくすることができます.インデックスプッシュデータを作成するコードの例は、次のとおりです.
# -*- 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("  ")


テストの結果,単一語呼び出し時間は10 ms程度,同一語呼び出しは1000回程度,時間は約1.7 sであったが,elasticsearch自体がキャッシュを行うため,同一語を複数回呼び出す時間の消費は大幅に減少したが,redisより8倍程度遅い.
結論
テンセント語ベクトルのリリースにより、さまざまなembedding作業を容易に行うことができますが、データが大きいため、呼び出しやデバッグは特に便利ではありません.ここではredisとelasticsearchに基づく2つの解決策を示したが,元の語ベクトルファイルをキャッシュすることに限られ,より高度な機能はブログに引き続き注目し,その後,ベクトル検索に関する技術について議論する.