MOAI 2025 學習指南

讀題 → 思考 → 分析 → 設計 → 寫程式 → 驗證,每一題都帶你走一遍。
看完這份文件後,你不只能拿到分,更能獨立解類似的題


先看這張速讀表

你現在的目標建議先看
想知道整份資料夾怎樣用第 0 節
想先背通用解題流程「通用解題思維模板」
想做 ML 題Problem 1
想做 NLP 題Problem 2
想做 CV 題Problem 3
想賽前最後複習「賽前 Checklist」

目錄

  1. 先看這裡:六個檔案怎麼用
  2. 通用解題思維模板
  3. Problem 1 — ML 路徑難度回歸
  4. Problem 2 — NLP 文本情感分類
  5. Problem 3 — CV 條件變分自編碼器 CVAE
  6. 賽前 Checklist

0. 六個檔案怎麼用

我在 moai2025/ 資料夾為你生成了 6 份提交版 notebook

檔案對應原題用途
ML-part1.ipynbmoai-2025-ml.ipynb主 ML 解答(grader 全綠 + 線性回歸 submission)
ML-part2.ipynbPart 2 強化:HistGradientBoosting + 5-fold CV
NLP-part1.ipynbmoai-2025-nlp.ipynbNLP grader 滿分 (45/45)
NLP-part2.ipynbBiLSTM 衝 Kaggle leaderboard
CV-part1.ipynbMOAI-CV.ipynbCVAE 架構 + loss (grader 50/50)
CV-part2.ipynbConv-CVAE + KL warm-up,目標 FID < 30

在 Kaggle 上的執行步驟

1. 打開 kaggle.com → 建立 New Notebook
2. 右側 Sidebar → Add Data → 搜尋對應 dataset:
   - moai-2025-ml-task
   - moai-2025-nlp-task
   - moai-2025-cv-task
3. File → Upload Notebook → 選上面其中一份 .ipynb
4. 右側 Settings → Accelerator → 選 GPU T4 x2 (NLP/CV 必開)
5. Run All → 確認 grader 全綠 → 右上 Save Version → Submit to Competition

在本地驗證(不需 GPU 的部分)

cd /Users/laoiongtek/Desktop/moai/moai2025
jupyter notebook ML-part1.ipynb   # 路徑要改成本地相對路徑

注意:原 notebook 用 /kaggle/input/... 路徑。本地跑時把它改成 ./moai-2025-ml-task/...

提交到 Google Form 的最終檔名

類別要交什麼
NLPNLP-part1.ipynb + NLP-part2.ipynb
CVCV-part1.ipynb + CV-part2.ipynb + CV-model.pth
ML依 Google Form 指示交 ML-part1.ipynb,或同時交 ML-part2.ipynb

通用解題思維模板

每一題都用這個 6 步骤思考。背下來。

┌─────────────┐
│ 1. 讀題     │  → 輸入是什麼?輸出是什麼?評分指標?
├─────────────┤
│ 2. 探索資料 │  → df.head() / df.describe() / 畫幾張圖
├─────────────┤
│ 3. 建立直覺 │  → 哪些特徵「合理」與目標相關?
├─────────────┤
│ 4. 設計方案 │  → 從最簡單的 baseline 開始,逐步加複雜度
├─────────────┤
│ 5. 寫程式   │  → 先讓它跑,再讓它對,再讓它快
├─────────────┤
│ 6. 驗證     │  → 跑 grader / 看 val loss / 視覺檢查
└─────────────┘

關鍵心法

心法意思
不要一開始就寫複雜模型先讓 baseline 能跑、能評分
每一步都要可驗證印出 shape、前 5 行、acc
怪結果先 debug不要急著換模型架構
時間先保底再優化第 1 小時做 baseline,後面再衝分

Problem 1 — ML 路徑難度回歸

1.1 讀題

任務:給每條遊戲路徑(高度序列、地形、氣候),預測 1-5 的難度分。

