静かなる名辞

pythonとプログラミングのこと


【python】scikit-learnで大規模疎行列を扱うときのTips

はじめに

 自然言語処理などで大規模疎行列を扱うことがあります。一昔前はNLPといえばこれでした(最近は低次元密行列で表現することのほうが多いですが)。

 疎行列はその特性をうまく生かして扱うとパフォーマンス上のメリットが得られる反面、うかつにdenseな表現に展開してしまうと効率が悪くなって激遅になったり、あっさりメモリから溢れたりします。

 scikit-learnでやる場合、うっかり使うと自動的にdenseな表現に展開されてしまう、という事故が起こりがちで、要するに使えるモデルに制約があり、注意が必要です。その辺の基本的なことをまとめておきます。

 目次

疎行列ってなに?

 まず、Pythonで疎行列といえばscipy.sparse.csr_matrixなどのことです。

scipy.sparse.csr_matrix — SciPy v1.4.1 Reference Guide

 内部の構造の詳細などは、こちらの記事が参考になります。

scipy.sparseの内部データ構造 – はむかず!

 重要なのは、まずこの方式にすると「メモリ効率がいい」ということです。これは単純に嬉しいですし、CPUキャッシュのことを考えても、パフォーマンス上大きなメリットがあります。また、0の要素は飛ばして探索できるので、うまく使うと効率も良くなります。

 大規模疎行列を相手にするときは、できるだけ疎行列のまま取り扱うことが重要になります。

特徴抽出する

 自然言語処理系のタスクだと、CountVectorizerやTfidfVectorizerが使えます。どちらもデフォルトでcsr_matrixを返してくれるので、素直に使えば疎行列が出てきます。

sklearn.feature_extraction.text.CountVectorizer — scikit-learn 0.22.1 documentation
sklearn.feature_extraction.text.TfidfVectorizer — scikit-learn 0.22.1 documentation

 もう少し幅広いタスクで使いたい場合は、DictVectrizerが便利でしょう。こちらもデフォルトではsparseな表現を返します(オプションでnumpy配列を返すようにすることも可能)。

sklearn.feature_extraction.DictVectorizer — scikit-learn 0.22.1 documentation

特徴選択する

 特徴抽出したあと素直に使うとだいたい変数が多すぎて使いづらいので、普通は変数選択をすると思います。sklearn.feature_selectionのものなら、これはだいたいデフォルトで疎行列のままの取り扱いに対応しています。

1.13. Feature selection — scikit-learn 0.22.1 documentation

 疎行列としてinputすれば疎行列で出てきます。速度もそうした方が速いです。

sklearnの変数選択は疎行列型(csr_matrix)でやると速いっぽいよ - 静かなる名辞

標準化する

 StandardScalerで標準化する場合は、with_mean=Falseを指定してください。これは平均0にしない標準化です。標準化の式の分子で平均を引かないものです。

 それだけで疎行列型のまま標準化することができます。

This scaler can also be applied to sparse CSR or CSC matrices by passing with_mean=False to avoid breaking the sparsity structure of the data.

sklearn.preprocessing.StandardScaler — scikit-learn 0.22.1 documentation


scikit-learnのStandardScalerで疎行列型のまま標準化する - 静かなる名辞

次元削減する

 Truncated SVDという素晴らしい手法があり、実装上も疎行列に対応しているので、こちらを使ってください。逆に、これ以外の選択肢は(おそらく)ありません。

sklearn.decomposition.TruncatedSVD — scikit-learn 0.22.1 documentation

 ただし、次元削減した方が良いのか、しない方が良いのかはなんとも言えません。次元削減は行わないで、疎行列型のまま後段のモデルに突っ込むという選択もあるからです。ぶっちゃけ性能は大して変わらないし、次元削減に時間がかかるのと大規模密行列になってしまうぶんだけ遅くなるかもしれない……という微妙な性質があります。

 それでも、次元削減が必要ならやればできます。

その他の各種transformerを使う

 transformの返り値がsparse matrixになるかどうかを確認してください。油断しているとnumpy配列に変換されます。

 リファレンスからある程度は読み取れますが、わからないことも多いので、一度動かして確かめた方が良いと思います。

分類や回帰など、予測に使う

 Truncated SVDで次元削減をした場合は勝手にnumpy配列になっているので、どんなモデルにも入力できます(実用的な速度と性能が両立できるかは別)。

 csr_matrixのまま突っ込む場合は、そのまま入力できるestimatorとできないestimatorがあるので、注意が必要です。これを確認するには、リファレンスのfitメソッドのパラメータを見ます。

 たとえばRandomForestClassifierだと、

X : array-like or sparse matrix of shape = [n_samples, n_features]

3.2.4.3.1. sklearn.ensemble.RandomForestClassifier — scikit-learn 0.22.1 documentation

 という記述があり、sparse matrixと書いてあるのが「疎行列型でも受け付けて、適切に取り扱ってあげますよ」という意味です。一方、たとえばLinearDiscriminantAnalysisだと(あまり使う人もいないと思いますが)、

X : array-like, shape (n_samples, n_features)

