Pythonで大量のテキストの類似度チェックを行うメモ

はじめに

最近は大規模言語モデルのコーパスづくりに四苦八苦しています。
収集したテキストには、多くの重複データが含まれるためそれらを削除する作業が大切です。

重複削除はCなどのコンパイル言語で高速にやるのが常套手段なのですが、今回はあえて、pythonライブラリで実行してみました

関連

例えばこちら


テキスト

webから収集した、7万件ほどのテキストを使います。

Hojicharに入っている類似度判定アルゴリズム

hojicharは、テキストを簡便にクリーニングするための便利なライブラリです。

ただし、類似度判定は、速度が遅いとの声をよく聞きます。 MinHashアルゴリズムというものを使っているのですが、結局、pythonでもろもろ動かしている*ので、あまり速度が早くないようです。
*ハッシュの処理、set判定のためのfor loopなど

code中にも、todoとして、c++で実装なおしたい的なことが書いてありました。

以下のような類似度判定クラスを作っておきます。


import joblib
from hojichar import deduplication, Document
class Deduplicator:
    def __init__(self, hasher, cache_path="cache", init=False):
        self.hasher = hasher
        self.cache_path = cache_path
        if init:
            self.seen = set()
        else:
            try:
                self.seen = joblib.load(self.cache_path)
            except FileNotFoundError:
                print("No cache found, initializing new cache.")
                self.seen = set()

    def is_duplicated(self, text):
        doc = Document(text)
        self.hasher.apply(doc)

        for lsh in doc.dedup_lsh:
            if lsh in self.seen:
                self.seen.add(lsh)
                return True

            self.seen.add(lsh)
        return False

    def save_state(self):
        joblib.dump(self.seen, self.cache_path)

all_linesに適当なテキストを入れておき、データ数を変えながら、処理時間を見ていきます。

res_list=[]

for check_n in [100,200,500,1000,2000]:
    dedup=Deduplicator( deduplication.GenerateDedupLSH())
    start = time.time()
    for i in range(check_n):
        (dedup.is_duplicated(all_lines[i]))
    print(f"check_n:{check_n} time:{time.time()-start}")
    res_list.append({"check_n":check_n,"time":time.time()-start})

結果
2000件で20秒はかかりました。大規模言語モデルのコーパスは、軽くmillion以上のデータがあるので、これをそのまま用いるのは、流石に無理筋といえます。

RapidFuzz

類似度判定は普通、テキストをハッシュ化して行います。
こちらのライブラリは、普通にテキストを比較するようです。内部はcで動いているようなので、速度も担保されます。並列化(workersの指定)も容易です。

以下のコードでは、文書間の類似度を計算するmatrixを生成します
(類似テキストの削除のアルゴリズムは実装してないので、正確な比較ではない点に注意)

import time
from rapidfuzz.process import cdist

res_list=[]
for check_n in [100,200,500,1000,2000]:
#for check_n in [100,1000,5000,10000,20000,50000]:
    start = time.time()
    scores = cdist(all_lines[:check_n], all_lines[:check_n],workers=1)
    #scores = cdist(all_lines[:check_n], all_lines[:check_n],workers=16)
    print(f"check_n:{check_n} time:{time.time()-start}")
    res_list.append({"check_n":check_n,"time":time.time()-start})
#%time scores = cdist(all_lines[:check_n], all_lines[:check_n],workers=16)

1並列の結果

まずは普通に回した結果。2000件で6秒なので、hojicharよりも早いです。

1並列 with 文書を短くする

重複の多い文章というのは、わりと出だしからそっくりなことが多いです。そこで、はじめの100文字のみを判定してみました。その結果、2000件で3秒まで処理速度が減りました。

16並列 with 文書を短くする

このライブラリの良いところは、わりと簡単に並列化ができる点です。例えば16並列で動かしてみたところ、2000件で0.2秒という結果になりました。コーパスの重複判定用途としては、最低でもこれくらいはないときついです。

結果は以下の通り
check_n:100 time:0.05085897445678711
check_n:1000 time:0.06066083908081055
check_n:2000 time:0.20825457572937012
check_n:5000 time:1.2620820999145508
check_n:10000 time:4.878591775894165
check_n:20000 time:19.23476004600525
check_n:50000 time:117.07648491859436

log-logプロットを打ったところ、1000件以上のrecordでは、わりときれいな直線に乗りました。類似度判定のコストは、O(N^2)っぽいので、妥当な結果です。
これで、必要な計算量を推定できます。

まとめ

類似度判定の速度を軽く比較しました。
Pythonだと遅いのは当たり前なんですが、アルゴリズム上、Cなんかを使っても、O(N^2)は避けられないので、やはり、Nを小さく留めるのが一つの戦略になります。LSHや、クラスタリングなどが必要ということになりそうです。


この記事が気に入ったらサポートをしてみませんか?