nonzeroexit commited on
Commit
5b6b7cd
·
verified ·
1 Parent(s): 32d795d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +1118 -136
app.py CHANGED
@@ -2,188 +2,1170 @@ import gradio as gr
2
  import joblib
3
  import numpy as np
4
  import pandas as pd
5
- from propy import AAComposition, Autocorrelation, CTD, PseudoAAC
6
  import torch
 
 
7
  from transformers import BertTokenizer, BertModel
8
  from lime.lime_tabular import LimeTabularExplainer
9
  from math import expm1
10
 
11
- # Load AMP Classifier and Scaler
12
- model = joblib.load("RF.joblib")
13
- scaler = joblib.load("norm (4).joblib")
14
-
15
- # Load ProtBert
16
- tokenizer = BertTokenizer.from_pretrained("Rostlab/prot_bert", do_lower_case=False)
17
- protbert_model = BertModel.from_pretrained("Rostlab/prot_bert")
18
- device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
19
- protbert_model = protbert_model.to(device).eval()
20
-
21
- # Define selected features (146 RFE-selected features)
22
- selected_features = ["_SolventAccessibilityC3", "_SecondaryStrC1", "_SecondaryStrC3", "_ChargeC1", "_PolarityC1",
23
- "_NormalizedVDWVC1", "_HydrophobicityC3", "_SecondaryStrT23", "_PolarizabilityD1001", "_PolarizabilityD2001",
24
- "_PolarizabilityD3001", "_SolventAccessibilityD1001", "_SolventAccessibilityD2001", "_SolventAccessibilityD3001",
25
- "_SecondaryStrD1001", "_SecondaryStrD1075", "_SecondaryStrD2001", "_SecondaryStrD3001", "_ChargeD1001",
26
- "_ChargeD1025", "_ChargeD2001", "_ChargeD3075", "_ChargeD3100", "_PolarityD1001", "_PolarityD1050",
27
- "_PolarityD2001", "_PolarityD3001", "_NormalizedVDWVD1001", "_NormalizedVDWVD2001", "_NormalizedVDWVD2025",
28
- "_NormalizedVDWVD2050", "_NormalizedVDWVD3001", "_HydrophobicityD1001", "_HydrophobicityD2001",
29
- "_HydrophobicityD3001", "_HydrophobicityD3025", "A", "R", "D", "C", "E", "Q", "H", "I", "M", "P", "Y", "V",
30
- "AR", "AV", "RC", "RL", "RV", "CR", "CC", "CL", "CK", "EE", "EI", "EL", "HC", "IA", "IL", "IV", "LA", "LC", "LE",
31
- "LI", "LT", "LV", "KC", "MA", "MS", "SC", "TC", "TV", "YC", "VC", "VE", "VL", "VK", "VV",
32
- "MoreauBrotoAuto_FreeEnergy30", "MoranAuto_Hydrophobicity2", "MoranAuto_Hydrophobicity4",
33
- "GearyAuto_Hydrophobicity20", "GearyAuto_Hydrophobicity24", "GearyAuto_Hydrophobicity26",
34
- "GearyAuto_Hydrophobicity27", "GearyAuto_Hydrophobicity28", "GearyAuto_Hydrophobicity29",
35
- "GearyAuto_Hydrophobicity30", "GearyAuto_AvFlexibility22", "GearyAuto_AvFlexibility26",
36
- "GearyAuto_AvFlexibility27", "GearyAuto_AvFlexibility28", "GearyAuto_AvFlexibility29", "GearyAuto_AvFlexibility30",
37
- "GearyAuto_Polarizability22", "GearyAuto_Polarizability24", "GearyAuto_Polarizability25",
38
- "GearyAuto_Polarizability27", "GearyAuto_Polarizability28", "GearyAuto_Polarizability29",
39
- "GearyAuto_Polarizability30", "GearyAuto_FreeEnergy24", "GearyAuto_FreeEnergy25", "GearyAuto_FreeEnergy30",
40
- "GearyAuto_ResidueASA21", "GearyAuto_ResidueASA22", "GearyAuto_ResidueASA23", "GearyAuto_ResidueASA24",
41
- "GearyAuto_ResidueASA30", "GearyAuto_ResidueVol21", "GearyAuto_ResidueVol24", "GearyAuto_ResidueVol25",
42
- "GearyAuto_ResidueVol26", "GearyAuto_ResidueVol28", "GearyAuto_ResidueVol29", "GearyAuto_ResidueVol30",
43
- "GearyAuto_Steric18", "GearyAuto_Steric21", "GearyAuto_Steric26", "GearyAuto_Steric27", "GearyAuto_Steric28",
44
- "GearyAuto_Steric29", "GearyAuto_Steric30", "GearyAuto_Mutability23", "GearyAuto_Mutability25",
45
- "GearyAuto_Mutability26", "GearyAuto_Mutability27", "GearyAuto_Mutability28", "GearyAuto_Mutability29",
46
- "GearyAuto_Mutability30", "APAAC1", "APAAC4", "APAAC5", "APAAC6", "APAAC8", "APAAC9", "APAAC12", "APAAC13",
47
- "APAAC15", "APAAC18", "APAAC19", "APAAC24"]
48
-
49
- # --- FIX (LIME): seed the random background so explanations are reproducible
50
- # across Space restarts. (Loading a real saved training sample here would
51
- # produce more faithful weights; see build_lime_background.py for that path.)
52
- np.random.seed(42)
53
- sample_data = np.random.rand(500, len(selected_features))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  explainer = LimeTabularExplainer(
55
- training_data=sample_data,
 
 
56
  feature_names=selected_features,
57
- class_names=["AMP", "Non-AMP"],
 
 
 
 
 
58
  mode="classification",
59
- random_state=42,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  )
61
 
