From c5ecb5e1247c688ef3da06cadd956d849ba4fb99 Mon Sep 17 00:00:00 2001 From: Floke Date: Wed, 18 Jun 2025 14:22:35 +0000 Subject: [PATCH] =?UTF-8?q?Feat:=20Hyperparameter-Tuning=20mit=20GridSearc?= =?UTF-8?q?hCV=20und=20SMOTE-Pipeline=20f=C3=BCr=20ML?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Implementierung von Hyperparameter-Tuning: Der Trainingsprozess (`train_technician_model`) verwendet nun `GridSearchCV` von scikit-learn, um systematisch die besten Hyperparameter für das RandomForest-Modell zu finden. - Integration einer imblearn-Pipeline: SMOTE (zur Klassen-Balancierung) und der RandomForestClassifier wurden in eine `imblearn.pipeline.Pipeline` integriert. Dies stellt sicher, dass das Oversampling bei der Kreuzvalidierung korrekt nur auf den Trainings-Folds angewendet wird, um Datenlecks zu vermeiden. - Erweiterte Modellevaluation: Der beste durch GridSearchCV gefundene Estimator wird nun für die finale Evaluation auf dem Testset verwendet und als finales Modell gespeichert. Die besten gefundenen Parameter und die Cross-Validation-Genauigkeit werden geloggt. - Code-Struktur: Die `train_technician_model`-Methode wurde umfassend überarbeitet, um die neue Pipeline- und GridSearchCV-Logik zu implementieren. Entsprechende Imports (`GridSearchCV`, `ImbPipeline`) wurden hinzugefügt. --- brancheneinstufung.py | 73 ++++++++++++++++++++++++++----------------- 1 file changed, 45 insertions(+), 28 deletions(-) diff --git a/brancheneinstufung.py b/brancheneinstufung.py index 2c6df4d4..2327e1e7 100644 --- a/brancheneinstufung.py +++ b/brancheneinstufung.py @@ -61,6 +61,8 @@ from sklearn.impute import SimpleImputer from sklearn.tree import DecisionTreeClassifier, export_text from sklearn.metrics import accuracy_score, classification_report, confusion_matrix import concurrent.futures # Fuer parallele Verarbeitung +from sklearn.model_selection import GridSearchCV +from imblearn.pipeline import Pipeline as ImbPipeline # Alias, um Kollision mit sklearn.pipeline zu vermeiden # Spezifische externe Tools try: @@ -9219,51 +9221,66 @@ class DataProcessor: X_test_imputed = pd.DataFrame(X_test_imputed, columns=feature_columns_ml) # feature_columns_ml verwenden # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - # +++ NEU: Klassen-Balancierung mit SMOTE auf den Trainingsdaten ++++++++ + # +++ ANPASSUNG HIER: GridSearchCV mit Pipeline für SMOTE & RandomForest + # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - self.logger.info("Führe Klassen-Balancierung mit SMOTE auf den Trainingsdaten durch...") - self.logger.info(f"Klassenverteilung VOR SMOTE:\n{y_train.value_counts()}") + # 4. Erstellen einer Pipeline und Definieren des Parameter-Grids - smote = SMOTE(random_state=42) - # Wichtig: SMOTE wird auf die imputierten Trainingsdaten angewendet - X_train_resampled, y_train_resampled = smote.fit_resample(X_train_imputed, y_train) + # Schritt 1: SMOTE für Klassen-Balancierung + # Schritt 2: RandomForestClassifier als Modell + pipeline = ImbPipeline([ + ('smote', SMOTE(random_state=42)), + ('classifier', RandomForestClassifier(random_state=42, n_jobs=-1)) # class_weight nicht nötig bei SMOTE + ]) - self.logger.info(f"Klassenverteilung NACH SMOTE:\n{y_train_resampled.value_counts()}") - self.logger.info(f"Größe des Trainingssets nach Resampling: {len(X_train_resampled)} Zeilen.") - # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - # +++ ENDE SMOTE-BLOCK ++++++++++++++++++++++++++++++++++++++++++++++++++ - # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - - # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - # +++ ANPASSUNG HIER: RandomForest statt Decision Tree ++++++++++++++++++ - # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - # 4. Random Forest Training - # n_estimators=100 ist ein guter Standardwert. - # class_weight='balanced' hilft bei unausgewogenen Klassen. - # n_jobs=-1 nutzt alle verfügbaren CPU-Kerne und kann das Training beschleunigen. - rf_classifier = RandomForestClassifier(n_estimators=100, random_state=42, class_weight='balanced', n_jobs=-1) - - self.logger.info(f"Starte Training des Random Forest Modells auf {X_train_imputed.shape[0]} Trainingsbeispielen...") + # Definieren der Hyperparameter, die getestet werden sollen. + # WICHTIG: Die Parameternamen müssen mit dem Namen des Schritts in der Pipeline beginnen (z.B. 'classifier__...') + param_grid = { + 'classifier__n_estimators': [100, 200], # Anzahl der Bäume + 'classifier__max_depth': [10, 20, None], # Maximale Tiefe der Bäume (None = unbegrenzt) + 'classifier__min_samples_split': [2, 5], # Mindestanzahl Samples für einen Split + 'classifier__min_samples_leaf': [1, 2] # Mindestanzahl Samples in einem Blatt + } + # HINWEIS: Dies sind 2 * 3 * 2 * 2 = 24 Kombinationen. + # Mit cv=3 (siehe unten) werden 24 * 3 = 72 Modelle trainiert. Dies kann dauern! + # Für einen schnellen Test können Sie die Anzahl der Optionen reduzieren. + + # Initialisieren von GridSearchCV + # cv=3 bedeutet 3-fache Kreuzvalidierung. + # scoring='accuracy' bedeutet, dass die beste Kombination anhand der Genauigkeit ausgewählt wird. + # verbose=2 gibt detaillierte Log-Ausgaben während der Suche. + grid_search = GridSearchCV(estimator=pipeline, param_grid=param_grid, cv=3, scoring='accuracy', verbose=2, n_jobs=-1) + + self.logger.info("Starte Hyperparameter-Tuning mit GridSearchCV...") start_fit_time = time.time() - rf_classifier.fit(X_train_imputed, y_train) # << HIER WIRD DER RF TRAINIERT + # Fitte das Grid auf den (noch nicht resampleten) Trainingsdaten. + # Die Pipeline kümmert sich intern darum, dass SMOTE nur auf die Trainings-Folds angewendet wird. + grid_search.fit(X_train_imputed, y_train) end_fit_time = time.time() - self.logger.info(f"Modelltraining abgeschlossen. Dauer: {end_fit_time - start_fit_time:.2f} Sekunden.") + self.logger.info(f"GridSearchCV-Suche abgeschlossen. Dauer: {end_fit_time - start_fit_time:.2f} Sekunden.") + + # Beste Parameter und bestes Modell ausgeben und speichern + self.logger.info(f"Beste gefundene Parameter: {grid_search.best_params_}") + self.logger.info(f"Beste Cross-Validation Accuracy: {grid_search.best_score_:.4f}") + + # Das beste Modell ist das, das mit den besten Parametern auf den *gesamten* Trainingsdaten trainiert wurde. + best_classifier = grid_search.best_estimator_ # Modell speichern - self.model = rf_classifier # Zuweisung des neuen Modells + self.model = best_classifier # Zuweisung des besten gefundenen Modells try: model_dir = os.path.dirname(model_out) if model_dir and not os.path.exists(model_dir): os.makedirs(model_dir, exist_ok=True) with open(model_out, 'wb') as f: - pickle.dump(rf_classifier, f) # Das trainierte rf_classifier Objekt speichern - self.logger.info(f"Random Forest Modell erfolgreich gespeichert in '{model_out}'.") + pickle.dump(best_classifier, f) # Das beste Modell speichern + self.logger.info(f"Bestes RandomForest Modell erfolgreich gespeichert in '{model_out}'.") except Exception as e: - self.logger.error(f"FEHLER beim Speichern des Modells in '{model_out}': {e}") + self.logger.error(f"FEHLER beim Speichern des besten Modells in '{model_out}': {e}") # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # +++ ENDE ANPASSUNG ++++++++++++++++++++++++++++++++++++++++++++++++++++ # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + # Feature-Liste speichern (bleibt unverändert) self._expected_features = feature_columns_ml try: