👑

Kaggle調査① 「LLM/時系列 関係コンペ」

に公開
2025/11/18

本記事の動機

皆さんご存じの通り、Kaggle は最新の機械学習技術を学ぶのには最高峰の環境です。私自身 Kaggle をやっている訳ではありませんが、コンペ上位入賞者の知見は共有され、それを読むだけでも大変勉強になります。

それらは非常に実践的で、かつ最新の技術です。本ブログでは、時系列や言語モデルに関連した論文等の解説を試みていますが、それだけで、自身の適応したい環境に応用できる訳ではありません。主流アーキテクチャーやオプティマイザー、ライブラリなど、様々な要点を抑えて初めて良いモデルを作る事ができます。

今回は、そんな最近のトレンドを押さえたい、という動機から、直近のLLM/時系列に関係する Kaggleコンペの上位解法 をまとめていきたいと思います。

注意事項

以下のまとめは私の理解に基づき、 事実とは異なった(間違った)解釈が含まれる 可能性があります。

1つ1つのコンペ、それぞれの解法を完全に理解する事は非常に難しい です。勿論時間をかければある程度の理解は叶うかもしれませんが、解説記事やコードが不完全なケースもありますし、私は多くのコンペをまとめる予定ですので、1つのコンペにかける時間にも限りがあります。そのため、全体的な辻褄が合いそうであれば細かな内容は軽視 します。なので、よくよく考えるとこの詳細が分からない、といった内容も多々含まれます。

本記事は自分自身の知識向上のために書きますが、多くの方の参考になれば幸いです。しかし、間違い等も多く含まれる可能性があるので、あくまで本記事は参考程度にとどめ、可能な限り元ソースを読んで理解していただく事をお願いします。

記事の方針

  • 2025年11月以前の終了したコンペを対象
  • LLM/時系列に関係すると私が判断したコンペを対象
  • 使用されるライブラリやモデル、汎用的なテクニックについて注視
  • そのため、コンペに依存した内容(特徴量の前処理やハックに近い解法など)は基本的に扱いません
  • 以下で抽出された工夫などは私の独断と偏見によるもので、本当に優れた工夫を見逃している可能性は多々あります
  • 基本的に上位入賞者の解法を取り扱う予定です
  • 解法読解のしやすさのため、チームを組んでいない(チーム数が少ない)参加者の解法を優先します ※2位解法を飛ばして3位解法とかになるかもしれませんが、深い理由はありません

Jigsaw - Agile Community Rules Classification

https://www.kaggle.com/competitions/jigsaw-agile-community-rules

概要

特定のルールとそのルールに違反するコメント例と違反していないコメント例が与えられ、ある対象のコメントがそのルールに違反しているかどうかを0~1の確率で判定する。

モデル

1st

model Public Score Private Score
Qwen3-14b 0.9297 0.9239
Qwen2.5-14b 0.9287 0.9232
Qwen3-8b 0.9272 0.9236
Qwen3-4b-instruct-2507 0.9258 0.9198
llama3.1-8b 0.9257 0.9202
Ettin-400M 0.8991 0.8944
Ensemble 0.9344 0.9290

3rd

model Public Score Private Score
2 x Qwen2.5-instruct-14b 0.90828 0.90248
2 x Qwen3-instruct-14b 0.92571 0.92049
2 x Qwen2.5-instruct-7b 0.91024 0.90637
2 x Qwen3-instruct-8b 0.89598 0.88969
2 x bge-base-v1.5 0.90952 0.89714
Qwen3-embedding-0.6b 0.88856 0.88021
DeBERTa-v3-small 0.89546 0.88946

学習と工夫

1st

  • Unsloth で 4bit/8bit量子化
  • peft や SFTTrainer を使って LoRA + SFT ( Supervised Fine-Tuning )
  • チャット形式に変換(system: ルール、user: コメント+rule、assistant: Yes/No)して学習。train_on_responses_only を使い、assistant の "Yes/No" トークン部分だけに損失を当てて学習 ※つまり別のヘッドをつけてバイナリで学習するのではなく、あくまでも会話形式として学習し、さらに一部のtoken にだけ loss をつける.
  • 推論も同様 "Yes/No" に関連する "yes"/"Yes"/"Y"/"True" などのtokenのみの logit を拾って合成。
  • adamw_8bit を使って Optimizer も圧縮

