Edited at

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

More than 1 year has passed since last update.

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


リンク確率とは

リンク確率は、あるメンション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.csvをsqliteにぶち込むとなお良いのですが、ここでは簡略化のために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