🔍

RustでTF-IDFを用いた効率的な文書検索

2025/01/18に公開2

なにかといろいろなところであると便利なのが検索。
だけど案外実装がめんどかったりしていままでいろんなもので実装してこなかったので、
最近初めたrustで文章検索を実装してみたお話です。

最終的にWikipedia10万件を
200ms弱で全検索できるようになります。

あと色々ガラパゴス気味です。

あと私自身適当なのでこの記事を信用しないこと。
これ約束。

基本的なお話

文章検索はある文章集合の中で行い、
その文章集合を一般に corpus(コーパス) と言われています

言語がどのように使われるかを調べるためにコンピュータに保存された書かれたものまたは話されたもののコレクション

今回はそのcorpusっていう文章集合の中からqueryを用いてある文章を取り出すお話です。

なのでここでは

文章集合 => corpus
文章集合中のある文章 => doc

って言いますね.

またstop wordというものも記しておきましょう
stop wordは文章中でとても一般的な単語でかつ 重要な意味を持たない単語のことを指しています
たとえば
"私" などの代名詞
"しかし" などの接続詞
など、とてもありふれていているものです.

あとこの記事では理想的なことばかり言っていますがまぁうまくいきますよ、NLPって.

TF-IDF

文章検索システムの8割がこの設計を用いているっていわれてる王道のやつです.
で、
TF-IDF はある文書中の単語tの重要度を示します。
式でかくと

TF\verb|-|IDF(t) = TF(t) \times IDF(t)
  • 値が大きいほうが重要度を持ちます

これから TF-IDF は
TF と IDF ってやつを掛け合わせた値だってことがわかりますね.

じゃあTFとIDFは何者なのか ですが

  • TF(t) = \frac{ある単語tの出現回数}{doc中のすべての単語の出現回数}
    (Term Frequency)
    docの単語集合がもつ単語tの頻出度を示す
    これは直感的にわかる式なので説明は割愛

  • IDF(t) = \log\frac{corpus中のdoc数}{単語tを含むdoc数 + 1}
    (Inverse Document Frequency)
    corpusの単語集合がもつ単語tの希少度を示す
    logが用いられているのはstop word非常に一般的な単語で過度なスコア上昇を防ぐため
    また+1にするのはlogによる負の値を出さないため

とまとまりました.
難しくはないですね.
これはとてもシンプルですがとてもうまく働きます.

TF-IDFがうまく働く仕組み

ここで思うのはTFだけでもうまくいきそう、、
という考えですが、 まぁ TFとIDFを乗算すると何がよいのか考えてみましょう.

考えるにあたってまず、TF とIDF の良いところと悪いところを考えてみましょう.

  • TF
    • 良いところ
      • docの単語tの頻出回数からtがキーワードかわかる
    • 悪いところ
      • stop wordなどに過度に反応し、重要でない単語にもスコアを振ってしまう.
  • IDF
    • 良いところ
      • corpusの単語tの希少度からtが含まれる文章の希少性がわかる
    • 悪いところ
      • あくまで範囲がcorpusであり、あるdocの単語については評価できない

これらは値が大きいほうがその意味合いをより大きくもちます

そうすると、まずTFの悪いところから考えてみて、
TFの悪いところで出てくる単語tは
IDFでは値がとても小さくなること
がわかります
これは簡単ですね
たとえば TFが重要でない単語"私" にとても高い、 たとえば 1.0 を振ったとしましょう
TFにはIDFがかけられるので IDFの値を考えてみて、
"私" はとても一般的な単語なのでcorpus上で含まれる回数は多そうです.
よって IDFを 0.0と考えてみると、

TF\verb|-|IDF(t) = TF(t) \times IDF(t)
0.0 = 1.0 \times 0.0

で TF-IDFは 0.0となり TFとIDFを乗算することで なんだかうまくいくことがわかります。
またTFがほんとうに重要な単語の場合 IDFがとても高くなることは無いはずなので
重要な単語が出てきた際もちゃんと機能することがわかります.

またIDFはコーパスすべてにおいて平等な値をもつことから これらを掛け合わせることで コーパスすべてにおいてある程度平等な値 つまり正規化され 別doc同士で比較してもあまり問題でなくなることもわかります.

これでなぜTFとIDFを掛け合わせるととてもうまくある単語tの重要度を示せるかわかりました.

