| """ |
| Treino de modelo de classificação multiclasse: Real (0) vs IA (1) vs CGI (2). |
| |
| Treina XGBoost + Random Forest, compara, salva o melhor. |
| |
| Uso: |
| python ml/train_model.py |
| python ml/train_model.py --input dataset_treino.csv --output ml/model.joblib |
| """ |
|
|
| import argparse |
| import sys |
| from pathlib import Path |
|
|
| import numpy as np |
| import pandas as pd |
| from sklearn.model_selection import StratifiedKFold, cross_val_score |
| from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier |
| from sklearn.preprocessing import StandardScaler |
| from sklearn.pipeline import Pipeline |
| from sklearn.metrics import classification_report, confusion_matrix |
| import joblib |
|
|
| |
| try: |
| from xgboost import XGBClassifier |
| HAS_XGB = True |
| except ImportError: |
| HAS_XGB = False |
| print("[AVISO] xgboost não instalado. Usando GradientBoosting do sklearn como fallback.") |
|
|
|
|
| |
| META_COLUMNS = {"video_file", "frame_index", "label"} |
|
|
|
|
| def load_dataset(csv_path: str) -> tuple[pd.DataFrame, np.ndarray, np.ndarray, list[str]]: |
| """Carrega CSV e separa features / labels.""" |
| df = pd.read_csv(csv_path) |
|
|
| print(f"Dataset carregado: {len(df)} amostras") |
| print(f"Distribuição de classes:") |
| label_names = {0: "Real", 1: "IA", 2: "CGI"} |
| for label, count in df["label"].value_counts().sort_index().items(): |
| print(f" {label} ({label_names.get(label, '?')}): {count}") |
|
|
| feature_cols = [c for c in df.columns if c not in META_COLUMNS] |
| X = df[feature_cols].values.astype(np.float32) |
| y = df["label"].values.astype(np.int32) |
|
|
| |
| nan_mask = np.isnan(X) | np.isinf(X) |
| if nan_mask.any(): |
| nan_count = nan_mask.sum() |
| print(f"[AVISO] {nan_count} valores NaN/Inf encontrados. Substituindo por 0.") |
| X = np.nan_to_num(X, nan=0.0, posinf=0.0, neginf=0.0) |
|
|
| return df, X, y, feature_cols |
|
|
|
|
| def build_models() -> dict: |
| """Retorna dict de modelos candidatos.""" |
| models = {} |
|
|
| |
| models["RandomForest"] = Pipeline([ |
| ("scaler", StandardScaler()), |
| ("clf", RandomForestClassifier( |
| n_estimators=300, |
| max_depth=12, |
| min_samples_leaf=5, |
| class_weight="balanced", |
| random_state=42, |
| n_jobs=-1, |
| )), |
| ]) |
|
|
| |
| if HAS_XGB: |
| models["XGBoost"] = Pipeline([ |
| ("scaler", StandardScaler()), |
| ("clf", XGBClassifier( |
| n_estimators=500, |
| max_depth=8, |
| learning_rate=0.05, |
| subsample=0.8, |
| colsample_bytree=0.8, |
| objective="multi:softprob", |
| eval_metric="mlogloss", |
| random_state=42, |
| n_jobs=-1, |
| )), |
| ]) |
| else: |
| models["GradientBoosting"] = Pipeline([ |
| ("scaler", StandardScaler()), |
| ("clf", GradientBoostingClassifier( |
| n_estimators=300, |
| max_depth=6, |
| learning_rate=0.05, |
| subsample=0.8, |
| random_state=42, |
| )), |
| ]) |
|
|
| return models |
|
|
|
|
| def evaluate_models(models: dict, X: np.ndarray, y: np.ndarray) -> str: |
| """Avalia modelos com cross-validation e retorna o nome do melhor.""" |
| cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) |
| results = {} |
|
|
| for name, model in models.items(): |
| print(f"\n--- {name} ---") |
| scores = cross_val_score(model, X, y, cv=cv, scoring="f1_macro", n_jobs=-1) |
| mean_score = float(np.mean(scores)) |
| std_score = float(np.std(scores)) |
| results[name] = mean_score |
| print(f" F1-macro (5-fold): {mean_score:.4f} (+/- {std_score:.4f})") |
| print(f" Scores por fold: {[f'{s:.4f}' for s in scores]}") |
|
|
| best = max(results, key=results.get) |
| print(f"\nMelhor modelo: {best} (F1={results[best]:.4f})") |
| return best |
|
|
|
|
| def train_final(model, X: np.ndarray, y: np.ndarray, feature_cols: list[str]): |
| """Treina o modelo final em todo o dataset e exibe relatório.""" |
| model.fit(X, y) |
|
|
| y_pred = model.predict(X) |
| label_names = ["Real", "IA", "CGI"] |
|
|
| print("\n" + "=" * 50) |
| print("RELATÓRIO FINAL (treino completo)") |
| print("=" * 50) |
| print(classification_report(y, y_pred, target_names=label_names)) |
|
|
| print("Matriz de Confusão:") |
| cm = confusion_matrix(y, y_pred) |
| print(f"{'':>8} {'Real':>8} {'IA':>8} {'CGI':>8}") |
| for i, row in enumerate(cm): |
| print(f"{label_names[i]:>8} {row[0]:>8} {row[1]:>8} {row[2]:>8}") |
|
|
| |
| clf = model.named_steps["clf"] |
| if hasattr(clf, "feature_importances_"): |
| importances = clf.feature_importances_ |
| sorted_idx = np.argsort(importances)[::-1] |
| print("\nTop 10 features mais importantes:") |
| for rank, idx in enumerate(sorted_idx[:10]): |
| print(f" {rank+1}. {feature_cols[idx]}: {importances[idx]:.4f}") |
|
|
| return model |
|
|
|
|
| def main(): |
| parser = argparse.ArgumentParser(description="Treina modelo de classificação multiclasse") |
| parser.add_argument("--input", default="dataset_treino.csv", help="CSV de entrada") |
| parser.add_argument("--output", default="ml/model.joblib", help="Arquivo do modelo salvo") |
| args = parser.parse_args() |
|
|
| if not Path(args.input).exists(): |
| print(f"Arquivo não encontrado: {args.input}") |
| print("Execute primeiro: python ml/extract_features.py") |
| sys.exit(1) |
|
|
| df, X, y, feature_cols = load_dataset(args.input) |
|
|
| if len(np.unique(y)) < 2: |
| print("Dataset precisa de pelo menos 2 classes. Adicione mais vídeos.") |
| sys.exit(1) |
|
|
| print(f"\nFeatures: {len(feature_cols)}") |
| print(f"Shape: {X.shape}") |
|
|
| |
| models = build_models() |
| best_name = evaluate_models(models, X, y) |
|
|
| |
| best_model = models[best_name] |
| train_final(best_model, X, y, feature_cols) |
|
|
| |
| output_path = Path(args.output) |
| output_path.parent.mkdir(parents=True, exist_ok=True) |
|
|
| save_data = { |
| "model": best_model, |
| "feature_columns": feature_cols, |
| "label_map": {0: "Real", 1: "IA", 2: "CGI"}, |
| "model_name": best_name, |
| } |
| joblib.dump(save_data, output_path) |
| print(f"\nModelo salvo: {output_path}") |
| print(f"Para carregar: joblib.load('{output_path}')") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|