3rd

  • Slow ensemble 群(Qwen2.5/3 14Bなどの巨大LLM)と Fast ensemble 群(軽量encoder系モデル)を用意.
  • 最終token の hidden state を特徴量として古典ML(LGBM/XGBoost/SVC)でbinary学習して出力
  • Fast ensemble 群では Triplet loss や ArcFace を使って学習
  • TTT(Test-Time Training)で、submit 時のテストデータで与えられる positive_example_* / negative_example_*を使って追加学習.
  • vLLM を使って推論高速化

所感とメモ

  • 1st の解法はシンプル. SFTで一貫した学習になっているのがすごい.
  • decoder の巨大モデルは、やはり巨大ゆえに強い. encoder系の言語モデルで巨大なものは少ない. 最近だと ModernBERT などの選択肢もあるが、1stも3rdも使ってなかったので何故だろうと調べてみたが、以下の様なコメントを発見.

I should have made all the essential models for the winning run public.
In my case both Ettin and Modernbert were better than Deberta, but I didn't try hard on them, they were only used to enrich the ensemble.
[Reply]
ok, modernbert was also weaker for me. Thanks for the reply.

  • 改めて感じるが、アンサンブルって強い
  • 学習方法も多様. 通常通り次 Token を生成して Y/N の loss を当てたり、hidden state を別の学習機の入力に使ったり

MAP - Charting Student Math Misunderstandings

https://www.kaggle.com/competitions/map-charting-student-math-misunderstandings/

概要

数学の問題における生徒の選択肢とそれを選んだ理由の自由記述から、その記述に含まれる 数学的な誤概念 の有無と誤概念の種類を当てる

モデル

1st

model loss[seed1, seed2, seed3] MAP@3[seed1, seed2, seed3]
deepseek-ai/DeepSeek-R1-Distill-Qwen-7B 0.2716 [0.2809, 0.2871, 0.2835] 0.9444 [0.9426, 0.9428, 0.9421]
Qwen/Qwen3-8B 0.2677 [0.2813, 0.2777, 0.2756] 0.9455 [0.9433, 0.9450, 0.9437]
zai-org/GLM-Z1-9B-0414 0.2627 [0.2783, 0.2762, 0.2761] 0.9469 [0.9455, 0.9433, 0.9452]
deepseek-ai/DeepSeek-R1-Distill-Qwen-14B 0.2621 [0.2782, 0.2698, 0.2738] 0.9464 [0.9421, 0.9443, 0.9450]
Qwen/Qwen3-14B 0.2614 [0.2707, 0.2744, 0.2695] 0.9477 [0.9442, 0.9451, 0.9461]
Qwen/Qwen3-32B 0.2589 [0.2687, 0.2718, 0.2700] 0.9484 [0.9465, 0.9451, 0.9454]
zai-org/GLM-Z1-32B-0414 0.2560 [0.2713, 0.2681, 0.2682] 0.9480 [0.9450, 0.9463, 0.9462]
Qwen/Qwen3-32B+zai-org/GLM-Z1-32B-0414 0.2530 0.9496

2nd

model data LB PB use final inferrence time
Qwen2.5-14B-AWQ total data 0.948 0.946 submission 1(best) 20min
Qwen3-14B-AWQ fold1 0.948 0.943 submission 1(best) 20min
Qwen2.5-32B-AWQ fold1 0.948 0.944 submission 1(best) 40min
Qwen2.5-32B-AWQ total data 0.949 0.945 submission 1(best) 40min
QWQ-32B-AWQ ext data 80k total data training distal 0.949 0.948 submission 2 40min
Qwen2.5-32B-AWQ ext data 80k fold1 distal 0.949 0.947 submission 2 40min
Qwen3-14B-AWQ ext data 80k fold1 distal 0.950 0.945 submission 2 20min

3rd

model task cv lb* pd use final inferrence time Note (other info)
Qwen/Qwen2.5-14B-Instruct-AWQ CausalLM 0.9469 0.943 0.942 ✅ 30 min
Qwen/Qwen2.5-32B-Instruct-AWQ CausalLM 0.9498 0.949 0.944 ✅ 60 min
Qwen/Qwen2.5-72B-Instruct-GPTQ CausalLM - 0.950 0.946 ✅ 190 min
Qwen/Qwen2.5-72B-Instruct-GPTQ CausalLM - 0.950 0.948 190 min use EMA for training (Single Best)
Qwen/Qwen2.5-32B-Instruct-AWQ CausalLM 0.9474 0.945 0.941 60 min Without choices in prompts
Qwen/Qwen2.5-72B-Instruct-GPTQ CausalLM - 0.946 0.940 190 min Without choices in prompts

学習と工夫

1st

  • 以下の内容を入力する. AAAAAAAAA:BBBB には True_Correct:NA や True_Misconception:Incomplete などが入る.
<|im_start|>user
**Question:** {QuestionText}
**Choices:** {MC_Choices}
**Correct Answer:** {Answer}
**Common Misconceptions:** {MisconceptionCandidates}
**Student Answer:** {MC_Answer}
**Student Explanation:** {StudentExplanation}
<|im_end|>
<|im_start|>assistant
AAAAAAAA:BBBB<|im_end|>
  • 方向性として、上述のtokenを入力した時の最後の隠れ層を取り出し、nn.Linear(hidden_size, 1) として logit 化する事である. つまり、質問内容 + AAAAAAAA:BBBB を入力し、AAAAAAAA:BBBB という target に関する特徴量を隠れ層から得ている.
  • しかし上述の方法では 質問内容 が共通部分となり、65種類の? AAAAAAAA:BBBB を得るのは計算に無駄がある. そこで 質問内容 + AAAAAAAA:BBBB + CCCCCCCC:DDDD + ... のようにtargetを連続で入力し、かつ target 同士が互いに Attention を計算しないように mask し(FlexAttention と呼んでいる)、1度の入力で、65種の target に関する特徴量を得ている.
  • 5-fold × 複数seed 学習している
  • LoRA ではなく、offload_adam ※本人製作※ を使ってメモリを軽量化し、full-parameter training を行っている.
  • より巨大なモデル Qwen/Qwen3-235B-A22B-Thinking-2507-FP8 を使って、そのラベルを正当化する 短い根拠文 を生成し、それをマルチタスク用の学習として加える. つまり、通常の上述の CrossEntropy に加えて、 短い根拠文 を予測するようなloss を加えて学習する.

2nd

  • Generate one student output with a rare candidate for each question. とあり、各質問のレアな target に対して追加の StudentExplanation をLLMで生成した. その際に、そのレアな target の StudentExplanation をより精度良く作成するために、プロンプトの中に contrastive なペア群を埋め込む事を行った
  • soft ラベル(0 ~ 1の確率likeな数字)を付けて hard ラベル(0 or 1)の過学習を抑止する事を考える
  • まず 4 LLM models (phi_4_reasoning_14b, qwen3_32b, mistral_12b, qwen2_5_72b) in 5 fold を hard ラベルで学習し、train + extra data に対して soft ラベルを作る.
  • qwen3-14b で、A〜BM の記号tokenに対する hard label での loss + 最終トークンの logits(A〜BM) を soft label で学習.
  • 全体的に LoRA なのか full parameter の fine tuning なのかは不明.
  • 65カテゴリを37に再分類して学習.
  • 37分類×4モデル+65分類×4モデルを等重みで融合。
  • AWQ ( Activation-aware Weight Quantization ) と vllm を使って推論を高速化

3rd

  • LoRA SFT かつ multi-task learning. True / False (2 classes), Correct / Misconception / Neither (3 classes), Misconception (36 classes) で分類の multi task.
  • R-Drop (Regularized Dropout), AWP(Adversarial Weight Perturbation), EMA(Exponential Moving Average)を使って精度向上
  • bfloat16をfloat16に変更することで、推論速度が2倍になり、padding="max_length" ではなく padding=False に設定することで、さらに2倍の高速化を実現した.
  • CausalLM とは GPT のような Decoder モデルの事.
  • 14B/32Bで全件推論し、top1確率 ≤ 0.95 の 難例だけ 72B で再推論.

所感とメモ

  • シンプルに、知らない手法がたくさんあった
  • offload_adam を自作してるの凄い。どこかで使ってみたい
  • AWQ ( Activation-aware Weight Quantization ) とは量子化手法である. 学習後に適用.
  • EMA とは重みの更新手法である. Weight-Teacher と Weight-Student を持っておき、通常の backward で Weight-Student を更新し、その一部割合で Weight-Teacher を更新し、Weight-Teacher → Weight-Student にコピーして学習を繰り返す. 疑似コード: teacher_wt.copy_(teacher_wt*decay + student_wt*(1-decay))
  • FlexAttention のようなテクニックは応用したい
  • 短い根拠文 を作っての multi-task 学習も良い. こう見ると、やはり1位の解法は相対的にシンプルに見える
  • 2段階の学習で疑似ラベルを初段で作るのも有効そうである

NeurIPS - Ariel Data Challenge 2025

https://www.kaggle.com/competitions/ariel-data-challenge-2025/

概要

AIRS(分光)と FGS1(単色フォトメトリ) という検出器がある。

