Qwik กับ MLOps Dashboard
MLOps Dashboard เป็นเครื่องมือสำคัญสำหรับ ML Engineers ในการ Monitor Model Performance, Track Experiments, จัดการ Pipeline และดู Metrics ต่างๆ ปัญหาคือ Dashboard มักมี Components จำนวนมาก เช่น Charts, Tables, Metrics Cards, Log Viewers ทำให้โหลดช้าเมื่อใช้ Framework แบบ Hydration
Qwik แก้ปัญหานี้ด้วย Resumability JavaScript โหลดเฉพาะ Component ที่ User กำลัง Interact ทำให้ Dashboard ที่มี 50+ Components โหลดเร็วเท่ากับที่มี 5 Components เพราะ Initial JavaScript Bundle แทบเป็นศูนย์
สร้าง MLOps Dashboard ด้วย Qwik
// src/routes/dashboard/index.tsx
// MLOps Dashboard — Main Page
import { component$, useStore, useTask$ } from '@builder.io/qwik';
import { routeLoader$ } from '@builder.io/qwik-city';
import { ModelMetrics } from '~/components/model-metrics';
import { ExperimentTable } from '~/components/experiment-table';
import { PipelineStatus } from '~/components/pipeline-status';
import { DriftMonitor } from '~/components/drift-monitor';
// Server-side Data Loading
export const useModels = routeLoader$(async () => {
const res = await fetch('http://mlflow:5000/api/2.0/mlflow/registered-models/list');
return res.json();
});
export const useExperiments = routeLoader$(async () => {
const res = await fetch('http://mlflow:5000/api/2.0/mlflow/experiments/search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ max_results: 20 }),
});
return res.json();
});
export default component$(() => {
const models = useModels();
const experiments = useExperiments();
const dashboard = useStore({
activeTab: 'overview',
refreshInterval: 30000,
selectedModel: null as string | null,
});
return (
<div class="min-h-screen bg-zinc-950 text-white">
<header class="border-b border-zinc-800 px-6 py-4">
<h1 class="text-2xl font-bold">MLOps Dashboard</h1>
<nav class="flex gap-4 mt-3">
{['overview', 'experiments', 'models', 'pipelines', 'monitoring'].map(
(tab) => (
<button
key={tab}
class={`px-4 py-2 rounded-lg text-sm font-medium transition
`}
onClick$={() => { dashboard.activeTab = tab; }}
>
{tab.charAt(0).toUpperCase() + tab.slice(1)}
</button>
)
)}
</nav>
</header>
<main class="p-6">
{dashboard.activeTab === 'overview' && (
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<MetricCard title="Active Models"
value={models.value?.registered_models?.length || 0}
trend="+2 this week" />
<MetricCard title="Experiments"
value={experiments.value?.experiments?.length || 0}
trend="+5 this week" />
<MetricCard title="Avg Latency"
value="45ms" trend="-3ms" />
<MetricCard title="Success Rate"
value="99.2%" trend="+0.1%" />
</div>
)}
{dashboard.activeTab === 'experiments' && (
<ExperimentTable experiments={experiments.value} />
)}
{dashboard.activeTab === 'monitoring' && (
<DriftMonitor modelName={dashboard.selectedModel} />
)}
{dashboard.activeTab === 'pipelines' && (
<PipelineStatus />
)}
</main>
</div>
);
});
// Metric Card — โหลด JS เฉพาะเมื่อ Hover/Click
const MetricCard = component$<{
title: string; value: string | number; trend: string;
}>((props) => {
return (
<div class="bg-zinc-900 rounded-xl p-5 border border-zinc-800
hover:border-indigo-500/50 transition">
<p class="text-sm text-zinc-400">{props.title}</p>
<p class="text-3xl font-bold mt-1">{props.value}</p>
<p class="text-sm text-emerald-400 mt-1">{props.trend}</p>
</div>
);
});
Model Monitoring Component
// src/components/drift-monitor.tsx
// Data Drift & Model Performance Monitor
import { component$, useSignal, useVisibleTask$ } from '@builder.io/qwik';
interface DriftData {
feature: string;
psi: number; // Population Stability Index
ks_stat: number; // Kolmogorov-Smirnov Statistic
status: 'stable' | 'warning' | 'drift';
timestamp: string;
}
interface ModelPerf {
accuracy: number;
precision: number;
recall: number;
f1: number;
latency_p50: number;
latency_p99: number;
throughput: number;
}
export const DriftMonitor = component$<{ modelName: string | null }>(
(props) => {
const driftData = useSignal<DriftData[]>([]);
const perfData = useSignal<ModelPerf | null>(null);
const isLoading = useSignal(true);
// useVisibleTask$ — โหลดเฉพาะเมื่อ Component Visible
useVisibleTask$(async ({ track, cleanup }) => {
track(() => props.modelName);
if (!props.modelName) return;
const fetchData = async () => {
try {
const [driftRes, perfRes] = await Promise.all([
fetch(`/api/monitoring/drift/`),
fetch(`/api/monitoring/performance/`),
]);
driftData.value = await driftRes.json();
perfData.value = await perfRes.json();
} catch (e) {
console.error('Monitoring fetch error:', e);
} finally {
isLoading.value = false;
}
};
await fetchData();
const interval = setInterval(fetchData, 30000);
cleanup(() => clearInterval(interval));
});
if (isLoading.value) {
return <div class="text-zinc-400">Loading monitoring data...</div>;
}
return (
<div class="space-y-6">
<h2 class="text-xl font-semibold">
Model: {props.modelName}
</h2>
{/* Performance Metrics */}
{perfData.value && (
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4">
<PerfCard label="Accuracy"
value={`%`}
threshold={0.9} current={perfData.value.accuracy} />
<PerfCard label="F1 Score"
value={perfData.value.f1.toFixed(3)}
threshold={0.85} current={perfData.value.f1} />
<PerfCard label="Latency P99"
value={`ms`}
threshold={100} current={perfData.value.latency_p99}
inverse />
<PerfCard label="Throughput"
value={`/s`}
threshold={100} current={perfData.value.throughput} />
</div>
)}
{/* Drift Table */}
<div class="bg-zinc-900 rounded-xl border border-zinc-800 overflow-hidden">
<div class="px-5 py-3 border-b border-zinc-800">
<h3 class="font-medium">Feature Drift Detection</h3>
</div>
<table class="w-full text-sm">
<thead class="text-zinc-400 border-b border-zinc-800">
<tr>
<th class="px-5 py-3 text-left">Feature</th>
<th class="px-5 py-3 text-left">PSI</th>
<th class="px-5 py-3 text-left">KS Stat</th>
<th class="px-5 py-3 text-left">Status</th>
</tr>
</thead>
<tbody>
{driftData.value.map((d) => (
<tr key={d.feature} class="border-b border-zinc-800/50">
<td class="px-5 py-3 font-mono">{d.feature}</td>
<td class="px-5 py-3">{d.psi.toFixed(4)}</td>
<td class="px-5 py-3">{d.ks_stat.toFixed(4)}</td>
<td class="px-5 py-3">
<span class={`px-2 py-1 rounded text-xs font-medium
`}>
{d.status.toUpperCase()}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
);
const PerfCard = component$<{
label: string; value: string;
threshold: number; current: number; inverse?: boolean;
}>((props) => {
const isGood = props.inverse
? props.current <= props.threshold
: props.current >= props.threshold;
return (
<div class={`rounded-xl p-4 border `}>
<p class="text-sm text-zinc-400">{props.label}</p>
<p class="text-2xl font-bold mt-1">{props.value}</p>
</div>
);
});
MLOps API Routes
// src/routes/api/monitoring/drift/[model]/index.ts
// API Route — Drift Detection Data
import type { RequestHandler } from '@builder.io/qwik-city';
export const onGet: RequestHandler = async ({ params, json }) => {
const modelName = params.model;
// เรียก ML Monitoring Service
const response = await fetch(
`http://ml-monitor:8080/api/v1/drift/`,
{ headers: { 'Authorization': `Bearer ` } }
);
if (!response.ok) {
json(response.status, { error: 'Failed to fetch drift data' });
return;
}
const data = await response.json();
json(200, data);
};
// src/routes/api/monitoring/performance/[model]/index.ts
export const onGet: RequestHandler = async ({ params, json }) => {
const modelName = params.model;
// Query Prometheus สำหรับ Model Metrics
const queries = {
accuracy: `ml_model_accuracy{model=""}`,
latency_p50: `histogram_quantile(0.5, ml_model_latency_bucket{model=""})`,
latency_p99: `histogram_quantile(0.99, ml_model_latency_bucket{model=""})`,
throughput: `rate(ml_model_predictions_total{model=""}[5m])`,
};
const results: Record<string, number> = {};
for (const [key, query] of Object.entries(queries)) {
const res = await fetch(
`http://prometheus:9090/api/v1/query?query=`
);
const data = await res.json();
results[key] = parseFloat(data?.data?.result?.[0]?.value?.[1] || '0');
}
json(200, results);
};
// src/routes/api/pipelines/index.ts
// API Route — Pipeline Status
export const onGet: RequestHandler = async ({ json }) => {
// Query Airflow/Kubeflow สำหรับ Pipeline Status
const res = await fetch('http://airflow:8080/api/v1/dags', {
headers: { 'Authorization': `Basic ` },
});
const data = await res.json();
const pipelines = data.dags?.map((dag: any) => ({
id: dag.dag_id,
status: dag.is_paused ? 'paused' : 'active',
lastRun: dag.last_parsed_time,
schedule: dag.schedule_interval,
})) || [];
json(200, pipelines);
};
MLOps Workflow Automation
# mlops_workflow.py — MLOps Pipeline Automation
import mlflow
from mlflow.tracking import MlflowClient
import requests
import json
from datetime import datetime
class MLOpsWorkflow:
"""จัดการ MLOps Workflow"""
def __init__(self, tracking_uri="http://mlflow:5000"):
mlflow.set_tracking_uri(tracking_uri)
self.client = MlflowClient()
def train_and_log(self, model_name, train_func, params, X_train, y_train,
X_test, y_test):
"""Train Model และ Log ไป MLflow"""
with mlflow.start_run(run_name=f"{model_name}_{datetime.now():%Y%m%d}"):
# Log Parameters
mlflow.log_params(params)
# Train
model = train_func(X_train, y_train, **params)
# Evaluate
predictions = model.predict(X_test)
from sklearn.metrics import accuracy_score, f1_score
accuracy = accuracy_score(y_test, predictions)
f1 = f1_score(y_test, predictions, average='weighted')
# Log Metrics
mlflow.log_metrics({
"accuracy": accuracy,
"f1_score": f1,
"train_size": len(X_train),
"test_size": len(X_test),
})
# Log Model
mlflow.sklearn.log_model(model, "model",
registered_model_name=model_name)
print(f"Model {model_name}: accuracy={accuracy:.4f} f1={f1:.4f}")
return model
def promote_model(self, model_name, version, stage="Production"):
"""Promote Model ไป Production"""
self.client.transition_model_version_stage(
name=model_name,
version=version,
stage=stage,
)
print(f"Model {model_name} v{version} promoted to {stage}")
def check_drift(self, model_name, current_data, reference_data):
"""ตรวจจับ Data Drift"""
from scipy.stats import ks_2samp
import numpy as np
drift_results = []
for col in current_data.columns:
stat, p_value = ks_2samp(
reference_data[col].dropna(),
current_data[col].dropna(),
)
# PSI Calculation
psi = self._calculate_psi(
reference_data[col].dropna().values,
current_data[col].dropna().values,
)
status = "stable"
if psi > 0.2 or p_value < 0.01:
status = "drift"
elif psi > 0.1 or p_value < 0.05:
status = "warning"
drift_results.append({
"feature": col,
"ks_stat": float(stat),
"p_value": float(p_value),
"psi": float(psi),
"status": status,
})
return drift_results
def _calculate_psi(self, expected, actual, bins=10):
"""คำนวณ Population Stability Index"""
import numpy as np
breakpoints = np.linspace(
min(expected.min(), actual.min()),
max(expected.max(), actual.max()),
bins + 1,
)
expected_pct = np.histogram(expected, breakpoints)[0] / len(expected)
actual_pct = np.histogram(actual, breakpoints)[0] / len(actual)
expected_pct = np.clip(expected_pct, 0.001, None)
actual_pct = np.clip(actual_pct, 0.001, None)
psi = np.sum((actual_pct - expected_pct) *
np.log(actual_pct / expected_pct))
return psi
# ตัวอย่าง
# workflow = MLOpsWorkflow()
# workflow.train_and_log("fraud_detector", train_xgb, params, X_train, y_train, X_test, y_test)
# workflow.promote_model("fraud_detector", version=3, stage="Production")
Qwik Resumability คืออะไร
Qwik ใช้ Resumability แทน Hydration JavaScript โหลดเฉพาะ Component ที่ User Interact Serialize State ใน HTML แล้ว Resume ทำให้ Initial Load เร็วมาก เหมาะกับ Dashboard ที่มี Components เยอะ
ทำไมต้องใช้ Qwik กับ MLOps Dashboard
MLOps Dashboard มี Components เยอะ Charts Tables Metrics Logs ถ้าใช้ React ต้อง Hydrate ทุก Component Qwik โหลดเฉพาะ Component ที่ User ดู Dashboard 50+ Components โหลดเร็วเท่ากับ 5 Components
MLOps Workflow ประกอบด้วยอะไรบ้าง
Data Pipeline, Feature Engineering, Model Training, Evaluation, Model Registry, Deployment, Monitoring และ Feedback Loop เครื่องมือนิยม เช่น MLflow, Kubeflow, Airflow, Seldon ครอบคลุมตั้งแต่ Data ถึง Production
วิธี Monitor ML Model ใน Production ทำอย่างไร
ติดตาม Accuracy, Latency, Throughput ตรวจจับ Data Drift ด้วย PSI และ KS Test เปรียบเทียบ Prediction Distribution กับ Training ตั้ง Alert เมื่อ Metrics ต่ำกว่า Threshold ใช้ Dashboard แสดง Real-time Metrics
สรุป
Qwik Resumability เป็นทางเลือกที่ดีสำหรับ MLOps Dashboard ที่มี Components จำนวนมาก โหลดเร็วเพราะ JavaScript โหลดเฉพาะ Component ที่ User Interact ใช้ routeLoader$ สำหรับ Server-side Data Loading, useVisibleTask$ สำหรับ Client-side Effects ที่โหลดเมื่อ Visible รวมกับ MLflow, Prometheus และ Airflow สำหรับ Backend สร้าง MLOps Platform ที่ครบวงจร