ナード戦隊データマン

データサイエンスを用いて悪と戦うぞ

deeptypeを実行するためのパイプライン (エンティティリンキング)

deeptypeは、type systemという「エンティティの型を予測するモデル」を用いてエンティティリンキングの曖昧正解消を行うためのツールです。この記事では、deeptype自体の訓練方法は説明しませんが、deeptypeの訓練済みモデルをどのように使えばエンティティ曖昧正解消ができるのかということと、そのパイプラインについて書きます。

注意: 実行のためにはgpuと20G以上のメモリが必要です。

実行のフロー

Untitled drawing.jpg

文章を入力し、結果としてmentionとentityのペアのリストが出力されます。

メンション候補とは、ngramによって得た出力がmention entity dictionaryに含まれているものだけを得たものです。メンション候補を利用して文書をトークナイズ(分割)し、トークナイズされた単語列と候補メンションをdeeptypeを用いた予測器に入力します。すると、mentionごとのentityが得られますが、その中で役に立たないペアを除外したものが、最終的な結果としてリターンされます。

事前準備

deeptypeのREADME.mdを読んで、訓練までを自力で実行してモデルを生成しておいてください。

次に、便利なモジュールを作成します。(1)メンションの候補を利用してトークナイズするためのモジュール, (2)deeptypeを用いて曖昧正解消するためのモジュール。

モジュール(1) menconn.py

from operator import itemgetter
import numpy as np
import re
import MeCab

def mention_index(ts, ss, sep=' '):
    con = [[] for t in ts]
    fin = [False for t in ts]
    for i, s in enumerate(ss):
        for j, t in enumerate(ts):
            if fin[j] is True:
                continue            
            if con[j]:
                tmp = itemgetter(*con[j])(ss)
                if isinstance(tmp, tuple):
                    tmp = list(tmp) + [ss[i]]
                elif isinstance(tmp, str):
                    arr = []
                    arr.append(tmp)
                    tmp = arr + [ss[i]]
                ndl = sep.join(tmp)
            else:
                ndl = ss[i]
            if ndl == t:
                con[j].append(i)
                fin[j] = True
            elif t.startswith(ss[i]) and not t.startswith(ndl):
                con[j] = [i]
                if ss[i] == t:
                    fin[j] = True
            elif t.startswith(ndl):
                con[j].append(i)
            else:
                con[j] = []
    return con

def is_all_none(arr):
    for a in arr:
        if a is not None:
            return False
    return True

def index_in_sentence(con):
    con_s = con[:]
    stack_list = []
    while(not is_all_none(con_s)):
        appeared_value = []
        appeared_index = []
        stack = []
        for i, c in enumerate(con_s):
            if c is None:
                continue
            if np.sum(np.in1d(c, appeared_value)) > 0:
                continue
            else:
                stack.append(c)
                appeared_index.append(i)
                for v in c:
                    appeared_value.append(v)
        for index in appeared_index:
            con_s[index] = None
        stack_list.append(stack)
    return stack_list

def build_sentence(ss, sl):
    stack = sl[:]
    stack.sort()
    stack = [x for x in stack if x]
    sentence = []
    c = stack.pop(0)
    tmp = []
    for i, s in enumerate(ss):
        if i in c:
            tmp.append(i)
            if c[-1] == i:
                sentence.append(tmp)
                tmp = []
                if stack:
                    c = stack.pop(0)
                    
        else:
            sentence.append([i])
    return sentence

def build_sentences(ss, slist, sep=' '):
    sentences = []
    for sl in slist:
        line = []
        sentence = build_sentence(ss, sl)
        for word in sentence:
            w = itemgetter(*word)(ss)
            if isinstance(w, tuple):
                line.append(sep.join(list(w)))
            elif isinstance(w, str):
                line.append(w)
        sentences.append(line)
    return sentences

def mecab_tokenize(sentence):
    tagger = MeCab.Tagger("-Owakati")
    return tagger.parse(sentence).split()

def run(ts, ss, sep=' '):
    return build_sentences(ss, index_in_sentence(mention_index(ts, ss, sep=sep)), sep=sep)

def ja_tokenize(sentence, ts, tokenize=mecab_tokenize):
    ss = tokenize(sentence)
    result = []
    for d in run(ts, ss, sep=''):
        result += d
    return result

def en_tokenize(sentence, ts):
    ss = re.sub(r"[^\w ]", '', sentence).lower().split()
    result = []
    for d in run(ts, ss):
        result += d
    return result

モジュール(2) typelinking.py

import sys
import os
dataroot = os.path.join(os.path.dirname(__file__), '../')
sys.path.append(os.path.join(dataroot, "learning/"))

import time
import marisa_trie
import pickle
from dataset import *
from wikidata_linker_utils.offset_array import OffsetArray
import train_type as tp
from collections import defaultdict
from tqdm import tqdm
import re

