From c6ad3a213a73cd390eccf1d657ea8e1a0639d2f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D1=91=D0=BC=20=D0=90=D0=BB=D0=B5=D0=B9?= =?UTF-8?q?=D0=BA=D0=B8=D0=BD?= Date: Wed, 30 Oct 2024 01:16:58 +0400 Subject: [PATCH] Predicted progress! From "very bad" to just "bad". --- controllers/controller.py | 8 +- main.py | 10 +++ services/ml/generate_synthetic_data.py | 96 ++++++++------------ services/ml/modelBuilder.py | 119 ++++++++++++------------- services/service.py | 22 ++++- 5 files changed, 123 insertions(+), 132 deletions(-) diff --git a/controllers/controller.py b/controllers/controller.py index 219b23b..fa640c0 100644 --- a/controllers/controller.py +++ b/controllers/controller.py @@ -6,9 +6,11 @@ import os router = APIRouter() # Инициализация сервиса -MODEL_PATH = os.getenv("MODEL_PATH", "services/laptop_price_model.pkl") -FEATURE_COLUMNS_PATH = os.getenv("FEATURE_COLUMNS_PATH", "services/feature_columns.pkl") -laptop_service = LaptopService(model_path=MODEL_PATH, feature_columns_path=FEATURE_COLUMNS_PATH) +MODEL_PATH = os.getenv("MODEL_PATH", "services/ml/laptop_price_model.pkl") +FEATURE_COLUMNS_PATH = os.getenv("FEATURE_COLUMNS_PATH", "services/ml/feature_columns.pkl") +POLY_PATH = os.getenv("POLY_PATH", "services/ml/poly_transformer.pkl") +SCALER_PATH = os.getenv("SCALER_PATH", "services/ml/scaler.pkl") +laptop_service = LaptopService(model_path=MODEL_PATH, feature_columns_path=FEATURE_COLUMNS_PATH, poly_path=POLY_PATH, scaler_path=SCALER_PATH) @router.post("/predict_price/", response_model=PredictPriceResponse, summary="Predict laptop price", description="Predict the price of a laptop based on its specifications.", response_description="The predicted price of the laptop.") def predict_price(data: LaptopCreate): diff --git a/main.py b/main.py index a980e41..33c4166 100644 --- a/main.py +++ b/main.py @@ -1,7 +1,17 @@ from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware from controllers import controller app = FastAPI() +# Настройка CORS +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # Замените на список допустимых доменов, например: ["http://localhost:8080"] + allow_credentials=True, + allow_methods=["*"], # Разрешить все методы (GET, POST, PUT и т.д.) + allow_headers=["*"], # Разрешить все заголовки +) + # Подключение маршрутов app.include_router(controller.router) diff --git a/services/ml/generate_synthetic_data.py b/services/ml/generate_synthetic_data.py index f21e2c6..92dd818 100644 --- a/services/ml/generate_synthetic_data.py +++ b/services/ml/generate_synthetic_data.py @@ -74,81 +74,55 @@ def generate_release_year(): def calculate_price(brand, processor, ram, os, ssd, display, gpu, weight, battery_size, release_year, display_type): base_price = 30000 # базовая цена в условных единицах - # Добавление стоимости в зависимости от бренда + # Бренд brand_premium = { - 'Apple': 40000, - 'MSI': 35000, - 'Dell': 15000, - 'HP': 12000, - 'Lenovo': 10000, - 'Microsoft': 18000, - 'Asus': 8000, - 'Acer': 7000, - 'Samsung': 9000, - 'Toshiba': 8500 + 'Apple': 40000, 'MSI': 35000, 'Dell': 15000, 'HP': 12000, 'Lenovo': 10000, + 'Microsoft': 18000, 'Asus': 8000, 'Acer': 7000, 'Samsung': 9000, 'Toshiba': 8500 } - base_price += brand_premium.get(brand, 10000) # дефолтный премиум + base_price += brand_premium.get(brand, 10000) - # Добавление стоимости в зависимости от процессора - if 'i3' in processor or 'Ryzen 3' in processor: - base_price += 5000 - elif 'i5' in processor or 'Ryzen 5' in processor: - base_price += 10000 - elif 'i7' in processor or 'Ryzen 7' in processor: - base_price += 15000 - - # Добавление стоимости за RAM - base_price += ram * 2000 # 2000 условных единиц за каждый GB RAM - - # Добавление стоимости за ОС - if os == 'Windows 11': - base_price += 5000 - elif os == 'macOS': - base_price += 15000 - elif os == 'Linux': - base_price += 3000 - # Windows 10 считается стандартной и не добавляет стоимости - - # Добавление стоимости за SSD - if ssd > 0: - base_price += ssd * 100 # 100 условных единиц за каждый GB SSD - - # Добавление стоимости за размер дисплея - base_price += (display - 13) * 5000 # 5000 условных единиц за каждый дюйм больше 13" - - # Добавление стоимости за тип дисплея - display_type_premium = { - 'HD': 0, - 'Full HD': 5000, - '4K': 15000, - 'OLED': 20000 + # Процессор + processor_premium = { + 'Intel Core i3': 5000, 'Intel Core i5': 10000, 'Intel Core i7': 15000, + 'AMD Ryzen 3': 5000, 'AMD Ryzen 5': 10000, 'AMD Ryzen 7': 15000 } + for key, value in processor_premium.items(): + if key in processor: + base_price += value + break + + # RAM - уменьшаем его коэффициент + base_price += ram * 1000 + + # SSD - также уменьшаем его коэффициент + base_price += ssd * 50 + + # Дисплей + base_price += (display - 13) * 5000 + + # Тип дисплея + display_type_premium = {'HD': 0, 'Full HD': 12000, '4K': 30000, 'OLED': 35000} base_price += display_type_premium.get(display_type, 0) - # Добавление стоимости за GPU - gpu_premium = { - 'Integrated': 0, - 'NVIDIA GeForce GTX 1650': 15000, - 'NVIDIA GeForce RTX 3060': 25000, - 'AMD Radeon RX 5600M': 20000 - } + # GPU + gpu_premium = {'Integrated': 0, 'NVIDIA GeForce GTX 1650': 25000, 'NVIDIA GeForce RTX 3060': 40000, 'AMD Radeon RX 5600M': 35000} base_price += gpu_premium.get(gpu, 0) - # Добавление стоимости за вес (легкие ноутбуки дороже) - base_price += (3.0 - weight) * 5000 # Чем легче, тем дороже + # Вес + base_price += (3.0 - weight) * 8000 # Чем легче, тем дороже - # Добавление стоимости за размер батареи - base_price += battery_size * 100 # 100 условных единиц за каждый Вт⋅ч батареи + # Батарея + base_price += battery_size * 250 - # Добавление стоимости за год выпуска (новые модели дороже) + # Год выпуска current_year = datetime.now().year - base_price += (current_year - release_year) * 2000 # 2000 условных единиц за каждый год назад + base_price += (current_year - release_year) * 5000 - # Добавление случайного шума для реалистичности - noise = np.random.normal(0, 5000) # среднее 0, стандартное отклонение 5000 + # Добавление случайного шума + noise = np.random.normal(0, 5000) # Шум для увеличения разброса final_price = base_price + noise - return max(round(final_price, 2), 5000) # минимальная цена 5000 условных единиц + return max(round(final_price, 2), 5000) # Функция для генерации синтетических данных diff --git a/services/ml/modelBuilder.py b/services/ml/modelBuilder.py index e443556..28644cf 100644 --- a/services/ml/modelBuilder.py +++ b/services/ml/modelBuilder.py @@ -1,23 +1,17 @@ import pandas as pd -from sklearn.model_selection import train_test_split -from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor -from sklearn.linear_model import LinearRegression +from sklearn.model_selection import train_test_split, GridSearchCV +from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score +from sklearn.preprocessing import PolynomialFeatures, StandardScaler import matplotlib.pyplot as plt import joblib import numpy as np # Шаг 1: Загрузка данных -df = pd.read_csv('../../datasets/synthetic_laptops.csv') # Убедитесь, что путь к файлу правильный +df = pd.read_csv('../../datasets/synthetic_laptops.csv') # Шаг 2: Проверка и очистка имен столбцов -print("Имена столбцов до очистки:") -print(df.columns.tolist()) - -# Приведение имен столбцов к нижнему регистру и удаление пробелов df.columns = df.columns.str.strip().str.lower() -print("\nИмена столбцов после очистки:") -print(df.columns.tolist()) # Шаг 3: Проверка наличия необходимых столбцов required_columns = [ @@ -25,20 +19,13 @@ required_columns = [ 'gpu', 'weight', 'battery_size', 'release_year', 'display_type', 'price' ] missing_columns = [col for col in required_columns if col not in df.columns] - if missing_columns: - print(f"\nОтсутствуют следующие столбцы: {missing_columns}") raise Exception(f"Отсутствуют столбцы: {missing_columns}") -else: - print("\nВсе необходимые столбцы присутствуют.") # Шаг 4: Удаление строк с пропущенными значениями df = df.dropna(subset=required_columns) -print(f"\nКоличество строк после удаления пропусков: {df.shape[0]}") # Шаг 5: Очистка и преобразование колонок - -# Функция для очистки числовых колонок, если они строковые def clean_numeric_column(column, remove_chars=['₹', ',', ' ']): if column.dtype == object: for char in remove_chars: @@ -47,77 +34,81 @@ def clean_numeric_column(column, remove_chars=['₹', ',', ' ']): else: return column -# Очистка числовых колонок (исключая 'price', если уже числовая) numerical_columns = ['ram', 'ssd', 'display', 'weight', 'battery_size', 'release_year'] for col in numerical_columns: df[col] = clean_numeric_column(df[col]) -# Проверка на пропущенные значения после очистки df = df.dropna(subset=['price'] + numerical_columns) -print(f"\nКоличество строк после очистки числовых колонок: {df.shape[0]}") -# Шаг 6: Выбор необходимых столбцов (все уже включены) -# df уже содержит все необходимые столбцы - -print("\nПример данных после предобработки:") -print(df.head()) - -# Шаг 7: Преобразование категориальных переменных с помощью One-Hot Encoding +# Шаг 6: Преобразование категориальных переменных с помощью One-Hot Encoding categorical_features = ['brand', 'processor', 'os', 'gpu', 'display_type'] df = pd.get_dummies(df, columns=categorical_features, drop_first=True) -print("\nИмена колонок после One-Hot Encoding:") -print(df.columns.tolist()) -# Шаг 8: Разделение данных на обучающую и тестовую выборки +# Шаг 7: Разделение данных на X и y X = df.drop('price', axis=1) y = df['price'] -X_train, X_test, y_train, y_test = train_test_split( - X, y, test_size=0.2, random_state=42 -) +# Шаг 8: Создание полиномиальных и интерактивных признаков степени 2 +poly = PolynomialFeatures(degree=2, interaction_only=True, include_bias=False) +X_poly = poly.fit_transform(X) -print(f"\nРазмер обучающей выборки: {X_train.shape}") -print(f"Размер тестовой выборки: {X_test.shape}") +# Шаг 9: Масштабирование признаков +scaler = StandardScaler() +X_poly_scaled = scaler.fit_transform(X_poly) -# Шаг 9: Обучение моделей -model_rf = RandomForestRegressor(n_estimators=100, random_state=42) -model_rf.fit(X_train, y_train) +# Шаг 10: Разделение на обучающую и тестовую выборки +X_train, X_test, y_train, y_test = train_test_split(X_poly_scaled, y, test_size=0.5, random_state=42) -print("\nМодели успешно обучены.") - -# Шаг 10: Оценка моделей -models = { - 'Random Forest': model_rf, +# Шаг 11: Настройка гиперпараметров с использованием GridSearchCV +param_grid = { + 'n_estimators': [100, 200], + 'max_depth': [10, 20], + 'max_features': ['sqrt', 'log2', 0.5], + 'min_samples_split': [5, 10], + 'min_samples_leaf': [2, 4] } -for name, mdl in models.items(): - y_pred = mdl.predict(X_test) - mae = mean_absolute_error(y_test, y_pred) - rmse = mean_squared_error(y_test, y_pred, squared=False) - r2 = r2_score(y_test, y_pred) - print(f"{name} - MAE: {mae}, RMSE: {rmse}, R²: {r2}") +grid_search = GridSearchCV(RandomForestRegressor(random_state=42), param_grid, cv=3, scoring='neg_mean_absolute_error') +grid_search.fit(X_train, y_train) -# Шаг 11: Сохранение модели и списка признаков -joblib.dump(model_rf, 'laptop_price_model.pkl') -print("\nМодель Random Forest сохранена как 'laptop_price_model.pkl'.") +# Лучшая модель +best_model = grid_search.best_estimator_ +# Шаг 12: Предсказания и оценка +y_pred = best_model.predict(X_test) +mae = mean_absolute_error(y_test, y_pred) +rmse = mean_squared_error(y_test, y_pred, squared=False) +r2 = r2_score(y_test, y_pred) +print(f"Лучшие параметры: {grid_search.best_params_}") +print(f"Random Forest - MAE: {mae}, RMSE: {rmse}, R²: {r2}") + +# Шаг 13: Сохранение модели feature_columns = X.columns.tolist() joblib.dump(feature_columns, 'feature_columns.pkl') -print("Сохранены названия признаков в 'feature_columns.pkl'.") +joblib.dump(best_model, 'laptop_price_model.pkl') +joblib.dump(poly, 'poly_transformer.pkl') +joblib.dump(scaler, 'scaler.pkl') +print("Модель, трансформер и скейлер сохранены.") -# Получение важности признаков -importances = model_rf.feature_importances_ +# Шаг 14: Важность признаков +# Количество признаков, которые нужно отобразить +top_n = 15 + +# Важность признаков +importances = best_model.feature_importances_ indices = np.argsort(importances)[::-1] -# Вывод наиболее важных признаков -print("Важность признаков:") -for f in range(X_train.shape[1]): - print(f"{f + 1}. {feature_columns[indices[f]]} ({importances[indices[f]]})") +# Отображаем только топ-N признаков +top_indices = indices[:top_n] +top_importances = importances[top_indices] +top_features = np.array(poly.get_feature_names_out())[top_indices] -# Визуализация важности признаков +# Построение графика plt.figure(figsize=(12, 8)) -plt.title("Важность признаков (Random Forest)") -plt.bar(range(X_train.shape[1]), importances[indices], align='center') -plt.xticks(range(X_train.shape[1]), [feature_columns[i] for i in indices], rotation=90) +plt.title(f"Топ-{top_n} признаков по важности (Random Forest)") +plt.bar(range(top_n), top_importances, align='center') +plt.xticks(range(top_n), top_features, rotation=45, ha='right') +plt.xlabel("Признаки") +plt.ylabel("Важность") plt.tight_layout() -plt.show() +plt.show() \ No newline at end of file diff --git a/services/service.py b/services/service.py index c71f023..7fe0ad5 100644 --- a/services/service.py +++ b/services/service.py @@ -4,7 +4,7 @@ from typing import List, Dict from schemas.schemas import LaptopCreate, LaptopResponse, PredictPriceResponse class LaptopService: - def __init__(self, model_path: str, feature_columns_path: str): + def __init__(self, model_path: str, feature_columns_path: str, poly_path: str, scaler_path: str): try: self.model = joblib.load(model_path) except FileNotFoundError: @@ -19,6 +19,14 @@ class LaptopService: except Exception as e: raise Exception(f"Error loading feature columns: {str(e)}") + try: + self.poly_transformer = joblib.load(poly_path) + self.scaler = joblib.load(scaler_path) + except FileNotFoundError: + raise Exception("Polynomial transformer or scaler file not found.") + except Exception as e: + raise Exception(f"Error loading polynomial transformer or scaler: {str(e)}") + def predict_price(self, data: Dict[str, any]) -> PredictPriceResponse: # Преобразование данных в DataFrame input_df = pd.DataFrame([data]) @@ -26,15 +34,21 @@ class LaptopService: # Применение One-Hot Encoding к категориальным признакам input_df = pd.get_dummies(input_df, columns=['processor', 'os'], drop_first=True) - # Добавление отсутствующих признаков, если они есть + # Добавление отсутствующих признаков for col in self.feature_columns: if col not in input_df.columns and col != 'price': input_df[col] = 0 - # Упорядочивание колонок согласно обучающей выборке + # Упорядочивание колонок input_df = input_df[self.feature_columns] + # Преобразование с использованием PolynomialFeatures + input_poly = self.poly_transformer.transform(input_df) + + # Масштабирование данных + input_scaled = self.scaler.transform(input_poly) + # Предсказание цены - predicted_price = self.model.predict(input_df)[0] + predicted_price = self.model.predict(input_scaled)[0] return PredictPriceResponse(predicted_price=round(predicted_price, 2))