ナード戦隊データマン

機械学習と自然言語処理についてのブログ

VoxELの5つの言語のELモデルを自動作成

VoxEL1とは、マルチリンガルのエンティティリンキングのための評価用データセットです。

やりたいこと

複数言語に対応できるアルゴリズムを用いてELを言語ごとに訓練します。ここで使う言語はVoxELで定義されている、ドイツ語・イタリア語・スペイン語・フランス語・英語とします。

スクリプト全体の実行をテストしていないので、訓練以降の事柄(VoxELでの評価)は今回は行いません。

実行方法

git clone https://github.com/sugiyamath/elmult
cd elmult
pip install -r requirements.txt
python install_nltk.py
cd project
chmod +x run.sh
./run.sh

run.shの中身

#!/bin/bash

cd wiki_processor
./download_wikipedia.sh &
./download_entity_vector.sh &
wait

./extract.sh
./fix_txt_format_entvec.sh

cd ../train
python3 train.py

まず、wiki_processorと呼ばれるwikipedia抽出ツール群に入り、5つの言語のwikipediaのダンプと、Ousiaのエンティティベクトルをダウンロードします。

次に、extract.shで必要なデータを抽出します。例えば、mention-entity統計(メンションとエンティティの各々のペアが出現した回数の辞書)や、訓練データなどがWikipediaから抽出されます。また、Wikipediaからword2vecも訓練されます。

そして、fix_txt_format_entvec.shによって、エンティティベクトルの読み込みを高速化するためにバイナリ化します。

最後に、それらのデータの存在性をtrain.py内で確かめており、必要なデータの存在性がチェックできたら、訓練が開始されます。

どんなモデルなのか

train.pyの中では、以下のモデルを定義しています。

def build_model(
        embedding_matrix,
        n_words,
        n_char,
        wdim=200,
        cdim=25,
        rdim=1,
        entdim=100,
):
    in1 = Input(batch_shape=(None, None), dtype='int32')
    in2 = Input(batch_shape=(None, None, None), dtype='int32')
    in3 = Input(batch_shape=(None, None), dtype='int32')
    in5 = Input(shape=(entdim, ))
    in6 = Input(shape=(1, ))
    wemb1 = Embedding(n_words,
                      wdim,
                      weights=[embedding_matrix],
                      trainable=False,
                      mask_zero=False)(in1)
    cemb1 = Embedding(n_char, cdim, mask_zero=False)(in2)
    cemb1 = TimeDistributed(Bidirectional(LSTM(cdim)))(cemb1)
    remb1 = Embedding(2, rdim, mask_zero=False)(in3)
    out = Concatenate()([wemb1, cemb1, remb1])
    out = Dropout(0.5)(out)
    out = SeparableConv1D(32, kernel_size=3, padding="same",
                          activation="relu")(out)
    out1 = GlobalAvgPool1D()(out)
    out2 = GlobalMaxPool1D()(out)
    out = Concatenate()([out1, out2])

    x3 = Dense(512, activation="relu")(in5)
    x4 = Dense(1, activation="relu")(in6)

    out = Dense(512, activation="relu")(out)
    out = Concatenate()([out, x3])
    out = Dense(512, activation="relu")(out)
    out = Concatenate()([out, x4])
    out = Dense(512, activation="relu")(out)
    out = Dropout(0.5)(out)
    out = Dense(1, activation="sigmoid")(out)
    model = Model([in1, in2, in3, in5, in6], out)
    model.compile(optimizer="nadam",
                  loss="binary_crossentropy",
                  metrics=["acc"])
    return model

入力1は単語IDの系列で、入力2は単語を更に文字で分割したものです。入力3は対象のメンションの位置を1、そうでない位置を0とした系列です。入力5は、候補エンティティのエンティティベクトルです。入力6は、リンク確率(対象メンションにおいて、候補エンティティが出現する確率)です。

入力1,2,3はEmbeddingへ入力され、結合されます。結合されたものは、CNNを通し、プーリングされます。

入力5, 6は、プーリングされた1,2,3と順次結合され、DenseとDropoutを通ります。

最終的に、「当該メンションの候補エンティティは抽出対象であるか」を2値で予測します。

※ OOMが生じる場合はネットワークのサイズを変更してください。

課題

VoxELを使ってどうやって評価するのかというのが課題です。というのも、VoxELは、正解エンティティのwikipedia言語が複数混在しており、例えばフランス語であれば、フランス語wikipediaと英語wikipediaが混在しています。なので、langlinksテーブルを用いて英語エンティティへ変換する必要があります。変換できないものについてはそのまま残して良いでしょう。

(ただ、この評価方法が正しい評価方法なのかは微妙です。)

VoxELをjsonへ変換する

とりあえず、VoxELをjsonへ変換しておきます。NIFという形式になっていて扱いにくいからです。

# coding: utf-8
from pynif import NIFCollection
import json
 
 
def nif2dict(nif):
    out = []
    for context in nif.contexts:
        if context.mention is None:
            continue
        tmp = {"sentence": context.mention, "entities": []}
        for phrase in context.phrases:
            try:
                tmp["entities"].append(
                    {
                        "entity": phrase.taIdentRef,
                        "begin": phrase.beginIndex,
                        "end": phrase.endIndex
                    }
                )
            except:
                continue
 
        if tmp["entities"]:
            out.append(tmp)
    return out
 
 
def nif2json(lang="en"):
    paths = ["./VoxEL/rVoxEL-{}.ttl", "./VoxEL/sVoxEL-{}.ttl"]
    prefix = ["r", "s"]
 
    for path, p in zip(paths, prefix):
        with open(path.format(lang)) as f:
            data = NIFCollection.loads(f.read(), format='turtle')
        out = nif2dict(data)
        with open("./{}_{}.json".format(p, lang), "w") as f:
            json.dump(out, f, indent=4)
 
 
def run(langs=["en", "de", "es", "fr", "it"]):
    for lang in langs:
        nif2json(lang)
 
 
if __name__ == "__main__":
    run()

参考