def create_indices2title(infile, outfile):
    out = defaultdict(list)
    with open(infile) as f:
        for line in tqdm(f):
            it = line.replace('\n', '').split('\t')
            target = 'wiki/'
            start = it[0].index(target)
            end = start + len(target)
            k, v = it[0][:start], it[0][end:]
            out[int(it[1])][k] = v
    with open(outfile, 'wb') as f:
        pickle.dump(out, f)

def get_prob(tagger_ins,sentence_splits):
    ps = tagger_ins.predict_proba_sentences([sentence_splits])
    output = [i for i in ps]
    probs = output[0]['type']
    return probs[0]


def load_trie(language_path):
    trie_index2indices_values = OffsetArray.load(
        join(language_path, "trie_index2indices")
    )
    trie_index2indices_counts = OffsetArray(
        np.load(join(language_path, "trie_index2indices_counts.npy")),
        trie_index2indices_values.offsets
    )
    trie = marisa_trie.Trie().load(
        join(language_path, "trie.marisa")
    )
    return trie_index2indices_values, trie_index2indices_counts, trie

def solve_indices_and_linkprob(mention, trie, trie_index2indices_values, trie_index2indices_counts, min_prob=0.01):
    anchor = trie.get(mention)
    if anchor is not None:
        indices = trie_index2indices_values[anchor]
        link_probs = trie_index2indices_counts[anchor]
        link_probs = link_probs / link_probs.sum()
        mask = link_probs > min_prob
        indices = indices[mask]
        link_probs = link_probs[mask]
    else:
        indices = None
        link_probs = 0.0
    return indices, link_probs

def simple_tokenize(sentence):
    sentence = re.sub(r'[^\w ]', '', sentence).lower()
    return sentence.split()

def solve_model_probs(sentence, tagger, tokenize=simple_tokenize):
    sent_splits = tokenize(sentence)
    model_probs = get_prob(tagger,sent_splits)
    return sent_splits, model_probs

def solve_type_probs(mention, sent_splits, model_probs, type_oracle, indices, alpha_type_belief=0.5):
    try:
        token_location = sent_splits.index(mention)
        type_belief = model_probs[token_location]
        assignments = type_oracle.classify(indices)
        type_probs = type_belief[assignments]
        type_probs = alpha_type_belief * type_probs + (1.0 - alpha_type_belief)
    except ValueError:
        type_probs = None
    return type_probs

def solve_full_score(link_probs, type_probs, beta=0.99):
    full_score = link_probs * (1.0 - beta + beta * type_probs)
    return full_score

def pick_top_entity(full_score, indices, indices2title):
    index = full_score.argmax()
    top_pick = indices[index]
    return indices2title[top_pick]

def run(mentions, sent_splits, model_probs,  indices2title, type_oracle, trie, trie_index2indices_values, trie_index2indices_counts, only_link=False):
    entities = []
    for mention in mentions:
        indices, link_probs = solve_indices_and_linkprob(mention, trie, trie_index2indices_values, trie_index2indices_counts)
        if indices is not None:
            if only_link:
                full_score = link_probs
            else:
                type_probs = solve_type_probs(mention, sent_splits, model_probs, type_oracle, indices)
                if type_probs is not None:
                    full_score = solve_full_score(link_probs, type_probs)
                    entity = pick_top_entity(full_score, indices, indices2title)
                else:
                    entity = None
        else:
            entity = None
        entities.append(entity)
    return entities

モジュールの置き場所

deeptypeのlearningをロードするため、deeptype/module/に配置してください。

jupyter notebookから試す

In[1]:

import sys
sys.path.append("../module/")
sys.path.append("../learning/")

import pickle
import menconn
from typelinking import *
import time
import marisa_trie
import pickle
from dataset import *
from wikidata_linker_utils.offset_array import OffsetArray
import train_type as tp
from collections import defaultdict
import re
from functools import partial
from tqdm import tqdm_notebook
import os
from nltk import ngrams

In[2]:

out = defaultdict(dict)
with open("../data/wikidata/wikidata_wikititle2wikidata.tsv") as f:
    for line in tqdm_notebook(f):
        it = line.replace('\n', '').split('\t')
        target = 'wiki/'
        start = it[0].index(target)
        end = start + len(target)
        k, v = it[0][:start], it[0][end:]
        out[int(it[1])][k] = v
with open("../data/wikidata/indicies2wikititle.pkl", 'wb') as f:
    pickle.dump(out, f)

In[3]:

dataroot = '..'
tagger = tp.SequenceTagger(os.path.join(dataroot,'ja_model/'))
type_oracle = load_oracle_classification(os.path.join(dataroot, "data/classifications/type_classification"))
trie_index2indices_values, trie_index2indices_counts, trie = load_trie(os.path.join(dataroot, 'data/ja_trie'))
with open(os.path.join(dataroot, 'data/wikidata/indicies2wikititle.pkl'), 'rb') as hdl:
    indices2title = pickle.load(hdl)

