From c9fba56622b81d41ed47ac58c71d4907e6783848 Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Mon, 8 Sep 2025 13:31:11 +0300 Subject: [PATCH] model checkpoint manager --- NN/models/cnn_model.py | 134 ++++-- NN/models/dqn_agent.py | 181 +++++-- .../BrowserTools-1.2.0-extension.zip | Bin 0 -> 23268 bytes utils/checkpoint_manager.py | 105 +++-- utils/model_registry.py | 446 ++++++++++++++++++ web/clean_dashboard.py | 114 +++-- 6 files changed, 838 insertions(+), 142 deletions(-) create mode 100644 mcp_servers/browser-tools-mcp/BrowserTools-1.2.0-extension.zip create mode 100644 utils/model_registry.py diff --git a/NN/models/cnn_model.py b/NN/models/cnn_model.py index bac607e..13a25aa 100644 --- a/NN/models/cnn_model.py +++ b/NN/models/cnn_model.py @@ -21,6 +21,7 @@ from typing import Dict, Any, Optional, Tuple # Import checkpoint management from utils.checkpoint_manager import save_checkpoint, load_best_checkpoint +from utils.model_registry import get_model_registry # Configure logging logger = logging.getLogger(__name__) @@ -774,42 +775,107 @@ class CNNModelTrainer: # Return realistic loss values based on random baseline performance return {'main_loss': 0.693, 'total_loss': 0.693, 'accuracy': 0.5} # ln(2) for binary cross-entropy at random chance - def save_model(self, filepath: str, metadata: Optional[Dict] = None): - """Save model with metadata""" - save_dict = { - 'model_state_dict': self.model.state_dict(), - 'optimizer_state_dict': self.optimizer.state_dict(), - 'scheduler_state_dict': self.scheduler.state_dict(), - 'training_history': self.training_history, - 'model_config': { - 'input_size': self.model.input_size, - 'feature_dim': self.model.feature_dim, - 'output_size': self.model.output_size, - 'base_channels': self.model.base_channels + def save_model(self, filepath: str = None, metadata: Optional[Dict] = None): + """Save model with metadata using unified registry""" + try: + from utils.model_registry import save_model + + # Prepare model data + model_data = { + 'model_state_dict': self.model.state_dict(), + 'optimizer_state_dict': self.optimizer.state_dict(), + 'scheduler_state_dict': self.scheduler.state_dict(), + 'training_history': self.training_history, + 'model_config': { + 'input_size': self.model.input_size, + 'feature_dim': self.model.feature_dim, + 'output_size': self.model.output_size, + 'base_channels': self.model.base_channels + } } - } - - if metadata: - save_dict['metadata'] = metadata - - torch.save(save_dict, filepath) - logger.info(f"Enhanced CNN model saved to {filepath}") + + if metadata: + model_data['metadata'] = metadata + + # Use unified registry if no filepath specified + if filepath is None or filepath.startswith('models/'): + # Extract model name from filepath or use default + model_name = "enhanced_cnn" + if filepath: + model_name = filepath.split('/')[-1].replace('_latest.pt', '').replace('.pt', '') + + success = save_model( + model=self.model, + model_name=model_name, + model_type='cnn', + metadata={'full_checkpoint': model_data} + ) + if success: + logger.info(f"Enhanced CNN model saved to unified registry: {model_name}") + return success + else: + # Legacy direct file save + torch.save(model_data, filepath) + logger.info(f"Enhanced CNN model saved to {filepath} (legacy mode)") + return True + + except Exception as e: + logger.error(f"Failed to save CNN model: {e}") + return False - def load_model(self, filepath: str) -> Dict: - """Load model from file""" - checkpoint = torch.load(filepath, map_location=self.device) - - self.model.load_state_dict(checkpoint['model_state_dict']) - self.optimizer.load_state_dict(checkpoint['optimizer_state_dict']) - - if 'scheduler_state_dict' in checkpoint: - self.scheduler.load_state_dict(checkpoint['scheduler_state_dict']) - - if 'training_history' in checkpoint: - self.training_history = checkpoint['training_history'] - - logger.info(f"Enhanced CNN model loaded from {filepath}") - return checkpoint.get('metadata', {}) + def load_model(self, filepath: str = None) -> Dict: + """Load model from unified registry or file""" + try: + from utils.model_registry import load_model + + # Use unified registry if no filepath or if it's a models/ path + if filepath is None or filepath.startswith('models/'): + model_name = "enhanced_cnn" + if filepath: + model_name = filepath.split('/')[-1].replace('_latest.pt', '').replace('.pt', '') + + model = load_model(model_name, 'cnn') + if model is None: + logger.warning(f"Could not load model {model_name} from unified registry") + return {} + + # Load full checkpoint data from metadata + registry = get_model_registry() + if model_name in registry.metadata['models']: + model_data = registry.metadata['models'][model_name] + if 'full_checkpoint' in model_data: + checkpoint = model_data['full_checkpoint'] + + self.optimizer.load_state_dict(checkpoint['optimizer_state_dict']) + if 'scheduler_state_dict' in checkpoint: + self.scheduler.load_state_dict(checkpoint['scheduler_state_dict']) + if 'training_history' in checkpoint: + self.training_history = checkpoint['training_history'] + + logger.info(f"Enhanced CNN model loaded from unified registry: {model_name}") + return checkpoint.get('metadata', {}) + + return {} + + else: + # Legacy direct file load + checkpoint = torch.load(filepath, map_location=self.device) + + self.model.load_state_dict(checkpoint['model_state_dict']) + self.optimizer.load_state_dict(checkpoint['optimizer_state_dict']) + + if 'scheduler_state_dict' in checkpoint: + self.scheduler.load_state_dict(checkpoint['scheduler_state_dict']) + + if 'training_history' in checkpoint: + self.training_history = checkpoint['training_history'] + + logger.info(f"Enhanced CNN model loaded from {filepath} (legacy mode)") + return checkpoint.get('metadata', {}) + + except Exception as e: + logger.error(f"Failed to load CNN model: {e}") + return {} def create_enhanced_cnn_model(input_size: int = 60, feature_dim: int = 50, diff --git a/NN/models/dqn_agent.py b/NN/models/dqn_agent.py index b7d4d21..ec103a6 100644 --- a/NN/models/dqn_agent.py +++ b/NN/models/dqn_agent.py @@ -16,6 +16,7 @@ sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath( # Import checkpoint management from utils.checkpoint_manager import save_checkpoint, load_best_checkpoint +from utils.model_registry import get_model_registry # Configure logger logger = logging.getLogger(__name__) @@ -1329,54 +1330,140 @@ class DQNAgent: return False # No improvement - def save(self, path: str): - """Save model and agent state""" - os.makedirs(os.path.dirname(path), exist_ok=True) - - # Save policy network - self.policy_net.save(f"{path}_policy") - - # Save target network - self.target_net.save(f"{path}_target") - - # Save agent state - state = { - 'epsilon': self.epsilon, - 'update_count': self.update_count, - 'losses': self.losses, - 'optimizer_state': self.optimizer.state_dict(), - 'best_reward': self.best_reward, - 'avg_reward': self.avg_reward - } - - torch.save(state, f"{path}_agent_state.pt") - logger.info(f"Agent state saved to {path}_agent_state.pt") - - def load(self, path: str): - """Load model and agent state""" - # Load policy network - self.policy_net.load(f"{path}_policy") - - # Load target network - self.target_net.load(f"{path}_target") - - # Load agent state + def save(self, path: str = None): + """Save model and agent state using unified registry""" try: - agent_state = torch.load(f"{path}_agent_state.pt", map_location=self.device, weights_only=False) - self.epsilon = agent_state['epsilon'] - self.update_count = agent_state['update_count'] - self.losses = agent_state['losses'] - self.optimizer.load_state_dict(agent_state['optimizer_state']) - - # Load additional metrics if they exist - if 'best_reward' in agent_state: - self.best_reward = agent_state['best_reward'] - if 'avg_reward' in agent_state: - self.avg_reward = agent_state['avg_reward'] - - logger.info(f"Agent state loaded from {path}_agent_state.pt") - except FileNotFoundError: - logger.warning(f"Agent state file not found at {path}_agent_state.pt, using default values") + from utils.model_registry import save_model + + # Use unified registry if no path or if it's a models/ path + if path is None or path.startswith('models/'): + model_name = "dqn_agent" + if path: + model_name = path.split('/')[-1].replace('_agent_state', '').replace('.pt', '') + + # Prepare full agent state + agent_state = { + 'epsilon': self.epsilon, + 'update_count': self.update_count, + 'losses': self.losses, + 'optimizer_state': self.optimizer.state_dict(), + 'best_reward': self.best_reward, + 'avg_reward': self.avg_reward, + 'policy_net_state': self.policy_net.state_dict(), + 'target_net_state': self.target_net.state_dict() + } + + success = save_model( + model=self.policy_net, # Save policy net as main model + model_name=model_name, + model_type='dqn', + metadata={'full_agent_state': agent_state} + ) + + if success: + logger.info(f"DQN agent saved to unified registry: {model_name}") + return + + else: + # Legacy direct file save + os.makedirs(os.path.dirname(path), exist_ok=True) + + # Save policy network + self.policy_net.save(f"{path}_policy") + + # Save target network + self.target_net.save(f"{path}_target") + + # Save agent state + state = { + 'epsilon': self.epsilon, + 'update_count': self.update_count, + 'losses': self.losses, + 'optimizer_state': self.optimizer.state_dict(), + 'best_reward': self.best_reward, + 'avg_reward': self.avg_reward + } + + torch.save(state, f"{path}_agent_state.pt") + logger.info(f"Agent state saved to {path}_agent_state.pt (legacy mode)") + + except Exception as e: + logger.error(f"Failed to save DQN agent: {e}") + + def load(self, path: str = None): + """Load model and agent state from unified registry or file""" + try: + from utils.model_registry import load_model + + # Use unified registry if no path or if it's a models/ path + if path is None or path.startswith('models/'): + model_name = "dqn_agent" + if path: + model_name = path.split('/')[-1].replace('_agent_state', '').replace('.pt', '') + + model = load_model(model_name, 'dqn') + if model is None: + logger.warning(f"Could not load DQN agent {model_name} from unified registry") + return + + # Load full agent state from metadata + registry = get_model_registry() + if model_name in registry.metadata['models']: + model_data = registry.metadata['models'][model_name] + if 'full_agent_state' in model_data: + agent_state = model_data['full_agent_state'] + + # Restore agent state + self.epsilon = agent_state['epsilon'] + self.update_count = agent_state['update_count'] + self.losses = agent_state['losses'] + self.optimizer.load_state_dict(agent_state['optimizer_state']) + + # Load additional metrics if they exist + if 'best_reward' in agent_state: + self.best_reward = agent_state['best_reward'] + if 'avg_reward' in agent_state: + self.avg_reward = agent_state['avg_reward'] + + # Load network states + if 'policy_net_state' in agent_state: + self.policy_net.load_state_dict(agent_state['policy_net_state']) + if 'target_net_state' in agent_state: + self.target_net.load_state_dict(agent_state['target_net_state']) + + logger.info(f"DQN agent loaded from unified registry: {model_name}") + return + + return + + else: + # Legacy direct file load + # Load policy network + self.policy_net.load(f"{path}_policy") + + # Load target network + self.target_net.load(f"{path}_target") + + # Load agent state + try: + agent_state = torch.load(f"{path}_agent_state.pt", map_location=self.device, weights_only=False) + self.epsilon = agent_state['epsilon'] + self.update_count = agent_state['update_count'] + self.losses = agent_state['losses'] + self.optimizer.load_state_dict(agent_state['optimizer_state']) + + # Load additional metrics if they exist + if 'best_reward' in agent_state: + self.best_reward = agent_state['best_reward'] + if 'avg_reward' in agent_state: + self.avg_reward = agent_state['avg_reward'] + + logger.info(f"Agent state loaded from {path}_agent_state.pt (legacy mode)") + except FileNotFoundError: + logger.warning(f"Agent state file not found at {path}_agent_state.pt, using default values") + + except Exception as e: + logger.error(f"Failed to load DQN agent: {e}") def get_position_info(self): """Get current position information""" diff --git a/mcp_servers/browser-tools-mcp/BrowserTools-1.2.0-extension.zip b/mcp_servers/browser-tools-mcp/BrowserTools-1.2.0-extension.zip new file mode 100644 index 0000000000000000000000000000000000000000..ad89b45a624f36ddbeab9b4127f4d7c291c5c138 GIT binary patch literal 23268 zcmaI7bC73Evo-o_+qP}nwr$(p)9z{8wr#t6+O}z}=% zGBcuTugq17G9aL+fPWqmWE<^&P5$eJ1Rw;MSUNk{nbDbfxSH9!SUK1;sHwsMAQJIs zEt39@f2^Aa3;+o92?zlA7mVV+;2;5z{}|LsTee-6+wj+Fz>=u?;9u!j*RO?v54 zyAV(%bl*Z_4RHc!qO&4s0OuB7q7H+5MvmTm@WA)Z%SkRD<+xJ-Fd1-E_Th9_T}F0x zd>*oXV>Io3U^UjM>o}(t>RVn*@lbPGzDRY({&%sI6sXVXro7`={umRe zFf^m`zDGz4ttsd?_o-L%Sxz4EQ-Q=V65&L!d76)7`@yldH!diCUD;dlw!^#O<;R;J zpDKd-J;df{IR+!6#wamrDhFV-D51YhrUuwp1M81T?FoTtn(;fGRg{b_9`eH~| z1o4gK04Gg>zMo@%US;jpapIBaiqtS03&)};g!Sf(i6DDmWkHiIQ)bQDss!c(?oid?9^;bn!3H%3~QBuv;OMkqLhF7oU*n54YgiYv9k;iSXgrKm zXQhK1GTAxvL0gfH=$vF2t*k zU3>YZ9q`}@AL$Ryg1`YI(7g~n8-ZEf{$LIYzrKZLuV!PHI0#=+6a4~O?KMKA0_P&VnOSMwdNDFc1 zNC?TPF(+TCAr|T!bsLp0P*NTdhs}wLGDSIl19-^iRG+Y+*@o+;rCG}1fOlqvYZdI3 z#rpw;Xi@x0c7F+qUSj=le_O)kk$n7jAB{5?#Vf#_)|(RhJu>-DRWbW`dwF?niM;Ou z138k;F_ghJ4Poq)veJKjJ~W&v)c8%&vsg(-;&{QBLzwe0SZUpTO-V|O3zr8bE+h-Vx=7?|%DJXYdEzbslHw ztOaow@i8V`qH5AR`-g^*(qTBGO5ya9!WFzuSxZ`G#tH&N3%c}ILJ7{Ac`}BrvC5HU zeIW7FH(?iDI%V?jC!Y{3DwQA!17@EnPFC{ zrtrUf+X`iX7(~7ZMMATVAhgX_225+oO^6_ECdVRoSQ3*Hs~VV?r+=XxLnj<+!;^Dt zS>qvArDbl8@N?h!#{EP^%n`wfcY9z)$&CQwTkYF1&hifu(=YnDT*BduiJ`S(HKerP zOrxAZ1=~VYhZ!(lx8Te%IFQevI*;ymzgXz))dgp{9XORz2q}H$gV=1hdLxFD7nv9w zKx0W|g^0~UisX>#w( zNTP;3Y>Sz?++_$z0r52ySF!y(v?sU4F}k$TJ|Na6l0TW}qLQ8uiFv!c?d~g$iTh`h z;B(DPto1u@%UQk0o>`$X8-KZDaP&0B0Zp;b`OJ-Shqw*>Jj#OWgmVS;4xga3r{|gu zJDxl+YD97cOmd!(XCY7WibpqA=9sapetP>xNyd>)OWjP|*$qna3m0@0x&4qanmYKE zuS}?4e#$NB^^@wCBiKAi>#wVZoFv5pHJpHJ(KAX!Wr#C1vHZ57Y^ za!e=8(+*O3(Np$8PvGM8*MxrUkzrHOhsAqGoU6;GcZWBQxnHS4qFYNSVoxrJdtZA% zM!eWUqbR!zL_gaXb7)5}QKSReX+PBjT)ew~Zg!FiPwl%2V|k94N% zetd)SnAl7A1#KJ34Wa;Oo$Z>EG-t)qwATHl1^xoiD^o^;9v|%w)c)xAdM*2M(+Kuwh111ci_eZC{{HLS;2mDYF$}xtNI#T zyeNfO!H2sym74c0i^MkUz>e|kX$VZV?Kk`q?aU8&zjRmIHpo{T<{Zxrb=_O zRHH9vMTa~Syo=AlUYq3;x#xxC_nX+9eZEHh@z6itI*$XVcDjJ-Lz!D$X>b;M=W^Ae zckHK7?hIBi5P4cadJbCR>DpQ+kVT#&dg0vtqdva?z<^1uQGJjPSXki4=2I!ryt+1< z7PI$JAe!Y%EmZc63r0a)!kb0J)~ZTn>Fo{T$J9==>(;`e7?gnTOLq_b-PUJUO404xOobV8#qz9dfcupci|&{QL{b@^V)x zoYkg^QEI59<*e?L5xN8%GfM>_QNG5r$B(b?x+`;P{6ItE%WzSOHcg87Sws%zKt-`M zpt^kAY7~CvwCW8w_pLyz?r_Z)~I?oW{m!E3JEWv zh$aJw{ii%HT{B}ken)_MY;9dFz4+qfWmVU>D`!v6)_53KXcPQC$V*tR!^m-u$Oz;1 z0^TS+s&Y_7>!##cftum$Mn``rS^Y*h91bGwp&C)908gUDB(55lya(tr^f@7Icsvai zHf<@48~mXoI&SI-cK)Zpg%({|V7_}Tm7@T);63Rn@MJwzo^z9eMln_&D- zcnlf$J6pOG6FQY%VU=VfrV7P!>=Dg;D+&)A8UFqSf2331>cxH#l7cs!O}4!u9xPI> zHBnBzYfEo*A}tjPR=B0wi399qf5h1;{KnUN|IVe{ioy{&ffZMaQVlzAwO@GvvP1!Q z9PHKcw#F~Ixf-Es?EXh~Wr4Orpg(xGmM*UheSdhaV=3IYxpQe;`P39W)iKuIgMz;b zVj~Z5-s#8b=I7A#ERW1`q1x1x6=?|}c}U;Ar19#05Q&6YVe|X5?5I+$h5LripRhY8nX)KB;!N9mhgq9|tdzy!I(UC5sXYSZW& zAKmw9Fxk%YKf#64()uBsk*%Vl1j@C-ZgH~&*ULKK+`1kgZ!izSzT$P|ZH$hMUh4CjYkZMLEl`{D!n(E-WM_lku#BJbStMCXR*FIy?KUmr zySu6E(|xQktmj@?ADzA_%#r&k5|$&Mg+0QcKTw9M>XMB{nQEc=L0>t4=&&Zx3O6-j zdnC<~ob*na7OiLBv}WG9)OWd*|5{w%_q2~ITRHYr&I^M~fDLb4?##5c?7&`2TL{JA zVANlQ`&D!(R|!!R5^=SMWc`&?3-`3z`d+lbuGz^7?fGRDvwsk{(aZE`n9U~0d#%?D zQ*1<5bB#eDl#sPPC5aIr-!vwKX0{Mm(ZIe8!h9`pl}*$Y-L!14UYZpbzm-nj^4(Hs z7O;d3vE3@BSXv@`bm4oa#@nYc8}pk^a!`c*E6(oX%+nwOo?m2Gp(^lq=`jqBm2dnz zXei!~q2ez)KIXw^Q$+$?AMEN17W3VOmwWxH3D-XY{}4@I5KOr*H~`=uI{80DGuv7I zzsdLVUkdskiKeO94_5~VTNipuS3BE(5zY36S_>EGI*WhozcFz(`5xpzM%W$S$hn>$ zZj^YS67t~a62*6YG)p$b7n?G6Irn6{2ChnZRW}Q{hDYleIr1Qaaci59?3^SojM2V? zN6Nf!O5sR~BeuzpXs&mCdp6Uicg zQ#H}vqJ}DK>T*rIxFd^KiI#+-cC&r;egpsGIs%%ZM%-WL{&DbsT>sy7hyFkS0P8<- zY)2z|Gu!`7ulN5my}stNaoH44+X1=4zcuKJ1k4pbf`O z3>N~x_$Ri&e}A~WbVGn3Ctr6i=_QAW8shVP-Q?!(-_YZ%c#dlgUviI^E~`)2d1s6GfuPLFy+lbCYRn zwfGMGw}3a|%I_NNKaw~0{1Y1jrceA``sBQdm}))nM(7V<&yt8M>yo^{?=t{aCvWL2 zxx;Bx1cQQ=yYZb*7tZ}gp~tQHD9QqI{z*ukQu^S67*(}vpz3g!n7-MmZ^Sr7g5+Y; zUSNudukOH-*-U=JO`RL+p!678aM`?LO$PqA@k@6epUN|z1Pnu)7}Cey9|iH!MR@RPN(F#3?P?G5Wly9-pU4<{Y}5CP9UwYQj~`V9(z z=wUf<;KYKc*OWvuI*1oIc;xP_NM?#`%JQF&62eO<{3#L+$_b3e`2oS@J@n^QM5Y}C zh@Fh>Jl=EpbAxl@8F+2AFo(U5F9b$`q)$1?7S93Y$D5r{+w#9nD4 znR%$?Y&Pk*$CkcTYLvsuj2|O{$gSpj5>Jje7``1S%vdSyZM3FV4f9XjSh;+AcOh zNF_v)w*YA{>j9qb_R2{R&{)l$z|V3T-%f6h^{dsRkn#cPT|3uI9oHC2pH;OAwZ0m; zUGlQ1UXLSF8d?{g6W{>P3M{!fly;`fdA0|3jP;7C3`{{gX=7*HmyWvNR2(1$l!yhz zoff;)9a5UA5eOd&0mS&tJ5K2QiZMQ5iEg2y@1|Yk+^O&XT1Lnkg+%PrD2djlJ8Z?Q z?u?}#KbXXkk0|iz^>~k(4dG0_gHMnL8%CpcG)J(TxuqU5az%^qS!QXW{RJB5gh$~_ zV=E9nu4iShM=0MUWj_rcJIr=Ls48UG9#1wHADx8xyCJAXh^0Xbcu3CLFw3UqCx~?T zcGs#H2jBe0_-|#3l+yngAmN*8{dq@ipK8!JAgPR+_r-C^lhT zphIU$2=AX%L&h=Iv=ftE5KXrt0!}2qSB)w$`BH`bMSfn-re%8Iq%#np9s@G{dPZ`f zZbn0T^F|TAMl&vR=+m4Y^bmTUnUs8(r{Ewz$|n>3QTVgDMpF6SOUXPy6bd@0)q2dm zCG!Jn5e2ILShvSATNkPUi4Fn^nUk?=SyVGpNerK|8NFL`=#Xjb0?1p}$Jl$gOhi2H z)QL;ZFuA+-;SIzd2;{&=CigW2J61j9| z&?Z;bf|$mevIGahVpr}AB2N#uQ*@q*txFdG&%6TCHDg#oVrZY#zp+BPh?g7reBF!! z0)6k;|4CGBk}~!bHd&onJjD8s}3(l3@Xuk7wVAWFPmknMeND` zyt8$1{FQh$Cra4&Gq8d$rz`5@t?+=$N@QiRqmz9pYdzMAIX_(#cisl;D7;k)N#BMk&*u=0ZLk9Me~S!65Lw&SLK*W8YCFAFsK7aU*5e$QJ3_t8_`4OO2Oaf+aHB z?cRYT?N)clTz^H2DfPNs@!Kv$Ng`>eoRATZDrxe_O^T%M9wNEBKgHUOSR3~;7f&`| zwM1-3FJyaZoW!0~-umNdD<1pPjcRa-HPZ{e9fpI)LU!G=wS=`=3H1hGmh!;}DFO`n48G2eLa<^jzT zp+0IzOhUduRu%G;)A^|7VaTyu3`R?PEJRvDtB03*et={Eoc0C#x9=JDS(stI*2E=t z0M!9fMpO?_Vu{fXNskySJ`4SdX2MmMGh%#nJ0&NjsG|B|u6ad1>^fadwf^SPg2N@p zc9@j1v3~bRa~A2|SfJv&<+E!padS@(xhl3X1S=GLX@Zi3i{T8cRjR^SzyZqM^IgoD zTWmk=EH&h$VEyGSjkV-HPK{sW5le9(yaBjH-w=rn z;IKZ|P;KOiZV(h_&dqo1N zRTKX5d}bPUn6!t`1qUy7Z(as;mK6RG6e zHDQp6un{3(INuUyWNCM0q@g3dJx}&;9eRp?KKBP95b$BTdjfWL!S|u(R+~El{XC2D z9wd)F<2$OWt(Vsvg-&#mLTz8v^^f%@Tv6iYtcF_3XBv0Om15gM zdb!Tnn?*#eK{j5I8B2WNLd=c%ylHZY5vEWay0P7FU5loI<#!SiFuboTwmy|_mQopw zD?e{oY0zops93Mc){w7L&upZhR-Q{s{eIR}I$RSL^1YSE6kvz=CtGw+3DjUBI-(kl z_HGqi&5EvCPU5Uq5N+8S&a*K#RtV^J*;{T%JRT0zknyxcUM7@fy}(#Hj9?9ey(0RQ z@7B3nn5Ok&u_Rg@7!`>(jysDF)sKfWv`SV4MlJ`E;$9}eiE_kkxRmgH0h80lL{WE$^6~c$ zK%=Q-QJpHysyP)QRf$tI5h=G6y~wrTaMIC@Fr=(4GIv#ae%6i z7S8Ci#^*pv(dl&-1L>%fO_e|{qxshR#dh;!NrbvBW;fCJ8Qp>!LP zcFjt3Tek-9tPm&tL|dwjZ4`WscgmcIw4F|k*aXwy4j^OX)IKWRU+H+Z{xP*7-6Z!` zI(0f2yvISFgu{NK!Dhb!?^icRq^miKBbt(sp_D>p^437BdIR;1>OO@~k9zd^`M5k= zIr5sZwY)5L#QfH?#I~#oQA1Njqx4oh>@kU!>EB2MP})pqJ`0rux`=A8_HVhsCs=ZV zI-n2l7F$Q1fBha`amKY6XCTd5chXGugvQcU7*T=8s4MlEB5JC$TvPE>6__ zIYsP%BkCI2v$tJ;u}J|-JF1{^imX+Yz3G(c6Lm8a6+Wfcxp0Iiy5EB8?eRi-RkyMz z?9+K2rVAswnD`C51$!4nzQ!M{4*j+ru1Ss9cIb!G!-ydo($I5_hm*alUv4LKu6WLO)uASYfp5o&=b|{qOFY!%%?jW(r%81QDsy|52 zpq7Lu?*|T>k%g`frILM8&p#&|>F_Eog;$SNc`i?7R9%o@@S2DEW4yGfH!m6I3s6NH zY)A7J5_}ZmnwKvCc4fyHW-|x>Fr2$c^07|--2jZMc9=)a4z{dPm0Oz-XYz7mXB7-; z?YU{ts`03tNAEdVpz+%wpiU^x2Nl1Jmp&rPQ3O6Bw3}C_g}bVir6=NuNo3zB*CofMA&O5# zDh@5dR(IQ!geLdeX98?Mk-vX%kh4HCt2NIf0C2!9{~0}H=h|3ve8C7ll-O+jaFRVw zYDwS9q5FFR&EC%Ez`Ur5?V8z{e$HXKb}}%UJ-LkW?;j+StLz01_<+!{Sw5jZ)d5pv zpZ^pkb_Z8t>ycHUgyfgDeakQEPnoF>L^mn#7y5ek4@38to70Rf(gC5gE)cedkc#@6 zL@V?%hyh^O^FcvrtK0dVogH?(4nI)QP^wXB|ByZ!5aQWyWXhn=A{Si%9#M-vSc(0I zH`vu4?!jA18YZi<7|$n8+y$5??wjz3UmC~2S(OP=Gq7yygwmM$1QohHhE891&qd`41CB%R%vUcY- zN^RHf`cebmcBi1j7*=dT-)_k_9EG0eYIftg?Hq9em`&6Qx#%D3tSv1qYj-Q%`7d2h zDDG{W{jDR17^)3bp1ANFBT7cDw%6Iiz!lHyVeB>*-qPDOCKxd1NHgs$+%6*CB}q_a z0~iaMXH%3ZHF2xNuiRU6nxoICcJZ>;+!u zAb#JTW&IMo8Kgn3Bmcch;V}AoJ}I@CQGYphfuiR5EwYYa`$Obs(TNI~q^qBG;L7$y zXWL8#pRpbJ*%|^jk2(5bh#MnfPRw5p+3aP6xN?oQ0GE`<-% zj9y+P+n1E;>Im&|R_~l`?9Zp4{9c}iAK=xDcdT<0?w!4{4w}K5ny*ksgI2G<|ZHZMTryq zJHLch$HcjX#eq!qwdH3>0y2pfBB41CUKbf_m|&Ykr1kI7?u9;+?je)8hSb7% zT$&ZTvJz9u71%*XREC-N$b{5fO{1NGY|fMbcY9QyWZA@oi9Rl%G+dBF0HLkCU_vvX zq}0j}%0yd>sh{;(S+*HNGVGi^`s+wEC_l*Smli3K$rY%J_rbRJ$4mss%2kyv=^-YM zv@}b^C1+CpkV?1uXvMONsAvVq5Y+sbv)F1Lu|8H`b7%(+M5h7G(ie5V{qN$2*N#_h zp$fLjW4o|BJEN^A@X#N&97t^jEkHl^jW**9%PTiQX`lP4k(2k}=Vk4;AvSICEnMl* z>Un&P9Gx$5N6n#PEx?=F!uxvG=q1NXWiV-%M)Tl;9#cjSG)y%_gDnsmm8y5SHg-98 zZn5up5+U~T3?y8(=@RPBk+ilPt1YlB0-5xUj_>Q{3Qzk&Zih}R&9O8z+|nb zn!d67ECR`3TM(7y*YBNGtZUb4Q`j0&eK|F!@VCy_qur8cECBy7avl?1dTZ&U z<9(TRMQzt!*KBPeWi+JQQz{=(uGupvW}~wmmz_m9j`U@;`Q#9}Ax{<1K8CL9bYV@Q zGC(9OCkq1wkwF@0Pd{-`Wh!0ceoQ7VPe`Dw6XUD4z_cRZ18{u2sSe$rrABrpjUeSTZh=OWwpw;GlEIR7rsQ!0{1kihjNOy=jQ> zXFYDGE4}p=BLs9>KAH$?oH_xrJjkTGxp#RR>tV4Q-qgr`%Bg{|~5 z&uHXJcS33N^w+E8xZ15>X8 zI*jaNZi8TYZghT5EH!GJ2Tm2I3t-yLuB1OBMq76LNyvcZBG0Z99b!^G@&Xb3r=dGG zEA)2%X?wGBI4GbS{WA~7wK0Hq4N{lP$&KkPwb9){7@JG(E}f9KCZx2{Oym)4LyOrF z7d%0Zi#3OA5sN}vtGlR4y|IU&HRPs%tskeKF#Z;P-%1dIs+CX~GwzwNmX@%Y*2}R# z6iJ+|OA{~$BhYoauVbQ0SSJBn>aT<9+7|x$2vR*?&*^N}Z+Sl&_Iq{rZWf}iD`dK> z`Im&C*O+6+#~HN(Ba=$8D!ADIgbkAz%|q|L3$OT@8-}T_DHQFKEEBk<#3KXY&h`A< z=B?a~63w@}rAP3Cd`!LN6!z%K-oy*lqj{JMe@eXRm4CN7Nr{ciB`4oX$(y(NRFf7g z{mq|%_I0pyqu9)(XrR*c(=@~-$U3>LA9E_glB=m`g|ZG)&2EMoP8mb4>+U8c8}^q{ z-!=(*!>`RAin^49zGQg94OhtW#1vLl+THOZQkMD9WBuOVJXgJn+p|#mbG-}U!+Xx) zWOEAz&A@=63?f{(mZJ}L>Dv}gpnl!1$S=|x;CWe28A^uhJ2Ye^750XX!!QE)Yckes zg-@-9*|NqUa>oYEx3-=a8N>(}jmA&T@Q$^xiwM_wef z-8_+6>lvL`GI}EEKO0pf^#ZXk+uNrF>?=!UoJSMjY0)Lz*V+;~M+S+zhXsO?EmVF> z|LK4J%DV4553kiec$QrxD!@zqQd{^2yZ?1N@k_2>&b6<%45U{Q=IYlAbB*3pUz&BG zgW!t%``!w+QHlI3yY~JMul|#&X}6&BYj3}I^!*|Ivi1slagIzHi_%0hdLHvPXWt8Y zTTJ*d@0dR;Xz<|}xu>kGW6CLe>yr7trS^H2{huQh`7udCZ9Vr>BUz4GHw5+ja%wKz zts+%LS*MFjPAqgBajHpMgpBsNOP${{nU(G5x|3wO9=R0$_dWq?f{>!OHx?G9CFyGc z6FxS>($TtWf2#KYSbfNe*ecRJU0Y8=Yv|U>M87-Qsbk((SRmEQScJ173sq}@?wYDt zSTLpK{4@Q=#B?(^8YtZS=85Km^nhm?&u={@2IHiNT6~=)EhG0uloA#>x-+00yFHH;&y`bzX9=+-U zH)ISBT4zL!%ye)EzJEFq(c@we>%PyWztE(cIB!W&6w;%Hq`iu1NE$DM3_ZdfBF`B% z)!!p~299t5bP9f}zm0)V^5fZU8N{Qd-QfHG#Ue$LiNkwy>by~?C~mPoIZC&mx1 z>HgYC2g}pFGdLB2KCVjB(EAQJ^*Zop z`KHu4xZ8J^+c`G^P-Dk}xy^BlnkbkUou~Km-TeC7CI}FfowN2eN+cFHcmR1GL8Gr|Tb=r2*AZ8oQ}f$|8Ai8+#`=dfC|hgkGMt zo$WFq>eX=&89duY>TVZ8xbh2e%UH!+brxKV60Xl)<*sqvKVJNOOC6TP6CY=V48-PW z=|#uEQV;i`QYFh1N+{Vd_c){;bz=_jJnnEd46t|iL`6C2=*RqDhkq+e*}y-J9XX-J zyugpLEOZ2FXdr|h`eP|zv0EL=cj!7(c~6wU(rQ?P7m2Z=a4;}x!=+*c0$lw+Mr_cG zU;ov@&Tn`eHNpb`{^{2K=RPhM_Wu!l|GUbY;s5k;|GR(v|Dz-y*4nh+WXJ5iRdJ*S zZx%%5`GH$pi$^E}(JUBNA`@VQ=_MqSJRFLyme_a*@BQ*cN~TjtDZz!B1R_fQmZw0F zw61XL_v`(0^{I|hjpoOlvHBM=XRk-VUWQbQ*--DLMgJk)yTyx|09UL@CSBRt5$!MU z%{6I{(gDyq%1!x%Eom`zBcV9O@Q3V#xmsMJ2o@}9V#@_mgT1pKR2~V#MnhN_Dv!>t zwF1*7%le5+XW!dBcuBM~TwrpTDHBS5A;XF0>h7$(vhqS&VZ5cx7MRg8m`=6^2R@NA zdU2Yb`++pW8o9nxXUr^#N2p??Dx$dR)cXe=Db}M6W$IDDn5KH?PSE)F;X5b`v!DtxpU!fz^PncQAow6wwCjDZvqLhxZFpMtDpKmq0HLNDAB>RDP zXV3BGNVCkyRLDG;*NfPR&#VLELwO!#IPsfND$ZWq_<5dO=cG_i{aaao@Dg5F3_XN2 zHAcAj@50X_&65+)e)QkIm#-&?!N)nerptuYkYjwSZ)Q&gRcq3PL-yi&x}$Zd^rz`t zRpl`gNemik2&0tvtF{eD&Z`rxe+Cl@;YrO{M(c|!J+3hH=ncN0cO-r1m}yv5gn>F$ z-3Z=tt%*K9w8Te}>*#Snw;CBAaRzdeSc^!WJbnsGZa>PQxMHC(%JfCRZgGlX%}O$e zEI5tYo|Vr;cB{5cBFVkANK)n!lyjzi-DOI58Wvj|vsD?>X|Fd=j@15H+ff2OVO`GP zYPmIkhK(CVMxyDS7X;^@r5N2J3FS-gEek{3j8*VkRS~Kv{ZW8!( z>gPAgG?VI!iJ$JsAL{!QJz8YC6d2ni1m$Z10uyF|{gGPES!#pIM0?EZ*!80xVzSBA z=T7RBuCv(_rh2618aqwgYgIGd#q7_#2fiq$MhM^x7wRm6`Ix#~lT~-Ger`5}OpJqP zp)@(5C-YY0piLldIXpqEiq_&q>Jr zQ-Nn&9CIg?!VimG{2k98;1^CHV0YYp^b~N6)xbI{SwwJm7{$P-npTAaHldF}<+>hi zTP0a!UB>0L4aKoYFecUp{`TV#L8e{P=tIpw!%_(rxqKO7Iy9vSXBrN_ZD$=CL-4{3 z!;TG-vAFQ!&~IMMvzCB^piJlfrqCeB?WkJX_|?6ejzVM1Fsd}_^5X6=284UL7>Pv& z05#*{C1mMfmj2+wD??cSbcunqM&iF$% z*mJ%5C>@{gE<7G;$#Y|*b2AsuDh;Z{~sD8l7f(RkjfYl2~NWUrw zH6qaH4w@Lp%H=u1Kc0*mbY}u4fc$g_a0Is0U(R}>Keky=U1nO}(}&aZ_p!W!x*oj=4Hqrp+J{TJ4DL`X>+&fxNBOJsX zs1@Q-9)GBcM%qTx_EUKygIs9sM$4y9BGoQ5acc@w-ecNCHsvs|HW*SIQ_0Klar zsl+-67xX+l*_S+ZmHiZQXF!QXLk(RaUM+z+0W4iiL(Jpe0f$eSP*cmbl{y<{8u&S< z8hZ>rQVTipZF6?}Sw0PxRY^FPPX+W)%71P=WFp`@}ivbQofb8-EvC~~m>my+s?VAg_xWY*#z`)^7r zw|D`EAVJvS7a!;-F5#~xF|`tQl`v_%Mm03<8{GC2PL&UTq6zmpqIg!3^_$a|s~65% z!C$nQ;7xF*b+SQ(x8Z?(AI~H@d8d4h9+5>y{6EKT(Qg!KKiH>P+ys>wt|Io2;b^%) zUHOrs;QKP!-JdA`vg7tp%&J$qsu9v!GZ--s)>xT!x8#k7Yj(qybzhB<)r#V zA>f$kov=~9^v#CZZV$<<1jv&L|JK^wfIM?D>uD_$a2SfzR3Es6hDXSE4`-5_>^C%i zsD=2D71E7vW5ml+anxOR{7Hf1=eEm}yRG9Ak<;GGBUB&{E<7ZhZOdI)6B%9-E4w%( zLvp^QLq8-~D$e@V`RUE?t(~L(>z^FR*gBeG{=NM5Po)1R2Uh%VRmnxgefKOvT&?&-$AYH;Vgl(DoQz0 zdc`f%K*Km?g@w{3g|?~l!nCoqE;nK`7E#OTi#n(5srPmN4si4BPBn@1bo4RmxIb7) zUofHdu3$B>@ZfoWtR^)3GVEUZyIM0x+On+DV8WDo(%L|mOpEM#(pu>#CAvE&S@}e< z)GuqP$#?!59yI#gkQ=Q-*J^H9O^I7<7 zqjY4mu(-kRMWp%+ct?1ibTV?ezma~2T#b%x%>E5j|Ay{&O=we-pET)PRx*rI+W1it zWvxGE+T^8;Wk+R_HnP;hg(M$u`K2jyk_sXG)9>URibvHnt`tGIh)3_wW)>jP72`%) znT*xLqkGIMd2=(SG?77UD}B17;?%_#D}FllLF~!%&N_rYetwMj1h~Dt0(dbBaS|4D z126A24<`ht5=9yi-~Me1{tF6DR|YN@RIJUV|oLv4gai$Av}ymHq-r zHe95@aCK(eA9FplQE2%e^7$vy?{Q$LHZDf(U~}nlH;W^%@1RkkNBv zPTY$RpdDK)5x{k@ru&L->3$U|WERWmIq~E$Dy8OmLds;f{Ql2`U#V&#CbFd}wG@;3 z4l@W4iUJU=r{CJB-TM`ZgYV`In?2%-M%2nn-_v3zNO6Ru`uOEsGNGOinf#-%G84C=u%+ku5h$m_UG83^m;uxGIizT z_j7UfW9P^GwOGuS>;_RP*v*-kH%d9jiJwuJ-vZRcfWN~&m#rQ-U2xz65ZZMGqns*g z&P<1>ym$|6xNb;XG}8`vEBDvAxEEgn^9SP==$KltHgACCChQWTMa%=5j1Z#|CPLO$H5|UG_sx!sQNWrU#@-iC;%R?IF~|SHk}c7^57*0@bXrO2SFaF8_?r z$tG?Ks3uk)S1Bic+#uv1r5HUlK}>rHazEral^C@MbMV8<`D4OyF7?nXk79tqxnu_% zh)z)viCZE)IH0id6&l`6IiOUJ0$yrDtU#7A2y~C|N|utpk+B)a?VrP=WH+WgR4La;FRBIw5SVNZ1eP;bIX9G4Y5aw!I^hWbcoN`K>0) zVu>i&KJvoxJ1az0Y#$jGdn0IC7L%m?{AKYcY2pN(SWLiyoXYGGTiPZH^K zw%@98;u4CX0eF$K5iWXge?q967KiNnSqSP8M1#Xbz;6yWc{lo~-0bSBNUnRqs!9 z)Zd*zu|BK>g7rM{%?<9^mKq~p%aE8cUkdT_f*P&dTuzs8P%3LOAgMW>? zg{d-?<};k$uUtk$D&wWIE{UO1?ahq%7_}s$%y3Na)nLB_)HbE#%&!k>Wd7{>_4w`V zHmqoV{_M=;rJ@z4mRG-<4#q#&L%j_O8{@mKe-v>1>u3MB6E;}8VT`{a4}#V$Q*I1j zenJ1Fk$8F#5H_v7k{*T$IT-M8jP)9;<<$#cx7&sRNn^kI8GkZ~7?n>C@teZB<6hiPWl*GZzUV%fWb-_~ zZbOuFBIYI8QVAH#3bH{IMzDD2nHL!C{b&e@vvnbEccTSyV|K*XjOIb0yU7U)4n7UV z91LrlJAVf@hxxsm+=Oqi#Qv6g9!R%1aDbbMyyY0~e#Et^{RVRDhnbVx%fQrBg6}hsd!YGGXxq9cU$Z#Je}8sjw@bbtM8jo@T zVf>R|xE%+V1FSD?y0-vt)u4Cr(uuRXgiC`l2@WdXkv z(_PK=mjpu8=GAb;5o9h|+S%XqXX*D2s+$go1oNP}u(wWtI|_%qk~QpH*mbIyQ6P*B z$64~q&k98lnwnBHvoR3ucwY=j1Wx|iP@JV&_b&VlTr+btEm0JlOh(*Paq@1fG^Hz< zl~7Fdoq6BJR~R>m{ArLC#-O#SpbZ7#nL(yTi&zk5{@b0nS6K{}IHZNjX`t30>m{!^ zbJNGvAy~ak5h?A-hW@5gsm<>~(1%%TSN3BOPO+vDbJT+uFeL#d^i7ActVi_RX$mA> z(>XI$KWc2Z-P9ZGJxJR5115vecFNrndm{8H&qsgfjnAF#RO+)Ag5D(~dbPGZ z_V}!#pNsqG*S*7le#+0EP#R`VyZ6lFYz8WEKJozgxWMSu?-@#Bwwl<49t@PDQ#CMH zeR@n&ilvu{MCK zx#J^6Q)^!AwMPo2mLxE;3WFONTHVv4UQoqOWP%4VspU*z%*cE}%2cn{IP>yEYg5WY z$mGb;aBRTwtx8SEJCqTXO0X&%ws0=v&^J*Hb@koZ(x;-m1vKlrzxh=0aX#LuC%h(iVCRu)*RPhIc29^Z;R4O*BcTvL4pTO z6Dfmc4{+&-p9J?Ec$_RxZzE2d-3D82e#Vx!qTPhr=-gh$_c(Q9U%%kdixG!R~(j_aB~>dJeA^7g7;cUzHBwF8Rwt7etPb-Sf?V3#rh-Jb#|0iC~8;Ej3=GW zx^~Hb%kch}=!ig=iJi2wCDqSgL%G&&)NWjLM1jHAU$7j|wmZ8i@V1i&l$YO|(Pp`x zDA)RQtR0@7A5ew>54kwQUwJF(8ofcE{9;r{zn55M*|EuIXwcq(zvxAqN0hlM2&n$F zz>>P0?q`FQ*^xPOYI!$V2yRa9NIVT)7u@u!WchG{3`%bY9=P+ay03g#7m#XHc z8Zjif{+=69X_nhsY1i+~sCBUETp2AjRq3U6iPqBHD2@f0i?aM=nOsa2$$6xNQ1ok* z$d7=z^-=;=^jvX-vd;r;GQSkgtf1f6J`46_Ejq=!78`b>^>}>(_rEqsTKh>_sOEf= ziQEhfJRQ^IY+*ukqbPke!BM>%0rwD7mzi|K%mz$iZHPBcuQe%orJE;)y>3Eim~*Mg zW!G!*F6SrQTtn$xAr+}y8NeL-2FXo~i1?O1mS@oWvHKSgi``32=lf`r&n`jO(HWV^ z>sYVMx4jKvbu3-5-i8O}Wbh?y5X-RZN8yjhM4PTs5kV?nwGzMZJb$)X@x@7t?P_g> zBsO<)R4aVB_hkGrTq3mQ==vn)5y3Q-{r5wJ9fhaRu9o6)vN4wDt(rLNIlE^CH}-F9xgCWs!Adlz`HeSrugX)>py z>!P8T49y-R*kTD7o44`I!9fdnj$!5g0q<=KO@Y(1q+@;7)LG0q3T{>LGE<$BtX>za zhTHO} zdQvp}&YD5%24Zq?dy`5){vyWq=NDhPm;8ROiQM?3tdYix-ACc7_T_H_!Wm|(Zrl-b z^Tgk)sGbD*#oQ0Udiy$l{TljJ9R2W{OaHOvGck!K8J8_={uQzNf{w((7;MQ#nCPCe z#7XT>VfJ6wzs}bB)D6nFii22<>^TkUnKQ!f*-8@M=H#7XqMmFMdY6e+G zl|gfAA(!jt04s#11Z&m^-Xh&dpQUG^4ktER^tM)?46*4VTLnCq`j0*kd}A3&j$vwq zK{~$(V)CD9;C`Y#jul)d9U)4sO7J~TQsh+(bZq7q8DR}P`^

