Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -4,11 +4,10 @@ import numpy as np
|
|
| 4 |
import seaborn as sns
|
| 5 |
import matplotlib.pyplot as plt
|
| 6 |
from statistics import mode, StatisticsError
|
| 7 |
-
import io
|
| 8 |
|
| 9 |
-
#
|
| 10 |
from sklearn.model_selection import train_test_split, GridSearchCV
|
| 11 |
-
from sklearn.preprocessing import StandardScaler
|
| 12 |
from sklearn.pipeline import Pipeline
|
| 13 |
from sklearn.metrics import confusion_matrix, accuracy_score, f1_score
|
| 14 |
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
|
|
@@ -16,29 +15,22 @@ from sklearn.linear_model import LogisticRegression
|
|
| 16 |
from sklearn.svm import SVC
|
| 17 |
from sklearn.neural_network import MLPClassifier
|
| 18 |
|
| 19 |
-
#
|
| 20 |
import torch
|
| 21 |
import torch.nn as nn
|
| 22 |
import torch.nn.functional as F
|
| 23 |
-
import torch.utils.data
|
| 24 |
-
import random
|
| 25 |
|
| 26 |
-
#
|
| 27 |
-
|
| 28 |
-
page_title="WEEKO - AI Reuse Analyzer",
|
| 29 |
-
page_icon="♻️",
|
| 30 |
-
layout="wide"
|
| 31 |
-
)
|
| 32 |
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
|
|
|
| 36 |
|
|
|
|
| 37 |
class DummyTabTransformerClassifier:
|
| 38 |
-
"""
|
| 39 |
-
Placeholder modello: in realtà è un MLP,
|
| 40 |
-
ma finge di essere un TabTransformer
|
| 41 |
-
"""
|
| 42 |
def __init__(self, input_dim=8):
|
| 43 |
self.clf = MLPClassifier(hidden_layer_sizes=(max(16,input_dim*2), max(8,input_dim)),
|
| 44 |
max_iter=100, random_state=42, alpha=0.01, learning_rate_init=0.01)
|
|
@@ -52,13 +44,10 @@ class DummyTabTransformerClassifier:
|
|
| 52 |
return self.clf.predict_proba(X)
|
| 53 |
else:
|
| 54 |
preds = self.clf.predict(X)
|
| 55 |
-
return np.array([[1.0,
|
| 56 |
|
| 57 |
class DummySAINTClassifier:
|
| 58 |
-
"""
|
| 59 |
-
Placeholder modello: in realtà è un MLP,
|
| 60 |
-
ma finge di essere un SAINT
|
| 61 |
-
"""
|
| 62 |
def __init__(self, input_dim=8):
|
| 63 |
self.clf = MLPClassifier(hidden_layer_sizes=(max(20,input_dim*2), max(10,input_dim)),
|
| 64 |
max_iter=120, random_state=42, alpha=0.005, learning_rate_init=0.005)
|
|
@@ -72,7 +61,7 @@ class DummySAINTClassifier:
|
|
| 72 |
return self.clf.predict_proba(X)
|
| 73 |
else:
|
| 74 |
preds = self.clf.predict(X)
|
| 75 |
-
return np.array([[1.0,0.0] if p
|
| 76 |
|
| 77 |
MODELS = {
|
| 78 |
"Random Forest": RandomForestClassifier(random_state=42, n_estimators=100, class_weight='balanced'),
|
|
@@ -83,10 +72,7 @@ MODELS = {
|
|
| 83 |
"SAINT (Dummy)": DummySAINTClassifier()
|
| 84 |
}
|
| 85 |
|
| 86 |
-
|
| 87 |
-
# 2) VAE per generative reuse (Fase 2)
|
| 88 |
-
##########################################
|
| 89 |
-
|
| 90 |
class MiniVAE(nn.Module):
|
| 91 |
def __init__(self, input_dim=5, latent_dim=2):
|
| 92 |
super().__init__()
|
|
@@ -103,7 +89,7 @@ class MiniVAE(nn.Module):
|
|
| 103 |
def reparameterize(self, mu, logvar):
|
| 104 |
std = torch.exp(0.5 * logvar)
|
| 105 |
eps = torch.randn_like(std)
|
| 106 |
-
return mu + eps
|
| 107 |
|
| 108 |
def decode(self, z):
|
| 109 |
h = F.relu(self.fc3(z))
|
|
@@ -120,57 +106,53 @@ def vae_loss(recon_x, x, mu, logvar):
|
|
| 120 |
kld = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp())
|
| 121 |
return recon_loss + kld
|
| 122 |
|
| 123 |
-
|
| 124 |
-
# Feature sets
|
| 125 |
-
##########################################
|
| 126 |
DEFAULT_FEATURES_STEP1 = ['length','width','RUL','margin','shape','weight','thickness']
|
| 127 |
-
ML_FEATURES_STEP1
|
| 128 |
-
VAE_FEATURES_STEP2
|
| 129 |
|
| 130 |
-
|
| 131 |
-
# Mappatura forma -> shape_code
|
| 132 |
-
##########################################
|
| 133 |
SHAPE_MAPPING = {
|
| 134 |
-
'axisymmetric':
|
| 135 |
-
'sheet_metal':
|
| 136 |
-
'alloy_plate':
|
| 137 |
-
'complex_plastic':
|
| 138 |
}
|
| 139 |
|
| 140 |
-
|
| 141 |
-
# Generazione dataset sintetico
|
| 142 |
-
##########################################
|
| 143 |
def generate_synthetic_data(n_samples=300, seed=42):
|
| 144 |
np.random.seed(seed)
|
| 145 |
-
length = np.clip(np.random.normal(100,
|
| 146 |
-
width = np.clip(np.random.normal(50,
|
| 147 |
-
RUL = np.clip(np.random.normal(500,
|
| 148 |
-
margin = np.clip(np.random.normal(150,150,n_samples)
|
| 149 |
-
shapes = np.random.choice([
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
thickness = np.clip(np.random.normal(8,4,n_samples), 0.5, 30)
|
| 153 |
|
| 154 |
return pd.DataFrame({
|
| 155 |
-
'length': length,
|
| 156 |
-
'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
})
|
| 158 |
|
| 159 |
-
|
| 160 |
-
# dimension_match + assegnazione classe
|
| 161 |
-
##########################################
|
| 162 |
def dimension_match(row, target_length, target_width, target_shape,
|
| 163 |
target_weight, target_thickness,
|
| 164 |
tol_len, tol_wid, tol_weight, tol_thickness):
|
| 165 |
-
cond_length = abs(row['length'] - target_length)
|
| 166 |
-
cond_width = abs(row['width'] - target_width)
|
| 167 |
-
cond_shape = row['shape']
|
| 168 |
-
cond_weight = abs(row['weight']
|
| 169 |
-
|
| 170 |
-
return 1 if (cond_length and cond_width and cond_shape and cond_weight and
|
| 171 |
|
| 172 |
def assign_class(row, threshold_score=0.5, alpha=0.5, beta=0.5):
|
| 173 |
-
rul_norm
|
| 174 |
margin_norm = (row['margin']+200)/800.0
|
| 175 |
score = alpha*rul_norm + beta*margin_norm
|
| 176 |
if row['compat_dim']==1 and score>=threshold_score:
|
|
@@ -178,28 +160,25 @@ def assign_class(row, threshold_score=0.5, alpha=0.5, beta=0.5):
|
|
| 178 |
else:
|
| 179 |
return "Upcycling Creativo"
|
| 180 |
|
| 181 |
-
|
| 182 |
-
# --- Fase 1: Preparazione Dataset
|
| 183 |
-
##########################################
|
| 184 |
def prepare_dataset():
|
| 185 |
st.header("♻️ 1. Preparazione Dataset EoL")
|
| 186 |
-
|
| 187 |
-
tab1, tab2 = st.tabs(["Carica/Genera Dati", "Definisci Compatibilità & Target"])
|
| 188 |
data = None
|
| 189 |
with tab1:
|
| 190 |
-
|
| 191 |
-
if
|
| 192 |
-
|
| 193 |
if st.button("Genera Dati"):
|
| 194 |
-
data=generate_synthetic_data(n_samples=
|
| 195 |
st.session_state.data_source="generated"
|
| 196 |
else:
|
| 197 |
-
|
| 198 |
-
if
|
| 199 |
try:
|
| 200 |
-
data=pd.read_csv(
|
| 201 |
if not all(col in data.columns for col in DEFAULT_FEATURES_STEP1):
|
| 202 |
-
st.error(f"
|
| 203 |
data=None
|
| 204 |
else:
|
| 205 |
st.session_state.data_source="uploaded"
|
|
@@ -209,111 +188,100 @@ def prepare_dataset():
|
|
| 209 |
|
| 210 |
if data is not None:
|
| 211 |
with tab2:
|
| 212 |
-
st.subheader("Parametri
|
| 213 |
c1,c2=st.columns(2)
|
| 214 |
with c1:
|
| 215 |
-
t_len=st.number_input("Lunghezza target (mm)",50.0,250.0,100.0
|
| 216 |
-
t_wid=st.number_input("Larghezza target (mm)",20.0,150.0,50.0
|
| 217 |
-
t_shape=st.selectbox("Forma target", list(SHAPE_MAPPING.keys())
|
| 218 |
with c2:
|
| 219 |
-
t_weight=st.number_input("Peso target (kg)",10.0,
|
| 220 |
-
t_thick=st.number_input("Spessore target (mm)",0.5,
|
| 221 |
|
| 222 |
st.markdown("**Tolleranze**")
|
| 223 |
col_tol1,col_tol2=st.columns(2)
|
| 224 |
with col_tol1:
|
| 225 |
-
tol_len=st.slider("
|
| 226 |
-
tol_wid=st.slider("
|
| 227 |
with col_tol2:
|
| 228 |
-
tol_we=st.slider("
|
| 229 |
-
tol_th=st.slider("
|
| 230 |
|
| 231 |
st.markdown("**Score RUL & Margin**")
|
| 232 |
-
|
| 233 |
-
alpha=st.slider("Peso RUL
|
| 234 |
-
beta=st.slider("Peso Margin
|
| 235 |
|
| 236 |
-
#
|
| 237 |
data['shape_code']=data['shape'].map(SHAPE_MAPPING).fillna(-1).astype(int)
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
alpha=alpha, beta=beta),
|
| 253 |
-
axis=1)
|
| 254 |
|
| 255 |
st.session_state.target_params={
|
| 256 |
-
"target_length": t_len,
|
| 257 |
-
"
|
| 258 |
-
"target_shape": t_shape,
|
| 259 |
-
"target_weight": t_weight,
|
| 260 |
"target_thickness": t_thick,
|
| 261 |
-
"tol_len": tol_len,
|
| 262 |
-
"
|
| 263 |
-
"tol_weight": tol_we,
|
| 264 |
-
"tol_thickness": tol_th
|
| 265 |
}
|
| 266 |
st.session_state.score_params={
|
| 267 |
-
"threshold_score":
|
| 268 |
"alpha": alpha,
|
| 269 |
"beta": beta
|
| 270 |
}
|
| 271 |
|
| 272 |
st.dataframe(data.head(10))
|
| 273 |
-
st.write("Distribuzione
|
| 274 |
|
| 275 |
-
numeric_cols
|
| 276 |
if not numeric_cols.empty:
|
| 277 |
-
fig,ax=plt.subplots(
|
| 278 |
sns.heatmap(numeric_cols.corr(), annot=True, cmap='viridis', fmt=".2f", ax=ax)
|
| 279 |
st.pyplot(fig)
|
| 280 |
|
| 281 |
st.session_state.data=data
|
| 282 |
-
|
| 283 |
-
st.download_button("Scarica Dataset Elaborato
|
| 284 |
|
| 285 |
-
|
| 286 |
-
# Fase 2: Training Modelli ML
|
| 287 |
-
#######################################
|
| 288 |
def train_models(data):
|
| 289 |
st.header("🤖 2. Addestramento ML (Riutilizzo vs Upcycling)")
|
| 290 |
if data is None:
|
| 291 |
-
st.error("
|
| 292 |
return
|
| 293 |
if 'Target' not in data.columns:
|
| 294 |
-
st.error("Colonna 'Target'
|
| 295 |
return
|
| 296 |
|
| 297 |
-
# X,y
|
| 298 |
features_to_use=[f for f in ML_FEATURES_STEP1 if f in data.columns]
|
| 299 |
if not features_to_use:
|
| 300 |
-
st.error("Nessuna feature
|
| 301 |
return
|
| 302 |
|
| 303 |
-
X=data[features_to_use].copy()
|
| 304 |
-
y=data['Target'].map({"Riutilizzo Funzionale":0,
|
| 305 |
-
|
| 306 |
-
# Se c'è una sola classe
|
| 307 |
if len(y.unique())<2:
|
| 308 |
-
st.error("Il dataset
|
| 309 |
return
|
|
|
|
|
|
|
| 310 |
|
| 311 |
-
|
| 312 |
-
st.write(f"Dati: {len(X_train)} train, {len(X_test)} test.")
|
| 313 |
-
tune_rf=st.checkbox("Ottimizza RandomForest (GridSearchCV)", value=False)
|
| 314 |
-
|
| 315 |
trained_pipelines={}
|
| 316 |
results=[]
|
|
|
|
| 317 |
for name,model in MODELS.items():
|
| 318 |
st.subheader(f"Modello: {name}")
|
| 319 |
from sklearn.pipeline import Pipeline
|
|
@@ -323,17 +291,18 @@ def train_models(data):
|
|
| 323 |
])
|
| 324 |
try:
|
| 325 |
if tune_rf and name=="Random Forest":
|
| 326 |
-
st.write("
|
| 327 |
param_grid={
|
| 328 |
'clf__n_estimators':[50,100],
|
| 329 |
-
'clf__max_depth':[None,10,
|
| 330 |
}
|
| 331 |
-
|
|
|
|
| 332 |
grid.fit(X_train,y_train)
|
| 333 |
-
|
| 334 |
st.write(f"Migliori parametri: {grid.best_params_}")
|
| 335 |
-
y_pred=
|
| 336 |
-
pipe_to_use=
|
| 337 |
else:
|
| 338 |
pipe.fit(X_train,y_train)
|
| 339 |
y_pred=pipe.predict(X_test)
|
|
@@ -341,16 +310,15 @@ def train_models(data):
|
|
| 341 |
|
| 342 |
acc=accuracy_score(y_test,y_pred)
|
| 343 |
f1=f1_score(y_test,y_pred,average='weighted')
|
| 344 |
-
results.append({"Modello":name,"Accuracy":acc,"F1 Score":f1})
|
| 345 |
trained_pipelines[name]=pipe_to_use
|
| 346 |
|
| 347 |
cm=confusion_matrix(y_test,y_pred)
|
| 348 |
fig,ax=plt.subplots()
|
| 349 |
-
sns.heatmap(cm,annot=True,fmt='d',
|
| 350 |
plt.xlabel("Pred")
|
| 351 |
plt.ylabel("True")
|
| 352 |
st.pyplot(fig)
|
| 353 |
-
|
| 354 |
st.metric("Accuracy",f"{acc:.3f}")
|
| 355 |
st.metric("F1 Score",f"{f1:.3f}")
|
| 356 |
|
|
@@ -358,295 +326,323 @@ def train_models(data):
|
|
| 358 |
st.error(f"Errore addestramento {name}: {e}")
|
| 359 |
|
| 360 |
if results:
|
| 361 |
-
|
| 362 |
-
st.dataframe(
|
| 363 |
-
st.session_state.train_results=
|
| 364 |
st.session_state.models=trained_pipelines
|
| 365 |
else:
|
| 366 |
st.error("Nessun modello addestrato con successo.")
|
| 367 |
st.session_state.models=None
|
| 368 |
|
| 369 |
-
|
| 370 |
-
# Fase 3: Inferenza + Trigger Upcycling -> VAE
|
| 371 |
-
#######################################
|
| 372 |
def model_inference(trained_pipelines, data):
|
| 373 |
-
st.header("🔮 3. Inferenza: Step 1
|
| 374 |
|
| 375 |
if not trained_pipelines:
|
| 376 |
-
st.error("Prima addestra i modelli (Fase 2).")
|
| 377 |
return
|
| 378 |
if data is None:
|
| 379 |
-
st.error("Nessun dataset
|
| 380 |
-
return
|
| 381 |
-
if 'target_params' not in st.session_state or 'score_params' not in st.session_state:
|
| 382 |
-
st.error("Parametri target non definiti. Completa la Fase 1.")
|
| 383 |
return
|
| 384 |
|
| 385 |
-
|
|
|
|
| 386 |
|
| 387 |
-
with st.form(
|
| 388 |
-
st.subheader("Inserisci
|
| 389 |
c1,c2,c3=st.columns(3)
|
| 390 |
with c1:
|
| 391 |
length=st.number_input("Lunghezza (mm)",0.0,300.0,float(data_stats['length'].median()),step=1.0)
|
| 392 |
-
width=st.number_input("Larghezza (mm)",0.0,200.0,float(data_stats['width'].median()),step=1.0)
|
| 393 |
-
shape_name=st.selectbox("Forma", list(SHAPE_MAPPING.keys()))
|
| 394 |
with c2:
|
| 395 |
-
weight=st.number_input("Peso (kg)",0.0,300.0,float(data_stats['weight'].median()),step=1.0)
|
| 396 |
-
thickness=st.number_input("Spessore (mm)",0.0,50.0,float(data_stats['thickness'].median()),step=0.5)
|
| 397 |
-
RUL=st.number_input("RUL (0-1000)",0,1000,int(data_stats['RUL'].median()))
|
| 398 |
with c3:
|
| 399 |
-
val_merc=st.number_input("Valore Mercato (€)",0.0,1e5,
|
| 400 |
-
costo_rip=st.number_input("Costo Riparazione (€)",0.0,1e5,50.0,step=10.0)
|
| 401 |
|
| 402 |
-
|
| 403 |
|
| 404 |
-
if
|
| 405 |
margin= val_merc - costo_rip
|
| 406 |
-
shape_code= SHAPE_MAPPING.get(shape_name
|
| 407 |
|
| 408 |
-
# Prepariamo input per dimension_match
|
| 409 |
input_dict={
|
| 410 |
-
"length":
|
| 411 |
-
"width":
|
| 412 |
-
"shape":
|
| 413 |
-
"weight":
|
| 414 |
-
"thickness":
|
| 415 |
-
"RUL":
|
| 416 |
-
"margin":
|
| 417 |
}
|
| 418 |
temp_df=pd.DataFrame([input_dict])
|
| 419 |
|
| 420 |
-
#
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
temp_df['
|
| 426 |
-
|
| 427 |
|
| 428 |
-
#
|
|
|
|
|
|
|
| 429 |
try:
|
| 430 |
-
X_inference=
|
| 431 |
except KeyError as e:
|
| 432 |
-
st.error(f"Mancano
|
| 433 |
return
|
| 434 |
|
| 435 |
-
#
|
| 436 |
-
|
| 437 |
details=[]
|
| 438 |
for name,pipe in trained_pipelines.items():
|
| 439 |
try:
|
| 440 |
-
|
| 441 |
proba=pipe.predict_proba(X_inference)[0]
|
| 442 |
details.append({
|
| 443 |
-
"Modello":
|
| 444 |
-
"Pred
|
| 445 |
-
"
|
| 446 |
-
"
|
| 447 |
})
|
| 448 |
-
|
| 449 |
except Exception as e:
|
| 450 |
st.error(f"Errore predizione {name}: {e}")
|
| 451 |
|
| 452 |
-
if not
|
| 453 |
-
st.error("
|
| 454 |
return
|
| 455 |
-
|
| 456 |
# Aggrega con mode
|
|
|
|
| 457 |
try:
|
| 458 |
-
final_pred=mode(
|
| 459 |
except StatisticsError:
|
| 460 |
-
# Se c'è
|
| 461 |
-
|
| 462 |
-
final_pred=1 if
|
| 463 |
|
| 464 |
final_label="Riutilizzo Funzionale" if final_pred==0 else "Upcycling Creativo"
|
| 465 |
-
st.subheader("Risultato Aggregato
|
| 466 |
-
st.metric("Classe
|
| 467 |
|
| 468 |
-
with st.expander("Dettagli
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
st.dataframe(
|
| 473 |
|
| 474 |
-
# Se
|
| 475 |
if final_label=="Upcycling Creativo":
|
| 476 |
st.markdown("---")
|
| 477 |
-
st.subheader("
|
| 478 |
|
| 479 |
if not st.session_state.get("vae_trained_on_eol",False):
|
| 480 |
-
st.error("VAE non addestrato. Vai
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 510 |
else:
|
| 511 |
-
st.success("
|
| 512 |
|
| 513 |
-
|
| 514 |
-
# Fase 4: Training VAE
|
| 515 |
-
#######################################
|
| 516 |
def vae_training_phase():
|
| 517 |
-
st.header("🧬 Training VAE
|
| 518 |
-
|
| 519 |
if 'data' not in st.session_state or st.session_state['data'] is None:
|
| 520 |
-
st.error("
|
| 521 |
return
|
| 522 |
-
|
| 523 |
data=st.session_state['data']
|
| 524 |
-
|
| 525 |
-
feats=
|
| 526 |
if not feats:
|
| 527 |
-
st.error(f"
|
| 528 |
return
|
| 529 |
-
st.write(f"
|
| 530 |
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
bs=st.selectbox("Batch Size",[16,32,64,128],index=1)
|
| 536 |
|
| 537 |
if not st.session_state.get("vae_trained_on_eol",False):
|
| 538 |
-
st.warning("VAE non addestrato
|
| 539 |
if st.button("Avvia Training VAE"):
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
|
|
|
|
|
|
|
|
|
| 544 |
for c in X_vae.columns:
|
| 545 |
if X_vae[c].isnull().any():
|
| 546 |
X_vae[c].fillna(X_vae[c].median(), inplace=True)
|
| 547 |
|
| 548 |
# Scalatura
|
| 549 |
-
|
| 550 |
-
scaler
|
| 551 |
-
|
| 552 |
-
st.session_state["vae_scaler"]=scaler
|
| 553 |
|
| 554 |
dataset=torch.utils.data.TensorDataset(torch.tensor(X_scaled,dtype=torch.float32))
|
| 555 |
-
loader=torch.utils.data.DataLoader(dataset,batch_size=bs,shuffle=True)
|
| 556 |
-
|
|
|
|
| 557 |
|
| 558 |
losses=[]
|
| 559 |
vae.train()
|
| 560 |
for epoch in range(int(ep)):
|
| 561 |
-
epoch_loss=0
|
| 562 |
for (batch,) in loader:
|
| 563 |
optimizer.zero_grad()
|
| 564 |
-
recon,mu,logvar=vae(batch)
|
| 565 |
-
loss=vae_loss(recon,batch,mu,logvar)
|
| 566 |
loss.backward()
|
| 567 |
optimizer.step()
|
| 568 |
-
epoch_loss+=loss.item()
|
| 569 |
-
|
| 570 |
-
losses.append(
|
| 571 |
st.progress((epoch+1)/ep)
|
| 572 |
st.session_state["vae_trained_on_eol"]=True
|
| 573 |
-
st.success(f"
|
| 574 |
st.line_chart(losses)
|
| 575 |
else:
|
| 576 |
-
st.success("VAE
|
| 577 |
-
if st.button("Riallena
|
| 578 |
st.session_state["vae_trained_on_eol"]=False
|
| 579 |
st.rerun()
|
| 580 |
|
| 581 |
-
|
| 582 |
-
# Fase 5: Dashboard
|
| 583 |
-
#######################################
|
| 584 |
def show_dashboard():
|
| 585 |
st.header("📊 Dashboard")
|
| 586 |
-
|
| 587 |
-
|
|
|
|
| 588 |
return
|
| 589 |
-
|
| 590 |
st.subheader("Distribuzione Classi EoL")
|
| 591 |
st.write(data['Target'].value_counts())
|
| 592 |
|
| 593 |
if 'train_results' in st.session_state and st.session_state['train_results'] is not None:
|
| 594 |
-
st.subheader("Risultati Modelli ML")
|
| 595 |
df_res=st.session_state['train_results']
|
|
|
|
| 596 |
st.dataframe(df_res)
|
| 597 |
else:
|
| 598 |
-
st.info("
|
| 599 |
|
| 600 |
st.subheader("Stato VAE")
|
| 601 |
if st.session_state.get("vae_trained_on_eol",False):
|
| 602 |
-
st.success("VAE
|
| 603 |
else:
|
| 604 |
-
st.warning("VAE non addestrato
|
| 605 |
|
| 606 |
-
|
| 607 |
-
# Fase 6: Guida
|
| 608 |
-
#######################################
|
| 609 |
def show_help():
|
| 610 |
-
st.header("ℹ️ Guida")
|
| 611 |
st.markdown("""
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 633 |
def reset_app():
|
| 634 |
-
|
| 635 |
-
'data','models','train_results','vae','vae_trained_on_eol',
|
| 636 |
-
'vae_scaler','target_params','score_params','data_source'
|
| 637 |
-
]
|
| 638 |
-
for k in keys:
|
| 639 |
if k in st.session_state:
|
| 640 |
del st.session_state[k]
|
| 641 |
-
st.success("
|
| 642 |
st.experimental_rerun()
|
| 643 |
|
| 644 |
-
|
| 645 |
-
# MAIN
|
| 646 |
-
#######################################
|
| 647 |
def main():
|
| 648 |
-
st.sidebar.title("Menu
|
| 649 |
-
step=st.sidebar.radio("Fasi", [
|
| 650 |
"♻️ Dataset",
|
| 651 |
"🤖 Addestramento ML (Step 1)",
|
| 652 |
"🔮 Inferenza (Step 1 & 2)",
|
|
@@ -661,8 +657,8 @@ def main():
|
|
| 661 |
elif step=="🤖 Addestramento ML (Step 1)":
|
| 662 |
train_models(st.session_state.get('data',None))
|
| 663 |
elif step=="🔮 Inferenza (Step 1 & 2)":
|
| 664 |
-
if
|
| 665 |
-
st.error("
|
| 666 |
else:
|
| 667 |
model_inference(st.session_state['models'], st.session_state.get('data',None))
|
| 668 |
elif step=="🧬 Training VAE (Step 2)":
|
|
@@ -674,3 +670,4 @@ def main():
|
|
| 674 |
|
| 675 |
if __name__=="__main__":
|
| 676 |
main()
|
|
|
|
|
|
| 4 |
import seaborn as sns
|
| 5 |
import matplotlib.pyplot as plt
|
| 6 |
from statistics import mode, StatisticsError
|
|
|
|
| 7 |
|
| 8 |
+
# Scikit-learn
|
| 9 |
from sklearn.model_selection import train_test_split, GridSearchCV
|
| 10 |
+
from sklearn.preprocessing import StandardScaler
|
| 11 |
from sklearn.pipeline import Pipeline
|
| 12 |
from sklearn.metrics import confusion_matrix, accuracy_score, f1_score
|
| 13 |
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
|
|
|
|
| 15 |
from sklearn.svm import SVC
|
| 16 |
from sklearn.neural_network import MLPClassifier
|
| 17 |
|
| 18 |
+
# PyTorch
|
| 19 |
import torch
|
| 20 |
import torch.nn as nn
|
| 21 |
import torch.nn.functional as F
|
|
|
|
|
|
|
| 22 |
|
| 23 |
+
# Transformers per la GenAI testuale
|
| 24 |
+
from transformers import pipeline
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
+
############### STREAMLIT SETUP ###############
|
| 27 |
+
st.set_page_config(page_title="WEEKO - AI Reuse Analyzer",
|
| 28 |
+
page_icon="♻️",
|
| 29 |
+
layout="wide")
|
| 30 |
|
| 31 |
+
############### MODELLI PLACEHOLDER ############
|
| 32 |
class DummyTabTransformerClassifier:
|
| 33 |
+
"""Finto modello: in realtà un MLP."""
|
|
|
|
|
|
|
|
|
|
| 34 |
def __init__(self, input_dim=8):
|
| 35 |
self.clf = MLPClassifier(hidden_layer_sizes=(max(16,input_dim*2), max(8,input_dim)),
|
| 36 |
max_iter=100, random_state=42, alpha=0.01, learning_rate_init=0.01)
|
|
|
|
| 44 |
return self.clf.predict_proba(X)
|
| 45 |
else:
|
| 46 |
preds = self.clf.predict(X)
|
| 47 |
+
return np.array([[1.0,0.0] if p==0 else [0.0,1.0] for p in preds])
|
| 48 |
|
| 49 |
class DummySAINTClassifier:
|
| 50 |
+
"""Finto modello: in realtà un MLP."""
|
|
|
|
|
|
|
|
|
|
| 51 |
def __init__(self, input_dim=8):
|
| 52 |
self.clf = MLPClassifier(hidden_layer_sizes=(max(20,input_dim*2), max(10,input_dim)),
|
| 53 |
max_iter=120, random_state=42, alpha=0.005, learning_rate_init=0.005)
|
|
|
|
| 61 |
return self.clf.predict_proba(X)
|
| 62 |
else:
|
| 63 |
preds = self.clf.predict(X)
|
| 64 |
+
return np.array([[1.0,0.0] if p==0 else [0.0,1.0] for p in preds])
|
| 65 |
|
| 66 |
MODELS = {
|
| 67 |
"Random Forest": RandomForestClassifier(random_state=42, n_estimators=100, class_weight='balanced'),
|
|
|
|
| 72 |
"SAINT (Dummy)": DummySAINTClassifier()
|
| 73 |
}
|
| 74 |
|
| 75 |
+
############### VAE PER FASE 2 ###############
|
|
|
|
|
|
|
|
|
|
| 76 |
class MiniVAE(nn.Module):
|
| 77 |
def __init__(self, input_dim=5, latent_dim=2):
|
| 78 |
super().__init__()
|
|
|
|
| 89 |
def reparameterize(self, mu, logvar):
|
| 90 |
std = torch.exp(0.5 * logvar)
|
| 91 |
eps = torch.randn_like(std)
|
| 92 |
+
return mu + eps*std
|
| 93 |
|
| 94 |
def decode(self, z):
|
| 95 |
h = F.relu(self.fc3(z))
|
|
|
|
| 106 |
kld = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp())
|
| 107 |
return recon_loss + kld
|
| 108 |
|
| 109 |
+
############### Feature sets ###############
|
|
|
|
|
|
|
| 110 |
DEFAULT_FEATURES_STEP1 = ['length','width','RUL','margin','shape','weight','thickness']
|
| 111 |
+
ML_FEATURES_STEP1 = ['length','width','shape_code','weight','thickness','RUL','margin','compat_dim']
|
| 112 |
+
VAE_FEATURES_STEP2 = ['length','width','weight','thickness','shape_code']
|
| 113 |
|
| 114 |
+
############### SHAPE MAPPING ###############
|
|
|
|
|
|
|
| 115 |
SHAPE_MAPPING = {
|
| 116 |
+
'axisymmetric':0,
|
| 117 |
+
'sheet_metal':1,
|
| 118 |
+
'alloy_plate':2,
|
| 119 |
+
'complex_plastic':3
|
| 120 |
}
|
| 121 |
|
| 122 |
+
############### GENERAZIONE DATI SINTETICI ###############
|
|
|
|
|
|
|
| 123 |
def generate_synthetic_data(n_samples=300, seed=42):
|
| 124 |
np.random.seed(seed)
|
| 125 |
+
length = np.clip(np.random.normal(100,20,n_samples),50,250)
|
| 126 |
+
width = np.clip(np.random.normal(50,15,n_samples),20,150)
|
| 127 |
+
RUL = np.clip(np.random.normal(500,250,n_samples),0,1000).astype(int)
|
| 128 |
+
margin = np.clip(np.random.normal(150,150,n_samples),-200,600).astype(int)
|
| 129 |
+
shapes = np.random.choice(list(SHAPE_MAPPING.keys()), p=[0.4,0.3,0.2,0.1], size=n_samples)
|
| 130 |
+
weight = np.clip(np.random.normal(80,30,n_samples),10,250)
|
| 131 |
+
thickness= np.clip(np.random.normal(8,4,n_samples),0.5,30)
|
|
|
|
| 132 |
|
| 133 |
return pd.DataFrame({
|
| 134 |
+
'length': length,
|
| 135 |
+
'width': width,
|
| 136 |
+
'RUL': RUL,
|
| 137 |
+
'margin': margin,
|
| 138 |
+
'shape': shapes,
|
| 139 |
+
'weight': weight,
|
| 140 |
+
'thickness': thickness
|
| 141 |
})
|
| 142 |
|
| 143 |
+
############### dimension_match + assign_class ###############
|
|
|
|
|
|
|
| 144 |
def dimension_match(row, target_length, target_width, target_shape,
|
| 145 |
target_weight, target_thickness,
|
| 146 |
tol_len, tol_wid, tol_weight, tol_thickness):
|
| 147 |
+
cond_length = abs(row['length'] - target_length)<= tol_len
|
| 148 |
+
cond_width = abs(row['width'] - target_width) <= tol_wid
|
| 149 |
+
cond_shape = (row['shape']==target_shape)
|
| 150 |
+
cond_weight = abs(row['weight']-target_weight)<=tol_weight
|
| 151 |
+
cond_thick = abs(row['thickness']-target_thickness)<=tol_thickness
|
| 152 |
+
return 1 if (cond_length and cond_width and cond_shape and cond_weight and cond_thick) else 0
|
| 153 |
|
| 154 |
def assign_class(row, threshold_score=0.5, alpha=0.5, beta=0.5):
|
| 155 |
+
rul_norm = row['RUL']/1000.0
|
| 156 |
margin_norm = (row['margin']+200)/800.0
|
| 157 |
score = alpha*rul_norm + beta*margin_norm
|
| 158 |
if row['compat_dim']==1 and score>=threshold_score:
|
|
|
|
| 160 |
else:
|
| 161 |
return "Upcycling Creativo"
|
| 162 |
|
| 163 |
+
############### 1) PHASE: PREPARE DATASET ###############
|
|
|
|
|
|
|
| 164 |
def prepare_dataset():
|
| 165 |
st.header("♻️ 1. Preparazione Dataset EoL")
|
| 166 |
+
tab1, tab2 = st.tabs(["Carica/Genera Dati","Definisci Compatibilità & Target"])
|
|
|
|
| 167 |
data = None
|
| 168 |
with tab1:
|
| 169 |
+
data_opt = st.radio("Fonte Dati", ["Genera dati sintetici","Carica CSV"], horizontal=True)
|
| 170 |
+
if data_opt=="Genera dati sintetici":
|
| 171 |
+
ns=st.slider("Campioni sintetici",100,2000,500,step=100)
|
| 172 |
if st.button("Genera Dati"):
|
| 173 |
+
data=generate_synthetic_data(n_samples=ns)
|
| 174 |
st.session_state.data_source="generated"
|
| 175 |
else:
|
| 176 |
+
upfile=st.file_uploader("Carica CSV con feature minime", type=["csv"])
|
| 177 |
+
if upfile:
|
| 178 |
try:
|
| 179 |
+
data=pd.read_csv(upfile)
|
| 180 |
if not all(col in data.columns for col in DEFAULT_FEATURES_STEP1):
|
| 181 |
+
st.error(f"Mancano colonne minime: {DEFAULT_FEATURES_STEP1}")
|
| 182 |
data=None
|
| 183 |
else:
|
| 184 |
st.session_state.data_source="uploaded"
|
|
|
|
| 188 |
|
| 189 |
if data is not None:
|
| 190 |
with tab2:
|
| 191 |
+
st.subheader("Parametri per Compatibilità")
|
| 192 |
c1,c2=st.columns(2)
|
| 193 |
with c1:
|
| 194 |
+
t_len=st.number_input("Lunghezza target (mm)",50.0,250.0,100.0)
|
| 195 |
+
t_wid=st.number_input("Larghezza target (mm)",20.0,150.0,50.0)
|
| 196 |
+
t_shape=st.selectbox("Forma target", list(SHAPE_MAPPING.keys()))
|
| 197 |
with c2:
|
| 198 |
+
t_weight=st.number_input("Peso target (kg)",10.0,300.0,80.0)
|
| 199 |
+
t_thick=st.number_input("Spessore target (mm)",0.5,50.0,8.0)
|
| 200 |
|
| 201 |
st.markdown("**Tolleranze**")
|
| 202 |
col_tol1,col_tol2=st.columns(2)
|
| 203 |
with col_tol1:
|
| 204 |
+
tol_len=st.slider("Tol. lunghezza ±",0.0,30.0,5.0,step=0.5)
|
| 205 |
+
tol_wid=st.slider("Tol. larghezza ±",0.0,20.0,3.0,step=0.5)
|
| 206 |
with col_tol2:
|
| 207 |
+
tol_we=st.slider("Tol. peso ±",0.0,50.0,10.0,step=1.0)
|
| 208 |
+
tol_th=st.slider("Tol. spessore ±",0.0,5.0,1.0,step=0.1)
|
| 209 |
|
| 210 |
st.markdown("**Score RUL & Margin**")
|
| 211 |
+
thr_sc=st.slider("Soglia score",0.0,1.0,0.5,step=0.05)
|
| 212 |
+
alpha=st.slider("Peso RUL(α)",0.0,1.0,0.5,step=0.05)
|
| 213 |
+
beta =st.slider("Peso Margin(β)",0.0,1.0,0.5,step=0.05)
|
| 214 |
|
| 215 |
+
# shape_code
|
| 216 |
data['shape_code']=data['shape'].map(SHAPE_MAPPING).fillna(-1).astype(int)
|
| 217 |
+
data['compat_dim']= data.apply(lambda r: dimension_match(r,
|
| 218 |
+
target_length=t_len,
|
| 219 |
+
target_width=t_wid,
|
| 220 |
+
target_shape=t_shape,
|
| 221 |
+
target_weight=t_weight,
|
| 222 |
+
target_thickness=t_thick,
|
| 223 |
+
tol_len=tol_len,
|
| 224 |
+
tol_wid=tol_wid,
|
| 225 |
+
tol_weight=tol_we,
|
| 226 |
+
tol_thickness=tol_th), axis=1)
|
| 227 |
+
|
| 228 |
+
data['Target'] = data.apply(lambda r: assign_class(r,
|
| 229 |
+
threshold_score=thr_sc,
|
| 230 |
+
alpha=alpha,beta=beta), axis=1)
|
|
|
|
|
|
|
| 231 |
|
| 232 |
st.session_state.target_params={
|
| 233 |
+
"target_length": t_len, "target_width": t_wid,
|
| 234 |
+
"target_shape": t_shape, "target_weight": t_weight,
|
|
|
|
|
|
|
| 235 |
"target_thickness": t_thick,
|
| 236 |
+
"tol_len": tol_len, "tol_wid": tol_wid,
|
| 237 |
+
"tol_weight": tol_we, "tol_thickness": tol_th
|
|
|
|
|
|
|
| 238 |
}
|
| 239 |
st.session_state.score_params={
|
| 240 |
+
"threshold_score": thr_sc,
|
| 241 |
"alpha": alpha,
|
| 242 |
"beta": beta
|
| 243 |
}
|
| 244 |
|
| 245 |
st.dataframe(data.head(10))
|
| 246 |
+
st.write("Distribuzione classi:", data["Target"].value_counts())
|
| 247 |
|
| 248 |
+
numeric_cols=data.select_dtypes(include=np.number)
|
| 249 |
if not numeric_cols.empty:
|
| 250 |
+
fig,ax=plt.subplots()
|
| 251 |
sns.heatmap(numeric_cols.corr(), annot=True, cmap='viridis', fmt=".2f", ax=ax)
|
| 252 |
st.pyplot(fig)
|
| 253 |
|
| 254 |
st.session_state.data=data
|
| 255 |
+
csv_proc=data.to_csv(index=False).encode('utf-8')
|
| 256 |
+
st.download_button("Scarica Dataset Elaborato", csv_proc, "dataset_processed.csv")
|
| 257 |
|
| 258 |
+
############### 2) PHASE: TRAIN MODELLI ML ############
|
|
|
|
|
|
|
| 259 |
def train_models(data):
|
| 260 |
st.header("🤖 2. Addestramento ML (Riutilizzo vs Upcycling)")
|
| 261 |
if data is None:
|
| 262 |
+
st.error("Dataset non disponibile (Fase 1).")
|
| 263 |
return
|
| 264 |
if 'Target' not in data.columns:
|
| 265 |
+
st.error("Colonna 'Target' mancante. Rivedi la Fase 1.")
|
| 266 |
return
|
| 267 |
|
|
|
|
| 268 |
features_to_use=[f for f in ML_FEATURES_STEP1 if f in data.columns]
|
| 269 |
if not features_to_use:
|
| 270 |
+
st.error("Nessuna feature per l'addestramento ML.")
|
| 271 |
return
|
| 272 |
|
| 273 |
+
X = data[features_to_use].copy()
|
| 274 |
+
y = data['Target'].map({"Riutilizzo Funzionale":0,"Upcycling Creativo":1})
|
|
|
|
|
|
|
| 275 |
if len(y.unique())<2:
|
| 276 |
+
st.error("Il dataset ha una sola classe. Non si può addestrare.")
|
| 277 |
return
|
| 278 |
+
X_train,X_test,y_train,y_test = train_test_split(X,y,test_size=0.25,random_state=42,stratify=y)
|
| 279 |
+
st.write(f"Train={len(X_train)}, Test={len(X_test)}")
|
| 280 |
|
| 281 |
+
tune_rf= st.checkbox("Ottimizza RandomForest (GridSearchCV)",False)
|
|
|
|
|
|
|
|
|
|
| 282 |
trained_pipelines={}
|
| 283 |
results=[]
|
| 284 |
+
|
| 285 |
for name,model in MODELS.items():
|
| 286 |
st.subheader(f"Modello: {name}")
|
| 287 |
from sklearn.pipeline import Pipeline
|
|
|
|
| 291 |
])
|
| 292 |
try:
|
| 293 |
if tune_rf and name=="Random Forest":
|
| 294 |
+
st.write("Esecuzione GridSearchCV su RandomForest...")
|
| 295 |
param_grid={
|
| 296 |
'clf__n_estimators':[50,100],
|
| 297 |
+
'clf__max_depth':[None,10],
|
| 298 |
}
|
| 299 |
+
from sklearn.model_selection import GridSearchCV
|
| 300 |
+
grid=GridSearchCV(pipe,param_grid,cv=2,scoring='accuracy',n_jobs=-1)
|
| 301 |
grid.fit(X_train,y_train)
|
| 302 |
+
best_est=grid.best_estimator_
|
| 303 |
st.write(f"Migliori parametri: {grid.best_params_}")
|
| 304 |
+
y_pred=best_est.predict(X_test)
|
| 305 |
+
pipe_to_use=best_est
|
| 306 |
else:
|
| 307 |
pipe.fit(X_train,y_train)
|
| 308 |
y_pred=pipe.predict(X_test)
|
|
|
|
| 310 |
|
| 311 |
acc=accuracy_score(y_test,y_pred)
|
| 312 |
f1=f1_score(y_test,y_pred,average='weighted')
|
| 313 |
+
results.append({"Modello":name, "Accuracy":acc, "F1 Score":f1})
|
| 314 |
trained_pipelines[name]=pipe_to_use
|
| 315 |
|
| 316 |
cm=confusion_matrix(y_test,y_pred)
|
| 317 |
fig,ax=plt.subplots()
|
| 318 |
+
sns.heatmap(cm,annot=True,fmt='d',cmap="Greens",ax=ax)
|
| 319 |
plt.xlabel("Pred")
|
| 320 |
plt.ylabel("True")
|
| 321 |
st.pyplot(fig)
|
|
|
|
| 322 |
st.metric("Accuracy",f"{acc:.3f}")
|
| 323 |
st.metric("F1 Score",f"{f1:.3f}")
|
| 324 |
|
|
|
|
| 326 |
st.error(f"Errore addestramento {name}: {e}")
|
| 327 |
|
| 328 |
if results:
|
| 329 |
+
df_res=pd.DataFrame(results).sort_values(by="Accuracy",ascending=False)
|
| 330 |
+
st.dataframe(df_res)
|
| 331 |
+
st.session_state.train_results=df_res
|
| 332 |
st.session_state.models=trained_pipelines
|
| 333 |
else:
|
| 334 |
st.error("Nessun modello addestrato con successo.")
|
| 335 |
st.session_state.models=None
|
| 336 |
|
| 337 |
+
############### 3) PHASE: INFERE ############
|
|
|
|
|
|
|
| 338 |
def model_inference(trained_pipelines, data):
|
| 339 |
+
st.header("🔮 3. Inferenza: Step 1 & Step 2 (VAE + GenAI)")
|
| 340 |
|
| 341 |
if not trained_pipelines:
|
| 342 |
+
st.error("Prima addestra i modelli ML (Fase 2).")
|
| 343 |
return
|
| 344 |
if data is None:
|
| 345 |
+
st.error("Nessun dataset (Fase 1).")
|
|
|
|
|
|
|
|
|
|
| 346 |
return
|
| 347 |
|
| 348 |
+
# Usiamo mediane del dataset per default
|
| 349 |
+
data_stats=data
|
| 350 |
|
| 351 |
+
with st.form("inference_form"):
|
| 352 |
+
st.subheader("Inserisci Dati EoL")
|
| 353 |
c1,c2,c3=st.columns(3)
|
| 354 |
with c1:
|
| 355 |
length=st.number_input("Lunghezza (mm)",0.0,300.0,float(data_stats['length'].median()),step=1.0)
|
| 356 |
+
width= st.number_input("Larghezza (mm)",0.0,200.0,float(data_stats['width'].median()),step=1.0)
|
| 357 |
+
shape_name = st.selectbox("Forma", list(SHAPE_MAPPING.keys()))
|
| 358 |
with c2:
|
| 359 |
+
weight= st.number_input("Peso (kg)",0.0,300.0,float(data_stats['weight'].median()),step=1.0)
|
| 360 |
+
thickness= st.number_input("Spessore (mm)",0.0,50.0,float(data_stats['thickness'].median()),step=0.5)
|
| 361 |
+
RUL= st.number_input("RUL (0-1000)",0,1000,int(data_stats['RUL'].median()))
|
| 362 |
with c3:
|
| 363 |
+
val_merc= st.number_input("Valore Mercato (€)",0.0,1e5,float(data_stats['margin'].median()+200),step=10.0)
|
| 364 |
+
costo_rip= st.number_input("Costo Riparazione (€)",0.0,1e5,50.0,step=10.0)
|
| 365 |
|
| 366 |
+
sub=st.form_submit_button("Predizione Step 1")
|
| 367 |
|
| 368 |
+
if sub:
|
| 369 |
margin= val_merc - costo_rip
|
| 370 |
+
shape_code = SHAPE_MAPPING.get(shape_name,-1)
|
| 371 |
|
|
|
|
| 372 |
input_dict={
|
| 373 |
+
"length":length,
|
| 374 |
+
"width":width,
|
| 375 |
+
"shape":shape_name,
|
| 376 |
+
"weight":weight,
|
| 377 |
+
"thickness":thickness,
|
| 378 |
+
"RUL":RUL,
|
| 379 |
+
"margin":margin
|
| 380 |
}
|
| 381 |
temp_df=pd.DataFrame([input_dict])
|
| 382 |
|
| 383 |
+
# compat_dim
|
| 384 |
+
if 'target_params' not in st.session_state:
|
| 385 |
+
st.error("Parametri target non definiti. Fase 1 mancante.")
|
| 386 |
+
return
|
| 387 |
+
param_t=st.session_state.target_params
|
| 388 |
+
temp_df['compat_dim'] = temp_df.apply(lambda r:
|
| 389 |
+
dimension_match(r, **param_t), axis=1)
|
| 390 |
|
| 391 |
+
# shape_code
|
| 392 |
+
temp_df['shape_code']= shape_code
|
| 393 |
+
# Manteniamo solo le col ML
|
| 394 |
try:
|
| 395 |
+
X_inference=temp_df[ML_FEATURES_STEP1]
|
| 396 |
except KeyError as e:
|
| 397 |
+
st.error(f"Mancano feature: {e}")
|
| 398 |
return
|
| 399 |
|
| 400 |
+
# Eseguiamo predizione con i modelli
|
| 401 |
+
preds=[]
|
| 402 |
details=[]
|
| 403 |
for name,pipe in trained_pipelines.items():
|
| 404 |
try:
|
| 405 |
+
p_num=pipe.predict(X_inference)[0]
|
| 406 |
proba=pipe.predict_proba(X_inference)[0]
|
| 407 |
details.append({
|
| 408 |
+
"Modello":name,
|
| 409 |
+
"Pred(0=Riu,1=Upc)": p_num,
|
| 410 |
+
"Prob_Riutilizzo": proba[0],
|
| 411 |
+
"Prob_Upcycling": proba[1]
|
| 412 |
})
|
| 413 |
+
preds.append(p_num)
|
| 414 |
except Exception as e:
|
| 415 |
st.error(f"Errore predizione {name}: {e}")
|
| 416 |
|
| 417 |
+
if not preds:
|
| 418 |
+
st.error("Nessuna predizione valida.")
|
| 419 |
return
|
|
|
|
| 420 |
# Aggrega con mode
|
| 421 |
+
from statistics import mode, StatisticsError
|
| 422 |
try:
|
| 423 |
+
final_pred=mode(preds)
|
| 424 |
except StatisticsError:
|
| 425 |
+
# Se c'è tie, guardiamo la media upcycling
|
| 426 |
+
avg_upc= np.mean([d["Prob_Upcycling"] for d in details])
|
| 427 |
+
final_pred=1 if avg_upc>=0.5 else 0
|
| 428 |
|
| 429 |
final_label="Riutilizzo Funzionale" if final_pred==0 else "Upcycling Creativo"
|
| 430 |
+
st.subheader("Risultato Aggregato")
|
| 431 |
+
st.metric("Classe", final_label)
|
| 432 |
|
| 433 |
+
with st.expander("Dettagli singoli modelli"):
|
| 434 |
+
df_det=pd.DataFrame(details)
|
| 435 |
+
df_det["Prob_Riutilizzo"]= df_det["Prob_Riutilizzo"].apply(lambda x:f"{x:.1%}")
|
| 436 |
+
df_det["Prob_Upcycling"]= df_det["Prob_Upcycling"].apply(lambda x:f"{x:.1%}")
|
| 437 |
+
st.dataframe(df_det)
|
| 438 |
|
| 439 |
+
# Se Upcycling Creativo => Step 2 (VAE + GenAI)
|
| 440 |
if final_label=="Upcycling Creativo":
|
| 441 |
st.markdown("---")
|
| 442 |
+
st.subheader("Fase 2: Generazione con VAE + GenAI Testuale")
|
| 443 |
|
| 444 |
if not st.session_state.get("vae_trained_on_eol",False):
|
| 445 |
+
st.error("VAE non addestrato. Vai a '🧬 Training VAE (Step 2)'.")
|
| 446 |
+
return
|
| 447 |
+
vae_model= st.session_state.get("vae",None)
|
| 448 |
+
vae_scaler=st.session_state.get("vae_scaler",None)
|
| 449 |
+
if vae_model is None or vae_scaler is None:
|
| 450 |
+
st.error("VAE o scaler mancanti in session.")
|
| 451 |
+
return
|
| 452 |
+
|
| 453 |
+
n_ideas=st.number_input("Quante idee generare col VAE?",1,10,3)
|
| 454 |
+
if st.button("Genera Configurazioni + Testo Upcycling"):
|
| 455 |
+
vae_model.eval()
|
| 456 |
+
with torch.no_grad():
|
| 457 |
+
lat_dim= vae_model.fc21.out_features
|
| 458 |
+
z=torch.randn(n_ideas, lat_dim)
|
| 459 |
+
recon= vae_model.decode(z)
|
| 460 |
+
|
| 461 |
+
arr=recon.numpy()
|
| 462 |
+
try:
|
| 463 |
+
arr_inv= vae_scaler.inverse_transform(arr)
|
| 464 |
+
feat_names= vae_scaler.feature_names_in_
|
| 465 |
+
df_gen= pd.DataFrame(arr_inv, columns=feat_names)
|
| 466 |
+
|
| 467 |
+
# Riconverti shape_code -> shape
|
| 468 |
+
if 'shape_code' in df_gen.columns:
|
| 469 |
+
df_gen['shape_code']= df_gen['shape_code'].round().astype(int)
|
| 470 |
+
inv_map={v:k for k,v in SHAPE_MAPPING.items()}
|
| 471 |
+
df_gen['shape']= df_gen['shape_code'].map(inv_map).fillna('unknown')
|
| 472 |
+
|
| 473 |
+
st.write("**Configurazioni generate (VAE)**")
|
| 474 |
+
st.dataframe(df_gen.round(2))
|
| 475 |
+
|
| 476 |
+
# --- Integrazione GenAI testuale con Transformers ---
|
| 477 |
+
st.markdown("#### Suggerimenti testuali per ciascuna configurazione")
|
| 478 |
+
|
| 479 |
+
# Carichiamo pipeline testuale (distilgpt2, ad es.)
|
| 480 |
+
# Se su HF Spaces serve un modello leggero
|
| 481 |
+
text_generator = pipeline(
|
| 482 |
+
"text-generation",
|
| 483 |
+
model="distilgpt2",
|
| 484 |
+
device=0 if torch.cuda.is_available() else -1
|
| 485 |
+
)
|
| 486 |
+
|
| 487 |
+
def gen_upcycle_text(row):
|
| 488 |
+
shape = row.get("shape","unknown")
|
| 489 |
+
thick = row.get("thickness",0.0)
|
| 490 |
+
wei = row.get("weight",0.0)
|
| 491 |
+
prompt= (
|
| 492 |
+
f"Ho un componente EoL con forma {shape}, spessore {thick:.1f} mm, peso {wei:.1f} kg.\n"
|
| 493 |
+
"Dammi un'idea creativa di upcycling (in italiano) con passaggi principali:"
|
| 494 |
+
)
|
| 495 |
+
result= text_generator(prompt, max_new_tokens=50, do_sample=True, top_k=50)
|
| 496 |
+
return result[0]["generated_text"]
|
| 497 |
+
|
| 498 |
+
ideas_text=[]
|
| 499 |
+
for i, r in df_gen.iterrows():
|
| 500 |
+
text_sugg = gen_upcycle_text(r)
|
| 501 |
+
ideas_text.append(text_sugg)
|
| 502 |
+
|
| 503 |
+
for i, r in df_gen.iterrows():
|
| 504 |
+
st.write(f"**Idea {i+1}** - shape={r['shape']}, thickness={r['thickness']:.1f}, weight={r['weight']:.1f}")
|
| 505 |
+
st.info(ideas_text[i])
|
| 506 |
+
st.markdown("---")
|
| 507 |
+
|
| 508 |
+
except Exception as e:
|
| 509 |
+
st.error(f"Errore decoding VAE: {e}")
|
| 510 |
else:
|
| 511 |
+
st.success("Predetto: Riutilizzo Funzionale. Nessun passaggio generativo richiesto.")
|
| 512 |
|
| 513 |
+
############### 4) PHASE: TRAINING VAE ############
|
|
|
|
|
|
|
| 514 |
def vae_training_phase():
|
| 515 |
+
st.header("🧬 Training VAE (Step 2)")
|
|
|
|
| 516 |
if 'data' not in st.session_state or st.session_state['data'] is None:
|
| 517 |
+
st.error("Nessun dataset in session. Torna a Fase 1.")
|
| 518 |
return
|
|
|
|
| 519 |
data=st.session_state['data']
|
| 520 |
+
|
| 521 |
+
feats=[f for f in VAE_FEATURES_STEP2 if f in data.columns]
|
| 522 |
if not feats:
|
| 523 |
+
st.error(f"Nessuna feature da usare per VAE. Servirebbero: {VAE_FEATURES_STEP2}")
|
| 524 |
return
|
| 525 |
+
st.write(f"VAE userà feature: {feats}")
|
| 526 |
|
| 527 |
+
lat_dim= st.slider("Dimensione Latente",2,10,2)
|
| 528 |
+
ep= st.number_input("Epochs VAE",10,300,50)
|
| 529 |
+
lr= st.number_input("Learning Rate VAE",1e-5,1e-2,1e-3,format="%e")
|
| 530 |
+
bs= st.selectbox("Batch Size",[16,32,64], index=1)
|
|
|
|
| 531 |
|
| 532 |
if not st.session_state.get("vae_trained_on_eol",False):
|
| 533 |
+
st.warning("VAE non addestrato.")
|
| 534 |
if st.button("Avvia Training VAE"):
|
| 535 |
+
# Inizializza
|
| 536 |
+
st.session_state["vae"]= MiniVAE(input_dim=len(feats), latent_dim=lat_dim)
|
| 537 |
+
st.session_state["vae_trained_on_eol"]=False
|
| 538 |
+
|
| 539 |
+
from sklearn.preprocessing import StandardScaler
|
| 540 |
+
X_vae= data[feats].copy()
|
| 541 |
+
# Riempi NaN
|
| 542 |
for c in X_vae.columns:
|
| 543 |
if X_vae[c].isnull().any():
|
| 544 |
X_vae[c].fillna(X_vae[c].median(), inplace=True)
|
| 545 |
|
| 546 |
# Scalatura
|
| 547 |
+
scaler= StandardScaler()
|
| 548 |
+
X_scaled= scaler.fit_transform(X_vae)
|
| 549 |
+
st.session_state["vae_scaler"]= scaler
|
|
|
|
| 550 |
|
| 551 |
dataset=torch.utils.data.TensorDataset(torch.tensor(X_scaled,dtype=torch.float32))
|
| 552 |
+
loader= torch.utils.data.DataLoader(dataset,batch_size=bs,shuffle=True)
|
| 553 |
+
vae=st.session_state["vae"]
|
| 554 |
+
optimizer= torch.optim.Adam(vae.parameters(),lr=lr)
|
| 555 |
|
| 556 |
losses=[]
|
| 557 |
vae.train()
|
| 558 |
for epoch in range(int(ep)):
|
| 559 |
+
epoch_loss=0
|
| 560 |
for (batch,) in loader:
|
| 561 |
optimizer.zero_grad()
|
| 562 |
+
recon, mu, logvar= vae(batch)
|
| 563 |
+
loss= vae_loss(recon, batch, mu, logvar)
|
| 564 |
loss.backward()
|
| 565 |
optimizer.step()
|
| 566 |
+
epoch_loss+= loss.item()
|
| 567 |
+
avg_loss= epoch_loss/len(dataset)
|
| 568 |
+
losses.append(avg_loss)
|
| 569 |
st.progress((epoch+1)/ep)
|
| 570 |
st.session_state["vae_trained_on_eol"]=True
|
| 571 |
+
st.success(f"VAE addestrato. Ultimo Loss ~ {avg_loss:.2f}")
|
| 572 |
st.line_chart(losses)
|
| 573 |
else:
|
| 574 |
+
st.success("VAE già addestrato. Se vuoi rifare training, clicca 'Riallena'.")
|
| 575 |
+
if st.button("Riallena"):
|
| 576 |
st.session_state["vae_trained_on_eol"]=False
|
| 577 |
st.rerun()
|
| 578 |
|
| 579 |
+
############### 5) PHASE: DASHBOARD ############
|
|
|
|
|
|
|
| 580 |
def show_dashboard():
|
| 581 |
st.header("📊 Dashboard")
|
| 582 |
+
data= st.session_state.get('data', None)
|
| 583 |
+
if data is None:
|
| 584 |
+
st.error("Nessun dataset.")
|
| 585 |
return
|
| 586 |
+
|
| 587 |
st.subheader("Distribuzione Classi EoL")
|
| 588 |
st.write(data['Target'].value_counts())
|
| 589 |
|
| 590 |
if 'train_results' in st.session_state and st.session_state['train_results'] is not None:
|
|
|
|
| 591 |
df_res=st.session_state['train_results']
|
| 592 |
+
st.subheader("Risultati modelli ML")
|
| 593 |
st.dataframe(df_res)
|
| 594 |
else:
|
| 595 |
+
st.info("Nessun risultato di training ML")
|
| 596 |
|
| 597 |
st.subheader("Stato VAE")
|
| 598 |
if st.session_state.get("vae_trained_on_eol",False):
|
| 599 |
+
st.success("VAE addestrato. Pronto per generare idee di upcycling.")
|
| 600 |
else:
|
| 601 |
+
st.warning("VAE non addestrato")
|
| 602 |
|
| 603 |
+
############### 6) PHASE: GUIDA ############
|
|
|
|
|
|
|
| 604 |
def show_help():
|
| 605 |
+
st.header("ℹ️ Guida all'Uso")
|
| 606 |
st.markdown("""
|
| 607 |
+
**Flusso a due fasi (Riutilizzo vs Upcycling) e generazione creativa VAE + GenAI**:
|
| 608 |
+
|
| 609 |
+
1. **Fase 1 (Dataset)**:
|
| 610 |
+
- Generi/carichi dati su dimensioni, shape, RUL, margin, ecc.
|
| 611 |
+
- Definisci parametri e tolleranze per la compatibilità, assegni classi ("Riutilizzo Funzionale" vs "Upcycling Creativo").
|
| 612 |
+
|
| 613 |
+
2. **Fase 2 (Addestramento ML)**:
|
| 614 |
+
- Alleni vari modelli (RF, SVM...) per predire la classe su nuovi EoL.
|
| 615 |
+
|
| 616 |
+
3. **Fase 3 (Inferenza)**:
|
| 617 |
+
- Inserisci un nuovo EoL.
|
| 618 |
+
- Se la classe è "Riutilizzo Funzionale", stop.
|
| 619 |
+
- Se "Upcycling Creativo", prosegui con generazione di soluzioni (VAE)...
|
| 620 |
+
|
| 621 |
+
4. **Fase 4 (Training VAE)**:
|
| 622 |
+
- Prima devi addestrare il VAE su feature geometriche.
|
| 623 |
+
- Finito l'allenamento, potrai generare configurazioni fittizie per l'upcycling (dim, spessore...).
|
| 624 |
+
|
| 625 |
+
5. **Fase 3 (continuazione)**:
|
| 626 |
+
- Appena generi col VAE, un modello di GenAI testuale (distilgpt2) crea suggerimenti di riuso creativo in linguaggio naturale.
|
| 627 |
+
|
| 628 |
+
6. **Fase 5 (Dashboard)**:
|
| 629 |
+
- Visualizzi le metriche e lo stato dei modelli.
|
| 630 |
+
|
| 631 |
+
Puoi resettare l'app in qualsiasi momento dal pulsante "Reset App" nella sidebar.
|
| 632 |
+
""")
|
| 633 |
+
|
| 634 |
+
############### RESET ############
|
| 635 |
def reset_app():
|
| 636 |
+
for k in ["data","models","train_results","vae","vae_trained_on_eol","vae_scaler","target_params","score_params","data_source"]:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 637 |
if k in st.session_state:
|
| 638 |
del st.session_state[k]
|
| 639 |
+
st.success("App reset. Ricarico l'interfaccia.")
|
| 640 |
st.experimental_rerun()
|
| 641 |
|
| 642 |
+
############### MAIN ############
|
|
|
|
|
|
|
| 643 |
def main():
|
| 644 |
+
st.sidebar.title("WEEKO - Menu")
|
| 645 |
+
step= st.sidebar.radio("Fasi:", [
|
| 646 |
"♻️ Dataset",
|
| 647 |
"🤖 Addestramento ML (Step 1)",
|
| 648 |
"🔮 Inferenza (Step 1 & 2)",
|
|
|
|
| 657 |
elif step=="🤖 Addestramento ML (Step 1)":
|
| 658 |
train_models(st.session_state.get('data',None))
|
| 659 |
elif step=="🔮 Inferenza (Step 1 & 2)":
|
| 660 |
+
if st.session_state.get('models') is None:
|
| 661 |
+
st.error("Devi addestrare i modelli ML (Fase 2).")
|
| 662 |
else:
|
| 663 |
model_inference(st.session_state['models'], st.session_state.get('data',None))
|
| 664 |
elif step=="🧬 Training VAE (Step 2)":
|
|
|
|
| 670 |
|
| 671 |
if __name__=="__main__":
|
| 672 |
main()
|
| 673 |
+
|