關鍵限制

部分限制 / 目標
Part 1(50%)只能用 LinearRegression,按子題正確性給分
Part 2(50%)可用任何模型,按 Kaggle 評分

評分指標

  • R2=1SSresSStotR^2 = 1 - \frac{\text{SS}_{\text{res}}}{\text{SS}_{\text{tot}}}
  • 1.0 = 完美預測;0.0 = 跟猜平均值一樣爛;負數 = 比猜平均還爛

1.2 探索資料

打開 train.csv,先看:

train_df.head()
train_df.describe()
train_df['difficulty'].hist(bins=30)

會觀察到

觀察意義
elevation_profilelist of float要自己從序列抽特徵
terrain_type 是字串之後要做編碼
difficulty 多集中在 1.5 - 4.0預測值通常不會太極端

1.3 建立直覺

問自己:什麼樣的路徑會難?

直覺對應特徵
走得越久越累length (= len(elevation_profile))
高低起伏越大越難elevation_rangeelevation_stddev
連續陡坡更難一階差分的標準差、最大上升步幅
累計爬升越多越累total_ascent
高山地形難one-hot terrain_type_Mountain/Alpine
雪地比沙漠難annual_snowtemp_range

反直覺檢查

欄位為什麼可能沒用
popularity這是主觀評分,未必代表難度
elevation_avg平均海拔高,不代表起伏大

1.4 設計方案

Part 1 子題逐一拆解

題目 1.1:difficulty 統計

  • 思考:「count、mean、std、min、25%、50%、75%、max」這 8 個數值,pandas 有沒有一鍵函數?
  • Series.describe()
difficulty_stats = train_df['difficulty'].describe()

題目 2.2:長度特徵

  • elevation_profile 是 list → len() 給點數
  • ⚠️ grader 要求 int 型別(不是 numpy.int64) → .astype(int)
train_df['length'] = train_df['elevation_profile'].apply(len).astype(int)

題目 2.3.1:4 個高度特徵

從 list 計算統計量:

def _arr(p): return np.asarray(p, dtype=float)
train_df['elevation_avg']    = train_df['elevation_profile'].apply(lambda p: _arr(p).mean())
train_df['elevation_range']  = train_df['elevation_profile'].apply(lambda p: _arr(p).max() - _arr(p).min())
train_df['elevation_stddev'] = train_df['elevation_profile'].apply(lambda p: _arr(p).std())
train_df['elevation_change'] = train_df['elevation_profile'].apply(lambda p: p[-1] - p[0])

⚠️ .std() 預設 ddof=0(母體標準差) — pandas 的 .std() 預設 ddof=1。grader 用的是 numpy.std(),所以記得用 np.std

題目 2.4:one-hot encoding

  • 思考:grader 檢查 SHA hash → 每一個欄位的值序列必須完全相同
  • pd.get_dummies(...) 預設輸出是 booluint8,hash 對不上
  • 必須強制 .astype(int)True/False 轉成 1/0
train_df_terrain = pd.get_dummies(train_df, columns=['terrain_type'], prefix='terrain_type')
oh_cols = [c for c in train_df_terrain.columns if c.startswith('terrain_type_')]
train_df_terrain[oh_cols] = train_df_terrain[oh_cols].astype(int)

題目 2.5:相關性矩陣 + 配色

  • 相關係數 r[1,1]r \in [-1, 1],0 表示無關
  • 必須對稱配色:負相關紅、無關白、正相關藍 → RdBu
  • vmin/vmax 必須是 ±1(不是當前 matrix 的 min/max,否則色彩偏移)
correlation_matrix = train_df[CORR_COLS].corr()
colored_correlation = correlation_matrix.style.background_gradient(
    cmap='RdBu', vmin=-1, vmax=1)

1.5 Part 2 — 進階特徵工程

核心策略:線性回歸無法學非線性關係。要榨出更多 R²,必須在特徵工程下功夫,把非線性訊號手動轉成線性訊號

