言語処理100本ノック-85(TruncatedSVD):主成分分析による次元圧縮

[言語処理100本ノック 2015](http://www.cl.ecei.tohoku.ac.jp/nlp100/)の85本目「主成分分析による次元圧縮」の記録です。
約40万次元を300次元に圧縮します。今回は主成分分析ではなく特異値分解をしています。scikit-learnで主成分分析するとしたら疎行列でできずに、「どちらも次元削減だからいいだろう！」と妥協しました。
主成分分析は、[有名なCoursera機械学習オンラインコースの8週目](https://qiita.com/FukuharaYohei/items/7a71be58818719cdf73c#2-dimensionality-reduction)で習いました。コースに興味があれば、記事[「Coursera機械学習入門オンライン講座虎の巻(文系社会人にオススメ)」](https://qiita.com/FukuharaYohei/items/b2143413063376e97948)を参照ください。

# 参考リンク

|リンク|備考|
|:--|:--|
|[085.主成分分析による次元圧縮.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)/085.%E4%B8%BB%E6%88%90%E5%88%86%E5%88%86%E6%9E%90%E3%81%AB%E3%82%88%E3%82%8B%E6%AC%A1%E5%85%83%E5%9C%A7%E7%B8%AE.ipynb)|回答プログラムのGitHubリンク|
|[素人の言語処理100本ノック:85](https://qiita.com/segavvy/items/f1a7f3200c3b771e8568)|言語処理100本ノックで常にお世話になっています|
|[TruncatedSVD](https://scikit-learn.org/stable/modules/generated/sklearn.decomposition.TruncatedSVD.html)|TruncatedSVDの公式ヘルプ|
|[PCAとSVDの関連について](https://qiita.com/horiem/items/71380db4b659fb9307b4)|主成分分析と特異値分解の違い1|
|[PCAとSVDの関係性を示す](https://qiita.com/sakami/items/50b8485159312573e3c7)|主成分分析と特異値分解の違い2|

# 環境

|種類|バージョン|内容|
|:--|:--|:--|
|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を使って管理しています|

上記環境で、以下のPython追加パッケージを使っています。通常のpipでインストールするだけです。

|種類|バージョン|
|:--|:--|
|matplotlib|3.1.1|
|numpy|1.17.4|
|pandas|0.25.3|
|scipy|1.4.1|
|scikit-learn|0.21.3|


# 課題
## [第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)」*を使っています。

### 85. 主成分分析による次元圧縮
> 84で得られた単語文脈行列に対して，主成分分析を適用し，単語の意味ベクトルを300次元に圧縮せよ. 

# 回答
## 回答プログラム [085.主成分分析による次元圧縮.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)/085.%E4%B8%BB%E6%88%90%E5%88%86%E5%88%86%E6%9E%90%E3%81%AB%E3%82%88%E3%82%8B%E6%AC%A1%E5%85%83%E5%9C%A7%E7%B8%AE.ipynb)

```python
import matplotlib.pyplot as plt
import numpy as np
from scipy import io
from sklearn.decomposition import TruncatedSVD

matrix_x = io.loadmat('084.matrix_x.mat')['x']

# 読込を確認
print('matrix_x Shape:', matrix_x.shape)
print('matrix_x Number of non-zero entries:', matrix_x.nnz)
print('matrix_x Format:', matrix_x.getformat())

# 次元圧縮
svd = TruncatedSVD(300)
matrix_x300 = svd.fit_transform(matrix_x)

print(type(matrix_x300))
print('matrix_x300 Shape:',matrix_x300.shape)

print('Explained Variance Ratio Sum:', svd.explained_variance_ratio_.sum())
ev_ratio = svd.explained_variance_ratio_
ev_ratio = np.hstack([0,ev_ratio.cumsum()])
plt.plot(ev_ratio)
plt.show()

np.savez_compressed('085.matrix_x300.npz', matrix_x300)
```

## 回答解説
前回ノックで保存したmat形式のファイルを読み込みます。

```python
matrix_x = io.loadmat('084.matrix_x.mat')['x']

# 読込を確認
print('matrix_x Shape:', matrix_x.shape)
print('matrix_x Number of non-zero entries:', matrix_x.nnz)
print('matrix_x Format:', matrix_x.getformat())
```

上記の出力ですが、Shapeもゼロ以外の要素も前回と同じであることが確認できます。しかし、フォーマットが`lil`で保存したのに`csc`になっています。そういうものなのでしょうか。特に気にせずに進めます。

```bash
matrix_x Shape: (388836, 388836)
matrix_x Number of non-zero entries: 447875
matrix_x Format: csc
```

今回のメインの次元圧縮部分です。といっても関数`TruncatedSVD`を使っているだけなので何も苦労はしていません。約8分かかりました。

```python
svd = TruncatedSVD(300)
matrix_x300 = svd.fit_transform(matrix_x)

print(type(matrix_x300))
print('matrix_x300 Shape:',matrix_x300.shape)
```

返り値を確認すると、`numpy.ndarray`形式のようです。確かに次元圧縮しているので疎行列ではなく、密行列が正しいのでしょう。


```bash
<class 'numpy.ndarray'>
matrix_x300 Shape: (388836, 300)
```

では、どれだけ分散を保つことができているかを見てみます。記事[「scikit-learnで主成分分析（累積寄与率を求める）」](http://swdrsker.hatenablog.com/entry/2016/12/16/023115)を参考にしています。

```python
print('Explained Variance Ratio Sum:', svd.explained_variance_ratio_.sum())
ev_ratio = svd.explained_variance_ratio_
ev_ratio = np.hstack([0,ev_ratio.cumsum()])
plt.plot(ev_ratio)
plt.show()
```

3割程度。低い・・・。もっと次元数を増やした方がいいのだろうか。
[「99%を超えることが望ましいです。ただ、95%や90%をしきい値とすることもあるようです。」](https://qiita.com/FukuharaYohei/items/7a71be58818719cdf73c#232-choosing-the-number-of-principal-components)と習ったのですが・・・

```bash
Explained Variance Ratio Sum: 0.31949196039604355
```

折れ線グラフで主成分ごとに分散の保持割合を累積させたものです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134273/11c6be38-24b5-e688-1729-42f8f202666b.png)

ファイルを軽くするために関数`save_compressed`で圧縮保存しています。それでも118MBのファイルサイズです。前回の疎行列で保存したファイルサイズが7MBだったので、次元圧縮しても密行列化したことで膨らみましたね。ちなみに圧縮保存する前のメモリ使用量は933MBなので圧縮によってだいぶ小さくできているようです。ただ、その反面9秒かかっていた時間が、36まで増えました。
保存に関しては記事[「numpy配列の直列化方法によるファイル容量の違いを比較」](https://www.haya-programming.com/entry/2018/12/26/210346)を参考にしました。

```python
np.savez_compressed('085.matrix_x300.npz', matrix_x300)
```

# おまけ
## Tips: 主成分分析できないの？
課題に書かれているとおり「主成分分析」できないか調べました。ネックとなるのは疎行列をInputとして使えるかです。
Stackoverflowの["Performing PCA on large sparse matrix by using sklearn"](https://stackoverflow.com/questions/33603787/performing-pca-on-large-sparse-matrix-by-using-sklearn)に疎行列は無理と書かれていました。
[[MRG] Implement randomized PCA #12841](https://github.com/scikit-learn/scikit-learn/pull/12841)では、疎行列をInputにする機能を実装中らしいが、Openのままです・・・
密行列でメモリに乗り切る量でちょっとずつ学習できないものかとも考えましたが、仮にできても時間が膨大に掛かりそうなので諦めました・・・

## Tips: 次元を450でやってみた
3割程度の分散保持率は低いな、と感じて次元を増やしてみました。
最初は1000次元でやったのですが、メモリ不足でエラー発生・・・
600次元だと30分経っても終わらず、面倒だったので途中打ち切り。
450次元だと18分で終わりました。その時の分散保持率は38%でだいぶ上がっています。
比較するとこんな感じ。

|次元|処理時間|返り値(matrix_x300)メモリ|ファイルサイズ|
|:-:|:-:|:-:|:-:|
|300次元|8分|0.9GB|118MB|
|450次元|18分|1.40GB|178MB|