metadata.py 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856
  1. """
  2. Metadata management system for ML models.
  3. This module provides comprehensive metadata management for trained models,
  4. including version tracking, training statistics, model information, and
  5. speaker/class management for different model types.
  6. """
  7. import os
  8. import json
  9. import time
  10. import hashlib
  11. import socket
  12. import platform
  13. from datetime import datetime, timezone
  14. from typing import Dict, Any, List, Optional, Union
  15. from dataclasses import dataclass, field, asdict
  16. from enum import Enum
  17. import logging
  18. import torch
  19. import torch.nn as nn
  20. import numpy as np
  21. class ModelType(Enum):
  22. """Types of supported models."""
  23. WAKEWORD = "wakeword"
  24. VOICE_RECOGNITION = "voice_recognition"
  25. SPEAKER_EMBEDDING = "speaker_embedding"
  26. AUDIO_CLASSIFICATION = "audio_classification"
  27. CUSTOM = "custom"
  28. class TrainingPhase(Enum):
  29. """Training phase indicators."""
  30. INITIAL = "initial"
  31. FINE_TUNING = "fine_tuning"
  32. TRANSFER_LEARNING = "transfer_learning"
  33. CONTINUED = "continued"
  34. @dataclass
  35. class TrainingStatistics:
  36. """Statistics from the training process."""
  37. total_epochs: int = 0
  38. final_train_loss: float = 0.0
  39. final_train_accuracy: float = 0.0
  40. best_val_loss: float = float('inf')
  41. best_val_accuracy: float = 0.0
  42. best_epoch: int = 0
  43. training_duration_seconds: float = 0.0
  44. total_parameters: int = 0
  45. trainable_parameters: int = 0
  46. convergence_epoch: Optional[int] = None
  47. early_stopping_triggered: bool = False
  48. final_learning_rate: float = 0.0
  49. max_memory_usage_mb: Optional[float] = None
  50. def to_dict(self) -> Dict[str, Any]:
  51. """Convert to dictionary."""
  52. return asdict(self)
  53. @classmethod
  54. def from_dict(cls, data: Dict[str, Any]) -> 'TrainingStatistics':
  55. """Create from dictionary."""
  56. return cls(**data)
  57. @dataclass
  58. class HardwareInfo:
  59. """Information about training hardware."""
  60. hostname: str = field(default_factory=socket.gethostname)
  61. platform_system: str = field(default_factory=platform.system)
  62. platform_release: str = field(default_factory=platform.release)
  63. platform_version: str = field(default_factory=platform.version)
  64. cpu_count: int = field(default_factory=os.cpu_count)
  65. python_version: str = field(default_factory=platform.python_version)
  66. pytorch_version: str = field(default_factory=lambda: torch.__version__)
  67. cuda_available: bool = field(default_factory=torch.cuda.is_available)
  68. cuda_version: Optional[str] = None
  69. gpu_count: int = 0
  70. gpu_names: List[str] = field(default_factory=list)
  71. total_memory_gb: Optional[float] = None
  72. def __post_init__(self):
  73. """Initialize CUDA and GPU information."""
  74. if self.cuda_available:
  75. self.cuda_version = torch.version.cuda
  76. self.gpu_count = torch.cuda.device_count()
  77. self.gpu_names = [torch.cuda.get_device_name(i) for i in range(self.gpu_count)]
  78. # Try to get memory information
  79. try:
  80. import psutil
  81. self.total_memory_gb = psutil.virtual_memory().total / (1024**3)
  82. except ImportError:
  83. pass
  84. def to_dict(self) -> Dict[str, Any]:
  85. """Convert to dictionary."""
  86. return asdict(self)
  87. @classmethod
  88. def from_dict(cls, data: Dict[str, Any]) -> 'HardwareInfo':
  89. """Create from dictionary."""
  90. return cls(**data)
  91. @dataclass
  92. class AudioConfig:
  93. """Audio processing configuration."""
  94. sample_rate: int = 16000
  95. bit_depth: int = 16
  96. channels: int = 1
  97. audio_length_seconds: float = 1.5
  98. chunk_length_samples: int = 24000 # 1.5s at 16kHz
  99. n_mels: int = 40
  100. n_fft: int = 512
  101. hop_length: int = 160
  102. win_length: int = 400
  103. fmin: float = 80.0
  104. fmax: float = 8000.0
  105. feature_type: str = "mel_spectrogram" # mel_spectrogram, mfcc, raw
  106. normalization: str = "db" # db, power, none
  107. def to_dict(self) -> Dict[str, Any]:
  108. """Convert to dictionary."""
  109. return asdict(self)
  110. @classmethod
  111. def from_dict(cls, data: Dict[str, Any]) -> 'AudioConfig':
  112. """Create from dictionary."""
  113. return cls(**data)
  114. @dataclass
  115. class DatasetInfo:
  116. """Information about the training dataset."""
  117. total_samples: int = 0
  118. train_samples: int = 0
  119. val_samples: int = 0
  120. test_samples: int = 0
  121. num_classes: int = 0
  122. class_names: List[str] = field(default_factory=list)
  123. class_distribution: Dict[str, int] = field(default_factory=dict)
  124. data_source_paths: List[str] = field(default_factory=list)
  125. augmentation_applied: bool = False
  126. augmentation_config: Dict[str, Any] = field(default_factory=dict)
  127. preprocessing_steps: List[str] = field(default_factory=list)
  128. def to_dict(self) -> Dict[str, Any]:
  129. """Convert to dictionary."""
  130. return asdict(self)
  131. @classmethod
  132. def from_dict(cls, data: Dict[str, Any]) -> 'DatasetInfo':
  133. """Create from dictionary."""
  134. return cls(**data)
  135. @dataclass
  136. class ModelArchitectureInfo:
  137. """Information about model architecture."""
  138. architecture_name: str = "unknown"
  139. architecture_type: str = "unknown" # cnn, rnn, transformer, hybrid
  140. input_shape: List[int] = field(default_factory=list)
  141. output_shape: List[int] = field(default_factory=list)
  142. total_parameters: int = 0
  143. trainable_parameters: int = 0
  144. model_size_mb: float = 0.0
  145. layer_count: int = 0
  146. activation_functions: List[str] = field(default_factory=list)
  147. optimizer_config: Dict[str, Any] = field(default_factory=dict)
  148. loss_function: str = "unknown"
  149. custom_components: List[str] = field(default_factory=list)
  150. def to_dict(self) -> Dict[str, Any]:
  151. """Convert to dictionary."""
  152. return asdict(self)
  153. @classmethod
  154. def from_dict(cls, data: Dict[str, Any]) -> 'ModelArchitectureInfo':
  155. """Create from dictionary."""
  156. return cls(**data)
  157. class ModelMetadata:
  158. """
  159. Comprehensive metadata management for ML models.
  160. This class handles all metadata aspects including training information,
  161. model architecture, dataset details, and format-specific requirements.
  162. """
  163. def __init__(self, model_type: ModelType = ModelType.CUSTOM):
  164. """
  165. Initialize model metadata.
  166. Args:
  167. model_type: Type of the model (wakeword, voice_recognition, etc.)
  168. """
  169. self.model_type = model_type
  170. self.creation_timestamp = datetime.now(timezone.utc).isoformat()
  171. self.last_modified = self.creation_timestamp
  172. self.version = "1.0.0"
  173. self.format_version = "1.0"
  174. # Core information
  175. self.model_name = "untitled_model"
  176. self.description = ""
  177. self.author = "Unknown"
  178. self.organization = ""
  179. self.license = "Private"
  180. self.tags = []
  181. # Training information
  182. self.training_phase = TrainingPhase.INITIAL
  183. self.training_statistics = TrainingStatistics()
  184. self.hardware_info = HardwareInfo()
  185. self.audio_config = AudioConfig()
  186. self.dataset_info = DatasetInfo()
  187. self.architecture_info = ModelArchitectureInfo()
  188. # File information
  189. self.file_format = "unknown"
  190. self.file_hash = ""
  191. self.file_size_bytes = 0
  192. self.compression_used = False
  193. self.encryption_used = False
  194. # Model-specific data
  195. self.model_specific = self._initialize_model_specific()
  196. # Performance metrics
  197. self.performance_metrics = {}
  198. self.benchmark_results = {}
  199. # Deployment information
  200. self.deployment_targets = []
  201. self.optimization_level = "none" # none, quantized, pruned, distilled
  202. self.inference_requirements = {}
  203. # Validation and testing
  204. self.validation_results = {}
  205. self.test_results = {}
  206. self.cross_validation_scores = []
  207. # Compatibility
  208. self.framework_version = torch.__version__
  209. self.python_version = platform.python_version()
  210. self.dependencies = {}
  211. # Custom fields (extensible)
  212. self.custom_fields = {}
  213. def _initialize_model_specific(self) -> Dict[str, Any]:
  214. """Initialize model-type specific metadata."""
  215. if self.model_type == ModelType.WAKEWORD:
  216. return {
  217. "wakewords": ["custom", "system_command"],
  218. "wakeword_classes": {
  219. 0: "custom",
  220. 1: "system_command",
  221. 2: "negative"
  222. },
  223. "detection_threshold": 0.5,
  224. "false_positive_rate": 0.0,
  225. "false_negative_rate": 0.0,
  226. "wake_word_durations": {},
  227. "background_noise_types": []
  228. }
  229. elif self.model_type == ModelType.VOICE_RECOGNITION:
  230. return {
  231. "num_speakers": 0,
  232. "speakers": {}, # speaker_id: speaker_name
  233. "embedding_dimension": 256,
  234. "verification_threshold": 0.5,
  235. "identification_threshold": 0.3,
  236. "equal_error_rate": 0.0,
  237. "speaker_enrollment_samples": {},
  238. "cross_speaker_accuracy": 0.0
  239. }
  240. elif self.model_type == ModelType.SPEAKER_EMBEDDING:
  241. return {
  242. "embedding_dimension": 256,
  243. "pooling_method": "statistical",
  244. "normalization": "l2",
  245. "cosine_similarity_threshold": 0.5,
  246. "embedding_model_type": "ecapa_tdnn"
  247. }
  248. elif self.model_type == ModelType.AUDIO_CLASSIFICATION:
  249. return {
  250. "num_classes": 0,
  251. "class_names": [],
  252. "class_weights": {},
  253. "multi_label": False,
  254. "hierarchy": {}
  255. }
  256. else:
  257. return {}
  258. def update_from_training_config(self, config: 'TrainerConfig'):
  259. """Update metadata from training configuration."""
  260. self.audio_config.sample_rate = config.sample_rate
  261. self.audio_config.audio_length_seconds = config.audio_length
  262. self.audio_config.n_mels = config.n_mels
  263. self.audio_config.n_fft = config.n_fft
  264. self.audio_config.hop_length = config.hop_length
  265. self.audio_config.win_length = config.win_length
  266. self.file_format = config.model_format.value
  267. self.encryption_used = config.use_password_protection
  268. # Store training hyperparameters
  269. self.custom_fields['training_config'] = {
  270. 'batch_size': config.batch_size,
  271. 'learning_rate': config.learning_rate,
  272. 'num_epochs': config.num_epochs,
  273. 'early_stopping_patience': config.early_stopping_patience,
  274. 'optimizer': 'AdamW',
  275. 'scheduler_type': config.scheduler_type,
  276. 'weight_decay': config.weight_decay,
  277. 'gradient_clip_norm': config.gradient_clip_norm,
  278. 'use_mixed_precision': config.use_mixed_precision
  279. }
  280. def update_from_model(self, model: nn.Module):
  281. """Update metadata from model architecture."""
  282. # Count parameters
  283. total_params = sum(p.numel() for p in model.parameters())
  284. trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
  285. self.architecture_info.total_parameters = total_params
  286. self.architecture_info.trainable_parameters = trainable_params
  287. self.training_statistics.total_parameters = total_params
  288. self.training_statistics.trainable_parameters = trainable_params
  289. # Get model size
  290. param_size = sum(p.nelement() * p.element_size() for p in model.parameters())
  291. buffer_size = sum(b.nelement() * b.element_size() for b in model.buffers())
  292. self.architecture_info.model_size_mb = (param_size + buffer_size) / (1024 * 1024)
  293. # Architecture details
  294. self.architecture_info.architecture_name = model.__class__.__name__
  295. self.architecture_info.layer_count = len(list(model.modules()))
  296. # Extract activation functions
  297. activations = []
  298. for module in model.modules():
  299. if hasattr(module, '__class__'):
  300. module_name = module.__class__.__name__
  301. if any(act in module_name.lower() for act in ['relu', 'gelu', 'tanh', 'sigmoid', 'leaky', 'silu', 'swish']):
  302. activations.append(module_name)
  303. self.architecture_info.activation_functions = list(set(activations))
  304. def update_from_dataset(self, train_loader, val_loader=None, test_loader=None):
  305. """Update metadata from dataset information."""
  306. if train_loader:
  307. self.dataset_info.train_samples = len(train_loader.dataset)
  308. # Try to get class information
  309. if hasattr(train_loader.dataset, 'classes'):
  310. self.dataset_info.class_names = train_loader.dataset.classes
  311. self.dataset_info.num_classes = len(train_loader.dataset.classes)
  312. elif hasattr(train_loader.dataset, 'targets'):
  313. targets = train_loader.dataset.targets
  314. if isinstance(targets, torch.Tensor):
  315. targets = targets.numpy()
  316. unique_classes = np.unique(targets)
  317. self.dataset_info.num_classes = len(unique_classes)
  318. # Count class distribution
  319. unique, counts = np.unique(targets, return_counts=True)
  320. self.dataset_info.class_distribution = {str(cls): int(count) for cls, count in zip(unique, counts)}
  321. if val_loader:
  322. self.dataset_info.val_samples = len(val_loader.dataset)
  323. if test_loader:
  324. self.dataset_info.test_samples = len(test_loader.dataset)
  325. self.dataset_info.total_samples = (
  326. self.dataset_info.train_samples +
  327. self.dataset_info.val_samples +
  328. self.dataset_info.test_samples
  329. )
  330. def update_from_training_results(self, metrics: 'TrainingMetrics'):
  331. """Update metadata from training results."""
  332. summary = metrics.get_summary()
  333. self.training_statistics.total_epochs = summary['current_epoch']
  334. self.training_statistics.best_epoch = summary['best_epoch']
  335. self.training_statistics.best_val_loss = summary['best_val_loss']
  336. self.training_statistics.best_val_accuracy = summary['best_val_accuracy']
  337. self.training_statistics.training_duration_seconds = summary['total_training_time']
  338. if summary['final_train_loss'] is not None:
  339. self.training_statistics.final_train_loss = summary['final_train_loss']
  340. if summary['final_val_loss'] is not None:
  341. self.training_statistics.final_val_loss = summary['final_val_loss']
  342. # Store detailed metrics
  343. self.performance_metrics['training_loss_history'] = metrics.metrics['train_loss']
  344. self.performance_metrics['validation_loss_history'] = metrics.metrics['val_loss']
  345. self.performance_metrics['training_accuracy_history'] = metrics.metrics['train_accuracy']
  346. self.performance_metrics['validation_accuracy_history'] = metrics.metrics['val_accuracy']
  347. self.performance_metrics['learning_rate_history'] = metrics.metrics['learning_rates']
  348. self.performance_metrics['epoch_times'] = metrics.metrics['epoch_times']
  349. def update_wakeword_info(self, wakeword_classes: Dict[int, str], thresholds: Dict[str, float] = None):
  350. """Update wakeword-specific metadata."""
  351. if self.model_type != ModelType.WAKEWORD:
  352. return
  353. self.model_specific["wakeword_classes"] = wakeword_classes
  354. self.model_specific["wakewords"] = list(wakeword_classes.values())
  355. if thresholds:
  356. self.model_specific.update(thresholds)
  357. def update_speaker_info(self, speakers: Dict[str, str], embedding_dim: int = None):
  358. """Update voice recognition specific metadata."""
  359. if self.model_type != ModelType.VOICE_RECOGNITION:
  360. return
  361. # Convert speaker dict to proper format (speaker_id: speaker_name)
  362. if isinstance(list(speakers.keys())[0], str) and not list(speakers.keys())[0].isdigit():
  363. # speakers = {name: id} -> convert to {id: name}
  364. speaker_dict = {f"speaker{str(idx).zfill(3)}": name for idx, name in enumerate(speakers.keys())}
  365. else:
  366. speaker_dict = speakers
  367. self.model_specific["speakers"] = speaker_dict
  368. self.model_specific["num_speakers"] = len(speaker_dict)
  369. if embedding_dim:
  370. self.model_specific["embedding_dimension"] = embedding_dim
  371. def add_test_results(self, test_results: Dict[str, Any]):
  372. """Add test results to metadata."""
  373. self.test_results.update(test_results)
  374. self.last_modified = datetime.now(timezone.utc).isoformat()
  375. def add_benchmark_results(self, benchmark_name: str, results: Dict[str, Any]):
  376. """Add benchmark results."""
  377. self.benchmark_results[benchmark_name] = {
  378. 'results': results,
  379. 'timestamp': datetime.now(timezone.utc).isoformat()
  380. }
  381. self.last_modified = datetime.now(timezone.utc).isoformat()
  382. def add_deployment_target(self, target: str, requirements: Dict[str, Any] = None):
  383. """Add deployment target information."""
  384. if target not in self.deployment_targets:
  385. self.deployment_targets.append(target)
  386. if requirements:
  387. self.inference_requirements[target] = requirements
  388. self.last_modified = datetime.now(timezone.utc).isoformat()
  389. def set_file_info(self, filepath: str):
  390. """Update file-related information."""
  391. if os.path.exists(filepath):
  392. self.file_size_bytes = os.path.getsize(filepath)
  393. # Generate file hash
  394. hash_sha256 = hashlib.sha256()
  395. with open(filepath, "rb") as f:
  396. for chunk in iter(lambda: f.read(4096), b""):
  397. hash_sha256.update(chunk)
  398. self.file_hash = hash_sha256.hexdigest()
  399. self.last_modified = datetime.now(timezone.utc).isoformat()
  400. def increment_version(self, version_type: str = "patch"):
  401. """Increment version number."""
  402. major, minor, patch = map(int, self.version.split('.'))
  403. if version_type == "major":
  404. major += 1
  405. minor = 0
  406. patch = 0
  407. elif version_type == "minor":
  408. minor += 1
  409. patch = 0
  410. else: # patch
  411. patch += 1
  412. self.version = f"{major}.{minor}.{patch}"
  413. self.last_modified = datetime.now(timezone.utc).isoformat()
  414. def to_dict(self) -> Dict[str, Any]:
  415. """Convert metadata to dictionary format."""
  416. return {
  417. # Core information
  418. 'model_type': self.model_type.value,
  419. 'creation_timestamp': self.creation_timestamp,
  420. 'last_modified': self.last_modified,
  421. 'version': self.version,
  422. 'format_version': self.format_version,
  423. 'model_name': self.model_name,
  424. 'description': self.description,
  425. 'author': self.author,
  426. 'organization': self.organization,
  427. 'license': self.license,
  428. 'tags': self.tags,
  429. # Training information
  430. 'training_phase': self.training_phase.value,
  431. 'training_statistics': self.training_statistics.to_dict(),
  432. 'hardware_info': self.hardware_info.to_dict(),
  433. 'audio_config': self.audio_config.to_dict(),
  434. 'dataset_info': self.dataset_info.to_dict(),
  435. 'architecture_info': self.architecture_info.to_dict(),
  436. # File information
  437. 'file_format': self.file_format,
  438. 'file_hash': self.file_hash,
  439. 'file_size_bytes': self.file_size_bytes,
  440. 'compression_used': self.compression_used,
  441. 'encryption_used': self.encryption_used,
  442. # Model-specific data
  443. 'model_specific': self.model_specific,
  444. # Performance and testing
  445. 'performance_metrics': self.performance_metrics,
  446. 'benchmark_results': self.benchmark_results,
  447. 'validation_results': self.validation_results,
  448. 'test_results': self.test_results,
  449. 'cross_validation_scores': self.cross_validation_scores,
  450. # Deployment
  451. 'deployment_targets': self.deployment_targets,
  452. 'optimization_level': self.optimization_level,
  453. 'inference_requirements': self.inference_requirements,
  454. # Compatibility
  455. 'framework_version': self.framework_version,
  456. 'python_version': self.python_version,
  457. 'dependencies': self.dependencies,
  458. # Custom
  459. 'custom_fields': self.custom_fields
  460. }
  461. @classmethod
  462. def from_dict(cls, data: Dict[str, Any]) -> 'ModelMetadata':
  463. """Create metadata from dictionary."""
  464. metadata = cls(ModelType(data.get('model_type', 'custom')))
  465. # Core information
  466. metadata.creation_timestamp = data.get('creation_timestamp', metadata.creation_timestamp)
  467. metadata.last_modified = data.get('last_modified', metadata.last_modified)
  468. metadata.version = data.get('version', metadata.version)
  469. metadata.format_version = data.get('format_version', metadata.format_version)
  470. metadata.model_name = data.get('model_name', metadata.model_name)
  471. metadata.description = data.get('description', metadata.description)
  472. metadata.author = data.get('author', metadata.author)
  473. metadata.organization = data.get('organization', metadata.organization)
  474. metadata.license = data.get('license', metadata.license)
  475. metadata.tags = data.get('tags', metadata.tags)
  476. # Training information
  477. if 'training_phase' in data:
  478. metadata.training_phase = TrainingPhase(data['training_phase'])
  479. if 'training_statistics' in data:
  480. metadata.training_statistics = TrainingStatistics.from_dict(data['training_statistics'])
  481. if 'hardware_info' in data:
  482. metadata.hardware_info = HardwareInfo.from_dict(data['hardware_info'])
  483. if 'audio_config' in data:
  484. metadata.audio_config = AudioConfig.from_dict(data['audio_config'])
  485. if 'dataset_info' in data:
  486. metadata.dataset_info = DatasetInfo.from_dict(data['dataset_info'])
  487. if 'architecture_info' in data:
  488. metadata.architecture_info = ModelArchitectureInfo.from_dict(data['architecture_info'])
  489. # File information
  490. metadata.file_format = data.get('file_format', metadata.file_format)
  491. metadata.file_hash = data.get('file_hash', metadata.file_hash)
  492. metadata.file_size_bytes = data.get('file_size_bytes', metadata.file_size_bytes)
  493. metadata.compression_used = data.get('compression_used', metadata.compression_used)
  494. metadata.encryption_used = data.get('encryption_used', metadata.encryption_used)
  495. # Model-specific data
  496. metadata.model_specific = data.get('model_specific', metadata.model_specific)
  497. # Performance and testing
  498. metadata.performance_metrics = data.get('performance_metrics', metadata.performance_metrics)
  499. metadata.benchmark_results = data.get('benchmark_results', metadata.benchmark_results)
  500. metadata.validation_results = data.get('validation_results', metadata.validation_results)
  501. metadata.test_results = data.get('test_results', metadata.test_results)
  502. metadata.cross_validation_scores = data.get('cross_validation_scores', metadata.cross_validation_scores)
  503. # Deployment
  504. metadata.deployment_targets = data.get('deployment_targets', metadata.deployment_targets)
  505. metadata.optimization_level = data.get('optimization_level', metadata.optimization_level)
  506. metadata.inference_requirements = data.get('inference_requirements', metadata.inference_requirements)
  507. # Compatibility
  508. metadata.framework_version = data.get('framework_version', metadata.framework_version)
  509. metadata.python_version = data.get('python_version', metadata.python_version)
  510. metadata.dependencies = data.get('dependencies', metadata.dependencies)
  511. # Custom
  512. metadata.custom_fields = data.get('custom_fields', metadata.custom_fields)
  513. return metadata
  514. def save_to_file(self, filepath: str):
  515. """Save metadata to JSON file."""
  516. os.makedirs(os.path.dirname(filepath), exist_ok=True)
  517. with open(filepath, 'w', encoding='utf-8') as f:
  518. json.dump(self.to_dict(), f, indent=2, ensure_ascii=False)
  519. @classmethod
  520. def load_from_file(cls, filepath: str) -> 'ModelMetadata':
  521. """Load metadata from JSON file."""
  522. with open(filepath, 'r', encoding='utf-8') as f:
  523. data = json.load(f)
  524. return cls.from_dict(data)
  525. def get_summary(self) -> str:
  526. """Get a human-readable summary of the metadata."""
  527. summary = f"Model: {self.model_name}\n"
  528. summary += f"Type: {self.model_type.value}\n"
  529. summary += f"Version: {self.version}\n"
  530. summary += f"Author: {self.author}\n"
  531. summary += f"Created: {self.creation_timestamp}\n"
  532. summary += f"Description: {self.description}\n\n"
  533. summary += "Architecture:\n"
  534. summary += f" Name: {self.architecture_info.architecture_name}\n"
  535. summary += f" Parameters: {self.architecture_info.total_parameters:,}\n"
  536. summary += f" Trainable: {self.architecture_info.trainable_parameters:,}\n"
  537. summary += f" Size: {self.architecture_info.model_size_mb:.2f} MB\n\n"
  538. summary += "Training:\n"
  539. summary += f" Epochs: {self.training_statistics.total_epochs}\n"
  540. summary += f" Best Epoch: {self.training_statistics.best_epoch}\n"
  541. summary += f" Best Val Accuracy: {self.training_statistics.best_val_accuracy:.4f}\n"
  542. summary += f" Duration: {self.training_statistics.training_duration_seconds:.2f}s\n\n"
  543. summary += "Dataset:\n"
  544. summary += f" Total Samples: {self.dataset_info.total_samples:,}\n"
  545. summary += f" Classes: {self.dataset_info.num_classes}\n"
  546. summary += f" Train/Val/Test: {self.dataset_info.train_samples}/{self.dataset_info.val_samples}/{self.dataset_info.test_samples}\n\n"
  547. if self.model_type == ModelType.VOICE_RECOGNITION:
  548. summary += "Speakers:\n"
  549. for speaker_id, speaker_name in self.model_specific.get('speakers', {}).items():
  550. summary += f" {speaker_id}: {speaker_name}\n"
  551. elif self.model_type == ModelType.WAKEWORD:
  552. summary += "Wakewords:\n"
  553. for wakeword in self.model_specific.get('wakewords', []):
  554. summary += f" - {wakeword}\n"
  555. return summary
  556. class MetadataManager:
  557. """Manager class for handling model metadata operations."""
  558. def __init__(self, logger: Optional[logging.Logger] = None):
  559. self.logger = logger or logging.getLogger(__name__)
  560. def create_metadata(self, model_type: ModelType, model_name: str,
  561. author: str = "Unknown") -> ModelMetadata:
  562. """Create new metadata instance."""
  563. metadata = ModelMetadata(model_type)
  564. metadata.model_name = model_name
  565. metadata.author = author
  566. self.logger.info(f"Created metadata for {model_type.value} model: {model_name}")
  567. return metadata
  568. def merge_metadata(self, base_metadata: ModelMetadata,
  569. update_metadata: ModelMetadata) -> ModelMetadata:
  570. """Merge two metadata instances."""
  571. # Create a copy of base metadata
  572. merged = ModelMetadata.from_dict(base_metadata.to_dict())
  573. # Update with new information
  574. update_dict = update_metadata.to_dict()
  575. # Merge training statistics (keep best results)
  576. if update_metadata.training_statistics.best_val_accuracy > merged.training_statistics.best_val_accuracy:
  577. merged.training_statistics = update_metadata.training_statistics
  578. # Always update architecture and dataset info
  579. merged.architecture_info = update_metadata.architecture_info
  580. merged.dataset_info = update_metadata.dataset_info
  581. # Merge performance metrics
  582. merged.performance_metrics.update(update_metadata.performance_metrics)
  583. merged.benchmark_results.update(update_metadata.benchmark_results)
  584. # Update modification time
  585. merged.last_modified = datetime.now(timezone.utc).isoformat()
  586. merged.increment_version("patch")
  587. return merged
  588. def validate_metadata(self, metadata: ModelMetadata) -> List[str]:
  589. """Validate metadata and return list of issues."""
  590. issues = []
  591. if not metadata.model_name or metadata.model_name == "untitled_model":
  592. issues.append("Model name not set")
  593. if not metadata.description:
  594. issues.append("Model description is empty")
  595. if metadata.training_statistics.total_epochs == 0:
  596. issues.append("No training statistics available")
  597. if metadata.dataset_info.total_samples == 0:
  598. issues.append("No dataset information available")
  599. if metadata.architecture_info.total_parameters == 0:
  600. issues.append("Model architecture information missing")
  601. # Model-specific validations
  602. if metadata.model_type == ModelType.VOICE_RECOGNITION:
  603. if metadata.model_specific.get('num_speakers', 0) == 0:
  604. issues.append("No speakers defined for voice recognition model")
  605. elif metadata.model_type == ModelType.WAKEWORD:
  606. if not metadata.model_specific.get('wakewords'):
  607. issues.append("No wakewords defined for wakeword model")
  608. return issues
  609. def export_metadata(self, metadata: ModelMetadata, format: str = "json") -> str:
  610. """Export metadata to various formats."""
  611. if format.lower() == "json":
  612. return json.dumps(metadata.to_dict(), indent=2, ensure_ascii=False)
  613. elif format.lower() == "yaml":
  614. try:
  615. import yaml
  616. return yaml.dump(metadata.to_dict(), default_flow_style=False, allow_unicode=True)
  617. except ImportError:
  618. raise ImportError("PyYAML not available for YAML export")
  619. elif format.lower() == "summary":
  620. return metadata.get_summary()
  621. else:
  622. raise ValueError(f"Unsupported export format: {format}")
  623. def create_training_report(self, metadata: ModelMetadata) -> str:
  624. """Create a comprehensive training report."""
  625. report = f"# Training Report: {metadata.model_name}\n\n"
  626. # Basic Information
  627. report += "## Basic Information\n"
  628. report += f"- **Model Type**: {metadata.model_type.value}\n"
  629. report += f"- **Version**: {metadata.version}\n"
  630. report += f"- **Author**: {metadata.author}\n"
  631. report += f"- **Created**: {metadata.creation_timestamp}\n"
  632. report += f"- **Last Modified**: {metadata.last_modified}\n"
  633. report += f"- **Description**: {metadata.description}\n\n"
  634. # Architecture
  635. report += "## Model Architecture\n"
  636. arch = metadata.architecture_info
  637. report += f"- **Architecture**: {arch.architecture_name}\n"
  638. report += f"- **Total Parameters**: {arch.total_parameters:,}\n"
  639. report += f"- **Trainable Parameters**: {arch.trainable_parameters:,}\n"
  640. report += f"- **Model Size**: {arch.model_size_mb:.2f} MB\n"
  641. report += f"- **Layers**: {arch.layer_count}\n"
  642. if arch.activation_functions:
  643. report += f"- **Activations**: {', '.join(arch.activation_functions)}\n"
  644. report += "\n"
  645. # Training Results
  646. report += "## Training Results\n"
  647. stats = metadata.training_statistics
  648. report += f"- **Total Epochs**: {stats.total_epochs}\n"
  649. report += f"- **Best Epoch**: {stats.best_epoch}\n"
  650. report += f"- **Final Train Loss**: {stats.final_train_loss:.6f}\n"
  651. report += f"- **Final Train Accuracy**: {stats.final_train_accuracy:.4f}\n"
  652. report += f"- **Best Validation Loss**: {stats.best_val_loss:.6f}\n"
  653. report += f"- **Best Validation Accuracy**: {stats.best_val_accuracy:.4f}\n"
  654. report += f"- **Training Duration**: {stats.training_duration_seconds:.2f} seconds\n"
  655. if stats.early_stopping_triggered:
  656. report += "- **Early Stopping**: Triggered\n"
  657. report += "\n"
  658. # Dataset Information
  659. report += "## Dataset Information\n"
  660. dataset = metadata.dataset_info
  661. report += f"- **Total Samples**: {dataset.total_samples:,}\n"
  662. report += f"- **Training Samples**: {dataset.train_samples:,}\n"
  663. report += f"- **Validation Samples**: {dataset.val_samples:,}\n"
  664. report += f"- **Test Samples**: {dataset.test_samples:,}\n"
  665. report += f"- **Number of Classes**: {dataset.num_classes}\n"
  666. if dataset.class_distribution:
  667. report += "- **Class Distribution**:\n"
  668. for class_name, count in dataset.class_distribution.items():
  669. report += f" - {class_name}: {count:,} samples\n"
  670. report += "\n"
  671. # Hardware Information
  672. report += "## Hardware Information\n"
  673. hw = metadata.hardware_info
  674. report += f"- **Hostname**: {hw.hostname}\n"
  675. report += f"- **Platform**: {hw.platform_system} {hw.platform_release}\n"
  676. report += f"- **CPU Cores**: {hw.cpu_count}\n"
  677. report += f"- **CUDA Available**: {hw.cuda_available}\n"
  678. if hw.cuda_available:
  679. report += f"- **CUDA Version**: {hw.cuda_version}\n"
  680. report += f"- **GPU Count**: {hw.gpu_count}\n"
  681. if hw.gpu_names:
  682. report += f"- **GPUs**: {', '.join(hw.gpu_names)}\n"
  683. if hw.total_memory_gb:
  684. report += f"- **Total Memory**: {hw.total_memory_gb:.1f} GB\n"
  685. report += "\n"
  686. # Model-specific Information
  687. if metadata.model_type == ModelType.VOICE_RECOGNITION:
  688. report += "## Speaker Information\n"
  689. speakers = metadata.model_specific.get('speakers', {})
  690. report += f"- **Number of Speakers**: {len(speakers)}\n"
  691. if speakers:
  692. report += "- **Speakers**:\n"
  693. for speaker_id, speaker_name in speakers.items():
  694. report += f" - {speaker_id}: {speaker_name}\n"
  695. embedding_dim = metadata.model_specific.get('embedding_dimension', 'Unknown')
  696. report += f"- **Embedding Dimension**: {embedding_dim}\n"
  697. report += "\n"
  698. elif metadata.model_type == ModelType.WAKEWORD:
  699. report += "## Wakeword Information\n"
  700. wakewords = metadata.model_specific.get('wakewords', [])
  701. report += f"- **Wakewords**: {', '.join(wakewords)}\n"
  702. classes = metadata.model_specific.get('wakeword_classes', {})
  703. if classes:
  704. report += "- **Classes**:\n"
  705. for class_id, class_name in classes.items():
  706. report += f" - {class_id}: {class_name}\n"
  707. threshold = metadata.model_specific.get('detection_threshold', 'Not set')
  708. report += f"- **Detection Threshold**: {threshold}\n"
  709. report += "\n"
  710. # Test Results
  711. if metadata.test_results:
  712. report += "## Test Results\n"
  713. for metric, value in metadata.test_results.items():
  714. if isinstance(value, float):
  715. report += f"- **{metric}**: {value:.6f}\n"
  716. else:
  717. report += f"- **{metric}**: {value}\n"
  718. report += "\n"
  719. return report