AIRSは二次元のピクセルを持ち、分光された光量を検出する。分光された光は波長毎に広がり、横軸は波長の次元を持ち、縦軸は単なる空間方向となる。AIRS はこの計測をある時間ステップ単位で行い、つまり X x Y x time の次元を持つ。波長毎に縦軸を足し上げる事で、ある時間の波長毎の光量が確認できる。FGS1は時間ごとの総光量のみを検出するが、AIRSよりも高頻度でサンプリングされる。

このコンペでは、ある恒星を公転している惑星について、惑星が通過した時の恒星からの光を観測し、その減光量(惑星の大気がその光に干渉するため)を計測する事で、その惑星の大気を間接的に調査する事を目的としている。それはトランジット分光法と呼ばれ、宇宙線やノイズの影響を排除した後に、\frac{in-transit光量}{out-of-transit光量}のような比(とその誤差)を波長毎に計算し、submit する。

所感とメモ

  • 時系列かなと思って調べたら全然雰囲気が違ったので深堀はあまりしなかった(できなかった)が、基本的には物理法則をモデル化して、ベイズ推論やシミュレーションでバックグラウンドを推定するなど、とても専門的な解法が並んでいた
  • なんでもかんでも機械学習は良くないと改めて実感した
  • 純粋に、NNを試している参加者もいたが、なぜそれがワークしないのかについて、1位の方の以下のコメントが良かったので書いておく

I wonder do you have some high-level insights on why Bayesian without NN work well in 2024 and 2025. As our team tried end-to-end deep neural networks (DNNs) which is extremely vulnerable and still need ensemble with other methods for robustness. My insight is that

  1. The actual noise functions is linearly like so that DNNs overfit it.
  2. The training data is small so that DNNs overfit it.
  3. The data sample is not infomation-abundent along with outliers so that DNNs overfit it.

↓返答

Bayesian methods have an edge here because the underlying phsyics are relatively simple, but at the same time challenging for neural networks to identify. You just can't get away with ignoring the domain knowledge - neural networks are not going to learn that here (except perhaps if you provide enough training data so that they can essentially memorize all solutions).

CMI - Detect Behavior with Sensor Data

https://www.kaggle.com/competitions/cmi-detect-behavior-with-sensor-data

概要

手首に付けたデバイス「Helios」のセンサーデータから以下を当てる分類問題

  1. 「BFRB っぽい動き」か「日常の何でもない動き」か(2値)
  2. 18種類ある具体的なジェスチャーのうちどれか(多クラス)

※ハック?のような後処理が効いたようなので、それを使ってない解法も選択.

モデルと学習と工夫

1st

inference code を見る限りでは、以下のような構造が登場する。

