import gradio as gr import pandas as pd import numpy as np from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.model_selection import train_test_split from sklearn.naive_bayes import MultinomialNB from sklearn.linear_model import LogisticRegression from sklearn.svm import LinearSVC from sklearn.metrics import classification_report, confusion_matrix, accuracy_score import plotly.graph_objects as go # Global variables to store the trained model, vectorizer, and categories global_vectorizer = None global_model = None global_classes = None def train_classifier(file_obj, algorithm): global global_vectorizer, global_model, global_classes if file_obj is None: return "Please upload a CSV or Excel labeled training file.", None, None, gr.update(visible=False) try: if file_obj.name.endswith('.csv'): df = pd.read_csv(file_obj.name) else: df = pd.read_excel(file_obj.name) except Exception as e: return f"Error reading file: {str(e)}", None, None, gr.update(visible=False) # Standardize column headers text_col, label_col = None, None for col in df.columns: if col.lower() in ['text', 'document', 'content', 'body', 'sentence']: text_col = col elif col.lower() in ['label', 'category', 'class', 'target', 'topic']: label_col = col if not text_col or not label_col: # Fallbacks string_cols = df.select_dtypes(include=['object']).columns if len(string_cols) >= 2: text_col = string_cols[0] label_col = string_cols[1] else: return "Could not find 'Text' and 'Label' columns. Make sure your sheet has at least two columns.", None, None, gr.update(visible=False) df = df.dropna(subset=[text_col, label_col]) if len(df) < 10: return "Training dataset is too small. Please provide at least 10 labeled rows.", None, None, gr.update(visible=False) texts = df[text_col].astype(str).tolist() labels = df[label_col].astype(str).tolist() # Split X_train, X_test, y_train, y_test = train_test_split(texts, labels, test_size=0.25, random_state=42) # Vectorizer vectorizer = TfidfVectorizer(stop_words='english', max_features=2000) X_train_vec = vectorizer.fit_transform(X_train) X_test_vec = vectorizer.transform(X_test) # Model select if algorithm == "Naive Bayes": model = MultinomialNB() elif algorithm == "Logistic Regression": model = LogisticRegression(random_state=42, max_iter=1000) else: # Linear SVM model = LinearSVC(random_state=42) model.fit(X_train_vec, y_train) preds = model.predict(X_test_vec) # Metrics acc = accuracy_score(y_test, preds) classes = sorted(list(set(labels))) report = classification_report(y_test, preds, output_dict=True) report_df = pd.DataFrame(report).transpose().round(3).reset_index().rename(columns={"index": "Metric Class"}) # Save globals for real-time inference global_vectorizer = vectorizer global_model = model global_classes = classes # 4. Generate Visual Plotly Confusion Matrix cm = confusion_matrix(y_test, preds, labels=classes) fig = go.Figure(data=go.Heatmap( z=cm, x=classes, y=classes, colorscale='Oranges', text=cm, texttemplate="%{text}", hoverinfo='z' )) fig.update_layout( title=f"Confusion Matrix (Test Accuracy: {acc:.2%})", paper_bgcolor='#16100c', plot_bgcolor='#16100c', font_color='#f4eee6', xaxis=dict(title="Predicted label", gridcolor='rgba(255,255,255,0.05)'), yaxis=dict(title="True label", gridcolor='rgba(255,255,255,0.05)'), margin=dict(l=40, r=40, t=50, b=40) ) metrics_summary_html = f"""
Model Testing Accuracy
{acc:.2%}
Number of Target Classes
{len(classes)}
""" return "", metrics_summary_html, fig, report_df, gr.update(visible=True) def classify_new_text(new_text): global global_vectorizer, global_model, global_classes if global_model is None or global_vectorizer is None: return "Please train a classification model first using the panel on the left.", None if not new_text or len(new_text.strip()) < 3: return "Please enter a valid text to classify.", None # Vectorize vec = global_vectorizer.transform([new_text]) # Predict if hasattr(global_model, "predict_proba"): probs = global_model.predict_proba(vec)[0] else: # LinearSVC uses decision function decision = global_model.decision_function(vec)[0] # Map decision scores to pseudo-probabilities via softmax or sigmoid if len(global_classes) == 2: # For binary LinearSVC, decision is a single float probs = np.array([1 / (1 + np.exp(decision)), 1 / (1 + np.exp(-decision))]) else: exp_scores = np.exp(decision - np.max(decision)) probs = exp_scores / exp_scores.sum() pred_idx = np.argmax(probs) predicted_label = global_classes[pred_idx] confidence = probs[pred_idx] # Generate horizontal Plotly bar chart fig = go.Figure(go.Bar( x=probs, y=global_classes, orientation='h', marker=dict(color='#ff7043', line=dict(width=1, color='#16100c')), text=[f"{p:.1%}" for p in probs], textposition='auto' )) fig.update_layout( title="Class Probability Distribution", paper_bgcolor='#16100c', plot_bgcolor='#16100c', font_color='#f4eee6', xaxis=dict(showgrid=True, gridcolor='rgba(255,255,255,0.05)', range=[0, 1]), yaxis=dict(gridcolor='rgba(255,255,255,0.05)'), margin=dict(l=40, r=40, t=50, b=40) ) result_html = f"""
Predicted Category
{predicted_label}
Confidence Score: {confidence:.2%}
""" return result_html, fig theme = gr.themes.Default( primary_hue="orange", neutral_hue="stone" ).set( body_background_fill="#0d0907", body_text_color="#c4bbae", block_background_fill="#16100c", block_border_width="1px", block_label_text_color="#f4eee6" ) with gr.Blocks(theme=theme, title="Text Classifier Studio") as demo: gr.Markdown( """ # 🏷️ Custom Text Classification Studio ### 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! """ ) error_msg = gr.Markdown("", visible=False) with gr.Row(): with gr.Column(scale=1): file_obj = gr.File(label="Upload Training CSV or Excel", file_types=[".csv", ".xlsx"]) gr.Markdown("💡 **Tip**: Make sure your sheet has a **Text** column and a **Label** column (e.g., 'Politics', 'Sports', 'Art').") algorithm = gr.Radio( choices=["Naive Bayes", "Logistic Regression", "Linear Support Vector (SVM)"], value="Naive Bayes", label="Classification Algorithm" ) train_btn = gr.Button("Train Custom Classifier", variant="primary") with gr.Column(scale=2): stats_box = gr.HTML() with gr.Tabs(): with gr.TabItem("Validation & Diagnostics"): plot_cm = gr.Plot() table_report = gr.Dataframe(headers=["Metric Class", "precision", "recall", "f1-score", "support"]) with gr.TabItem("Live Model Playground"): with gr.Group(visible=False) as inference_group: new_text_input = gr.Textbox( label="Enter New Text to Classify", placeholder="Write or paste any paragraph here to test the trained model in real-time...", lines=5 ) predict_btn = gr.Button("Predict Category", variant="secondary") prediction_result = gr.HTML() plot_probs = gr.Plot() no_model_warning = gr.Markdown( "⚠️ **No Model Trained Yet**: Upload a training dataset on the left and click 'Train Custom Classifier' to unlock the live playground!", visible=True ) def on_train_success(file_obj, algo): err, stats, plot, report, update_group = train_classifier(file_obj, algo) if err: return gr.update(value=err, visible=True), "", None, None, gr.update(visible=False), gr.update(visible=True) return gr.update(visible=False), stats, plot, report, update_group, gr.update(visible=False) train_btn.click( on_train_success, inputs=[file_obj, algorithm], outputs=[error_msg, stats_box, plot_cm, table_report, inference_group, no_model_warning] ) predict_btn.click( classify_new_text, inputs=[new_text_input], outputs=[prediction_result, plot_probs] ) if __name__ == "__main__": demo.launch()