Jomaric commited on
Commit
96f48d5
·
0 Parent(s):

feat: initial release of machine learning space

Browse files
Files changed (3) hide show
  1. README.md +19 -0
  2. app.py +260 -0
  3. requirements.txt +5 -0
README.md ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Custom Text Classification Studio
3
+ emoji: 🏷️
4
+ colorFrom: red
5
+ colorTo: blue
6
+ sdk: gradio
7
+ app_file: app.py
8
+ pinned: false
9
+ ---
10
+
11
+ # Custom Text Classification Studio
12
+
13
+ An interactive educational machine learning application designed to help digital humanities and social science students learn supervised learning. Students can upload a labeled dataset, train a classifier locally, and instantly test predictions in real-time.
14
+
15
+ ### Features
16
+ 1. **Interactive Training**: Choose between Multinomial Naive Bayes, Logistic Regression, or Linear SVM models.
17
+ 2. **Visual Diagnostic Dashboards**: View accuracy scores, comprehensive classification reports, and interactive Plotly confusion matrix heatmaps.
18
+ 3. **Live Testing Playground**: Type or paste new, unseen paragraphs and view predicted categories along with a full probability distribution bar chart.
19
+ 4. **Zero-Server dependencies**: Trains in seconds completely inside the Space memory.
app.py ADDED
@@ -0,0 +1,260 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import pandas as pd
3
+ import numpy as np
4
+ from sklearn.feature_extraction.text import TfidfVectorizer
5
+ from sklearn.model_selection import train_test_split
6
+ from sklearn.naive_bayes import MultinomialNB
7
+ from sklearn.linear_model import LogisticRegression
8
+ from sklearn.svm import LinearSVC
9
+ from sklearn.metrics import classification_report, confusion_matrix, accuracy_score
10
+ import plotly.graph_objects as go
11
+
12
+ # Global variables to store the trained model, vectorizer, and categories
13
+ global_vectorizer = None
14
+ global_model = None
15
+ global_classes = None
16
+
17
+ def train_classifier(file_obj, algorithm):
18
+ global global_vectorizer, global_model, global_classes
19
+
20
+ if file_obj is None:
21
+ return "Please upload a CSV or Excel labeled training file.", None, None, gr.update(visible=False)
22
+
23
+ try:
24
+ if file_obj.name.endswith('.csv'):
25
+ df = pd.read_csv(file_obj.name)
26
+ else:
27
+ df = pd.read_excel(file_obj.name)
28
+ except Exception as e:
29
+ return f"Error reading file: {str(e)}", None, None, gr.update(visible=False)
30
+
31
+ # Standardize column headers
32
+ text_col, label_col = None, None
33
+ for col in df.columns:
34
+ if col.lower() in ['text', 'document', 'content', 'body', 'sentence']:
35
+ text_col = col
36
+ elif col.lower() in ['label', 'category', 'class', 'target', 'topic']:
37
+ label_col = col
38
+
39
+ if not text_col or not label_col:
40
+ # Fallbacks
41
+ string_cols = df.select_dtypes(include=['object']).columns
42
+ if len(string_cols) >= 2:
43
+ text_col = string_cols[0]
44
+ label_col = string_cols[1]
45
+ else:
46
+ return "Could not find 'Text' and 'Label' columns. Make sure your sheet has at least two columns.", None, None, gr.update(visible=False)
47
+
48
+ df = df.dropna(subset=[text_col, label_col])
49
+
50
+ if len(df) < 10:
51
+ return "Training dataset is too small. Please provide at least 10 labeled rows.", None, None, gr.update(visible=False)
52
+
53
+ texts = df[text_col].astype(str).tolist()
54
+ labels = df[label_col].astype(str).tolist()
55
+
56
+ # Split
57
+ X_train, X_test, y_train, y_test = train_test_split(texts, labels, test_size=0.25, random_state=42)
58
+
59
+ # Vectorizer
60
+ vectorizer = TfidfVectorizer(stop_words='english', max_features=2000)
61
+ X_train_vec = vectorizer.fit_transform(X_train)
62
+ X_test_vec = vectorizer.transform(X_test)
63
+
64
+ # Model select
65
+ if algorithm == "Naive Bayes":
66
+ model = MultinomialNB()
67
+ elif algorithm == "Logistic Regression":
68
+ model = LogisticRegression(random_state=42, max_iter=1000)
69
+ else: # Linear SVM
70
+ model = LinearSVC(random_state=42)
71
+
72
+ model.fit(X_train_vec, y_train)
73
+ preds = model.predict(X_test_vec)
74
+
75
+ # Metrics
76
+ acc = accuracy_score(y_test, preds)
77
+ classes = sorted(list(set(labels)))
78
+ report = classification_report(y_test, preds, output_dict=True)
79
+ report_df = pd.DataFrame(report).transpose().round(3).reset_index().rename(columns={"index": "Metric Class"})
80
+
81
+ # Save globals for real-time inference
82
+ global_vectorizer = vectorizer
83
+ global_model = model
84
+ global_classes = classes
85
+
86
+ # 4. Generate Visual Plotly Confusion Matrix
87
+ cm = confusion_matrix(y_test, preds, labels=classes)
88
+
89
+ fig = go.Figure(data=go.Heatmap(
90
+ z=cm,
91
+ x=classes,
92
+ y=classes,
93
+ colorscale='Oranges',
94
+ text=cm,
95
+ texttemplate="%{text}",
96
+ hoverinfo='z'
97
+ ))
98
+
99
+ fig.update_layout(
100
+ title=f"Confusion Matrix (Test Accuracy: {acc:.2%})",
101
+ paper_bgcolor='#16100c',
102
+ plot_bgcolor='#16100c',
103
+ font_color='#f4eee6',
104
+ xaxis=dict(title="Predicted label", gridcolor='rgba(255,255,255,0.05)'),
105
+ yaxis=dict(title="True label", gridcolor='rgba(255,255,255,0.05)'),
106
+ margin=dict(l=40, r=40, t=50, b=40)
107
+ )
108
+
109
+ metrics_summary_html = f"""
110
+ <div style='display: grid; grid-template-columns: repeat(2, 1fr); gap: 1rem; margin-bottom: 1.5rem;'>
111
+ <div style='background: rgba(255, 255, 255, 0.03); border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 8px; padding: 1rem; text-align: center;'>
112
+ <div style='font-size: 0.75rem; text-transform: uppercase; color: #ff7043; letter-spacing: 0.1em;'>Model Testing Accuracy</div>
113
+ <div style='font-size: 2rem; font-weight: bold; margin-top: 0.5rem;'>{acc:.2%}</div>
114
+ </div>
115
+ <div style='background: rgba(255, 255, 255, 0.03); border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 8px; padding: 1rem; text-align: center;'>
116
+ <div style='font-size: 0.75rem; text-transform: uppercase; color: #ff7043; letter-spacing: 0.1em;'>Number of Target Classes</div>
117
+ <div style='font-size: 2rem; font-weight: bold; margin-top: 0.5rem;'>{len(classes)}</div>
118
+ </div>
119
+ </div>
120
+ """
121
+
122
+ return "", metrics_summary_html, fig, report_df, gr.update(visible=True)
123
+
124
+ def classify_new_text(new_text):
125
+ global global_vectorizer, global_model, global_classes
126
+
127
+ if global_model is None or global_vectorizer is None:
128
+ return "Please train a classification model first using the panel on the left.", None
129
+
130
+ if not new_text or len(new_text.strip()) < 3:
131
+ return "Please enter a valid text to classify.", None
132
+
133
+ # Vectorize
134
+ vec = global_vectorizer.transform([new_text])
135
+
136
+ # Predict
137
+ if hasattr(global_model, "predict_proba"):
138
+ probs = global_model.predict_proba(vec)[0]
139
+ else: # LinearSVC uses decision function
140
+ decision = global_model.decision_function(vec)[0]
141
+ # Map decision scores to pseudo-probabilities via softmax or sigmoid
142
+ if len(global_classes) == 2:
143
+ # For binary LinearSVC, decision is a single float
144
+ probs = np.array([1 / (1 + np.exp(decision)), 1 / (1 + np.exp(-decision))])
145
+ else:
146
+ exp_scores = np.exp(decision - np.max(decision))
147
+ probs = exp_scores / exp_scores.sum()
148
+
149
+ pred_idx = np.argmax(probs)
150
+ predicted_label = global_classes[pred_idx]
151
+ confidence = probs[pred_idx]
152
+
153
+ # Generate horizontal Plotly bar chart
154
+ fig = go.Figure(go.Bar(
155
+ x=probs,
156
+ y=global_classes,
157
+ orientation='h',
158
+ marker=dict(color='#ff7043', line=dict(width=1, color='#16100c')),
159
+ text=[f"{p:.1%}" for p in probs],
160
+ textposition='auto'
161
+ ))
162
+
163
+ fig.update_layout(
164
+ title="Class Probability Distribution",
165
+ paper_bgcolor='#16100c',
166
+ plot_bgcolor='#16100c',
167
+ font_color='#f4eee6',
168
+ xaxis=dict(showgrid=True, gridcolor='rgba(255,255,255,0.05)', range=[0, 1]),
169
+ yaxis=dict(gridcolor='rgba(255,255,255,0.05)'),
170
+ margin=dict(l=40, r=40, t=50, b=40)
171
+ )
172
+
173
+ result_html = f"""
174
+ <div style='background: rgba(255, 112, 67, 0.05); border-left: 4px solid #ff7043; border-radius: 4px; padding: 1.5rem; margin-bottom: 1rem;'>
175
+ <div style='font-size: 0.8rem; text-transform: uppercase; color: #f4eee6; letter-spacing: 0.1em;'>Predicted Category</div>
176
+ <div style='font-size: 2.2rem; font-weight: bold; color: #ff7043; margin-top: 0.5rem;'>{predicted_label}</div>
177
+ <div style='font-size: 0.95rem; margin-top: 0.5rem; opacity: 0.8;'>Confidence Score: <strong>{confidence:.2%}</strong></div>
178
+ </div>
179
+ """
180
+
181
+ return result_html, fig
182
+
183
+ theme = gr.themes.Default(
184
+ primary_hue="orange",
185
+ neutral_hue="stone"
186
+ ).set(
187
+ body_background_fill="#0d0907",
188
+ body_text_color="#c4bbae",
189
+ block_background_fill="#16100c",
190
+ block_border_width="1px",
191
+ block_label_text_color="#f4eee6"
192
+ )
193
+
194
+ with gr.Blocks(theme=theme, title="Text Classifier Studio") as demo:
195
+ gr.Markdown(
196
+ """
197
+ # 🏷️ Custom Text Classification Studio
198
+ ### Upload a labeled training sheet (CSV containing Text and Category labels) to train a custom machine learning classifier locally. Test it instantly with live texts!
199
+ """
200
+ )
201
+
202
+ error_msg = gr.Markdown("", visible=False)
203
+
204
+ with gr.Row():
205
+ with gr.Column(scale=1):
206
+ file_obj = gr.File(label="Upload Training CSV or Excel", file_types=[".csv", ".xlsx"])
207
+ gr.Markdown("💡 **Tip**: Make sure your sheet has a **Text** column and a **Label** column (e.g., 'Politics', 'Sports', 'Art').")
208
+
209
+ algorithm = gr.Radio(
210
+ choices=["Naive Bayes", "Logistic Regression", "Linear Support Vector (SVM)"],
211
+ value="Naive Bayes",
212
+ label="Classification Algorithm"
213
+ )
214
+
215
+ train_btn = gr.Button("Train Custom Classifier", variant="primary")
216
+
217
+ with gr.Column(scale=2):
218
+ stats_box = gr.HTML()
219
+
220
+ with gr.Tabs():
221
+ with gr.TabItem("Validation & Diagnostics"):
222
+ plot_cm = gr.Plot()
223
+ table_report = gr.Dataframe(headers=["Metric Class", "precision", "recall", "f1-score", "support"])
224
+
225
+ with gr.TabItem("Live Model Playground"):
226
+ with gr.Group(visible=False) as inference_group:
227
+ new_text_input = gr.Textbox(
228
+ label="Enter New Text to Classify",
229
+ placeholder="Write or paste any paragraph here to test the trained model in real-time...",
230
+ lines=5
231
+ )
232
+ predict_btn = gr.Button("Predict Category", variant="secondary")
233
+ prediction_result = gr.HTML()
234
+ plot_probs = gr.Plot()
235
+
236
+ no_model_warning = gr.Markdown(
237
+ "⚠️ **No Model Trained Yet**: Upload a training dataset on the left and click 'Train Custom Classifier' to unlock the live playground!",
238
+ visible=True
239
+ )
240
+
241
+ def on_train_success(file_obj, algo):
242
+ err, stats, plot, report, update_group = train_classifier(file_obj, algo)
243
+ if err:
244
+ return gr.update(value=err, visible=True), "", None, None, gr.update(visible=False), gr.update(visible=True)
245
+ return gr.update(visible=False), stats, plot, report, update_group, gr.update(visible=False)
246
+
247
+ train_btn.click(
248
+ on_train_success,
249
+ inputs=[file_obj, algorithm],
250
+ outputs=[error_msg, stats_box, plot_cm, table_report, inference_group, no_model_warning]
251
+ )
252
+
253
+ predict_btn.click(
254
+ classify_new_text,
255
+ inputs=[new_text_input],
256
+ outputs=[prediction_result, plot_probs]
257
+ )
258
+
259
+ if __name__ == "__main__":
260
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ pandas
2
+ numpy
3
+ scikit-learn
4
+ plotly
5
+ openpyxl