iT邦幫忙

2025 iThome 鐵人賽

DAY 29
0
AI & Data

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

Day 29 – 雙模型比較與真隨機分流

  • 分享至 

  • xImage
  •  

🎯 本日目標

今天,我們將讓推薦系統的 A/B 測試更貼近實際產品:
除了雙模型比較與隨機分流外,
還要讓 CTR 不再永遠是 100%,能真實反映使用者行為。

透過本日內容,你將學會:

  • 使用 random.choice() 讓分流機制成為真正隨機
  • 新增「我都不喜歡」按鈕記錄負樣本(clicked=False
  • 報表支援 即時重新整理,即刻更新統計資料

🧱 Step 1. 真隨機分流邏輯

在 FastAPI 的 /recommend_ab 分流函式中,
我們改用 random.choice() 讓每次推薦隨機分派模型:

import random

def choose_model_by_time():
    """改為真正隨機分流,模擬真實 A/B Test"""
    return random.choice(["AnimeRecsysModel", "AnimeRecsysTFIDF"])

✅ 這樣每位使用者、每次推薦都可能落在不同模型,
讓 A/B 測試分佈更自然、更具統計代表性。


⚖️ Step 2. 雙模型比較頁 — /src/api/pages/ab_multiple.py

這個頁面同時顯示兩個模型的推薦清單,
並新增「😐 我都不喜歡」按鈕,用來記錄使用者不感興趣的情況。

# ⚖️ 雙模型推薦比較頁(/src/api/pages/ab_multiple.py)

import os
import pandas as pd
import requests
import streamlit as st
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="⚖️ 雙模型推薦比較", layout="wide")
st.title("⚖️ 雙模型推薦比較頁")

st.markdown("""
本頁同時顯示兩個模型的推薦結果,  
使用者可對比推薦清單並提供正負反饋,  
讓 CTR 更真實反映實際互動情況。
""")

nickname = st.text_input("請輸入你的暱稱 👤", placeholder="例如:Josh、Mina、Ken")
if not nickname:
    st.info("請輸入暱稱後再繼續。")

@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()
selected_anime = st.multiselect("選擇你喜歡的動畫(最多5部) 🎥", anime_list, max_selections=5)

def get_recommendations(model_name, user_id, anime_titles):
    payload = {"user_id": user_id, "anime_titles": anime_titles}
    params = {"model_name": model_name}
    res = requests.post(f"{FASTAPI_URL}/recommend", json=payload, params=params)
    if res.status_code == 200:
        return res.json()
    else:
        st.error(f"❌ {model_name} 取得推薦失敗:{res.text}")
        return None

def log_click_event(user_id, model_name, model_version, title, page, clicked=True):
    event = {
        "user_id": user_id,
        "model_name": model_name,
        "model_version": model_version,
        "recommended_title": title if title else None,
        "clicked": clicked,
        "timestamp": datetime.utcnow().isoformat(),
        "page": page
    }
    try:
        r = requests.post(f"{FASTAPI_URL}/log-ab-event", json=event)
        if r.status_code != 200:
            st.warning(f"⚠️ 記錄失敗:{r.text}")
    except requests.exceptions.RequestException:
        st.warning("⚠️ 無法連線至 FastAPI")

if st.button("🚀 取得雙模型推薦結果"):
    if not nickname:
        st.warning("請先輸入暱稱。")
    elif not selected_anime:
        st.warning("請至少選擇一部動畫。")
    else:
        model_a, model_b = "AnimeRecsysModel", "AnimeRecsysTFIDF"
        res_a = get_recommendations(model_a, nickname, selected_anime)
        res_b = get_recommendations(model_b, nickname, selected_anime)
        if res_a and res_b:
            st.session_state["rec_a"], st.session_state["rec_b"] = res_a["recommendations"], res_b["recommendations"]
            st.session_state["model_a"], st.session_state["model_b"] = model_a, model_b
            st.success("✅ 已取得兩模型推薦結果!")

if "rec_a" in st.session_state and "rec_b" in st.session_state:
    col1, col2 = st.columns(2)

    with col1:
        st.subheader(f"🧠 模型 A:{st.session_state['model_a']}")
        for i, title in enumerate(st.session_state["rec_a"][:10], 1):
            if st.button(f"A{i}. {title}", key=f"a_{i}"):
                log_click_event(nickname, st.session_state["model_a"], 1, title, page="ab_multiple", clicked=True)
        if st.button("😐 我都不喜歡模型 A 的推薦", key="dislike_a"):
            log_click_event(nickname, st.session_state["model_a"], 1, None, page="ab_multiple", clicked=False)
            st.info("已記錄:使用者對模型 A 的推薦不感興趣。")

    with col2:
        st.subheader(f"🎯 模型 B:{st.session_state['model_b']}")
        for i, title in enumerate(st.session_state["rec_b"][:10], 1):
            if st.button(f"B{i}. {title}", key=f"b_{i}"):
                log_click_event(nickname, st.session_state["model_b"], 1, title, page="ab_multiple", clicked=True)
        if st.button("😐 我都不喜歡模型 B 的推薦", key="dislike_b"):
            log_click_event(nickname, st.session_state["model_b"], 1, None, page="ab_multiple", clicked=False)
            st.info("已記錄:使用者對模型 B 的推薦不感興趣。")