model classの詳細メモ
  • SEBlock: 時系列次元を平均し、特徴量次元に対する Sigmoid の Gate を作り、元の数値と掛ける事で有効でない特徴量の数値を小さくする

  • ResidualSEBlock: Conv1d -> BatchNorm1d -> ReLU -> Conv1d -> BatchNorm1d -> SEBlock -> 残差結合 -> MaxPool1d(2) -> Dropout. Conv1d で特徴量を横断しながら時系列方向にに畳み込みを行い、SEBlock で特徴量をフィルターしながら、残差結合を行い、時系列方向に pooling を行う.

  • AttentionLayer: 簡易的な Softmax Attention のようなもので、特徴量次元を nn.Tanh(nn.Linear(C, 1)) で -1 ~ 1 の1次元に絞り、それを時系列に対して Softmax し、入力x に掛け、時系列方向を全て足し上げる. x(B, T, C)の入力に対して y(B, C) の出力となる.

  • CrossAttention: ある branch A のQ(B, T, C) に対して、別の branch B,C,...(N個) K(B, N * T, C) と V(B, N * T, C) の Attention を計算する.

  • IMUCrossAttentionFusion: imu1, imu2, imu3, thm, tof の合計5つの branch があり、Q: imu1 に対しての KV: imu2, imu3, thm, tof, Q: imu2 に対しての KV: imu1, imu3, thm, tof, ... と5パターンのCrossAttention の計算を行う

  • ImuModelLSTM: LSTM と書いてあるが、ここにLSTMは使われていない. IMU特徴量に対して ResidualSEBlock ブロックが2レイヤー適用される

  • ThreeBranchModelLSTM: メインモジュールその1. IMU に ImuModelLSTM (つまりResidualSEBlockが2段)、THM と TOF に ResidualSEBlockをそれぞれ1段、その出力を IMUCrossAttentionFusion で CrossAttention、その出力の 5 branch 分を 横に concat して bidirectional LSTM -> AttentionLayer -> FFN となっている

  • ThreeBranchModelAtt: メインモジュールその2. imu1, imu2, imu3, thm, tof を出すために ResidualSEBlock を2段、それぞれに PositionalEncoding(※Transformer初期のPEと同じ)をのせて別々に MultiHeadSelfAttention -> AttentionLayer. imu1, imu2, imu3, thm, tof の出力を concat して FFN.

  • CoordAttention: Conv1d を特徴量方向と時系列方向に使い、特徴量方向に Sigmoid で weight を作り, 時系列方向に Sigmoid で weight を作り, それを外積して元の入力に対する Gate を作って掛ける

  • ResCNNBlock: 入力x を2段 CNN -> CoordAttention, その出力と入力を足す(入力との残差)-> ReLU (と MaxPool).

  • TempCNNBlock: Conv1d -> BatchNorm1d -> ReLU -> CoordAttention -> 入力と残差結合

  • MLPAttention: AttentionLayer と似ている. 時系列方向の weight を作って掛けて足し上げて y(B, C) の出力となる

  • ThreeBranchModelCNN: メインモジュールその3. imu1, imu2, imu3, thm, tof に ResCNNBlock を3段、その出力を横に concat して CoordAttention - Dropout -> TempCNNBlock を 4段 -> MLPAttention -> FFN

  • 特徴量を5ブランチ(IMUで3つと, THM, TOF)に分ける.
  • 欠損が多いので、その割合によって IMU-only モデルか All-feature モデルのどちらを使って推論するかを決める
  • 各ブランチ独立に、Gating構造(入力に対して 0~1の値をかけて信号を間引く)とその残差結合を持つブロックを2段程度かませて、各ブランチ\bold{B}_iを出力する
  • 出力\bold{B}_iを3つの手法でブレンドする
    1. \bold{B}_i横断の CrossAttention. \bold{B}_i と \bold{B}_j のようなAttentionを計算. その後 concat して Bidirectional LSTM -> FFNにかける
    2. \bold{B}_i独自に SelfAttention. その後 concat して FFN
    3. \bold{B}_iを concat して、CNN を4段かまして MLPAttention -> FFN
  • torch.optim.Adam, lr = 4e-4 or ​lr = 5e-4, ​weight_decay = 3e-4, CosineAnnealingWarmRestarts ( + LinearLR)
  • Mixup などのデータ拡張を利用

6th

  • メイン module とは別にシンプルな U-Net を訓練して、ジェスチャーセグメントを推定してメインへの入力としている
  • Conv1d -> ReLU -> BN ( -> MaxPooling) を x3 して、推定されたジェスチャーセグメントとそれ以外で 時系列方向に平均を取り 512次元に落とす(AvgPooling) というブロック構造を取る
  • このブロックを、特徴量のブランチ毎に流し(計6ブランチ)、6つの out(B, 512)を得る.
  • out(B, 512) を独自に MLP にして loss x 6 を計算、out の一部のブランチだけで concat して MLP して loss 計算など、様々なカテゴリで loss を計算し、全体を最適化しつつ、ブランチ単位にも最適化している
  • 異なるランダムシードを持つデータセット全体でそれぞれ50回トレーニングし、100個のNNの出力を平均化
  • RAdamScheduleFree が optimizer
  • Augmentation は使っていない
  • EMA も改善なし. アンサンブルが多いので、そっちで改善されている可能性あり
  • モデル評価の表で、Cross Validation (単一と 全CVモデル平均) から full training までの評価値の流れがまとめられている

所感とメモ

  • シンプルな驚きとして、CNNやLSTMなど、初期の構造が今も活躍するという事
  • 1st では Gating のような各特徴量への重要度を掛けて残差結合するという構造が多く、ここでは紹介していない上位入賞者も似たような構造の採用が見られた
  • データ量にも依存するだろうが、時系列で Conv1d がここまで強いとは
  • やはりそのデータセットに起因する構造を作る事が大事に思う. 何でもかんでも Transformer という事はない
  • それにしても 6th のシンプルさには驚愕... full data で50回x2モデル訓練して100モデル平均は圧巻. この知見は目から鱗
  • 6th ではデータ拡張が無かったが、これはシンプルに前処理と Conv の構造がかみ合っている(つまり様々なカーネルで特徴抽出できている)結果だと思う
  • Convなどの layer の積み重ねだが、割と浅くても(4層ぐらい)問題なさそうに見える
  • 時系列のデータ拡張 について勉強になった. ChatGPT で簡単にまとめたものも載せておく.
