Era Boost について

6 min read読了の目安(約5400字

Numerai Advent Calendar 2020 20 日目の記事です

Numerai について

Numerai は、自社暗号通貨Numeraire(以下、NMR)を作った世界初のAIヘッジファンドです。

日本語の紹介記事は以下のふたつが参考になります。

今回話すこと

Rank Learning + Era Boost に neutralization をかけると、correlation を含めたパフォーマンスが上がるよという話。

導入

Numerai を攻略することはマーケットを攻略することに繋がるため、容易ではない。Numerai Forum では、参加者達がアイデアを出し合って、攻略方法を模索していたり、トーナメントそのものの仕様について議論していたりする。今回の記事では、その中でも初期のスレッドにある、Era Boost に関連した話題について議論したい。

Era Boost とは?

学習モデルのtraining data 中のパフォーマンスの悪いeraについて再度学習を行うことで、パフォーマンスの良いモデルを作ろうという手法らしい。おそらく、Numerai Tournament についてのスライドのP.13のBalance across Eras の実現方法の一つのなのだろう。実際にパフォームするかについては私は明確な答えを持ち合わせていないため、各自試していただきたい。まぁまぁ良いモデルができる。

私の初期モデル

コンペに参加した当初、Numerai Tournament は株価の順位付けをする問題だと解釈していた。ターゲットのデータ数が0.0, 0.25, 0.5, 0.75, 1.0 それぞれに20%ずつ割り振られていたからである。そのため、Rank Learning の手法が使えると考え、XGBRegressorではなく、XGBRankerを使用していた。しかし結果はイマイチ、INTEGRATION_TEST に負けてしまった。その後、しばらくはRegression の問題として捉えることとなる。

Numerai Forum にて

Rank Learning についての話題がForum に上がっていた。OHwAで取り上げられていたらしい。surajp氏がすごいパフォーマンスをあげていたので、Target Nomi 変更後、試してみることにした。

結果

まず、XGBRanker + Era Boostの結果がこちら。まぁまぁ悪そうである。
XGBRanker + Era Boost
次に、XGBRanker + Era Boost にneutralization をかけた結果がこちら。以前と比べるとかなり良さそうである。
XGBRanker + Era Boost with neutralization
特筆すべき点は、おそらくValidation Mean(validation data 上でのmean spearman correlation)が向上していることであろう。通常、neutralization をかけるとVaidation Mean
は下がるとされているので、これは意外な結果である。
今回紹介した内容が、Tournament発展の助けとなれば幸いである。

最後に

Era Boost のコードはForum にあるが、これをRank Learning 仕様にするとなると若干の修正が必要である。最後に修正ver を載せて、本記事を終わらせたいと思う。

Era Boost with Rank Learning
import numpy
from xgboost import XGBRanker

def ar1(x):
    return np.corrcoef(x[:-1], x[1:])[0,1]

def autocorr_penalty(x):
    n = len(x)
    p = ar1(x)
    return np.sqrt(1 + 2*np.sum([((n - i)/n)*p**i for i in range(1,n)]))

def smart_sharpe(x):
    return np.mean(x)/(np.std(x, ddof=1)*autocorr_penalty(x))

def spearmanr(target, pred):
    return np.corrcoef(
        target,
        pred.rank(pct=True, method="first")
    )[0, 1]

def era_boost_train(X, y, group, val, era_col, proportion=0.5, trees_per_step=10, num_iters=200):
    model = XGBRanker(booster="gbtree", max_depth=depth, learning_rate=lr, n_estimators=trees_per_step,
                      colsample_bytree=0.1, n_jobs=-1)
    features = X.columns
    ecdf = val[["id", "era"]].groupby("era").agg(["count"])
    egroup = ecdf[ecdf.columns[0]].values
    egroup = list(map(lambda x: [x], egroup))
    eval_set = [] 
    for era in val["era"].unique():
        era_set = val[val["era"]==era]
        eval_set.append((era_set[features], era_set["target"]))
    model.fit(X, y, group=group, eval_set=eval_set, eval_group=egroup, early_stopping_rounds=5)
    new_df = X.copy()
    new_df["target"] = y
    new_df["era"] = era_col
    for i in range(num_iters-1):
        print(f"iteration {i}")
        # score each era
        print("predicting on train")
        preds = model.predict(X)
        new_df["pred"] = preds
        era_scores = pd.Series(index=new_df["era"].unique())
        print("getting per era scores")
        for era in new_df["era"].unique():
            era_df = new_df[new_df["era"] == era]
            era_scores[era] = spearmanr(era_df["pred"], era_df["target"])
        era_scores.sort_values(inplace=True)
        worst_eras = era_scores[era_scores <= era_scores.quantile(proportion)].index
        print(list(worst_eras))
        worst_df = new_df[new_df["era"].isin(worst_eras)]
        era_scores.sort_index(inplace=True)
        era_scores.plot(kind="bar")
        print("performance over time")
        plt.show()
        print("autocorrelation")
        print(ar1(era_scores))
        print("mean correlation")
        print(np.mean(era_scores))
        print("sharpe")
        print(np.mean(era_scores)/np.std(era_scores))
        print("smart sharpe")
        print(smart_sharpe(era_scores))
        model.n_estimators += trees_per_step
        booster = model.get_booster()
        print("fitting on worst eras")
        print()
        print()
        cdf = worst_df.groupby("era").agg(["count"])
        group = cdf[cdf.columns[0]].values
        model.fit(worst_df[features], worst_df["target"], group=group, eval_set=eval_set,
                  eval_group=egroup, xgb_model=booster)
    return model

cdf = train[["id", "era"]].groupby("era").agg(["count"])
group = cdf[cdf.columns[0]].values

boost_model = era_boost_train(train[features], train["target"], group_train, validation_data,
                              era_col=train["era"], proportion=0.5, trees_per_step=10, num_iters=20)

多分これでいけるはず。XGBRegressor よりだいぶ時間がかかります。
間違っていたら言ってください。修正します。

numerai or die...!

※ English version will be later.