VBAF.ML.MLOps.ps1
|
#Requires -Version 5.1 <# .SYNOPSIS MLOps - Experiment Tracking, Drift Detection, Retraining, CI/CD .DESCRIPTION Implements MLOps workflows from scratch. Designed as a TEACHING resource - every step explained. Features included: - Experiment tracking : log runs, params, metrics (MLflow-like) - Model monitoring : track performance over time - Data drift detection : detect when input data changes - Retraining triggers : auto-flag when model needs retraining - CI/CD examples : pipeline scripts for automation Standalone - works with all VBAF ML modules. .NOTES Part of VBAF - Phase 7 Production Features - v2.1.0 PS 5.1 compatible Teaching project - MLOps concepts explained step by step! #> # ============================================================ # TEACHING NOTE: What is MLOps? # MLOps = ML + DevOps. It applies software engineering practices # to the ML lifecycle: # # DEVELOP : experiment tracking (what did I try?) # DEPLOY : model registry + serving (already built!) # MONITOR : is the model still working well in production? # RETRAIN : trigger retraining when performance degrades # AUTOMATE : CI/CD pipelines for the whole cycle # # Without MLOps: # "Which hyperparams gave us R2=0.998?" -> nobody knows! # "Why did accuracy drop last Tuesday?" -> nobody knows! # # With MLOps: everything is tracked, versioned, auditable. # ============================================================ $script:ExperimentStorePath = Join-Path $env:USERPROFILE "VBAFRegistry\experiments" $script:ActiveExperiment = $null $script:ActiveRun = $null # ============================================================ # EXPERIMENT TRACKING (MLflow-like) # ============================================================ # TEACHING NOTE: MLflow is the industry standard for experiment # tracking. Our version has the same concepts: # # EXPERIMENT : a project (e.g. "HousePricePrediction") # RUN : one training attempt within the experiment # PARAMS : hyperparameters used (lr=0.01, epochs=100) # METRICS : results achieved (R2=0.998, RMSE=2.1) # ARTIFACTS : files produced (model.json, plot.png) # # You can compare runs to find what worked best! # ============================================================ function New-VBAFExperiment { param([string]$Name, [string]$Description = "") if (-not (Test-Path $script:ExperimentStorePath)) { New-Item -ItemType Directory -Path $script:ExperimentStorePath -Force | Out-Null } $expPath = Join-Path $script:ExperimentStorePath $Name if (-not (Test-Path $expPath)) { New-Item -ItemType Directory -Path $expPath -Force | Out-Null } $indexPath = Join-Path $expPath "experiment.json" if (-not (Test-Path $indexPath)) { @{ Name = $Name Description = $Description Created = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") Runs = @{} } | ConvertTo-Json -Depth 5 | Set-Content $indexPath -Encoding UTF8 } $script:ActiveExperiment = $Name Write-Host ("🧪 Experiment: {0}" -f $Name) -ForegroundColor Green if ($Description -ne "") { Write-Host (" {0}" -f $Description) -ForegroundColor DarkGray } return $Name } function Start-VBAFRun { param( [string] $RunName = "", [string] $ModelType = "", [hashtable] $Params = @{} ) if ($null -eq $script:ActiveExperiment) { Write-Host "❌ No active experiment. Call New-VBAFExperiment first." -ForegroundColor Red return $null } $runId = (Get-Date).ToString("yyyyMMdd_HHmmss") $name = if ($RunName -ne "") { $RunName } else { "run_$runId" } $script:ActiveRun = @{ RunId = $runId RunName = $name ModelType = $ModelType Started = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") Ended = $null Params = $Params Metrics = @{} Tags = @{} Artifacts = @() Status = "running" } Write-Host ("▶️ Run started: {0}" -f $name) -ForegroundColor Cyan if ($Params.Count -gt 0) { Write-Host " Params: " -NoNewline -ForegroundColor DarkGray foreach ($k in $Params.Keys) { Write-Host ("{0}={1} " -f $k, $Params[$k]) -NoNewline -ForegroundColor White } Write-Host "" } return $runId } function Set-VBAFRunParam { param([string]$Key, [object]$Value) if ($null -eq $script:ActiveRun) { Write-Host "❌ No active run" -ForegroundColor Red; return } $script:ActiveRun.Params[$Key] = $Value } function Set-VBAFRunMetric { param([string]$Key, [double]$Value, [int]$Step = -1) if ($null -eq $script:ActiveRun) { Write-Host "❌ No active run" -ForegroundColor Red; return } if (-not $script:ActiveRun.Metrics.ContainsKey($Key)) { $script:ActiveRun.Metrics[$Key] = @() } $script:ActiveRun.Metrics[$Key] += @{ Value=$Value; Step=$Step; Time=(Get-Date).ToString("HH:mm:ss") } } function Set-VBAFRunTag { param([string]$Key, [string]$Value) if ($null -eq $script:ActiveRun) { Write-Host "❌ No active run" -ForegroundColor Red; return } $script:ActiveRun.Tags[$Key] = $Value } function Add-VBAFRunArtifact { param([string]$Path, [string]$Description = "") if ($null -eq $script:ActiveRun) { Write-Host "❌ No active run" -ForegroundColor Red; return } $script:ActiveRun.Artifacts += @{ Path=$Path; Description=$Description } } function Stop-VBAFRun { param([string]$Status = "completed") # completed, failed, cancelled if ($null -eq $script:ActiveRun) { Write-Host "❌ No active run" -ForegroundColor Red; return } $script:ActiveRun.Ended = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") $script:ActiveRun.Status = $Status # Save run to experiment store $expPath = Join-Path $script:ExperimentStorePath $script:ActiveExperiment $runPath = Join-Path $expPath "$($script:ActiveRun.RunId).json" $script:ActiveRun | ConvertTo-Json -Depth 10 | Set-Content $runPath -Encoding UTF8 # Update experiment index $indexPath = Join-Path $expPath "experiment.json" $index = Get-Content $indexPath -Raw | ConvertFrom-Json $runSummary = [PSCustomObject]@{ RunName = $script:ActiveRun.RunName ModelType = $script:ActiveRun.ModelType Started = $script:ActiveRun.Started Status = $Status Path = $runPath } $index.Runs | Add-Member -NotePropertyName $script:ActiveRun.RunId -NotePropertyValue $runSummary -Force $index | ConvertTo-Json -Depth 10 | Set-Content $indexPath -Encoding UTF8 $color = switch ($Status) { "completed" {"Green"} "failed" {"Red"} default {"Yellow"} } Write-Host ("⏹️ Run {0}: {1}" -f $Status, $script:ActiveRun.RunName) -ForegroundColor $color # Print final metrics if ($script:ActiveRun.Metrics.Count -gt 0) { Write-Host " Final metrics: " -NoNewline -ForegroundColor DarkGray foreach ($k in $script:ActiveRun.Metrics.Keys) { $vals = $script:ActiveRun.Metrics[$k] $last = $vals[-1].Value Write-Host ("{0}={1} " -f $k, [Math]::Round($last, 4)) -NoNewline -ForegroundColor White } Write-Host "" } $script:ActiveRun = $null } function Get-VBAFExperimentRuns { param([string]$ExperimentName = "", [int]$TopN = 10) $expName = if ($ExperimentName -ne "") { $ExperimentName } else { $script:ActiveExperiment } if ($null -eq $expName) { Write-Host "❌ No experiment specified" -ForegroundColor Red; return } $indexPath = Join-Path $script:ExperimentStorePath "$expName\experiment.json" if (-not (Test-Path $indexPath)) { Write-Host "❌ Experiment not found: $expName" -ForegroundColor Red; return } $index = Get-Content $indexPath -Raw | ConvertFrom-Json $runs = $index.Runs.PSObject.Properties Write-Host "" Write-Host ("📋 Experiment: {0}" -f $expName) -ForegroundColor Green Write-Host (" {0,-20} {1,-15} {2,-10} {3}" -f "Run Name","Model","Status","Started") -ForegroundColor Yellow Write-Host (" {0}" -f ("-" * 65)) -ForegroundColor DarkGray $count = 0 foreach ($run in ($runs | Sort-Object Name -Descending)) { if ($count -ge $TopN) { break } $r = $run.Value $color = switch ($r.Status) { "completed" {"White"} "failed" {"Red"} default {"Yellow"} } Write-Host (" {0,-20} {1,-15} {2,-10} {3}" -f $r.RunName, $r.ModelType, $r.Status, $r.Started) -ForegroundColor $color $count++ } Write-Host "" } function Compare-VBAFRuns { param([string]$ExperimentName = "", [string]$MetricKey = "") $expName = if ($ExperimentName -ne "") { $ExperimentName } else { $script:ActiveExperiment } $expPath = Join-Path $script:ExperimentStorePath $expName $runFiles = Get-ChildItem $expPath -Filter "*.json" | Where-Object { $_.Name -ne "experiment.json" } if ($runFiles.Count -eq 0) { Write-Host "No runs found" -ForegroundColor Yellow; return } Write-Host "" Write-Host ("🏆 Run Comparison: {0}" -f $expName) -ForegroundColor Green $rows = @() foreach ($f in $runFiles) { $run = Get-Content $f.FullName -Raw | ConvertFrom-Json if ($run.Status -ne "completed") { continue } $metricStr = "" if ($null -ne $run.Metrics) { $run.Metrics.PSObject.Properties | ForEach-Object { $vals = $_.Value if ($vals -is [array] -and $vals.Count -gt 0) { $last = $vals[-1].Value $metricStr += "{0}={1} " -f $_.Name, [Math]::Round($last,4) } } } $paramStr = "" if ($null -ne $run.Params) { $run.Params.PSObject.Properties | ForEach-Object { $paramStr += "{0}={1} " -f $_.Name, $_.Value } } $rows += @{ Name=$run.RunName; Model=$run.ModelType; Metrics=$metricStr.Trim(); Params=$paramStr.Trim() } } Write-Host (" {0,-20} {1,-20} {2,-30} {3}" -f "Run","Model","Metrics","Params") -ForegroundColor Yellow Write-Host (" {0}" -f ("-" * 85)) -ForegroundColor DarkGray foreach ($row in $rows) { Write-Host (" {0,-20} {1,-20} {2,-30} {3}" -f $row.Name, $row.Model, $row.Metrics, $row.Params) -ForegroundColor White } Write-Host "" } # ============================================================ # DATA DRIFT DETECTION # ============================================================ # TEACHING NOTE: Data drift = input data in production is # different from training data! # # Example: You trained on house prices from 2020. # In 2026, houses are larger on average. The model gets # inputs it never saw during training -> predictions degrade! # # Detection methods: # Mean drift : is the average feature value shifting? # Std drift : is the variance changing? # KS statistic : statistical test for distribution change # PSI : Population Stability Index (industry standard) # # PSI interpretation: # PSI < 0.1 : no significant change (green) # PSI < 0.2 : moderate change (yellow) - monitor closely # PSI >= 0.2 : significant drift (red) - consider retraining! # ============================================================ function Get-VBAFDriftReport { param( [double[][]] $ReferenceData, # training data distribution [double[][]] $ProductionData, # recent production inputs [string[]] $FeatureNames = @(), [double] $WarnThreshold = 0.1, [double] $AlertThreshold = 0.2 ) $nFeatures = $ReferenceData[0].Length if ($FeatureNames.Length -eq 0) { $FeatureNames = 0..($nFeatures-1) | ForEach-Object { "feature$_" } } Write-Host "" Write-Host "🌊 Data Drift Report" -ForegroundColor Green Write-Host (" Reference samples : {0}" -f $ReferenceData.Length) -ForegroundColor Cyan Write-Host (" Production samples: {0}" -f $ProductionData.Length) -ForegroundColor Cyan Write-Host "" Write-Host (" {0,-15} {1,8} {2,8} {3,8} {4,8} {5,8} {6}" -f "Feature","Ref Mean","Prod Mean","Ref Std","Prod Std","PSI","Status") -ForegroundColor Yellow Write-Host (" {0}" -f ("-" * 75)) -ForegroundColor DarkGray $driftResults = @() for ($f = 0; $f -lt $nFeatures; $f++) { $refVals = $ReferenceData | ForEach-Object { [double]$_[$f] } $prodVals = $ProductionData | ForEach-Object { [double]$_[$f] } $refMean = ($refVals | Measure-Object -Average).Average $prodMean = ($prodVals | Measure-Object -Average).Average $refStd = [Math]::Sqrt((($refVals | ForEach-Object { ($_ - $refMean) * ($_ - $refMean) } | Measure-Object -Sum).Sum / $refVals.Count)) $prodStd = [Math]::Sqrt((($prodVals | ForEach-Object { ($_ - $prodMean) * ($_ - $prodMean) } | Measure-Object -Sum).Sum / $prodVals.Count)) # PSI: Population Stability Index $psi = 0.0 $nBins = 10 $minVal = [Math]::Min(($refVals | Measure-Object -Minimum).Minimum, ($prodVals | Measure-Object -Minimum).Minimum) $maxVal = [Math]::Max(($refVals | Measure-Object -Maximum).Maximum, ($prodVals | Measure-Object -Maximum).Maximum) $binW = ($maxVal - $minVal) / $nBins if ($binW -gt 0) { for ($b = 0; $b -lt $nBins; $b++) { $lo = $minVal + $b * $binW $hi = $lo + $binW $refP = [Math]::Max(0.0001, (@($refVals | Where-Object { $_ -ge $lo -and $_ -lt $hi }).Count / $refVals.Count)) $proP = [Math]::Max(0.0001, (@($prodVals | Where-Object { $_ -ge $lo -and $_ -lt $hi }).Count / $prodVals.Count)) $psi += ($proP - $refP) * [Math]::Log($proP / $refP) } } $status = if ($psi -ge $AlertThreshold) { "🔴 DRIFT" } elseif ($psi -ge $WarnThreshold) { "🟡 WARN" } else { "🟢 OK" } $color = if ($psi -ge $AlertThreshold) { "Red" } elseif ($psi -ge $WarnThreshold) { "Yellow" } else { "Green" } Write-Host (" {0,-15} {1,8:F2} {2,8:F2} {3,8:F2} {4,8:F2} {5,8:F4} {6}" -f ` $FeatureNames[$f], $refMean, $prodMean, $refStd, $prodStd, $psi, $status) -ForegroundColor $color $driftResults += @{ Feature=$FeatureNames[$f]; PSI=$psi; Status=$status; RefMean=$refMean; ProdMean=$prodMean } } $maxPSI = ($driftResults | ForEach-Object { $_.PSI } | Measure-Object -Maximum).Maximum $drifted = @($driftResults | Where-Object { $_.PSI -ge $AlertThreshold }).Count Write-Host "" Write-Host (" Max PSI: {0:F4} Drifted features: {1}/{2}" -f $maxPSI, $drifted, $nFeatures) -ForegroundColor White if ($drifted -gt 0) { Write-Host " ⚠️ Retraining recommended!" -ForegroundColor Red } else { Write-Host " ✅ No significant drift detected" -ForegroundColor Green } Write-Host "" return $driftResults } # ============================================================ # AUTOMATED RETRAINING TRIGGERS # ============================================================ # TEACHING NOTE: When should you retrain a model? # 1. Performance degradation: accuracy drops below threshold # 2. Data drift: input distribution shifts significantly # 3. Schedule: retrain every N days regardless # 4. Volume: retrain after N new labeled samples available # # VBAF implements all four trigger types! # ============================================================ function New-VBAFRetrainingPolicy { param( [string] $ModelName, [double] $MinAccuracy = 0.90, # retrain if accuracy drops below this [double] $MaxDriftPSI = 0.20, # retrain if PSI exceeds this [int] $MaxAgeDays = 30, # retrain if model is older than this [int] $MinNewSamples = 100 # retrain when this many new samples available ) $policy = @{ ModelName = $ModelName MinAccuracy = $MinAccuracy MaxDriftPSI = $MaxDriftPSI MaxAgeDays = $MaxAgeDays MinNewSamples = $MinNewSamples Created = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") } Write-Host ("📋 Retraining policy created for: {0}" -f $ModelName) -ForegroundColor Green Write-Host (" Accuracy threshold : < {0}" -f $MinAccuracy) -ForegroundColor White Write-Host (" Max drift PSI : > {0}" -f $MaxDriftPSI) -ForegroundColor White Write-Host (" Max model age : {0} days" -f $MaxAgeDays) -ForegroundColor White Write-Host (" New samples needed : {0}" -f $MinNewSamples) -ForegroundColor White return $policy } function Test-VBAFRetrainingNeeded { param( [hashtable] $Policy, [double] $CurrentAccuracy = 1.0, [double] $CurrentMaxPSI = 0.0, [datetime] $ModelTrainedDate = [datetime]::Now, [int] $NewSamplesCount = 0 ) $triggers = @() $needed = $false if ($CurrentAccuracy -lt $Policy.MinAccuracy) { $triggers += "Accuracy {0:F4} below threshold {1}" -f $CurrentAccuracy, $Policy.MinAccuracy $needed = $true } if ($CurrentMaxPSI -gt $Policy.MaxDriftPSI) { $triggers += "Drift PSI {0:F4} exceeds threshold {1}" -f $CurrentMaxPSI, $Policy.MaxDriftPSI $needed = $true } $ageDays = ([datetime]::Now - $ModelTrainedDate).TotalDays if ($ageDays -gt $Policy.MaxAgeDays) { $triggers += "Model age {0:F0} days exceeds {1} day limit" -f $ageDays, $Policy.MaxAgeDays $needed = $true } if ($NewSamplesCount -ge $Policy.MinNewSamples) { $triggers += "{0} new samples available (threshold: {1})" -f $NewSamplesCount, $Policy.MinNewSamples $needed = $true } Write-Host "" Write-Host ("🔄 Retraining Check: {0}" -f $Policy.ModelName) -ForegroundColor Green Write-Host (" Accuracy : {0:F4} (min: {1})" -f $CurrentAccuracy, $Policy.MinAccuracy) -ForegroundColor White Write-Host (" Max PSI : {0:F4} (max: {1})" -f $CurrentMaxPSI, $Policy.MaxDriftPSI) -ForegroundColor White Write-Host (" Model age : {0:F0} days (max: {1})" -f $ageDays, $Policy.MaxAgeDays) -ForegroundColor White Write-Host (" New samples: {0} (min: {1})" -f $NewSamplesCount, $Policy.MinNewSamples) -ForegroundColor White Write-Host "" if ($needed) { Write-Host " ⚠️ RETRAINING TRIGGERED:" -ForegroundColor Red foreach ($t in $triggers) { Write-Host (" - {0}" -f $t) -ForegroundColor Yellow } } else { Write-Host " ✅ No retraining needed" -ForegroundColor Green } Write-Host "" return @{ Needed=$needed; Triggers=$triggers } } # ============================================================ # CI/CD INTEGRATION # ============================================================ # TEACHING NOTE: CI/CD for ML means automating the pipeline: # Commit code -> Run tests -> Train model -> Evaluate -> # Compare to baseline -> Promote if better -> Deploy # # Our implementation provides: # - Invoke-VBAFMLPipeline : full train/eval/compare/save pipeline # - Export-VBAFPipelineScript : generate a .ps1 CI script # - Test-VBAFModelGate : pass/fail gate for promotion decisions # ============================================================ function Invoke-VBAFMLPipeline { param( [string] $PipelineName, [string] $ExperimentName, [scriptblock]$TrainBlock, # { param($data) ... return $model } [scriptblock]$EvalBlock, # { param($model, $data) ... return @{R2=...; RMSE=...} } [object] $TrainData, [hashtable] $BaselineMetrics = @{}, [hashtable] $Params = @{}, [string] $ModelName = "", [string] $ModelType = "" ) Write-Host "" Write-Host ("🏗️ ML Pipeline: {0}" -f $PipelineName) -ForegroundColor Green Write-Host (" {0}" -f ("-" * 45)) -ForegroundColor DarkGray # Step 1: Start experiment run New-VBAFExperiment -Name $ExperimentName | Out-Null Start-VBAFRun -RunName $PipelineName -ModelType $ModelType -Params $Params | Out-Null $pipelineResult = @{ Success=$false; Model=$null; Metrics=@{}; Promoted=$false } try { # Step 2: Train Write-Host " [1/4] Training..." -ForegroundColor Cyan $sw = [System.Diagnostics.Stopwatch]::StartNew() $model = & $TrainBlock $TrainData $sw.Stop() $trainTime = [Math]::Round($sw.Elapsed.TotalSeconds, 2) Set-VBAFRunMetric -Key "train_time_s" -Value $trainTime Write-Host (" Done in {0}s" -f $trainTime) -ForegroundColor DarkGray # Step 3: Evaluate Write-Host " [2/4] Evaluating..." -ForegroundColor Cyan $metrics = & $EvalBlock $model $TrainData foreach ($k in $metrics.Keys) { Set-VBAFRunMetric -Key $k -Value $metrics[$k] Write-Host (" {0} = {1}" -f $k, [Math]::Round($metrics[$k],4)) -ForegroundColor White } $pipelineResult.Metrics = $metrics # Step 4: Compare to baseline Write-Host " [3/4] Comparing to baseline..." -ForegroundColor Cyan $promoted = $true if ($BaselineMetrics.Count -gt 0) { foreach ($k in $BaselineMetrics.Keys) { if ($metrics.ContainsKey($k)) { $isR2Like = $k -match "R2|accuracy|f1|auc" $isBetter = if ($isR2Like) { $metrics[$k] -ge $BaselineMetrics[$k] } ` else { $metrics[$k] -le $BaselineMetrics[$k] } $arrow = if ($isBetter) { "✅" } else { "❌" } Write-Host (" {0} {1}: {2:F4} vs baseline {3:F4}" -f $arrow, $k, $metrics[$k], $BaselineMetrics[$k]) -ForegroundColor $(if ($isBetter) {"Green"} else {"Red"}) if (-not $isBetter) { $promoted = $false } } } } $pipelineResult.Promoted = $promoted Set-VBAFRunTag -Key "promoted" -Value "$promoted" # Step 5: Save if promoted Write-Host " [4/4] Saving..." -ForegroundColor Cyan if ($promoted -and $ModelName -ne "") { Write-Host " Model promoted! ✅" -ForegroundColor Green } else { Write-Host " Model NOT promoted (below baseline)" -ForegroundColor Yellow } $pipelineResult.Model = $model $pipelineResult.Success = $true Stop-VBAFRun -Status "completed" } catch { Write-Host (" ❌ Pipeline failed: {0}" -f $_.Exception.Message) -ForegroundColor Red Stop-VBAFRun -Status "failed" } Write-Host "" $statusMsg = if ($pipelineResult.Promoted) { "✅ PASSED - model promoted" } else { "⚠️ NOT PROMOTED - below baseline" } Write-Host ("🏁 Pipeline {0}: {1}" -f $PipelineName, $statusMsg) -ForegroundColor $(if ($pipelineResult.Promoted) {"Green"} else {"Yellow"}) Write-Host "" return $pipelineResult } function Test-VBAFModelGate { param( [hashtable] $Metrics, [hashtable] $Gates # e.g. @{R2=0.95; RMSE=5.0} ) $passed = $true $fails = @() Write-Host "" Write-Host "🚦 Model Quality Gate:" -ForegroundColor Green foreach ($k in $Gates.Keys) { if (-not $Metrics.ContainsKey($k)) { Write-Host (" ⚠️ Missing metric: {0}" -f $k) -ForegroundColor Yellow continue } $isR2Like = $k -match "R2|accuracy|f1|auc" $ok = if ($isR2Like) { $Metrics[$k] -ge $Gates[$k] } else { $Metrics[$k] -le $Gates[$k] } $sym = if ($ok) { "✅" } else { "❌" } $col = if ($ok) { "Green" } else { "Red" } Write-Host (" {0} {1}: {2:F4} (gate: {3})" -f $sym, $k, $Metrics[$k], $Gates[$k]) -ForegroundColor $col if (-not $ok) { $passed = $false; $fails += $k } } $result = if ($passed) { "PASSED ✅" } else { "FAILED ❌ ($($fails -join ', '))" } Write-Host (" Gate result: {0}" -f $result) -ForegroundColor $(if ($passed) {"Green"} else {"Red"}) Write-Host "" return $passed } function Export-VBAFPipelineScript { param( [string] $OutputPath = ".\vbaf_pipeline.ps1", [string] $ModelName = "MyModel", [string] $ModelType = "LinearRegression", [string] $DatasetName = "HousePrice" ) $script = @" #Requires -Version 5.1 # VBAF CI/CD Pipeline Script - Generated $(Get-Date -Format "yyyy-MM-dd HH:mm") # Run this script to train, evaluate and promote a model automatically. param( [string]`$Environment = "dev", # dev, staging, prod [switch]`$ForceRetrain = `$false ) Write-Host "🚀 VBAF ML Pipeline - Environment: `$Environment" -ForegroundColor Green # 1. Load framework `$scriptPath = Split-Path -Parent `$MyInvocation.MyCommand.Definition . (Join-Path `$scriptPath "VBAF.LoadAll.ps1") # 2. Load data `$data = Get-VBAFDataset -Name "$DatasetName" `$scaler = [StandardScaler]::new() `$Xs = `$scaler.FitTransform(`$data.X) # 3. Run pipeline `$result = Invoke-VBAFMLPipeline `` -PipelineName "CI_`$(Get-Date -Format 'yyyyMMdd_HHmm')" `` -ExperimentName "$ModelName" `` -ModelType "$ModelType" `` -TrainData `$data `` -BaselineMetrics @{ R2=0.95; RMSE=5.0 } `` -Params @{ scaler="StandardScaler" } `` -ModelName "$ModelName" `` -TrainBlock { param(`$d) `$m = [LinearRegression]::new() `$m.Fit(`$Xs, `$d.y) return `$m } `` -EvalBlock { param(`$m, `$d) `$preds = `$m.Predict(`$Xs) `$metrics = Get-RegressionMetrics `$d.y `$preds return @{ R2=`$metrics.R2; RMSE=`$metrics.RMSE } } # 4. Quality gate `$gateResult = Test-VBAFModelGate -Metrics `$result.Metrics -Gates @{ R2=0.95; RMSE=5.0 } # 5. Save if gate passed if (`$gateResult -and `$result.Promoted) { Save-VBAFModel -ModelName "$ModelName" -Model `$result.Model `` -ModelType "$ModelType" -Metrics `$result.Metrics `` -DatasetName "$DatasetName" -BumpType "patch" Write-Host "✅ Model saved to registry" -ForegroundColor Green exit 0 } else { Write-Host "❌ Model did not pass quality gate - not saved" -ForegroundColor Red exit 1 } "@ $script | Set-Content $OutputPath -Encoding UTF8 Write-Host ("📄 Pipeline script exported: {0}" -f $OutputPath) -ForegroundColor Green return $OutputPath } # ============================================================ # TEST # 1. Run VBAF.LoadAll.ps1 # # --- Experiment tracking --- # 2. New-VBAFExperiment -Name "HousePriceExperiments" -Description "Compare regression models" # Start-VBAFRun -RunName "linear_baseline" -ModelType "LinearRegression" -Params @{scaler="Standard"} # Set-VBAFRunMetric -Key "R2" -Value 0.998 # Set-VBAFRunMetric -Key "RMSE" -Value 2.1 # Stop-VBAFRun # Start-VBAFRun -RunName "ridge_v1" -ModelType "RidgeRegression" -Params @{scaler="Standard"; lambda=0.1} # Set-VBAFRunMetric -Key "R2" -Value 0.999 # Set-VBAFRunMetric -Key "RMSE" -Value 1.8 # Stop-VBAFRun # Get-VBAFExperimentRuns # Compare-VBAFRuns # # --- Drift detection --- # 3. $data = Get-VBAFDataset -Name "HousePrice" # $ref = $data.X # training distribution # # Simulate production drift: larger houses # $prod = $data.X | ForEach-Object { @([double]($_[0]*1.5), [double]$_[1], [double]$_[2]) } # Get-VBAFDriftReport -ReferenceData $ref -ProductionData $prod ` # -FeatureNames @("size_sqm","bedrooms","age_years") # # --- Retraining policy --- # 4. $policy = New-VBAFRetrainingPolicy -ModelName "HousePricePredictor" ` # -MinAccuracy 0.95 -MaxDriftPSI 0.2 -MaxAgeDays 30 # Test-VBAFRetrainingNeeded -Policy $policy -CurrentAccuracy 0.92 ` # -CurrentMaxPSI 0.05 -ModelTrainedDate (Get-Date).AddDays(-10) # # --- Export CI/CD script --- # 5. Export-VBAFPipelineScript -OutputPath "C:\Temp\vbaf_pipeline.ps1" # ============================================================ Write-Host "📦 VBAF.ML.MLOps.ps1 loaded [v2.1.0 🏭]" -ForegroundColor Green Write-Host " Experiment Tracking:" -ForegroundColor Cyan Write-Host " New-VBAFExperiment" -ForegroundColor Cyan Write-Host " Start-VBAFRun / Stop-VBAFRun" -ForegroundColor Cyan Write-Host " Set-VBAFRunMetric / Param / Tag" -ForegroundColor Cyan Write-Host " Get-VBAFExperimentRuns" -ForegroundColor Cyan Write-Host " Compare-VBAFRuns" -ForegroundColor Cyan Write-Host " Drift Detection:" -ForegroundColor Cyan Write-Host " Get-VBAFDriftReport" -ForegroundColor Cyan Write-Host " Retraining:" -ForegroundColor Cyan Write-Host " New-VBAFRetrainingPolicy" -ForegroundColor Cyan Write-Host " Test-VBAFRetrainingNeeded" -ForegroundColor Cyan Write-Host " CI/CD:" -ForegroundColor Cyan Write-Host " Invoke-VBAFMLPipeline" -ForegroundColor Cyan Write-Host " Test-VBAFModelGate" -ForegroundColor Cyan Write-Host " Export-VBAFPipelineScript" -ForegroundColor Cyan Write-Host "" Write-Host " Quick start:" -ForegroundColor Yellow Write-Host ' New-VBAFExperiment -Name "MyExperiment"' -ForegroundColor White Write-Host ' Start-VBAFRun -RunName "run1" -ModelType "LinearRegression" -Params @{lr=0.01}' -ForegroundColor White Write-Host ' Set-VBAFRunMetric -Key "R2" -Value 0.998' -ForegroundColor White Write-Host ' Stop-VBAFRun' -ForegroundColor White Write-Host ' Compare-VBAFRuns' -ForegroundColor White Write-Host "" |