言語処理100本ノック-82(Context Word):文脈の抽出

[言語処理100本ノック 2015](http://www.cl.ecei.tohoku.ac.jp/nlp100/)の82本目「文脈の抽出」の記録です。
今回も後続のための前処理系で特に難しい処理をしておらず、技術的には解説すること少ないです。ただ、問題文が素人にはわかりずらく、理解に少し時間を要しました。

# 参考リンク

|リンク|備考|
|:--|:--|
|[082.文脈の抽出.ipynb](https://github.com/YoheiFukuhara/nlp100/blob/master/09.%E3%83%99%E3%82%AF%E3%83%88%E3%83%AB%E7%A9%BA%E9%96%93%E6%B3%95%20(I)/082.%E6%96%87%E8%84%88%E3%81%AE%E6%8A%BD%E5%87%BA.ipynb)|回答プログラムのGitHubリンク|
|[素人の言語処理100本ノック:82](http://qiita.com/segavvy/items/a8e269294f2c834cff08)|言語処理100本ノックで常にお世話になっています|
|[言語処理100本ノック 2015年版 (80～82)](http://kenichia.hatenablog.com/entry/2016/03/03/170718)|第9章で参考にしました|

# 環境

|種類|バージョン|内容|
|:--|:--|:--|
|OS|Ubuntu18.04.01 LTS|仮想で動かしています|
|pyenv|1.2.15|複数Python環境を使うことがあるのでpyenv使っています|
|Python|3.6.9|pyenv上でpython3.6.9を使っています<br>3.7や3.8系を使っていないことに深い理由はありません<br>パッケージはvenvを使って管理しています|

# 課題
## [第9章: ベクトル空間法 (I)](http://www.cl.ecei.tohoku.ac.jp/nlp100/#ch9)
>[enwiki-20150112-400-r10-105752.txt.bz2](http://www.cl.ecei.tohoku.ac.jp/nlp100/data/enwiki-20150112-400-r10-105752.txt.bz2)は，2015年1月12日時点の英語のWikipedia記事のうち，約400語以上で構成される記事の中から，ランダムに1/10サンプリングした105,752記事のテキストをbzip2形式で圧縮したものである．このテキストをコーパスとして，単語の意味を表すベクトル（分散表現）を学習したい．第9章の前半では，コーパスから作成した単語文脈共起行列に主成分分析を適用し，単語ベクトルを学習する過程を，いくつかの処理に分けて実装する．第9章の後半では，学習で得られた単語ベクトル（300次元）を用い，単語の類似度計算やアナロジー（類推）を行う．
>
>なお，問題83を素直に実装すると，大量（約7GB）の主記憶が必要になる． メモリが不足する場合は，処理を工夫するか，1/100サンプリングのコーパス[enwiki-20150112-400-r100-10576.txt.bz2](http://www.cl.ecei.tohoku.ac.jp/nlp100/data/enwiki-20150112-400-r100-10576.txt.bz2)を用いよ．

今回は*「1/100サンプリングのコーパス[enwiki-20150112-400-r100-10576.txt.bz2](http://www.cl.ecei.tohoku.ac.jp/nlp100/data/enwiki-20150112-400-r100-10576.txt.bz2)」*を使っています。

### 82. 文脈の抽出
>81で作成したコーパス中に出現するすべての単語tに関して，単語$ t $と文脈語$ c $のペアをタブ区切り形式ですべて書き出せ．ただし，文脈語の定義は次の通りとする．
>
>- ある単語$ t $の前後$ d $単語を文脈語$ c $として抽出する（ただし，文脈語に単語tそのものは含まない）
>- 単語$ t $を選ぶ度に，文脈幅$ d $は{1,2,3,4,5}の範囲でランダムに決める．


### 課題補足
#### 「文脈語」とは
対象とする単語を**「対象語」(Target word)**、対象語の前後を**「文脈語」(Context word)**と呼びます。対象語から何単語までを文脈語とするかを**「文脈幅」(Context Window SizeまたはWindow Size)**と呼びます。

課題の元ファイルにある下記の例文で説明します。
> No surface details of **Adrastea** are known due to the low resolution of available images

例えば上記で*Adrastea*を対象語とすると、前後にある"details", "of", "are", "known"が文脈幅2の文脈語です。
では、仮に文脈幅を2として上記文に対して今回の課題を実行する場合、今回は下記のようなファイルを作成します。
※ *「単語$ t $を選ぶ度に，文脈幅$ d $は{1,2,3,4,5}の範囲でランダムに決める」*部分は無視して解説

|1列名|2列目|
|:-:|:-:|
|No|surface|
|No|details|
|surface|No|
|surface|details|
|surface|of|
|details|No|
|details|surface|
|details|of|
|details|Adrastea|

# 回答

## 回答プログラム [082.文脈の抽出.ipynb](https://github.com/YoheiFukuhara/nlp100/blob/master/09.%E3%83%99%E3%82%AF%E3%83%88%E3%83%AB%E7%A9%BA%E9%96%93%E6%B3%95%20(I)/082.%E6%96%87%E8%84%88%E3%81%AE%E6%8A%BD%E5%87%BA.ipynb)
20行程度の短いプログラムですが、データ量が多いので処理に10分程度かかります。
また、作成されるファイルは800MB程度のサイズとなり大きいので注意が必要です。
ちなみに9割以上が[記事「素人の言語処理100本ノック:82」](http://qiita.com/segavvy/items/216888c3549cea3d8e81)のコピペです。

```python
import random

with open('./081.corpus.txt') as file_in, \
     open('./082.context.txt', mode='w') as file_out:
    for i, line in enumerate(file_in):
        tokens = line.strip.split(' ')
        for j in range(len(tokens)):
            d = random.randint(1, 5)        # 文脈幅d
            
            # 前後d語以内の語の列挙
            for k in range(max(j - d, 0), min(j + d + 1, len(tokens))):
                
                # 自分自身の場合は出力しない
                if j != k:
                    file_out.writelines(tokens[j]+'\t'+tokens[k]+'\n')
        if i < 4:
            print(len(tokens), tokens)
        else:
            print('\r Processding line: {0}'.format(i), end='')
```

## 回答解説
下記コードがメインの部分です。
対象語の場所`j`から文脈幅`d`を増減させた数のループです。ただ単純に増減させると最初の方の単語は負の数になったり、最後の方の単語は合計単語数を超過してしまったりとするので`max`と`min`関数を使って幅の調整をしています。

```python
# 前後d語以内の語の列挙
for k in range(max(j - d, 0), min(j + d + 1, len(tokens))):
                
    # 自分自身の場合は出力しない
    if j != k:
        file_out.writelines(tokens[j]+'\t'+tokens[k]+'\n')
```

4行目まではコンソールに対象語の数と処理対象文を、それ以降は何行目を処理しているかを出力しています。

```python
if i < 4:
    print(len(tokens), tokens)
else:
    print('\r Processding line: {0}'.format(i), end='')
```

### こぼれ話(トークン化失敗)
文のトークン化に関する失敗談です。
最初はあまり考えずに下記のように`split`関数を使っていました。

```python
tokens = line.split()
```

しかし、結果がこんなになってしまったものもあり、後続でPandas使ったときにエラーで気づきました。

```
"b")("s"	"c
−	"b")("s"
−	"c
```

本当はこうなるべきでした。これは一見、スペース区切りになっているように見えてスペースっぽい部分には`\xa0`が使われています。[前々回記事の「\xa0について」](https://qiita.com/FukuharaYohei/items/56a5a34ab8417baead76#xa0%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6)で少し触れています。

```
known	k" = √("s"("s" − "a")("s" − "b")("s" − "c
```

で、正しくすべく`strip`関数を使ってスペースだけで区切っています。

```python
tokens = line.strip.split(' ')
```