時系列 augmentation

ノートブックの中身をざっと確認しました。ここで言っている「これらのデータ拡張」は:

  • GaussianNoise
  • PinkNoiseSNR
  • TimeStretch
  • TimeShift
  • ButterFilter
  • MixUp

の6種類です。CMIコンペの IMU 時系列(形状はだいたい (L, C):長さ L × チャンネル数 C)に対して、具体的にどう変換しているかを順番に説明します。


共通の仕組み(SignalTransform / Compose / OneOf)

まず、すべての拡張クラスはベースクラス SignalTransform を継承しています。

class SignalTransform:
    def __init__(self, always_apply: bool = False, p: float = 0.5):
        self.always_apply = always_apply
        self.p = p

    def __call__(self, y: np.ndarray):
        if self.always_apply:
            return self.apply(y)
        else:
            if np.random.rand() < self.p:
                return self.apply(y)
            else:
                return y

    def apply(self, y: np.ndarray):
        raise NotImplementedError
  • always_apply=False かつ p のとき
    → np.random.rand() < p の場合だけ apply を呼ぶ(確率的に適用)。
  • always_apply=True のとき
    → 毎回必ず apply を呼ぶ。

さらに、

  • Compose([tr1, tr2, ...])
    → tr1(tr2(...(x))) のように、順番に拡張を適用。
  • OneOf([tr1, tr2, ...])
    → リストから1つランダムに選んで、その拡張だけを適用。

という構造になっています。


1. GaussianNoise(白色ガウシアンノイズの加算)

class GaussianNoise(SignalTransform):
    def __init__(
        self, always_apply: bool = False, 
        p: float = 0.5, max_noise_amplitude: float = 0.20, **kwargs
    ):
        super().__init__(always_apply, p)
        self.noise_amplitude = (0.0, max_noise_amplitude)

    def apply(self, x: np.ndarray, **params):
        noise_amplitude = np.random.uniform(*self.noise_amplitude)
        noise = np.random.randn(*x.shape)  # shape (L, C)
        augmented = (x + noise * noise_amplitude).astype(x.dtype)
        return augmented

具体的な処理

  1. noise_amplitude ~ Uniform(0, max_noise_amplitude) を1つサンプリング。
  2. noise = N(0,1) の標準正規ノイズを、x と同じ形 (L, C) で生成。
  3. x + noise_amplitude * noise を計算して返す。

イメージ

  • 各タイムステップ・各チャンネルに独立な白色ガウシアンノイズを足している。
  • max_noise_amplitude が小さいほど、元の波形から大きくは崩れない「微小なゆらぎ」になる。

2. PinkNoiseSNR(指定 SNR のピンクノイズ加算)

class PinkNoiseSNR(SignalTransform):
    def __init__(self, always_apply=False, p=0.5, min_snr=5.0, max_snr=20.0, **kwargs):
        super().__init__(always_apply, p)
        self.min_snr = min_snr
        self.max_snr = max_snr

    def apply(self, y: np.ndarray, **params):
        snr = np.random.uniform(self.min_snr, self.max_snr)  # [min_snr, max_snr] dB
        a_signal = np.sqrt((y ** 2).max(axis=0))             # 各チャンネルのピーク振幅 (C,)
        a_noise  = a_signal / (10 ** (snr / 20))             # 各チャンネルのノイズ目標振幅 (C,)

        # L 長さのピンクノイズをチャンネル数分生成 → (L, C)
        pink_noise = np.stack(
            [cn.powerlaw_psd_gaussian(1, len(y)) for _ in range(y.shape[1])],
            axis=1
        )

        # 現在のノイズ振幅
        a_pink = np.sqrt((pink_noise ** 2).max(axis=0))      # (C,)

        # 目標振幅になるようチャンネルごとに正規化
        pink_noise_normalized = pink_noise * (a_noise / a_pink)

        augmented = (y + pink_noise_normalized).astype(y.dtype)
        return augmented

ポイント

  • SNR(dB) をランダムに選ぶ:
    snr ~ Uniform(min_snr, max_snr)。
  • 元信号 y の各チャンネルごとの最大振幅 a_signal を基準に、
    「このくらいの SNR になるノイズ振幅 a_noise」 を計算。
  • powerlaw_psd_gaussian(1, L) で 1/f スペクトルのピンクノイズを生成し、
    それをチャンネル毎にスケーリングして、所望の SNR に合わせる。
  • 最後に y + pink_noise_normalized を返す。

