はじめに
表題の通りなのですが、サンプル数が多いデータに対してランダムフォレストを使うと思いの外メモリを食います。
また、ストレージにダンプしようとすると、ストレージ容量も消費します。
現象
なにはともあれやってみましょう。
import pickle from sklearn.datasets import make_classification from sklearn.model_selection import train_test_split from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import classification_report def main(): X, y = make_classification( n_samples=10**5, n_features=50, n_informative=10, n_classes=5) X_train, X_test, y_train, y_test = train_test_split(X, y) rfc = RandomForestClassifier(n_estimators=100, n_jobs=-1) rfc.fit(X_train, y_train) y_pred = rfc.predict(X_test) print(classification_report(y_test, y_pred)) with open("rfc.pickle", "wb") as f: pickle.dump(rfc, f) if __name__ == "__main__": main()
サンプル数は10**5です。ビッグデータというほどでもないけど、大抵の機械学習が問題なく使えるだけのデータがあるとします。
結果の善し悪しにはさほど興味がないのですが、一応見ておきます。
precision recall f1-score support 0 0.84 0.83 0.83 4926 1 0.87 0.87 0.87 5102 2 0.88 0.90 0.89 4963 3 0.85 0.81 0.83 5018 4 0.85 0.88 0.87 4991 accuracy 0.86 25000 macro avg 0.86 0.86 0.86 25000 weighted avg 0.86 0.86 0.86 25000
まあ、こんなものでしょう。
問題はメモリ消費量です。実行中に数百MBを消費します。データそのものは10**5行50列ですから、64bit浮動小数点数型としても40MBくらいで済むはずです。ランダムフォレスト本体がやたらメモリを消費しています。
これで出力されるrfc.pickleの容量は約210MBです。やたら大きくなっています。
原因
ランダムフォレストは決定木です。そしてデフォルトの設定では、枝は伸び放題で過学習気味です。
ためしに総ノード数を見てみます。この方法は正しいのかどうかはわかりませんが、スタックオーバーフローに書いてありました。
print(rfc.estimators_[0].tree_.node_count) # => 21193
けっこうたくさんノードがありますね。これが諸悪の根源で、各ノードごとに数byte~数十byte程度は消費するでしょうから、木全体では意外と大きな記憶領域を必要とし、そんな木を100本も500本も使うランダムフォレストはとても記憶領域の消費量が大きくなる……ということです。
こうなるとメモリを無駄に食うしキャッシュも効かないし遅いし、なかなか憂慮すべき事態です。
対策
根本的な対策の方向性としては、
- 性能を損なわない程度に木の複雑性を下げてノード数を減らす
- 性能を損なわない程度に木の本数を下げる
の2通りがありえます。今回みたいなデータの場合、木の本数はなかなか減らしづらいと思うので、木の複雑性を下げる方でやります。これなら上手くすれば過学習対策的に働き、ほとんど性能を損なわずに容量を減らせ、更に速くなります。
やることは普通の過学習対策です。
import pickle from sklearn.datasets import make_classification from sklearn.model_selection import train_test_split from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import classification_report def main(): X, y = make_classification( n_samples=10**5, n_features=50, n_informative=10, n_classes=5) X_train, X_test, y_train, y_test = train_test_split(X, y) rfc = RandomForestClassifier( n_estimators=100, min_samples_leaf=5, n_jobs=-1) rfc.fit(X_train, y_train) y_pred = rfc.predict(X_test) print(classification_report(y_test, y_pred)) with open("rfc.pickle", "wb") as f: pickle.dump(rfc, f) if __name__ == "__main__": main()
precision recall f1-score support 0 0.86 0.85 0.85 4952 1 0.87 0.85 0.86 5033 2 0.86 0.85 0.86 5048 3 0.84 0.86 0.85 4951 4 0.87 0.89 0.88 5016 accuracy 0.86 25000 macro avg 0.86 0.86 0.86 25000 weighted avg 0.86 0.86 0.86 25000
この状態で出力されるrfc.pickleは約100MBです。ほとんど性能を損なわずに(むしろ汎化性能をあげつつ)半分になりました。
また、メモリ空間をやたら消費する現象への対策にはならないものの、ストレージへの保存ではjoblibを使って圧縮することも可能です。上の対策を施した上で、更に圧縮も行います。
import joblib # 中略 joblib.dump(rfc, "rfc.pickle", 3)
これで約21MBになりました。
まとめ
このようにランダムフォレストは意外とメモリに優しくない手法なので、使い方を気をつけようという話です。もちろんデータ数が多いときの話で、1000未満でやるときとかは気にする必要は(ほとんど)ありません。
ランダムフォレストってなんとなくもっさりする(すごく遅くはないけどあまり速くもない)印象でしたが、ただでさえifの嵐なのに決定木全体がキャッシュに乗らないのか……ということに気づけたのが今回の発見でした。速くない訳ですね。