Spaces:
Sleeping
Sleeping
| import os | |
| import torch | |
| import torch.nn as nn | |
| import pandas as pd | |
| import numpy as np | |
| import pickle | |
| import re | |
| import gradio as gr | |
| from transformers import DebertaV2Model, DebertaV2Tokenizer | |
| # ========================== | |
| # Configuration | |
| # ========================== | |
| DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu') | |
| MAX_LENGTH = 256 | |
| MODELS_DIR = './models/' | |
| CAT_ENCODER_PATH = os.path.join(MODELS_DIR, 'cat_encoder.pkl') | |
| MISC_ENCODER_PATH = os.path.join(MODELS_DIR, 'misc_encoder.pkl') | |
| FEATURE_COLS_PATH = os.path.join(MODELS_DIR, 'feature_cols.pkl') | |
| DEFAULT_MODEL = 'map_2025_best_model_fold7.pt' | |
| # ========================== | |
| # Feature Extraction | |
| # ========================== | |
| def extract_math_features(text): | |
| if not isinstance(text, str): | |
| return { | |
| 'frac_count': 0, 'number_count': 0, 'operator_count': 0, | |
| 'decimal_count': 0, 'question_mark': 0, 'math_keyword_count': 0 | |
| } | |
| features = { | |
| 'frac_count': len(re.findall(r'FRAC_\d+_\d+|\\frac', text)), | |
| 'number_count': len(re.findall(r'\b\d+\b', text)), | |
| 'operator_count': len(re.findall(r'[\+\-\*\/\=]', text)), | |
| 'decimal_count': len(re.findall(r'\d+\.\d+', text)), | |
| 'question_mark': int('?' in text), | |
| 'math_keyword_count': len(re.findall(r'solve|calculate|equation|fraction|decimal', text.lower())) | |
| } | |
| return features | |
| def create_features(df): | |
| for col in ['QuestionText', 'MC_Answer', 'StudentExplanation']: | |
| df[col] = df[col].fillna('') | |
| df['mc_answer_len'] = df['MC_Answer'].str.len() | |
| df['explanation_len'] = df['StudentExplanation'].str.len() | |
| df['question_len'] = df['QuestionText'].str.len() | |
| df['explanation_to_question_ratio'] = df['explanation_len'] / (df['question_len'] + 1) | |
| for col in ['QuestionText', 'MC_Answer', 'StudentExplanation']: | |
| mf = df[col].apply(extract_math_features).apply(pd.Series) | |
| prefix = 'mc_' if col == 'MC_Answer' else 'exp_' if col == 'StudentExplanation' else '' | |
| mf.columns = [f'{prefix}{c}' for c in mf.columns] | |
| df = pd.concat([df, mf], axis=1) | |
| df['sentence'] = ( | |
| "Question: " + df['QuestionText'] + | |
| " Answer: " + df['MC_Answer'] + | |
| " Explanation: " + df['StudentExplanation'] | |
| ) | |
| return df | |
| # ========================== | |
| # Model Definition | |
| # ========================== | |
| class MathMisconceptionModel(nn.Module): | |
| def __init__(self, n_categories, n_misconceptions, feature_dim): | |
| super().__init__() | |
| self.bert = DebertaV2Model.from_pretrained('microsoft/deberta-v3-small') | |
| self.tokenizer = DebertaV2Tokenizer.from_pretrained('microsoft/deberta-v3-small') | |
| self.feature_processor = nn.Sequential( | |
| nn.Linear(feature_dim, 64), | |
| nn.ReLU(), | |
| nn.Dropout(0.3) | |
| ) | |
| self.category_head = nn.Sequential( | |
| nn.Linear(768 + 64, 256), | |
| nn.ReLU(), | |
| nn.Dropout(0.2), | |
| nn.Linear(256, n_categories) | |
| ) | |
| self.misconception_head = nn.Sequential( | |
| nn.Linear(768 + 64, 256), | |
| nn.ReLU(), | |
| nn.Dropout(0.2), | |
| nn.Linear(256, n_misconceptions) | |
| ) | |
| def forward(self, input_texts, features): | |
| tokens = self.tokenizer( | |
| input_texts, | |
| padding=True, | |
| truncation=True, | |
| max_length=MAX_LENGTH, | |
| return_tensors="pt" | |
| ).to(DEVICE) | |
| outputs = self.bert(**tokens) | |
| text_emb = outputs.last_hidden_state[:, 0, :] | |
| feat_emb = self.feature_processor(features) | |
| combined = torch.cat([text_emb, feat_emb], dim=1) | |
| return self.category_head(combined), self.misconception_head(combined) | |
| # ========================== | |
| # Load Resources | |
| # ========================== | |
| try: | |
| with open(CAT_ENCODER_PATH, 'rb') as f: | |
| cat_enc = pickle.load(f) | |
| with open(MISC_ENCODER_PATH, 'rb') as f: | |
| misc_enc = pickle.load(f) | |
| with open(FEATURE_COLS_PATH, 'rb') as f: | |
| feature_cols = pickle.load(f) | |
| except FileNotFoundError as e: | |
| print(f"Error loading resources: {e}") | |
| exit() | |
| # Dummy scaler (no scaling) | |
| class IdentityScaler: | |
| def transform(self, X): | |
| return X | |
| scaler = IdentityScaler() | |
| # ========================== | |
| # Prediction Logic | |
| # ========================== | |
| def predict(model_name, question, mc_answer, explanation, export_csv): | |
| model_path = os.path.join(MODELS_DIR, model_name) | |
| if not os.path.exists(model_path): | |
| return "Model not found.", None | |
| data = { | |
| 'QuestionText': [question], | |
| 'MC_Answer': [mc_answer], | |
| 'StudentExplanation': [explanation] | |
| } | |
| df = pd.DataFrame(data) | |
| processed_df = create_features(df.copy()) | |
| for col in feature_cols: | |
| if col not in processed_df.columns: | |
| processed_df[col] = 0 | |
| features = processed_df[feature_cols].fillna(0).values | |
| features_scaled = scaler.transform(features) | |
| model = MathMisconceptionModel( | |
| n_categories=len(cat_enc.classes_), | |
| n_misconceptions=len(misc_enc.classes_), | |
| feature_dim=features_scaled.shape[1] | |
| ).to(DEVICE) | |
| model.load_state_dict(torch.load(model_path, map_location=DEVICE)) | |
| model.eval() | |
| text = processed_df['sentence'].tolist() | |
| features_tensor = torch.tensor(features_scaled, dtype=torch.float).to(DEVICE) | |
| with torch.no_grad(): | |
| cat_logits, misc_logits = model(text, features_tensor) | |
| cat_pred = torch.argmax(cat_logits, 1).cpu().item() | |
| misc_pred = torch.argmax(misc_logits, 1).cpu().item() | |
| predicted_category = cat_enc.inverse_transform([cat_pred])[0] | |
| predicted_misconception = misc_enc.inverse_transform([misc_pred])[0] | |
| result_text = ( | |
| f"Predicted Category: {predicted_category}\n" | |
| f"Predicted Misconception: {predicted_misconception}" | |
| ) | |
| csv_path = None | |
| if export_csv: | |
| export_df = pd.DataFrame([{ | |
| "Question": question, | |
| "MC_Answer": mc_answer, | |
| "Student_Explanation": explanation, | |
| "Predicted_Category": predicted_category, | |
| "Predicted_Misconception": predicted_misconception, | |
| "Model_Used": model_name | |
| }]) | |
| csv_path = "predictions.csv" | |
| file_exists = os.path.isfile(csv_path) | |
| export_df.to_csv(csv_path, mode='a', header=not file_exists, index=False) | |
| return result_text, csv_path | |
| # ========================== | |
| # Gradio UI | |
| # ========================== | |
| model_files = [f for f in os.listdir(MODELS_DIR) if f.endswith('.pt')] | |
| iface = gr.Interface( | |
| fn=predict, | |
| inputs=[ | |
| gr.Dropdown(model_files, value=DEFAULT_MODEL, label="Select Model"), | |
| gr.Textbox(label="Enter Question", lines=3), | |
| gr.Textbox(label="Enter Correct Answer (MC_Answer)", lines=1), | |
| gr.Textbox(label="Enter Student's Explanation", lines=5), | |
| gr.Checkbox(label="Export Prediction to CSV") | |
| ], | |
| outputs=[ | |
| gr.Textbox(label="Prediction Result"), | |
| gr.File(label="CSV File (if exported)") | |
| ], | |
| title="Math Misconception Predictor", | |
| description="Select a model and provide the question, correct answer, and student's explanation to get a prediction.", | |
| theme=gr.themes.Soft() | |
| ) | |
| if __name__ == "__main__": | |
| iface.launch( | |
| server_name="0.0.0.0", | |
| server_port=7860, | |
| share=True, | |
| debug=False, | |
| show_error=True | |
| ) | |