一階差分(坡度)

高度差 np.diff(profile) 是「每段坡度」:

diffs = np.diff(profile)
total_ascent  = diffs[diffs > 0].sum()       # 累計爬升
total_descent = -diffs[diffs < 0].sum()      # 累計下降
slope_std     = diffs.std()                  # 起伏劇烈度
max_step_up   = diffs.max()                  # 最陡上坡

二階差分(曲率)— Part 2 的秘密武器

second = np.diff(profile, n=2)
curvature_std  = second.std()       # 曲率變化(連續陡坡偵測)
curvature_mean = np.abs(second).mean()

比例型特徵

roughness      = (total_ascent + total_descent) / length    # 平均坡度
net_gain_ratio = elevation_change / elevation_range          # 淨升降占比
snow_to_rain   = annual_snow / annual_rain                   # 氣候惡劣指標

1.6 ML 寫程式注意事項

注意事項簡短說明
特徵工程寫成函數compute_features(df) 同時用在 train 和 test
預測值要限制範圍線性回歸可能超出 [1, 5]
避免除零例如 df['x'] / df['y'].replace(0, 1)

1.7 驗證

final_model = train_linear_model(train_full, FEATS)
print(f"訓練 R² = {final_model.metrics['r2']:.4f}")    # 期望 0.85+

如果訓練 R² 很高但 Kaggle 分很低 → 過擬合 → 減特徵或用正則化(Ridge/Lasso)。


Problem 2 — NLP 文本情感分類

2.1 讀題

任務:給一段英文 Twitter 文本,分類成 6 種情緒(悲傷 0、喜悅 1、愛 2、憤怒 3、恐懼 4、驚訝 5)。

重點限制

部分限制 / 目標
Part 1(45 分)必須用 GloVe + PyTorch,通過 grader 子題
Part 2(55 分)用 Kaggle 準確率評分,分數和準確率是平方關係

2.2 探索資料

train_df['label'].value_counts()
# 1 (joy):     ~33%
# 0 (sadness): ~29%
# 3 (anger):   ~14%
# 4 (fear):    ~12%
# 2 (love):    ~8%
# 5 (surprise):~4%

警訊:類別嚴重不平衡 → 永遠猜「喜悅」就有 33% 準確率。但要過 grader 的 30%,必須真學到東西。

類別分佈提醒重點
joy 最多、surprise 最少不能只靠猜最多類拿高分

2.3 建立直覺

為什麼用 GloVe?

原因解釋
單詞可轉成向量語意關係會被編進數字表示裡
神經網路只能吃數字所以文字必須先 vectorize
白名單可取得比賽中不需要額外聯網

為什麼要固定句子長度?

  • 神經網路要求批次內所有樣本 shape 相同
  • 解法:截斷到 MAX_LEN,不夠補零 (padding)

為什麼用 BiLSTM 而不是 MLP(Part 2)?

模型差別
MLP展平後容易失去詞序資訊
BiLSTM能同時看前後文,較適合句子分類

2.4 設計方案

題目 1.1:text_to_vec_v1

  • GloVe 只認小寫 → text.lower().split()
  • 每個詞查向量 → glove_model[w]
def text_to_vec_v1(text):
    return np.array([glove_model[w] for w in text.lower().split()])

題目 1.2 + 1.3:固定長度 + 處理未知詞

陷阱glove_model["asdvnalsdn"] 會 raise KeyError。 解法:先 if w in glove_model 過濾。

MAX_LEN, EMB_DIM = 30, 50
def text_to_vec_v2(text):
    words = text.lower().split()[:MAX_LEN]
    vec = np.zeros((MAX_LEN, EMB_DIM), dtype=np.float32)  # 預先補 0
    for i, w in enumerate(words):
        if w in glove_model:
            vec[i] = glove_model[w]
    return vec

