自動タグ付け機能でも作ってみる - 8: Dump/Restoreを実装する

再構成

学習データ(?)としてWikipediaの解析に時間がかかるので、 Wikipediaの解析結果を保存しておいて、キーワードの選定時にはその解析結果を読み込んで使用するよう再構成する。

figure-1

dump/restoreKeywordRanker に追加した。

処理の流れ(再修正版)

併せて処理の流れも少し変更する。

figure-2

Dumpする内容

BM25の関数

$$ CW(w,d) = IDF(w) \cdot TF(w,d) \cdot \frac{k_1 + 1}{k_1 \cdot (1 - b + (b \cdot NDL(d))) + TF(w,d)} $$

を、学習時に使用するWikipediaの文書と、タグ付けしたい文書(以下「対象文書」)を分離した形に変えると、 IDFとNDLの定義が変わる。

  • $ IDF(w) = log( \frac{(\text{対象文書群の総数}) + ( \text{Wikipedia文書の総数} )}{( \text{単語wが出現する対象文書数} ) + ( \text{単語wが出現するWikipediaの文書数} )} ) $
  • $ NDL(d) = \frac{(\text{文書dの単語数}) \cdot (\text{Wikipedia文書の総数} + \text{対象文書群の総数})}{\text{Wikipediaの総単語数 + 対象文書群の総単語数}} $

つまり、Wikipediaの情報として保存するべきデータは次の通り。

figure-3

実装

JSON?とも思ったけど、Wikipediaに登場するすべての単語を保存するとなると、 それなりに大きいものになりそうなので、おとなしく pickle パッケージでバイナリにダンプしておく。

def __init__(self) -> None:
    super().__init__()

    # 単語wの文書dにおける出現回数
    self.query_freq: Dict[Word, Dict[str, int]] = dd(lambda: dd(int))
    # 文書dの単語数
    self.doc_length: Dict[str, int] = dd(int)
    # 総単語数
    self.word_count: int = 0

    # 集計済み全文書数
    self.stored_doc_count: int = 0
    # 集計済み全単語数
    self.stored_word_count: int = 0
    # 集計済み単語出現文書数
    self.stored_word_freq: Dict[Word, int] = dd(int)

def dump(self, stream: BinaryIO) -> None:
    pickle.dump({
        "doc_count": len(self.doc_length),
        "word_count": self.word_count,
        "word_freq": {word: len(freq) for word, freq in self.query_freq.items()},
    }, stream)

def restore(self, stream: BinaryIO) -> None:
    raw = pickle.load(stream)
    self.stored_doc_count = raw['doc_count']
    self.stored_word_count = raw['word_count']
    self.stored_word_freq = raw['word_freq']

こう書いてみると、インクリメンタルに学習できたほうが良さそうに見える。 restore して追加の学習データを取り込んで、再度 dump できるように書き換え。

def dump(self, stream: BinaryIO) -> None:
    word_freq: Dict[Word, int] =
        {word: len(freq) for word, freq in self.query_freq.items()}
    for word, count in self.stored_word_freq.items():
        word_freq[word] += count

    pickle.dump({
        "doc_count": len(self.doc_length) + self.stored_doc_count,
        "word_count": self.word_count + self.stored_word_count,
        "word_freq": word_freq
    }, stream)

def restore(self, stream: BinaryIO) -> None:
    raw = pickle.load(stream)
    self.stored_doc_count = raw['doc_count']
    self.stored_word_count = raw['word_count']
    self.stored_word_freq = raw['word_freq']

restore したデータを重み算出に使う

KeywordRankerweighted_keywords において、 stored_* を計算に組み込んでおく。

@@ -78,16 +78,22 @@ class KeywordRanker(WordStorage):
         すべての文書の平均単語数の逆数
         (文書の総数) / (総単語数)
         '''
-        adli = doc_count / self.word_count
+        adli = (doc_count + self.stored_doc_count) / \
+            (self.word_count + self.stored_word_count)

         ret: Dict[str, List[Word]] = dd(lambda: [])

         for word, freq in self.query_freq.items():
-            idf = log(doc_count / len(freq))
+            idf = log(
+                (self.stored_doc_count + doc_count) / (len(freq) + self.stored_word_freq[word]))

             for path, count in freq.items():

さほど難しくはない。

next

次は、学習データじゃなくてタグ付けしたい対象文書を読み込みできるようにする。 MarkdownParser の実装…