ナード戦隊データマン

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

リンク確率でメンション検知を行う

メンション検知とは、文章の中で重要性の高い語を抜き出すことを言います。ここでは、リンク確率を用いてルールベースで検知してみます。

リンク確率とは

リンク確率は、あるメンションxがあるとき、「xがアンカーとして現れるwikipedia記事数」を「xがテキストとして現れるwikipedia記事数」で割ったものです。考えただけで時間がかかりそうな処理ですね!

求める手順

  1. アンカーが指し示すエンティティと、アンカーに使われているテキスト(メンション)のペアをwikipedia dumpから抜き出し、それをsqliteへぶち込み、 メンションをインデクスする。
  2. wikipedia dumpからリンク確率を求める。この際に、メンションのテーブルを作るために前述のsqliteデータベースを使う。
  3. 求めたリンク確率を使ってメンション抽出する。(本当は、リンク確率もsqliteにぶち込むと良い。)

mention-entity dictionaryを求める

事前に、enwikiのダンプをダウンロードしてください。(pages-articles) https://dumps.wikimedia.org/enwiki/

# -*- coding: utf-8 -*-
 
import bz2
import logging
import re
from gensim.corpora import wikicorpus
import mwparserfromhell
from itertools import imap
from multiprocessing.pool import Pool
from functools import partial
import multiprocessing
from collections import Counter
import h5py
import gc
  

def _parse_wiki_text(title, wiki_text):
    try:
        return mwparserfromhell.parse(wiki_text)
 
    except Exception:
        logger.exception('Failed to parse wiki text: %s', title)
        return mwparserfromhell.parse('')

def get_entities(dumpfile, outfile):
    ignored_ns = (
        'wikipedia:', 'category:', 'file:', 'portal:', 'template:', 'mediawiki:',
        'user:', 'help:', 'book:', 'draft:'
    )
        
    extracted_pages = wikicorpus.extract_pages(bz2.BZ2File(dumpfile))
    
    for i, (title, wiki_txt, wiki_id) in enumerate(extracted_pages):
        if any([title.lower().startswith(ns) for ns in ignored_ns]):
            continue
        if i % 10000 == 0:
            print("Processed: " + str(c))
            gc.collect()
        for node in _parse_wiki_text(unicode(title), unicode(wiki_txt)).nodes:
            if isinstance(node, mwparserfromhell.nodes.Wikilink):
                if node.text is not None and node.text.strip_code() not in ['', ' ', '  ', '   ']:
                    with open(outfile, "a") as file:
                        file.write(u'\t'.join([node.title.strip_code(), node.text.strip_code()]).encode('utf-8').strip()+"\n")
                else:
                    with open(outfile, "a") as file:
                        file.write(u'\t'.join([node.title.strip_code(), node.title.strip_code()]).encode('utf-8').strip()+"\n")
            del(node)
        del(title)
        del(wiki_txt)


        
def run_it(wikifile, dictfile):
    get_entities(wikifile, dictfile)

if __name__ == "__main__":    
    run_it(
        "enwiki-20180120-pages-articles.xml.bz2",
        "entity_enwiki_all"
    )

linkprobabilityの分母と分子を求める

事前に以下のコードとdbを作成してください。

  1. mention-entity dictionaryをぶち込んだsqliteデータベース。
  2. sqliteデータベースからメンションを取得するためのクラスCandidate。

Candidateの使用は、以下のコードを見て察してください。

# -*- coding: utf-8 -*-
import sys
sys.path.insert(0, "../../modules/")

reload(sys)
sys.setdefaultencoding('utf8')

import bz2
import logging
import re
from gensim.corpora import wikicorpus
import mwparserfromhell
from itertools import imap
from multiprocessing.pool import Pool
from functools import partial
import multiprocessing
from collections import Counter, defaultdict
import h5py
import gc
from candidates import Candidate
import numpy as np
from nltk import ngrams
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
handler = logging.FileHandler('kb.log')
handler.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)

REDIRECT_REGEXP = re.compile(
    ur"(?:\#|#)(?:REDIRECT|転送)[:\s]*(?:\[\[(.*)\]\]|(.*))", re.IGNORECASE
)

def _return_it(data):
    return data

def _parse_wiki_text(title, wiki_text):
    try:
        return mwparserfromhell.parse(wiki_text)
 
    except Exception:
        logger.exception('Failed to parse wiki text: %s', title)
        return mwparserfromhell.parse('')

def prepare_mentions(cand):
    out = []
    table1 = {}
    table2 = {}
    for j,mention in enumerate(cand.itermentions()):
        mention = mention[0]
        if mention is None:
            continue
        if re.match('^[A-Za-z0-9][A-Za-z0-9 ]*$', mention) is None:
            continue
        if len(mention.split()) > 9:
            continue
        out.append(mention)
        table1[mention] = 0
        table2[mention] = 0
    return out, table1, table2