題目 2:Dataset / DataLoader

  • TensorDataset(X, y) 把特徵和標籤綁在一起
  • random_split 做 8:2 分割(設 seed 以可重現
  • DataLoader 訓練 shuffle=True,驗證 shuffle=False

題目 3:模型架構

最簡單能過的版本:

class SentenceClassifier(nn.Module):
    def __init__(self):
        super().__init__()
        self.net = nn.Sequential(
            nn.Flatten(),                       # (B, 30, 50) → (B, 1500)
            nn.Linear(30*50, 128), nn.ReLU(),
            nn.Dropout(0.3),                    # 防過擬合
            nn.Linear(128, 128),  nn.ReLU(),
            nn.Linear(128, 6),                  # 6 類 logits
        )
    def forward(self, x): return self.net(x)

題目 4:訓練 — 隱藏陷阱!

⚠️ 最大的坑:原 notebook 把訓練迴圈標為「請不要修改」,但裡面漏了 optimizer.zero_grad()

不修補的話:

  • 梯度會在 batch 之間累積
  • Adam 用累積後的巨大梯度更新 → 訓練爆炸或不收斂

最優雅解法:sub-class Adam,在 step() 後自動 zero_grad

class Adam(torch.optim.Adam):
    def step(self, *args, **kwargs):
        out = super().step(*args, **kwargs)
        super().zero_grad(set_to_none=True)
        return out

optimizer = Adam(model.parameters(), lr=1e-3, weight_decay=1e-5)

isinstance(optimizer, torch.optim.Adam) 仍為 True → grader 還是過。

2.5 Part 2 — BiLSTM 衝高分

為什麼換架構?

模型理論驗證準確率為什麼
MLP (Part 1)35-45%丟掉序列訊息
BiLSTM88-92%雙向上下文捕捉
Transformer92-95%注意力機制更強

設計重點

  1. 完整 GloVe vocab:用 nn.Embedding.from_pretrained 一次塞進 ~40 萬詞
  2. 可微調freeze=False — 讓 GloVe 在訓練中自己 fine-tune
  3. Masked mean pooling:padding 不該影響 hidden state
    mask = (x != 0).unsqueeze(-1).float()
    o = (lstm_out * mask).sum(1) / mask.sum(1).clamp(min=1)
    
  4. Cosine LR + grad clip:穩定訓練、避免梯度爆炸
  5. 保留 best checkpoint:每 epoch 看 val acc,存最好那次的權重

2.6 NLP 寫程式注意事項

注意事項簡短說明
predictions 要是 1D Tensor通常用 .argmax(dim=1) 即可
predictions 要是 Tensor提交 cell 會自動呼叫 .cpu().numpy()
Tokenize 建議用 regex.split() 更能去掉標點

Problem 3 — CV 條件變分自編碼器 CVAE

3.1 讀題

任務:用 FashionMNIST,訓練一個 CVAE(條件變分自編碼器),給定類別標籤 → 生成對應類別的衣物圖片。

評分

部分評分方式
Part 1(50 分)4 個函數的 unit test:reparameterize / encode / decode / loss
Part 2(50 分)FID 分數
FID 規則分數影響
任一類別像素 std ≤ 300 分
FID-RMS ≤ 30滿分
30 < FID-RMS < 50線性插值
FID-RMS ≥ 500 分

3.2 先補背景知識:什麼是 VAE / CVAE?

自編碼器(AE)

x ──[Encoder]──> z(壓縮表徵)──[Decoder]──> x'

目標:x' ≈ x。但 z 空間「散亂」,無法生成新樣本。

變分自編碼器(VAE)

強制 z 服從標準正態分佈

x ──[Encoder]──> μ, σ ──> z ~ N(μ, σ²) ──[Decoder]──> x'

好處:訓練完後直接 z = randn() 就能生成新圖。

Loss

L=xx2重構+βDKL(N(μ,σ2)N(0,1))讓 z 接近標準正態L = \underbrace{\|x - x'\|^2}_{\text{重構}} + \beta \cdot \underbrace{D_{KL}(N(\mu,\sigma^2) \| N(0,1))}_{\text{讓 z 接近標準正態}}

KL 散度展開(高斯分佈間):

DKL=12i(1+lnσi2μi2σi2)D_{KL} = -\frac{1}{2} \sum_i \left(1 + \ln \sigma_i^2 - \mu_i^2 - \sigma_i^2 \right)

條件式(C)VAE

把 label y 餵給 encoder + decoder:

(x, y) ──[Encoder]──> μ, σ ──> z ──> (z, y) ──[Decoder]──> x'

這樣 decode(randn, y=3) 就會生成「label=3」那類的圖。

3.3 重參數化技巧(Reparameterization Trick)

為什麼需要?

問題說明
想從 N(μ, σ²) 採樣但採樣本身不可導
結果梯度無法直接穿過這一步

解法

把隨機性「移」到一個獨立變數 ε ~ N(0, 1): z=μ+σϵz = \mu + \sigma \cdot \epsilon 這樣 z 是 μ、σ 的確定函數 + 一個常數隨機量 → 可導!

短例子:

z = mu + std * eps

程式碼

def reparameterize(self, mu, logvar):
    std = torch.exp(0.5 * logvar)        # logvar = ln(σ²) → σ = exp(logvar/2)
    eps = torch.randn_like(std)           # ⚠️ 必須用 randn_like
    return mu + eps * std

⚠️ 大坑:題目強制用 torch.randn_like(),因為 grader 的 part1_answer.pt 是用 torch.manual_seed(42) + randn_like 預先計算的。換成 torch.randn() 或 numpy 就永遠不會通過

3.4 Encoder 設計

按題目規格:

x flat (784) ⨁ y_onehot (10) → fc(794→400) → ELU → mu (400→20), logvar (400→20)
self.enc_fc      = nn.Linear(28*28 + 10, 400)
self.enc_act     = nn.ELU()
self.enc_mu      = nn.Linear(400, 20)
self.enc_logvar  = nn.Linear(400, 20)

def encode_param(self, x, y):
    x_flat = x.view(x.size(0), -1)
    y_oh   = F.one_hot(y, num_classes=10).float()
    h = self.enc_act(self.enc_fc(torch.cat([x_flat, y_oh], dim=1)))
    return self.enc_mu(h), self.enc_logvar(h)

3.5 Decoder 設計

z (20) ⨁ y_onehot (10) → fc(30→400) → ELU → fc(400→784) → Sigmoid → reshape 1×28×28

⚠️ Sigmoid 必要 — 像素值要在 [0, 1]。

3.6 Loss 函數

⚠️ per-sample loss:訓練端寫死 loss.mean().backward(),所以你的 loss 必須回 shape (B,)不能 自己 mean。

def compute_vae_loss(model, x, y, beta=1):
    mu, logvar = model.encode_param(x, y)
    z          = model.reparameterize(mu, logvar)
    x_recon    = model.decode(z, y)
    recon = F.mse_loss(x_recon, x, reduction='none').view(x.size(0), -1).sum(dim=1)
    kld   = -0.5 * (1 + logvar - mu.pow(2) - logvar.exp()).sum(dim=1)
    return recon + beta * kld   # shape (B,)

3.7 Part 2 — Conv-CVAE 衝 FID

為什麼 FC 版不夠?

  • FC 把圖片展平 → 失去空間局部結構(一個 pixel 跟它隔壁的關係)
  • Conv 用 3×3 kernel 抓邊緣、紋理 → 重構更銳利

改進清單

  1. Conv encoder:3 層 stride=2 → (28→14→7→4)
  2. ConvTranspose decoder:對稱反過來
  3. BatchNorm:穩定訓練、加速收斂
  4. LeakyReLU:避免死神經元
  5. Label tile to channels:把 y 展開成 10 個 channel 拼到圖上 → encoder 在每個空間位置都看得到 label

Mode Collapse 預防:KL Warm-up

beta = min(1.0, (epoch + 1) / 10) * 0.5
  • 前 10 epoch 慢慢上 KL 權重 → 給 decoder 時間先學會重構
  • 最終 β=0.5(不滿)→ 保留圖片銳利度,避免「全部生成同一張平均臉」

像素 std > 30 怎麼確保?

  • 訓練好的 CVAE 不會輸出純色 → 自然 std > 30
  • 如果還是不夠 → 提升 latent_size(20→64)增加多樣性

3.8 訓練超參

設定為什麼
batch_size128T4 GPU 夠用,statistical 估計穩定
lr1e-3Adam 預設經驗值
epochs40FashionMNIST 收斂相對快
latent_size64Part 2 用 64,比 Part 1 的 20 更豐富
schedulerCosineLR後期自動降 lr 細調

3.9 提交流程

# 1. 訓練完
save_model('./vae.pth', cvae, opt)

# 2. 重新讀回(驗證能 load)
cvae.load_state_dict(load_model('vae.pth')[0])

# 3. 為每類生成 100 張圖(總共 1000 張)
cvae.make_dataset(n_samples_per_class=100)

# 4. 跑 grader
from grader import part2
print(f"Score: {part2():.2f}/50.00")

# 5. 把 vae.pth 改名 CV-model.pth 提交

賽前 Checklist

競賽前一天

  • 確認帳號能登入 Bohrium 平台
  • 把 GloVe 向量、Inception 模型、CVAE template 寫過一遍
  • import torch; torch.cuda.is_available() 確認 GPU 可用
  • 知道 grader 函數叫什麼(task_grader.q1_1_check 等)

進場第一小時

  • 讀完三題的 markdown,找出所有「請不要修改」cell
  • 跑 baseline 確認資料載入成功
  • 先過所有 Part 1 子題拿基本分

中段(2-4 小時)

  • ML:feature engineering,目標 R² > 0.85
  • NLP:BiLSTM 訓 12 epoch,目標 acc > 88%
  • CV:Conv-CVAE 訓 40 epoch,目標 FID-RMS < 30

最後一小時

  • 全部 Run All 確認沒紅字
  • 檢查 submission.csv 格式(有沒有多一欄、有沒有 NaN)
  • CV 確認 imgs/generated/0..9/ 各 100 張圖
  • 存檔! 別在最後 5 分鐘才存

提交檢查

  • 檔名正確(NLP-part1.ipynbCV-model.pth
  • 上傳到 Google Form 看到綠色勾勾
  • 截圖 Kaggle leaderboard 分數

最後叮嚀

  1. 讀題比寫程式重要 — 大坑都在「請不要修改」、「請使用 X 函數」、「形狀為 (B, …)」這些細節裡
  2. 每寫一段都跑一次 — 不要寫 100 行才執行
  3. Print everythingprint(x.shape) 是你最好的朋友
  4. 看不懂報錯先 google — Stack Overflow 通常 5 秒內有答案
  5. 時間到一半還沒過 grader → 先放棄優化,保底通過
  6. 記得吃飯喝水 — 6 小時很長
最後提醒重點
先讀題很多分數藏在格式與限制裡
邊寫邊測不要累積太多未驗證程式
多印資訊shape、loss、sample output 都很重要
先保底grader 未過時不要急著追高分

祝你在 MOAI 2025 拿到金牌 🥇


本指南配合 moai2025/ 內 6 份提交版 notebook 服用:
ML-part1.ipynb · ML-part2.ipynb · NLP-part1.ipynb · NLP-part2.ipynb · CV-part1.ipynb · CV-part2.ipynb


🎬 配套教學影片

讀完文件後,建議搭配以下影片加深理解:

影片內容連結
MOAI 2025 學習指南教學影片六步解題框架、ML/NLP/CV 核心思路、常見陷阱點此觀看
Built with LogoFlowershow