Era Boost について
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 にneutralization をかけた結果がこちら。以前と比べるとかなり良さそうである。
特筆すべき点は、おそらくValidation Mean(validation data 上でのmean spearman correlation)が向上していることであろう。通常、neutralization をかけるとVaidation Mean
は下がるとされているので、これは意外な結果である。
今回紹介した内容が、Tournament発展の助けとなれば幸いである。
最後に
Era Boost のコードはForum にあるが、これをRank Learning 仕様にするとなると若干の修正が必要である。最後に修正ver を載せて、本記事を終わらせたいと思う。
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=5, learning_rate=0.01, 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_train = 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.
Discussion