はじめに
kaggleなどでコードの再現性を担保したいとき、以下のように乱数シードを固定することがあると思います。
def seed_everything(seed: int):
random.seed(seed)
os.environ["PYTHONHASHSEED"] = str(seed)
np.random.seed(seed)
ここで環境変数PYTHONHASHSEED
が出てきますが、これはPython内部でハッシュ値を生成する際の固定シードに使われるようです。
以前に機械学習系のコードを書いているときに、これらのシードを設定しているはずなのに再現ができず、悩んでいたことがありました。結論としては、上の方法ではPYTHONHASHSEED
が実質的に固定できていないことが原因でした。
実験
まずpythonのコード側からPYTHONHASHSEED
を変更する場合です。以下のように、PYTHONHASHSEED
を同じ値に設定してからhash()を実行しているはずですが、hash値が一致しません。
※実行環境はWindowsです。
# 明示的にPYTHONHASHSEEDを未設定にしておく
$ set PYTHONHASHSEED=
$ python -c "import os; os.environ['PYTHONHASHSEED']='0'; print(hash('hoge'))"
7011951800243422814
$ python -c "import os; os.environ['PYTHONHASHSEED']='0'; print(hash('hoge'))"
-2235082839225683546 # 結果が一致しない
次に、PYTHONHASHSEED
を設定してからpythonを実行します。すると、hash値が一致します。
# PYTHONHASHSEEDを設定
$ set PYTHONHASHSEED=42
$ python -c "import os; print(hash('hoge'))"
8970739480496197780
$ python -c "import os; print(hash('hoge'))"
8970739480496197780
上の実験で結果が一致しない原因
KaggleのDiscussionによると、Pythonインタープリタは、起動時にのみPYTHONHASHSEEDを使用するようです。読み込まれた後から環境変数を上書きで変えてもダメということですね。
影響を受けたことがあるケース
上の現象でこれまでに悩んだケースを紹介します。
LightGBMの列指定
LightGBMで特徴量を変えて実験をするとき、使わない特徴量unuse_cols
を定義しておき、以下のような感じで最終的に使う列を決めるということをしたことがあります。
use_cols = list(set(df.columns) - set(unuse_cols))
このとき、lightgbmや他のモジュールのシードは固定しているはずなのにうまく実験が再現できず悩んでいました。
結果的には、この実装では列の並び順が不定になってしまうということが原因でした。
上のコードでは、set
は順序を保持しない集合なので、listに戻した時の並び順はハッシュ値に依存することになります。
そこで、以下のように列順を固定するようにしたところ、実験が再現できるようになりました。
use_cols = sorted(list(set(df.columns) - set(unuse_cols)))
なお、LightGBMで列順が変わると学習結果が変わるかどうかは、データセットに依存するようです。
似たコードでも、この問題が発生するケースと発生しないケースがありました。
この問題を再現するコードを書いてみました。
import os
import random
import warnings
import lightgbm as lgb
import numpy as np
import pandas as pd
from sklearn.metrics import mean_squared_error
from sklearn.model_selection import KFold
SEED = 42
os.environ['PYTHONHASHSEED'] = str(SEED) # ほんとは固定できていない
random.seed(SEED)
np.random.seed(SEED)
warnings.simplefilter('ignore')
if __name__ == "__main__":
n_col = 1000
n_data = 9999
features = [f"col{i}" for i in range(n_col)] + ["target"]
values = np.random.randint(0, 10, (n_data, n_col + 1))
train_df = pd.DataFrame(dict(zip(features, values)))
# 列順がPYTHONHASHSEEDの影響を受け、結果が変わる
features = list(set(list(train_df.columns)) - set(["target"]))
# 以下のようにした場合は、PYTHONHASHSEEDによらず結果は一致する
# features = sorted(list(set(list(train_df.columns)) - set(["target"])))
folds = KFold(n_splits=5)
folds = list(folds.split(train_df[features], train_df["target"]))
lgb_train = lgb.Dataset(train_df[features], train_df["target"])
params = {
'seed': SEED,
'objective': 'regression',
'metric': 'mse',
'learning_rate': 0.05,
'verbosity': -1,
}
bst = lgb.cv(
params,
lgb_train,
num_boost_round=10000,
early_stopping_rounds=50,
return_cvbooster=True,
folds=folds,
)
cvbooster = bst["cvbooster"]
result = np.array(cvbooster.predict(train_df[features])).mean(axis=0)
print(f"lightgbm version: {lgb.__version__}")
print(f'MSE: {mean_squared_error(train_df["target"], result)}')
上のコードを実行するとき、以下のようにpython実行前にPYTHONHASHSEED
を固定していないと、結果が一致しないことがあります。(一致するケースもあります)
$ set PYTHONHASHSEED=
$ python lightgbm_reproductivity.py
lightgbm version: 3.3.2
MSE: 2.04715139091826
$ python lightgbm_reproductivity.py
lightgbm version: 3.3.2
MSE: 2.0461430881687113
set PYTHONHASHSEED=42
のように事前に固定するか、列順をsorted()
などを使ってハッシュに依存せず一意の順番になるようにすると、結果を再現できるようになりました。
おそらく、GBDTの学習時に各ノードで使う列を決定するときに、誤差が同じ列がある場合は列順に依存して決定する、といったケースがあるのではないかと思っています。(誰か詳しい人教えてください、、、)
PyCaretのsession_id
PyCaretではsession_id
というパラメータでシードを固定するのですが、うまく実験が再現できないことがありました。
OS側でPYTHONHASHSEED
を固定するとうまくいきました。
この問題に対する対策
個人的には以下の2点を対策しています。
- なるべくhashに依存するようなコードを書かない(上のsetを使ったケースなどを避ける)
- それでも避けられない場合は、ユーザ環境変数などであらかじめ
PYTHONHASHSEED
を指定しておく
os.environ["PYTHONHASHSEED"]でも問題ない場合
最初にあげたseed_everything()
はよく見かけますし、こちらの記事にも記載があるように、os.environ["PYTHONHASHSEED"]
でも問題ないケースはあると思います。
推測ですが、ライブラリ内部でmultiprocessing
などを利用している場合、os.environ["PYTHONHASHSEED"]
で設定した環境変数がプロセス起動時に読み込まれるので、問題が発生しないのではないかと思います。