trainmodel_ver2 / app.py
justinahsu's picture
Update app.py
5ef472a verified
Raw
History Blame Contribute Delete
9.26 kB
# -*- coding: utf-8 -*-
"""
Hugging Face Spaces / Gradio app
步驟導引:資料 → 切分/訓練 → 評估(Accuracy+混淆矩陣) → 測試介面
資料欄位需為: text, label (label 為 0/1)
"""
import io
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import re
import gradio as gr
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.feature_extraction.text import CountVectorizer, TfidfTransformer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix
# -------------------------
# 內建示範資料(30 筆)
# -------------------------
SAMPLE_DATA = [
("這家餐廳超好吃,我下次還要來!", 1),
("服務人員很親切,體驗很棒", 1),
("環境乾淨、氣氛舒服,推!", 1),
("口味不錯,份量也剛好", 1),
("真的超難吃,後悔來了", 0),
("太鹹又冷掉,失望", 0),
("等很久,服務態度也不好", 0),
("分量很少,完全不值這個價錢", 0),
("便宜又好吃,CP值很高", 1),
("味道一般般,下次可能不會再來", 0),
("甜點驚艷!好吃到想哭", 1),
("餐點很油膩,吃完不舒服", 0),
("飲料清爽解膩,搭配主餐剛好", 1),
("今天的餐點都冷掉了…", 0),
("出餐速度快,餐點擺盤也很漂亮", 1),
("座位太擁擠,聊天很吵", 0),
("牛肉很嫩、湯頭很香", 1),
("海鮮新鮮沒有腥味,值得再訪", 1),
("價格偏高,內容卻普通", 0),
("外帶包裝用心,回家吃也很好吃", 1),
("收銀動線很亂,結帳排超久", 0),
("主廚特餐驚喜連連,口味層次豐富", 1),
("烤物有點焦味,失望", 0),
("店內油煙味太重", 0),
("小菜與主餐搭配出色,份量足", 1),
("白飯硬到像沒熟", 0),
("服務貼心會主動加水與關心口味", 1),
("臨時加點又等了二十分鐘", 0),
("週年限定套餐超值,朋友都喜歡", 1),
("衛生紙與餐具不足,需要一直跟店員拿", 0),
]
SAMPLE_DF = pd.DataFrame(SAMPLE_DATA, columns=["text", "label"])
# -------------------------
# 工具函式(仍保留,雖目前 UI 不再使用上傳/貼上)
# -------------------------
def read_csv_file(file_obj) -> pd.DataFrame:
return pd.read_csv(file_obj, encoding_errors="ignore")
def read_csv_text(text_block: str) -> pd.DataFrame:
return pd.read_csv(io.StringIO(text_block))
def check_df(df: pd.DataFrame) -> str:
cols = set(df.columns.str.lower())
need = {"text", "label"}
if not need.issubset(cols):
return "❌ CSV 需要兩個欄位:text, label(0/1)。"
if df.empty:
return "❌ 資料為空。"
return ""
def normalize(text: str) -> str:
t = text
t = re.sub(r"(太|超|非常)?好吃(死了|到爆|極了)?", " POS_GOOD ", t)
t = re.sub(r"(太|超|非常)?難吃(死了|到爆|極了)?", " NEG_BAD ", t)
t = re.sub(r"(不|沒|沒有)", " NOT ", t)
return t
def build_pipeline() -> Pipeline:
"""字 n-gram + TF-IDF + Logistic Regression"""
return Pipeline([
("vect", CountVectorizer(preprocessor=normalize, analyzer="char", ngram_range=(2, 5))),
("tfidf", TfidfTransformer()),
("clf", LogisticRegression(max_iter=200, class_weight="balanced", C=0.8, solver="liblinear")),
])
def plot_confusion_matrix(y_true, y_pred, labels=("負面(0)", "正面(1)")):
cm = confusion_matrix(y_true, y_pred)
fig, ax = plt.subplots(figsize=(4, 3))
im = ax.imshow(cm, cmap="Blues")
ax.set_title("Confusion Matrix - Logistic Regression")
ax.set_xlabel("Predicted")
ax.set_ylabel("True")
ax.set_xticks([0, 1]); ax.set_yticks([0, 1])
ax.set_xticklabels(labels); ax.set_yticklabels(labels)
for i in range(cm.shape[0]):
for j in range(cm.shape[1]):
ax.text(j, i, cm[i, j], ha="center", va="center", color="black")
fig.colorbar(im, ax=ax, fraction=0.046, pad=0.04)
fig.tight_layout()
return fig
# -------------------------
# Step1:載入/追加
# -------------------------
def use_sample_dataset():
df = SAMPLE_DF.copy()
msg = f"✅ 已載入內建示範資料,共 {len(df)} 筆。"
return df, df.iloc[::-1], msg
def append_example(df, new_text, new_label):
"""把一筆 (text, label) 追加到現有資料集"""
if df is None or not isinstance(df, pd.DataFrame) or df.empty:
df = SAMPLE_DF.copy() # 保險:尚未載入時以內建資料起始
if not new_text or not str(new_text).strip():
return df, df.iloc[::-1], "❌ 請先輸入文字。"
try:
y = int(new_label)
if y not in (0, 1):
raise ValueError
except Exception:
return df, df.iloc[::-1], "❌ 標籤須為 0 或 1。"
new_row = pd.DataFrame([[str(new_text).strip(), y]], columns=["text", "label"])
df2 = pd.concat([df, new_row], ignore_index=True)
msg = f"✅ 已加入 1 筆,目前共 {len(df2)} 筆。"
return df2, df2.iloc[::-1],msg
# -------------------------
# Step2:訓練與評估
# -------------------------
def train_and_eval(df, test_ratio):
if df is None:
return None, "❌ 請先載入資料。", None, None
df = df.dropna(subset=["text", "label"]).copy()
df["label"] = df["label"].astype(int)
X_train, X_test, y_train, y_test = train_test_split(
df["text"], df["label"],
test_size=test_ratio, random_state=42, stratify=df["label"]
)
model = build_pipeline()
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
acc = accuracy_score(y_test, y_pred)
rpt = classification_report(y_test, y_pred, digits=3)
rpt_text = f"Accuracy: {acc:.3f}\n\n{rpt}"
fig = plot_confusion_matrix(y_test, y_pred)
dist = df["label"].value_counts().sort_index().to_dict()
tip = f"資料分布:0→{dist.get(0,0)} , 1→{dist.get(1,0)}"
return model, rpt_text + "\n" + tip, fig, "✅ 訓練完成,可以到下一步做測試。"
# -------------------------
# Step3:單句推論
# -------------------------
def predict_one(text, model):
if model is None:
return "❌ 尚未訓練模型。"
if not text or not text.strip():
return "請先輸入文字。"
proba = model.predict_proba([text])[0][1]
label = "正面(1)" if proba >= 0.5 else "負面(0)"
return f"{label}(信心 {proba:.2f})"
# -------------------------
# 介面
# -------------------------
with gr.Blocks(title="情感分類小幫手(LogReg + 字 n-gram)", theme=gr.themes.Soft()) as demo:
gr.Markdown("## 情感分類小幫手(LogReg + 字 n-gram)\n一步一步完成:資料 → 訓練 → 評估 → 測試")
# 讓學生進入頁面就有 30 筆資料
state_df = gr.State(SAMPLE_DF.copy())
state_model = gr.State()
with gr.Tab("Step 1|建立/擴充資料集"):
gr.Markdown(
"本活動不需上傳檔案,**直接用內建 30 筆資料**,並可在下方 **新增一筆 (text,label)** 進行擴充。\n"
"- label:0=負面、1=正面\n"
)
# 操作列:載入預設資料 + 新增一筆
with gr.Row():
btn_use_sample = gr.Button("重新載入內建示範資料(30筆)")
with gr.Row():
add_text = gr.Textbox(label="新增一筆文字", placeholder="例:上菜超快,服務又好!", lines=2)
add_label = gr.Radio(choices=[0, 1], value=1, label="標籤 (0=負面, 1=正面)")
btn_add = gr.Button("加入到資料集")
msg1 = gr.Markdown()
df_preview = gr.Dataframe(
headers=["text", "label"],
label="資料預覽(全部資料)",
wrap=True,
interactive=False,
value=SAMPLE_DF.iloc[::-1]
)
# 綁定事件
btn_use_sample.click(fn=use_sample_dataset, outputs=[state_df, df_preview, msg1])
btn_add.click(fn=append_example, inputs=[state_df, add_text, add_label], outputs=[state_df, df_preview, msg1])
with gr.Tab("Step 2|切分與訓練"):
gr.Markdown("設定測試集比例,按下 **開始訓練**。")
split = gr.Slider(0.1, 0.5, value=0.3, step=0.05, label="測試集比例 test_size")
btn_train = gr.Button("開始訓練")
train_msg = gr.Markdown()
report_box = gr.Textbox(label="評估報告(Accuracy / Precision / Recall / F1)", lines=12)
cm_plot = gr.Plot(label="混淆矩陣")
btn_train.click(
fn=train_and_eval,
inputs=[state_df, split],
outputs=[state_model, report_box, cm_plot, train_msg]
)
with gr.Tab("Step 3|測試/推論"):
gr.Markdown("單句測試")
with gr.Row():
test_text = gr.Textbox(label="單句輸入", placeholder="例:這家好吃到爆!", lines=2)
btn_pred = gr.Button("預測")
pred_out = gr.Textbox(label="結果", interactive=False)
btn_pred.click(fn=predict_one, inputs=[test_text, state_model], outputs=[pred_out])
if __name__ == "__main__":
demo.launch()