sudachipyで簡易的に同義語辞書をつかう

# TL;DR
- sudachiの同義語辞書(synonym.txt)から同義語グループidと代表語の組み合わせを生成
- 生成した組み合わせをつかってsudachipyで簡易的に同義語辞書を使えるようにする
- 例としてわかちがき後同義語辞書をつかって正規化する

# 目的
テキストからの情報抽出やテキストの類似度計算などのタスクを行う際に、sudachiでの形態素解析で同義語を使いたかったのですが、sudachipyではsudachiの同義語辞書を利用できませんでした。
簡易的でいいので、sudachipyで簡易的に同義語辞書が使えるようにします。
今回の目的は、あくまでも**形態素解析後の正規化**です。特に、わかちがき後に同義語を同じ見出しに揃えることを目的としています。したがって、同義語の展開は行いません。

# sudachiの同義語辞書

sudachiの同義語辞書は[ドキュメント](https://github.com/WorksApplications/SudachiDict/blob/develop/docs/synonyms.md)によると、


> Sudachi 辞書に登録されている語に対して同義語情報を付与したものです。 Sudachi 辞書と同じライセンスで提供されます。

とのことです。


同義語辞書のソースはテキストファイルで公開されています。
https://github.com/WorksApplications/SudachiDict/blob/develop/src/main/text/synonyms.txt

# pythonでsudachiを使う
pythonでsudachiを使う場合は、pipを使ってinstallすることができます。

```
pip install sudachipy sudachidict_core

```

以下の要領で形態素を取得できます。

```python
from sudachipy import tokenizer
from sudachipy import dictionary

tokenizer_obj = dictionary.Dictionary().create()
mode = tokenizer.Tokenizer.SplitMode.B

token = tokenizer_obj.tokenize("食べ", mode)[0]

token.surface() # => '食べ'
token.dictionary_form() # => '食べる'
token.reading_form() # => 'タベ'
token.part_of_speech() # => ['動詞', '一般', '*', '*', '下一段-バ行', '連用形-一般']
```

また、sudachiは文字の正規化ができます。

```python
token.normalized_form()
```

# 同義語辞書の編集
ここからが本題。

同義語ファイルを一部抜粋すると、以下のように形式で作成されています。

```

000001,1,0,1,0,0,0,(),曖昧,,
000001,1,0,1,0,0,2,(),あいまい,,
000001,1,0,2,0,0,0,(),不明確,,
000001,1,0,3,0,0,0,(),あやふや,,
000001,1,0,4,0,0,0,(),不明瞭,,
000001,1,0,5,0,0,0,(),不確か,,

000002,1,0,1,0,0,0,(),宛て先,,
000002,1,0,1,0,0,2,(),あて先,,
000002,1,0,1,0,0,2,(),宛先,,
000002,1,0,2,0,0,0,(),送り先,,
000002,1,0,3,0,0,0,(),送付先,,
000002,1,0,4,0,0,0,(),届け先,,
000002,1,0,5,0,0,0,(),発送先,,
000002,1,0,6,0,0,0,(),配送先,,

```

1語1行で記述し、同義語グループ間は空行で区切られて、フォーマットは以下の通りです。

```
0 : グループ番号
1 : 体言/用言フラグ (省略可)
2 : 展開制御フラグ (省略可)
3 : グループ内の語彙番号 (省略可)
4 : 同一語彙素内での語形種別 (省略可)
5 : 同じ語形の語の中での略語情報 (省略可)
6 : 同じ語形の語の中での表記ゆれ情報 (省略可)
7 : 分野情報 (省略可)
8 : 見出し
9 : 予約
10 : 予約
```

詳細な説明は、[ドキュメント](https://github.com/WorksApplications/SudachiDict/blob/develop/docs/synonyms.md#%E5%90%8C%E7%BE%A9%E8%AA%9E%E8%BE%9E%E6%9B%B8%E3%82%BD%E3%83%BC%E3%82%B9-%E3%83%95%E3%82%A9%E3%83%BC%E3%83%9E%E3%83%83%E3%83%88)を参照してください。


今回重要なのは

- 0 : グループ番号
- 3 : グループ内の語彙番号
- 6 : 同じ語形の語の中での表記ゆれ情報

の３つです。

グループ番号は、ソース内で同義語の管理・識別に使用する6桁の数字です。
グループ内の語彙番号は、グループ内における、語彙素の管理番号です。"1"始まりで連番を付与します。
同じ語形の語の中での表記ゆれ情報は、同じ略語・略称形の語 (3、4、5の番号が同じもの) における、表記の関連性を示す情報です。0がその略語・略称形の語の代表語になります。


すべてを確認したわけではないですが、synonym.txtでは各値の昇順に並んでいます。
つまり、**各同義語グループの先頭は複数ある語彙素のうちいずれかの代表語**です。
また、**管理番号1の語彙素を同義語グループの代表的な語彙素であるとすることで、各同義語グループの先頭の語をその同義語グループの代表語として扱う**ことができます。

このルールに則って、グループ番号と同義語グループの代表語の見出しの組み合わせを作成します。

```python
import csv

with open("synonyms.txt", "r") as f:
    reader = csv.reader(f)
    data = [r for r in reader]

output_data = []
synonym_set = []
synonym_group_id = None
for line in data:
    if not line:
        if synonym_group_id:
            base_keyword = synonym_set[0]
            output_data.append([
                synonym_group_id, base_keyword
            ])
        synonym_set = []
        continue
    else:
        synonym_group_id = line[0]
        synonym_set.append(line[8])

with open("synonyms_base.csv", "w") as f:
    writer = csv.writer(f)
    writer.writerows(output_data)

```


## sudachipyで同義語を使う準備
sudachipyでは同義語辞書を取得できませんが、トークンが該当する同義語グループのidを取得することができます。

```python

token.synonym_group_ids()
# => [1]
```

この取得した同義語グループidで先程生成した組み合わせから同義語の代表語を取得します。
一点注意するのは、synonym.txtの同義語グループのidは6桁の数字の文字列ですが、取得できるidはintであることです。

```python

import csv
with open('synonym_base.csv', "r") as f:
    reader = csv.reader(f)
    data = [[int(r[0]), r[1]] for r in reader]
    synonyms = dict(data)

synonym_group_ids = token.synonym_group_ids()
if synonym_group_ids:
    # 複数ありうるけどとりあえず先頭を選択
    surface = synonyms[synonym_group_ids[0]]
```


# わかちがき
生成した同義語のデータを使って、わかちがきを正規化します。
`fetch_synonym_surface`では同義語があった場合は同義語グループの代表語を、同義語がない場合は正規化された見出し語を返すようにします。

```python
import csv
with open('synonym_base.csv', "r") as f:
    reader = csv.reader(f)
    data = [[int(r[0]), r[1]] for r in reader]
    synonyms = dict(data)
    
def fetch_synonym_surface(token):
    synonym_group_ids = token.synonym_group_ids()
    if synonym_group_ids:
        # 複数ありうるけどとりあえず先頭を選択
        surface = synonyms[synonym_group_ids[0]]
    else:
        surface = token.normalized_form()
    return surface
```

以下は、
1. わかちがきだけ
2. わかちがき + 正規化
3. わかちがき + 同義語正規化

のコードと結果の比較です。


```python

def wakati(sentence):
    tokenizer_obj = dictionary.Dictionary().create()
    mode = tokenizer.Tokenizer.SplitMode.C
    return " ".join([m.surface() for m in tokenizer_obj.tokenize(sentence, mode)])

def wakati_normalized(sentence):
    tokenizer_obj = dictionary.Dictionary().create()
    mode = tokenizer.Tokenizer.SplitMode.C
    return " ".join([m.normalized_form() for m in tokenizer_obj.tokenize(sentence, mode)])

def wakati_synonym_normalized(sentence):
    tokenizer_obj = dictionary.Dictionary().create()
    mode = tokenizer.Tokenizer.SplitMode.C
    return " ".join([fetch_synonym_surface(m) for m in tokenizer_obj.tokenize(sentence, mode)])

sentence = "アドビはアメリカのお金であるmoneyを生み出す会社"

print("1:", wakati(sentence))
print("2:", wakati_normalized(sentence))
print("3:", wakati_synonym_normalized(sentence))
    
```

```
1: アドビ は アメリカ の お金 で ある money を 生み出す 会社
2: アドビ は アメリカ の お金 だ 有る マネー を 生み出す 会社
3: アドビシステムズ は アメリカ合衆国 の お金 だ 有る お金 を 生み出す 会社
```