なぜピンクノイズ?

  • 白色ノイズは全周波数帯でフラットですが、
    ピンクノイズは低周波数側が強く、実際のセンサドリフトやゆっくりした揺らぎに近い。
  • そのため、「現実っぽいノイズ」を足してロバスト性を上げる、という意図です。

3. TimeStretch(線形補間での時間伸縮)

class TimeStretch(SignalTransform):
    def __init__(self, max_rate=1.5, min_rate=0.5, always_apply=False, p=0.5):
        super().__init__(always_apply, p)
        self.max_rate = max_rate
        self.min_rate = min_rate

    def apply(self, x: np.ndarray):
        """
        x: shape (L,) or (L, C)
        rate: 1.2 → 20% 長く, 0.8 → 20% 短く
        """
        rate = np.random.uniform(self.min_rate, self.max_rate)
        L = x.shape[0]
        L_new = int(L / rate)
        orig_idx = np.linspace(0, L - 1, num=L)
        new_idx  = np.linspace(0, L - 1, num=L_new)

        if x.ndim == 1:
            return np.interp(new_idx, orig_idx, x)
        elif x.ndim == 2:
            return np.stack(
                [np.interp(new_idx, orig_idx, x[:, i]) for i in range(x.shape[1])],
                axis=1
            )
        else:
            raise ValueError("Only 1D or 2D arrays are supported.")

具体的な処理

  1. 伸縮率 rate ~ Uniform(min_rate, max_rate) をサンプリング。

  2. 元長さ L から、新しい長さ L_new = int(L / rate) を計算。

    • rate > 1 → データを「遅く再生」するイメージ(長さが短くなる or 長くなるかは実装次第だが、このコードでは L_new = L / rate)。
  3. orig_idx = [0, ..., L-1] と new_idx = [0, ..., L-1] を L_new 分割で作る。

  4. 各チャンネルごとに np.interp で線形補間し、L_new 長さの時系列に引き伸ばし/縮める。

イメージ

  • 波形の「時間軸」を伸縮することで、「同じ動作だが少しゆっくり/早く行っている」ようなパターンを合成。
  • その後の処理でパディングやトリムをして、最終的な長さをそろえることが多いです。

4. TimeShift(時間方向の平行移動)

class TimeShift(SignalTransform):
    def __init__(self, always_apply=False, p=0.5, max_shift_pct=0.25, padding_mode="replace"):
        super().__init__(always_apply, p)
        
        assert 0 <= max_shift_pct <= 1.0
        assert padding_mode in ["replace", "zero"]
        
        self.max_shift_pct = max_shift_pct
        self.padding_mode  = padding_mode

    def apply(self, x: np.ndarray, **params):
        assert x.ndim == 2, "`x` must be a 2D array with shape (L, N)"
        
        L = x.shape[0]
        max_shift = int(L * self.max_shift_pct)
        shift = np.random.randint(-max_shift, max_shift + 1)
        # (ここで時間軸方向に roll して、端の穴を埋めているイメージ)
        ...
        if self.padding_mode == "zero":
            if shift > 0:
                augmented[:shift, :] = 0
            elif shift < 0:
                augmented[shift:, :] = 0

        return augmented

コードが途中で省略されていますが、ノートの説明文から処理内容は以下の通りです。

処理のステップ

  1. 入力が (L, C) であることをチェック。

  2. max_shift = int(L * max_shift_pct)
    例: max_shift_pct=0.25 で L=200 なら ±50 ステップまでずらす。

  3. shift ~ randint(-max_shift, +max_shift) をサンプリング。

  4. np.roll(x, shift, axis=0) のように、時間方向にシフト。

  5. そのままだとロールで「巻き戻り」部分が混ざるので、

    • padding_mode="zero" のとき:

      • 右にシフト (shift>0) → 先頭 shift ステップを 0 埋め
      • 左にシフト (shift<0) → 末尾 |shift| ステップを 0 埋め
    • padding_mode="replace" のとき:

      • ロールでできた「空白」を、元信号の端の値で埋める(境界の値を伸ばす)ような実装が想定されます。

イメージ

  • 「動作の開始タイミング」が前にずれたり後ろにずれたりしたようなデータを作る。
  • モデルに「少し開始時刻がずれていても同じラベル」と学習させるのが目的。

5. ButterFilter(バターワース低域フィルタ)

