iT邦幫忙

2025 iThome 鐵人賽

DAY 27
0
AI & Data

從0開始的MLFLOW應用搭建系列 第 27

Day 27 – 建立 A/B Test 資料收集機制

  • 分享至 

  • xImage
  •  

🎯 本日目標

今天,我們要讓推薦系統不只「提供推薦」,
而是開始能蒐集使用者互動資料,為後續的 A/B Test 分析 做準備。

當使用者在 Streamlit 介面中點擊推薦動畫時,
我們會將事件(包含暱稱、模型名稱、點擊動畫、時間)
傳送到 FastAPI 的 /log-ab-event 端點,並記錄成 log。


🧩 今日架構重點

[使用者點擊推薦動畫]
        ↓
Streamlit → FastAPI /log-ab-event
        ↓
紀錄 CSV (ab_events.csv)

⚙️ Step 1. FastAPI — 行為紀錄端點(已完成)

你的 FastAPI 已內建 /log-ab-event

@app.post("/log-ab-event")
def log_ab_event(event: ABEvent):
    LOG_DIR = "/usr/mlflow/workspace/logs"
    os.makedirs(LOG_DIR, exist_ok=True)
    log_path = os.path.join(LOG_DIR, "ab_events.csv")

    file_exists = os.path.isfile(log_path)
    with open(log_path, "a", newline="", encoding="utf-8") as f:
        writer = csv.writer(f)
        if not file_exists:
            writer.writerow(["timestamp", "user_id", "model_name", "model_version", "recommended_title", "clicked"])
        writer.writerow([
            event.timestamp.isoformat(),
            event.user_id,
            event.model_name,
            event.model_version,
            event.recommended_title,
            event.clicked
        ])
    return {"message": "Event logged successfully ✅", "event": event.dict()}

✅ 每次呼叫 /log-ab-event,都會把事件寫入
/usr/mlflow/workspace/logs/ab_events.csv


🧠 Step 2. Streamlit — 加入點擊紀錄功能

今天我們要優化 Streamlit 程式,
將「取得推薦結果」與「點擊事件紀錄」分開成兩段邏輯。


🔧 /src/api/main_streamlit.py

import os
import pandas as pd
import streamlit as st
import requests
from datetime import datetime

FASTAPI_URL = os.getenv("FASTAPI_URL", "http://localhost:8000")
ANIME_CSV_PATH = "/src/api/notebooks/data/anime_clean.csv"

st.set_page_config(page_title="🎬 Anime Recommender", layout="wide")
st.title("🎬 Anime Recommendation System")

st.markdown("""
這是一個基於 **MLflow + FastAPI** 的推薦系統展示 🎯  
請輸入暱稱並選擇你喜歡的動畫,我們會為你推薦相似作品!
""")

# --- Step 1. 使用者暱稱 ---
nickname = st.text_input("請輸入你的暱稱 👤", placeholder="例如:Josh、小智、NekoFan")
if not nickname:
    st.info("請輸入暱稱後再繼續。")

# --- Step 2. 載入動畫清單 ---
@st.cache_data
def load_anime_list():
    if not os.path.exists(ANIME_CSV_PATH):
        st.error(f"❌ 找不到資料檔案:{ANIME_CSV_PATH}")
        return []
    df = pd.read_csv(ANIME_CSV_PATH)
    return df["name"].dropna().unique().tolist()

anime_list = load_anime_list()

# --- Step 3. 使用者選擇動畫 ---
selected_anime = st.multiselect(
    "選擇你喜歡的動畫(最多5部) 🎥",
    anime_list,
    max_selections=5,
    placeholder="例如:Naruto、Bleach、Attack on Titan..."
)

# --- Step 4. 定義輔助函式 ---
def get_recommendations(user_id: str, anime_titles: list[str]):
    """呼叫 FastAPI /recommend API"""
    payload = {"user_id": user_id, "anime_titles": anime_titles}
    params = {"model_name": "AnimeRecsysModel"}
    res = requests.post(f"{FASTAPI_URL}/recommend", json=payload, params=params)
    if res.status_code == 200:
        return res.json()
    else:
        st.error(f"❌ 無法取得推薦結果:{res.text}")
        return None