62
- # Feature extraction function
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  def extract_features(sequence):
64
- sequence = ''.join([aa for aa in sequence.upper() if aa in "ACDEFGHIKLMNPQRSTVWY"])
 
 
 
 
 
65
  if len(sequence) < 10:
66
- return "Error: Sequence too short."
 
 
 
 
 
67
 
68
  try:
69
- dipeptide_features = AAComposition.CalculateAADipeptideComposition(sequence)
70
- filtered_dipeptide_features = {k: dipeptide_features[k] for k in list(dipeptide_features.keys())[:420]}
71
- ctd_features = CTD.CalculateCTD(sequence)
72
- auto_features = Autocorrelation.CalculateAutoTotal(sequence)
73
- pseudo_features = PseudoAAC.GetAPseudoAAC(sequence, lamda=9)
74
-
75
- all_features_dict = {}
76
- all_features_dict.update(ctd_features)
77
- all_features_dict.update(filtered_dipeptide_features)
78
- all_features_dict.update(auto_features)
79
- all_features_dict.update(pseudo_features)
80
-
81
- feature_df_all = pd.DataFrame([all_features_dict])
82
- normalized_array = scaler.transform(feature_df_all.values)
83
- normalized_df = pd.DataFrame(normalized_array, columns=feature_df_all.columns)
84
-
85
- if not set(selected_features).issubset(normalized_df.columns):
86
- return "Error: Some selected features are missing."
87
-
88
- selected_df = normalized_df[selected_features].fillna(0)
89
- return selected_df.values
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  except Exception as e:
91
- return f"Error in feature extraction: {str(e)}"
92
 