TF-IDFを用いて検索する

さて TF-IDFをつかってdocの単語tの重要度を示せましたがここからどうやって検索を実装するかを考えなければなりません。

普段よく使うgoogleなどの検索エンジンで私たちは 探したい文章に含まれてそうな単語の羅列を入力して検索しています.

つまり、現在得られている あるdocの単語の重要度から
入力された単語の羅列(query) を重要な単語として含むdocを探すことができれば うまくいきそうです.

しかしこれではまだ具体的にどうすればプログラムで実装できるか考えられないのですが、まぁとりあえずいろいろ考えてみましょう

比較しやすくする

入力された単語の羅列というのは面倒なのでここからqueryと呼びます
queryは重要な単語の羅列なのですが これはある種の文章として受け取れます.
なのでqueryはTF-IDFで統計してやることができて、

そうするとqueryとcorpus上のdocを楽に比べることができるかもしれません.
おなじTF-IDFという値で正規化できましたからね.

扱いやすくする

みなさんは辞書中で目的の文章を調べるのにどうやっているでしょうか.
索引(index)を引きます.

つまり
関連させたいことはあるdocで単語tがどれだけ重要なのかをうまく示せるindexがほしいです.
これはテーブルで示すとわかりやすそうですよね

メロス その他の単語...
doc1 1.0 0.1 ...
doc2 0.0 0.9 ...

これは良さそうですよね
プログラムでは配列として表せそうで良さそうです.

[
doc1:[単語1のTF-IDF, 単語2のTF-IDF, 単語3のTF-IDF, ...]
doc2:[単語1のTF-IDF, 単語2のTF-IDF, 単語3のTF-IDF, ...]
]

こうすると、indexに対応する単語が存在し ある単語の重要度を知りたければ indexから重要度を取ってこればいいだけになりました.

あとこれはvectorとしてみることもできて、 内積をとればありうる最大値と最小値の間で、文章がどれだけ似ているかもわかるようになります.

いろいろ便利につかえそう(。。)

設計

TF-IDFをつかって文章を検索する方法がまとまりました

まず文章集合からindexを作ります
このindexは

[
doc1:[単語1のTF-IDF, 単語2のTF-IDF, 単語3のTF-IDF, ...]
doc2:[単語1のTF-IDF, 単語2のTF-IDF, 単語3のTF-IDF, ...]
]

のような形をとるようにします.

そしてqueryは比較操作がしやすいよう

query:[単語1のTF-IDF, 単語2のTF-IDF, 単語3のTF-IDF, ...]

とすることにしました.

あとはqueryとこれらdocのすべてで内積を取れば
値の大きいものは類似していて
ちいさいものは似ていないはずなので

queryとこれらdocのすべてで内積 の値の大きい順にソートする

これで検索が実装できそうです.

実際に実装する

さて今まではかなり理想的で、実際に実装したときのパフォーマンスなどを考えていません.

なのでまず改善できそうなところをさくっと改善しちゃいましょう

目標

  • メモリ使用量を小さく
  • 高速に
  • 意味ない単語に重要度を割り振らないように

単語統計器の実装

TF, IDFの改良

vectorとして比較したいとき、0-1に正規化されていたほうが便利です
IDFの値域は0-∞をとるのでこれをなんとかしなければなりません.

よく知られている方法としてsoft-maxがあります.
これはAI関連でよく使われていて、 vectorの値がそれぞれ0-1に正規化され 要素の合計が1になるというとてもよいものなのですが 計算量が多いので
今回は0-1に正規化するために単におこりうる最大値で割ります

IDF改良

IDF(t) = \frac{\ln(1.0 + \frac{corpus中のdoc数}{1.0 + 単語tを含むdoc数})}{MAXidf}

MAX IDF 自体はMAXidf = \ln(1.0 + \frac{corpus中のdoc数}{2.0}) で出てきますね

実装

    #[inline(always)]
    pub fn idf_max(&self, total_doc_count: u64) -> f64 {
        (1.0 + total_doc_count as f64 / (2.0)).ln()
    }

    #[inline(always)]
    pub fn idf_calc(total_doc_count: u64, max_idf: f64, doc_count: u32) -> f64 {
        (1.0 + total_doc_count as f64 / (1.0 + doc_count as f64)).ln() / max_idf
    }

