はじめに
自然言語処理では,「名寄せ」が往々にして必要になります。「名寄せ」とは,多種多様な表現を同一名にマージする作業のことです。例えば,「ドコモ」や「エヌ・ティ・ティ移動通信網」という語は,すべて同じ「NTTドコモ」を指しています。このように,表現は違っていても同じものを指している語は同一の表現にまとめてしまった方が何かと便利です(逆を言うと,「ドコモ」と「NTTドコモ」を別モノとして扱ってしまうと不都合です)。
いちばん簡単に「名寄せ」をする方法は,「ドコモ→NTTドコモ」のような名寄せ辞書を作ってしまうことです。今回は,オープンな言語資源であるWikipediaを使って,簡単な名寄せ辞書を作ってみたいと思います。
準備
Wikipediaにはリダイレクトという機能があります。
リダイレクト(転送)とは、ある記事へリンクしたときに、別のページに転送する機能のことです。また、そのようなページをリダイレクトページ(転送ページ)と呼びます。(出典: https://ja.wikipedia.org/wiki/Wikipedia:リダイレクト)
例えばWikipediaページのエヌ・ティ・ティ移動通信網というページやドコモというページは,すべて「NTTドコモ」というページへリダイレクトされます。リダイレクトは主に,略称や別名や別の表記のような,表現の違いを吸収するためにあります。つまり,リダイレクト元のページタイトルは,リダイレクト先のページタイトルに「名寄せ」されているといえます。
Wikipediaのリダイレクト関係を使えば名寄せ辞書を作れそうですが,果たしてどうすればよいでしょうか。
ご存知の方も多いと思いますが,Wikipediaでは種々のダンプデータが提供されています。ページ情報やリダイレクト関係のダンプデータもあるので,今回はこれを使って名寄せ辞書を作ってみます(その他にも,全記事のダンプデータを使えば,分散表現を獲得したり上位下位関係を抽出できたりします)。
まず,ページ情報のダンプデータとリダイレクト関係のダンプデータをダウンロードして解凍します。そこそこ大きいので注意してください。最新のダンプデータはここです。
$ wget https://dumps.wikimedia.org/jawiki/latest/jawiki-latest-page.sql.gz
$ gunzip jawiki-latest-page.sql.gz
$ wget https://dumps.wikimedia.org/jawiki/latest/jawiki-latest-redirect.sql.gz
$ gunzip jawiki-latest-redirect.sql.gz
page.sql.gz
がページ情報のダンプデータ,redirect.sql.gz
がリダイレクト関係のダンプデータです。
次に,mysql2sqlite
というユーティリティツールを使って,解凍したダンプデータをSqlite3で扱えるように変換し,pages.db
にインポートします。
$ git clone https://github.com/dumblob/mysql2sqlite.git
$ ./mysql2sqlite jawiki-latest-page.sql | sqlite3 pages.db
$ ./mysql2sqlite jawiki-latest-redirect.sql | sqlite3 pages.db
pages.db
の中身を見てみます。
$ sqlite3 ./pages.db
SQLite version 3.8.5 2014-08-15 22:37:57
Enter ".help" for usage hints.
sqlite> .schema
CREATE TABLE `page` (
`page_id` integer NOT NULL PRIMARY KEY AUTOINCREMENT
, `page_namespace` integer NOT NULL DEFAULT '0'
, `page_title` varbinary(255) NOT NULL DEFAULT ''
, `page_restrictions` varbinary(255) NOT NULL DEFAULT ''
, `page_counter` integer NOT NULL DEFAULT '0'
, `page_is_redirect` integer NOT NULL DEFAULT '0'
, `page_is_new` integer NOT NULL DEFAULT '0'
, `page_random` double NOT NULL DEFAULT '0'
, `page_touched` varbinary(14) NOT NULL DEFAULT ''
, `page_links_updated` varbinary(14) DEFAULT NULL
, `page_latest` integer NOT NULL DEFAULT '0'
, `page_len` integer NOT NULL DEFAULT '0'
, `page_content_model` varbinary(32) DEFAULT NULL
, `page_lang` varbinary(35) DEFAULT NULL
, UNIQUE (`page_namespace`,`page_title`)
);
CREATE TABLE `redirect` (
`rd_from` integer NOT NULL DEFAULT '0'
, `rd_namespace` integer NOT NULL DEFAULT '0'
, `rd_title` varbinary(255) NOT NULL DEFAULT ''
, `rd_interwiki` varbinary(32) DEFAULT NULL
, `rd_fragment` varbinary(255) DEFAULT NULL
, PRIMARY KEY (`rd_from`)
);
CREATE INDEX "idx_page_page_random" ON "page" (`page_random`);
CREATE INDEX "idx_page_page_len" ON "page" (`page_len`);
CREATE INDEX "idx_page_page_redirect_namespace_len" ON "page" (`page_is_redirect`,`page_namespace`,`page_len`);
CREATE INDEX "idx_redirect_rd_ns_title" ON "redirect" (`rd_namespace`,`rd_title`,`rd_from`);
sqlite> .quit
page
とredirect
というテーブルが確認できます。ちゃんとインポートできてそうです。
各テーブルの中身はこんな感じです(必要なところだけ抜粋)。
-
page
テーブル
カラム名 | 中身 |
---|---|
page_id |
ページID |
page_title |
ページタイトル |
page_is_redirect |
ページがリダイレクト元かどうか(1ならリダイレクト元) |
-
redirect
テーブル
カラム名 | 中身 |
---|---|
rd_from |
リダイレクト元のページID |
rd_title |
リダイレクト先のページタイトル |
次節から,Pythonを使ってデータベースを操作していきます。Pythonのバージョンは2.7です。データベースに接続しておきましょう。
import sqlite3
conn = sqlite3.connect('pages.db')
c = conn.cursor()
名寄せ辞書の構築
早速,名寄せ辞書を構築してみます。といっても,SQLを実行するだけなので1行で済みます。「任意のページがリダイレクト元ページのとき,リダイレクト元のページ名とリダイレクト先のページ名のtuple
を返す」というSQLを実行します。
redirect_of = dict(
c.execute(
'select page.page_title, redirect.rd_title from page \
inner join redirect on page.page_id=redirect.rd_from and page.page_is_redirect=1'
).fetchall()
)
print redirect_of[u'NTT_DoCoMo'] # NTTドコモ
print redirect_of[u'Docomo'] # NTTドコモ
できました。redirect_ofの引数に任意の語を入れると,リダイレクト関係がある場合,リダイレクト先のページタイトルが返ってきます。なお,Wikipediaのシステム上,半角空白はアンダースコアに,先頭のローマ字は必ず大文字になるので,検索するときは"NTT DoCoMo"は"NTT_DoCoMo"に,"docomo"は"Docomo"にしなければなりません。
ちなみに,dict([(key1, value1), (key2, value2), ...])
のようにtuple
のlist
を渡すと,{key1: value1, key2: value2, ...}
のようなdict
型に変換できるので便利です。
同義語の抽出
ついでに,「NTTドコモ」にリダイレクトするページの名前を「NTTドコモ」の同義語と考えて,同義語の抽出もやってみます。
まず,リダイレクト先のページタイトルが「NTTドコモ」であるページID(すなわちリダイレクト元のページID)を抽出します。そして,ページIDからページ名を抽出します。これがリダイレクト元のページ名です。途中でzip(*indices)
としているのは,簡単に言えば行と列を入れ替える操作です。よく使う(気がする)ので覚えておくと便利です。
entity = u'NTTドコモ'
indices = c.execute( # リダイレクト元のページIDを抽出
'select rd_from from redirect where rd_title=(?)',
(entity, )
).fetchall()
# indices -> [(29456,), (88074,), ..., (2250921,)]
# zip(*indices) -> [(29456, 88074, ..., , 2250921)]
synonyms = c.execute( # ページIDからページ名を抽出
'select page_title from page where page_id in ({})'.format(
', '.join('?' for _ in indices)
),
zip(*indices)[0]
).fetchall()
# synonyms -> [(u'NTT_DoCoMo',), (u'NTT_DoCoMoグループ',), ..., (u'エヌ・ティ・ティ移動通信網',)]
# zip(*synonyms) -> [(u'NTT_DoCoMo', u'NTT_DoCoMoグループ', ..., u'エヌ・ティ・ティ移動通信網')]
for synonym in zip(*synonyms)[0]: # ページ名がNoneでなければ表示
if synonym is not None:
print synonym
NTT_DoCoMo,NTT_DoCoMoグループ,Docomo,NTTDoCoMoなど,「NTTドコモ」の同義語が抽出できました。
データベース操作を終える前には接続を切りましょう。
conn.close()
おわりに
Wikipediaのダンプデータを使って,簡単な名寄せ辞書を作成しました。リダイレクト関係以外にも,Wikipediaの豊富なリンク構造(相互参照機能)を使って名寄せ辞書を構築する方法が知られています。みなさんもWikiepdiaをどんどん使い倒していきましょう。