Merge pull request #204 from Hestia-Homes/main

Completed docker build, testing new github actions process
This commit is contained in:
KhalimCK 2023-09-01 18:05:53 +01:00 committed by GitHub
commit c90b00dd55
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 359 additions and 113 deletions

View file

@ -17,7 +17,7 @@ jobs:
run: | run: |
ls ls
cd model_data/simulation_system cd model_data/simulation_system
pip install -r requirements.txt pip install -r requirements/training/training.txt
python3 training.py --train-filepath ./model_build_data/change_data/rdsap_full/train_validation_data.parquet --test-filepath ./model_build_data/change_data/rdsap_full/test_data.parquet python3 training.py --train-filepath ./model_build_data/change_data/rdsap_full/train_validation_data.parquet --test-filepath ./model_build_data/change_data/rdsap_full/test_data.parquet
cd model_directory/RDSAP_CHANGE cd model_directory/RDSAP_CHANGE

View file

@ -64,6 +64,7 @@ jobs:
tags: ${{ steps.set_ecr_credentials.outputs.ecr_uri }}:${{ github.sha }} tags: ${{ steps.set_ecr_credentials.outputs.ecr_uri }}:${{ github.sha }}
cache-from: type=gha cache-from: type=gha
cache-to: type=gha,mode=max cache-to: type=gha,mode=max
platform: linux/amd64
- name: Deploy to AWS Lambda via Serverless - name: Deploy to AWS Lambda via Serverless
env: env:

View file

@ -13,6 +13,7 @@ from numpy import ndarray
from pathlib import Path from pathlib import Path
from typing import Protocol, NamedTuple, Any from typing import Protocol, NamedTuple, Any
import pandas as pd import pandas as pd
from training import S3FSClient
class MLModel(Protocol): class MLModel(Protocol):
@ -25,7 +26,9 @@ class MLModel(Protocol):
Providing a path, this function will load the model to be used. Will load to internal variable Providing a path, this function will load the model to be used. Will load to internal variable
""" """
def save_model(self, output_filepath: Path) -> None: def save_model(
self, output_filepath: Path, s3_client: S3FSClient | None = None
) -> None:
""" """
Providing a path, this function will save the model to be used. Providing a path, this function will save the model to be used.
""" """

View file

