arunalk722 commited on
Commit
62bbce5
·
verified ·
1 Parent(s): 3cc8716

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +214 -222
app.py CHANGED
@@ -1,222 +1,214 @@
1
- import os
2
- import torch
3
- import torch.nn as nn
4
- import pandas as pd
5
- import numpy as np
6
- import pickle
7
- import re
8
- import gradio as gr
9
- from transformers import DebertaV2Model, DebertaV2Tokenizer
10
- from sklearn.preprocessing import StandardScaler
11
-
12
- # ==========================
13
- # Configuration
14
- # ==========================
15
- DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
16
- MAX_LENGTH = 256
17
- MODELS_DIR = './models/'
18
- CAT_ENCODER_PATH = os.path.join(MODELS_DIR, 'cat_encoder.pkl')
19
- MISC_ENCODER_PATH = os.path.join(MODELS_DIR, 'misc_encoder.pkl')
20
- FEATURE_COLS_PATH = os.path.join(MODELS_DIR, 'feature_cols.pkl')
21
- TRAIN_DATA_PATH = './dataset/train.csv'
22
- DEFAULT_MODEL = 'map_2025_best_model_fold7.pt'
23
-
24
- # ==========================
25
- # Feature Extraction (from training script)
26
- # ==========================
27
- def extract_math_features(text):
28
- if not isinstance(text, str):
29
- return {
30
- 'frac_count': 0, 'number_count': 0, 'operator_count': 0,
31
- 'decimal_count': 0, 'question_mark': 0, 'math_keyword_count': 0
32
- }
33
- features = {
34
- 'frac_count': len(re.findall(r'FRAC_\d+_\d+|\\frac', text)),
35
- 'number_count': len(re.findall(r'\b\d+\b', text)),
36
- 'operator_count': len(re.findall(r'[\+\-\*\/\=]', text)),
37
- 'decimal_count': len(re.findall(r'\d+\.\d+', text)),
38
- 'question_mark': int('?' in text),
39
- 'math_keyword_count': len(re.findall(r'solve|calculate|equation|fraction|decimal', text.lower()))
40
- }
41
- return features
42
-
43
- def create_features(df):
44
- for col in ['QuestionText', 'MC_Answer', 'StudentExplanation']:
45
- df[col] = df[col].fillna('')
46
- df['mc_answer_len'] = df['MC_Answer'].str.len()
47
- df['explanation_len'] = df['StudentExplanation'].str.len()
48
- df['question_len'] = df['QuestionText'].str.len()
49
- df['explanation_to_question_ratio'] = df['explanation_len'] / (df['question_len'] + 1)
50
- for col in ['QuestionText', 'MC_Answer', 'StudentExplanation']:
51
- mf = df[col].apply(extract_math_features).apply(pd.Series)
52
- prefix = 'mc_' if col == 'MC_Answer' else 'exp_' if col == 'StudentExplanation' else ''
53
- mf.columns = [f'{prefix}{c}' for c in mf.columns]
54
- df = pd.concat([df, mf], axis=1)
55
- df['sentence'] = (
56
- "Question: " + df['QuestionText'] +
57
- " Answer: " + df['MC_Answer'] +
58
- " Explanation: " + df['StudentExplanation']
59
- )
60
- return df
61
-
62
- # ==========================
63
- # Deep Learning Model (from training script)
64
- # ==========================
65
- class MathMisconceptionModel(nn.Module):
66
- def __init__(self, n_categories, n_misconceptions, feature_dim):
67
- super().__init__()
68
- self.bert = DebertaV2Model.from_pretrained('microsoft/deberta-v3-small')
69
- self.tokenizer = DebertaV2Tokenizer.from_pretrained('microsoft/deberta-v3-small')
70
- self.feature_processor = nn.Sequential(
71
- nn.Linear(feature_dim, 64),
72
- nn.ReLU(),
73
- nn.Dropout(0.3)
74
- )
75
- self.category_head = nn.Sequential(
76
- nn.Linear(768 + 64, 256),
77
- nn.ReLU(),
78
- nn.Dropout(0.2),
79
- nn.Linear(256, n_categories)
80
- )
81
- self.misconception_head = nn.Sequential(
82
- nn.Linear(768 + 64, 256),
83
- nn.ReLU(),
84
- nn.Dropout(0.2),
85
- nn.Linear(256, n_misconceptions)
86
- )
87
-
88
- def forward(self, input_texts, features):
89
- tokens = self.tokenizer(
90
- input_texts,
91
- padding=True,
92
- truncation=True,
93
- max_length=MAX_LENGTH,
94
- return_tensors="pt"
95
- ).to(DEVICE)
96
- outputs = self.bert(**tokens)
97
- text_emb = outputs.last_hidden_state[:, 0, :]
98
- feat_emb = self.feature_processor(features)
99
- combined = torch.cat([text_emb, feat_emb], dim=1)
100
- return self.category_head(combined), self.misconception_head(combined)
101
-
102
- # ==========================
103
- # Load Resources
104
- # ==========================
105
- try:
106
- with open(CAT_ENCODER_PATH, 'rb') as f:
107
- cat_enc = pickle.load(f)
108
- with open(MISC_ENCODER_PATH, 'rb') as f:
109
- misc_enc = pickle.load(f)
110
- with open(FEATURE_COLS_PATH, 'rb') as f:
111
- feature_cols = pickle.load(f)
112
-
113
- # Fit scaler on the original training data
114
- train_df = pd.read_csv(TRAIN_DATA_PATH)
115
- processed_train_df = create_features(train_df.copy())
116
- for col in feature_cols:
117
- if col not in processed_train_df.columns:
118
- processed_train_df[col] = 0
119
- train_features = processed_train_df[feature_cols].fillna(0).values
120
- scaler = StandardScaler().fit(train_features)
121
-
122
- except FileNotFoundError as e:
123
- print(f"Error loading resources: {e}")
124
- exit()
125
-
126
- # ==========================
127
- # Prediction Logic
128
- # ==========================
129
- def predict(model_name, question, mc_answer, explanation, export_csv):
130
- model_path = os.path.join(MODELS_DIR, model_name)
131
- if not os.path.exists(model_path):
132
- return "Model not found.", None
133
-
134
- # Create DataFrame for prediction
135
- data = {
136
- 'QuestionText': [question],
137
- 'MC_Answer': [mc_answer],
138
- 'StudentExplanation': [explanation]
139
- }
140
- df = pd.DataFrame(data)
141
-
142
- # Feature engineering
143
- processed_df = create_features(df.copy())
144
- for col in feature_cols:
145
- if col not in processed_df.columns:
146
- processed_df[col] = 0
147
- features = processed_df[feature_cols].fillna(0).values
148
- features_scaled = scaler.transform(features)
149
-
150
- # Load model
151
- model = MathMisconceptionModel(
152
- n_categories=len(cat_enc.classes_),
153
- n_misconceptions=len(misc_enc.classes_),
154
- feature_dim=features_scaled.shape[1]
155
- ).to(DEVICE)
156
- model.load_state_dict(torch.load(model_path, map_location=DEVICE))
157
- model.eval()
158
-
159
- # Prediction
160
- text = processed_df['sentence'].tolist()
161
- features_tensor = torch.tensor(features_scaled, dtype=torch.float).to(DEVICE)
162
-
163
- with torch.no_grad():
164
- cat_logits, misc_logits = model(text, features_tensor)
165
- cat_pred = torch.argmax(cat_logits, 1).cpu().item()
166
- misc_pred = torch.argmax(misc_logits, 1).cpu().item()
167
-
168
- predicted_category = cat_enc.inverse_transform([cat_pred])[0]
169
- predicted_misconception = misc_enc.inverse_transform([misc_pred])[0]
170
-
171
- result_text = (
172
- f"Predicted Category: {predicted_category}\n"
173
- f"Predicted Misconception: {predicted_misconception}"
174
- )
175
-
176
- csv_path = None
177
- if export_csv:
178
- export_df = pd.DataFrame([{
179
- "Question": question,
180
- "MC_Answer": mc_answer,
181
- "Student_Explanation": explanation,
182
- "Predicted_Category": predicted_category,
183
- "Predicted_Misconception": predicted_misconception,
184
- "Model_Used": model_name
185
- }])
186
- csv_path = "predictions.csv"
187
- file_exists = os.path.isfile(csv_path)
188
- export_df.to_csv(csv_path, mode='a', header=not file_exists, index=False)
189
-
190
- return result_text, csv_path
191
-
192
- # ==========================
193
- # Gradio UI
194
- # ==========================
195
- model_files = [f for f in os.listdir(MODELS_DIR) if f.endswith('.pt')]
196
-
197
- iface = gr.Interface(
198
- fn=predict,
199
- inputs=[
200
- gr.Dropdown(model_files, value=DEFAULT_MODEL, label="Select Model"),
201
- gr.Textbox(label="Enter Question", lines=3),
202
- gr.Textbox(label="Enter Correct Answer (MC_Answer)", lines=1),
203
- gr.Textbox(label="Enter Student's Explanation", lines=5),
204
- gr.Checkbox(label="Export Prediction to CSV")
205
- ],
206
- outputs=[
207
- gr.Textbox(label="Prediction Result"),
208
- gr.File(label="CSV File (if exported)")
209
- ],
210
- title="Math Misconception Predictor",
211
- description="Select a model and provide the question, correct answer, and student's explanation to get a prediction.",
212
- theme=gr.themes.Soft()
213
- )
214
-
215
- if __name__ == "__main__":
216
- iface.launch(
217
- server_name="0.0.0.0", # Allow external connections
218
- server_port=7860, # Default Gradio port
219
- share=True, # Create public link
220
- debug=False, # Disable debug mode for production
221
- show_error=True # Show errors to users
222
- )
 