sklearn.discriminant_analysis.LinearDiscriminantAnalysis — scikit-learn 0.22.1 documentation

 と書いてあります。array-likeのときは、渡せば動くけど、内部的にはdenseな表現(numpy配列)に変換されてしまう公算が大きいです。でもけっきょくのところはよくわからないので、実際に入れて動くかどうか試してみた方が良いでしょう。

 他に実例は省略しますがnumpy arrayと書いてあるときも(たぶん)あり、この場合はたぶんsparse matrixだとエラーを吐きます。

 実際に動かしてみないと挙動がわからないこともままあるので、突っ込んでみてエラーが出ないか、メモリ消費が異常に膨れ上がらないかを確認しながら作業した方が良いと思います。

 以下は疎行列型でも行ける(と思う)代表的なestimatorのリストです。

 分類器

  • sklearn.ensemble.RandomForestClassifier
  • sklearn.svm.SVC
  • sklearn.svm.LinearSCV
  • sklearn.naive_bayes.MultinomialNB

 非負のみ。文書分類向き

  • sklearn.linear_model.LogisticRegression

 回帰モデル

  • sklearn.ensemble.RandomForestRegressor
  • sklearn.svm.SVR
  • sklearn.linear_model.ElasticNet

 代表的なものはだいたい対応しているけど、たまに使えないのもあるという感じです。

実際にやってみる

 20newsgroupsの文書ベクトルを返してくれるものがあるので、それでやります。

Classes 20
Samples total 18846
Dimensionality 130107
Features real

sklearn.datasets.fetch_20newsgroups_vectorized — scikit-learn 0.22.1 documentation

sklearnのfetch_20newsgroups_vectorizedでベクトル化された20 newsgroupsを試す - 静かなる名辞

 ご覧の通りでかいので、ナイーブにnumpy配列に変換して扱おうとすると苦労します。前にやったときは密行列に変換しようとしていろいろ苦労していましたが、疎行列型のままやった方がシンプルです。

 もちろん通例通り、Pipelineを使ってモデルを組み合わせます。

【python】sklearnのPipelineを使うとできること - 静かなる名辞

from sklearn.datasets import fetch_20newsgroups_vectorized
from sklearn.feature_selection import SelectKBest
from sklearn.decomposition import TruncatedSVD
from sklearn.svm import LinearSVC
from sklearn.pipeline import Pipeline
from sklearn.metrics import classification_report

def main():
    train = fetch_20newsgroups_vectorized(subset="train")
    test = fetch_20newsgroups_vectorized(subset="test")
    X_train, y_train = train.data, train.target
    X_test, y_test = test.data, test.target
    target_names = train.target_names

    skb = SelectKBest(k=5000)
    tsvd = TruncatedSVD(n_components=1000)
    svm = LinearSVC()
    clf = Pipeline([("skb", skb), ("tsvd", tsvd), ("svm", svm)])
    clf.fit(X_train, y_train)
    y_pred = clf.predict(X_test)
    print(classification_report(
        y_test, y_pred, target_names=target_names))

if __name__ == "__main__":
    main()

 実行したところ、1分くらいで処理が完了しました。パフォーマンスのよさが伺えます。

 結果。

                          precision    recall  f1-score   support

             alt.atheism       0.70      0.68      0.69       319
           comp.graphics       0.70      0.71      0.70       389
 comp.os.ms-windows.misc       0.71      0.71      0.71       394
comp.sys.ibm.pc.hardware       0.66      0.64      0.65       392
   comp.sys.mac.hardware       0.76      0.76      0.76       385
          comp.windows.x       0.79      0.72      0.75       395
            misc.forsale       0.81      0.87      0.84       390
               rec.autos       0.83      0.83      0.83       396
         rec.motorcycles       0.91      0.90      0.90       398
      rec.sport.baseball       0.84      0.89      0.87       397
        rec.sport.hockey       0.92      0.95      0.94       399
               sci.crypt       0.89      0.88      0.89       396
         sci.electronics       0.66      0.65      0.66       393
                 sci.med       0.81      0.78      0.79       396
               sci.space       0.86      0.87      0.86       394
  soc.religion.christian       0.76      0.91      0.83       398
      talk.politics.guns       0.68      0.88      0.77       364
   talk.politics.mideast       0.91      0.81      0.85       376
      talk.politics.misc       0.71      0.54      0.62       310
      talk.religion.misc       0.61      0.41      0.49       251

                accuracy                           0.78      7532
               macro avg       0.78      0.77      0.77      7532
            weighted avg       0.78      0.78      0.78      7532

 今回は性能を重視していないのでこの程度です。このタスクだとできるだけ次元を維持したまま(疎行列型のまま)ナイーブベイズに入れたほうが速くて性能が出るという知見を以前に得ています。その場合は0.83くらいまで出ています。

【python】sklearnのfetch_20newsgroupsで文書分類を試す(5) - 静かなる名辞

まとめ

 うまく疎行列型配列を使うと数桁くらい時間を節約できます。ぜひ活用してみてください。

 こちらの記事もおすすめです。
scikit-learnのモデルに疎行列(csr_matrix)を渡したときの速度 - 静かなる名辞