@ -12,11 +12,10 @@ from typing import Any
from pathlib import Path from pathlib import Path
import pandas as pd import pandas as pd
from autogluon.tabular import TabularDataset, TabularPredictor from autogluon.tabular import TabularDataset, TabularPredictor
from sklearn.metrics import mean_absolute_percentage_error
from core.Logger import logger from core.Logger import logger
from core.Metrics import Metrics from core.Metrics import Metrics
from core.Settings import METRIC_FILENAME from core.Settings import METRIC_FILENAME
from MLModel.BaseMLModel import MLModel from core.CloudClient import S3FSClient
AUTOGLUON_HYPERPARAMETERS = [ AUTOGLUON_HYPERPARAMETERS = [
"problem_type", "problem_type",
@ -52,19 +51,36 @@ class AutogluonModel:
self.output_filepath = output_filepath self.output_filepath = output_filepath
self.predictions = None self.predictions = None
def load_model(self, filepath: str | Path) -> None: def load_model(
self,
filepath: str | Path,
s3_client: S3FSClient,
model_folder: str = "local_model",
) -> None:
""" """
Providing a path, this function will load the model to be used. Will load to internal variable Providing a path, this function will load the model to be used. Will load to internal variable
""" """
filepath = str(filepath) filepath = str(filepath)
if s3_client.client is None:
logger.info("In local development mode - no need for s3 client")
self.model = TabularPredictor.load(path=filepath)
else:
logger.info(f"Loading model from s3")
s3_client.download_model(filepath=filepath, model_folder=model_folder)
self.model = TabularPredictor.load(path=model_folder)
self.model = TabularPredictor.load(path=filepath) def save_model(self, output_filepath: Path, s3fs_client: S3FSClient) -> None:
def save_model(self, output_filepath: Path | None = None) -> None:
""" """
Providing a path, this function will save the model to be used. Providing a path, this function will save the model to be used.
""" """
logger.info("Using AutoGluon Model - Model saving already occured") if s3fs_client.client is None:
logger.info("In local development mode - no need for s3 client")
logger.info("Using AutoGluon Model - Model saving already occured")
else:
logger.info(f"Saving model into s3")
s3_location = s3fs_client.model_bucket + "/" + str(output_filepath)
s3fs_client.client.put(str(output_filepath), s3_location, recursive=True)
logger.info("Save complete")
def train_model( def train_model(
self, data: pd.DataFrame, target_column: str, hyperparameters: dict self, data: pd.DataFrame, target_column: str, hyperparameters: dict

View file

@ -0,0 +1,95 @@
"""
Set up the client to be used for downloading and uploading model files
"""
import os
import s3fs
from core.Logger import logger
class S3FSClient:
"""
Set up the correct client to upload files to s3
"""
def __init__(self, runtime_environment: str = "local") -> None:
self.client: s3fs.S3FileSystem | None = None
self.model_bucket: str
self.client_factory(runtime_environment)
self.determine_model_bucket(runtime_environment)
def client_factory(self, runtime_environment: str = "local"):
"""
Select the correct s3 client to use
"""
if runtime_environment == "local":
logger.info("No S3 client setup required")
elif runtime_environment == "local-mock":
logger.info(f"S3 settings for {runtime_environment}")
self.client = s3fs.S3FileSystem(
key=os.environ.get("AWS_ACCESS_KEY_ID", "admin"),
secret=os.environ.get("AWS_SECRET_ACCESS_KEY", "password"),
client_kwargs={
"endpoint_url": os.environ.get(
"ENDPOINT_URL", "http://localhost:9000"
)
},
)
elif runtime_environment in ["dev", "staging", "prod"]:
logger.info(f"S3 settings for {runtime_environment}")
# Key/ token should be in session/lambda for this
self.client = s3fs.S3FileSystem()
else:
raise NotImplementedError("No correspnding runtime environment")
def determine_model_bucket(self, runtime_environment: str) -> None:
"""
For the given environment, return the correct bucket for models
"""
if runtime_environment == "local":
logger.info("In local development - no need for s3")
elif runtime_environment in ["local-mock", "dev"]:
# TODO: get from enironment
self.model_bucket = "retrofit-model-directory-dev"
elif runtime_environment in ["staging", "prod"]:
self.model_bucket = f"retrofit-model-directory-{runtime_environment}"
else:
raise NotImplementedError("No corresponding runtime environment")
def download_model(self, filepath: str, model_folder: str):
"""
For the file path, download the model locally so that we can load the model
"""
if self.client is None:
logger.info("No need to download model as local development")
else:
def list_files_recursively(folder_path, client):
all_files = []
for root, dirs, files in client.walk(folder_path):
for file in files:
s3_path = os.path.join(root, file)
all_files.append(s3_path)
return all_files
# List all files in the specified S3 folder and its subfolders
files = list_files_recursively(
f"{self.model_bucket}/{filepath}", client=self.client
)
# Download each file
for file in files:
# Extract the filename from the S3 path
filename = file.split(filepath)[-1]
# Define the local path where you want to save the file
local_path = os.path.join("local_model", filename)
# Download the file from S3 to the local directory
self.client.get(file, local_path)
print(f"Downloaded {filename} to {local_path}")
print("Download completed.")

View file

@ -1,6 +1,54 @@
import pandas as pd import pandas as pd
import os import os
from typing import Protocol from typing import Protocol
import boto3
from io import BytesIO, StringIO
def read_parquet_from_s3(bucket_name, file_key):
"""
Read a CSV file from S3 using boto3 and pandas.
:param bucket_name: Name of the S3 bucket.
:param file_key: Key of the file (including directory path within the bucket).
:param aws_access_key_id: AWS Access Key ID
:param aws_secret_access_key: AWS Secret Access Key
:return: DataFrame containing the CSV data.
"""
# Initialize the S3 client
s3_client = boto3.client("s3")
# Get the object
s3_object = s3_client.get_object(Bucket=bucket_name, Key=file_key)
# Read the CSV body into a DataFrame
csv_body = s3_object["Body"].read()
df = pd.read_parquet(BytesIO(csv_body))
return df
def read_csv_from_s3(bucket_name, file_key, index_col):
"""
Read a CSV file from S3 using boto3 and pandas.
:param bucket_name: Name of the S3 bucket.
:param file_key: Key of the file (including directory path within the bucket).
:param aws_access_key_id: AWS Access Key ID
:param aws_secret_access_key: AWS Secret Access Key
:return: DataFrame containing the CSV data.
"""
# Initialize the S3 client
s3_client = boto3.client("s3")
# Get the object
s3_object = s3_client.get_object(Bucket=bucket_name, Key=file_key)
# Read the CSV body into a DataFrame
csv_body = s3_object["Body"].read().decode("utf-8")
df = pd.read_csv(StringIO(csv_body), index_col=index_col)
return df
class DataLoader(Protocol): class DataLoader(Protocol):
@ -55,6 +103,9 @@ class S3MockDataLoader:
}, },
} }
if not filepath.startswith("s3://"):
filepath = "s3://" + filepath
if filepath.endswith(".parquet"): if filepath.endswith(".parquet"):
df = pd.read_parquet(filepath, storage_options=storage_options) df = pd.read_parquet(filepath, storage_options=storage_options)
if index_col is not None: if index_col is not None:
@ -76,20 +127,15 @@ class S3DataLoader:
@staticmethod @staticmethod
def load(filepath: str, index_col: str | None = None) -> pd.DataFrame: def load(filepath: str, index_col: str | None = None) -> pd.DataFrame:
filepath_split = filepath.split("s3://")[-1].split("/", 1)
storage_options = { bucket = filepath_split[0]
"key": os.environ.get("AWS_ACCESS_KEY_ID"), key = filepath_split[1]
"secret": os.environ.get("AWS_SECRET_ACCESS_KEY"),
}
if filepath.endswith(".parquet"): if filepath.endswith(".parquet"):
df = pd.read_parquet(filepath, storage_options=storage_options) df = read_parquet_from_s3(bucket, key)
if index_col is not None: if index_col is not None:
df = df.set_index(index_col) df = df.set_index(index_col)
elif filepath.endswith(".csv"): elif filepath.endswith(".csv"):
df = pd.read_csv( df = read_csv_from_s3(bucket, key, index_col)
filepath, index_col=index_col, storage_options=storage_options
)
else: else:
raise ValueError(f"File format not supported for file: {filepath}") raise ValueError(f"File format not supported for file: {filepath}")