def log_click_event(user_id: str, model_name: str, model_version: int, title: str):
    """呼叫 /log-ab-event 紀錄點擊事件"""
    event = {
        "user_id": user_id,
        "model_name": model_name,
        "model_version": model_version,
        "recommended_title": title,
        "clicked": True,
        "timestamp": datetime.utcnow().isoformat()
    }
    try:
        r = requests.post(f"{FASTAPI_URL}/log-ab-event", json=event)
        if r.status_code == 200:
            st.toast(f"✅ 已紀錄點擊:{title}")
        else:
            st.warning(f"⚠️ 紀錄失敗:{r.text}")
    except requests.exceptions.RequestException:
        st.warning("⚠️ 無法連線至 FastAPI。")


# --- Step 5. 取得推薦結果 ---
if st.button("🚀 取得推薦結果"):
    if not nickname:
        st.warning("請先輸入暱稱。")
    elif not selected_anime:
        st.warning("請至少選擇一部動畫。")
    else:
        data = get_recommendations(nickname, selected_anime)
        if data:
            st.session_state["recommendations"] = data.get("recommendations", [])
            st.session_state["model_name"] = data.get("model_name", "AnimeRecsysModel")
            st.session_state["model_version"] = 1
            st.success("✅ 推薦結果已更新!")

# --- Step 6. 顯示推薦結果並可記錄點擊 ---
if "recommendations" in st.session_state:
    recs = st.session_state["recommendations"]
    model_name = st.session_state.get("model_name", "AnimeRecsysModel")
    model_version = st.session_state.get("model_version", 1)

    if recs:
        st.markdown("---")
        st.subheader(f"✨ 為 **{nickname}** 推薦的動畫:")
        for i, title in enumerate(recs[:10], 1):
            if st.button(f"{i}. {title}", key=f"rec_{i}"):
                log_click_event(nickname, model_name, model_version, title)
    else:
        st.warning("⚠️ 沒有可顯示的推薦結果。")

📂 Step 3. 事件紀錄檔結構

每當使用者點擊動畫時,FastAPI 都會在 /usr/mlflow/workspace/logs/ab_events.csv 中追加記錄:

https://ithelp.ithome.com.tw/upload/images/20251011/2017862605fQ3wZzFf.png


🧪 Step 4. 測試流程

1️⃣ 啟動整個系統

docker-compose up --build

2️⃣ 開啟 Streamlit
👉 http://localhost:8501

3️⃣ 操作流程:

  • 輸入暱稱(例如 Josh)
  • 選擇喜歡的動畫
  • 點擊「🚀 取得推薦結果」
  • 點擊任一推薦動畫

4️⃣ 驗證結果
進入 FastAPI 容器檢查紀錄:

docker exec -it fastapi bash
cat /usr/mlflow/workspace/logs/ab_events.csv

你會看到點擊事件已被成功寫入 🎉


✅ 今日重點回顧

項目 說明
🧠 新增 /log-ab-event API FastAPI 紀錄使用者互動行為
🧩 Streamlit 整合 點擊推薦動畫即上報事件
📂 Log 儲存 ab_events.csv 可做後續分析
🔗 下一步 Day 28 會讀取這份紀錄並分析點擊率 (CTR)、模型效果

🎉 Day 27 完成!
我們正式讓推薦系統具備「使用者行為回饋」功能。
明天(Day 28),你將學會如何將這些紀錄轉換為可視化報表,
分析不同模型在真實互動下的成效差異。


上一篇
Day 26 – 用 Streamlit 打造動畫推薦系統介面
下一篇
Day 28 – 建立 AB Test 結果分析頁
系列文
從0開始的MLFLOW應用搭建29
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言