Spaces:
Sleeping
Sleeping
devjas1
commited on
Commit
·
7bc29cd
1
Parent(s):
d6ff372
(FEAT)[Refactor Confidence Visualization and Update CSS]: Remove legacy confidence progress HTML function, enhance softmax confidence calculation, and implement theme-aware custom styles for better UI consistency.
Browse files(FEAT)[Revise Sidebar and Input Column UI]: Update sidebar header and app description.
- Enhance modality selection with help text
- Improve layout for model comparison and input handling.
(FEAT)[Enhance Dataset Preparation Guidance]: Add detailed instructions for dataset structure, file naming, and public data sources in the training UI and README.
(FEAT)[Enhance Input Column with Action Buttons]: Refactor input column to use side-by-side layout for action buttons, adding a "Reset All" button and improving user interaction.
- modules/TRAINING_MODELS_README.md +44 -1
- modules/training_ui.py +26 -0
- modules/ui_components.py +100 -161
- static/style.css +56 -77
- utils/confidence.py +4 -70
modules/TRAINING_MODELS_README.md
CHANGED
@@ -28,7 +28,50 @@ This unified architecture ensures that any improvements to the training process
|
|
28 |
|
29 |
---
|
30 |
|
31 |
-
##
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
32 |
|
33 |
With the new unified architecture, you can train models using either the command line or the interactive web UI, depending on your needs.
|
34 |
|
|
|
28 |
|
29 |
---
|
30 |
|
31 |
+
## � Acquiring and Preparing Datasets
|
32 |
+
|
33 |
+
To train a model, you need a dataset of polymer spectra organized in a specific way. The training engine expects a directory containing two subdirectories:
|
34 |
+
|
35 |
+
- `stable/`: Contains spectra for unweathered, stable polymers.
|
36 |
+
- `weathered/`: Contains spectra for weathered, degraded polymers.
|
37 |
+
|
38 |
+
**Example Directory Structure:**
|
39 |
+
|
40 |
+
```
|
41 |
+
/my_dataset
|
42 |
+
├── /stable
|
43 |
+
│ ├── sample_01.txt
|
44 |
+
│ ├── sample_02.csv
|
45 |
+
│ └── ...
|
46 |
+
└── /weathered
|
47 |
+
├── sample_101.txt
|
48 |
+
├── sample_102.json
|
49 |
+
└── ...
|
50 |
+
```
|
51 |
+
|
52 |
+
### Data Format
|
53 |
+
|
54 |
+
Each file inside the `stable` and `weathered` folders should be a two-column text-based format representing a single spectrum:
|
55 |
+
|
56 |
+
- **Column 1**: Wavenumber (in cm⁻¹)
|
57 |
+
- **Column 2**: Intensity / Absorbance
|
58 |
+
- **Supported File Types**: `.txt`, `.csv`, `.json`
|
59 |
+
- **Separators**: Comma, space, or tab.
|
60 |
+
|
61 |
+
### Finding Public Datasets
|
62 |
+
|
63 |
+
If you don't have your own data, you can find public datasets from various sources. Here are some starting points and keywords for your search:
|
64 |
+
|
65 |
+
- **Open Specy**: A fantastic community-driven library for Raman and FTIR spectra. You can search for specific polymers and download data.
|
66 |
+
- **RRUFF™ Project**: An integrated database of Raman spectra, X-ray diffraction, and chemistry data for minerals. While not polymer-focused, it's a great example of a spectral database.
|
67 |
+
- **NIST Chemistry WebBook**: Contains FTIR spectra for many chemical compounds.
|
68 |
+
- **GitHub & Kaggle**: Search for "polymer spectroscopy dataset", "Raman spectra plastic", or "FTIR microplastics".
|
69 |
+
|
70 |
+
When using public data, you may need to manually classify and organize the files into the `stable`/`weathered` structure based on the sample descriptions provided with the dataset.
|
71 |
+
|
72 |
+
---
|
73 |
+
|
74 |
+
## �🛠️ How to Train Models
|
75 |
|
76 |
With the new unified architecture, you can train models using either the command line or the interactive web UI, depending on your needs.
|
77 |
|
modules/training_ui.py
CHANGED
@@ -113,6 +113,32 @@ def render_dataset_selection():
|
|
113 |
|
114 |
def render_dataset_upload():
|
115 |
"""Render dataset upload interface"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
116 |
st.markdown("##### Upload Dataset")
|
117 |
|
118 |
uploaded_files = st.file_uploader(
|
|
|
113 |
|
114 |
def render_dataset_upload():
|
115 |
"""Render dataset upload interface"""
|
116 |
+
with st.expander("ℹ️ How to Prepare Your Dataset for Training"):
|
117 |
+
st.markdown(
|
118 |
+
"""
|
119 |
+
For the model to train correctly, your dataset needs to be structured properly.
|
120 |
+
|
121 |
+
**1. File Naming & Labeling:**
|
122 |
+
The system can infer the class (`stable` or `weathered`) from the filename. For example, a file named `stable_polymer_1.txt` or `weathered_sample.csv` will be automatically categorized.
|
123 |
+
|
124 |
+
Alternatively, you can upload all your files regardless of name and use the labeling tool that appears below to manually assign each file to a class.
|
125 |
+
|
126 |
+
**2. File Format:**
|
127 |
+
- Each file should contain a single spectrum.
|
128 |
+
- The format should be two columns: **Wavenumber** and **Intensity**.
|
129 |
+
- Supported file types: `.txt`, `.csv`, `.json`.
|
130 |
+
|
131 |
+
**3. Finding Data:**
|
132 |
+
If you need data, here are some great public resources to get started:
|
133 |
+
- **Open Specy**: A large, community-driven library for Raman and FTIR spectra.
|
134 |
+
- **RRUFF™ Project**: An integrated database of Raman spectra, X-ray diffraction, and chemistry data for minerals.
|
135 |
+
- **NIST Chemistry WebBook**: Contains FTIR spectra for many chemical compounds.
|
136 |
+
- **GitHub & Kaggle**: Search for "polymer spectroscopy dataset", "Raman spectra plastic", or "FTIR microplastics".
|
137 |
+
|
138 |
+
When using public data, you may need to manually classify and organize the files into the `stable`/`weathered` structure based on the sample descriptions provided with the dataset.
|
139 |
+
"""
|
140 |
+
)
|
141 |
+
|
142 |
st.markdown("##### Upload Dataset")
|
143 |
|
144 |
uploaded_files = st.file_uploader(
|
modules/ui_components.py
CHANGED
@@ -67,51 +67,6 @@ def create_spectrum_plot(x_raw, y_raw, x_resampled, y_resampled, _cache_key=None
|
|
67 |
return Image.open(buf)
|
68 |
|
69 |
|
70 |
-
def render_confidence_progress(
|
71 |
-
probs: np.ndarray,
|
72 |
-
labels: list[str] = ["Stable", "Weathered"],
|
73 |
-
highlight_idx: Union[int, None] = None,
|
74 |
-
side_by_side: bool = True,
|
75 |
-
):
|
76 |
-
"""Render Streamlit native progress bars with scientific formatting."""
|
77 |
-
p = np.asarray(probs, dtype=float)
|
78 |
-
p = np.clip(p, 0.0, 1.0)
|
79 |
-
|
80 |
-
if side_by_side:
|
81 |
-
cols = st.columns(len(labels))
|
82 |
-
for i, (lbl, val, col) in enumerate(zip(labels, p, cols)):
|
83 |
-
with col:
|
84 |
-
is_highlighted = highlight_idx is not None and i == highlight_idx
|
85 |
-
label_text = f"**{lbl}**" if is_highlighted else lbl
|
86 |
-
st.markdown(f"{label_text}: {val*100:.1f}%")
|
87 |
-
st.progress(int(round(val * 100)))
|
88 |
-
else:
|
89 |
-
# Vertical layout for better readability
|
90 |
-
for i, (lbl, val) in enumerate(zip(labels, p)):
|
91 |
-
is_highlighted = highlight_idx is not None and i == highlight_idx
|
92 |
-
|
93 |
-
# Create a container for each probability
|
94 |
-
with st.container():
|
95 |
-
col1, col2 = st.columns([3, 1])
|
96 |
-
with col1:
|
97 |
-
if is_highlighted:
|
98 |
-
st.markdown(f"**{lbl}** ← Predicted")
|
99 |
-
else:
|
100 |
-
st.markdown(f"{lbl}")
|
101 |
-
with col2:
|
102 |
-
st.metric(label="", value=f"{val*100:.1f}%", delta=None)
|
103 |
-
|
104 |
-
# Progress bar with conditional styling
|
105 |
-
if is_highlighted:
|
106 |
-
st.progress(int(round(val * 100)))
|
107 |
-
st.caption("🎯 **Model Prediction**")
|
108 |
-
else:
|
109 |
-
st.progress(int(round(val * 100)))
|
110 |
-
|
111 |
-
if i < len(labels) - 1: # Add spacing between items
|
112 |
-
st.markdown("")
|
113 |
-
|
114 |
-
|
115 |
from typing import Optional
|
116 |
|
117 |
|
@@ -161,25 +116,9 @@ def render_sidebar():
|
|
161 |
# Header
|
162 |
st.header("AI-Driven Polymer Classification")
|
163 |
st.caption(
|
164 |
-
"
|
165 |
)
|
166 |
|
167 |
-
# Modality Selection
|
168 |
-
st.markdown("##### Spectroscopy Modality")
|
169 |
-
modality = st.selectbox(
|
170 |
-
"Choose Modality",
|
171 |
-
["raman", "ftir"],
|
172 |
-
index=0,
|
173 |
-
key="modality_select",
|
174 |
-
format_func=lambda x: f"{'Raman' if x == 'raman' else 'FTIR'}",
|
175 |
-
)
|
176 |
-
|
177 |
-
# Display modality info
|
178 |
-
if modality == "ftir":
|
179 |
-
st.info("FTIR mode: 400-4000 cm-1 range with atmospheric correction")
|
180 |
-
else:
|
181 |
-
st.info("Raman mode: 200-4000 cm-1 range with standard preprocessing")
|
182 |
-
|
183 |
# Model selection
|
184 |
st.markdown("##### AI Model Selection")
|
185 |
|
@@ -202,6 +141,7 @@ def render_sidebar():
|
|
202 |
model_labels,
|
203 |
key="model_select",
|
204 |
on_change=on_model_change,
|
|
|
205 |
)
|
206 |
model_choice = selected_label.split(" ", 1)[1]
|
207 |
|
@@ -212,37 +152,51 @@ def render_sidebar():
|
|
212 |
with st.expander("About This App", icon=":material/info:", expanded=False):
|
213 |
st.markdown(
|
214 |
"""
|
215 |
-
**AI-Driven Polymer
|
216 |
-
|
217 |
-
**Purpose**: Classify polymer degradation using AI<br>
|
218 |
-
**Input**: Raman spectroscopy .txt files<br>
|
219 |
-
**Models**: CNN architectures for classification<br>
|
220 |
-
**Modalities**: Raman and FTIR spectroscopy support<br>
|
221 |
-
**Features**: Multi-model comparison and analysis<br>
|
222 |
|
|
|
223 |
|
224 |
-
**
|
225 |
-
- Dr. Sanmukh Kuppannagari (Mentor)<br>
|
226 |
-
- Dr. Metin Karailyan (Mentor)<br>
|
227 |
-
- Jaser Hasan (Author)<br>
|
228 |
|
|
|
|
|
|
|
|
|
|
|
|
|
229 |
|
230 |
-
**Links
|
231 |
-
[HF Space](https://huggingface.co/spaces/dev-jas/polymer-aging-ml)
|
232 |
[GitHub Repository](https://github.com/KLab-AI3/ml-polymer-recycling)
|
233 |
|
|
|
|
|
|
|
|
|
|
|
234 |
|
235 |
-
**Citation
|
236 |
Neo et al., 2023, *Resour. Conserv. Recycl.*, 188, 106718.
|
237 |
-
|
238 |
-
"""
|
239 |
-
unsafe_allow_html=True,
|
240 |
)
|
241 |
|
242 |
|
243 |
def render_input_column():
|
244 |
st.markdown("##### Data Input")
|
245 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
246 |
mode = st.radio(
|
247 |
"Input mode",
|
248 |
["Upload File", "Batch Upload", "Sample Data"],
|
@@ -332,6 +286,7 @@ def render_input_column():
|
|
332 |
options,
|
333 |
key="sample_select",
|
334 |
on_change=on_sample_change,
|
|
|
335 |
)
|
336 |
if sel != "-- Select Sample --":
|
337 |
st.session_state["status_message"] = (
|
@@ -365,19 +320,21 @@ def render_input_column():
|
|
365 |
# Store for other modules to access
|
366 |
st.session_state["inference_ready"] = inference_ready
|
367 |
|
368 |
-
#
|
369 |
-
|
370 |
-
|
371 |
-
|
|
|
|
|
|
|
|
|
|
|
372 |
)
|
373 |
-
|
374 |
-
"Reset All",
|
375 |
-
on_click=reset_ephemeral_state,
|
376 |
-
help="Clear all uploaded files and results.",
|
377 |
-
)
|
378 |
|
379 |
# Handle form submission
|
380 |
-
if submitted
|
381 |
st.session_state["run_uuid"] = uuid.uuid4().hex[:8]
|
382 |
if st.session_state.get("batch_mode"):
|
383 |
batch_files = st.session_state.get("batch_files", [])
|
@@ -604,16 +561,10 @@ def render_results_column():
|
|
604 |
return -sum(p * math.log(p) for p in ps)
|
605 |
|
606 |
def _badge(text, kind="info"):
|
607 |
-
|
608 |
-
|
609 |
-
"warn": ("#7c2d12", "#fde68a"),
|
610 |
-
"good": ("#064e3b", "#bbf7d0"),
|
611 |
-
"bad": ("#7f1d1d", "#fecaca"),
|
612 |
-
}
|
613 |
-
bg, fg = palette.get(kind, palette["info"])
|
614 |
st.markdown(
|
615 |
-
f"<span
|
616 |
-
f"border-radius:6px;font-size:0.80rem;white-space:nowrap'>{text}</span>",
|
617 |
unsafe_allow_html=True,
|
618 |
)
|
619 |
|
@@ -935,18 +886,17 @@ def render_results_column():
|
|
935 |
with st.container():
|
936 |
st.markdown("### 🔍 Methodology & Interpretation")
|
937 |
|
938 |
-
|
939 |
-
st.markdown("Analysis Pipeline")
|
940 |
process_steps = [
|
941 |
-
"📁 **Data
|
942 |
-
"
|
943 |
-
"🧠 **AI Inference**:
|
944 |
-
"📊 **Classification**:
|
945 |
-
"✅ **Validation**:
|
946 |
]
|
947 |
|
948 |
for step in process_steps:
|
949 |
-
st.markdown(step)
|
950 |
|
951 |
st.markdown("---")
|
952 |
|
@@ -959,11 +909,10 @@ def render_results_column():
|
|
959 |
st.markdown("**Stable (Unweathered) Polymers:**")
|
960 |
st.info(
|
961 |
"""
|
962 |
-
-
|
963 |
-
- Minimal
|
964 |
-
-
|
965 |
-
-
|
966 |
-
itable for recycling applications
|
967 |
"""
|
968 |
)
|
969 |
|
@@ -971,49 +920,47 @@ def render_results_column():
|
|
971 |
st.markdown("**Weathered (Degraded) Polymers:**")
|
972 |
st.warning(
|
973 |
"""
|
974 |
-
-
|
975 |
-
-
|
976 |
-
-
|
977 |
-
- May require
|
978 |
"""
|
979 |
)
|
980 |
|
981 |
st.markdown("---")
|
982 |
|
983 |
# Applications
|
984 |
-
st.markdown("#### Research Applications")
|
985 |
|
986 |
applications = [
|
987 |
-
"
|
988 |
-
"♻️ **
|
989 |
-
"🌱 **Environmental Science**:
|
990 |
-
"🏭 **
|
991 |
-
"
|
992 |
]
|
993 |
|
994 |
for app in applications:
|
995 |
-
st.markdown(app)
|
996 |
|
997 |
# Technical details
|
998 |
-
|
999 |
-
|
|
|
1000 |
st.markdown(
|
1001 |
"""
|
1002 |
-
**Model
|
1003 |
-
-
|
1004 |
-
-
|
1005 |
-
|
1006 |
-
|
1007 |
-
|
1008 |
-
**
|
1009 |
-
|
1010 |
-
|
1011 |
-
-
|
1012 |
-
|
1013 |
-
**Data
|
1014 |
-
- Input: Raman spectra (any length)
|
1015 |
-
- Resampling: Linear interpolation to 500 points
|
1016 |
-
- Normalization: None (preserves intensity relationships)
|
1017 |
"""
|
1018 |
)
|
1019 |
|
@@ -1023,7 +970,8 @@ def render_results_column():
|
|
1023 |
)
|
1024 |
|
1025 |
with st.expander("Spectrum Preprocessing Results", expanded=False):
|
1026 |
-
st.
|
|
|
1027 |
|
1028 |
# Add some context about the preprocessing
|
1029 |
st.markdown(
|
@@ -1071,6 +1019,7 @@ def render_results_column():
|
|
1071 |
- **Content:** Must contain two columns: `wavenumber` and `intensity`.
|
1072 |
- **Separators:** Values can be separated by spaces or commas.
|
1073 |
- **Preprocessing:** Your spectrum will be automatically resampled to 500 data points to match the model's input requirements.
|
|
|
1074 |
"""
|
1075 |
)
|
1076 |
else:
|
@@ -1094,6 +1043,7 @@ def render_results_column():
|
|
1094 |
- **Content:** Must contain two columns: `wavenumber` and `intensity`.
|
1095 |
- **Separators:** Values can be separated by spaces or commas.
|
1096 |
- **Preprocessing:** Your spectrum will be automatically resampled to 500 data points to match the model's input requirements.
|
|
|
1097 |
"""
|
1098 |
)
|
1099 |
|
@@ -1120,27 +1070,16 @@ def render_comparison_tab():
|
|
1120 |
"Compare predictions across different AI models for comprehensive analysis."
|
1121 |
)
|
1122 |
|
1123 |
-
#
|
1124 |
-
|
1125 |
-
|
1126 |
-
|
1127 |
-
|
1128 |
-
|
1129 |
-
|
1130 |
-
|
1131 |
-
|
1132 |
-
|
1133 |
-
key="comparison_tab_modality", # Independant key for session state to avoid duplication of UI elements
|
1134 |
-
) # Note: Intentially not synching back to avoid state conflicts
|
1135 |
-
|
1136 |
-
with col_mod2:
|
1137 |
-
# Filter models by modality
|
1138 |
-
compatible_models = models_for_modality(modality)
|
1139 |
-
if not compatible_models:
|
1140 |
-
st.error(f"No models available for {modality.upper()} modality")
|
1141 |
-
return
|
1142 |
-
|
1143 |
-
st.info(f"📊 {len(compatible_models)} models available for {modality.upper()}")
|
1144 |
|
1145 |
# Enhanced model selection with metadata
|
1146 |
st.markdown("##### Select Models for Comparison")
|
|
|
67 |
return Image.open(buf)
|
68 |
|
69 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
70 |
from typing import Optional
|
71 |
|
72 |
|
|
|
116 |
# Header
|
117 |
st.header("AI-Driven Polymer Classification")
|
118 |
st.caption(
|
119 |
+
"Analyze and classify polymer degradation with a suite of explainable AI models for Raman & FTIR spectroscopy. — v0.02"
|
120 |
)
|
121 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
122 |
# Model selection
|
123 |
st.markdown("##### AI Model Selection")
|
124 |
|
|
|
141 |
model_labels,
|
142 |
key="model_select",
|
143 |
on_change=on_model_change,
|
144 |
+
width="stretch",
|
145 |
)
|
146 |
model_choice = selected_label.split(" ", 1)[1]
|
147 |
|
|
|
152 |
with st.expander("About This App", icon=":material/info:", expanded=False):
|
153 |
st.markdown(
|
154 |
"""
|
155 |
+
**AI-Driven Polymer Analysis Platform**
|
|
|
|
|
|
|
|
|
|
|
|
|
156 |
|
157 |
+
**Purpose**: Classify, analyze, and understand polymer degradation using explainable AI.
|
158 |
|
159 |
+
**Input**: Raman & FTIR spectra in `.txt`, `.csv`, or `.json` formats.
|
|
|
|
|
|
|
160 |
|
161 |
+
**Features**:
|
162 |
+
- Single & Batch Spectrum Analysis
|
163 |
+
- Multi-Model Performance Comparison
|
164 |
+
- Interactive Model Training Hub
|
165 |
+
- Explainable AI (XAI) with feature importance
|
166 |
+
- Modality-Aware Preprocessing
|
167 |
|
168 |
+
**Links**
|
169 |
+
[HF Space](https://huggingface.co/spaces/dev-jas/polymer-aging-ml)
|
170 |
[GitHub Repository](https://github.com/KLab-AI3/ml-polymer-recycling)
|
171 |
|
172 |
+
**Contributors**
|
173 |
+
- Dr. Sanmukh Kuppannagari (Mentor)
|
174 |
+
- Dr. Metin Karailyan (Mentor)
|
175 |
+
- Jaser Hasan (Author)
|
176 |
+
|
177 |
|
178 |
+
**Citation (Baseline Model)**
|
179 |
Neo et al., 2023, *Resour. Conserv. Recycl.*, 188, 106718.
|
180 |
+
https://doi.org/10.1016/j.resconrec.2022.106718
|
181 |
+
"""
|
|
|
182 |
)
|
183 |
|
184 |
|
185 |
def render_input_column():
|
186 |
st.markdown("##### Data Input")
|
187 |
|
188 |
+
# Modality Selection - Moved from sidebar to be the primary context setter
|
189 |
+
st.markdown("###### 1. Choose Spectroscopy Modality")
|
190 |
+
modality = st.selectbox(
|
191 |
+
"Choose Modality",
|
192 |
+
["raman", "ftir"],
|
193 |
+
index=0,
|
194 |
+
key="modality_select",
|
195 |
+
format_func=lambda x: f"{'Raman' if x == 'raman' else 'FTIR'}",
|
196 |
+
help="Select the type of spectroscopy data you are analyzing. This choice affects preprocessing steps.",
|
197 |
+
width=325,
|
198 |
+
)
|
199 |
+
|
200 |
mode = st.radio(
|
201 |
"Input mode",
|
202 |
["Upload File", "Batch Upload", "Sample Data"],
|
|
|
286 |
options,
|
287 |
key="sample_select",
|
288 |
on_change=on_sample_change,
|
289 |
+
width=350,
|
290 |
)
|
291 |
if sel != "-- Select Sample --":
|
292 |
st.session_state["status_message"] = (
|
|
|
320 |
# Store for other modules to access
|
321 |
st.session_state["inference_ready"] = inference_ready
|
322 |
|
323 |
+
# --- Action Buttons ---
|
324 |
+
# Using columns for a side-by-side layout
|
325 |
+
col1, col2 = st.columns(2)
|
326 |
+
with col1:
|
327 |
+
submitted = st.button(
|
328 |
+
"Run Analysis",
|
329 |
+
type="primary",
|
330 |
+
disabled=not inference_ready,
|
331 |
+
use_container_width=True,
|
332 |
)
|
333 |
+
with col2:
|
334 |
+
st.button("Reset All", on_click=reset_ephemeral_state, use_container_width=True)
|
|
|
|
|
|
|
335 |
|
336 |
# Handle form submission
|
337 |
+
if submitted:
|
338 |
st.session_state["run_uuid"] = uuid.uuid4().hex[:8]
|
339 |
if st.session_state.get("batch_mode"):
|
340 |
batch_files = st.session_state.get("batch_files", [])
|
|
|
561 |
return -sum(p * math.log(p) for p in ps)
|
562 |
|
563 |
def _badge(text, kind="info"):
|
564 |
+
# This function now relies on CSS classes defined in style.css
|
565 |
+
# for better separation of concerns and maintainability.
|
|
|
|
|
|
|
|
|
|
|
566 |
st.markdown(
|
567 |
+
f"<span class='badge badge-{kind}'>{text}</span>",
|
|
|
568 |
unsafe_allow_html=True,
|
569 |
)
|
570 |
|
|
|
886 |
with st.container():
|
887 |
st.markdown("### 🔍 Methodology & Interpretation")
|
888 |
|
889 |
+
st.markdown("#### Analysis Pipeline")
|
|
|
890 |
process_steps = [
|
891 |
+
"📁 **Data Input**: Upload a spectrum file (`.txt`, `.csv`, `.json`) and select the spectroscopy modality (Raman or FTIR).",
|
892 |
+
"🔬 **Modality-Aware Preprocessing**: The spectrum is automatically processed with steps tailored to the selected modality, including baseline correction, smoothing, normalization, and resampling to a fixed length (500 points).",
|
893 |
+
"🧠 **AI Inference**: A selected model from the registry (e.g., `Figure2CNN`, `ResNet`, `EnhancedCNN`) analyzes the processed spectrum to identify key patterns.",
|
894 |
+
"📊 **Classification & Confidence**: The model outputs a binary prediction (Stable vs. Weathered) along with a detailed probability breakdown and confidence score.",
|
895 |
+
"✅ **Validation & Explainability**: Results are presented with technical diagnostics, and where possible, explainability metrics to interpret the model's decision.",
|
896 |
]
|
897 |
|
898 |
for step in process_steps:
|
899 |
+
st.markdown(f"- {step}")
|
900 |
|
901 |
st.markdown("---")
|
902 |
|
|
|
909 |
st.markdown("**Stable (Unweathered) Polymers:**")
|
910 |
st.info(
|
911 |
"""
|
912 |
+
- **Spectral Signature**: Sharp, well-defined peaks corresponding to the polymer's known vibrational modes.
|
913 |
+
- **Chemical State**: Minimal evidence of oxidation or chain scission. The polymer backbone is intact.
|
914 |
+
- **Model Behavior**: The AI identifies a strong match with the spectral fingerprint of a non-degraded reference material.
|
915 |
+
- **Implication**: Suitable for high-quality recycling applications.
|
|
|
916 |
"""
|
917 |
)
|
918 |
|
|
|
920 |
st.markdown("**Weathered (Degraded) Polymers:**")
|
921 |
st.warning(
|
922 |
"""
|
923 |
+
- **Spectral Signature**: Peak broadening, baseline shifts, and the emergence of new peaks (e.g., carbonyl group at ~1715 cm⁻¹).
|
924 |
+
- **Chemical State**: Evidence of oxidation, hydrolysis, or other degradation pathways.
|
925 |
+
- **Model Behavior**: The AI detects features that deviate significantly from the reference fingerprint, indicating chemical alteration.
|
926 |
+
- **Implication**: May require more intensive processing or be unsuitable for certain recycling streams.
|
927 |
"""
|
928 |
)
|
929 |
|
930 |
st.markdown("---")
|
931 |
|
932 |
# Applications
|
933 |
+
st.markdown("#### Research & Industrial Applications")
|
934 |
|
935 |
applications = [
|
936 |
+
" **Material Science**: Quantify degradation rates and study aging mechanisms in novel polymers.",
|
937 |
+
"♻️ **Circular Economy**: Automate the quality control and sorting of post-consumer plastics for recycling.",
|
938 |
+
"🌱 **Environmental Science**: Analyze the weathering of microplastics in various environmental conditions.",
|
939 |
+
"🏭 **Industrial QC**: Monitor material integrity and predict product lifetime in manufacturing processes.",
|
940 |
+
"🤖 **AI-Driven Discovery**: Use explainability features to generate new hypotheses about material behavior.",
|
941 |
]
|
942 |
|
943 |
for app in applications:
|
944 |
+
st.markdown(f"- {app}")
|
945 |
|
946 |
# Technical details
|
947 |
+
with st.expander(
|
948 |
+
"🔧 Technical Architecture Details", expanded=False
|
949 |
+
):
|
950 |
st.markdown(
|
951 |
"""
|
952 |
+
**Model Architectures:**
|
953 |
+
- The app features a registry of models, including the `Figure2CNN` baseline, `ResNet` variants, and more advanced custom architectures like `EnhancedCNN` and `HybridSpectralNet`.
|
954 |
+
- Each model is trained on a comprehensive dataset of stable and weathered polymer spectra.
|
955 |
+
|
956 |
+
**Unified Training Engine:**
|
957 |
+
- A central `TrainingEngine` ensures that all models are trained and validated using a consistent, reproducible 10-fold cross-validation strategy.
|
958 |
+
- This engine can be accessed via the **CLI** (`scripts/train_model.py`) for automated experiments or the **UI** ("Model Training Hub") for interactive use.
|
959 |
+
|
960 |
+
**Explainability & Transparency (XAI):**
|
961 |
+
- **Feature Importance**: The system is designed to incorporate SHAP and gradient-based methods to highlight which spectral regions most influence a prediction.
|
962 |
+
- **Uncertainty Quantification**: Advanced models can estimate both model (epistemic) and data (aleatoric) uncertainty.
|
963 |
+
- **Data Provenance**: The enhanced data pipeline tracks every preprocessing step, ensuring full traceability from raw data to final prediction.
|
|
|
|
|
|
|
964 |
"""
|
965 |
)
|
966 |
|
|
|
970 |
)
|
971 |
|
972 |
with st.expander("Spectrum Preprocessing Results", expanded=False):
|
973 |
+
st.markdown("---")
|
974 |
+
st.markdown("##### Spectral Analysis")
|
975 |
|
976 |
# Add some context about the preprocessing
|
977 |
st.markdown(
|
|
|
1019 |
- **Content:** Must contain two columns: `wavenumber` and `intensity`.
|
1020 |
- **Separators:** Values can be separated by spaces or commas.
|
1021 |
- **Preprocessing:** Your spectrum will be automatically resampled to 500 data points to match the model's input requirements.
|
1022 |
+
- **Examples:** Use the "Sample Data" input mode to see examples, or find public data on sites like Open Specy.
|
1023 |
"""
|
1024 |
)
|
1025 |
else:
|
|
|
1043 |
- **Content:** Must contain two columns: `wavenumber` and `intensity`.
|
1044 |
- **Separators:** Values can be separated by spaces or commas.
|
1045 |
- **Preprocessing:** Your spectrum will be automatically resampled to 500 data points to match the model's input requirements.
|
1046 |
+
- **Examples:** Use the "Sample Data" input mode to see examples, or find public data on sites like Open Specy.
|
1047 |
"""
|
1048 |
)
|
1049 |
|
|
|
1070 |
"Compare predictions across different AI models for comprehensive analysis."
|
1071 |
)
|
1072 |
|
1073 |
+
# Use the global modality selector from the main page
|
1074 |
+
modality = st.session_state.get("modality_select", "raman")
|
1075 |
+
st.info(
|
1076 |
+
f"Comparing models using **{modality.upper()}** preprocessing parameters. You can change this on the 'Upload and Run' page."
|
1077 |
+
)
|
1078 |
+
|
1079 |
+
compatible_models = models_for_modality(modality)
|
1080 |
+
if not compatible_models:
|
1081 |
+
st.error(f"No models available for {modality.upper()} modality")
|
1082 |
+
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1083 |
|
1084 |
# Enhanced model selection with metadata
|
1085 |
st.markdown("##### Select Models for Comparison")
|
static/style.css
CHANGED
@@ -1,13 +1,14 @@
|
|
1 |
-
/*
|
2 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
*/
|
7 |
-
|
|
|
8 |
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&family=Fira+Code:wght@400&display=swap');
|
9 |
|
10 |
-
/*
|
11 |
.stApp,
|
12 |
section[data-testid="stSidebar"],
|
13 |
div[data-testid="stMetricValue"],
|
@@ -21,7 +22,7 @@ div[data-testid="stMetricLabel"] {
|
|
21 |
font-family: 'Fira Code', monospace;
|
22 |
}
|
23 |
|
24 |
-
/*
|
25 |
div[data-testid="stTabs"]>div[role="tablist"]+div {
|
26 |
min-height: 400px;
|
27 |
/* Uses the secondary background color, which is different in light and dark modes */
|
@@ -41,7 +42,7 @@ div[data-testid="stTabs"]>div[role="tablist"]+div {
|
|
41 |
background-color: var(--secondary-background-color);
|
42 |
}
|
43 |
|
44 |
-
/*
|
45 |
.kv-row {
|
46 |
display: flex;
|
47 |
justify-content: space-between;
|
@@ -66,7 +67,44 @@ div[data-testid="stTabs"]>div[role="tablist"]+div {
|
|
66 |
text-align: right;
|
67 |
}
|
68 |
|
69 |
-
/*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
70 |
div.stExpander>details>summary::-webkit-details-marker,
|
71 |
div.stExpander>details>summary::marker,
|
72 |
div[data-testid="stExpander"] summary svg {
|
@@ -74,6 +112,7 @@ div[data-testid="stExpander"] summary svg {
|
|
74 |
}
|
75 |
|
76 |
div.stExpander>details>summary::after {
|
|
|
77 |
content: '↓';
|
78 |
font-size: 0.75rem;
|
79 |
font-weight: 900;
|
@@ -82,39 +121,21 @@ div.stExpander>details>summary::after {
|
|
82 |
border-radius: 999px;
|
83 |
/* The primary color is set in config.toml and adapted by Streamlit */
|
84 |
background-color: var(--primary);
|
85 |
-
color:
|
86 |
-
/*
|
87 |
-
|
88 |
transition: background-color 0.2s ease-in-out;
|
89 |
}
|
90 |
|
91 |
div.stExpander>details>summary:hover::after {
|
92 |
-
/* Using a fixed darker shade on hover. A more advanced solution could use color-mix() in CSS. */
|
93 |
filter: brightness(90%);
|
94 |
}
|
95 |
|
96 |
-
/* Specialized Expander Labels */
|
97 |
-
.expander-results div[data-testid="stExpander"] summary::after {
|
98 |
-
content: "RESULTS";
|
99 |
-
background-color: #16A34A;
|
100 |
-
/* Green is universal for success */
|
101 |
-
|
102 |
-
}
|
103 |
-
|
104 |
div[data-testid="stExpander"] details {
|
105 |
-
content: "RESULTS";
|
106 |
background-color: var(--primary);
|
107 |
border-radius: 10px;
|
108 |
padding: 10px
|
109 |
}
|
110 |
|
111 |
-
.expander-advanced div[data-testid="stExpander"] summary::after {
|
112 |
-
content: "ADVANCED";
|
113 |
-
background-color: #D97706;
|
114 |
-
/* Amber is universal for warning/technical */
|
115 |
-
|
116 |
-
}
|
117 |
-
|
118 |
[data-testid="stExpanderDetails"] {
|
119 |
padding: 16px 4px 4px 4px;
|
120 |
background-color: transparent;
|
@@ -122,7 +143,7 @@ div[data-testid="stExpander"] details {
|
|
122 |
margin-top: 12px;
|
123 |
}
|
124 |
|
125 |
-
/*
|
126 |
section[data-testid="stSidebar"]>div:first-child {
|
127 |
background-color: var(--secondary-background-color);
|
128 |
border-right: 1px solid rgba(128, 128, 128, 0.2);
|
@@ -138,52 +159,10 @@ div[data-testid="stMetricLabel"] {
|
|
138 |
opacity: 0.8;
|
139 |
}
|
140 |
|
141 |
-
/*
|
142 |
:focus-visible {
|
143 |
/* The focus outline now uses the theme's primary color */
|
144 |
-
outline:
|
145 |
outline-offset: 2px;
|
146 |
border-radius: 8px;
|
147 |
-
}
|
148 |
-
|
149 |
-
.st-key-csv-button,
|
150 |
-
.st-key-json-button,
|
151 |
-
.st-key-clearall-button {
|
152 |
-
display: block;
|
153 |
-
border: 1px double #1a1a1a98;
|
154 |
-
max-width: 100%;
|
155 |
-
border-radius: 8px;
|
156 |
-
|
157 |
-
|
158 |
-
|
159 |
-
}
|
160 |
-
|
161 |
-
.st-key-page-link-container {
|
162 |
-
padding: 5px;
|
163 |
-
display: inline-block;
|
164 |
-
justify-items: center;
|
165 |
-
align-self: center;
|
166 |
-
align-content: center;
|
167 |
-
border: 1px double #1a1a1a98;
|
168 |
-
border-radius: 8px;
|
169 |
-
background-color: var(--secondary-background-color);
|
170 |
-
max-width: 100%;
|
171 |
-
|
172 |
-
}
|
173 |
-
|
174 |
-
.st-key-buttons-container {
|
175 |
-
display: flex;
|
176 |
-
max-width: 100%;
|
177 |
-
}
|
178 |
-
|
179 |
-
|
180 |
-
|
181 |
-
/* .st-key-csv-button:hover,
|
182 |
-
.st-key-json-button:hover,
|
183 |
-
.st-key-clearall-button:hover {
|
184 |
-
padding: .25px;
|
185 |
-
}
|
186 |
-
.st-key-page-link {
|
187 |
-
color: var(--text-color);
|
188 |
-
text-decoration: none;
|
189 |
-
}*/
|
|
|
1 |
+
/* ==========================================================================
|
2 |
+
POLYMEROS - THEME-AWARE CUSTOM STYLESHEET
|
3 |
+
==========================================================================
|
4 |
+
This file uses Streamlit's theme variables (e.g., var(--text-color))
|
5 |
+
to ensure all custom components adapt to both light and dark themes.
|
6 |
+
========================================================================== */
|
7 |
+
|
8 |
+
/* --- Font Imports --- */
|
9 |
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&family=Fira+Code:wght@400&display=swap');
|
10 |
|
11 |
+
/* --- Base & Typography --- */
|
12 |
.stApp,
|
13 |
section[data-testid="stSidebar"],
|
14 |
div[data-testid="stMetricValue"],
|
|
|
22 |
font-family: 'Fira Code', monospace;
|
23 |
}
|
24 |
|
25 |
+
/* --- Custom Containers: Tabs & Info Boxes --- */
|
26 |
div[data-testid="stTabs"]>div[role="tablist"]+div {
|
27 |
min-height: 400px;
|
28 |
/* Uses the secondary background color, which is different in light and dark modes */
|
|
|
42 |
background-color: var(--secondary-background-color);
|
43 |
}
|
44 |
|
45 |
+
/* --- Key-Value Pair Styling --- */
|
46 |
.kv-row {
|
47 |
display: flex;
|
48 |
justify-content: space-between;
|
|
|
67 |
text-align: right;
|
68 |
}
|
69 |
|
70 |
+
/* --- Custom Badges (replaces inline styles) --- */
|
71 |
+
.badge {
|
72 |
+
display: inline-block;
|
73 |
+
padding: 4px 8px;
|
74 |
+
font-size: 0.80rem;
|
75 |
+
font-weight: 500;
|
76 |
+
line-height: 1;
|
77 |
+
text-align: center;
|
78 |
+
white-space: nowrap;
|
79 |
+
vertical-align: baseline;
|
80 |
+
border-radius: 6px;
|
81 |
+
font-family: 'Inter', sans-serif;
|
82 |
+
}
|
83 |
+
|
84 |
+
.badge-info {
|
85 |
+
color: #e2e8f0;
|
86 |
+
background-color: #334155;
|
87 |
+
}
|
88 |
+
|
89 |
+
.badge-warn {
|
90 |
+
color: #7c2d12;
|
91 |
+
background-color: #fde68a;
|
92 |
+
}
|
93 |
+
|
94 |
+
.badge-good {
|
95 |
+
color: #064e3b;
|
96 |
+
background-color: #bbf7d0;
|
97 |
+
}
|
98 |
+
|
99 |
+
.badge-bad {
|
100 |
+
color: #7f1d1d;
|
101 |
+
background-color: #fecaca;
|
102 |
+
}
|
103 |
+
|
104 |
+
|
105 |
+
/* --- Custom Expander Styling --- */
|
106 |
+
|
107 |
+
/* Hide default expander arrow */
|
108 |
div.stExpander>details>summary::-webkit-details-marker,
|
109 |
div.stExpander>details>summary::marker,
|
110 |
div[data-testid="stExpander"] summary svg {
|
|
|
112 |
}
|
113 |
|
114 |
div.stExpander>details>summary::after {
|
115 |
+
/* Custom indicator */
|
116 |
content: '↓';
|
117 |
font-size: 0.75rem;
|
118 |
font-weight: 900;
|
|
|
121 |
border-radius: 999px;
|
122 |
/* The primary color is set in config.toml and adapted by Streamlit */
|
123 |
background-color: var(--primary);
|
124 |
+
color: white;
|
125 |
+
/* Assuming primary color is dark enough for white text */
|
|
|
126 |
transition: background-color 0.2s ease-in-out;
|
127 |
}
|
128 |
|
129 |
div.stExpander>details>summary:hover::after {
|
|
|
130 |
filter: brightness(90%);
|
131 |
}
|
132 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
133 |
div[data-testid="stExpander"] details {
|
|
|
134 |
background-color: var(--primary);
|
135 |
border-radius: 10px;
|
136 |
padding: 10px
|
137 |
}
|
138 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
139 |
[data-testid="stExpanderDetails"] {
|
140 |
padding: 16px 4px 4px 4px;
|
141 |
background-color: transparent;
|
|
|
143 |
margin-top: 12px;
|
144 |
}
|
145 |
|
146 |
+
/* --- Sidebar & Metrics --- */
|
147 |
section[data-testid="stSidebar"]>div:first-child {
|
148 |
background-color: var(--secondary-background-color);
|
149 |
border-right: 1px solid rgba(128, 128, 128, 0.2);
|
|
|
159 |
opacity: 0.8;
|
160 |
}
|
161 |
|
162 |
+
/* --- Interactivity & Accessibility --- */
|
163 |
:focus-visible {
|
164 |
/* The focus outline now uses the theme's primary color */
|
165 |
+
outline: 2px solid var(--primary);
|
166 |
outline-offset: 2px;
|
167 |
border-radius: 8px;
|
168 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
utils/confidence.py
CHANGED
@@ -1,12 +1,15 @@
|
|
1 |
"""Confidence calculation and visualization utilities.
|
2 |
Provides normalized softmax confidence and color-coded badges"""
|
|
|
3 |
from typing import Tuple, List
|
4 |
import numpy as np
|
5 |
import torch
|
6 |
import torch.nn.functional as F
|
7 |
|
8 |
|
9 |
-
def calculate_softmax_confidence(
|
|
|
|
|
10 |
"""Calculate normalized confidence using softmax
|
11 |
Args:
|
12 |
logits: Raw model logits tensor
|
@@ -63,75 +66,6 @@ def format_confidence_display(confidence: float, level: str, emoji: str) -> str:
|
|
63 |
return f"{emoji} **{level}** ({confidence:.1%})"
|
64 |
|
65 |
|
66 |
-
def create_confidence_progress_html(
|
67 |
-
probabilities: np.ndarray,
|
68 |
-
labels: List[str],
|
69 |
-
highlight_idx: int
|
70 |
-
) -> str:
|
71 |
-
"""
|
72 |
-
Create HTML for confidence progress bars
|
73 |
-
|
74 |
-
Args:
|
75 |
-
probabilities: Array of class probabilities
|
76 |
-
labels: List of class labels
|
77 |
-
highlight_idx: Index of predicted class to highlight
|
78 |
-
|
79 |
-
Returns:
|
80 |
-
HTML string for progress bars
|
81 |
-
"""
|
82 |
-
if len(probabilities) == 0 or len(labels) == 0:
|
83 |
-
return "<p>No confidence data available</p>"
|
84 |
-
|
85 |
-
html_parts = []
|
86 |
-
|
87 |
-
for i, (prob, label) in enumerate(zip(probabilities, labels)):
|
88 |
-
# ===Color based on whether this is the predicted class===
|
89 |
-
if i == highlight_idx:
|
90 |
-
if prob >= 0.80:
|
91 |
-
color = "#22c55e" # green-500
|
92 |
-
text_color = "#ffffff"
|
93 |
-
elif prob >= 0.60:
|
94 |
-
color = "#eab308" # yellow-500
|
95 |
-
text_color = "#000000"
|
96 |
-
else:
|
97 |
-
color = "#ef4444" # red-500
|
98 |
-
text_color = "#ffffff"
|
99 |
-
else:
|
100 |
-
color = "#e5e7eb" # gray-200
|
101 |
-
text_color = "#6b7280" # gray-500
|
102 |
-
|
103 |
-
percentage = prob * 100
|
104 |
-
|
105 |
-
html_parts.append(f"""
|
106 |
-
<div style="margin-bottom: 8px;">
|
107 |
-
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px;">
|
108 |
-
<span style="font-size: 0.875rem; font-weight: 500; color: #374151;">{label}</span>
|
109 |
-
<span style="font-size: 0.875rem; color: #6b7280;">{percentage:.1f}%</span>
|
110 |
-
</div>
|
111 |
-
<div style="width: 100%; background-color: #f3f4f6; border-radius: 0.375rem; height: 20px; overflow: hidden;">
|
112 |
-
<div style="
|
113 |
-
width: {percentage}%;
|
114 |
-
height: 100%;
|
115 |
-
background-color: {color};
|
116 |
-
display: flex;
|
117 |
-
align-items: center;
|
118 |
-
justify-content: center;
|
119 |
-
transition: width 0.3s ease;
|
120 |
-
">
|
121 |
-
{f'<span style="color: {text_color}; font-size: 0.75rem; font-weight: 600;">{percentage:.1f}%</span>' if percentage > 20 else ''}
|
122 |
-
</div>
|
123 |
-
</div>
|
124 |
-
</div>
|
125 |
-
""")
|
126 |
-
|
127 |
-
return f"""
|
128 |
-
<div style="padding: 16px; background-color: #f9fafb; border-radius: 0.5rem; border: 1px solid #e5e7eb;">
|
129 |
-
<h4 style="margin: 0 0 12px 0; font-size: 1rem; color: #374151;">Confidence Breakdown</h4>
|
130 |
-
{''.join(html_parts)}
|
131 |
-
</div>
|
132 |
-
"""
|
133 |
-
|
134 |
-
|
135 |
def calculate_legacy_confidence(logits_list: List[float]) -> Tuple[float, str, str]:
|
136 |
"""
|
137 |
Calculate confidence using legacy logit margin method for backward compatibility
|
|
|
1 |
"""Confidence calculation and visualization utilities.
|
2 |
Provides normalized softmax confidence and color-coded badges"""
|
3 |
+
|
4 |
from typing import Tuple, List
|
5 |
import numpy as np
|
6 |
import torch
|
7 |
import torch.nn.functional as F
|
8 |
|
9 |
|
10 |
+
def calculate_softmax_confidence(
|
11 |
+
logits: torch.Tensor,
|
12 |
+
) -> Tuple[np.ndarray, float, str, str]:
|
13 |
"""Calculate normalized confidence using softmax
|
14 |
Args:
|
15 |
logits: Raw model logits tensor
|
|
|
66 |
return f"{emoji} **{level}** ({confidence:.1%})"
|
67 |
|
68 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
69 |
def calculate_legacy_confidence(logits_list: List[float]) -> Tuple[float, str, str]:
|
70 |
"""
|
71 |
Calculate confidence using legacy logit margin method for backward compatibility
|