Source code for autogluon.tabular.models.image_prediction.image_predictor


from typing import Optional
import logging
import os
import pickle

import numpy as np
import pandas as pd

from autogluon.core.constants import BINARY, MULTICLASS, REGRESSION, QUANTILE, SOFTCLASS
from autogluon.core.features.types import R_OBJECT, S_IMAGE_PATH
from autogluon.core.models import AbstractModel
from autogluon.core.utils import get_cpu_count, try_import_autogluon_vision

logger = logging.getLogger(__name__)


# FIXME: Avoid hard-coding 'image' column name
# TODO: Handle multiple image columns?
# TODO: Handle multiple images in a single image column?
# TODO: Add regression support
# TODO: refit_full does not work as expected: It won't use all data, will just split train data internally.
[docs]class ImagePredictorModel(AbstractModel): """Wrapper of autogluon.vision.ImagePredictor""" nn_model_name = 'image_predictor' def __init__(self, **kwargs): super().__init__(**kwargs) self._label_column_name = 'label' # Whether to load inner model when loading. Set to None on init as it is only used during save/load self._load_model = None self._internal_feature_map = None self._dummy_pred_proba = None # Dummy value to predict if image is NaN def _get_default_auxiliary_params(self) -> dict: default_auxiliary_params = super()._get_default_auxiliary_params() extra_auxiliary_params = dict( get_features_kwargs=dict( valid_raw_types=[R_OBJECT], required_special_types=[S_IMAGE_PATH], ), ) default_auxiliary_params.update(extra_auxiliary_params) return default_auxiliary_params @classmethod def _get_default_ag_args(cls) -> dict: default_ag_args = super()._get_default_ag_args() extra_ag_args = { 'valid_stacker': False, 'problem_types': [BINARY, MULTICLASS, REGRESSION], } default_ag_args.update(extra_ag_args) return default_ag_args def _preprocess(self, X, fit=False, **kwargs): X = super()._preprocess(X, **kwargs) if fit: X_features = list(X.columns) if len(X_features) != 1: raise AssertionError(f'ImagePredictorModel only supports one image feature, but {len(X_features)} were given: {X_features}') if X_features[0] != 'image': self._internal_feature_map = {X_features[0]: 'image'} if self._internal_feature_map: X = X.rename(columns=self._internal_feature_map) from autogluon.vision import ImageDataset if isinstance(X, ImageDataset): # Use normal DataFrame, otherwise can crash due to `class` attribute conflicts X = pd.DataFrame(X) return X def _fit(self, X: pd.DataFrame, y: pd.Series, X_val: Optional[pd.DataFrame] = None, y_val: Optional[pd.Series] = None, time_limit: Optional[int] = None, sample_weight=None, verbosity=2, **kwargs): # try_import_mxnet() try_import_autogluon_vision() from autogluon.vision import ImagePredictor params = self._get_model_params() X = self.preprocess(X, fit=True) if X_val is not None: X_val = self.preprocess(X_val) if sample_weight is not None: # TODO: support logger.log(15, "\tsample_weight not yet supported for ImagePredictorModel, this model will ignore them in training.") X = X.reset_index(drop=True) y = y.reset_index(drop=True) if X_val is not None: X_val = X_val.reset_index(drop=True) y_val = y_val.reset_index(drop=True) X[self._label_column_name] = y if X_val is not None: X_val[self._label_column_name] = y_val null_indices = X['image'] == '' # TODO: Consider some kind of weighting of the two options so there isn't a harsh cutoff at 50 # FIXME: What if all rows in a class are null? Will probably crash. if null_indices.sum() > 50: self._dummy_pred_proba = self._compute_dummy_pred_proba(y[null_indices]) # FIXME: Do this one for better results else: # Not enough null to get a confident estimate of null label average, instead use all data average self._dummy_pred_proba = self._compute_dummy_pred_proba(y) if null_indices.sum() > 0: X = X[~null_indices] if X_val is not None: null_indices_val = X_val['image'] == '' if null_indices_val.sum() > 0: X_val = X_val[~null_indices_val] verbosity_image = max(0, verbosity - 1) root_logger = logging.getLogger() root_log_level = root_logger.level # TODO: ImagePredictor doesn't use problem_type in any way at present. # It also doesn't error or warn if problem_type is not one it expects. self.model = ImagePredictor( problem_type=self.problem_type, path=self.path, # eval_metric=self.eval_metric, # TODO: multiclass/binary vision problem works only with accuracy, regression with rmse verbosity=verbosity_image ) logger.log(15, f'\tHyperparameters: {params}') # FIXME: ImagePredictor crashes if given float time_limit if time_limit is not None: time_limit = int(time_limit) self.model.fit(train_data=X, tuning_data=X_val, time_limit=time_limit, hyperparameters=params, random_state=0) # self.model.set_verbosity(verbosity) # TODO: How to set verbosity of fit predictor? root_logger.setLevel(root_log_level) # Reset log level def _predict_proba(self, X, **kwargs): X = self.preprocess(X, **kwargs) # TODO: Add option to crash if null is present for faster predict_proba null_indices = X['image'] == '' if null_indices.sum() > 0: if self.num_classes is None: y_pred_proba = np.zeros(len(X)) else: y_pred_proba = np.zeros((len(X), self.num_classes)) X = X.reset_index(drop=True) null_indices = X['image'] == '' X = X[~null_indices] null_indices_rows = list(null_indices[null_indices].index) non_null_indices_rows = list(null_indices[~null_indices].index) y_pred_proba[null_indices_rows] = self._dummy_pred_proba y_pred_proba[non_null_indices_rows] = self.model.predict_proba(X, as_pandas=False) else: y_pred_proba = self.model.predict_proba(X, as_pandas=False) return self._convert_proba_to_unified_form(y_pred_proba) # TODO: Consider moving to AbstractModel or as a separate function # TODO: Test softclass def _compute_dummy_pred_proba(self, y): num_classes = self.num_classes if self.problem_type in [BINARY, MULTICLASS]: dummies = pd.get_dummies(y) dummy_columns = set(list(dummies.columns)) if len(dummies.columns) < num_classes: for c in range(num_classes): if c not in dummy_columns: dummies[c] = 0 dummies = dummies[list(range(num_classes))] pred_proba_mean = dummies.mean().values elif self.problem_type in [REGRESSION, QUANTILE, SOFTCLASS]: pred_proba_mean = y.mean() else: raise NotImplementedError(f'Computing dummy pred_proba is not implemented for {self.problem_type}.') return pred_proba_mean def _get_default_searchspace(self): try_import_autogluon_vision() from autogluon.vision.configs import presets_configs searchspace = presets_configs.preset_image_predictor['good_quality_fast_inference']['hyperparameters'] return searchspace def save(self, path: str = None, verbose=True) -> str: self._load_model = self.model is not None __model = self.model self.model = None # save this AbstractModel object without NN weights path = super().save(path=path, verbose=verbose) self.model = __model if self._load_model: image_nn_path = os.path.join(path, self.nn_model_name) self.model.save(image_nn_path) logger.log(15, f"\tSaved Image NN weights and model hyperparameters to '{image_nn_path}'.") self._load_model = None return path @classmethod def load(cls, path: str, reset_paths=True, verbose=True): model = super().load(path=path, reset_paths=reset_paths, verbose=verbose) if model._load_model: try_import_autogluon_vision() from autogluon.vision import ImagePredictor model.model = ImagePredictor.load(os.path.join(path, cls.nn_model_name)) model._load_model = None return model def get_memory_size(self) -> int: """Return the memory size by calculating the total number of parameters. Returns ------- memory_size The total memory size in bytes. """ return len(pickle.dumps(self.model._classifier, pickle.HIGHEST_PROTOCOL)) def _get_default_resources(self): num_cpus = get_cpu_count() try_import_autogluon_vision() from autogluon.vision import ImagePredictor num_gpus = ImagePredictor._get_num_gpus_available() return num_cpus, num_gpus