def get_entities(dumpfile, outfile, cand):
    ignored_ns = (
        'wikipedia:', 'category:', 'file:', 'portal:', 'template:', 'mediawiki:',
        'user:', 'help:', 'book:', 'draft:'
    )
        
    extracted_pages = wikicorpus.extract_pages(bz2.BZ2File(dumpfile))

    logger.info('Start preparing mentions')
    mentions, dfs, cas = prepare_mentions(cand)
    logger.info('Preparing Done.')

    pool = Pool(multiprocessing.cpu_count())

    for i, (title, wiki_txt, wiki_id) in enumerate(extracted_pages):
        if i%100 == 0:
            logger.info('Processed: %s', str(i))

        if any([title.lower().startswith(ns) for ns in ignored_ns]):
            continue
        if REDIRECT_REGEXP.match(wiki_txt):
            continue
 
        nodes = _parse_wiki_text(unicode(title), unicode(wiki_txt)).nodes

        texts = []
        links = []
        for node in nodes:
            if isinstance(node, mwparserfromhell.nodes.Wikilink):
                tmp1 = None
                try:
                    tmp1 = node.text.strip_code()
                except:
                    continue
                if tmp1 is not None:
                    texts.append(tmp1)
                    links.append(tmp1)
            elif isinstance(node, mwparserfromhell.nodes.Text):
                tmp2 = None
                try:
                    line = re.sub(r'[^a-zA-Z0-9 ]', '', unicode(node))
                    tmp2 = line.split()
                except:
                    continue
                if tmp2 is not None:
                    texts += tmp2


        grams = []
    for i in xrange(1,10):
            grams += [' '.join(gram) for gram in ngrams(texts, i)]

        texts = grams

        for text in list(set(texts)):
            try:
                dfs[text] += 1
            except:
                continue
        for link in list(set(links)):
            try:
                cas[link] += 1
            except:
                continue

    logger.info('100% done... wait a few minutes.')
    with open(outfile, "w") as f:
        f.write('mention,df,ca\n')
        for mention in mentions:
            f.write("{},{},{}\n".format(mention, dfs[mention], cas[mention]))
    
    #pd.DataFrame({"mentions":mentions, "df":dfs, "ca":cas}).to_csv(outfile)
    
def run_it(wikifile, dictfile, candfile):
    logger.info('Start reading database.')
    cand = Candidate(candfile)
    logger.info('Reading database done.')
    get_entities(wikifile, dictfile, cand)

if __name__ == "__main__":
    run_it(
        "enwiki-20180120-pages-articles.xml.bz2",
        "links2.csv",
        'candidates.db'
    )

link probabilityを用いてメンション検知をする

links2.csvsqliteにぶち込むとなお良いのですが、ここでは簡略化のためにpandasのmergeを用います。ここからはjupyterです。

In[1]:

sentence = """
Obama was born in 1961 in Honolulu, Hawaii, 
two years after the territory was admitted to the Union as the 50th state. 
Raised largely in Hawaii, 
Obama also spent one year of his childhood in Washington State and four years in Indonesia. 
After graduating from Columbia University in New York City in 1983, he worked as a community organizer in Chicago. 
"""

In[2]:

from nltk import ngrams
mentions = []
for n in range(1, 10):
    grams = ngrams(sentence.replace(',', '').replace('.','').split(), n)
    for gram in grams:
        mentions.append(' '.join(gram))

In[3]:

#sqliteを使っていないので時間がかかる
import pandas as pd
df = pd.merge(
    pd.DataFrame(mentions, columns=['mention']),
    pd.read_csv("../links2.csv")
)

In[4]:

df['linkprob'] = df['ca']/df['df']
df[df['linkprob'] > 0.05].drop_duplicates()['mention'].tolist()

Out[4]:

['Obama',
 'Honolulu',
 'Hawaii',
 'Union',
 'Washington',
 'State',
 'Indonesia',
 'Columbia',
 'Chicago',
 'Washington State',
 'New York',
 'community organizer',
 'admitted to the Union']

このように、link probabilityを用いるだけでそれなりのメンション検知ができました。

考察

link probabilityは、wikipedia記事の編集者がどれだけそのメンションを重要だと判断したかを表していると言えます。なぜなら、アンカーとして表されたものはその記事と関連性が強いものだと考えられるからです。

機械学習を用いる場合、Ousiaのiitb datasetsが役に立ちます。 https://github.com/studio-ousia/el-helpfulness-dataset

このデータセットでは、人間のアノテータ60人によってつけられたラベル(メンションの役立ち度)が付属しています。

参考

https://www.slideshare.net/ikuyamada/ss-50334449