?BC!+*Yfqfx3{>( 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 Optional[CheckpointMetadata]: - """Save a model checkpoint with improved error handling and validation""" + """Save a model checkpoint with improved error handling and validation using unified registry""" try: - timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') - checkpoint_id = f"{model_name}_{timestamp}" - - model_dir = self.base_dir / model_name - model_dir.mkdir(exist_ok=True) - - checkpoint_path = model_dir / f"{checkpoint_id}.pt" - + from utils.model_registry import save_checkpoint as registry_save_checkpoint + performance_score = self._calculate_performance_score(performance_metrics) - + if not force_save and not self._should_save_checkpoint(model_name, performance_score): logger.debug(f"Skipping checkpoint save for {model_name} - performance not improved") return None - - success = self._save_model_file(model, checkpoint_path, model_type) - if not success: - return None - - file_size_mb = checkpoint_path.stat().st_size / (1024 * 1024) - - metadata = CheckpointMetadata( - checkpoint_id=checkpoint_id, + + # Use unified registry for checkpointing + success = registry_save_checkpoint( + model=model, model_name=model_name, model_type=model_type, - file_path=str(checkpoint_path), - created_at=datetime.now(), - file_size_mb=file_size_mb, + performance_score=performance_score, + metadata={ + 'performance_metrics': performance_metrics, + 'training_metadata': training_metadata, + 'checkpoint_manager': True + } + ) + + if not success: + return None + + # Get checkpoint info from registry + registry = get_model_registry() + checkpoint_info = registry.metadata['models'][model_name]['checkpoints'][-1] + + # Create CheckpointMetadata object + metadata = CheckpointMetadata( + checkpoint_id=checkpoint_info['id'], + model_name=model_name, + model_type=model_type, + file_path=checkpoint_info['path'], + created_at=datetime.fromisoformat(checkpoint_info['timestamp']), + file_size_mb=0.0, # Will be calculated by registry performance_score=performance_score, accuracy=performance_metrics.get('accuracy'), loss=performance_metrics.get('loss'), @@ -112,9 +124,8 @@ class CheckpointManager: training_time_hours=training_metadata.get('training_time_hours') if training_metadata else None, total_parameters=training_metadata.get('total_parameters') if training_metadata else None ) - - # W&B disabled - + + # Update local checkpoint tracking self.checkpoints[model_name].append(metadata) self._rotate_checkpoints(model_name) self._save_metadata() @@ -128,14 +139,42 @@ class CheckpointManager: def load_best_checkpoint(self, model_name: str) -> Optional[Tuple[str, CheckpointMetadata]]: try: - # First, try the standard checkpoint system + from utils.model_registry import load_best_checkpoint as registry_load_checkpoint + + # First, try the unified registry + registry_result = registry_load_checkpoint(model_name, 'cnn') # Try CNN type first + if registry_result is None: + registry_result = registry_load_checkpoint(model_name, 'dqn') # Try DQN type + + if registry_result: + checkpoint_path, checkpoint_data = registry_result + + # Create CheckpointMetadata from registry data + metadata = CheckpointMetadata( + checkpoint_id=f"{model_name}_registry", + model_name=model_name, + model_type=checkpoint_data.get('model_type', 'unknown'), + file_path=checkpoint_path, + created_at=datetime.fromisoformat(checkpoint_data.get('timestamp', datetime.now().isoformat())), + file_size_mb=0.0, # Will be calculated by registry + performance_score=checkpoint_data.get('performance_score', 0.0), + accuracy=checkpoint_data.get('accuracy'), + loss=checkpoint_data.get('loss'), + reward=checkpoint_data.get('reward'), + pnl=checkpoint_data.get('pnl') + ) + + logger.debug(f"Loading checkpoint from unified registry for {model_name}") + return checkpoint_path, metadata + + # Fallback: Try the standard checkpoint system if model_name in self.checkpoints and self.checkpoints[model_name]: # Filter out checkpoints with non-existent files valid_checkpoints = [ - cp for cp in self.checkpoints[model_name] + cp for cp in self.checkpoints[model_name] if Path(cp.file_path).exists() ] - + if valid_checkpoints: best_checkpoint = max(valid_checkpoints, key=lambda x: x.performance_score) logger.debug(f"Loading best checkpoint for {model_name}: {best_checkpoint.checkpoint_id}") @@ -146,22 +185,22 @@ class CheckpointManager: logger.warning(f"Found {invalid_count} invalid checkpoint entries for {model_name}, cleaning up metadata") self.checkpoints[model_name] = [] self._save_metadata() - + # Fallback: Look for existing saved models in the legacy format logger.debug(f"No valid checkpoints found for model: {model_name}, attempting to find legacy saved models") legacy_model_path = self._find_legacy_model(model_name) - + if legacy_model_path: # Create checkpoint metadata for the legacy model using actual file data legacy_metadata = self._create_legacy_metadata(model_name, legacy_model_path) logger.debug(f"Found legacy model for {model_name}: {legacy_model_path}") return str(legacy_model_path), legacy_metadata - + # Only warn once per model to avoid spam if model_name not in self._warned_models: logger.info(f"No checkpoints found for {model_name}, starting fresh") self._warned_models.add(model_name) - + return None except Exception as e: diff --git a/utils/model_registry.py b/utils/model_registry.py new file mode 100644 index 0000000..2d91bb2 --- /dev/null +++ b/utils/model_registry.py @@ -0,0 +1,446 @@ +#!/usr/bin/env python3 +""" +Unified Model Registry for Centralized Model Management + +This module provides a unified interface for saving, loading, and managing +all machine learning models in the trading system. It consolidates model +storage from multiple locations into a single, organized structure. +""" + +import os +import json +import torch +import logging +import pickle +from pathlib import Path +from typing import Dict, Any, Optional, Tuple, List +from datetime import datetime +import hashlib + +logger = logging.getLogger(__name__) + +class ModelRegistry: + """ + Unified model registry for centralized model management. + Handles saving, loading, and organization of all ML models. + """ + + def __init__(self, base_dir: str = "models"): + """ + Initialize the model registry. + + Args: + base_dir: Base directory for model storage + """ + self.base_dir = Path(base_dir) + self.saved_dir = self.base_dir / "saved" + self.checkpoint_dir = self.base_dir / "checkpoints" + self.archive_dir = self.base_dir / "archive" + + # Model type directories + self.model_dirs = { + 'cnn': self.base_dir / "cnn", + 'dqn': self.base_dir / "dqn", + 'transformer': self.base_dir / "transformer", + 'hybrid': self.base_dir / "hybrid" + } + + # Ensure all directories exist + self._ensure_directories() + + # Metadata tracking + self.metadata_file = self.base_dir / "registry_metadata.json" + self.metadata = self._load_metadata() + + logger.info(f"Model Registry initialized at {self.base_dir}") + + def _ensure_directories(self): + """Ensure all required directories exist.""" + directories = [ + self.saved_dir, + self.checkpoint_dir, + self.archive_dir + ] + + # Add model type directories + for model_dir in self.model_dirs.values(): + directories.extend([ + model_dir / "saved", + model_dir / "checkpoints", + model_dir / "archive" + ]) + + for directory in directories: + directory.mkdir(parents=True, exist_ok=True) + + def _load_metadata(self) -> Dict[str, Any]: + """Load registry metadata.""" + if self.metadata_file.exists(): + try: + with open(self.metadata_file, 'r') as f: + return json.load(f) + except Exception as e: + logger.warning(f"Failed to load metadata: {e}") + return {'models': {}, 'last_updated': datetime.now().isoformat()} + + def _save_metadata(self): + """Save registry metadata.""" + self.metadata['last_updated'] = datetime.now().isoformat() + try: + with open(self.metadata_file, 'w') as f: + json.dump(self.metadata, f, indent=2) + except Exception as e: + logger.error(f"Failed to save metadata: {e}") + + def save_model(self, model: Any, model_name: str, model_type: str = 'cnn', + metadata: Optional[Dict[str, Any]] = None) -> bool: + """ + Save a model to the unified storage. + + Args: + model: The model to save + model_name: Name of the model + model_type: Type of model (cnn, dqn, transformer, hybrid) + metadata: Additional metadata to save + + Returns: + bool: True if successful, False otherwise + """ + try: + model_dir = self.model_dirs.get(model_type, self.saved_dir) + save_dir = model_dir / "saved" + + # Generate filename with timestamp + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + filename = f"{model_name}_{timestamp}.pt" + filepath = save_dir / filename + + # Also save as latest + latest_filepath = save_dir / f"{model_name}_latest.pt" + + # Save model + save_dict = { + 'model_state_dict': model.state_dict() if hasattr(model, 'state_dict') else {}, + 'model_class': model.__class__.__name__, + 'model_type': model_type, + 'timestamp': timestamp, + 'metadata': metadata or {} + } + + torch.save(save_dict, filepath) + torch.save(save_dict, latest_filepath) + + # Update metadata + if model_name not in self.metadata['models']: + self.metadata['models'][model_name] = {} + + self.metadata['models'][model_name].update({ + 'type': model_type, + 'latest_path': str(latest_filepath), + 'last_saved': timestamp, + 'save_count': self.metadata['models'][model_name].get('save_count', 0) + 1 + }) + + self._save_metadata() + + logger.info(f"Model {model_name} saved to {filepath}") + return True + + except Exception as e: + logger.error(f"Failed to save model {model_name}: {e}") + return False + + def load_model(self, model_name: str, model_type: str = 'cnn', + model_class: Optional[Any] = None) -> Optional[Any]: + """ + Load a model from the unified storage. + + Args: + model_name: Name of the model to load + model_type: Type of model (cnn, dqn, transformer, hybrid) + model_class: Model class to instantiate (if needed) + + Returns: + The loaded model or None if failed + """ + try: + model_dir = self.model_dirs.get(model_type, self.saved_dir) + save_dir = model_dir / "saved" + latest_filepath = save_dir / f"{model_name}_latest.pt" + + if not latest_filepath.exists(): + logger.warning(f"Model {model_name} not found at {latest_filepath}") + return None + + # Load checkpoint + checkpoint = torch.load(latest_filepath, map_location='cpu') + + # Instantiate model if class provided + if model_class is not None: + model = model_class() + model.load_state_dict(checkpoint['model_state_dict']) + else: + # Try to reconstruct model from state_dict + model = type('LoadedModel', (), {})() + model.state_dict = lambda: checkpoint['model_state_dict'] + model.load_state_dict = lambda state_dict: None + + logger.info(f"Model {model_name} loaded from {latest_filepath}") + return model + + except Exception as e: + logger.error(f"Failed to load model {model_name}: {e}") + return None + + def save_checkpoint(self, model: Any, model_name: str, model_type: str = 'cnn', + performance_score: float = 0.0, + metadata: Optional[Dict[str, Any]] = None) -> bool: + """ + Save a model checkpoint. + + Args: + model: The model to checkpoint + model_name: Name of the model + model_type: Type of model + performance_score: Performance score for this checkpoint + metadata: Additional metadata + + Returns: + bool: True if successful, False otherwise + """ + try: + model_dir = self.model_dirs.get(model_type, self.checkpoint_dir) + checkpoint_dir = model_dir / "checkpoints" + + # Generate checkpoint ID + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + checkpoint_id = f"{model_name}_{timestamp}_{performance_score:.4f}" + + filepath = checkpoint_dir / f"{checkpoint_id}.pt" + + # Save checkpoint + checkpoint_data = { + 'model_state_dict': model.state_dict() if hasattr(model, 'state_dict') else {}, + 'model_class': model.__class__.__name__, + 'model_type': model_type, + 'model_name': model_name, + 'performance_score': performance_score, + 'timestamp': timestamp, + 'metadata': metadata or {} + } + + torch.save(checkpoint_data, filepath) + + # Update metadata + if model_name not in self.metadata['models']: + self.metadata['models'][model_name] = {} + + if 'checkpoints' not in self.metadata['models'][model_name]: + self.metadata['models'][model_name]['checkpoints'] = [] + + checkpoint_info = { + 'id': checkpoint_id, + 'path': str(filepath), + 'performance_score': performance_score, + 'timestamp': timestamp + } + + self.metadata['models'][model_name]['checkpoints'].append(checkpoint_info) + + # Keep only top 5 checkpoints + checkpoints = self.metadata['models'][model_name]['checkpoints'] + if len(checkpoints) > 5: + checkpoints.sort(key=lambda x: x['performance_score'], reverse=True) + checkpoints_to_remove = checkpoints[5:] + + for checkpoint in checkpoints_to_remove: + try: + os.remove(checkpoint['path']) + except: + pass + + self.metadata['models'][model_name]['checkpoints'] = checkpoints[:5] + + self._save_metadata() + + logger.info(f"Checkpoint {checkpoint_id} saved with score {performance_score}") + return True + + except Exception as e: + logger.error(f"Failed to save checkpoint for {model_name}: {e}") + return False + + def load_best_checkpoint(self, model_name: str, model_type: str = 'cnn') -> Optional[Tuple[str, Any]]: + """ + Load the best checkpoint for a model. + + Args: + model_name: Name of the model + model_type: Type of model + + Returns: + Tuple of (checkpoint_path, checkpoint_data) or None + """ + try: + if model_name not in self.metadata['models']: + logger.warning(f"No metadata found for model {model_name}") + return None + + checkpoints = self.metadata['models'][model_name].get('checkpoints', []) + if not checkpoints: + logger.warning(f"No checkpoints found for model {model_name}") + return None + + # Find best checkpoint by performance score + best_checkpoint = max(checkpoints, key=lambda x: x['performance_score']) + checkpoint_path = best_checkpoint['path'] + + if not os.path.exists(checkpoint_path): + logger.warning(f"Checkpoint file not found: {checkpoint_path}") + return None + + checkpoint_data = torch.load(checkpoint_path, map_location='cpu') + + logger.info(f"Best checkpoint loaded for {model_name}: {best_checkpoint['id']}") + return checkpoint_path, checkpoint_data + + except Exception as e: + logger.error(f"Failed to load best checkpoint for {model_name}: {e}") + return None + + def archive_model(self, model_name: str, model_type: str = 'cnn') -> bool: + """ + Archive a model by moving it to archive directory. + + Args: + model_name: Name of the model to archive + model_type: Type of model + + Returns: + bool: True if successful, False otherwise + """ + try: + model_dir = self.model_dirs.get(model_type, self.saved_dir) + save_dir = model_dir / "saved" + archive_dir = model_dir / "archive" + + latest_filepath = save_dir / f"{model_name}_latest.pt" + + if not latest_filepath.exists(): + logger.warning(f"Model {model_name} not found to archive") + return False + + # Move to archive with timestamp + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + archive_filepath = archive_dir / f"{model_name}_archived_{timestamp}.pt" + + os.rename(latest_filepath, archive_filepath) + + logger.info(f"Model {model_name} archived to {archive_filepath}") + return True + + except Exception as e: + logger.error(f"Failed to archive model {model_name}: {e}") + return False + + def list_models(self, model_type: Optional[str] = None) -> Dict[str, Any]: + """ + List all models in the registry. + + Args: + model_type: Filter by model type (optional) + + Returns: + Dictionary of model information + """ + models_info = {} + + for model_name, model_data in self.metadata['models'].items(): + if model_type and model_data.get('type') != model_type: + continue + + models_info[model_name] = { + 'type': model_data.get('type'), + 'last_saved': model_data.get('last_saved'), + 'save_count': model_data.get('save_count', 0), + 'checkpoint_count': len(model_data.get('checkpoints', [])), + 'latest_path': model_data.get('latest_path') + } + + return models_info + + def cleanup_old_checkpoints(self, model_name: str, keep_count: int = 5) -> int: + """ + Clean up old checkpoints, keeping only the best ones. + + Args: + model_name: Name of the model + keep_count: Number of checkpoints to keep + + Returns: + Number of checkpoints removed + """ + if model_name not in self.metadata['models']: + return 0 + + checkpoints = self.metadata['models'][model_name].get('checkpoints', []) + if len(checkpoints) <= keep_count: + return 0 + + # Sort by performance score (descending) + checkpoints.sort(key=lambda x: x['performance_score'], reverse=True) + + # Remove old checkpoints + removed_count = 0 + for checkpoint in checkpoints[keep_count:]: + try: + os.remove(checkpoint['path']) + removed_count += 1 + except: + pass + + # Update metadata + self.metadata['models'][model_name]['checkpoints'] = checkpoints[:keep_count] + self._save_metadata() + + logger.info(f"Cleaned up {removed_count} old checkpoints for {model_name}") + return removed_count + + +# Global registry instance +_registry_instance = None + +def get_model_registry() -> ModelRegistry: + """Get the global model registry instance.""" + global _registry_instance + if _registry_instance is None: + _registry_instance = ModelRegistry() + return _registry_instance + +def save_model(model: Any, model_name: str, model_type: str = 'cnn', + metadata: Optional[Dict[str, Any]] = None) -> bool: + """ + Convenience function to save a model using the global registry. + """ + return get_model_registry().save_model(model, model_name, model_type, metadata) + +def load_model(model_name: str, model_type: str = 'cnn', + model_class: Optional[Any] = None) -> Optional[Any]: + """ + Convenience function to load a model using the global registry. + """ + return get_model_registry().load_model(model_name, model_type, model_class) + +def save_checkpoint(model: Any, model_name: str, model_type: str = 'cnn', + performance_score: float = 0.0, + metadata: Optional[Dict[str, Any]] = None) -> bool: + """ + Convenience function to save a checkpoint using the global registry. + """ + return get_model_registry().save_checkpoint(model, model_name, model_type, performance_score, metadata) + +def load_best_checkpoint(model_name: str, model_type: str = 'cnn') -> Optional[Tuple[str, Any]]: + """ + Convenience function to load the best checkpoint using the global registry. + """ + return get_model_registry().load_best_checkpoint(model_name, model_type) diff --git a/web/clean_dashboard.py b/web/clean_dashboard.py index 8950467..5991611 100644 --- a/web/clean_dashboard.py +++ b/web/clean_dashboard.py @@ -4710,53 +4710,85 @@ class CleanTradingDashboard: stored_models = [] + # Use unified model registry for saving + from utils.model_registry import save_model + # 1. Store DQN model if hasattr(self.orchestrator, 'rl_agent') and self.orchestrator.rl_agent: try: - if hasattr(self.orchestrator.rl_agent, 'save'): - save_path = self.orchestrator.rl_agent.save('models/saved/dqn_agent_session') - stored_models.append(('DQN', save_path)) - logger.info(f"Stored DQN model: {save_path}") + success = save_model( + model=self.orchestrator.rl_agent.policy_net, # Save policy network + model_name='dqn_agent_session', + model_type='dqn', + metadata={'session_save': True, 'dashboard_save': True} + ) + if success: + stored_models.append(('DQN', 'models/dqn/saved/dqn_agent_session_latest.pt')) + logger.info("Stored DQN model via unified registry") + else: + logger.warning("Failed to store DQN model via unified registry") except Exception as e: logger.warning(f"Failed to store DQN model: {e}") - + # 2. Store CNN model if hasattr(self.orchestrator, 'cnn_model') and self.orchestrator.cnn_model: try: - if hasattr(self.orchestrator.cnn_model, 'save'): - save_path = self.orchestrator.cnn_model.save('models/saved/cnn_model_session') - stored_models.append(('CNN', save_path)) - logger.info(f"Stored CNN model: {save_path}") + success = save_model( + model=self.orchestrator.cnn_model, + model_name='cnn_model_session', + model_type='cnn', + metadata={'session_save': True, 'dashboard_save': True} + ) + if success: + stored_models.append(('CNN', 'models/cnn/saved/cnn_model_session_latest.pt')) + logger.info("Stored CNN model via unified registry") + else: + logger.warning("Failed to store CNN model via unified registry") except Exception as e: logger.warning(f"Failed to store CNN model: {e}") - + # 3. Store Transformer model if hasattr(self.orchestrator, 'primary_transformer') and self.orchestrator.primary_transformer: try: - if hasattr(self.orchestrator.primary_transformer, 'save'): - save_path = self.orchestrator.primary_transformer.save('models/saved/transformer_model_session') - stored_models.append(('Transformer', save_path)) - logger.info(f"Stored Transformer model: {save_path}") + success = save_model( + model=self.orchestrator.primary_transformer, + model_name='transformer_model_session', + model_type='transformer', + metadata={'session_save': True, 'dashboard_save': True} + ) + if success: + stored_models.append(('Transformer', 'models/transformer/saved/transformer_model_session_latest.pt')) + logger.info("Stored Transformer model via unified registry") + else: + logger.warning("Failed to store Transformer model via unified registry") except Exception as e: logger.warning(f"Failed to store Transformer model: {e}") - - # 4. Store COB RL model + + # 4. Store COB RL model (if exists) if hasattr(self.orchestrator, 'cob_rl_agent') and self.orchestrator.cob_rl_agent: try: + # COB RL model might have different save method if hasattr(self.orchestrator.cob_rl_agent, 'save'): save_path = self.orchestrator.cob_rl_agent.save('models/saved/cob_rl_agent_session') stored_models.append(('COB RL', save_path)) logger.info(f"Stored COB RL model: {save_path}") except Exception as e: logger.warning(f"Failed to store COB RL model: {e}") - - # 5. Store Decision Fusion model + + # 5. Store Decision model if hasattr(self.orchestrator, 'decision_model') and self.orchestrator.decision_model: try: - if hasattr(self.orchestrator.decision_model, 'save'): - save_path = self.orchestrator.decision_model.save('models/saved/decision_fusion_session') - stored_models.append(('Decision Fusion', save_path)) - logger.info(f"Stored Decision Fusion model: {save_path}") + success = save_model( + model=self.orchestrator.decision_model, + model_name='decision_fusion_session', + model_type='hybrid', + metadata={'session_save': True, 'dashboard_save': True} + ) + if success: + stored_models.append(('Decision Fusion', 'models/hybrid/saved/decision_fusion_session_latest.pt')) + logger.info("Stored Decision Fusion model via unified registry") + else: + logger.warning("Failed to store Decision Fusion model via unified registry") except Exception as e: logger.warning(f"Failed to store Decision Fusion model: {e}") @@ -6706,13 +6738,39 @@ class CleanTradingDashboard: except Exception as e: logger.error(f"Error saving transformer checkpoint: {e}") - # Fallback to direct save + # Use unified registry for checkpoint try: - checkpoint_path = f"NN/models/saved/transformer_checkpoint_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pt" - transformer_trainer.save_model(checkpoint_path) - logger.info(f"TRANSFORMER: Fallback checkpoint saved: {checkpoint_path}") - except Exception as fallback_error: - logger.error(f"Fallback checkpoint save also failed: {fallback_error}") + from utils.model_registry import save_checkpoint as registry_save_checkpoint + + checkpoint_data = torch.load(checkpoint_path, map_location='cpu') if 'checkpoint_path' in locals() else checkpoint_data + + success = registry_save_checkpoint( + model=checkpoint_data, + model_name='transformer', + model_type='transformer', + performance_score=training_metrics['accuracy'], + metadata={ + 'training_samples': len(training_samples), + 'loss': training_metrics['total_loss'], + 'accuracy': training_metrics['accuracy'], + 'checkpoint_source': 'dashboard_training' + } + ) + + if success: + logger.info("TRANSFORMER: Checkpoint saved via unified registry") + else: + logger.warning("TRANSFORMER: Failed to save checkpoint via unified registry") + + except Exception as registry_error: + logger.warning(f"Unified registry save failed: {registry_error}") + # Fallback to direct save + try: + checkpoint_path = f"NN/models/saved/transformer_checkpoint_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pt" + transformer_trainer.save_model(checkpoint_path) + logger.info(f"TRANSFORMER: Fallback checkpoint saved: {checkpoint_path}") + except Exception as fallback_error: + logger.error(f"Fallback checkpoint save also failed: {fallback_error}") logger.info(f"TRANSFORMER: Trained on {len(training_samples)} samples, loss: {training_metrics['total_loss']:.4f}, accuracy: {training_metrics['accuracy']:.4f}")