MOAI 2025 學習指南
MOAI 2025 學習指南
從讀題 → 思考 → 分析 → 設計 → 寫程式 → 驗證,每一題都帶你走一遍。
看完這份文件後,你不只能拿到分,更能獨立解類似的題。
先看這張速讀表
| 你現在的目標 | 建議先看 |
|---|---|
| 想知道整份資料夾怎樣用 | 第 0 節 |
| 想先背通用解題流程 | 「通用解題思維模板」 |
| 想做 ML 題 | Problem 1 |
| 想做 NLP 題 | Problem 2 |
| 想做 CV 題 | Problem 3 |
| 想賽前最後複習 | 「賽前 Checklist」 |
目錄
- 先看這裡:六個檔案怎麼用
- 通用解題思維模板
- Problem 1 — ML 路徑難度回歸
- Problem 2 — NLP 文本情感分類
- Problem 3 — CV 條件變分自編碼器 CVAE
- 賽前 Checklist
0. 六個檔案怎麼用
我在 moai2025/ 資料夾為你生成了 6 份提交版 notebook:
| 檔案 | 對應原題 | 用途 |
|---|---|---|
ML-part1.ipynb | moai-2025-ml.ipynb | 主 ML 解答(grader 全綠 + 線性回歸 submission) |
ML-part2.ipynb | — | Part 2 強化:HistGradientBoosting + 5-fold CV |
NLP-part1.ipynb | moai-2025-nlp.ipynb | NLP grader 滿分 (45/45) |
NLP-part2.ipynb | — | BiLSTM 衝 Kaggle leaderboard |
CV-part1.ipynb | MOAI-CV.ipynb | CVAE 架構 + loss (grader 50/50) |
CV-part2.ipynb | — | Conv-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 的最終檔名
| 類別 | 要交什麼 |
|---|---|
| NLP | NLP-part1.ipynb + NLP-part2.ipynb |
| CV | CV-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 行、R² 或 acc |
| 怪結果先 debug | 不要急著換模型架構 |
| 時間先保底再優化 | 第 1 小時做 baseline,後面再衝分 |
Problem 1 — ML 路徑難度回歸
1.1 讀題
任務:給每條遊戲路徑(高度序列、地形、氣候),預測 1-5 的難度分。
關鍵限制:
| 部分 | 限制 / 目標 |
|---|---|
| Part 1(50%) | 只能用 LinearRegression,按子題正確性給分 |
| Part 2(50%) | 可用任何模型,按 Kaggle R² 評分 |
評分指標:R²
- 1.0 = 完美預測;0.0 = 跟猜平均值一樣爛;負數 = 比猜平均還爛
1.2 探索資料
打開 train.csv,先看:
train_df.head()
train_df.describe()
train_df['difficulty'].hist(bins=30)
會觀察到:
| 觀察 | 意義 |
|---|---|
elevation_profile 是 list of float | 要自己從序列抽特徵 |
terrain_type 是字串 | 之後要做編碼 |
difficulty 多集中在 1.5 - 4.0 | 預測值通常不會太極端 |
1.3 建立直覺
問自己:什麼樣的路徑會難?
| 直覺 | 對應特徵 |
|---|---|
| 走得越久越累 | length (= len(elevation_profile)) |
| 高低起伏越大越難 | elevation_range、elevation_stddev |
| 連續陡坡更難 | 一階差分的標準差、最大上升步幅 |
| 累計爬升越多越累 | total_ascent |
| 高山地形難 | one-hot terrain_type_Mountain/Alpine |
| 雪地比沙漠難 | annual_snow、temp_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(...)預設輸出是bool或uint8,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:相關性矩陣 + 配色
- 相關係數 ,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% | 丟掉序列訊息 |
| BiLSTM | 88-92% | 雙向上下文捕捉 |
| Transformer | 92-95% | 注意力機制更強 |
設計重點
- 完整 GloVe vocab:用
nn.Embedding.from_pretrained一次塞進 ~40 萬詞 - 可微調:
freeze=False— 讓 GloVe 在訓練中自己 fine-tune - Masked mean pooling:padding 不該影響 hidden state
mask = (x != 0).unsqueeze(-1).float() o = (lstm_out * mask).sum(1) / mask.sum(1).clamp(min=1) - Cosine LR + grad clip:穩定訓練、避免梯度爆炸
- 保留 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 ≤ 30 | 0 分 |
| FID-RMS ≤ 30 | 滿分 |
30 < FID-RMS < 50 | 線性插值 |
| FID-RMS ≥ 50 | 0 分 |
3.2 先補背景知識:什麼是 VAE / CVAE?
自編碼器(AE)
x ──[Encoder]──> z(壓縮表徵)──[Decoder]──> x'
目標:x' ≈ x。但 z 空間「散亂」,無法生成新樣本。
變分自編碼器(VAE)
強制 z 服從標準正態分佈:
x ──[Encoder]──> μ, σ ──> z ~ N(μ, σ²) ──[Decoder]──> x'
好處:訓練完後直接 z = randn() 就能生成新圖。
Loss:
KL 散度展開(高斯分佈間):
條件式(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 + 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 抓邊緣、紋理 → 重構更銳利
改進清單
- Conv encoder:3 層 stride=2 → (28→14→7→4)
- ConvTranspose decoder:對稱反過來
- BatchNorm:穩定訓練、加速收斂
- LeakyReLU:避免死神經元
- 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_size | 128 | T4 GPU 夠用,statistical 估計穩定 |
lr | 1e-3 | Adam 預設經驗值 |
epochs | 40 | FashionMNIST 收斂相對快 |
latent_size | 64 | Part 2 用 64,比 Part 1 的 20 更豐富 |
scheduler | CosineLR | 後期自動降 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.ipynb、CV-model.pth) - 上傳到 Google Form 看到綠色勾勾
- 截圖 Kaggle leaderboard 分數
最後叮嚀
- 讀題比寫程式重要 — 大坑都在「請不要修改」、「請使用 X 函數」、「形狀為 (B, …)」這些細節裡
- 每寫一段都跑一次 — 不要寫 100 行才執行
- Print everything —
print(x.shape)是你最好的朋友 - 看不懂報錯先 google — Stack Overflow 通常 5 秒內有答案
- 時間到一半還沒過 grader → 先放棄優化,保底通過
- 記得吃飯喝水 — 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 核心思路、常見陷阱 | 點此觀看 |