93
- # MIC prediction function
94
- def predictmic(sequence):
95
- sequence = ''.join([aa for aa in sequence.upper() if aa in "ACDEFGHIKLMNPQRSTVWY"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  if len(sequence) < 10:
97
- return {"Error": "Sequence too short or invalid."}
98
 
99
- seq_spaced = ' '.join(list(sequence))
100
- tokens = tokenizer(seq_spaced, return_tensors="pt", padding='max_length', truncation=True, max_length=512)
101
- tokens = {k: v.to(device) for k, v in tokens.items()}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
 
103
  with torch.no_grad():
104
- outputs = protbert_model(**tokens)
105
- embedding = outputs.last_hidden_state.mean(dim=1).squeeze().cpu().numpy().reshape(1, -1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
 
107
  bacteria_config = {
108
- "E.coli": {"model": "coli_xgboost_model.pkl", "scaler": "coli_scaler.pkl", "pca": None},
109
- "S.aureus": {"model": "aur_xgboost_model.pkl", "scaler": "aur_scaler.pkl", "pca": None},
110
- "P.aeruginosa": {"model": "arg_xgboost_model.pkl", "scaler": "arg_scaler.pkl", "pca": None},
111
- "K.Pneumonia": {"model": "pne_mlp_model.pkl", "scaler": "pne_scaler.pkl", "pca": "pne_pca.pkl"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  }
113
 
 
114
  mic_results = {}
115
- for bacterium, cfg in bacteria_config.items():
 
 
 
 
 
116
  try:
117
- # --- FIX (variable shadowing): renamed locals so the global `scaler`
118
- # and `model` (the AMP RF + its MinMax scaler) are NEVER overwritten.
119
- # The original code reused the names `scaler` and `model` here, which
120
- # silently broke the AMP classifier on every prediction after the
121
- # first MIC run.
122
- mic_scaler = joblib.load(cfg["scaler"])
123
- scaled = mic_scaler.transform(embedding)
124
- transformed = joblib.load(cfg["pca"]).transform(scaled) if cfg["pca"] else scaled
125
- mic_model = joblib.load(cfg["model"])
126
- mic_log = mic_model.predict(transformed)[0]
127
- mic = round(expm1(mic_log), 3)
128
- mic_results[bacterium] = mic
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  except Exception as e:
130
- mic_results[bacterium] = f"Error: {str(e)}"
 
 
 
 
 
 
131
 
132
  return mic_results
133
 
134
- # Main prediction function
 
 
 
 
135
  def full_prediction(sequence):
136
- features = extract_features(sequence)
137
- if isinstance(features, str):
138
- return features
139
 
140
- prediction = model.predict(features)[0]
141
- probabilities = model.predict_proba(features)[0]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
 
143
  try:
144
- class_index = list(model.classes_).index(prediction)
145
- confidence = round(probabilities[class_index] * 100, 2)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
  except Exception:
147
- confidence = "Unknown"
148
 
149
- amp_result = "Antimicrobial Peptide (AMP)" if prediction == 0 else "Non-AMP"
150
- result = f"Prediction: {amp_result}\nConfidence: {confidence}%\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
 
152
- # --- LIME first (per spec: LIME before SHAP in the HTML report).
153
- # explain_instance perturbs THIS single input sequence's feature row 2000
154
- # times and fits a local linear model; weights describe this specific input.
155
  try:
156
- explanation = explainer.explain_instance(
157
- data_row=features[0],
158
- predict_fn=model.predict_proba,
159
- num_features=10,
160
- num_samples=2000,
161
- )
162
- result += "\nTop Features Influencing Prediction (LIME):\n"
163
- for feat, weight in explanation.as_list():
164
- result += f"- {feat}: {round(weight, 4)}\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
  except Exception as e:
166
- result += f"\nLIME explanation failed: {str(e)}\n"
 
 
 
 
 
 
 
 
 
167
 
168
  if prediction == 0:
169
- mic_values = predictmic(sequence)
170
- result += "\nPredicted MIC Values (μM):\n"
171
- for org, mic in mic_values.items():
172
- result += f"- {org}: {mic}\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
  else:
174
- result += "\nMIC prediction skipped for Non-AMP sequences.\n"
 
 
 
 
 
175
 
176
  return result
177
 
178
- # Gradio UI
 
 
 
 
179
  iface = gr.Interface(
 
180
  fn=full_prediction,
181
- inputs=gr.Textbox(label="Enter Protein Sequence"),
182
- outputs=gr.Textbox(label="Results"),
183
- title="AMP & MIC Predictor + LIME Explanation",
184
- description="Paste an amino acid sequence (≥10 characters). Get AMP classification, MIC predictions, and LIME interpretability insights."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  )
186
 
187
- # --- FIX (launch): removed share=True. On Hugging Face Spaces the public URL
188
- # is provided by the platform; share=True is for local dev only.
189
- iface.launch()
 
 
 
 
 
 
2
  import joblib
3
  import numpy as np
4
  import pandas as pd
 
5
  import torch
6
+
7
+ from propy import AAComposition, Autocorrelation, CTD, PseudoAAC
8
  from transformers import BertTokenizer, BertModel
9
  from lime.lime_tabular import LimeTabularExplainer
10
  from math import expm1
11
 
12
+
13
+ # ============================================================
14
+ # CONFIG
15
+ # ============================================================
16
+
17
+ MODEL_PATH = "RF.joblib"
18
+ SCALER_PATH = "norm (4).joblib"
19
+
20
+ # REAL training data used by the 146-feature classifier
21
+ LIME_DATA_PATH = "lime_training_data.csv"
22
+
23
+
24
+ # ============================================================
25
+ # LOAD CLASSIFIER
26
+ # ============================================================
27
+
28
+ print("Loading Random Forest...")
29
+
30
+ model = joblib.load(MODEL_PATH)
31
+
32
+ print(
33
+ "Random Forest loaded."
34
+ )
35
+
36
+ print(
37
+ "RF expects:",
38
+ getattr(model, "n_features_in_", "unknown"),
39
+ "features"
40
+ )
41
+
42
+
43
+ # ============================================================
44
+ # LOAD SCALER
45
+ # ============================================================
46
+
47
+ print("Loading scaler...")
48
+
49
+ amp_scaler = joblib.load(SCALER_PATH)
50
+
51
+ print(
52
+ "Scaler loaded."
53
+ )
54
+
55
+ print(
56
+ "Scaler expects:",
57
+ getattr(
58
+ amp_scaler,
59
+ "n_features_in_",
60
+ "unknown"
61
+ ),
62
+ "features"
63
+ )
64
+
65
+
66
+ # ============================================================
67
+ # YOUR 146 FEATURES
68
+ # ============================================================
69
+
70
+ selected_features = [
71
+ "_SolventAccessibilityC3",
72
+ "_SecondaryStrC1",
73
+ "_SecondaryStrC3",
74
+ "_ChargeC1",
75
+ "_PolarityC1",
76
+ "_NormalizedVDWVC1",
77
+ "_HydrophobicityC3",
78
+ "_SecondaryStrT23",
79
+ "_PolarizabilityD1001",
80
+ "_PolarizabilityD2001",
81
+ "_PolarizabilityD3001",
82
+ "_SolventAccessibilityD1001",
83
+ "_SolventAccessibilityD2001",
84
+ "_SolventAccessibilityD3001",
85
+ "_SecondaryStrD1001",
86
+ "_SecondaryStrD1075",
87
+ "_SecondaryStrD2001",
88
+ "_SecondaryStrD3001",
89
+ "_ChargeD1001",
90
+ "_ChargeD1025",
91
+ "_ChargeD2001",
92
+ "_ChargeD3075",
93
+ "_ChargeD3100",
94
+ "_PolarityD1001",
95
+ "_PolarityD1050",
96
+ "_PolarityD2001",
97
+ "_PolarityD3001",
98
+ "_NormalizedVDWVD1001",
99
+ "_NormalizedVDWVD2001",
100
+ "_NormalizedVDWVD2025",
101
+ "_NormalizedVDWVD2050",
102
+ "_NormalizedVDWVD3001",
103
+ "_HydrophobicityD1001",
104
+ "_HydrophobicityD2001",
105
+ "_HydrophobicityD3001",
106
+ "_HydrophobicityD3025",
107
+
108
+ "A",
109
+ "R",
110
+ "D",
111
+ "C",
112
+ "E",
113
+ "Q",
114
+ "H",
115
+ "I",
116
+ "M",
117
+ "P",
118
+ "Y",
119
+ "V",
120
+
121
+ "AR",
122
+ "AV",
123
+ "RC",
124
+ "RL",
125
+ "RV",
126
+ "CR",
127
+ "CC",
128
+ "CL",
129
+ "CK",
130
+ "EE",
131
+ "EI",
132
+ "EL",
133
+ "HC",
134
+ "IA",
135
+ "IL",
136
+ "IV",
137
+ "LA",
138
+ "LC",
139
+ "LE",
140
+ "LI",
141
+ "LT",
142
+ "LV",
143
+ "KC",
144
+ "MA",
145
+ "MS",
146
+ "SC",
147
+ "TC",
148
+ "TV",
149
+ "YC",
150
+ "VC",
151
+ "VE",
152
+ "VL",
153
+ "VK",
154
+ "VV",
155
+
156
+ "MoreauBrotoAuto_FreeEnergy30",
157
+
158
+ "MoranAuto_Hydrophobicity2",
159
+ "MoranAuto_Hydrophobicity4",
160
+
161
+ "GearyAuto_Hydrophobicity20",
162
+ "GearyAuto_Hydrophobicity24",
163
+ "GearyAuto_Hydrophobicity26",
164
+ "GearyAuto_Hydrophobicity27",
165
+ "GearyAuto_Hydrophobicity28",
166
+ "GearyAuto_Hydrophobicity29",
167
+ "GearyAuto_Hydrophobicity30",
168
+
169
+ "GearyAuto_AvFlexibility22",
170
+ "GearyAuto_AvFlexibility26",
171
+ "GearyAuto_AvFlexibility27",
172
+ "GearyAuto_AvFlexibility28",
173
+ "GearyAuto_AvFlexibility29",
174
+ "GearyAuto_AvFlexibility30",
175
+
176
+ "GearyAuto_Polarizability22",
177
+ "GearyAuto_Polarizability24",
178
+ "GearyAuto_Polarizability25",
179
+ "GearyAuto_Polarizability27",
180
+ "GearyAuto_Polarizability28",
181
+ "GearyAuto_Polarizability29",
182
+ "GearyAuto_Polarizability30",
183
+
184
+ "GearyAuto_FreeEnergy24",
185
+ "GearyAuto_FreeEnergy25",
186
+ "GearyAuto_FreeEnergy30",
187
+
188
+ "GearyAuto_ResidueASA21",
189
+ "GearyAuto_ResidueASA22",
190
+ "GearyAuto_ResidueASA23",
191
+ "GearyAuto_ResidueASA24",
192
+ "GearyAuto_ResidueASA30",
193
+
194
+ "GearyAuto_ResidueVol21",
195
+ "GearyAuto_ResidueVol24",
196
+ "GearyAuto_ResidueVol25",
197
+ "GearyAuto_ResidueVol26",
198
+ "GearyAuto_ResidueVol28",
199
+ "GearyAuto_ResidueVol29",
200
+ "GearyAuto_ResidueVol30",
201
+
202
+ "GearyAuto_Steric18",
203
+ "GearyAuto_Steric21",
204
+ "GearyAuto_Steric26",
205
+ "GearyAuto_Steric27",
206
+ "GearyAuto_Steric28",
207
+ "GearyAuto_Steric29",
208
+ "GearyAuto_Steric30",
209
+
210
+ "GearyAuto_Mutability23",
211
+ "GearyAuto_Mutability25",
212
+ "GearyAuto_Mutability26",
213
+ "GearyAuto_Mutability27",
214
+ "GearyAuto_Mutability28",
215
+ "GearyAuto_Mutability29",
216
+ "GearyAuto_Mutability30",
217
+
218
+ "APAAC1",
219
+ "APAAC4",
220
+ "APAAC5",
221
+ "APAAC6",
222
+ "APAAC8",
223
+ "APAAC9",
224
+ "APAAC12",
225
+ "APAAC13",
226
+ "APAAC15",
227
+ "APAAC18",
228
+ "APAAC19",
229
+ "APAAC24"
230
+ ]
231
+
232
+
233
+ print(
234
+ "Selected feature count:",
235
+ len(selected_features)
236
+ )
237
+
238
+
239
+ # ============================================================
240
+ # VERIFY CLASSIFIER
241
+ # ============================================================
242
+
243
+ if hasattr(model, "n_features_in_"):
244
+
245
+ if model.n_features_in_ != len(selected_features):
246
+
247
+ raise ValueError(
248
+ "\n\nMODEL / FEATURE MISMATCH\n"
249
+ f"RF expects {model.n_features_in_} features.\n"
250
+ f"Your selected feature list contains "
251
+ f"{len(selected_features)} features.\n\n"
252
+ "Do not continue until these match."
253
+ )
254
+
255
+
256
+ # ============================================================
257
+ # LOAD REAL LIME TRAINING DATA
258
+ # ============================================================
259
+
260
+ print(
261
+ "\nLoading REAL training data for LIME..."
262
+ )
263
+
264
+ training_df = pd.read_csv(
265
+ LIME_DATA_PATH
266
+ )
267
+
268
+
269
+ # Remove accidental pandas index
270
+ if "Unnamed: 0" in training_df.columns:
271
+
272
+ training_df = training_df.drop(
273
+ "Unnamed: 0",
274
+ axis=1
275
+ )
276
+
277
+
278
+ print(
279
+ "Training CSV shape:",
280
+ training_df.shape
281
+ )
282
+
283
+
284
+ # ============================================================
285
+ # CHECK ALL 146 FEATURES EXIST
286
+ # ============================================================
287
+
288
+ missing_features = [
289
+ feature
290
+ for feature in selected_features
291
+ if feature not in training_df.columns
292
+ ]
293
+
294
+
295
+ if missing_features:
296
+
297
+ raise ValueError(
298
+ "\n\nLIME TRAINING DATA IS MISSING FEATURES:\n"
299
+ + "\n".join(
300
+ missing_features[:20]
301
+ )
302
+ )
303
+
304
+
305
+ # ============================================================
306
+ # PREPARE REAL LIME DATA
307
+ # ============================================================
308
+
309
+ lime_training_df = (
310
+ training_df[
311
+ selected_features
312
+ ]
313
+ .copy()
314
+ )
315
+
316
+
317
+ # Convert to numbers
318
+ lime_training_df = (
319
+ lime_training_df.apply(
320
+ pd.to_numeric,
321
+ errors="coerce"
322
+ )
323
+ )
324
+
325
+
326
+ # Missing numerical values
327
+ lime_training_df = (
328
+ lime_training_df.fillna(0)
329
+ )
330
+
331
+
332
+ # Convert to numpy
333
+ lime_training_data = (
334
+ lime_training_df
335
+ .values
336
+ .astype(np.float64)
337
+ )
338
+
339
+
340
+ print(
341
+ "Real LIME data:",
342
+ lime_training_data.shape
343
+ )
344
+
345
+
346
+ # ============================================================
347
+ # LIMIT LIME BACKGROUND
348
+ # ============================================================
349
+
350
+ # This is NOT generating artificial data.
351
+ # It only selects real rows from your training dataset.
352
+
353
+ MAX_LIME_BACKGROUND = 1000
354
+
355
+
356
+ if len(lime_training_data) > MAX_LIME_BACKGROUND:
357
+
358
+ rng = np.random.RandomState(42)
359
+
360
+ indices = rng.choice(
361
+ len(lime_training_data),
362
+ MAX_LIME_BACKGROUND,
363
+ replace=False
364
+ )
365
+
366
+ lime_training_data = (
367
+ lime_training_data[
368
+ indices
369
+ ]
370
+ )
371
+
372
+
373
+ print(
374
+ "LIME background:",
375
+ lime_training_data.shape
376
+ )
377
+
378
+
379
+ # ============================================================
380
+ # CREATE LIME EXPLAINER
381
+ # ============================================================
382
+
383
  explainer = LimeTabularExplainer(
384
+
385
+ training_data=lime_training_data,
386
+
387
  feature_names=selected_features,
388
+
389
+ class_names=[
390
+ "AMP",
391
+ "Non-AMP"
392
+ ],
393
+
394
  mode="classification",
395
+
396
+ discretize_continuous=True,
397
+
398
+ random_state=42
399
+ )
400
+
401
+
402
+ print(
403
+ "REAL LIME EXPLAINER READY."
404
+ )
405
+
406
+
407
+ # ============================================================
408
+ # LOAD PROTBERT
409
+ # ============================================================
410
+
411
+ print(
412
+ "\nLoading ProtBert..."
413
+ )
414
+
415
+
416
+ tokenizer = BertTokenizer.from_pretrained(
417
+ "Rostlab/prot_bert",
418
+ do_lower_case=False
419
+ )
420
+
421
+
422
+ protbert_model = BertModel.from_pretrained(
423
+ "Rostlab/prot_bert"
424
+ )
425
+
426
+
427
+ device = torch.device(
428
+ "cuda"
429
+ if torch.cuda.is_available()
430
+ else "cpu"
431
+ )
432
+
433
+
434
+ protbert_model = (
435
+ protbert_model
436
+ .to(device)
437
+ .eval()
438
  )
439
 
440
+
441
+ print(
442
+ "ProtBert loaded on:",
443
+ device
444
+ )
445
+
446
+
447
+ # ============================================================
448
+ # VALID AMINO ACIDS
449
+ # ============================================================
450
+
451
+ VALID_AA = set(
452
+ "ACDEFGHIKLMNPQRSTVWY"
453
+ )
454
+
455
+
456
+ # ============================================================
457
+ # CLEAN SEQUENCE
458
+ # ============================================================
459
+
460
+ def clean_sequence(sequence):
461
+
462
+ sequence = str(
463
+ sequence
464
+ ).upper().strip()
465
+
466
+
467
+ sequence = "".join(
468
+ aa
469
+ for aa in sequence
470
+ if aa in VALID_AA
471
+ )
472
+
473
+
474
+ return sequence
475
+
476
+
477
+ # ============================================================
478
+ # FEATURE EXTRACTION
479
+ # ============================================================
480
+
481
  def extract_features(sequence):
482
+
483
+ sequence = clean_sequence(
484
+ sequence
485
+ )
486
+
487
+
488
  if len(sequence) < 10:
489
+
490
+ return (
491
+ None,
492
+ "Sequence must contain at least 10 valid amino acids."
493
+ )
494
+
495
 
496
  try:
497
+
498
+ # ----------------------------------------------------
499
+ # DIPEPTIDE
500
+ # ----------------------------------------------------
501
+
502
+ dipeptide_features = (
503
+ AAComposition.CalculateAADipeptideComposition(
504
+ sequence
505
+ )
506
+ )
507
+
508
+
509
+ filtered_dipeptide_features = {
510
+
511
+ k: dipeptide_features[k]
512
+
513
+ for k in list(
514
+ dipeptide_features.keys()
515
+ )[:420]
516
+ }
517
+
518
+
519
+ # ----------------------------------------------------
520
+ # CTD
521
+ # ----------------------------------------------------
522
+
523
+ ctd_features = (
524
+ CTD.CalculateCTD(
525
+ sequence
526
+ )
527
+ )
528
+
529
+
530
+ # ----------------------------------------------------
531
+ # AUTOCORRELATION
532
+ # ----------------------------------------------------
533
+
534
+ auto_features = (
535
+ Autocorrelation.CalculateAutoTotal(
536
+ sequence
537
+ )
538
+ )
539
+
540
+
541
+ # ----------------------------------------------------
542
+ # PSEUDO AAC
543
+ # ----------------------------------------------------
544
+
545
+ pseudo_features = (
546
+ PseudoAAC.GetAPseudoAAC(
547
+ sequence,
548
+ lamda=9
549
+ )
550
+ )
551
+
552
+
553
+ # ----------------------------------------------------
554
+ # COMBINE
555
+ # ----------------------------------------------------
556
+
557
+ all_features = {}
558
+
559
+ all_features.update(
560
+ ctd_features
561
+ )
562
+
563
+ all_features.update(
564
+ filtered_dipeptide_features
565
+ )
566
+
567
+ all_features.update(
568
+ auto_features
569
+ )
570
+
571
+ all_features.update(
572
+ pseudo_features
573
+ )
574
+
575
+
576
+ feature_df = pd.DataFrame(
577
+ [all_features]
578
+ )
579
+
580
+
581
+ # ----------------------------------------------------
582
+ # ALIGN WITH SCALER
583
+ # ----------------------------------------------------
584
+
585
+ if hasattr(
586
+ amp_scaler,
587
+ "feature_names_in_"
588
+ ):
589
+
590
+ scaler_features = list(
591
+ amp_scaler.feature_names_in_
592
+ )
593
+
594
+
595
+ missing_scaler = [
596
+ f
597
+ for f in scaler_features
598
+ if f not in feature_df.columns
599
+ ]
600
+
601
+
602
+ if missing_scaler:
603
+
604
+ return (
605
+ None,
606
+ "Missing features required by scaler: "
607
+ + str(
608
+ missing_scaler[:10]
609
+ )
610
+ )
611
+
612
+
613
+ feature_df = (
614
+ feature_df[
615
+ scaler_features
616
+ ]
617
+ )
618
+
619
+
620
+ # ----------------------------------------------------
621
+ # NORMALIZATION
622
+ # ----------------------------------------------------
623
+
624
+ normalized_array = (
625
+ amp_scaler.transform(
626
+ feature_df
627
+ )
628
+ )
629
+
630
+
631
+ normalized_df = pd.DataFrame(
632
+ normalized_array,
633
+ columns=feature_df.columns
634
+ )
635
+
636
+
637
+ # ----------------------------------------------------
638
+ # SELECT THE SAME 146 FEATURES
639
+ # ----------------------------------------------------
640
+
641
+ missing_selected = [
642
+ f
643
+ for f in selected_features
644
+ if f not in normalized_df.columns
645
+ ]
646
+
647
+
648
+ if missing_selected:
649
+
650
+ return (
651
+ None,
652
+ "Missing selected features: "
653
+ + str(
654
+ missing_selected[:10]
655
+ )
656
+ )
657
+
658
+
659
+ selected_df = (
660
+ normalized_df[
661
+ selected_features
662
+ ]
663
+ .fillna(0)
664
+ )
665
+
666
+
667
+ X = (
668
+ selected_df
669
+ .values
670
+ .astype(np.float64)
671
+ )
672
+
673
+
674
+ # Final check
675
+ if X.shape[1] != len(
676
+ selected_features
677
+ ):
678
+
679
+ return (
680
+ None,
681
+ f"Feature count error: "
682
+ f"{X.shape[1]} generated."
683
+ )
684
+
685
+
686
+ return X, None
687
+
688
+
689
  except Exception as e:
 
690
 
691
+ return (
692
+ None,
693
+ f"{type(e).__name__}: {str(e)}"
694
+ )
695
+
696
+
697
+ # ============================================================
698
+ # MIC PREDICTION
699
+ # ============================================================
700
+
701
+ def predict_mic(sequence):
702
+
703
+ sequence = clean_sequence(
704
+ sequence
705
+ )
706
+
707
+
708
  if len(sequence) < 10:
 
709
 
710
+ return {
711
+ "Error": "Sequence too short."
712
+ }
713
+
714
+
715
+ # --------------------------------------------------------
716
+ # PROTBERT
717
+ # --------------------------------------------------------
718
+
719
+ seq_spaced = " ".join(
720
+ list(sequence)
721
+ )
722
+
723
+
724
+ tokens = tokenizer(
725
+ seq_spaced,
726
+ return_tensors="pt",
727
+ padding="max_length",
728
+ truncation=True,
729
+ max_length=512
730
+ )
731
+
732
+
733
+ tokens = {
734
+ k: v.to(device)
735
+ for k, v in tokens.items()
736
+ }
737
+
738
 
739
  with torch.no_grad():
740
+
741
+ outputs = (
742
+ protbert_model(
743
+ **tokens
744
+ )
745
+ )
746
+
747
+
748
+ embedding = (
749
+ outputs
750
+ .last_hidden_state
751
+ .mean(dim=1)
752
+ .squeeze()
753
+ .cpu()
754
+ .numpy()
755
+ .reshape(1, -1)
756
+ )
757
+
758
+
759
+ # --------------------------------------------------------
760
+ # BACTERIAL MODELS
761
+ # --------------------------------------------------------
762
 
763
  bacteria_config = {
764
+
765
+ "E. coli": {
766
+ "model":
767
+ "coli_xgboost_model.pkl",
768
+
769
+ "scaler":
770
+ "coli_scaler.pkl",
771
+
772
+ "pca":
773
+ None
774
+ },
775
+
776
+
777
+ "S. aureus": {
778
+ "model":
779
+ "aur_xgboost_model.pkl",
780
+
781
+ "scaler":
782
+ "aur_scaler.pkl",
783
+
784
+ "pca":
785
+ None
786
+ },
787
+
788
+
789
+ "P. aeruginosa": {
790
+ "model":
791
+ "arg_xgboost_model.pkl",
792
+
793
+ "scaler":
794
+ "arg_scaler.pkl",
795
+
796
+ "pca":
797
+ None
798
+ },
799
+
800
+
801
+ "K. pneumoniae": {
802
+ "model":
803
+ "pne_mlp_model.pkl",
804
+
805
+ "scaler":
806
+ "pne_scaler.pkl",
807
+
808
+ "pca":
809
+ "pne_pca.pkl"
810
+ }
811
  }
812
 
813
+
814
  mic_results = {}
815
+
816
+
817
+ for bacterium, config in (
818
+ bacteria_config.items()
819
+ ):
820
+
821
  try:
822
+
823
+ mic_scaler = joblib.load(
824
+ config["scaler"]
825
+ )
826
+
827
+
828
+ mic_model = joblib.load(
829
+ config["model"]
830
+ )
831
+
832
+
833
+ scaled = (
834
+ mic_scaler.transform(
835
+ embedding
836
+ )
837
+ )
838
+
839
+
840
+ if config["pca"]:
841
+
842
+ mic_pca = joblib.load(
843
+ config["pca"]
844
+ )
845
+
846
+
847
+ transformed = (
848
+ mic_pca.transform(
849
+ scaled
850
+ )
851
+ )
852
+
853
+ else:
854
+
855
+ transformed = scaled
856
+
857
+
858
+ mic_log = (
859
+ mic_model.predict(
860
+ transformed
861
+ )[0]
862
+ )
863
+
864
+
865
+ mic = round(
866
+ expm1(mic_log),
867
+ 3
868
+ )
869
+
870
+
871
+ mic_results[
872
+ bacterium
873
+ ] = mic
874
+
875
+
876
  except Exception as e:
877
+
878
+ mic_results[
879
+ bacterium
880
+ ] = (
881
+ f"Error: {str(e)}"
882
+ )
883
+
884
 
885
  return mic_results
886
 
887
+
888
+ # ============================================================
889
+ # FULL PREDICTION
890
+ # ============================================================
891
+
892
  def full_prediction(sequence):
 
 
 
893
 
894
+ sequence = clean_sequence(
895
+ sequence
896
+ )
897
+
898
+
899
+ if len(sequence) < 10:
900
+
901
+ return (
902
+ "Invalid sequence.\n\n"
903
+ "Please enter at least 10 valid "
904
+ "amino acids."
905
+ )
906
+
907
+
908
+ # ========================================================
909
+ # EXTRACT FEATURES
910
+ # ========================================================
911
+
912
+ features, error = (
913
+ extract_features(
914
+ sequence
915
+ )
916
+ )
917
+
918
+
919
+ if error:
920
+
921
+ return (
922
+ "Feature extraction failed:\n\n"
923
+ + error
924
+ )
925
+
926
+
927
+ # ========================================================
928
+ # RANDOM FOREST PREDICTION
929
+ # ========================================================
930
+
931
+ try:
932
+
933
+ prediction = (
934
+ model.predict(
935
+ features
936
+ )[0]
937
+ )
938
+
939
+
940
+ probabilities = (
941
+ model.predict_proba(
942
+ features
943
+ )[0]
944
+ )
945
+
946
+
947
+ except Exception as e:
948
+
949
+ return (
950
+ "AMP prediction failed:\n\n"
951
+ f"{type(e).__name__}: {e}"
952
+ )
953
+
954
+
955
+ # ========================================================
956
+ # CONFIDENCE
957
+ # ========================================================
958
 
959
  try:
960
+
961
+ class_index = list(
962
+ model.classes_
963
+ ).index(
964
+ prediction
965
+ )
966
+
967
+
968
+ confidence = round(
969
+ probabilities[
970
+ class_index
971
+ ] * 100,
972
+ 2
973
+ )
974
+
975
+
976
  except Exception:
 
977
 
978
+ confidence = None
979
+
980
+
981
+ # ========================================================
982
+ # AMP LABEL
983
+ # ========================================================
984
+
985
+ if prediction == 0:
986
+
987
+ amp_result = (
988
+ "Antimicrobial Peptide (AMP)"
989
+ )
990
+
991
+ else:
992
+
993
+ amp_result = (
994
+ "Non-AMP"
995
+ )
996
+
997
+
998
+ result = (
999
+ "====================================\n"
1000
+ "AMP CLASSIFICATION\n"
1001
+ "====================================\n\n"
1002
+ f"Prediction: {amp_result}\n"
1003
+ f"Confidence: {confidence}%\n"
1004
+ )
1005
+
1006
+
1007
+ # ========================================================
1008
+ # LIME
1009
+ # ========================================================
1010
+
1011
+ result += (
1012
+ "\n====================================\n"
1013
+ "LIME INTERPRETABILITY\n"
1014
+ "====================================\n"
1015
+ )
1016
+
1017
 
 
 
 
1018
  try:
1019
+
1020
+ # Reproducibility
1021
+ np.random.seed(42)
1022
+
1023
+
1024
+ explanation = (
1025
+ explainer.explain_instance(
1026
+
1027
+ data_row=features[0],
1028
+
1029
+ predict_fn=model.predict_proba,
1030
+
1031
+ labels=[
1032
+ int(prediction)
1033
+ ],
1034
+
1035
+ num_features=10,
1036
+
1037
+ num_samples=3000
1038
+ )
1039
+ )
1040
+
1041
+
1042
+ lime_results = (
1043
+ explanation.as_list(
1044
+ label=int(prediction)
1045
+ )
1046
+ )
1047
+
1048
+
1049
+ result += (
1050
+ "\nTop features contributing "
1051
+ "to this prediction:\n"
1052
+ )
1053
+
1054
+
1055
+ for feature, weight in (
1056
+ lime_results
1057
+ ):
1058
+
1059
+ if weight > 0:
1060
+
1061
+ direction = "↑"
1062
+
1063
+ else:
1064
+
1065
+ direction = "↓"
1066
+
1067
+
1068
+ result += (
1069
+ f"\n{direction} "
1070
+ f"{feature}: "
1071
+ f"{weight:.4f}"
1072
+ )
1073
+
1074
+
1075
  except Exception as e:
1076
+
1077
+ result += (
1078
+ "\n\nLIME failed:\n"
1079
+ f"{type(e).__name__}: {e}"
1080
+ )
1081
+
1082
+
1083
+ # ========================================================
1084
+ # MIC
1085
+ # ========================================================
1086
 
1087
  if prediction == 0:
1088
+
1089
+ result += (
1090
+ "\n\n====================================\n"
1091
+ "PREDICTED MIC VALUES\n"
1092
+ "====================================\n"
1093
+ )
1094
+
1095
+
1096
+ mic_results = predict_mic(
1097
+ sequence
1098
+ )
1099
+
1100
+
1101
+ for organism, value in (
1102
+ mic_results.items()
1103
+ ):
1104
+
1105
+ result += (
1106
+ f"\n{organism}: "
1107
+ f"{value} μM"
1108
+ )
1109
+
1110
+
1111
  else:
1112
+
1113
+ result += (
1114
+ "\n\nMIC prediction skipped "
1115
+ "for Non-AMP sequences."
1116
+ )
1117
+
1118
 
1119
  return result
1120
 
1121
+
1122
+ # ============================================================
1123
+ # GRADIO
1124
+ # ============================================================
1125
+
1126
  iface = gr.Interface(
1127
+
1128
  fn=full_prediction,
1129
+
1130
+ inputs=gr.Textbox(
1131
+ label="Protein Sequence",
1132
+ placeholder=(
1133
+ "Enter an amino acid sequence..."
1134
+ ),
1135
+ lines=5
1136
+ ),
1137
+
1138
+ outputs=gr.Textbox(
1139
+ label="Results",
1140
+ lines=30
1141
+ ),
1142
+
1143
+ title=(
1144
+ "AMP & MIC Predictor + "
1145
+ "LIME Interpretability"
1146
+ ),
1147
+
1148
+ description=(
1149
+ "Enter an amino acid sequence "
1150
+ "(≥10 amino acids) to obtain AMP "
1151
+ "classification, confidence, LIME "
1152
+ "interpretability, and MIC predictions."
1153
+ ),
1154
+
1155
+ examples=[
1156
+ [
1157
+ "GLWSKIKEVGKEAAKAAAKAAGKAALGAVSEAV"
1158
+ ]
1159
+ ],
1160
+
1161
+ allow_flagging="never"
1162
  )
1163
 
1164
+
1165
+ # ============================================================
1166
+ # LAUNCH
1167
+ # ============================================================
1168
+
1169
+ if __name__ == "__main__":
1170
+
1171
+ iface.launch()