1
+ import os
2
+ import torch
3
+ import torch.nn as nn
4
+ import pandas as pd
5
+ import numpy as np
6
+ import pickle
7
+ import re
8
+ import gradio as gr
9
+ from transformers import DebertaV2Model, DebertaV2Tokenizer
10
+
11
+ # ==========================
12
+ # Configuration
13
+ # ==========================
14
+ DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
15
+ MAX_LENGTH = 256
16
+ MODELS_DIR = './models/'
17
+ CAT_ENCODER_PATH = os.path.join(MODELS_DIR, 'cat_encoder.pkl')
18
+ MISC_ENCODER_PATH = os.path.join(MODELS_DIR, 'misc_encoder.pkl')
19
+ FEATURE_COLS_PATH = os.path.join(MODELS_DIR, 'feature_cols.pkl')
20
+ DEFAULT_MODEL = 'map_2025_best_model_fold7.pt'
21
+
22
+ # ==========================
23
+ # Feature Extraction
24
+ # ==========================
25
+ def extract_math_features(text):
26
+ if not isinstance(text, str):
27
+ return {
28
+ 'frac_count': 0, 'number_count': 0, 'operator_count': 0,
29
+ 'decimal_count': 0, 'question_mark': 0, 'math_keyword_count': 0
30
+ }
31
+ features = {
32
+ 'frac_count': len(re.findall(r'FRAC_\d+_\d+|\\frac', text)),
33
+ 'number_count': len(re.findall(r'\b\d+\b', text)),
34
+ 'operator_count': len(re.findall(r'[\+\-\*\/\=]', text)),
35
+ 'decimal_count': len(re.findall(r'\d+\.\d+', text)),
36
+ 'question_mark': int('?' in text),
37
+ 'math_keyword_count': len(re.findall(r'solve|calculate|equation|fraction|decimal', text.lower()))
38
+ }
39
+ return features
40
+
41
+ def create_features(df):
42
+ for col in ['QuestionText', 'MC_Answer', 'StudentExplanation']:
43
+ df[col] = df[col].fillna('')
44
+ df['mc_answer_len'] = df['MC_Answer'].str.len()
45
+ df['explanation_len'] = df['StudentExplanation'].str.len()
46
+ df['question_len'] = df['QuestionText'].str.len()
47
+ df['explanation_to_question_ratio'] = df['explanation_len'] / (df['question_len'] + 1)
48
+ for col in ['QuestionText', 'MC_Answer', 'StudentExplanation']:
49
+ mf = df[col].apply(extract_math_features).apply(pd.Series)
50
+ prefix = 'mc_' if col == 'MC_Answer' else 'exp_' if col == 'StudentExplanation' else ''
51
+ mf.columns = [f'{prefix}{c}' for c in mf.columns]
52
+ df = pd.concat([df, mf], axis=1)
53
+ df['sentence'] = (
54
+ "Question: " + df['QuestionText'] +
55
+ " Answer: " + df['MC_Answer'] +
56
+ " Explanation: " + df['StudentExplanation']
57
+ )
58
+ return df
59
+
60
+ # ==========================
61
+ # Model Definition
62
+ # ==========================
63
+ class MathMisconceptionModel(nn.Module):
64
+ def __init__(self, n_categories, n_misconceptions, feature_dim):
65
+ super().__init__()
66
+ self.bert = DebertaV2Model.from_pretrained('microsoft/deberta-v3-small')
67
+ self.tokenizer = DebertaV2Tokenizer.from_pretrained('microsoft/deberta-v3-small')
68
+ self.feature_processor = nn.Sequential(
69
+ nn.Linear(feature_dim, 64),
70
+ nn.ReLU(),
71
+ nn.Dropout(0.3)
72
+ )
73
+ self.category_head = nn.Sequential(
74
+ nn.Linear(768 + 64, 256),
75
+ nn.ReLU(),
76
+ nn.Dropout(0.2),
77
+ nn.Linear(256, n_categories)
78
+ )
79
+ self.misconception_head = nn.Sequential(
80
+ nn.Linear(768 + 64, 256),
81
+ nn.ReLU(),
82
+ nn.Dropout(0.2),
83
+ nn.Linear(256, n_misconceptions)
84
+ )
85
+
86
+ def forward(self, input_texts, features):
87
+ tokens = self.tokenizer(
88
+ input_texts,
89
+ padding=True,
90
+ truncation=True,
91
+ max_length=MAX_LENGTH,
92
+ return_tensors="pt"
93
+ ).to(DEVICE)
94
+ outputs = self.bert(**tokens)
95
+ text_emb = outputs.last_hidden_state[:, 0, :]
96
+ feat_emb = self.feature_processor(features)
97
+ combined = torch.cat([text_emb, feat_emb], dim=1)
98
+ return self.category_head(combined), self.misconception_head(combined)
99
+
100
+ # ==========================
101
+ # Load Resources
102
+ # ==========================
103
+ try:
104
+ with open(CAT_ENCODER_PATH, 'rb') as f:
105
+ cat_enc = pickle.load(f)
106
+ with open(MISC_ENCODER_PATH, 'rb') as f:
107
+ misc_enc = pickle.load(f)
108
+ with open(FEATURE_COLS_PATH, 'rb') as f:
109
+ feature_cols = pickle.load(f)
110
+ except FileNotFoundError as e:
111
+ print(f"Error loading resources: {e}")
112
+ exit()
113
+
114
+ # Dummy scaler (no scaling)
115
+ class IdentityScaler:
116
+ def transform(self, X):
117
+ return X
118
+
119
+ scaler = IdentityScaler()
120
+
121
+ # ==========================
122
+ # Prediction Logic
123
+ # ==========================
124
+ def predict(model_name, question, mc_answer, explanation, export_csv):
125
+ model_path = os.path.join(MODELS_DIR, model_name)
126
+ if not os.path.exists(model_path):
127
+ return "Model not found.", None
128
+
129
+ data = {
130
+ 'QuestionText': [question],
131
+ 'MC_Answer': [mc_answer],
132
+ 'StudentExplanation': [explanation]
133
+ }
134
+ df = pd.DataFrame(data)
135
+
136
+ processed_df = create_features(df.copy())
137
+ for col in feature_cols:
138
+ if col not in processed_df.columns:
139
+ processed_df[col] = 0
140
+ features = processed_df[feature_cols].fillna(0).values
141
+ features_scaled = scaler.transform(features)
142
+
143
+ model = MathMisconceptionModel(
144
+ n_categories=len(cat_enc.classes_),
145
+ n_misconceptions=len(misc_enc.classes_),
146
+ feature_dim=features_scaled.shape[1]
147
+ ).to(DEVICE)
148
+
149
+ model.load_state_dict(torch.load(model_path, map_location=DEVICE))
150
+ model.eval()
151
+
152
+ text = processed_df['sentence'].tolist()
153
+ features_tensor = torch.tensor(features_scaled, dtype=torch.float).to(DEVICE)
154
+
155
+ with torch.no_grad():
156
+ cat_logits, misc_logits = model(text, features_tensor)
157
+ cat_pred = torch.argmax(cat_logits, 1).cpu().item()
158
+ misc_pred = torch.argmax(misc_logits, 1).cpu().item()
159
+
160
+ predicted_category = cat_enc.inverse_transform([cat_pred])[0]
161
+ predicted_misconception = misc_enc.inverse_transform([misc_pred])[0]
162
+
163
+ result_text = (
164
+ f"Predicted Category: {predicted_category}\n"
165
+ f"Predicted Misconception: {predicted_misconception}"
166
+ )
167
+
168
+ csv_path = None
169
+ if export_csv:
170
+ export_df = pd.DataFrame([{
171
+ "Question": question,
172
+ "MC_Answer": mc_answer,
173
+ "Student_Explanation": explanation,
174
+ "Predicted_Category": predicted_category,
175
+ "Predicted_Misconception": predicted_misconception,
176
+ "Model_Used": model_name
177
+ }])
178
+ csv_path = "predictions.csv"
179
+ file_exists = os.path.isfile(csv_path)
180
+ export_df.to_csv(csv_path, mode='a', header=not file_exists, index=False)
181
+
182
+ return result_text, csv_path
183
+
184
+ # ==========================
185
+ # Gradio UI
186
+ # ==========================
187
+ model_files = [f for f in os.listdir(MODELS_DIR) if f.endswith('.pt')]
188
+
189
+ iface = gr.Interface(
190
+ fn=predict,
191
+ inputs=[
192
+ gr.Dropdown(model_files, value=DEFAULT_MODEL, label="Select Model"),
193
+ gr.Textbox(label="Enter Question", lines=3),
194
+ gr.Textbox(label="Enter Correct Answer (MC_Answer)", lines=1),
195
+ gr.Textbox(label="Enter Student's Explanation", lines=5),
196
+ gr.Checkbox(label="Export Prediction to CSV")
197
+ ],
198
+ outputs=[
199
+ gr.Textbox(label="Prediction Result"),
200
+ gr.File(label="CSV File (if exported)")
201
+ ],
202
+ title="Math Misconception Predictor",
203
+ description="Select a model and provide the question, correct answer, and student's explanation to get a prediction.",
204
+ theme=gr.themes.Soft()
205
+ )
206
+
207
+ if __name__ == "__main__":
208
+ iface.launch(
209
+ server_name="0.0.0.0",
210
+ server_port=7860,
211
+ share=True,
212
+ debug=False,
213
+ show_error=True
214
+ )