LoginSignup
8
2

More than 1 year has passed since last update.

os.environ["PYTHONHASHSEED"]では実験を再現できないケースがあった話

Last updated at Posted at 2022-03-24

はじめに

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で列順が変わると学習結果が変わるかどうかは、データセットに依存するようです。
似たコードでも、この問題が発生するケースと発生しないケースがありました。

この問題を再現するコードを書いてみました。

lightgbm_reproductivity.py
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"]で設定した環境変数がプロセス起動時に読み込まれるので、問題が発生しないのではないかと思います。

参考

8
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
2