class ButterFilter(SignalTransform):
    def __init__(self, always_apply=False, p=0.5, cutoff_freq=20, sampling_rate=200, order=4):
        super().__init__(always_apply, p)
        
        self.cutoff_freq   = cutoff_freq    # 例: 20 Hz
        self.sampling_rate = sampling_rate  # 例: 200 Hz
        self.order         = order          # フィルタ次数

    def apply(self, x: np.ndarray, **params):
        assert x.ndim == 2, "`x` must be a 2D array with shape (L, N)"
        return self.butter_lowpass_filter(x)

    def butter_lowpass_filter(self, data: np.ndarray):
        nyquist = 0.5 * self.sampling_rate           # ナイキスト周波数
        normal_cutoff = self.cutoff_freq / nyquist   # 正規化カットオフ
        b, a = butter(self.order, normal_cutoff, btype='low', analog=False)
        filtered_data = lfilter(b, a, data, axis=0)  # 各チャンネルを独立にフィルタリング
        return filtered_data

具体的な処理

  1. サンプリング周波数 Fs からナイキスト周波数 Fs/2 を計算。
  2. カットオフ周波数 Fc をナイキストで割って正規化。
  3. SciPy の butter(order, normal_cutoff, btype='low') で
    ローパス・バターワースフィルタの係数 b, a を設計。
  4. lfilter(b, a, data, axis=0) で、各チャンネルごとに時間方向へフィルタをかける。

イメージ

  • 高周波数のガサガサしたノイズを落として、低〜中周波の成分だけ残す。
  • IMU の揺れや動きの「なめらかなパターン」を強調して、細かいノイズを抑える前処理。

6. MixUp(時系列の線形合成)

MixUp 自体は SignalTransform のサブクラスではなく、Dataset / CustomDataset の中で行っている mixup です(コードは途中で省略されていますが、標準的な MixUp をしている形跡があります)。

一般的な MixUp の具体的な処理 はこんな感じです(時系列版):

  1. 2つのサンプル (X_i, y_i) と (X_j, y_j) を選ぶ。

    • ここでは X_i, X_j はそれぞれ (L, C) の IMU 時系列。
  2. λ ~ Beta(α, α)(例: α=0.2)をサンプリング。

  3. 入力を線形合成:
    [
    X_{\text{mix}} = \lambda X_i + (1 - \lambda) X_j
    ]

  4. ラベルも線形合成:

    • 回帰:
      [
      y_{\text{mix}} = \lambda y_i + (1 - \lambda) y_j
      ]
    • one-hot 分類:
      y_mix = λ * y_onehot_i + (1-λ) * y_onehot_j
  5. 学習時は (X_mix, y_mix) を1サンプルとして扱う。

このノートブックでも、

  • all_X にすべての時系列
  • all_y にターゲット
  • all_y_hard にハードラベル(クラス)

を持っていて、CustomDataset(..., mode="train") のときに、他のサンプルとペアを作って MixUp したものを返す構造になっているはずです。"Mixup y_true: ..." "Mixup y_true_hard: ..." をプリントして可視化しているセルもあります。

イメージ

  • 2つの動作パターンを「平均」したような時系列を作ることで、決定境界をなめらかにして、過学習を抑えるテクニック。
  • 画像で有名な MixUp と同じ発想で、それを (高さ, 幅, チャネル) ではなく (時間, チャネル) に適用しているだけです。

まとめ

ひとまず直近4つのコンペについて、一部の解法のみを取り上げまとめてみました。期待していたのは、シンプルな transfomer に最新の知見(optimizer や flash attention など)が入ったコードの発見、だったのですが、まだそういった内容には出会えていません。

それどころか、時系列データに関しては、そういった構造よりも Conv1d なんだなと、少し驚いた結果になりました。非常に勉強になります。現時点での簡単なまとめを書いておくと

  • LLM 系
    • Qwen / DeepSeek / GLM-Z1 あたりの「中〜大規模 CausalLM」が支配的
    • 学習は LoRA+SFT, あるいはフルFT+offload/量子化 が主流(Unsloth, AWQ, GPTQ, offload_adam, vLLM など)
  • 時系列系
    • 「Conv1d + 残差 + Attention + LSTM」みたいな古典構造がまだまだ強い(CMI)。
  • その他テクニック
    • TTT, FlexAttention, pseudo label, multi-task など

次回はいつになるか分かりませんが、さらに遡って各コンペについてまとめてみたいと思います。

参考

https://speakerdeck.com/masakiaota/kaggledeshi-yong-sarerudi-dui-xue-xi-fang-fa-awpnolun-wen-jie-shuo-toshi-zhuang-jie-shuo-adversarial-weight-perturbation-helps-robust-generalization

https://www.kaggle.com/code/bibhabasumohapatra/understanding-how-ema-works

Discussion