データナード

機械学習と自然言語処理についての備忘録 (旧ナード戦隊データマン)

paraphrase retrievalをfaiss+laserで試す

paraphrase retrievalの概念的なメモを前回の記事で行いましたが、今回はベースラインモデルとしてfaiss+laserを使います。

説明

事前準備

データのダウンロード

以下からQuora datasetをダウンロードします。

https://www.quora.com/q/quoradata/First-Quora-Dataset-Release-Question-Pairs

faissのインストール

faissを以下からcloneし、インストールします。

laserのインストール

laserを以下からcloneし、インストールします。

https://github.com/facebookresearch/LASER/

laserencoderのインストール

laserencoderとは、laserをon the flyで使うための、私が作った簡易ツールです。laserが完全に正しくインストールされていれば、2020/02/10時点では使えています。

laserencoder: https://github.com/sugiyamath/laserencoder

実行

データの分離

ソース文、ターゲット文、正解ペア、の3つのデータへ分離します。

from tqdm import tqdm

def sep(infile="./quora_duplicate_questions.tsv", outprefix="sep_"):
    stexts = set()
    ttexts = set()
    answer = []
    with open(infile) as f:
        for i, line in tqdm(enumerate(f)):
            if i == 0:
                continue
            row = line.strip().split("\t")
            try:
                a = int(row[-1])
            except:
                print(i)
            if a == 1:
                stext = row[-3]
                ttext = row[-2]
                if stext not in stexts and ttext not in ttexts:
                    answer.append((stext, ttext))
                stexts.add(stext)
                ttexts.add(ttext)

    with open(outprefix+"source.txt", "w") as f1,\
         open(outprefix+"target.txt", "w") as f2,\
         open(outprefix+"answer.txt", "w") as f3:
        for ans in tqdm(answer):
            f1.write(ans[0] + "\n")
            f2.write(ans[1] + "\n")
            f3.write('\t'.join(list(ans)) + "\n")


if __name__ == "__main__":
    sep()

ターゲット文のインデクシング

ターゲット文をlaserでエンコードしてからfaissへぶちこみます。

import faiss
import laserencoder

enc = laserencoder.Encoder()


def indexing(infile="./sep_target.txt"):
    with open(infile) as f:
        text = f.read()
        xb = enc.encode(text)
        d = 1024
        quantizer = faiss.IndexFlatL2(d)
        index = faiss.IndexIVFFlat(quantizer, d, 1014)
        index.train(xb)
        index.add(xb)
        faiss.write_index(index, "index.faiss")


if __name__ == "__main__":
    indexing()

ランク付け(予測)

各々のソース文に対して、Top 5件を予測します。

import faiss
import laserencoder
from tqdm import tqdm


enc = laserencoder.Encoder()
index = faiss.read_index("./index.faiss")


def predict(xq, k=5):
    D, I = index.search(xq, k)
    return D, I


if __name__ == "__main__":
    with open("./sep_source.txt") as f:
        text = f.read()
        stexts = text[:].split("\n")

    with open("./sep_target.txt") as f:
        ttexts = [line.strip() for line in f]
    xq = enc.encode(text)
    D, I = predict(xq)
    with open("pred_result.txt", "w") as f:
        for j, (dists, inds) in tqdm(enumerate(zip(D, I))):
            f.write(stexts[j]+"\t")
            for dist, ind in zip(dists, inds):
                f.write(ttexts[ind]+"::"+str(dist)+"\t")
            f.write("\n")

スコアリング(MRR)

MRRでスコアリングします。

def mrr(rank):
    return 1.0 / float(rank+1)


def calc_mrr(infile="./pred_result.txt", ansfile="./sep_answer.txt"):
    out = {"rows": [], "mean": 0}
    with open(infile) as f1, open(ansfile) as f2:
        for i, (line1, line2) in enumerate(zip(f1, f2)):
            row = line1.strip().split("\t")
            cands = {cand.split("::")[0]: r for r, cand in enumerate(row[1:])}
            ans = line2.strip().split("\t")[1]
            if ans in cands:
                out["rows"].append(mrr(cands[ans]))
            else:
                out["rows"].append(0.0)
    out["mean"] = sum(out["rows"]) / float(len(out["rows"]))
    return out


if __name__ == "__main__":
    import json
    out = calc_mrr()

    with open("score0.json", "w") as f:
        json.dump(out,f, indent=4)

結果

mean: 0.3966747470738048
$ cat score0.json.txt | grep 1.0, | wc -l
25450
$ cat score0.json.txt | grep 0.5, | wc -l
3808
$ cat score0.json.txt | grep 0.3 | wc -l
1205
$ cat score0.json.txt | grep 0.25 | wc -l
596
$ cat score0.json.txt | grep 0.2, | wc -l
385
$ cat score0.json.txt | grep 0.0 | wc -l
39097

考察

faiss + laserの方法は非常に汎用的ですが、今回はベースラインモデルとして考えることができます。

情報検索の問題のひとつなので、MRRのようなランクを考慮したスコアを使う方法を検討することができます。

ベースラインモデルを超えるためにできそうなことは、faissを使うという点は維持したまま、別のエンコーダを訓練することです。Deep Metric Learningのような手法を使えば、よりパラフレーズ検出に特化したエンコーダとして使える可能性があります。

ただ、laserのような多言語横断性を維持しつつ、パラフレーズ検出能力のさらに高いモデルを作る方法を考えることが課題です。

実用面から言えば、faissとlaserを使った方法はある程度は機能する可能性が高いですが、「検出される文は同義文でなければだめだ」という要件がある場合はより困難な問題になります。

ただし、faissとlaserを組み合わせる場合、margin-based methodによる方法を使った場合は精度は上がるようですが、miningの手法であってretrievalとして使えるかは検討する必要があります。retrievalタスクでは、ユーザーの投げたクエリにすぐさま結果を返さなければなりません。

その意味でいえば、このタスクで使う手法の評価に、精度の他に実行速度を考慮するようにしても良いかもしれません。さらに、現実的には、メモリ上にすべてのベクトルを置くことはできないため、大量のデータを扱う場合はディスク上にデータを置きながら実行する方法も検討する必要はあります。

参考

  1. GitHub - facebookresearch/faiss: A library for efficient similarity search and clustering of dense vectors.
  2. GitHub - facebookresearch/LASER: Language-Agnostic SEntence Representations
  3. Mean Reciprocal Rank | SpringerLink
  4. https://www.quora.com/q/quoradata/First-Quora-Dataset-Release-Question-Pairs
  5. [1811.01136] Margin-based Parallel Corpus Mining with Multilingual Sentence Embeddings