TF改良

TFもいじります. 今のままのTFはただたんに文章でどれだけその単語が内容を占めてるか なのですが
これだと
"TF-IDFのお話"
と今この記事
をTFでスコアリングしたとき 両文章のキーワードであるTF-IDFのTF値は
"TF-IDFのお話" tf = 0.33
この記事 tf = 0.01
と、"TF-IDF"のお話という内容のうっすい文章のほうが圧倒的に高いスコアをたたき出します
いうのは "TF-IDFのお話" という文章には3単語しかなくその中で"TF-IDF"は 1を占めてるためですね。
これはスコアを文章長で乗算したりしてもよいんですが、indexを0-1の範囲に制限すると、精度の問題が出てきてしまって よくないので

TF(t) = \frac{\ln(ある単語tの出現回数 + 1.0)}{\ln(doc中単語で最大の出現回数 + 1.0)}

自然対数を使って異常に大きな値を抑え、最大出現する単語の出現回数で割ります。

実装

    #[inline(always)]
    pub fn tf_calc(max_count: u32, count: u32) -> f64 {
        (count as f64 + 1.0).ln() / (max_count as f64 + 1.0).ln()
    }

効率化

TF−IDFを0−1に正規化できました。
次にメモリ効率を良くしたいと思います。
Float64は浮動小数を効率良く扱えますが、今回、0-1に正規化したのでFloat系がもつ指数部分が過剰に感じられます。
Float16などにするとメモリ効率があがりますが精度が下がりすぎてしまいます。
なのでu16を使います。

u16::Minを0 u16::MAXを1として、65536段階でおなじサイズのFloat16より0-1の正規化された値を表す場合優れています。

また一般にCPUは整数演算のほうが速度がはやいです.
なのでTFなどを演算する時点でもうu16にしてしまいましょう。

実装 変更

    #[inline(always)]
    pub fn idf_calc_as_u16(total_doc_count: u64, max_idf: f64, doc_count: u32) -> u16 {
        let normalized_value = (1.0 + total_doc_count as f64 / (1.0 + doc_count as f64)).ln() / max_idf;
        // 0~65535 にスケール
        (normalized_value * 65535.0).round() as u16
    }

    #[inline(always)]
    pub fn tf_calc_as_u16(max_count: u32, count: u32) -> u16 {
        let normalized_value = (count as f64 + 1.0).ln() / (max_count as f64 + 1.0).ln();
        // 0~65535 にスケール
        (normalized_value * 65535.0).round() as u16
    }

実装

https://github.com/371tti/tf-idf-vectorizer/blob/fix-git/src%2Fvectorizer%2Ftoken.rs

で色々必要だったりしなかったりするものを追加したのがこんな感じになります。
add_token()で単語集合とその単語の頻度を記録していって
各メソッドでいろんな統計をとれるやつですね。

Index生成

struct TokenFrequency は1docの単語集合とその統計を作ります
でそれをいったんまとめるものを実装します

https://github.com/371tti/tf-idf-vectorizer/blob/fix-git/src%2Fvectorizer%2Fanalyzer.rs

struct Document
あるdocのstruct TokenFrequencyを含む情報を入れとくためのもので、

struct DocumentAnalyzer<IdType>
struct DocumentとそのIDを保持して
corpus全体をまとめます。

でこれはあくまでIndexを生成するため、統計情報を収集するためのものです。
まぁ文章解析とかで再利用するためですね。

Index構造体作る

じゃあIndex構造体を作ります。

pub struct Index<IdType>
where
    IdType: Clone + Eq + std::hash::Hash + Serialize + std::fmt::Debug,
{
    // doc_id -> (圧縮ベクトル, 文書の総トークン数)
    // 圧縮ベクトル: インデックス順にトークンの TF を保持
    pub index: HashMap<IdType, (DefaultSparseVec<u16>, u64 /* token num */)>,
    pub avg_tokens_len: u64,  // 全文書の平均トークン長
    pub max_tokens_len: u64,  // 全文書の最大トークン長
    pub idf: Map<Vec<u8>>,    // fst::Map 形式の IDF
    pub total_doc_count: u64, // 文書総数
}

こんな感じですかね。

HashMap<IdType, (DefaultSparseVec<u16>, u64 /* token num */)>
がドキュメントのIDとTF−IDFベクトルを持ちます

