spaCy で自然言語から構造化データベースを作る


## 概要

[@yontiki](https://twitter.com/yontiki) です．

テキストを用いた統計分析や機械学習などのためには，対象とするテキストに対して形態素解析や係り受け解析，固有表現抽出などの処理を行う必要があります．

しかし，いちいち解析のたびに前処理コードを書いていると大変です．

この記事では，テキストを自然言語処理ライブラリ [spaCy](https://spacy.io/) によって解析し，その結果を MySQL データベースに構造化データとして格納することで，柔軟な解析や他のシステムとの連携が容易であるデータベースを作りました．

係り受けなどの言語情報が付与されたコーパスとしては既に[梵天](https://pj.ninjal.ac.jp/corpus_center/nwjc/)などがあり，テキストからコーパスを生成するサービスとしては[SKETCH ENGINE](https://www.sketchengine.eu/guide/create-corpus-from-files/)などがありますが，分析の柔軟さや後段の処理への接続のしやすさを考えると，自分でデータベースを作ることにも利点があります．


## データベースの構造
データベースは token，entity，entity_tokens という3つのファクトテーブルと
その他のディメンジョンテーブル（表層形，品詞，固有表現ラベルなど）からなっています．

### token
文中の単語を表すテーブルです．
text_id はこの単語が属す文の id，
head_id はこの単語の統語的な親である単語の id を示します．
この head_id を持つことによって，構文構造を扱うことができます．

```
+---------------+-------------+------+-----+---------+----------------+
| Field         | Type        | Null | Key | Default | Extra          |
+---------------+-------------+------+-----+---------+----------------+
| id            | int(11)     | NO   | PRI | NULL    | auto_increment |
| text_id       | int(11)     | YES  | MUL | NULL    |                |
| head_id       | int(11)     | YES  | MUL | NULL    |                |
| word_id       | varchar(20) | YES  | MUL | NULL    |                |
| tag_id        | varchar(20) | YES  | MUL | NULL    |                |
| pos_id        | varchar(20) | YES  | MUL | NULL    |                |
| dep_id        | varchar(20) | YES  | MUL | NULL    |                |
+---------------+-------------+------+-----+---------+----------------+
```

### entity
固有表現を表すテーブルです．
label_id は固有表現ラベル，
root_id は entity 中のヘッドとなる語の id を示しています．


```
+--------------+-------------+------+-----+---------+----------------+
| Field        | Type        | Null | Key | Default | Extra          |
+--------------+-------------+------+-----+---------+----------------+
| id           | int(11)     | NO   | PRI | NULL    | auto_increment |
| text_id      | int(11)     | YES  | MUL | NULL    |                |
| label_id     | varchar(20) | YES  | MUL | NULL    |                |
| root_id      | int(11)     | YES  | MUL | NULL    |                |
+--------------+-------------+------+-----+---------+----------------+

```

### entity_tokens
entity とそこに含まれる語を表すテーブルです．
一つの entity に複数の token が結びつけられる 1 対多の関係になっています．

```
+-----------+---------+------+-----+---------+----------------+
| Field     | Type    | Null | Key | Default | Extra          |
+-----------+---------+------+-----+---------+----------------+
| id        | int(11) | NO   | PRI | NULL    | auto_increment |
| entity_id | int(11) | YES  | MUL | NULL    |                |
| token_id  | int(11) | YES  | MUL | NULL    |                |
+-----------+---------+------+-----+---------+----------------+
```


## 今回用いたデータ
今回のデータベース構築には [Wikitext-JA](http://www.ar.media.kyoto-u.ac.jp/data/wikitext-ja/home-e.html) 中の「[良質な記事](https://ja.wikipedia.org/wiki/Wikipedia:%E8%89%AF%E8%B3%AA%E3%81%AA%E8%A8%98%E4%BA%8B)」を用いました．

Wikitext-JA は言語モデル訓練のためのデータセットで，日本語ウィキペディアに掲載されている記事のうち，一定の基準を満たした質の高い記事である「秀逸な記事」と「良質な記事」をクレンジングなどの処理をしたのちにまとめたものです．

データセットの[統計情報](http://www.ar.media.kyoto-u.ac.jp/data/wikitext-ja/home.html#%E7%B5%B1%E8%A8%88%E6%83%85%E5%A0%B1)は以下の通りです．


| 記事の種類 | 項目数 | 文中単語数の平均 | 文数 | 単語数 | 語彙サイズ |
|:--|:--|:--|:--|:--|:--|
| 秀逸な記事 | 85 | 61 | 35194 | 1318239 | 42113 |
| 良質な記事 | 1423 | 59 | 336707 | 12087117 | 181939 |
| 合計 | 1508 | 120 | 371901 | 13405356 | 224052 |



## コーパスからの情報抽出

### 係り受け

係り受け解析の結果を利用することで，指定した条件を満たす係り受け関係の使用頻度を調べることが可能です．

例として，以下のクエリでは「運営」の目的語となる語の頻度を求めています．


```
--- 親となる語の id を取得するサブクエリ
with hw as(
    select
        word.id as id
    from
        word
    where
        word.name = '運営'
),
--- 文法的関係の id を取得するサブクエリ
rel as(
    select
        dep.id as id
    from
        dep
    where
        dep.name = 'obj'
)
select
    dw.name,
    count(1) as cnt
from
    (
        select
            *
        from
            token
        where
            token.dep_id = (
                select
                    dep.id as id
                from
                    dep
                where
                    dep.name = 'obj'
            )
    ) as d
    join token as h
    on  d.head_id = h.id
    inner join
        hw
    on  h.word_id = hw.id
    join
        word as dw
    on  dw.id = d.word_id
group by
    d.word_id,
    d.dep_id
order by
    cnt desc
limit 10
;
```

結果は次の通りです．

```
+-----------+-----+
| name      | cnt |
+-----------+-----+
| 路線      |  15 |
| 図書館    |   5 |
| 政権      |   5 |
| 網        |   4 |
| すべて    |   4 |
| 発電所    |   4 |
| 鉄道      |   4 |
| 学校      |   4 |
| ホテル    |   3 |
| 軌道      |   3 |
+-----------+-----+
10 rows in set (1.07 sec)
```

### Named Entity の抽出

以下のクエリは固有表現ラベルが「Person」の固有表現を列挙しています．


```
WITH label AS
  (SELECT *
   FROM label
   WHERE label.name = 'Person')
SELECT 
    distinct(group_concat(word.name)) AS NE_Person
FROM entity
    JOIN label ON entity.label_id = label.id
    JOIN entity_tokens ON entity.id = entity_tokens.entity_id
    JOIN token ON token.id = entity_tokens.token_id
    JOIN word ON token.word_id = word.id
WHERE token.text_id < 100
GROUP BY entity.id
LIMIT 20;
```

結果は次の通りです．

```
+------------------------------------------+
| NE_Person                               |
+------------------------------------------+
| エリオット                               |
| グレース,・,イイジマ                     |
| グローヴ                                 |
| ジェーン,・,フェアウェザー               |
| ジョン,・,コットン,・,デイナ             |
| スクウェアダンス                         |
| デイナ                                   |
| バーネット                               |
| フィリップ,・,Ｏ,・,キーニー             |
| ポール,・,Ｊ,・,バーネット               |
| 中井,久夫                                |
| 丹羽,努                                  |
| 乾隆,帝                                  |
| 佐藤,毅彦                                |
| 光明                                     |
| 前川,恒雄                                |
| 吉本                                     |
| 吉本,龍司                                |
| 喜                                       |
| 国学者                                   |
+------------------------------------------+
20 rows in set (0.07 sec)
```

## 機械学習モデルへの入力の前処理に使う
### 文と単語の埋め込み表現
文ごとの単語の出現頻度から共起行列を作り行列分解を行うと，
文と単語の埋め込み表現を得ることができます．

文ごとの単語出現頻度は以下のクエリで得られます．

```
select text_id, word_id, count(word_id)
from token
group by text_id, word_id
;
```

これを使って単語埋め込みと文埋め込みを訓練します．
学習には [implicit](https://implicit.readthedocs.io/en/latest/quickstart.html) を使いました．

#### 単語ベクトル
「図書館」と類似度の高い語を並べてみます．
「蔵書」や「貸出」といった「図書館」と似た文脈で出てきそうな語が得られました．

```
図書館 1.0
図書 0.96500534
蔵書 0.95171493
開館 0.950919
貸出 0.9337341
図書室 0.9294477
分館 0.91890675
立 0.91566175
冊 0.9113471
公民館 0.90034026
```

#### 文ベクトル
適当な文を選択して，類似度の高い文を並べてみます．
一番上の文がタネとなった文です．

神社に関する文が拾えていることがわかります．

```
1.0000001 富士山頂上付近に鎮座する。江戸時代には薬師堂が建てられており、廃仏毀釈で薬師堂が廃されるにあたってその跡地に祀られた。祭神：大名牟遅命、少彦名命富士山頂上付近に鎮座する。江戸時代には薬師堂が建てられており、廃仏毀釈で薬師堂が廃されるにあたってその跡地に祀られた。
0.9997535 社格：式内社「大海神社二座」
0.999728 社格：神階帳「従一位 やきわらの明神」、伊豆国三宮か
0.9980063 境内社：野々宮神社、諏訪社、幸之神社、八王子社、山王神社
0.9975214 例祭：９月９日祭神：底津少童命、中津少童命、表津少童命例祭：９月９日「しがじんじゃ」。境内北寄りの大海神社近くに西面して鎮座する（位置）。祭神の綿津見三神（海三神）は住吉三神とともに禊祓で生まれた海の神で、阿曇氏祖神。志賀海神社（福岡県福岡市）を本社とする。
```


## Pythonスクリプト内でクエリの結果を使う
MySQLデータベースを使用しているので，クエリの結果を Python などの言語で書かれたプログラムの中で使うこともできます．


簡単なタスクでどのようなことができるかみてみましょう．

食べ物名の辞書を作ることを考えます．

ここでは，「食べる」の目的語としてコーパス中に現れた語を「料理」に対する類似度に基づいて並べ替える処理をしてみます．

まず，これらが対象となる語とそのコーパス内での頻度です．
頻度順で並べただけでは，「犬」や「害虫」などあまり人間が食べないもの（文化にもよりますが）が上位に現れてきます．

```
料理 7
もの 4
植物 4
昆虫 3
肉 3
害虫 2
犬 2
タマネギ 2
地衣類 2
葉 2
```


次にこれらを「料理」に対する類似度順に並べ替えます．
上位に人間の食べ物らしいものが多くなりました．


```
料理
料理 1.0
煮込み 0.7861639
野菜 0.76861334
タマネギ 0.71860176
肉 0.6216733
牛 0.53786176
牛肉 0.44340453
焼き 0.43666354
スパゲッティ 0.43096444
朝食 0.35971218
大麦 0.35241544
食べ物 0.34847498
```

## 最後に
本記事では自然言語処理ライブラリによる自然言語文の解析結果を格納したデータベースを作り，いくつかの用例を示しました．
今後は，実際のテキスト分析や機械学習の前処理に使ってみて改善点を見つけたり，分散処理などによるデータベース作成やクエリの高速化などを試していきたいです．