Wikipediaを使ったお手軽名寄せ辞書

# はじめに
自然言語処理では，「名寄せ」が往々にして必要になります。「名寄せ」とは，多種多様な表現を同一名にマージする作業のことです。例えば，「ドコモ」や「エヌ・ティ・ティ移動通信網」という語は，すべて同じ「NTTドコモ」を指しています。このように，表現は違っていても同じものを指している語は同一の表現にまとめてしまった方が何かと便利です（逆を言うと，「ドコモ」と「NTTドコモ」を別モノとして扱ってしまうと不都合です）。

いちばん簡単に「名寄せ」をする方法は，「ドコモ→NTTドコモ」のような名寄せ辞書を作ってしまうことです。今回は，オープンな言語資源であるWikipediaを使って，簡単な名寄せ辞書を作ってみたいと思います。

# 準備
Wikipediaにはリダイレクトという機能があります。

> リダイレクト（転送）とは、ある記事へリンクしたときに、別のページに転送する機能のことです。また、そのようなページをリダイレクトページ（転送ページ）と呼びます。（出典: https://ja.wikipedia.org/wiki/Wikipedia:リダイレクト）

例えばWikipediaページの[エヌ・ティ・ティ移動通信網](https://ja.wikipedia.org/wiki/エヌ・ティ・ティ移動通信網)というページや[ドコモ](https://ja.wikipedia.org/wiki/ドコモ)というページは，すべて「NTTドコモ」というページへリダイレクトされます。リダイレクトは主に，略称や別名や別の表記のような，表現の違いを吸収するためにあります。つまり，リダイレクト元のページタイトルは，リダイレクト先のページタイトルに「名寄せ」されているといえます。

Wikipediaのリダイレクト関係を使えば名寄せ辞書を作れそうですが，果たしてどうすればよいでしょうか。
ご存知の方も多いと思いますが，Wikipediaでは種々のダンプデータが[提供されています](https://ja.wikipedia.org/wiki/Wikipedia:データベースダウンロード)。ページ情報やリダイレクト関係のダンプデータもあるので，今回はこれを使って名寄せ辞書を作ってみます（その他にも，全記事のダンプデータを使えば，分散表現を獲得したり[上位下位関係を抽出できたり](https://alaginrc.nict.go.jp/hyponymy/)します）。
まず，ページ情報のダンプデータとリダイレクト関係のダンプデータをダウンロードして解凍します。そこそこ大きいので注意してください。最新のダンプデータは[ここ](https://dumps.wikimedia.org/jawiki/latest/)です。

```bash
$ 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`にインポートします。

```bash
$ 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`の中身を見てみます。

```SQL
$ 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です。データベースに接続しておきましょう。

```python
import sqlite3

conn = sqlite3.connect('pages.db')
c = conn.cursor()
```

# 名寄せ辞書の構築
早速，名寄せ辞書を構築してみます。といっても，SQLを実行するだけなので1行で済みます。「任意のページがリダイレクト元ページのとき，リダイレクト元のページ名とリダイレクト先のページ名の`tuple`を返す」というSQLを実行します。

```python
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)`としているのは，簡単に言えば行と列を入れ替える操作です。よく使う（気がする）ので覚えておくと便利です。

```python
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ドコモ」の同義語が抽出できました。

データベース操作を終える前には接続を切りましょう。

```python
conn.close()
```

# おわりに
Wikipediaのダンプデータを使って，簡単な名寄せ辞書を作成しました。リダイレクト関係以外にも，Wikipediaの豊富なリンク構造（相互参照機能）を使って名寄せ辞書を構築する方法が知られています。みなさんもWikiepdiaをどんどん使い倒していきましょう。