Rename src/streamlit_app.py to app.py
Browse files- app.py +382 -0
- src/streamlit_app.py +0 -40
app.py
ADDED
@@ -0,0 +1,382 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import streamlit as st
|
2 |
+
import plotly.express as px
|
3 |
+
import plotly.graph_objects as go
|
4 |
+
from plotly.subplots import make_subplots
|
5 |
+
import pandas as pd
|
6 |
+
import numpy as np
|
7 |
+
import json
|
8 |
+
import uuid
|
9 |
+
from datetime import datetime, timedelta
|
10 |
+
import time
|
11 |
+
from huggingface_hub import HfApi, login
|
12 |
+
from streamlit_option_menu import option_menu
|
13 |
+
import requests
|
14 |
+
import hashlib
|
15 |
+
import os
|
16 |
+
|
17 |
+
# Configure page
|
18 |
+
st.set_page_config(
|
19 |
+
page_title="ML Tracker - Free W&B Alternative",
|
20 |
+
page_icon="π",
|
21 |
+
layout="wide",
|
22 |
+
initial_sidebar_state="expanded"
|
23 |
+
)
|
24 |
+
|
25 |
+
# Initialize session state
|
26 |
+
if 'authenticated' not in st.session_state:
|
27 |
+
st.session_state.authenticated = False
|
28 |
+
if 'user_token' not in st.session_state:
|
29 |
+
st.session_state.user_token = None
|
30 |
+
if 'api_key' not in st.session_state:
|
31 |
+
st.session_state.api_key = None
|
32 |
+
if 'experiments' not in st.session_state:
|
33 |
+
st.session_state.experiments = {}
|
34 |
+
if 'current_experiment' not in st.session_state:
|
35 |
+
st.session_state.current_experiment = None
|
36 |
+
|
37 |
+
# Custom CSS for better styling
|
38 |
+
st.markdown("""
|
39 |
+
<style>
|
40 |
+
.main-header {
|
41 |
+
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
|
42 |
+
padding: 1rem;
|
43 |
+
border-radius: 10px;
|
44 |
+
margin-bottom: 1rem;
|
45 |
+
}
|
46 |
+
.metric-card {
|
47 |
+
background: #f8f9fa;
|
48 |
+
padding: 1rem;
|
49 |
+
border-radius: 8px;
|
50 |
+
border-left: 4px solid #667eea;
|
51 |
+
margin-bottom: 1rem;
|
52 |
+
}
|
53 |
+
.api-key-box {
|
54 |
+
background: #f1f3f4;
|
55 |
+
padding: 1rem;
|
56 |
+
border-radius: 8px;
|
57 |
+
font-family: monospace;
|
58 |
+
margin: 1rem 0;
|
59 |
+
}
|
60 |
+
.stButton > button {
|
61 |
+
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
|
62 |
+
color: white;
|
63 |
+
border: none;
|
64 |
+
border-radius: 6px;
|
65 |
+
padding: 0.5rem 1rem;
|
66 |
+
font-weight: 500;
|
67 |
+
}
|
68 |
+
</style>
|
69 |
+
""", unsafe_allow_html=True)
|
70 |
+
|
71 |
+
def generate_api_key(user_token):
|
72 |
+
"""Generate a unique API key for the user"""
|
73 |
+
return hashlib.sha256(f"{user_token}_{datetime.now().isoformat()}".encode()).hexdigest()[:32]
|
74 |
+
|
75 |
+
def authenticate_user():
|
76 |
+
"""Handle HuggingFace authentication"""
|
77 |
+
st.markdown('<div class="main-header"><h1 style="color: white; margin: 0;">π€ ML Tracker</h1><p style="color: white; margin: 0;">Free W&B Alternative on HuggingFace Spaces</p></div>', unsafe_allow_html=True)
|
78 |
+
|
79 |
+
col1, col2, col3 = st.columns([1, 2, 1])
|
80 |
+
|
81 |
+
with col2:
|
82 |
+
st.markdown("### π Connect with HuggingFace")
|
83 |
+
st.markdown("Enter your HuggingFace token to get started with experiment tracking!")
|
84 |
+
|
85 |
+
hf_token = st.text_input(
|
86 |
+
"HuggingFace Token",
|
87 |
+
type="password",
|
88 |
+
placeholder="hf_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
89 |
+
help="Get your token from https://huggingface.co/settings/tokens"
|
90 |
+
)
|
91 |
+
|
92 |
+
if st.button("π Connect & Generate API Key", use_container_width=True):
|
93 |
+
if hf_token:
|
94 |
+
try:
|
95 |
+
# Verify token
|
96 |
+
api = HfApi(token=hf_token)
|
97 |
+
user_info = api.whoami()
|
98 |
+
|
99 |
+
# Store authentication
|
100 |
+
st.session_state.authenticated = True
|
101 |
+
st.session_state.user_token = hf_token
|
102 |
+
st.session_state.api_key = generate_api_key(hf_token)
|
103 |
+
st.session_state.username = user_info['name']
|
104 |
+
|
105 |
+
st.success(f"β
Successfully connected as {user_info['name']}!")
|
106 |
+
time.sleep(1)
|
107 |
+
st.rerun()
|
108 |
+
|
109 |
+
except Exception as e:
|
110 |
+
st.error(f"β Authentication failed: {str(e)}")
|
111 |
+
else:
|
112 |
+
st.error("Please enter your HuggingFace token")
|
113 |
+
|
114 |
+
def show_api_key():
|
115 |
+
"""Display API key and usage instructions"""
|
116 |
+
st.markdown("### π Your API Key")
|
117 |
+
st.markdown(f'<div class="api-key-box"><strong>API Key:</strong> {st.session_state.api_key}</div>', unsafe_allow_html=True)
|
118 |
+
|
119 |
+
st.markdown("### π Usage Instructions")
|
120 |
+
st.code(f"""
|
121 |
+
# Install the client
|
122 |
+
pip install requests
|
123 |
+
|
124 |
+
# Python usage example
|
125 |
+
import requests
|
126 |
+
import json
|
127 |
+
|
128 |
+
API_KEY = "{st.session_state.api_key}"
|
129 |
+
BASE_URL = "https://your-space-url.hf.space"
|
130 |
+
|
131 |
+
# Log metrics
|
132 |
+
def log_metrics(experiment_name, step, metrics):
|
133 |
+
response = requests.post(
|
134 |
+
f"{BASE_URL}/api/log",
|
135 |
+
json={{
|
136 |
+
"api_key": API_KEY,
|
137 |
+
"experiment": experiment_name,
|
138 |
+
"step": step,
|
139 |
+
"metrics": metrics,
|
140 |
+
"timestamp": time.time()
|
141 |
+
}}
|
142 |
+
)
|
143 |
+
return response.json()
|
144 |
+
|
145 |
+
# Example usage
|
146 |
+
log_metrics("my_experiment", 1, {{
|
147 |
+
"loss": 0.5,
|
148 |
+
"accuracy": 0.85,
|
149 |
+
"learning_rate": 0.001
|
150 |
+
}})
|
151 |
+
""", language="python")
|
152 |
+
|
153 |
+
def generate_sample_data():
|
154 |
+
"""Generate sample experiment data for demonstration"""
|
155 |
+
experiments = {
|
156 |
+
"cnn_image_classification": {
|
157 |
+
"created_at": datetime.now() - timedelta(days=2),
|
158 |
+
"metrics": [],
|
159 |
+
"config": {
|
160 |
+
"model": "ResNet50",
|
161 |
+
"dataset": "CIFAR-10",
|
162 |
+
"epochs": 100,
|
163 |
+
"batch_size": 32,
|
164 |
+
"learning_rate": 0.001
|
165 |
+
}
|
166 |
+
},
|
167 |
+
"nlp_sentiment_analysis": {
|
168 |
+
"created_at": datetime.now() - timedelta(days=1),
|
169 |
+
"metrics": [],
|
170 |
+
"config": {
|
171 |
+
"model": "BERT",
|
172 |
+
"dataset": "IMDB",
|
173 |
+
"epochs": 50,
|
174 |
+
"batch_size": 16,
|
175 |
+
"learning_rate": 0.0001
|
176 |
+
}
|
177 |
+
}
|
178 |
+
}
|
179 |
+
|
180 |
+
# Generate sample metrics
|
181 |
+
for exp_name, exp_data in experiments.items():
|
182 |
+
metrics = []
|
183 |
+
for step in range(1, 101):
|
184 |
+
if exp_name == "cnn_image_classification":
|
185 |
+
loss = 2.3 * np.exp(-step/20) + 0.1 + np.random.normal(0, 0.05)
|
186 |
+
accuracy = 1 - 0.9 * np.exp(-step/15) + np.random.normal(0, 0.02)
|
187 |
+
val_loss = loss + np.random.normal(0, 0.1)
|
188 |
+
val_accuracy = accuracy - np.random.normal(0.05, 0.02)
|
189 |
+
|
190 |
+
metrics.append({
|
191 |
+
"step": step,
|
192 |
+
"loss": max(0, loss),
|
193 |
+
"accuracy": max(0, min(1, accuracy)),
|
194 |
+
"val_loss": max(0, val_loss),
|
195 |
+
"val_accuracy": max(0, min(1, val_accuracy)),
|
196 |
+
"timestamp": (datetime.now() - timedelta(days=2) + timedelta(minutes=step*2)).isoformat()
|
197 |
+
})
|
198 |
+
else:
|
199 |
+
loss = 1.8 * np.exp(-step/25) + 0.2 + np.random.normal(0, 0.03)
|
200 |
+
f1_score = 1 - 0.7 * np.exp(-step/20) + np.random.normal(0, 0.02)
|
201 |
+
precision = f1_score + np.random.normal(0, 0.02)
|
202 |
+
recall = f1_score + np.random.normal(0, 0.02)
|
203 |
+
|
204 |
+
metrics.append({
|
205 |
+
"step": step,
|
206 |
+
"loss": max(0, loss),
|
207 |
+
"f1_score": max(0, min(1, f1_score)),
|
208 |
+
"precision": max(0, min(1, precision)),
|
209 |
+
"recall": max(0, min(1, recall)),
|
210 |
+
"timestamp": (datetime.now() - timedelta(days=1) + timedelta(minutes=step*3)).isoformat()
|
211 |
+
})
|
212 |
+
|
213 |
+
exp_data["metrics"] = metrics
|
214 |
+
|
215 |
+
return experiments
|
216 |
+
|
217 |
+
def create_metric_charts(experiment_data):
|
218 |
+
"""Create interactive charts for experiment metrics"""
|
219 |
+
if not experiment_data["metrics"]:
|
220 |
+
st.warning("No metrics data available for this experiment.")
|
221 |
+
return
|
222 |
+
|
223 |
+
df = pd.DataFrame(experiment_data["metrics"])
|
224 |
+
|
225 |
+
# Get all numeric columns (excluding step and timestamp)
|
226 |
+
numeric_cols = [col for col in df.columns if col not in ['step', 'timestamp'] and pd.api.types.is_numeric_dtype(df[col])]
|
227 |
+
|
228 |
+
if not numeric_cols:
|
229 |
+
st.warning("No numeric metrics found.")
|
230 |
+
return
|
231 |
+
|
232 |
+
# Create subplots
|
233 |
+
n_metrics = len(numeric_cols)
|
234 |
+
n_cols = 2
|
235 |
+
n_rows = (n_metrics + n_cols - 1) // n_cols
|
236 |
+
|
237 |
+
fig = make_subplots(
|
238 |
+
rows=n_rows,
|
239 |
+
cols=n_cols,
|
240 |
+
subplot_titles=numeric_cols,
|
241 |
+
vertical_spacing=0.1,
|
242 |
+
horizontal_spacing=0.1
|
243 |
+
)
|
244 |
+
|
245 |
+
colors = px.colors.qualitative.Set3
|
246 |
+
|
247 |
+
for i, metric in enumerate(numeric_cols):
|
248 |
+
row = i // n_cols + 1
|
249 |
+
col = i % n_cols + 1
|
250 |
+
|
251 |
+
fig.add_trace(
|
252 |
+
go.Scatter(
|
253 |
+
x=df['step'],
|
254 |
+
y=df[metric],
|
255 |
+
mode='lines+markers',
|
256 |
+
name=metric,
|
257 |
+
line=dict(color=colors[i % len(colors)], width=2),
|
258 |
+
marker=dict(size=4),
|
259 |
+
hovertemplate=f"<b>{metric}</b><br>Step: %{{x}}<br>Value: %{{y:.4f}}<extra></extra>"
|
260 |
+
),
|
261 |
+
row=row,
|
262 |
+
col=col
|
263 |
+
)
|
264 |
+
|
265 |
+
fig.update_layout(
|
266 |
+
height=400 * n_rows,
|
267 |
+
showlegend=False,
|
268 |
+
title_text="Experiment Metrics Over Time",
|
269 |
+
title_x=0.5,
|
270 |
+
font=dict(size=12)
|
271 |
+
)
|
272 |
+
|
273 |
+
fig.update_xaxes(title_text="Step")
|
274 |
+
fig.update_yaxes(title_text="Value")
|
275 |
+
|
276 |
+
st.plotly_chart(fig, use_container_width=True)
|
277 |
+
|
278 |
+
def show_experiment_dashboard():
|
279 |
+
"""Display the main experiment dashboard"""
|
280 |
+
st.markdown('<div class="main-header"><h1 style="color: white; margin: 0;">π ML Experiment Dashboard</h1></div>', unsafe_allow_html=True)
|
281 |
+
|
282 |
+
# Load sample data if no experiments exist
|
283 |
+
if not st.session_state.experiments:
|
284 |
+
st.session_state.experiments = generate_sample_data()
|
285 |
+
|
286 |
+
# Sidebar for experiment selection
|
287 |
+
with st.sidebar:
|
288 |
+
st.markdown("### π¬ Experiments")
|
289 |
+
|
290 |
+
exp_names = list(st.session_state.experiments.keys())
|
291 |
+
if exp_names:
|
292 |
+
selected_exp = st.selectbox(
|
293 |
+
"Select Experiment",
|
294 |
+
exp_names,
|
295 |
+
key="exp_selector"
|
296 |
+
)
|
297 |
+
st.session_state.current_experiment = selected_exp
|
298 |
+
else:
|
299 |
+
st.info("No experiments found. Start logging metrics to see them here!")
|
300 |
+
return
|
301 |
+
|
302 |
+
st.markdown("### π Quick Stats")
|
303 |
+
if st.session_state.current_experiment:
|
304 |
+
exp_data = st.session_state.experiments[st.session_state.current_experiment]
|
305 |
+
st.metric("Total Steps", len(exp_data["metrics"]))
|
306 |
+
st.metric("Created", exp_data["created_at"].strftime("%Y-%m-%d"))
|
307 |
+
|
308 |
+
# Main dashboard content
|
309 |
+
if st.session_state.current_experiment:
|
310 |
+
exp_data = st.session_state.experiments[st.session_state.current_experiment]
|
311 |
+
|
312 |
+
# Experiment header
|
313 |
+
col1, col2 = st.columns([3, 1])
|
314 |
+
with col1:
|
315 |
+
st.markdown(f"## {st.session_state.current_experiment}")
|
316 |
+
with col2:
|
317 |
+
if st.button("π Refresh", use_container_width=True):
|
318 |
+
st.rerun()
|
319 |
+
|
320 |
+
# Configuration section
|
321 |
+
with st.expander("βοΈ Configuration", expanded=False):
|
322 |
+
config_df = pd.DataFrame(list(exp_data["config"].items()), columns=["Parameter", "Value"])
|
323 |
+
st.dataframe(config_df, use_container_width=True)
|
324 |
+
|
325 |
+
# Metrics overview
|
326 |
+
if exp_data["metrics"]:
|
327 |
+
latest_metrics = exp_data["metrics"][-1]
|
328 |
+
|
329 |
+
st.markdown("### π Latest Metrics")
|
330 |
+
cols = st.columns(len([k for k in latest_metrics.keys() if k not in ['step', 'timestamp']]))
|
331 |
+
|
332 |
+
for i, (key, value) in enumerate(latest_metrics.items()):
|
333 |
+
if key not in ['step', 'timestamp']:
|
334 |
+
with cols[i]:
|
335 |
+
st.metric(key.replace('_', ' ').title(), f"{value:.4f}")
|
336 |
+
|
337 |
+
# Charts section
|
338 |
+
st.markdown("### π Metrics Over Time")
|
339 |
+
create_metric_charts(exp_data)
|
340 |
+
|
341 |
+
# Raw data section
|
342 |
+
with st.expander("π Raw Data", expanded=False):
|
343 |
+
if exp_data["metrics"]:
|
344 |
+
df = pd.DataFrame(exp_data["metrics"])
|
345 |
+
st.dataframe(df, use_container_width=True)
|
346 |
+
else:
|
347 |
+
st.info("No metrics data available.")
|
348 |
+
|
349 |
+
def main():
|
350 |
+
"""Main application logic"""
|
351 |
+
if not st.session_state.authenticated:
|
352 |
+
authenticate_user()
|
353 |
+
else:
|
354 |
+
# Navigation menu
|
355 |
+
selected = option_menu(
|
356 |
+
menu_title=None,
|
357 |
+
options=["Dashboard", "API Key", "Logout"],
|
358 |
+
icons=["graph-up", "key", "box-arrow-right"],
|
359 |
+
menu_icon="cast",
|
360 |
+
default_index=0,
|
361 |
+
orientation="horizontal",
|
362 |
+
styles={
|
363 |
+
"container": {"padding": "0!important", "background-color": "#fafafa"},
|
364 |
+
"icon": {"color": "#667eea", "font-size": "18px"},
|
365 |
+
"nav-link": {"font-size": "16px", "text-align": "center", "margin": "0px", "--hover-color": "#eee"},
|
366 |
+
"nav-link-selected": {"background-color": "#667eea"},
|
367 |
+
}
|
368 |
+
)
|
369 |
+
|
370 |
+
if selected == "Dashboard":
|
371 |
+
show_experiment_dashboard()
|
372 |
+
elif selected == "API Key":
|
373 |
+
show_api_key()
|
374 |
+
elif selected == "Logout":
|
375 |
+
st.session_state.authenticated = False
|
376 |
+
st.session_state.user_token = None
|
377 |
+
st.session_state.api_key = None
|
378 |
+
st.session_state.experiments = {}
|
379 |
+
st.rerun()
|
380 |
+
|
381 |
+
if __name__ == "__main__":
|
382 |
+
main()
|
src/streamlit_app.py
DELETED
@@ -1,40 +0,0 @@
|
|
1 |
-
import altair as alt
|
2 |
-
import numpy as np
|
3 |
-
import pandas as pd
|
4 |
-
import streamlit as st
|
5 |
-
|
6 |
-
"""
|
7 |
-
# Welcome to Streamlit!
|
8 |
-
|
9 |
-
Edit `/streamlit_app.py` to customize this app to your heart's desire :heart:.
|
10 |
-
If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
|
11 |
-
forums](https://discuss.streamlit.io).
|
12 |
-
|
13 |
-
In the meantime, below is an example of what you can do with just a few lines of code:
|
14 |
-
"""
|
15 |
-
|
16 |
-
num_points = st.slider("Number of points in spiral", 1, 10000, 1100)
|
17 |
-
num_turns = st.slider("Number of turns in spiral", 1, 300, 31)
|
18 |
-
|
19 |
-
indices = np.linspace(0, 1, num_points)
|
20 |
-
theta = 2 * np.pi * num_turns * indices
|
21 |
-
radius = indices
|
22 |
-
|
23 |
-
x = radius * np.cos(theta)
|
24 |
-
y = radius * np.sin(theta)
|
25 |
-
|
26 |
-
df = pd.DataFrame({
|
27 |
-
"x": x,
|
28 |
-
"y": y,
|
29 |
-
"idx": indices,
|
30 |
-
"rand": np.random.randn(num_points),
|
31 |
-
})
|
32 |
-
|
33 |
-
st.altair_chart(alt.Chart(df, height=700, width=700)
|
34 |
-
.mark_point(filled=True)
|
35 |
-
.encode(
|
36 |
-
x=alt.X("x", axis=None),
|
37 |
-
y=alt.Y("y", axis=None),
|
38 |
-
color=alt.Color("idx", legend=None, scale=alt.Scale()),
|
39 |
-
size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
|
40 |
-
))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|