日本語Wikipediaのデータで言語モデル(KenLM, RNNLM)を学習させる手順のメモです。

kenlm | GitHub
https://github.com/kpu/kenlm

Faster RNNLM (HS/NCE) toolkit | GitHub
https://github.com/yandex/faster-rnnlm

動作確認環境は、次のとおり。

  • Ubuntu Linux 20.04 / WSL2
  • Python 3.10.11

Wikipediaデータの準備

日本語WikipediaのデータとWikiExtractorをダウンロードし、展開します.

mkdir -p data/wikipedia && cd $_
wget https://dumps.wikimedia.org/jawiki/latest/jawiki-latest-pages-articles.xml.bz2
wget https://raw.githubusercontent.com/apertium/WikiExtractor/master/WikiExtractor.py
python WikiExtractor.py --infn jawiki-latest-pages-articles.xml.bz2
cd ../..

data/wikipedia配下に、wiki.txtが出来ていることを確認します.

入手したテキストデータを、文字単位に分かち書きしたファイルに変換します。
ここでは、次のスクリプトで変換処理を行います。

prepare_wikidata.py

import sys

def scan_wikitext(wikitext_path):
    def print_entry(counter, title, body):
        for ln in body.split('\n'):
            if len(ln) > 0 and ln.endswith("。"):
                ln = " ".join(ln.strip())
                print(ln)
        print(f"{counter + 1}: {entry_title} processed", file=sys.stderr)

    with open(wikitext_path) as f:
        entry_counter = 0
        entry_title = None
        entry_body = ''
        for ln in f:
            if len(ln) == 1:
                if entry_title is None:
                    continue
                print_entry(entry_counter, entry_title, entry_body)
                entry_counter += 1
                entry_title = None
            elif entry_title is None:
                entry_title = ln.strip()[:-1]
                entry_body = ''
            else:
                entry_body += ln
        if entry_title is not None:
            print_entry(entry_counter, entry_title, entry_body)

if __name__ == '__main__':
    if len(sys.argv) == 0:
        print("usage: python prepare_wikidata.py [path of wiki.txt]")
    scan_wikitext(sys.argv[1])

スクリプトは次のように実行します。

python prepare_wikidata.py data/wikipedia/wiki.txt > data/wikipedia/wiki_character_split.txt

data/wikipedia配下に、wiki_character_split.txtが出来ていることを確認します.

KenLMでの学習と予測

次に、KenLMで言語モデルの学習と予測を行います。

kenlmのビルド

まずは、KenLMをビルドします。 次のREADMEのとおり実施します。

kenlm | GitHub
https://github.com/kpu/kenlm

以下のコマンドで依存Packageをインストールします。
このコマンドはDebian/Ubuntu向けです、他の環境は公式のREADMEを参照してください。

sudo apt install build-essential cmake libboost-system-dev libboost-thread-dev libboost-program-options-dev libboost-test-dev libeigen3-dev zlib1g-dev libbz2-dev liblzma-dev

ソースコードをcloneし、ビルドします。

git clone git@github.com:kpu/kenlm.git
cd kenlm
mkdir -p build
cd build
cmake ..
make -j 4

次のコマンドを実行して、ヘルプが出力されていればビルドは完了です。

bin/lmplz --help
bin/build_binary --help
cd ../../

モデルの学習

入力データをシャッフルします。

shuf data/wikipedia/wiki_character_split.txt > wiki_shuffled.txt

モデルを学習させます。

mkdir -p models/wikipedia/kenlm
kenlm/build/bin/lmplz --order 4 --discount_fallback < wiki_shuffled.txt > models/wikipedia/kenlm/kenlm_model.arpa
kenlm/build/bin/build_binary models/wikipedia/kenlm/kenlm_model.arpa models/wikipedia/kenlm/kenlm_model.bin

予測(perplexityの出力)

モデルを使って、テキストのperplexityを出力します。
ここでは、次のスクリプトで予測処理を行います。

predict_kenlm.py

import kenlm
import os

MODEL_BIN='models/wikipedia/kenlm/kenlm_model.bin'

if __name__ == '__main__':
    if not os.path.exists(MODEL_BIN):
        raise Exception("model file not found: {}".format(MODEL_BIN))
    # モデルのロード
    model = kenlm.LanguageModel(MODEL_BIN)

    # 予測
    for txt in [
        "脱字が存在する文章です。",
        "脱字が存在する文章す。",
    ]:
        sentence = " ".join(txt.strip())
        prob = model.score(sentence, bos=True, eos=True)
        perplexity = model.perplexity(sentence)
        print(perplexity, prob, txt)

        cnt = 0
        for prob, _, _ in model.full_scores(sentence):
            chara = sentence.split(' ')[cnt] if cnt < len(txt) else ''
            print(" ", prob, chara)
            cnt += 1

kenlmのpythonライブラリをインストールします。

