justinahsu commited on
Commit
d0eda6f
·
verified ·
1 Parent(s): 489592f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +211 -47
app.py CHANGED
@@ -1,18 +1,27 @@
1
- # 如果在 Colab,本區塊可先執行一次(本機已安裝就略過)
2
- !pip -q install scikit-learn pandas matplotlib gradio
 
 
 
 
3
 
 
 
4
  import pandas as pd
 
 
 
5
  from sklearn.model_selection import train_test_split
 
6
  from sklearn.feature_extraction.text import CountVectorizer, TfidfTransformer
7
  from sklearn.linear_model import LogisticRegression
8
- from sklearn.naive_bayes import MultinomialNB
9
- from sklearn.pipeline import Pipeline
10
  from sklearn.metrics import accuracy_score, classification_report, confusion_matrix
11
- import matplotlib.pyplot as plt
12
- import numpy as np
13
 
14
- # 1) 準備一個小型「示範資料集」(你也可以改成班級自建)
15
- data = [
 
 
 
16
  ("這家餐廳超好吃,我下次還要來!", 1),
17
  ("服務人員很親切,體驗很棒", 1),
18
  ("環境乾淨、氣氛舒服,推!", 1),
@@ -44,61 +53,216 @@ data = [
44
  ("週年限定套餐超值,朋友都喜歡", 1),
45
  ("衛生紙與餐具不足,需要一直跟店員拿", 0),
46
  ]
 
 
47
 
 
 
 
 
 
 
48
 
49
- df = pd.DataFrame(data, columns=["text", "label"])
50
 
 
 
 
51
 
52
- # 2) 訓練/測試 切分
53
- X_train, X_test, y_train, y_test = train_test_split(
54
- df["text"], df["label"], test_size=0.3, random_state=42, stratify=df["label"]
55
- )
56
 
57
- # 3) 做一條「處理流水線」:文字 -> 向量化 -> TF-IDF -> 模型
58
- # (A) 邏輯斯迴歸版(入門也很好用常見基線
59
- logreg_clf = Pipeline([
60
- # 中文建議用「字 n-gram」,避免斷詞問題;2~4字能抓到詞片段
61
- ("vect", CountVectorizer(analyzer="char", ngram_range=(2, 4))),
62
- ("tfidf", TfidfTransformer()),
63
- ("clf", LogisticRegression(max_iter=200))
64
- ])
 
65
 
66
- logreg_clf.fit(X_train, y_train)
67
- y_pred_lr = logreg_clf.predict(X_test)
68
 
69
- print("=== Logistic Regression ===")
70
- print("Accuracy:", accuracy_score(y_test, y_pred_lr))
71
- print(classification_report(y_test, y_pred_lr, digits=3))
 
 
 
 
72
 
73
- # 5) 畫一張混淆矩陣(看錯在哪)
74
- def plot_cm(y_true, y_pred, title):
 
75
  cm = confusion_matrix(y_true, y_pred)
76
- fig, ax = plt.subplots(figsize=(4,3))
77
- im = ax.imshow(cm)
78
- ax.set_title(title)
79
  ax.set_xlabel("Predicted")
80
  ax.set_ylabel("True")
81
- ax.set_xticks([0,1]); ax.set_yticks([0,1])
82
- ax.set_xticklabels(["負面(0)","正面(1)"]); ax.set_yticklabels(["負面(0)","正面(1)"])
83
- # 印數字
84
  for i in range(cm.shape[0]):
85
  for j in range(cm.shape[1]):
86
- ax.text(j, i, cm[i, j], ha="center", va="center")
87
- plt.show()
 
 
88
 
89
- plot_cm(y_test, y_pred_lr, "Confusion Matrix - Logistic Regression")
90
 
 
 
 
 
 
 
 
91
 
92
- # 6) 建立界面
93
- import gradio as gr
94
 
95
- with gr.Blocks(title="情感分類小幫手(LogReg + 字 n-gram)") as demo:
96
- gr.Markdown("### 輸入一句中文,AI 幫你判斷正/負面。")
97
- inp = gr.Textbox(label="輸入文字", lines=2, placeholder="例:這家好吃到爆!")
98
- submit = gr.Button("Submit")
99
- out = gr.Textbox(label="AI 判斷結果", interactive=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
 
101
- submit.click(fn=predict_one, inputs=inp, outputs=out)
102
 
103
- # Colab 若需要外網連結可改成 share=True
104
- demo.launch() # 或 demo.launch(share=True)
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Hugging Face Spaces / Gradio app
4
+ 步驟導引:資料 → 切分/訓練 → 評估(Accuracy+混淆矩陣) → 測試介面
5
+ 資料欄位需為: text, label (label 為 0/1)
6
+ """
7
 
8
+ import io
9
+ import numpy as np
10
  import pandas as pd
11
+ import matplotlib.pyplot as plt
12
+
13
+ import gradio as gr
14
  from sklearn.model_selection import train_test_split
15
+ from sklearn.pipeline import Pipeline
16
  from sklearn.feature_extraction.text import CountVectorizer, TfidfTransformer
17
  from sklearn.linear_model import LogisticRegression
 
 
18
  from sklearn.metrics import accuracy_score, classification_report, confusion_matrix
 
 
19
 
20
+
21
+ # -------------------------
22
+ # 內建示範資料(30 筆)
23
+ # -------------------------
24
+ SAMPLE_DATA = [
25
  ("這家餐廳超好吃,我下次還要來!", 1),
26
  ("服務人員很親切,體驗很棒", 1),
27
  ("環境乾淨、氣氛舒服,推!", 1),
 
53
  ("週年限定套餐超值,朋友都喜歡", 1),
54
  ("衛生紙與餐具不足,需要一直跟店員拿", 0),
55
  ]
56
+ SAMPLE_DF = pd.DataFrame(SAMPLE_DATA, columns=["text", "label"])
57
+
58
 
59
+ # -------------------------
60
+ # 工具函式
61
+ # -------------------------
62
+ def read_csv_file(file_obj) -> pd.DataFrame:
63
+ """從上傳檔案讀 CSV"""
64
+ return pd.read_csv(file_obj, encoding_errors="ignore")
65
 
 
66
 
67
+ def read_csv_text(text_block: str) -> pd.DataFrame:
68
+ """從貼上的 CSV 文字讀取(需含表頭 text,label)"""
69
+ return pd.read_csv(io.StringIO(text_block))
70
 
 
 
 
 
71
 
72
+ def check_df(df: pd.DataFrame) -> str:
73
+ """基欄位檢查回傳錯誤訊息(無錯回空字串"""
74
+ cols = set(df.columns.str.lower())
75
+ need = {"text", "label"}
76
+ if not need.issubset(cols):
77
+ return " CSV 需要兩個欄位:text, label(0/1)。"
78
+ if df.empty:
79
+ return "❌ 資料為空。"
80
+ return ""
81
 
 
 
82
 
83
+ def build_pipeline() -> Pipeline:
84
+ """字 n-gram + TF-IDF + Logistic Regression"""
85
+ return Pipeline([
86
+ ("vect", CountVectorizer(analyzer="char", ngram_range=(2, 4))),
87
+ ("tfidf", TfidfTransformer()),
88
+ ("clf", LogisticRegression(max_iter=200)),
89
+ ])
90
 
91
+
92
+ def plot_confusion_matrix(y_true, y_pred, labels=("負面(0)", "正面(1)")):
93
+ """回傳 matplotlib 圖物件(Gradio 會顯示)"""
94
  cm = confusion_matrix(y_true, y_pred)
95
+ fig, ax = plt.subplots(figsize=(4, 3))
96
+ im = ax.imshow(cm, cmap="Blues")
97
+ ax.set_title("Confusion Matrix - Logistic Regression")
98
  ax.set_xlabel("Predicted")
99
  ax.set_ylabel("True")
100
+ ax.set_xticks([0, 1]); ax.set_yticks([0, 1])
101
+ ax.set_xticklabels(labels); ax.set_yticklabels(labels)
 
102
  for i in range(cm.shape[0]):
103
  for j in range(cm.shape[1]):
104
+ ax.text(j, i, cm[i, j], ha="center", va="center", color="black")
105
+ fig.colorbar(im, ax=ax, fraction=0.046, pad=0.04)
106
+ fig.tight_layout()
107
+ return fig
108
 
 
109
 
110
+ # -------------------------
111
+ # Gradio 回呼:Step1 載入資料
112
+ # -------------------------
113
+ def use_sample_dataset():
114
+ df = SAMPLE_DF.copy()
115
+ msg = f"✅ 已載入內建示範資料,共 {len(df)} 筆。"
116
+ return df, df.head(10), msg
117
 
 
 
118
 
119
+ def load_from_file(file):
120
+ try:
121
+ df = read_csv_file(file)
122
+ err = check_df(df)
123
+ if err:
124
+ return gr.State(None), None, err
125
+ df = df.rename(columns={"Text": "text", "Label": "label"})
126
+ msg = f"✅ 已載入檔案:{len(df)} 筆。"
127
+ return df, df.head(10), msg
128
+ except Exception as e:
129
+ return gr.State(None), None, f"❌ 載入失敗:{e}"
130
+
131
+
132
+ def load_from_text(csv_text):
133
+ try:
134
+ df = read_csv_text(csv_text)
135
+ err = check_df(df)
136
+ if err:
137
+ return gr.State(None), None, err
138
+ df = df.rename(columns={"Text": "text", "Label": "label"})
139
+ msg = f"✅ 已載入貼上資料:{len(df)} 筆。"
140
+ return df, df.head(10), msg
141
+ except Exception as e:
142
+ return gr.State(None), None, f"❌ 解析失敗:{e}"
143
+
144
+
145
+ # -------------------------
146
+ # Gradio 回呼:Step2 訓練與評估
147
+ # -------------------------
148
+ def train_and_eval(df, test_ratio):
149
+ if df is None:
150
+ return None, "❌ 請先載入資料。", None, None
151
+
152
+ # 乾淨處理
153
+ df = df.dropna(subset=["text", "label"]).copy()
154
+ df["label"] = df["label"].astype(int)
155
+
156
+ # 切分
157
+ X_train, X_test, y_train, y_test = train_test_split(
158
+ df["text"], df["label"],
159
+ test_size=test_ratio, random_state=42, stratify=df["label"]
160
+ )
161
+
162
+ # 訓練
163
+ model = build_pipeline()
164
+ model.fit(X_train, y_train)
165
+
166
+ # 評估
167
+ y_pred = model.predict(X_test)
168
+ acc = accuracy_score(y_test, y_pred)
169
+ rpt = classification_report(y_test, y_pred, digits=3)
170
+ rpt_text = f"Accuracy: {acc:.3f}\n\n{rpt}"
171
+
172
+ # 圖
173
+ fig = plot_confusion_matrix(y_test, y_pred)
174
+
175
+ # 類別分布小提示
176
+ dist = df["label"].value_counts().sort_index().to_dict()
177
+ tip = f"資料分布:0→{dist.get(0,0)} , 1→{dist.get(1,0)}"
178
+
179
+ return model, rpt_text + "\n" + tip, fig, "✅ 訓練完成,可以到下一步做測試。"
180
+
181
+
182
+ # -------------------------
183
+ # Gradio 回呼:Step3 推論
184
+ # -------------------------
185
+ def predict_one(text, model):
186
+ if model is None:
187
+ return "❌ 尚未訓練模型。"
188
+ if not text or not text.strip():
189
+ return "請先輸入文字。"
190
+ proba = model.predict_proba([text])[0][1]
191
+ label = "正面(1)" if proba >= 0.5 else "負面(0)"
192
+ return f"{label}(信心 {proba:.2f})"
193
+
194
+
195
+ def predict_batch(df_texts, model):
196
+ if model is None:
197
+ return None
198
+ if df_texts is None or df_texts.empty:
199
+ return None
200
+ texts = df_texts.iloc[:, 0].astype(str).tolist()
201
+ probas = model.predict_proba(texts)[:, 1]
202
+ labels = (probas >= 0.5).astype(int)
203
+ out = pd.DataFrame({"text": texts, "prob_pos": probas, "pred": labels})
204
+ return out
205
+
206
+
207
+ # -------------------------
208
+ # 介面
209
+ # -------------------------
210
+ with gr.Blocks(title="情感分類小幫手(LogReg + 字 n-gram)", theme=gr.themes.Soft()) as demo:
211
+ gr.Markdown("## 情感分類小幫手(LogReg + 字 n-gram)\n一步一步完成:資料 → 訓練 → 評估 → 測試")
212
+
213
+ state_df = gr.State() # 保存目前資料集
214
+ state_model = gr.State() # 保存已訓練模型
215
+
216
+ with gr.Tab("Step 1|載入資料"):
217
+ gr.Markdown(
218
+ "上傳 **CSV (text,label)** 或貼上文字,或使用內建示範資料。\n"
219
+ "- label:0=負面、1=正面\n"
220
+ "- CSV 必須含表頭:`text,label`\n"
221
+ )
222
+ with gr.Row():
223
+ file_in = gr.File(label="上傳 CSV")
224
+ btn_load_file = gr.Button("讀取檔案")
225
+ with gr.Row():
226
+ txt_in = gr.Textbox(lines=5, label="或貼上 CSV 文字(需含表頭 text,label)")
227
+ btn_load_text = gr.Button("讀取貼上文字")
228
+ btn_use_sample = gr.Button("使用內建示範資料(30筆)")
229
+
230
+ msg1 = gr.Markdown()
231
+ df_preview = gr.Dataframe(headers=["text", "label"], label="資料預覽(前10筆)", interactive=False)
232
+
233
+ btn_use_sample.click(fn=use_sample_dataset, outputs=[state_df, df_preview, msg1])
234
+ btn_load_file.click(fn=load_from_file, inputs=[file_in], outputs=[state_df, df_preview, msg1])
235
+ btn_load_text.click(fn=load_from_text, inputs=[txt_in], outputs=[state_df, df_preview, msg1])
236
+
237
+ with gr.Tab("Step 2|切分與訓練"):
238
+ gr.Markdown("設定測試集比例,按下 **開始訓練**。")
239
+ split = gr.Slider(0.1, 0.5, value=0.3, step=0.05, label="測試集比例 test_size")
240
+ btn_train = gr.Button("開始訓練")
241
+ train_msg = gr.Markdown()
242
+ report_box = gr.Textbox(label="評估報告(Accuracy / Precision / Recall / F1)", lines=12)
243
+ cm_plot = gr.Plot(label="混淆矩陣")
244
+
245
+ btn_train.click(
246
+ fn=train_and_eval,
247
+ inputs=[state_df, split],
248
+ outputs=[state_model, report_box, cm_plot, train_msg]
249
+ )
250
+
251
+ with gr.Tab("Step 3|測試/推論"):
252
+ gr.Markdown("單句測試或批次測試。")
253
+ with gr.Row():
254
+ test_text = gr.Textbox(label="單句輸入", placeholder="例:這家好吃到爆!", lines=2)
255
+ btn_pred = gr.Button("預測")
256
+ pred_out = gr.Textbox(label="結果", interactive=False)
257
+
258
+ btn_pred.click(fn=predict_one, inputs=[test_text, state_model], outputs=[pred_out])
259
+
260
+ gr.Markdown("---\n**批次測試**:在下方貼入多行文字(第一欄為 text),點擊預測。")
261
+ df_infer = gr.Dataframe(headers=["text"], row_count=5, col_count=1)
262
+ btn_batch = gr.Button("批次預測")
263
+ df_result = gr.Dataframe(label="批次結果(prob_pos=正面機率, pred=預測標籤)", interactive=False)
264
 
265
+ btn_batch.click(fn=predict_batch, inputs=[df_infer, state_model], outputs=[df_result])
266
 
267
+ if __name__ == "__main__":
268
+ demo.launch()