他人のコードを斜め読み(Kaggle/M5)
昨年度末におこなわれたM5コンペ(米国内におけるウォルマート購買予測)での下記の解法を拝読
下記スライドとコードがシッカリ上がっていたので非常に勉強になった
https://github.com/marisakamozz/m5
全部を見て書き下すのは時間が無いので、忘れそう+自分にとって所見を完結にまとめる
今回は3つ
- RMSSE
- Cosine Annealing
- 特定の確率分布を仮定した損失関数の組み方
また、この解法での工夫点としてlgbmを用いた特徴量選択・マージ方法や期間の選択・複製、層の組み方はここでは一切触れない。
これに関してはそういうものだと思いこむことにする。
誤差定義について
この課題では2種類の誤差定義がされていた
1つはWeighted Root Mean Squared Scaled Error (WRMSSE) : Accuracy section
2つめはWeighted Scaled Pinball Loss (WSPL): Uncertainly section
WRMSSE
RMSSEをweightedしたもの。RMSSEはMean Absolute Scaled Error (MASE)の改良版で、各時系列データポイントの誤差をスケール考慮した指標。
Weightedすることで時系列におもみを付けられる。別次元で実施する必要があり、今回の場合直近28日(test全期間)の売上を重みとして用いる


RMSSEは興味深い式をしている。
分母はt=2~nの(実際の時系列変化量)の2乗の単純平均。
分子はt=n+1~n+hの予測誤差の2乗の単純平均
つまり分母は予測期間直前までの変化の激しさ、分子は予測期間における誤差の激しさを示しており(「激しさ」と表現しているのは2乗をしているため)、シンプルにいえばRMSSEは予測誤差を過去のボラでキャリブレーションしたものとなる。
購買データのようなゼロが多いデータにおいても除算が可能になる点がメリット
コンペ添付資料の通り、ゼロでないデータが存在しないとダメなので、要注意
WSPL
Scaled Pinball Loss (SPL)をWeigtedしたもので、weightedは先と同様に別次元で重み付け

添付資料はこんな感じで難しく書いてあって、「1」がintの1ではなくて、Booleanの1という点でウザいがPinball lossと呼ばれる損失関数をスケーリングしたもの。
スケーリングはWRMSSEと同様に変化量を用いており、”2乗ではなく”変化量の絶対値平均を用いる。分母がソレを示す
分子はピンホールロスよよばれるもので分位数で連結される連続関数。分位数の外側はバイアス強めにかけて、分位数極端だとそのバイアス更に強くなる
単純だけども賢い関数。上とは違うが下図の通。τが分位数.横軸yが予測値、縦軸ρが損失関数

http://www.lsta.upmc.fr/BIAU/bp.pdf
Cosine Annealing
これは常識だったかもしれない。でも念の為
Annealingと聞くと量子アニーリングを想像しちゃうが、アプローチとしては似ている。
Annealingは和訳で焼きなまし、つまり何度もトライしまくってなるべく理想とする状態に近似させる。
Cosine Annealingは学習率スケジューラーの一種で、周期的に学習率を変化させる
その変化をCosine関数に沿っているというもの
Torchに普通に実装がかいてあるので転載

周期としてT_max(=iteretion)を指定して、変化率の周期を設定。
局所最適解に陥らないように「たまに」大きな学習率を突っ込むっているイメージ
ちょっとわかりやすい学習率まとめ→https://www.kaggle.com/isbhargav/guide-to-pytorch-learning-rate-scheduling
Pytorchで扱える学習率スケジューラー→https://katsura-jp.hatenablog.com/entry/2019/07/24/143104#
特定の確率分布を仮定
一番ざっくりとした題名だ
言語モデルとかでも予測対象が確率であることはよくある
自分時系列専門であんまりそんなことは気にしてこなかった
でも、こんかい時系列で遭遇してしまったので、備忘
とはいえ、そんな難しいことではない。
予測する分布がT分布(仮に)を仮定するならば、予測値に対する誤差関数の定義する際にT分布を仮定すればよいだけ
上記githubから拝借する
criterionを途中で定義する。いつもどおりに
|
1 2 3 |
model
=
M5MLPLSTMModel(is_cats,
dims,
n_hidden=args.n_hidden,
dropout=args.dropout,
use_te=args.use_te) criterion
=
M5Distribution(dist=args.dist,
df=args.df) module
=
M5LightningModule(model,
criterion,
train_loader,
None,
None,
args) |
確率分布を定義
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
class
M5Distribution(): def
__init__(self,
dist='Normal',
use_exp=True,
df=1): self.dist
=
dist self.use_exp
=
use_exp self.df
=
df def
_to_dist(self,
dist_params): if
self.dist
==
'Normal': mean
=
dist_params[:,
:,
0] if
self.use_exp: std
=
dist_params[:,
:,
1].exp() else: std
=
F.softplus(dist_params[:,
:,
1]) return
Normal(mean,
std) elif
self.dist
==
'StudentT': mean
=
dist_params[:,
:,
0] if
self.use_exp: std
=
dist_params[:,
:,
1].exp() else: std
=
F.softplus(dist_params[:,
:,
1]) return
StudentT(self.df,
mean,
std) elif
self.dist
==
'NegativeBinomial': if
self.use_exp: total_count
=
dist_params[:,
:,
0].exp() else: total_count
=
F.softplus(dist_params[:,
:,
0]) logits
=
dist_params[:,
:,
1] return
NegativeBinomial(total_count,
logits=logits) else: raise
NotImplementedError() def
get_loss(self,
dist_params,
y): dist
=
self._to_dist(dist_params) loss
=
-
dist.log_prob(y).mean() return
loss |
get_loss中に予測値を変換している部分があるけど、to_distで確率分布を定義、のち対数確率の平均をlossとしている。
以上。