pip install https://github.com/kpu/kenlm/archive/master.zip

予測処理を実行して、Perplexityを出力します。

python predict_kenlm.py

以下のような出力になり、誤った文の方がPerplexityが高くなっていることが見えます。

43.4621701753186 -21.295448303222656 脱字が存在する文章です。
  -4.088757514953613 脱
  -3.4269466400146484 字
  -0.9862440228462219 が
  -2.4829885959625244 存
  -0.0017122072167694569 在
  -0.27959761023521423 す
  -0.001823164988309145 る
  -3.210365056991577 文
  -1.1511752605438232 章
  -0.8413480520248413 で
  -2.7702455520629883 す
  -1.0101044178009033 。
  -1.0441397428512573
96.10513676993703 -23.792959213256836 脱字が存在する文章す。
  -4.088757514953613 脱
  -3.4269466400146484 字
  -0.9862440228462219 が
  -2.4829885959625244 存
  -0.0017122072167694569 在
  -0.27959761023521423 す
  -0.001823164988309145 る
  -3.210365056991577 文
  -1.1511752605438232 章
  -3.458188056945801 す
  -2.787339448928833 。
  -1.9178202152252197

RNNLMでの学習と予測

次に、RNNLMで言語モデルの学習と予測を行います。

Faster RNNLMのビルド

まずは、Faster RNNLMをビルドします。
次のREADMEのとおり実施します。

Faster RNNLM (HS/NCE) toolkit | GitHub
https://github.com/yandex/faster-rnnlm

ソースコードをcloneします。

git clone git@github.com:yandex/faster-rnnlm.git
cd faster-rnnlm

eigen3をダウンロードします。
(build.shにまかせるとエラーになるため、手動でダウンロードしています)

wget https://gitlab.com/libeigen/eigen/-/archive/3.2.10/eigen-3.2.10.tar.gz
tar zxf eigen-3.2.10.tar.gz
mv eigen-3.2.10 eigen3

ビルドします。

./build.sh

次のコマンドを実行して、ヘルプが出力されていればビルドは完了です。

faster-rnnlm/rnnlm
cd ..

モデルの学習

入力データをシャッフルし、教師データと検証データに分割します。

shuf data/wikipedia/wiki_character_split.txt > wiki_shuffled.txt
split -n l/1/5 wiki_shuffled.txt > wiki_train.txt
split -n l/2/5 wiki_shuffled.txt >> wiki_train.txt
split -n l/3/5 wiki_shuffled.txt >> wiki_train.txt
split -n l/4/5 wiki_shuffled.txt >> wiki_train.txt
split -n l/5/5 wiki_shuffled.txt > wiki_valid.txt

モデルを学習させます。

mkdir -p models/wikipedia/rnnlm
faster-rnnlm/faster-rnnlm/rnnlm -train wiki_train.txt -valid wiki_valid.txt -rnnlm models/wikipedia/rnnlm/rnnlm_model -hidden 160 -rand-seed 1 -debug 1 -bptt 4 -bptt-block 10

予測(対数確率・エントロピーの出力)

モデルを使って、テキストのperplexityを出力します。
対象の文章が書かれたテキストファイルを用意します。

test1.txt

脱 字 が 存 在 す る 文 章 で す 。

test2.txt

脱 字 が 存 在 す る 文 章 す 。

以下のコマンドで予測を実行します。

faster-rnnlm/faster-rnnlm/rnnlm -rnnlm models/wikipedia/rnnlm/rnnlm_model -test test1.txt
faster-rnnlm/faster-rnnlm/rnnlm -rnnlm models/wikipedia/rnnlm/rnnlm_model -test test2.txt

以下のような出力になり、正しい文の方が対数確率が高くなっていることが見えます。

$ faster-rnnlm/faster-rnnlm/rnnlm -rnnlm models/wikipedia/rnnlm/rnnlm_model -test test1.txt
Read the vocabulary: 16093 words
Restoring existing nnet
Constructing RNN: layer_size=160, layer_type=sigmoid, layer_count=1, maxent_hash_size=0, maxent_order=0, vocab_size=16093, use_nce=0
Contructed HS: arity=2, height=31
-24.181795
Test entropy 6.179245

$ faster-rnnlm/faster-rnnlm/rnnlm -rnnlm models/wikipedia/rnnlm/rnnlm_model -test test2.txt
Read the vocabulary: 16093 words
Restoring existing nnet
Constructing RNN: layer_size=160, layer_type=sigmoid, layer_count=1, maxent_hash_size=0, maxent_order=0, vocab_size=16093, use_nce=0
Contructed HS: arity=2, height=31
-24.415205
Test entropy 6.758796

参考サイト

以下の記事を参考にさせていただきました。

はじめての自然言語処理 第10回 QuartzNet による音声認識の検証 | オブジェクトの広場
https://www.ogis-ri.co.jp/otc/hiroba/technical/similar-document-search/part10.html

以上。