CoolFace
Apppublic

matalee/hpce-dev

sourceHugging Faceapache-2.0updated 3mo agoView on Hugging Face
0likes
sklearn_model.py336 linesDownload Raw Back to models
1from __future__ import annotations2"""3Model-based Intent Inference ([2b] 모듈)4 5시나리오 무관 sklearn Logistic Regression 추론 머신러리.6- 학습 데이터(training_data)·dataset_path·model_prefix는 호출자(시나리오 엔진)가 주입7- StandardScaler + LogisticRegression Pipeline8- MLflow Registry 등록 (모델명: {model_prefix}{intent_id}_sklearn)9- seed 고정 (42)으로 재현성 확보10"""11import json12import logging13import random14from pathlib import Path15from typing import Any16 17import numpy as np18import mlflow19import mlflow.sklearn20from sklearn.linear_model import LogisticRegression21from sklearn.preprocessing import StandardScaler22from sklearn.pipeline import Pipeline23 24from config import settings25 26logger = logging.getLogger(__name__)27_model_cache: dict[str, Any] = {}28 29 30# ── Public API ────────────────────────────────────────────────31 32def _train_pipeline(X: list, y: list, seed: int = 42, train_params: dict | None = None) -> Pipeline:33    """StandardScaler + LogisticRegression 파이프라인을 학습한다.34 35    seed를 고정하여 재현성을 확보한다.36 37    Args:38        X: 특징 행렬.39        y: 레이블 벡터.40        seed: 난수 시드 (재현성).41        train_params: 시나리오 config L2.model.train의 하이퍼파라미터.42            class_weight·C를 override 가능 (기본 balanced·C=1.0).43 44    Returns:45        학습된 sklearn Pipeline.46    """47    tp = train_params or {}48    pipe = Pipeline([49        ("scaler", StandardScaler()),50        ("lr", LogisticRegression(51            random_state=seed,52            max_iter=500,53            C=tp.get("C", 1.0),54            class_weight=tp.get("class_weight", "balanced"),55        )),56    ])57    pipe.fit(np.array(X, dtype=float), np.array(y))58    return pipe59 60 61def _extract_from_dataset(62    intent_id: str,63    feature_names: list[str],64    seed: int,65    dataset_path: Path,66    neg_pos_ratio: float = 2.0,67) -> tuple[list[list[float]], list[int]] | None:68    """seed_dataset.json에서 intent_id에 대한 (X, y)를 추출한다.69 70    양성은 sample["intent_labels"]에 intent_id가 있는 경우(y=1), 음성은 그 외(y=0)이다.71    클래스 불균형 처리를 위해 음성은 neg_pos_ratio × n_pos 까지만 샘플링한다.72 73    Args:74        intent_id: 추출할 Intent ID.75        feature_names: feature 벡터를 구성할 feature 이름 순서.76        seed: 음성 샘플링에 사용할 난수 시드.77        dataset_path: seed_dataset.json 경로.78        neg_pos_ratio: 양성 대비 음성 샘플 비율 상한.79 80    Returns:81        (X, y) 튜플. 양성·음성이 각각 3건 미만이거나 데이터셋이 없으면 None.82    """83    if not dataset_path.exists():84        return None85    try:86        with open(dataset_path, encoding="utf-8") as f:87            dataset = json.load(f)88    except Exception as e:89        logger.warning(f"Failed to load seed_dataset.json: {e}")90        return None91 92    X_pos, X_neg = [], []93    for sample in dataset.get("samples", []):94        # batch + pattern + event 를 합친 전체 feature 벡터 (추론 시점과 동일 공간)95        feats = {96            **sample.get("batch_features", {}),97            **sample.get("pattern_features", {}),98            **sample.get("event_features", {}),99        }100        x = [float(feats.get(name, 0.0)) for name in feature_names]101        if intent_id in sample.get("intent_labels", {}):102            X_pos.append(x)103        else:104            X_neg.append(x)105 106    if len(X_pos) < 3 or len(X_neg) < 3:107        return None108 109    rng = random.Random(seed)110    n_neg_target = min(len(X_neg), max(int(len(X_pos) * neg_pos_ratio), 10))111    X_neg_sampled = rng.sample(X_neg, n_neg_target) if len(X_neg) > n_neg_target else X_neg112 113    X = X_pos + X_neg_sampled114    y = [1] * len(X_pos) + [0] * len(X_neg_sampled)115    return X, y116 117 118def train_and_register(119    intent_id: str,120    training_data: dict,121    dataset_path: Path,122    model_prefix: str,123    seed: int = 42,124    train_params: dict | None = None,125) -> Pipeline | None:126    """Intent의 학습 데이터로 모델을 학습하고 MLflow에 등록한다.127 128    데이터 소스 우선순위:129      1) dataset_path(seed_dataset.json)의 페르소나 시드 데이터셋130      2) training_data[intent_id]의 도메인 지식 X, y131 132    Args:133        intent_id: 학습할 Intent ID.134        training_data: Intent별 학습 정의(features·X·y 등).135        dataset_path: seed_dataset.json 경로.136        model_prefix: 시나리오별 MLflow 모델명 네임스페이스.137            등록명은 {model_prefix}{intent_id}_sklearn.138        seed: 난수 시드 (재현성).139        train_params: 학습 하이퍼파라미터(class_weight/C).140 141    Returns:142        학습된 Pipeline. 학습 데이터가 없으면 None.143    """144    data = training_data.get(intent_id)145    if data is None:146        return None147 148    feature_names = data["features"]149    model_name = f"{model_prefix}{intent_id}_sklearn"150 151    # 1) 시드 데이터셋 우선152    extracted = _extract_from_dataset(intent_id, feature_names, seed, dataset_path)153    if extracted is not None:154        X, y = extracted155        data_source = "seed_dataset"156    elif "X" in data and "y" in data:157        X, y = data["X"], data["y"]158        data_source = "domain_knowledge"159    else:160        return None161 162    pipe = _train_pipeline(X, y, seed=seed, train_params=train_params)163 164    mlflow.set_tracking_uri(settings.MLFLOW_URI)165    with mlflow.start_run(run_name=f"{model_name}_init"):166        mlflow.sklearn.log_model(167            pipe,168            "model",169            registered_model_name=model_name,170        )171        mlflow.log_params({172            "intent_id":     intent_id,173            "n_features":    len(feature_names),174            "n_samples":     len(y),175            "n_positive":    int(sum(y)),176            "seed":          seed,177            "data_source":   data_source,178            "feature_names": ",".join(feature_names),179        })180        train_acc = pipe.score(np.array(X, dtype=float), np.array(y))181        mlflow.log_metric("train_accuracy", train_acc)182 183    logger.info(f"Trained + registered: {model_name} "184                f"(source={data_source}, n={len(y)}, pos={int(sum(y))}, acc={train_acc:.3f})")185    return pipe186 187 188def _load_or_train(189    intent_id: str,190    training_data: dict,191    dataset_path: Path,192    model_prefix: str,193    train_params: dict | None = None,194) -> Pipeline | None:195    """모델을 캐시→MLflow Registry 순으로 로드하고, 없으면 학습·등록한다.196 197    프로세스 캐시(_model_cache)를 사용해 intent별로 1회만 로드/학습한다.198 199    Args:200        intent_id: 로드/학습할 Intent ID.201        training_data: Intent별 학습 정의.202        dataset_path: seed_dataset.json 경로.203        model_prefix: 시나리오별 MLflow 모델명 네임스페이스.204        train_params: 학습 하이퍼파라미터.205 206    Returns:207        로드 또는 학습된 Pipeline. 학습 정의가 없으면 None.208    """209    cache_key = f"{model_prefix}{intent_id}"210    if cache_key in _model_cache:211        return _model_cache[cache_key]212 213    if intent_id not in training_data:214        return None215 216    mlflow.set_tracking_uri(settings.MLFLOW_URI)217    uri = f"models:/{model_prefix}{intent_id}_sklearn/latest"218    try:219        pipe = mlflow.sklearn.load_model(uri)220    except Exception:221        pipe = train_and_register(222            intent_id, training_data=training_data,223            dataset_path=dataset_path, model_prefix=model_prefix,224            train_params=train_params,225        )226 227    _model_cache[cache_key] = pipe228    return pipe229 230 231def predict(232    intent_id: str,233    features: dict[str, Any],234    training_data: dict,235    dataset_path: Path,236    model_prefix: str,237    train_params: dict | None = None,238) -> float:239    """Intent ID에 대해 Model 기반 Score를 추론한다.240 241    features dict에서 학습에 사용된 피처들을 순서대로 추출하며,242    누락된 피처는 0.0으로 처리한다.243 244    Args:245        intent_id: 추론할 Intent ID.246        features: 추론에 사용할 feature dict.247        training_data: Intent별 학습 정의(시나리오 엔진 제공).248        dataset_path: seed_dataset.json 경로(시나리오 엔진 제공).249        model_prefix: 시나리오별 모델명 네임스페이스(시나리오 엔진 제공).250        train_params: 학습 하이퍼파라미터(class_weight/C, config L2.model.train).251 252    Returns:253        0~1 범위의 예측 점수. 모델이 없으면 0.0.254    """255    pipe = _load_or_train(intent_id, training_data, dataset_path, model_prefix, train_params)256    if pipe is None:257        return 0.0258 259    feature_names = training_data[intent_id]["features"]260    x = np.array([[float(features.get(name, 0.0)) for name in feature_names]])261 262    proba = pipe.predict_proba(x)[0][1]263    return float(proba)264 265 266def explain(267    intent_id: str,268    features: dict[str, Any],269    training_data: dict,270    dataset_path: Path,271    model_prefix: str,272    top: int = 3,273) -> list[dict]:274    """Model 추론의 feature 기여도를 분해한다.275 276    선형 파이프라인(StandardScaler + LogisticRegression)에서277    기여_i = coef_i × ((x_i - mean_i) / scale_i)로 계산하고,278    |기여| 상위 top개를 반환한다.279 280    Args:281        intent_id: 기여도를 분해할 Intent ID.282        features: 추론에 사용한 feature dict.283        training_data: Intent별 학습 정의.284        dataset_path: seed_dataset.json 경로.285        model_prefix: 시나리오별 모델명 네임스페이스.286        top: 반환할 상위 기여 feature 개수.287 288    Returns:289        feature별 기여 정보(label·contribution·direction·value) dict의 목록.290        모델이 없거나 분해에 실패하면 빈 목록.291    """292    pipe = _load_or_train(intent_id, training_data, dataset_path, model_prefix)293    if pipe is None or intent_id not in training_data:294        return []295    feats = training_data[intent_id]["features"]296    x = np.array([float(features.get(n, 0.0)) for n in feats])297    try:298        scaler = pipe.named_steps["scaler"]299        lr = pipe.named_steps["lr"]300        xs = (x - scaler.mean_) / scaler.scale_301        contrib = lr.coef_[0] * xs302    except Exception:303        return []304    items = sorted(zip(feats, contrib, x), key=lambda t: -abs(t[1]))[:top]305    return [{"label": n, "contribution": round(float(c), 4),306             "direction": "up" if c >= 0 else "down", "value": round(float(v), 2)}307            for n, c, v in items]308 309 310def train_all(311    training_data: dict,312    dataset_path: Path,313    model_prefix: str,314    seed: int = 42,315) -> dict[str, float]:316    """training_data의 모든 Model Intent를 학습·등록한다.317 318    Args:319        training_data: Intent별 학습 정의.320        dataset_path: seed_dataset.json 경로.321        model_prefix: 시나리오별 MLflow 모델명 네임스페이스.322        seed: 난수 시드 (재현성).323 324    Returns:325        학습에 성공한 Intent에 대한 {intent_id: 1.0} 매핑.326    """327    results = {}328    for intent_id in training_data.keys():329        pipe = train_and_register(330            intent_id, training_data=training_data, seed=seed,331            dataset_path=dataset_path, model_prefix=model_prefix,332        )333        if pipe is not None:334            results[intent_id] = 1.0335    return results336