Mecabとtf.dataを使ってlivedoorニュースコーパスを分かち書きする

#はじめに
@Suguru_Toyoharaさんが`tf.data.Dataset`の使い方をわかりやすくまとめていただいていますが、このエントリでは自然言語処理でよくやるコーパスを分かち書きしてデータセットを作る、というフローを`td.data`を使って行います。
#livedoor コーパス
livedoorコーパスはNHN Japan株式会社が運営する「livedoor ニュース」のうち、下記のクリエイティブ・コモンズライセンスが適用されるニュース記事を収集したもので、こちらのリンクからダウンロードができます。
[https://www.rondhuit.com/download.html](https://www.rondhuit.com/download.html)

このページの[ldcc-20140209.tar.gz](https://www.rondhuit.com/download/ldcc-20140209.tar.gz)をダウンロードして解凍します。

```
$wget https://www.rondhuit.com/download/ldcc-20140209.tar.gz
$ tar zfx ldcc-20140209.tar.gz
```

解凍したファイルは以下のようなディレクトリ構造になっています。

```terminal:textの中身
$tree text -L 1
text
├── CHANGES.txt
├── dokujo-tsushin
├── it-life-hack
├── kaden-channel
├── livedoor-homme
├── movie-enter
├── peachy
├── README.txt
├── smax
├── sports-watch
└── topic-news
```

```terminal:text/dokujo-tsushinの中身
# tree text/dokujo-tsushin
text/dokujo-tsushin
├── dokujo-tsushin-4778030.txt
├── dokujo-tsushin-4778031.txt
├── dokujo-tsushin-4782522.txt
├── dokujo-tsushin-4788357.txt
...
├── dokujo-tsushin-6915005.txt
└── LICENSE.txt
```
#分かち書きデータセット作成
解凍したコーパスをもとに`tf.data`と`Mecab`を使ってテキストの分かち書きを行っていきたいと思います。
※Mecab本体はインストール済みの状態とします。

## fugashiインストール
[`mecab-python3`はメンテされていない](https://twitter.com/polm23/status/1198243723621978112)とのことなので、今回はMecabラッパーに`fugashi`を選択しました。

```terminal
$ pip install fugashi
```

## データセット作成
`tensorflow_datasets`と`fugashi`などを読み込み、Mecabのtaggerを生成します。

```python
import os
import tensorflow as tf
import tensorflow_datasets as tfds
import fugashi

tagger = fugashi.Tagger('-Owakati')
```

Tensorflowではテキストファイルのコーパスを操作するAPIが数多く揃っており、`tf.compat.v1.gfile.ListDirectory`を使うと指定したディレクトリ配下のリストを取得できます。

```python:livedoorコーパス内のファイルとディレクトリ一覧を取得
text_dir = os.path.join(os.getcwd(),"text")
tf.compat.v1.gfile.ListDirectory(text_dir)
```

```
['CHANGES.txt',
 'dokujo-tsushin',
 'it-life-hack',
 'kaden-channel',
 'livedoor-homme',
 'movie-enter',
 'peachy',
 'README.txt',
 'smax',
 'sports-watch',
 'topic-news']
```

`tf.compat.v1.gfile.ListDirectory`をネストしてファイル一覧を取得し、`tf.data.TextLineDataset` APIを使って各カテゴリ内のファイルを取得します。

```python:データセット作成
text_datasets = []
text_dir = os.path.join(os.getcwd(),"text")
for d in tf.compat.v1.gfile.ListDirectory(text_dir):
    data_dir = os.path.join(text_dir,d)
    if os.path.isdir(data_dir):
        for file_name in tf.compat.v1.gfile.ListDirectory(data_dir):
            text_dataset = tf.data.TextLineDataset(os.path.join(data_dir,file_name))
            text_datasets.append(text_dataset)
 
```

作成したデータセットの中身を見てみましょう。

```python
plane_dataset = text_datasets[0]
iter_plane_dataset = iter(plane_dataset)
print(next(iter_plane_dataset).numpy().decode('utf-8'))
print(next(iter_plane_dataset).numpy().decode('utf-8'))
print(next(iter_plane_dataset).numpy().decode('utf-8'))
```

```
http://news.livedoor.com/article/detail/4778030/
2010-05-22T14:30:00+0900
友人代表のスピーチ、独女はどうこなしている？
```

~~`tf.compat.v1.gfile.ListDirectory`と`tf.data.TextLineDataset`を組み合わせることで、コーパスファイル群から簡単にデータセットを作成することができました。~~

この手順だと`text_datasets[0]`にはdokujo-tsushin-4778030.txtのテキストが、`text_datasets[1]`にはdokujo-tsushin-4778031.txtというように、ファイル単位で分割されてしまうため、文章単位となるように`concatenate`する必要がありました。

```python
parsed_dataset = text_datasets[0]
for text_dataset in text_datasets[1:]:
    parsed_dataset = parsed_dataset.concatenate(text_dataset)
```
これでデータセット作成の完了です。

## 分かち書きデータセットを作成
分かち書きデータセットを用意する場合、このデータセットに対してさらにfugashiのTaggerを当てる必要がありますが、`tf.data.TextLineDataset`の`map`を使えば、データセット作成時にまとめて分かち書きすることができます。

```python:分かち書きデータセット作成
def parse(text):
    '''
    データセットのテキストを1行ごとに分かち書き
    '''
    return tagger.parse(text.numpy().decode('utf-8')).split("\n")[0]

@tf.function
def parse_text(text):
    parsed = tf.py_function(parse,[text],[tf.string])
    return parsed[0]

text_datasets = []
text_dir = os.path.join(os.getcwd(),"text")
for d in tf.compat.v1.gfile.ListDirectory(text_dir):
    data_dir = os.path.join(text_dir,d)
    if os.path.isdir(data_dir):
        for file_name in tf.compat.v1.gfile.ListDirectory(data_dir):
            text_dataset = tf.data.TextLineDataset(os.path.join(data_dir,file_name)).map(parse_text) # mapでテキストをパース処理に渡す
            text_datasets.append(text_dataset)
            
parsed_dataset = text_datasets[0]
for text_dataset in text_datasets[1:]:
    parsed_dataset = parsed_dataset.concatenate(text_dataset)
```

今度は分かち書きされた結果がきちんと返っています。

```python
parsed_dataset = text_datasets[0]
iter_parsed_dataset = iter(parsed_dataset)
print(next(iter_parsed_dataset).numpy().decode('utf-8'))
print(next(iter_parsed_dataset).numpy().decode('utf-8'))
print(next(iter_parsed_dataset).numpy().decode('utf-8'))
```

```
http :// news . livedoor . com / article / detail / 4778030 /
2010 - 05 - 22 T 14 : 30 : 00 + 0900
友人 代表 の スピーチ 、 独 女 は どう こなし て いる ？
```

## テキストエンコード
機械学習で自然言語を扱う場合、分かち書き結果を個々のキーワードに分割し、数字にエンコードする必要があります。

[Tensorflow 2.0のテキストチュートリアル](https://www.tensorflow.org/tutorials/load_data/text)を参考に`tfds.features.text.Tokenizer`と`tfds.features.text.TokenTextEncoder`を使ってテキストをエンコードしてみます。

まずはトークナイザを生成します。

```python:トークナイザを生成
tokenizer = tfds.features.text.Tokenizer()
```

トークナイザはデフォルトで半角記号などを排除するので、それらが必要な場合は生成時に`reserved_tokens`で指定しておく必要があります。

```python:reserved_tokensあり
tokenizer = tfds.features.text.Tokenizer(reserved_tokens=["/",":"])
```

生成したトークナイザを使い、分かち書きデータセットを分割してボキャブラリを構築します。

```python:ボキャブラリ構築
vocabulary_set = set()
for line in parsed_dataset:
    some_tokens = tokenizer.tokenize(line.numpy())
    vocabulary_set.update(some_tokens)
```

ボキャブラリからエンコーダを生成します。

```python
encoder = tfds.features.text.TokenTextEncoder(vocabulary_set, tokenizer=tokenizer)
```

結果を確認します。

```python
sample_text = next(iter_parsed_dataset)
encoded_example = encoder.encode(sample_text.numpy().decode('utf-8'))

for enc in encoded_example:
    print("{} -> {}".format(enc, encoder.decode([enc])))
```

```
9058 -> もうすぐ
3331 -> ジューン
31397 -> ブライド
27220 -> と
2397 -> 呼ば
22334 -> れる
5673 -> ６月
31920 -> 独
32073 -> 女
20415 -> の
28229 -> 中
18191 -> に
986 -> は
8138 -> 自分
20415 -> の
10202 -> 式
986 -> は
36484 -> まだ
10551 -> な
26617 -> のに
2397 -> 呼ば
34 -> れ
27080 -> て
27726 -> ばかり
27509 -> という
6682 -> お祝い
903 -> 貧乏
5111 -> 状態
20415 -> の
35344 -> 人
4722 -> も
21766 -> 多い
20415 -> の
22018 -> で
986 -> は
31505 -> ない
12417 -> だろ
7199 -> う
23655 -> か
35100 -> さらに
22067 -> 出席
16090 -> 回数
18014 -> を
1399 -> 重ね
27080 -> て
27779 -> いく
27220 -> と
6523 -> こんな
6851 -> お願い
6769 -> ごと
18014 -> を
32709 -> さ
22334 -> れる
30766 -> こと
4722 -> も
3435 -> 少なく
31505 -> ない
```

作成したエンコーダを保存しておくことで、他のタスクにも使うことができます。

```python
encoder.save_to_file('sample/encoded.model')
```

エンコードファイルの中身はこのようになっています。

```エンコードファイルの中身
### TokenTextEncoder
### Metadata: {"has_tokenizer": true, "lowercase": false, "oov_buckets": 1, "oov_token": "UNK"}
一番
相談
取り入れ
幸恵
ばかり
多い
\できる
よう
みる
どう
何
回数
なら
話す
いっ
あげれ
者
って
他
よ
サプライズ
```

保存したエンコーダは`tfds.features.text.TokenTextEncoder.load_from_file`から呼び出すことができます。

```python
loaded_encoder = tfds.features.text.TokenTextEncoder.load_from_file('sample/encoded.model')

encoded_example = loaded_encoder.encode(sample_text.numpy().decode('utf-8'))

for enc in encoded_example:
    print("{} -> {}".format(enc, encoder.decode([enc])))
```

保存前とワードのIDが変わっていないことが確認できます。

```
9058 -> もうすぐ
3331 -> ジューン
31397 -> ブライド
27220 -> と
2397 -> 呼ば
22334 -> れる
5673 -> ６月
31920 -> 独
32073 -> 女
20415 -> の
28229 -> 中
18191 -> に
986 -> は
8138 -> 自分
20415 -> の
10202 -> 式
986 -> は
36484 -> まだ
10551 -> な
26617 -> のに
2397 -> 呼ば
34 -> れ
27080 -> て
27726 -> ばかり
27509 -> という
6682 -> お祝い
903 -> 貧乏
5111 -> 状態
20415 -> の
35344 -> 人
4722 -> も
21766 -> 多い
20415 -> の
22018 -> で
986 -> は
31505 -> ない
12417 -> だろ
7199 -> う
23655 -> か
35100 -> さらに
22067 -> 出席
16090 -> 回数
18014 -> を
1399 -> 重ね
27080 -> て
27779 -> いく
27220 -> と
6523 -> こんな
6851 -> お願い
6769 -> ごと
18014 -> を
32709 -> さ
22334 -> れる
30766 -> こと
4722 -> も
3435 -> 少なく
31505 -> ない
```

# まとめ
`tf.data`と`map`を組み合わせることで、簡単に分かち書きしたデータセットを作ることができます。

また`tensorflow_datasets`のトークナイザとテキストエンコーダを使うことで、エンコーダも簡単に構築でき、自然言語の機械学習を行う際の前処理がすごく楽になるのでとてもよいと思います。