言語処理100本ノック(2020)-80: ID番号への変換(keras)

[言語処理100本ノック 2020 (Rev2)](https://nlp100.github.io/ja/)の[「第9章: RNN, CNN」](https://nlp100.github.io/ja/ch09.html)の[80本目「ID番号への変換」](https://nlp100.github.io/ja/ch09.html#80-id%E7%95%AA%E5%8F%B7%E3%81%B8%E3%81%AE%E5%A4%89%E6%8F%9B)記録です。
Pythonあるあるですが、横着しようとしてすごく簡単にできるパッケージがないか調べた結果、逆に時間かかってしまった系です。多分4時間くらい解くのに時間かけてしまいました。また、少し思うところがあってノック内容に反することもしています。
[記事「まとめ: 言語処理100本ノックで学べることと成果」](https://qiita.com/FukuharaYohei/items/6f0af3c4e270d8af2d99)に[言語処理100本ノック 2015](http://www.cl.ecei.tohoku.ac.jp/nlp100/)についてはまとめていますが、追加で差分の[言語処理100本ノック 2020 (Rev2)](https://nlp100.github.io/ja/)についても更新します。

# 参考リンク

|リンク|備考|
|:--|:--|
|[80_ID番号への変換.ipynb](https://github.com/YoheiFukuhara/nlp100-2020/blob/main/09.RNN%2CCNN/80_ID%E7%95%AA%E5%8F%B7%E3%81%B8%E3%81%AE%E5%A4%89%E6%8F%9B.ipynb)|回答プログラムのGitHubリンク|
|[言語処理100本ノック 2020 第9章: RNN, CNN](https://qiita.com/nymwa/items/4542b45837a10766890b)|(PyTorchだけど)解き方の参考|
|[【言語処理100本ノック 2020】第9章: RNN, CNN](https://qiita.com/yamaru/items/6dbd1a44f5910d8848f6)|(PyTorchだけど)解き方の参考|
|[まとめ: 言語処理100本ノックで学べることと成果](https://qiita.com/FukuharaYohei/items/6f0af3c4e270d8af2d99)|言語処理100本ノックまとめ記事|
|[TextVectorization](https://www.tensorflow.org/api_docs/python/tf/keras/layers/TextVectorization)|ID化のkeras関数|
|[FreqDist](https://www.kite.com/python/docs/nltk.FreqDist)|辞書作成のnltk関数|

# 環境
後々GPUを使わないと厳しいので、Google Colaboratory使いました。Pythonやそのパッケージでより新しいバージョンありますが、新機能使っていないので、プリインストールされているものをそのまま使っています。TensorFlowはいつの間にか2.6から2.7に上がっています。

|種類|バージョン|内容|
|:--|:--|:--|
|Python|3.7.12|Google Colaboratoryのバージョン|
|google|2.0.3|Google Driveのマウントに使用|
|tensorflow|2.7.0|ディープラーニングの主要処理|
|nltk|3.2.5|Tokenの辞書作成に使用|
|pandas|1.1.5|行列に関する処理に使用|

# [第8章: ニューラルネット](https://nlp100.github.io/ja/ch08.html)
## 学習内容
> 深層学習フレームワークを用い，再帰型ニューラルネットワーク（RNN）や畳み込みニューラルネットワーク（CNN）を実装します．

### [80. ID番号への変換](https://nlp100.github.io/ja/ch09.html#80-id%E7%95%AA%E5%8F%B7%E3%81%B8%E3%81%AE%E5%A4%89%E6%8F%9B)
> 問題51で構築した学習データ中の単語にユニークなID番号を付与したい．学習データ中で最も頻出する単語に1，2番目に頻出する単語に2，……といった方法で，学習データ中で2回以上出現する単語にID番号を付与せよ．そして，与えられた単語列に対して，ID番号の列を返す関数を実装せよ．ただし，出現頻度が2回未満の単語のID番号はすべて0とせよ．

# 回答
## 回答結果
適当なサンプル文を入力します。

```python
print(vectorize_layer([["bar unknown_word(1)"]]).numpy)
```

辞書化されて、Numpy配列が返ってきます。これ見ただけでは意味わからないですね。

```:結果
<bound method _EagerTensorBase.numpy of <tf.Tensor: shape=(1, 18), dtype=int64, numpy=
array([[2081,    1,    0,    0,    0,    0,    0,    0,    0,    0,    0,
           0,    0,    0,    0,    0,    0,    0]])>>
```

今回は以下の2点について違反をしています。

> 1. 学習データ中で最も頻出する単語に1，2番目に頻出する単語に2，……といった方法で，学習データ中で2回以上出現する単語にID番号を付与せよ．
> 1. 出現頻度が2回未満の単語のID番号はすべて0とせよ．

1に関しては、特に頻出順位とID番号は関連付けていません。特に関連付けるメリットを見いだせず、逆に使っている関数にその機能がなかったためです。
2. に関しては0はmask用にしています。これが一般的なのではないでしょうか(kerasだけ？)。代わりに1をUnkownとして使っています。

## 回答プログラム [80_ID番号への変換.ipynb](https://github.com/YoheiFukuhara/nlp100-2020/blob/main/09.RNN%2CCNN/80_ID%E7%95%AA%E5%8F%B7%E3%81%B8%E3%81%AE%E5%A4%89%E6%8F%9B.ipynb)
GitHubには確認用コードも含めていますが、ここには必要なものだけ載せています。

```python
import nltk
import pandas as pd
import tensorflow as tf
from google.colab import drive

drive.mount('/content/drive')

BASE_PATH = '/content/drive/MyDrive/ColabNotebooks/ML/NLP100_2020/06.MachineLearning/'
max_len = 0


def get_vocabulary(type_):
    global max_len
    df = pd.read_table(BASE_PATH+type_+'.feature.txt')
    df.info()
    sr_title = df['title'].str.split().explode()
    max_len_ = df['title'].map(lambda x: len(x.split())).max()
    if max_len < max_len_:
        max_len = max_len_
    fdist = nltk.FreqDist(sr_title)
    print(f'Top 3 tokens: {fdist.most_common(3)}')
    return [k for k, v in fdist.items() if v > 1]


vocabulary = get_vocabulary('train')
vocabulary.extend(get_vocabulary('valid'))
vocabulary.extend(get_vocabulary('test')) # あまりこだわらずにテストデータセットも追加

# setで重複削除し、タプル形式に設定
tup_voc = tuple(set(vocabulary))

print(f'vocabulary size before removing duplicates: {len(vocabulary)}')
print(f'vocabulary size after removing duplicates: {len(tup_voc)}')
print(f'sample vocabulary: {tup_voc[:10]}')
print(f'max length is {max_len}')

vectorize_layer = tf.keras.layers.TextVectorization(
 output_mode='int',
 vocabulary=tup_voc,
 output_sequence_length=max_len)

print(f'sample vocabulary: {vectorize_layer.get_vocabulary()[:10]}')

model = tf.keras.models.Sequential()
model.add(tf.keras.Input(shape=(1,), dtype=tf.string))
model.add(vectorize_layer)

# いまいち仕様を調べていないが、一度modelに追加すると下記が有効になる
print(vectorize_layer([["bar unknown_word(1)"]]).numpy)
```

## 回答解説
### 辞書作成
今回の肝となるコードです。
`df['title'].str.split().explode()`で単語分割をして1列の`Series`を作り、`nltk.FreqDist(sr_title)`で辞書を作成しています。`if`の条件文で*「学習データ中で2回以上出現する単語にID番号を付与」*を実現しています。
また、`max_len_ = df['title'].map(lambda x: len(x.split())).max()`で1文の最大Token数を求めています。最大Token数は[`TextVectorization`関数](https://www.tensorflow.org/api_docs/python/tf/keras/layers/TextVectorization)に渡すために求めています。

```python:
def get_vocabulary(type_):
    global max_len
    df = pd.read_table(BASE_PATH+type_+'.feature.txt')
    df.info()
    sr_title = df['title'].str.split().explode()
    max_len_ = df['title'].map(lambda x: len(x.split())).max()
    if max_len < max_len_:
        max_len = max_len_
    fdist = nltk.FreqDist(sr_title)
    print(f'Top 3 tokens: {fdist.most_common(3)}')
    return [k for k, v in fdist.items() if v > 1]
```

テストデータセットも辞書化しています。実運用を考えると辞書化から除外すべきかとも思いますが、こだわらずにやっています。

```python:
vocabulary = get_vocabulary('train')
vocabulary.extend(get_vocabulary('valid'))
vocabulary.extend(get_vocabulary('test')) # あまりこだわらずにテストデータセットも追加
```

### 辞書のモデル組込み
[Kerasの`TextVectorization`関数](https://www.tensorflow.org/api_docs/python/tf/keras/layers/TextVectorization)を使っています。今回、ノックの*学習データ中で2回以上出現する単語にID番号を付与せよ*がなければ、本当にただテキストを渡すだけでした。しかし、1回以下の単語を除去するために事前に辞書を作ってパラメータ`vocabulary`に渡しています。パラメータ`max_tokens`を使えば、覚え込む単語数の上限は指定できるのですが、頻度での指定はできないです。

```python:
vectorize_layer = tf.keras.layers.TextVectorization(
 output_mode='int',
 vocabulary=tup_voc,
 output_sequence_length=max_len)

print(f'sample vocabulary: {vectorize_layer.get_vocabulary()[:10]}')

model = tf.keras.models.Sequential()
model.add(tf.keras.Input(shape=(1,), dtype=tf.string))
model.add(vectorize_layer)

# いまいち仕様を調べていないが、一度modelに追加すると下記が有効になる
print(vectorize_layer([["bar unknown_word(1)"]]).numpy)
```

以下の出力をします。1行目の[UNK]は未知語です。小さい息子の影響で「う○こ」に見えてしまいます。

```:結果
sample vocabulary: ['', '[UNK]', 'Changes', 'rampage', 'Politics', 'Gawker', 'mood', 'Quiznos', 'friendless', 'end']
<bound method _EagerTensorBase.numpy of <tf.Tensor: shape=(1, 18), dtype=int64, numpy=
array([[2081,    1,    0,    0,    0,    0,    0,    0,    0,    0,    0,
           0,    0,    0,    0,    0,    0,    0]])>>
```

# こぼれ話
今回、ノックの*学習データ中で2回以上出現する単語にID番号を付与せよ*に非常に苦しみました。
普通にPythonで組めばそんなに手間がかからないのですが、関数を色々探してしまいました。

## 没関数1: Tokenizer
[Keras の `Tokenizer`関数](https://www.tensorflow.org/api_docs/python/tf/keras/preprocessing/text/Tokenizer)を調べました。パラメータ`document_count`を使えばできるかも、と思いましたがそんなことは無かった。

## 没関数2: corpora.dictionary
[Gensim の `corpora.dictionary`関数](https://radimrehurek.com/gensim/corpora/dictionary.html)を調べました。もう忘れましたが、パラメータか関数を使ってできるかも、と思いましたがそんなことは無かった。