In[3]では、事前計算が可能な部分を関数の外で求めることによって、実用面で高速化できるようになっています。

In[4]:

import MeCab
start = time.time()
mtag = MeCab.Tagger('-Owakati')
sentence = """
バラク・オバマは基本的に言ってインテリ層に人気のある黒人だったが、
ドナルド・トランプは白人主義者や陰謀論者から人気を集めている。
"""
sentence_parsed = mtag.parse(sentence)
ts = []
sep = ''
for n in range(1, 10):
    n_grams = ngrams(sentence_parsed.split(), n)
    for grams in n_grams:
        ts.append(sep.join(list(grams)))
ts = [t for t in ts if trie.get(t) is not None]
end = time.time()
print('Time:{}'.format(end-start))

Out[4]: Time:0.001161336898803711

In[4]では、候補メンションを求めています。この時点で日本語のストップワードを利用することも可能です。

In[5]:

import time
start = time.time()
results = None
tokenize = partial(menconn.ja_tokenize, ts=ts)
sent_splits, model_probs = solve_model_probs(sentence, tagger, tokenize=tokenize)
entities = run(ts, sent_splits, model_probs, indices2title, type_oracle, trie, trie_index2indices_values, trie_index2indices_counts)
preds = []
for entity in entities:
    if entity is not None:
        preds.append(entity['ja'])
    else:
        preds.append(None)
results = [{'mention':x, 'pred': y} for x,y in zip(ts,preds)]
end = time.time()
print("Time:{}".format(end-start))

Out[5]: Time:0.019841432571411133

resultsの中身:

[{'mention': 'バラク', 'pred': 'バラク (チャガタイ家)'},
 {'mention': '・', 'pred': '中黒'},
 {'mention': 'オバマ', 'pred': 'バラク・オバマ'},
 {'mention': 'は', 'pred': 'は'},
 {'mention': '基本', 'pred': '基本'},
 {'mention': '的', 'pred': '的'},
 {'mention': 'に', 'pred': '日本の鉄道駅一覧 に'},
 {'mention': 'て', 'pred': 'て'},
 {'mention': 'インテリ', 'pred': 'インテリ'},
 {'mention': '層', 'pred': '層 (数学)'},
 {'mention': 'に', 'pred': '日本の鉄道駅一覧 に'},
 {'mention': '人気', 'pred': '人気'},
 {'mention': 'の', 'pred': 'の'},
 {'mention': 'ある', 'pred': '存在記号'},
 {'mention': '黒人', 'pred': '黒人'},
 {'mention': 'た', 'pred': 'た'},
 {'mention': 'が', 'pred': 'が'},
 {'mention': '、', 'pred': '読点'},
 {'mention': 'ドナルド', 'pred': 'ドナルドダック'},
 {'mention': '・', 'pred': '中黒'},
 {'mention': 'トランプ', 'pred': 'トランプ'},
 {'mention': 'は', 'pred': 'は'},
 {'mention': '白人', 'pred': '白人'},
 {'mention': '主義', 'pred': '主義'},
 {'mention': '者', 'pred': '個人'},
 {'mention': 'や', 'pred': 'や'},
 {'mention': '陰謀', 'pred': '陰謀'},
 {'mention': '論', 'pred': '論'},
 {'mention': '者', 'pred': '個人'},
 {'mention': 'から', 'pred': '1月2日'},
 {'mention': '人気', 'pred': '人気'},
 {'mention': 'を', 'pred': 'を'},
 {'mention': '集め', 'pred': 'コレクション'},
 {'mention': 'て', 'pred': 'て'},
 {'mention': 'いる', 'pred': '入部'},
 {'mention': '。', 'pred': '句点'},
 {'mention': 'のある', 'pred': '熊野丸'},
 {'mention': 'だった', 'pred': 'フジネットワーク'},
 {'mention': 'たが', 'pred': '箍'},
 {'mention': '主義者', 'pred': '主義者'},
 {'mention': '陰謀論', 'pred': '陰謀論'},
 {'mention': 'バラク・オバマ', 'pred': 'バラク・オバマ'},
 {'mention': 'ドナルド・トランプ', 'pred': 'ドナルド・トランプ'},
 {'mention': '陰謀論者', 'pred': '陰謀論'}]

In[5]で求めたresultsには、役立たないメンションが含まれていることがわかります。これらのメンションを最終的に除外することによって最終結果が求まりますが、メンションの除外のためには以前の記事である以下のリンクが参考になると思います。

https://qiita.com/sugiyamath/items/0484ff1ffc85297dd669

参考:

[1] https://github.com/openai/deeptype [2] https://arxiv.org/abs/1802.01021 [3] https://blog.openai.com/discovering-types-for-entity-disambiguation/