https://ithelp.ithome.com.tw/upload/images/20251012/20178626wOzLs7H1it.png


🎲 Step 3. 隨機分流頁 — /src/api/pages/ab_random.py

新增「我都不喜歡」按鈕,並讓後端每次隨機選擇模型。

# 🎲 隨機分流推薦頁(/src/api/pages/ab_random.py)

import os
import pandas as pd
import requests
import streamlit as st
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="🎲 隨機分流推薦", layout="wide")
st.title("🎲 A/B Test 隨機分流頁")

st.markdown("""
本頁使用 FastAPI `/recommend_ab` 隨機分流至不同模型,  
並新增「我都不喜歡」按鈕記錄負樣本,使 CTR 統計更真實。
""")

nickname = st.text_input("請輸入你的暱稱 👤", placeholder="例如:Josh、Mina、Ken")
if not nickname:
    st.info("請輸入暱稱後再繼續。")

@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()
selected_anime = st.multiselect("選擇你喜歡的動畫(最多5部) 🎥", anime_list, max_selections=5)

def get_random_recommend(user_id, anime_titles):
    payload = {"user_id": user_id, "anime_titles": anime_titles}
    res = requests.post(f"{FASTAPI_URL}/recommend_ab", json=payload)
    if res.status_code == 200:
        return res.json()
    else:
        st.error(f"❌ recommend_ab 取得推薦失敗:{res.text}")
        return None

def log_click_event(user_id, model_name, model_version, title, page, clicked=True):
    event = {
        "user_id": user_id,
        "model_name": model_name,
        "model_version": model_version,
        "recommended_title": title if title else None,
        "clicked": clicked,
        "timestamp": datetime.utcnow().isoformat(),
        "page": page
    }
    try:
        requests.post(f"{FASTAPI_URL}/log-ab-event", json=event)
    except requests.exceptions.RequestException:
        st.warning("⚠️ 無法記錄事件")

if st.button("🚀 取得隨機推薦結果"):
    if not nickname:
        st.warning("請先輸入暱稱。")
    elif not selected_anime:
        st.warning("請至少選擇一部動畫。")
    else:
        res = get_random_recommend(nickname, selected_anime)
        if res:
            st.session_state["random_recs"] = res["recommendations"]
            st.session_state["model_name"] = res["model_name"]
            st.success(f"✅ 本次使用模型:{st.session_state['model_name']}")

if "random_recs" in st.session_state:
    model_name = st.session_state["model_name"]
    recs = st.session_state["random_recs"]

    st.markdown("---")
    st.subheader(f"✨ 模型:{model_name}")
    for i, title in enumerate(recs[:10], 1):
        if st.button(f"{i}. {title}", key=f"r_{i}"):
            log_click_event(nickname, model_name, 1, title, page="ab_random", clicked=True)
    if st.button("😐 我都不喜歡以上推薦"):
        log_click_event(nickname, model_name, 1, None, page="ab_random", clicked=False)
        st.info("已記錄:使用者對本輪推薦沒有興趣。")

https://ithelp.ithome.com.tw/upload/images/20251012/20178626V8ItYT4BUt.png


📊 Step 4. 報表頁支援即時重新整理

使用 st.rerun() 清除快取並重新讀取最新紀錄,
讓報表更新更即時。

st.sidebar.markdown("### 🔄 重新整理報表")
if st.sidebar.button("重新載入資料"):
    st.cache_data.clear()
    st.rerun()

@st.cache_data(ttl=5.0)
def load_logs():
    if not os.path.exists(LOG_PATH):
        st.warning("⚠️ 找不到 ab_events.csv")
        return pd.DataFrame()
    df = pd.read_csv(LOG_PATH, on_bad_lines="skip")
    df["timestamp"] = pd.to_datetime(df["timestamp"], errors="coerce")
    return df

✅ 使用者也可以隨時點擊「重新載入」按鈕,
即時看到最新互動資料與 CTR 變化。
https://ithelp.ithome.com.tw/upload/images/20251012/20178626dWb5BpI0C8.png

https://ithelp.ithome.com.tw/upload/images/20251012/20178626pOBjGRPjAp.png


🧠 延伸思考:更多正負樣本設計

方法 說明
👍 / 👎 按鈕 在推薦結果旁加上「喜歡 / 不喜歡」按鈕,記錄二元樣本。
⭐ 評分機制 讓使用者給 1–5 分,分析模型平均滿意度。
🕒 停留時間 若整合前端事件(瀏覽時間或滑動距離),可作為隱性正樣本。
💬 文字回饋 收集使用者留言理由,可進行語意分析強化模型。

✅ 今日重點回顧

功能 成果
🔁 真隨機分流 使用 random.choice(),每次推薦獨立隨機分派
⚖️ 雙模型比較 同頁對比推薦結果並記錄正負樣本
🎲 分流推薦頁 加入「我都不喜歡」記錄 clicked=False
📊 報表頁 可即時重新整理顯示最新 CTR

🎉 Day 29 完成!
你現在的推薦系統不僅能做 A/B 測試,
還能同時蒐集正、負樣本數據,
CTR 分析將真實反映使用者對不同模型的接受度。

明天(Day 30),我們將總結整個 MLflow + FastAPI + Streamlit 流程,
完整回顧如何從開發到部署打造企業級模型應用。


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

尚未有邦友留言

立即登入留言