View file

@ -9,6 +9,8 @@ import pandas as pd
from pathlib import Path from pathlib import Path
import seaborn as sns import seaborn as sns
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
from core.CloudClient import S3FSClient
from core.Logger import logger
from core.Settings import ( from core.Settings import (
RESIDUAL_TRUE_LABEL, RESIDUAL_TRUE_LABEL,
RESIDUAL_PREDICTION_LABEL, RESIDUAL_PREDICTION_LABEL,
@ -62,6 +64,18 @@ class Metrics:
All metric functions used to generate a dictionary of metrics All metric functions used to generate a dictionary of metrics
""" """
def upload_metrics(self, output_filepath: Path, s3fs_client: S3FSClient) -> None:
"""
Providing a path, this function will save the metrics folders/files.
"""
if s3fs_client.client is None:
logger.info("In local development mode - no need to upload")
else:
logger.info(f"Saving metrics into s3")
s3_location = s3fs_client.model_bucket + "/" + str(output_filepath)
s3fs_client.client.put(str(output_filepath), s3_location, recursive=True)
logger.info("Save complete")
@staticmethod @staticmethod
def list_metric_functions() -> list: def list_metric_functions() -> list:
""" """

View file

@ -0,0 +1,83 @@
"""
"""
import pandas as pd
from pathlib import Path
from core.Logger import logger
from core.CloudClient import S3FSClient
from core.Metrics import Metrics
from core.Settings import BEST_MODEL_COLUMN_NAME
class RegistryHandler:
"""
Handles the loading of the registry depending on the environment
"""
def load_registry(
self, registry_path: Path, s3fs_client: S3FSClient, metrics: Metrics
):
"""
Depening on the environment, we will have to load from locally or s3 (mock/real)
"""
if s3fs_client.client is None:
logger.info("Using local development - no need for s3 load")
return self.load_local_registry(
registry_path=registry_path, metrics=metrics
)
s3_location = "s3://" + s3fs_client.model_bucket + "/" + str(registry_path)
logger.info(f"Check if registry exists")
if s3fs_client.client.exists(s3_location):
registry_df = pd.read_csv(
s3fs_client.client.open(s3_location), index_col=None
)
else:
logger.info("No registry found - creating new one")
registry_df = self.create_new_registry(metrics=metrics)
return registry_df
def load_local_registry(self, registry_path: Path, metrics: Metrics):
"""
In local development mode, load the registry
"""
if registry_path.exists():
logger.info("Registry file found - Loading into Dataframe")
registry_df = pd.read_csv(registry_path, index_col=None)
else:
logger.info("No registry found - creating new one")
registry_df = self.create_new_registry(metrics=metrics)
return registry_df
def create_new_registry(self, metrics: Metrics):
"""
If no registry is found, create a new one
"""
# TODO: Moved columns into settings: MODEL_DETAILS and Metrics class columns
columns = [
BEST_MODEL_COLUMN_NAME,
"model_type",
"model_name",
"model_location",
] + metrics.list_metric_functions()
registry_df = pd.DataFrame(columns=columns)
return registry_df
def save_registry(self, output_filepath: Path, s3fs_client: S3FSClient) -> None:
"""
Providing a path, this function will save the model to be used.
"""
if s3fs_client.client is None:
logger.info("In local development mode - no need for s3 client")
else:
logger.info(f"Saving registry into s3")
s3_location = s3fs_client.model_bucket + "/" + str(output_filepath)
s3fs_client.client.put(str(output_filepath), s3_location, recursive=True)
logger.info("Save complete")

View file

@ -26,7 +26,7 @@ MODEL_HYPERPARAMETERS = {
"autogluon": { "autogluon": {
"problem_type": "regression", "problem_type": "regression",
"eval_metric": "mean_absolute_error", "eval_metric": "mean_absolute_error",
"time_limit": 30, "time_limit": 45,
"presets": "medium_quality", "presets": "medium_quality",
"excluded_model_types": None, "excluded_model_types": None,
} }

View file

@ -18,19 +18,19 @@ services:
timeout: 20s timeout: 20s
retries: 3 retries: 3
simulation_system_training: # simulation_system_training:
build: # build:
context: ./ # context: ./
dockerfile: ./Dockerfiles/Dockerfile.training # dockerfile: ./Dockerfiles/Dockerfile.training
image: simulation_system_training # image: simulation_system_training
environment: # environment:
ENDPOINT_URL: http://minio:9000/ # ENDPOINT_URL: http://minio:9000/
AWS_ACCESS_KEY_ID: *MINIO_USER # AWS_ACCESS_KEY_ID: *MINIO_USER
AWS_SECRET_ACCESS_KEY: *MINIO_PASS # AWS_SECRET_ACCESS_KEY: *MINIO_PASS
tty: true # tty: true
depends_on: # depends_on:
minio: # minio:
condition: service_healthy # condition: service_healthy
# command: # command:
# ["bash"] # ["bash"]

View file

@ -13,35 +13,25 @@ def handler(event, context):
# Assuming a file in a bucket landing for now? # Assuming a file in a bucket landing for now?
# Assuming we have a model to use # Assuming we have a model to use
# bucket = event["Records"][0]["s3"]["bucket"]["name"]
# key = urllib.parse.unquote_plus(
# event["Records"][0]["s3"]["bucket"]["key"], encoding="utf-8"
# )
payload = event["body"] payload = event["body"]
data_path = payload["file_location"] data_path = payload["file_location"]
property_id = payload["property_id"] property_id = payload["property_id"]
portfolio_id = payload["portfolio_id"] portfolio_id = payload["portfolio_id"]
created_at = payload["created_at"] created_at = payload["created_at"]
# prediction_file = bucket + "/" + key
# TODO: put a model into s3, both locally and in aws
# model_path = os.environ.get("MODEL_PATH", "http://minio:9000/data/model_directory/")
model_path = os.environ.get(
"MODEL_PATH",
f"s3://retrofit-model-directory-{RUNTIME_ENVIRONMENT}/RDSAP_CHANGE/autogluon/rdsap_change-medium_quality-30"
"-2023-08-30_11-43-41/deployment/",
)
try: try:
outputs = prediction(model_path=model_path, data_path=data_path) # We could fix the model path but for the moment, we just take the best model path based on the registry
outputs = prediction(model_path=None, data_path=data_path)
# Store into s3, with key of {portfolio_id}-{property_id} # Store into s3, with key of {portfolio_id}-{property_id}
outputs.to_csv(
f"s3://retrofit-sap-prediction-{RUNTIME_ENVIRONMENT}/{portfolio_id}/{property_id}/{created_at}.csv"
)
except (Exception, KeyError, ValueError): storage_filepath = f"s3://retrofit-sap-predictions-{RUNTIME_ENVIRONMENT}/{portfolio_id}/{property_id}/" \
f"{created_at}.csv"
outputs.to_csv(storage_filepath)
return storage_filepath
except (Exception, KeyError, ValueError) as e:
print("Prediction failed") print("Prediction failed")
print(e)
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -11,6 +11,7 @@ from datetime import datetime
from MLModel.Models import AutogluonModel from MLModel.Models import AutogluonModel
from core.Logger import logger from core.Logger import logger
from core.DataLoader import dataloader_factory from core.DataLoader import dataloader_factory
from core.CloudClient import S3FSClient
from core.Settings import ( from core.Settings import (
BASE_REGISTRY_PATH, BASE_REGISTRY_PATH,
REGISTRY_FILE, REGISTRY_FILE,
@ -23,6 +24,8 @@ from core.Settings import (
TIMESTAMP = datetime.now().strftime(TIMESTAMP_FORMAT) TIMESTAMP = datetime.now().strftime(TIMESTAMP_FORMAT)
RUNTIME_ENVIRONMENT = os.environ.get("RUNTIME_ENVIRONMENT", "dev") RUNTIME_ENVIRONMENT = os.environ.get("RUNTIME_ENVIRONMENT", "dev")
CLIENT = S3FSClient(runtime_environment=RUNTIME_ENVIRONMENT)
# FOR TESTING # FOR TESTING
# For now just loading data first and then passing into function (i.e. as if we receive json data and convert to # For now just loading data first and then passing into function (i.e. as if we receive json data and convert to
@ -30,6 +33,11 @@ RUNTIME_ENVIRONMENT = os.environ.get("RUNTIME_ENVIRONMENT", "dev")
# TEST_DATA = DataLoader.load(filepath="../simulation_system/model_build_data/change_data/rdsap_full/test_data.parquet") # TEST_DATA = DataLoader.load(filepath="../simulation_system/model_build_data/change_data/rdsap_full/test_data.parquet")
# DATA = TEST_DATA.sample(1) # DATA = TEST_DATA.sample(1)
# For testing in dev s3
# Data path can be passed as so:
# python3 predictions.py --data-path s3://retrofit-data-dev/model_build_data/change_data/rdsap_full/test_data.parquet
# data_path="s3://retrofit-data-dev/model_build_data/change_data/rdsap_full/test_data.parquet"
def ingest_arguments() -> argparse.Namespace: def ingest_arguments() -> argparse.Namespace:
""" """
@ -76,9 +84,7 @@ def prediction(
logger.error("No registry path provided or registry doesn't exist") logger.error("No registry path provided or registry doesn't exist")
exit(1) exit(1)
elif RUNTIME_ENVIRONMENT == "dev": elif RUNTIME_ENVIRONMENT == "dev":
registry_path = ( registry_path = "s3://retrofit-model-directory-dev/model_directory/RDSAP_CHANGE/model_registry.csv"
"s3://retrofit-model-directory-dev/model_directory/RDSAP_CHANGE/model_registry.csv"
)
else: else:
raise NotImplemented("TO be implemented") raise NotImplemented("TO be implemented")
@ -131,11 +137,14 @@ def prediction(
logger.error("No other model currently") logger.error("No other model currently")
exit(1) exit(1)
model.load_model(filepath=model_location) model.load_model(
filepath=model_location, s3_client=CLIENT, model_folder="local_model"
)
logger.info("--- Generating Predictions ---") logger.info("--- Generating Predictions ---")
prediction = model.generate_predictions(data=data) prediction = model.generate_predictions(data=data)
return pd.concat([data["id"], prediction], axis=1)
return pd.concat([pd.Series(data.index, name='id'), prediction], axis=1)
# Save prediction some where? # Save prediction some where?
# prediction.to_csv("s3?") # prediction.to_csv("s3?")

View file

@ -1,3 +1,4 @@
autogluon==0.8.2 autogluon==0.8.2
pandas==1.5.3 pandas==1.5.3
s3fs==2023.6.0
pre-commit==3.3.3 pre-commit==3.3.3

View file

@ -1,5 +1,6 @@
boto3
autogluon==0.8.2 autogluon==0.8.2
pandas==1.5.3 pandas==1.5.3
s3fs==2023.6.0 s3fs
seaborn==0.12.2 seaborn==0.12.2
matplotlib==3.7.2 matplotlib==3.7.2

View file

@ -1,4 +1,5 @@
autogluon==0.8.2 autogluon==0.8.2
pandas==1.5.3 pandas==1.5.3
seaborn==0.12.2 seaborn==0.12.2
s3fs==2023.6.0
pre-commit==3.3.3 pre-commit==3.3.3

View file

@ -1,3 +1,4 @@
autogluon==0.8.2 autogluon==0.8.2
pandas==1.5.3 pandas==1.5.3
seaborn==0.12.2 seaborn==0.12.2
s3fs==2023.6.0

View file

@ -1,10 +1,7 @@
import argparse import argparse
import os import os
import shutil
# from s3pathlib import S3Path
# import boto3
from pathlib import Path from pathlib import Path
from datetime import datetime from datetime import datetime
import pandas as pd import pandas as pd
@ -13,9 +10,10 @@ from core.Logger import logger
from core.Metrics import Metrics, sort_by_metric from core.Metrics import Metrics, sort_by_metric
from core.DataLoader import dataloader_factory from core.DataLoader import dataloader_factory
from core.FeatureProcessor import FeatureProcessor from core.FeatureProcessor import FeatureProcessor
from core.CloudClient import S3FSClient
from core.RegistryHandler import RegistryHandler
from core.Settings import ( from core.Settings import (
MODEL_DIRECTORY, MODEL_DIRECTORY,
BASE_REGISTRY_PATH,
REGISTRY_FILE, REGISTRY_FILE,
MODEL_FOLDER, MODEL_FOLDER,
METRICS_FOLDER, METRICS_FOLDER,
@ -32,13 +30,8 @@ TIMESTAMP = datetime.now().strftime(TIMESTAMP_FORMAT)
RUNTIME_ENVIRONMENT = os.environ.get("RUNTIME_ENVIRONMENT", "local") RUNTIME_ENVIRONMENT = os.environ.get("RUNTIME_ENVIRONMENT", "local")
# STORAGE_OPTIONS = { CLIENT = S3FSClient(runtime_environment=RUNTIME_ENVIRONMENT)
# "key": os.environ.get("AWS_ACCESS_KEY_ID", 'admin'),
# "secret": os.environ.get("AWS_SECRET_ACCESS_KEY", 'password'),
# "client_kwargs": {
# "endpoint_url": os.environ.get("ENDPOINT_URL", "http://localhost:9000")
# }
# }
# FOR TESTING # FOR TESTING
# train_filepath = "./model_build_data/change_data/rdsap_full/train_validation_data.parquet" # train_filepath = "./model_build_data/change_data/rdsap_full/train_validation_data.parquet"
@ -48,22 +41,16 @@ RUNTIME_ENVIRONMENT = os.environ.get("RUNTIME_ENVIRONMENT", "local")
# hyperparameter = HYPERPARAMETERS # hyperparameter = HYPERPARAMETERS
# SUBSAMPLE_FACTOR = 200 # SUBSAMPLE_FACTOR = 200
# SESSION = boto3.Session() # Mock location
# train_filepath = "s3://retrofit-data-dev/model_build_data/change_data/rdsap_full/train_validation_data.parquet"
# test_filepath = "s3://retrofit-data-dev/model_build_data/change_data/rdsap_full/test_data.parquet"
# S3_CLIENT = SESSION.client(
# service_name="s3",
# aws_access_key_id=os.environ.get("AWS_ACCESS_KEY_ID", 'admin'),
# aws_secret_access_key=os.environ.get("AWS_SECRET_ACCESS_KEY", 'password'),
# endpoint_url=os.environ.get("ENDPOINT_URL", "http://localhost:9000")
# )
# S3_CLIENT.create_bucket # To run script in local mode:
# S3_CLIENT.list_buckets() # python3 training.py --train-filepath ./model_build_data/change_data/rdsap_full/train_validation_data.parquet --test-filepath ./model_build_data/change_data/rdsap_full/test_data.parquet
# df = pd.read_parquet( # To run script in local-mock mode:
# "s3://model_build_data/change_data/rdsap_full/train_validation_data.parquet", # python3 training.py --train-filepath s3://retrofit-data-dev/model_build_data/change_data/rdsap_full/train_validation_data.parquet --test-filepath s3://retrofit-data-dev/model_build_data/change_data/rdsap_full/test_data.parquet
# storage_options=STORAGE_OPTIONS
# )
def ingest_arguments() -> argparse.Namespace: def ingest_arguments() -> argparse.Namespace:
@ -166,27 +153,34 @@ def training(
) )
logger.info("--- Save Model ---") logger.info("--- Save Model ---")
model.save_model(output_filepath=model.output_filepath) model.save_model(output_filepath=model.output_filepath, s3fs_client=CLIENT)
logger.info("--- Generate evaluation metrics ---") logger.info("--- Generate evaluation metrics ---")
metrics = Metrics() metrics = Metrics()
metric_output_path = output_base / METRICS_FOLDER
metrics_df = model.model_evaluation( metrics_df = model.model_evaluation(
validation_data=test_df, validation_data=test_df,
target_column=target_column, target_column=target_column,
metrics_location=output_base / METRICS_FOLDER, metrics_location=metric_output_path,
metrics=metrics, metrics=metrics,
) )
metrics.upload_metrics(output_filepath=metric_output_path, s3fs_client=CLIENT)
logger.info("--- Generate metric outputs using predictions ---") logger.info("--- Generate metric outputs using predictions ---")
# metrics.generate_plot_suite() # metrics.generate_plot_suite()
plot_output_path = output_base / METRICS_FOLDER / RESIDUAL_FILE
metrics.generate_residual_plot( metrics.generate_residual_plot(
actuals=test_df[target_column], actuals=test_df[target_column],
predictions=model.predictions, predictions=model.predictions,
target_column=target_column, target_column=target_column,
output_filepath=output_base / METRICS_FOLDER / RESIDUAL_FILE, output_filepath=plot_output_path,
) )
metrics.upload_metrics(output_filepath=plot_output_path, s3fs_client=CLIENT)
# TODO: for cml, we might want to have class that outputs all data and plots to add to the report # TODO: for cml, we might want to have class that outputs all data and plots to add to the report
# If we want residual plot/ any plots, we will need to self host # If we want residual plot/ any plots, we will need to self host
@ -195,35 +189,27 @@ def training(
logger.info("--- Optimising model for deployment ---") logger.info("--- Optimising model for deployment ---")
deployment_model_path = model.optimise_model_for_deployment( deployment_model_path = output_base / DEPLOYMENT_FOLDER
deployment_path=output_base / DEPLOYMENT_FOLDER
) model.optimise_model_for_deployment(deployment_path=deployment_model_path)
logger.info( logger.info(
f"Optimised version of best model can be found at: {deployment_model_path}" f"Optimised version of best model can be found at: {deployment_model_path}"
) )
model.save_model(output_filepath=deployment_model_path, s3fs_client=CLIENT)
# TODO: Need a model registry - for now have this as a CSV # TODO: Need a model registry - for now have this as a CSV
# Save this in the model directory # Save this in the model directory
# Loading registry from s3 # Loading registry from s3
logger.info("--- Append registry with new model ---") logger.info("--- Append registry with new model ---")
registry_path = BASE_REGISTRY_PATH / target_column / REGISTRY_FILE registry_handler = RegistryHandler()
# registry_path = S3Path(MODEL_DIRECTORY, target_column, REGISTRY_FILE).uri
# registry = RegistryHandler(location=registry_path)
if registry_path.exists(): registry_path = Path(MODEL_DIRECTORY) / target_column / REGISTRY_FILE
logger.info("Registry file found - Loading into Dataframe")
registry_df = pd.read_csv(registry_path, index_col=None)
else:
# TODO: Moved columns into settings: MODEL_DETAILS and Metrics class columns
columns = [
BEST_MODEL_COLUMN_NAME,
"model_type",
"model_name",
"model_location",
] + metrics.list_metric_functions()
registry_df = pd.DataFrame(columns=columns) registry_df = registry_handler.load_registry(
registry_path=registry_path, s3fs_client=CLIENT, metrics=metrics
)
model_details_df = pd.DataFrame( model_details_df = pd.DataFrame(
[ [
@ -238,10 +224,6 @@ def training(
registry_row = pd.concat([model_details_df, metrics_df], axis=1) registry_row = pd.concat([model_details_df, metrics_df], axis=1)
registry_df = pd.concat([registry_df, registry_row], axis=0).reset_index(drop=True) registry_df = pd.concat([registry_df, registry_row], axis=0).reset_index(drop=True)
# TODO: will need a rebuild script metric script -i.e. if we add new metrics, we will want to load models and
# regenerate new metrics
# TODO: decide metric to optimise to
registry_df = sort_by_metric( registry_df = sort_by_metric(
registry_df, registry_df,
optimse_metric=OPTIMISE_METRIC, optimse_metric=OPTIMISE_METRIC,
@ -253,6 +235,13 @@ def training(
registry_path.parent.mkdir(parents=True, exist_ok=True) registry_path.parent.mkdir(parents=True, exist_ok=True)
registry_df.to_csv(registry_path, index=False) registry_df.to_csv(registry_path, index=False)
registry_handler.save_registry(output_filepath=registry_path, s3fs_client=CLIENT)
logger.info("--- Clean up ---")
if RUNTIME_ENVIRONMENT != "local" and Path(MODEL_DIRECTORY).exists():
logger.info("Removing local development files not in s3")
shutil.rmtree(Path(MODEL_DIRECTORY))
logger.info("--- Training Pipeline Complete --- ") logger.info("--- Training Pipeline Complete --- ")
@ -263,10 +252,6 @@ if __name__ == "__main__":
logger.info("---Ingest Arguments---") logger.info("---Ingest Arguments---")
args = ingest_arguments() args = ingest_arguments()
# To run script: python3 training.py --train-filepath
# ./model_build_data/change_data/rdsap_full/train_validation_data.parquet --test-filepath
# ./model_build_data/change_data/rdsap_full/test_data.parquet
# TODO: Ingest hyper parameters from somewhere - currently change at the top of script
training( training(
train_filepath=args.train_filepath, train_filepath=args.train_filepath,
test_filepath=args.test_filepath, test_filepath=args.test_filepath,