ここで出てくるDefaultSparseVec<u16>はスパースベクトルですね
https://github.com/371tti/vec-plus/blob/main/src%2Fvec%2Fdefault_sparse_vec.rs
スパースベクトルは0をデータがないものとしてメモリを節約するもので、TF-IDF-Vectorは性質上、現れない単語の向きが0になるのでスパースベクトルを使用することで大幅に使用メモリを減らせます.

他の値についてはIndex自体からある程度統計情報を復元するためのもので、
Index同士の合成や高度な検索をするときに役に立つものです。

Index構造体に統計情報を変換する

struct DocumentAnalyzer<IdType>に実装

    pub fn generate_index(&self) -> Index<IdType> {
        // 統計の初期化
        let total_doc_tokens_len = Arc::new(AtomicU64::new(0));
        let max_doc_tokens_len = Arc::new(AtomicU64::new(0));
        let now_prosessing = Arc::new(AtomicU64::new(0));

        // idf のfst生成
        let mut builder = MapBuilder::memory();
        let mut idf_vec = self.idf.get_idf_vector_ref_parallel(self.total_doc_count);
        idf_vec.sort_by(|a, b| a.0.cmp(b.0));
        for (token, idf) in idf_vec {
            builder.insert(token.as_bytes(), idf as u64).unwrap();
        }
        let idf = Arc::new(builder.into_map());

        // 並列処理用のスレッドセーフなIndex
        let index = Arc::new(Mutex::new(HashMap::new()));

        // ドキュメントごとの処理を並列化
        self.documents.par_iter().for_each(|(id, document)| {
            now_prosessing.fetch_add(1, Ordering::SeqCst);
            let mut tf_idf_sort_vec: Vec<u16> = Vec::new();

            let tf_idf_vec: HashMap<String, u16> =
                document.tokens.get_tfidf_hashmap_fst_parallel(&idf);

            let mut stream = idf.stream();
            while let Some((token, _)) = stream.next() {
                let tf_idf = *tf_idf_vec.get(str::from_utf8(token).unwrap()).unwrap_or(&0);
                tf_idf_sort_vec.push(tf_idf);
            }

            let tf_idf_csvec: DefaultSparseVec<u16> = DefaultSparseVec::from(tf_idf_sort_vec);
            let doc_tokens_len = document.tokens.get_total_token_count();

            total_doc_tokens_len.fetch_add(doc_tokens_len, Ordering::SeqCst);

            max_doc_tokens_len.fetch_max(doc_tokens_len, Ordering::SeqCst);

            let mut index_guard = index.lock().unwrap();
            index_guard.insert(id.clone(), (tf_idf_csvec, doc_tokens_len));
        });

        // 統計計算
        let avg_total_doc_tokens_len = (total_doc_tokens_len.load(Ordering::SeqCst)
            / self.total_doc_count as u64) as u64;
        let max_doc_tokens_len = max_doc_tokens_len.load(Ordering::SeqCst);

        // indexの返却
        Index::new_with_set(
            Arc::try_unwrap(index).unwrap_or(HashMap::new().into()).into_inner().unwrap(),
            Arc::try_unwrap(idf).unwrap(),
            avg_total_doc_tokens_len,
            max_doc_tokens_len,
            self.total_doc_count,
        )
    }

脳死で書いたんですがまぁこんな感じですね
ここで出てきているFSTは有限状態遷移器(Finite State Transducer, FST)というもので ツリー状かつおなじデータを共有するようにできており、
ソートされているかつ
高速な検索かつ
メモリ使用が小さい
という利点があるMapのようなものです。

やっていることとしてはstruct TokenFrequencyからメソッドを使ってTFとIDFを算出してるくらいですね。

検索の実装

では内積による類似度で検索するcosin類似度をstruct Indexに実装しちゃいましょう

impl<IdType> Index<IdType>
where
    IdType: Clone + Eq + std::hash::Hash + Serialize + std::fmt::Debug,
{
    // ---------------------------------------------------------------------------------------------
    // コンストラクタ
    // ---------------------------------------------------------------------------------------------
    pub fn new_with_set(
        index: HashMap<IdType, (DefaultSparseVec<u16>, u64)>,
        idf: Map<Vec<u8>>,
        avg_tokens_len: u64,
        max_tokens_len: u64,
        total_doc_count: u64,
    ) -> Self {
        Self {
            index,
            idf,
            avg_tokens_len,
            max_tokens_len,
            total_doc_count,
        }
    }

    // ---------------------------------------------------------------------------------------------
    // 公開メソッド: 検索 (Cosine Similarity)
    // ---------------------------------------------------------------------------------------------

    /// 単純なコサイン類似度検索
    pub fn search_cos_similarity(&self, query: &[&str], n: usize) -> Vec<(&IdType, f64)> {
        // クエリの CsVec を作成
        let query_csvec = self.build_query_csvec(query);

        // 類似度スコアを計算
        let mut similarities = self
            .index
            .iter()
            .filter_map(|(id, (doc_vec, _doc_len))| {
                let similarity = Self::cos_similarity(doc_vec, &query_csvec);
                (similarity > 0.0).then(|| (id, similarity))
            })
            .collect::<Vec<_>>();

        // スコア降順でソートして上位 n 件を返す
        similarities.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
        similarities.truncate(n);

        similarities
    }

    // ---------------------------------------------------------------------------------------------
    // プライベート: クエリ(&str のスライス)を CsVec<u16> に変換 (IDF を用いた TF-IDF)
    // ---------------------------------------------------------------------------------------------
    fn build_query_csvec(&self, query: &[&str]) -> DefaultSparseVec<u16> {
        // 1) クエリトークン頻度を作成
        let mut freq = TokenFrequency::new();
        freq.add_tokens(query);

        // 2) IDF からクエリの TF-IDF (u16) を生成
        let query_tfidf_map: HashMap<String, u16> = freq.get_tfidf_hashmap_fst_parallel(&self.idf);

        // 3) IDF の順序でソートされた Vec<u16> を作る
        let mut sorted_tfidf = Vec::new();
        let mut stream = self.idf.stream();
        while let Some((token_bytes, _)) = stream.next() {
            // トークンは bytes -> &str へ変換
            let token_str = str::from_utf8(token_bytes).unwrap_or("");
            let tfidf = query_tfidf_map.get(token_str).copied().unwrap_or(0);
            sorted_tfidf.push(tfidf);
        }

        // 4) CsVec に変換して返す
        DefaultSparseVec::from(sorted_tfidf)
    }


    // ---------------------------------------------------------------------------------------------
    // プライベート: コサイン類似度
    // ---------------------------------------------------------------------------------------------
    fn cos_similarity(vec_a: &DefaultSparseVec<u16>, vec_b: &DefaultSparseVec<u16>) -> f64 {
        // 内積
        let dot_product = vec_a.u64_dot(vec_b) as f64;

        // ノルム(ベクトルの長さ)
        let norm_a = (vec_a.u64_dot(vec_a) as f64).sqrt();
        let norm_b = (vec_b.u64_dot(vec_b) as f64).sqrt();

        // コサイン類似度を返す
        if norm_a > 0.0 && norm_b > 0.0 {
            dot_product / (norm_a * norm_b)
        } else {
            0.0
        }
    }

やった! これで完成だ!!

改良

コサイン類似度による検索は正直精度が良くないです
というのは重要な単語の類似度を測ってくれるのは良いのですが
重要でない単語の一致度も測ってしまいます。
また文章長にも大きく影響を受けてしまいます。
このため、コサイン類似度による検索というのは本当の意味で文章の類似度をとるものであり、私たちが重要単語の羅列を見せてうまい具合に検索してくれるものではありません。

BM25実装

コサイン類似度とは別の検索アルゴリズムを実装します
BM25(ベストマッチ25)です
これは重要でない単語の一致度や文章長の影響をある程度排除でき、さらにそのパラメーターを調整できます。

    // ---------------------------------------------------------------------------------------------
    // 公開メソッド: BM25 x TF-IDF 検索
    // ---------------------------------------------------------------------------------------------

    pub fn search_bm25_tfidf(&self, query: &[&str], n: usize, k1: f64, b: f64) -> Vec<(&IdType, f64)> {
        let query_csvec = self.build_query_csvec(query);

        let mut similarities = self
            .index
            .iter()
            .filter_map(|(id, (doc_vec, doc_len))| {
                let score = Self::bm25_with_csvec_optimized(
                    &query_csvec,
                    doc_vec,
                    *doc_len,
                    self.avg_tokens_len as f64,
                    k1,
                    b,
                );
                (score > 0.0).then(|| (id, score))
            })
            .collect::<Vec<_>>();

        similarities.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
        similarities.truncate(n);

        similarities
    }

    // ---------------------------------------------------------------------------------------------
    // BM25 実装 (公開: ほかで呼び出したい場合のみ pub)
    // ---------------------------------------------------------------------------------------------
    pub fn bm25_with_csvec_optimized(
        query_vec: &DefaultSparseVec<u16>, // クエリのTF-IDFベクトル(u16)
        doc_vec: &DefaultSparseVec<u16>,   // 文書のTF-IDFベクトル(u16)
        doc_len: u64,           // 文書のトークン数
        avg_doc_len: f64,       // 平均文書長
        k1: f64,                // BM25のパラメータ
        b: f64,                 // 文書長補正のパラメータ
    ) -> f64 {
        // 文書長補正を計算
        let len_norm = 1.0 - b + b * (doc_len as f64 / avg_doc_len);

        // 定数の事前計算
        const MAX_U16_AS_F64: f64 = 1.0 / (u16::MAX as f64); // 1 / 65535.0
        let k1_len_norm = k1 * len_norm;

        // クエリと文書のインデックスおよびデータ配列を直接取得
        let (query_indices, query_data) = (query_vec.as_slice_ind(), query_vec.as_slice_val());
        let (doc_indices, doc_data) = (doc_vec.as_slice_ind(), doc_vec.as_slice_val());

        let (mut q, mut d) = (0, 0);
        let (q_len, d_len) = (query_vec.nnz(), doc_vec.nnz());

        let mut score = 0.0;
        while q < q_len && d < d_len {
            let q_idx = query_indices[q];
            let d_idx = doc_indices[d];

            if q_idx == d_idx {
                let tf_f = (doc_data[d] as f64) * MAX_U16_AS_F64;
                let idf_f = (query_data[q] as f64) * MAX_U16_AS_F64;

                let numerator = tf_f * (k1 + 1.0);
                let denominator = tf_f + k1_len_norm;
                score += idf_f * (numerator / denominator);

                q += 1;
                d += 1;
            } else if q_idx < d_idx {
                q += 1;
            } else {
                d += 1;
            }
        }
        score
    }

実装できた。
でこれで
BM25でWikipedia10万件を検索(debug build)してみるとこんな感じ

BM25でWikipedia10万件を検索(debug build)

なかなか良い感じですよね。

最適化

https://github.com/371tti/tf-idf-vectorizer
これが成果物。

最適化とリリースビルドのパワーで
Wikipedia 10万件を検索してみると。

Enter your search query:
初音ミク
Search results (Time taken: 207.21ms):
1Document ID: index1_2499547_ボカロドリーム.json, Similarity: 0.8795
1Document ID: index1_4222614_プロジェクトセカイ カラフルステージ! feat.初音ミク.json, Similarity: 0.8795
1Document ID: index1_2433575_プロジェクトディーバ.json, Similarity: 0.8795
1Document ID: index1_2133735_初音ミク -Project DIVA Arcade- Original Song Collection.json, Similarity: 0.7892
1Document ID: index1_2279882_MOER feat.初音ミク -2nd anniversary-.json, Similarity: 0.7290
1Document ID: index1_2471134_初音ミク and Future Stars Project mirai.json, Similarity: 0.6600
1Document ID: index1_1305658_ピアプロ.json, Similarity: 0.6528
1Document ID: index1_2748137_EXIT TUNESボカロコンピシリーズ.json, Similarity: 0.6470
1Document ID: index1_2294173_VOCAROCK collection 2.json, Similarity: 0.6211
1Document ID: index1_3274653_マジカルミライ.json, Similarity: 0.6211
1Document ID: index1_4446166_Rockwell (ミュージシャン).json, Similarity: 0.6079
1Document ID: index1_2362978_Wowaka.json, Similarity: 0.5860
1Document ID: index1_4854800_プロジェクトセカイの登場人物.json, Similarity: 0.5552
1Document ID: index1_1777118_Megpoid.json, Similarity: 0.5524
1Document ID: index1_4526485_少女レイ.json, Similarity: 0.5431
1Document ID: index1_4526145_チュルリラ・チュルリラ・ダッダ ッダ!.json, Similarity: 0.5315
1Document ID: index1_4271463_BPM200以上はおやつに含まれます か.json, Similarity: 0.5209
1Document ID: index1_4604613_廃墟の国のアリス.json, Similarity: 0.5124
1Document ID: index1_3334626_MIKU-Pack music & artworks feat.初音ミク.json, Similarity: 0.4864
1Document ID: index1_4406006_インビジブル (kemuの曲).json, Similarity: 0.4776
1Document ID: index1_2313303_1925 (曲).json, Similarity: 0.4693
1Document ID: index1_1336625_ロディ.json, Similarity: 0.4362
1Document ID: index1_4893507_初音尋常小学校.json, Similarity: 0.4241
1Document ID: index1_2766386_さつき が てんこもり.json, Similarity: 0.4173
1Document ID: index1_2775840_ディンゴ (ゲーム会社).json, Similarity: 0.3718
1Document ID: index1_4882665_ROUNDABOUT (キタニタツヤのアル バム).json, Similarity: 0.3574
1Document ID: index1_3752180_#コンパス 戦闘摂理解析システム.json, Similarity: 0.3574
1Document ID: index1_4068113_ツユ.json, Similarity: 0.3387
1Document ID: index1_3712094_邪神ちゃんドロップキック.json, Similarity: 0.3361
1Document ID: index1_1369940_オリジン (曖昧さ回避).json, Similarity: 0.3349
1Document ID: index1_1619033_初音 (歌手).json, Similarity: 0.2963
1Document ID: index1_4079390_山口慎一 (ミュージシャン).json, Similarity: 0.2903
1Document ID: index1_2966536_IA -ARIA ON THE PLANETES-.json, Similarity: 0.2710
1Document ID: index1_3405748_あはれ!名作くん.json, Similarity: 0.2685
1Document ID: index1_1839529_ルーズ・ユアセルフ.json, Similarity: 0.2609
1Document ID: index1_2138909_うさみ航.json, Similarity: 0.2568
1Document ID: index1_1112604_アイドルマスターシリーズの作品 一覧.json, Similarity: 0.2542
1Document ID: index1_2273021_河野美術館本源氏物語.json, Similarity: 0.2542
1Document ID: index1_28373_J-POP.json, Similarity: 0.2417
1Document ID: index1_449055_下田麻美.json, Similarity: 0.2368
1Document ID: index1_4092648_長谷川唯 (声優).json, Similarity: 0.2342
1Document ID: index1_2639099_THE IDOLM@STERの楽曲一覧.json, Similarity: 0.2295
1Document ID: index1_1287408_モンスターハンター フロンティア オンライン.json, Similarity: 0.2220
1Document ID: index1_2078987_Simeji.json, Similarity: 0.2163
1Document ID: index1_1075844_らき☆すた.json, Similarity: 0.2082
1Document ID: index1_4206323_群青 (YOASOBIの曲).json, Similarity: 0.2079
1Document ID: index1_4323942_女学生探偵シリーズ.json, Similarity: 0.2044
1Document ID: index1_338851_提携カード.json, Similarity: 0.2021
1Document ID: index1_832131_落とし穴.json, Similarity: 0.2008
1Document ID: index1_2450719_どうしてこうなった.json, Similarity: 0.2008
1Document ID: index1_4023452_Steamアワード.json, Similarity: 0.1958
1Document ID: index1_1128115_すしあざらし.json, Similarity: 0.1917
1Document ID: index1_4291070_Eoheoh.json, Similarity: 0.1906
1Document ID: index1_894926_チロリアン.json, Similarity: 0.1905
1Document ID: index1_2518146_Imoutoid.json, Similarity: 0.1870
1Document ID: index1_790733_携帯電話ゲーム.json, Similarity: 0.1867
1Document ID: index1_4802309_日本シン人種図鑑.json, Similarity: 0.1860
1Document ID: index1_2909189_BAND-MAID.json, Similarity: 0.1810
1Document ID: index1_1937844_D.C. 〜ダ・カーポ〜 (アニメ).json, Similarity: 0.1798
1Document ID: index1_93284_徳川美術館.json, Similarity: 0.1781
1Document ID: index1_2062955_PlayStation 3.json, Similarity: 0.1769
1Document ID: index1_2026705_ブラック・ジャック.json, Similarity: 0.1762
1Document ID: index1_4927796_白井未来.json, Similarity: 0.1740
1Document ID: index1_344_田中久仁彦.json, Similarity: 0.1715
1Document ID: index1_4281656_ABU TV ソング・フェスティバル2020.json, Similarity: 0.1709
1Document ID: index1_3584710_柴田将平.json, Similarity: 0.1703
1Document ID: index1_2700846_石川界人.json, Similarity: 0.1662
1Document ID: index1_95850_緑川光.json, Similarity: 0.1650
1Document ID: index1_4202373_うらまる.json, Similarity: 0.1588
1Document ID: index1_4050748_豆柴の大群.json, Similarity: 0.1567
1Document ID: index1_3483630_2018年のテレビ (日本).json, Similarity: 0.1563
1Document ID: index1_4597650_だてぃが.json, Similarity: 0.1527
1Document ID: index1_3288996_山本亜衣.json, Similarity: 0.1502
1Document ID: index1_501734_エッチ・ケー・エス.json, Similarity: 0.1500
1Document ID: index1_457135_ラヴスナイパー.json, Similarity: 0.1490
1Document ID: index1_3754184_SEGA World Drivers Championship.json, Similarity: 0.1476
1Document ID: index1_3218662_乖離性ミリオンアーサー.json, Similarity: 0.1475
1Document ID: index1_3815881_さなゑちゃん.json, Similarity: 0.1470
1Document ID: index1_2231778_Rootless.json, Similarity: 0.1457
1Document ID: index1_237913_筑波サーキット.json, Similarity: 0.1455
1Document ID: index1_611146_劇場版ポケットモンスター セレビ ィ 時を超えた遭遇.json, Similarity: 0.1444
1Document ID: index1_1853942_森谷里美.json, Similarity: 0.1428
1Document ID: index1_2497034_源氏物語提要.json, Similarity: 0.1422
1Document ID: index1_1880941_若山詩音.json, Similarity: 0.1419
1Document ID: index1_4820790_金子正男.json, Similarity: 0.1414
1Document ID: index1_1929281_犬のおまわりさん.json, Similarity: 0.1413
1Document ID: index1_2048002_ピコ (歌手).json, Similarity: 0.1411
1Document ID: index1_2742161_タンクトップファイター.json, Similarity: 0.1405
1Document ID: index1_1245021_上田ケンジ.json, Similarity: 0.1389
1Document ID: index1_3475111_アイドル諜報機関LEVEL7.json, Similarity: 0.1388
1Document ID: index1_2333760_家庭の事情 おこんばんわの巻.json, Similarity: 0.1385
1Document ID: index1_3287988_にちようチャップリン.json, Similarity: 0.1382
1Document ID: index1_2127261_森戸みほ.json, Similarity: 0.1373
1Document ID: index1_2761311_大端絵里香.json, Similarity: 0.1357
1Document ID: index1_1279615_Mobage.json, Similarity: 0.1338
1Document ID: index1_1012758_サクライロノキセツ.json, Similarity: 0.1319
1Document ID: index1_4114406_小嶋貴之.json, Similarity: 0.1318
1Document ID: index1_4385094_さとうもか.json, Similarity: 0.1306
1Document ID: index1_4769707_にじさんじの出演一覧.json, Similarity: 0.1284
1Document ID: index1_540702_定岡小百合.json, Similarity: 0.1275

でメモリ使用量は612mb!!
メモリ使用量

楽しいですねNLPは。

Discussion

kanaruskanarus

自分の理解の確認を兼ねた質問なのですが、

  • TF-IDF

    ...
    じゃあTFとIDFは何者なのか ですが

    のところの

    IDF = \log{\frac{単語tを含むdoc数+1}{corpus中のdoc数}}

    って分子分母逆ですかね (?)

  • 単語統計器の実装

    ...

    TF, IDFの改良

    のところの

    MAXidf = \ln(1.0 + \frac{単語tを含むdoc数}{2.0})

    MAXidf = \ln(1.0 + \frac{corpus中のdoc数}{2.0})

    の typo ですかね (?)

371tti371tti

そうですねおかしいですね。
普通に間違えてます。
報告ありがとうございます。
修正しておきます。